phone.go 8.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336
  1. package device
  2. import (
  3. "context"
  4. "encoding/hex"
  5. "fmt"
  6. "regexp"
  7. "strconv"
  8. "strings"
  9. "time"
  10. "vocat/internal/modem"
  11. )
  12. var phonePattern = regexp.MustCompile(`\+?[0-9][0-9 ()-]{3,}[0-9]`)
  13. func (manager *Manager) readPhoneNumber(
  14. ctx context.Context,
  15. client modem.Client,
  16. ) (PhoneNumber, []string) {
  17. var warnings []string
  18. if response, err := manager.command(ctx, client, "AT+CNUM"); err == nil {
  19. if number := parsePhoneResponse(response, "+CNUM:"); number != "" {
  20. return PhoneNumber{
  21. Number: number,
  22. Source: PhoneSourceCNUM,
  23. Status: "号码来自模块/SIM 的 CNUM 记录",
  24. }, warnings
  25. }
  26. } else {
  27. warnings = append(warnings, "read CNUM: "+err.Error())
  28. }
  29. phonebook, phonebookWarnings := manager.readOwnNumberPhonebook(ctx, client)
  30. warnings = append(warnings, phonebookWarnings...)
  31. if phonebook != "" {
  32. return PhoneNumber{
  33. Number: phonebook,
  34. Source: PhoneSourceOwnNumber,
  35. Status: "号码来自 SIM Own Numbers 电话簿",
  36. }, warnings
  37. }
  38. raw, rawWarnings := manager.readEFMSISDN(ctx, client)
  39. warnings = append(warnings, rawWarnings...)
  40. if raw != "" {
  41. return PhoneNumber{
  42. Number: raw,
  43. Source: PhoneSourceEFMSISDN,
  44. Status: "号码来自 USIM EF_MSISDN 只读记录",
  45. }, warnings
  46. }
  47. return PhoneNumber{
  48. Status: "CNUM、Own Numbers 与 EF_MSISDN 均为空;号码不能由 IMSI/ICCID 推导,需要运营商或 IMS/VoWiFi 注册侧提供",
  49. }, warnings
  50. }
  51. func (manager *Manager) readOwnNumberPhonebook(
  52. ctx context.Context,
  53. client modem.Client,
  54. ) (number string, warnings []string) {
  55. previous := ""
  56. if response, err := manager.command(ctx, client, "AT+CPBS?"); err == nil {
  57. previous = parseSelectedPhonebook(response)
  58. } else {
  59. warnings = append(warnings, "query current phonebook: "+err.Error())
  60. }
  61. response, err := manager.command(ctx, client, `AT+CPBS="ON"`)
  62. if err != nil || !response.OK() {
  63. if err != nil {
  64. warnings = append(warnings, "select Own Numbers phonebook: "+err.Error())
  65. }
  66. return "", warnings
  67. }
  68. if previous != "" && previous != "ON" {
  69. defer func() {
  70. restoreCtx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
  71. defer cancel()
  72. if _, restoreErr := manager.command(
  73. restoreCtx,
  74. client,
  75. fmt.Sprintf(`AT+CPBS="%s"`, previous),
  76. ); restoreErr != nil {
  77. warnings = append(warnings, "restore phonebook: "+restoreErr.Error())
  78. }
  79. }()
  80. }
  81. start, end := 1, 2
  82. if response, rangeErr := manager.command(ctx, client, "AT+CPBR=?"); rangeErr == nil {
  83. if parsedStart, parsedEnd, ok := parsePhonebookRange(response); ok {
  84. start, end = parsedStart, parsedEnd
  85. }
  86. } else {
  87. warnings = append(warnings, "query Own Numbers range: "+rangeErr.Error())
  88. }
  89. if end > start+9 {
  90. end = start + 9
  91. }
  92. for index := start; index <= end; index++ {
  93. response, readErr := manager.command(ctx, client, fmt.Sprintf("AT+CPBR=%d", index))
  94. if readErr != nil {
  95. continue
  96. }
  97. if number := parsePhoneResponse(response, "+CPBR:"); number != "" {
  98. return number, warnings
  99. }
  100. }
  101. return "", warnings
  102. }
  103. func parseSelectedPhonebook(response modem.Response) string {
  104. value := valueAfterPrefix(response, "+CPBS:")
  105. values := csvValues(value)
  106. if len(values) == 0 {
  107. return ""
  108. }
  109. selected := strings.ToUpper(strings.Trim(values[0], `"`))
  110. if len(selected) < 1 || len(selected) > 4 {
  111. return ""
  112. }
  113. for _, character := range selected {
  114. if character < 'A' || character > 'Z' {
  115. return ""
  116. }
  117. }
  118. return selected
  119. }
  120. func parsePhonebookRange(response modem.Response) (int, int, bool) {
  121. pattern := regexp.MustCompile(`\((\d+)-(\d+)\)`)
  122. for _, line := range response.Lines {
  123. match := pattern.FindStringSubmatch(line)
  124. if len(match) != 3 {
  125. continue
  126. }
  127. start, startErr := strconv.Atoi(match[1])
  128. end, endErr := strconv.Atoi(match[2])
  129. if startErr == nil && endErr == nil && start > 0 && end >= start {
  130. return start, end, true
  131. }
  132. }
  133. return 0, 0, false
  134. }
  135. func parsePhoneResponse(response modem.Response, prefix string) string {
  136. for _, line := range response.Lines {
  137. if !strings.HasPrefix(strings.ToUpper(strings.TrimSpace(line)), strings.ToUpper(prefix)) {
  138. continue
  139. }
  140. for _, match := range phonePattern.FindAllString(line, -1) {
  141. if number := normalizePhoneNumber(match); number != "" {
  142. return number
  143. }
  144. }
  145. }
  146. return ""
  147. }
  148. func normalizePhoneNumber(value string) string {
  149. var result strings.Builder
  150. for _, character := range strings.TrimSpace(value) {
  151. switch {
  152. case character >= '0' && character <= '9':
  153. result.WriteRune(character)
  154. case character == '+' && result.Len() == 0:
  155. result.WriteRune(character)
  156. case character == ' ' || character == '-' || character == '(' || character == ')':
  157. default:
  158. return ""
  159. }
  160. }
  161. number := result.String()
  162. digits := strings.TrimPrefix(number, "+")
  163. if len(digits) < 5 || len(digits) > 20 {
  164. return ""
  165. }
  166. return number
  167. }
  168. type crsmPath struct {
  169. value string
  170. }
  171. func (manager *Manager) readEFMSISDN(
  172. ctx context.Context,
  173. client modem.Client,
  174. ) (string, []string) {
  175. var warnings []string
  176. for _, path := range []crsmPath{{}, {"3F007FFF"}, {"3F007F10"}} {
  177. recordLength, recordCount, ok := 0, 0, false
  178. for _, responseLength := range []int{0, 15} {
  179. status, err := manager.command(
  180. ctx,
  181. client,
  182. crsmCommand(192, 0, responseLength, path.value),
  183. )
  184. if err != nil {
  185. warnings = append(
  186. warnings,
  187. fmt.Sprintf(
  188. "read EF_MSISDN metadata (path %q, length %d): %v",
  189. path.value,
  190. responseLength,
  191. err,
  192. ),
  193. )
  194. continue
  195. }
  196. recordLength, recordCount, ok = msisdnRecordShape(status)
  197. if ok {
  198. break
  199. }
  200. }
  201. if !ok {
  202. continue
  203. }
  204. if recordCount > 10 {
  205. recordCount = 10
  206. }
  207. for index := 1; index <= recordCount; index++ {
  208. record, readErr := manager.command(
  209. ctx,
  210. client,
  211. crsmCommand(178, index, recordLength, path.value),
  212. )
  213. if readErr != nil {
  214. warnings = append(
  215. warnings,
  216. fmt.Sprintf(
  217. "read EF_MSISDN record %d (path %q): %v",
  218. index,
  219. path.value,
  220. readErr,
  221. ),
  222. )
  223. continue
  224. }
  225. if number := decodeMSISDNRecord(record); number != "" {
  226. return number, warnings
  227. }
  228. }
  229. }
  230. return "", warnings
  231. }
  232. func crsmCommand(operation, index, length int, path string) string {
  233. p2 := 0
  234. if operation == 178 {
  235. p2 = 4
  236. }
  237. command := fmt.Sprintf("AT+CRSM=%d,28480,%d,%d,%d", operation, index, p2, length)
  238. if path != "" {
  239. command += fmt.Sprintf(`,"","%s"`, path)
  240. }
  241. return command
  242. }
  243. func crsmPayload(response modem.Response) []byte {
  244. value := valueAfterPrefix(response, "+CRSM:")
  245. values := csvValues(value)
  246. if len(values) < 3 {
  247. return nil
  248. }
  249. sw1, sw1Err := strconv.Atoi(values[0])
  250. sw2, sw2Err := strconv.Atoi(values[1])
  251. if sw1Err != nil || sw2Err != nil ||
  252. !((sw1 == 144 && sw2 == 0) || sw1 == 145) {
  253. return nil
  254. }
  255. payload := strings.Trim(values[2], `" `)
  256. decoded, err := hex.DecodeString(payload)
  257. if err != nil {
  258. return nil
  259. }
  260. return decoded
  261. }
  262. func msisdnRecordShape(response modem.Response) (recordLength, recordCount int, ok bool) {
  263. payload := crsmPayload(response)
  264. for index := 0; index+1 < len(payload); index++ {
  265. tag := payload[index]
  266. length := int(payload[index+1])
  267. start := index + 2
  268. end := start + length
  269. if end > len(payload) {
  270. continue
  271. }
  272. if tag == 0x82 && length >= 5 {
  273. recordLength = int(payload[end-3])<<8 | int(payload[end-2])
  274. recordCount = int(payload[end-1])
  275. if recordLength >= 14 && recordLength <= 255 && recordCount > 0 {
  276. return recordLength, recordCount, true
  277. }
  278. }
  279. }
  280. if len(payload) >= 15 &&
  281. payload[4] == 0x6f && payload[5] == 0x40 &&
  282. payload[6] == 0x04 &&
  283. (payload[13] == 0x01 || payload[13] == 0x03) {
  284. fileSize := int(payload[2])<<8 | int(payload[3])
  285. recordLength = int(payload[14])
  286. if recordLength >= 14 && fileSize >= recordLength {
  287. return recordLength, fileSize / recordLength, true
  288. }
  289. }
  290. return 0, 0, false
  291. }
  292. func decodeMSISDNRecord(response modem.Response) string {
  293. payload := crsmPayload(response)
  294. if len(payload) < 14 {
  295. return ""
  296. }
  297. footer := payload[len(payload)-14:]
  298. storedLength := int(footer[0])
  299. if storedLength < 2 || storedLength == 0xff {
  300. return ""
  301. }
  302. bcdLength := storedLength - 1
  303. if bcdLength > 10 {
  304. bcdLength = 10
  305. }
  306. var result strings.Builder
  307. if footer[1]&0x70 == 0x10 {
  308. result.WriteByte('+')
  309. }
  310. for _, value := range footer[2 : 2+bcdLength] {
  311. for _, digit := range []byte{value & 0x0f, value >> 4} {
  312. if digit <= 9 {
  313. result.WriteByte('0' + digit)
  314. }
  315. }
  316. }
  317. return normalizePhoneNumber(result.String())
  318. }