Files
2026-08-09 05:33:21 +08:00

337 lines
8.3 KiB
Go

package device
import (
"context"
"encoding/hex"
"fmt"
"regexp"
"strconv"
"strings"
"time"
"vocat/internal/modem"
)
var phonePattern = regexp.MustCompile(`\+?[0-9][0-9 ()-]{3,}[0-9]`)
func (manager *Manager) readPhoneNumber(
ctx context.Context,
client modem.Client,
) (PhoneNumber, []string) {
var warnings []string
if response, err := manager.command(ctx, client, "AT+CNUM"); err == nil {
if number := parsePhoneResponse(response, "+CNUM:"); number != "" {
return PhoneNumber{
Number: number,
Source: PhoneSourceCNUM,
Status: "号码来自模块/SIM 的 CNUM 记录",
}, warnings
}
} else {
warnings = append(warnings, "read CNUM: "+err.Error())
}
phonebook, phonebookWarnings := manager.readOwnNumberPhonebook(ctx, client)
warnings = append(warnings, phonebookWarnings...)
if phonebook != "" {
return PhoneNumber{
Number: phonebook,
Source: PhoneSourceOwnNumber,
Status: "号码来自 SIM Own Numbers 电话簿",
}, warnings
}
raw, rawWarnings := manager.readEFMSISDN(ctx, client)
warnings = append(warnings, rawWarnings...)
if raw != "" {
return PhoneNumber{
Number: raw,
Source: PhoneSourceEFMSISDN,
Status: "号码来自 USIM EF_MSISDN 只读记录",
}, warnings
}
return PhoneNumber{
Status: "CNUM、Own Numbers 与 EF_MSISDN 均为空;号码不能由 IMSI/ICCID 推导,需要运营商或 IMS/VoWiFi 注册侧提供",
}, warnings
}
func (manager *Manager) readOwnNumberPhonebook(
ctx context.Context,
client modem.Client,
) (number string, warnings []string) {
previous := ""
if response, err := manager.command(ctx, client, "AT+CPBS?"); err == nil {
previous = parseSelectedPhonebook(response)
} else {
warnings = append(warnings, "query current phonebook: "+err.Error())
}
response, err := manager.command(ctx, client, `AT+CPBS="ON"`)
if err != nil || !response.OK() {
if err != nil {
warnings = append(warnings, "select Own Numbers phonebook: "+err.Error())
}
return "", warnings
}
if previous != "" && previous != "ON" {
defer func() {
restoreCtx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
defer cancel()
if _, restoreErr := manager.command(
restoreCtx,
client,
fmt.Sprintf(`AT+CPBS="%s"`, previous),
); restoreErr != nil {
warnings = append(warnings, "restore phonebook: "+restoreErr.Error())
}
}()
}
start, end := 1, 2
if response, rangeErr := manager.command(ctx, client, "AT+CPBR=?"); rangeErr == nil {
if parsedStart, parsedEnd, ok := parsePhonebookRange(response); ok {
start, end = parsedStart, parsedEnd
}
} else {
warnings = append(warnings, "query Own Numbers range: "+rangeErr.Error())
}
if end > start+9 {
end = start + 9
}
for index := start; index <= end; index++ {
response, readErr := manager.command(ctx, client, fmt.Sprintf("AT+CPBR=%d", index))
if readErr != nil {
continue
}
if number := parsePhoneResponse(response, "+CPBR:"); number != "" {
return number, warnings
}
}
return "", warnings
}
func parseSelectedPhonebook(response modem.Response) string {
value := valueAfterPrefix(response, "+CPBS:")
values := csvValues(value)
if len(values) == 0 {
return ""
}
selected := strings.ToUpper(strings.Trim(values[0], `"`))
if len(selected) < 1 || len(selected) > 4 {
return ""
}
for _, character := range selected {
if character < 'A' || character > 'Z' {
return ""
}
}
return selected
}
func parsePhonebookRange(response modem.Response) (int, int, bool) {
pattern := regexp.MustCompile(`\((\d+)-(\d+)\)`)
for _, line := range response.Lines {
match := pattern.FindStringSubmatch(line)
if len(match) != 3 {
continue
}
start, startErr := strconv.Atoi(match[1])
end, endErr := strconv.Atoi(match[2])
if startErr == nil && endErr == nil && start > 0 && end >= start {
return start, end, true
}
}
return 0, 0, false
}
func parsePhoneResponse(response modem.Response, prefix string) string {
for _, line := range response.Lines {
if !strings.HasPrefix(strings.ToUpper(strings.TrimSpace(line)), strings.ToUpper(prefix)) {
continue
}
for _, match := range phonePattern.FindAllString(line, -1) {
if number := normalizePhoneNumber(match); number != "" {
return number
}
}
}
return ""
}
func normalizePhoneNumber(value string) string {
var result strings.Builder
for _, character := range strings.TrimSpace(value) {
switch {
case character >= '0' && character <= '9':
result.WriteRune(character)
case character == '+' && result.Len() == 0:
result.WriteRune(character)
case character == ' ' || character == '-' || character == '(' || character == ')':
default:
return ""
}
}
number := result.String()
digits := strings.TrimPrefix(number, "+")
if len(digits) < 5 || len(digits) > 20 {
return ""
}
return number
}
type crsmPath struct {
value string
}
func (manager *Manager) readEFMSISDN(
ctx context.Context,
client modem.Client,
) (string, []string) {
var warnings []string
for _, path := range []crsmPath{{}, {"3F007FFF"}, {"3F007F10"}} {
recordLength, recordCount, ok := 0, 0, false
for _, responseLength := range []int{0, 15} {
status, err := manager.command(
ctx,
client,
crsmCommand(192, 0, responseLength, path.value),
)
if err != nil {
warnings = append(
warnings,
fmt.Sprintf(
"read EF_MSISDN metadata (path %q, length %d): %v",
path.value,
responseLength,
err,
),
)
continue
}
recordLength, recordCount, ok = msisdnRecordShape(status)
if ok {
break
}
}
if !ok {
continue
}
if recordCount > 10 {
recordCount = 10
}
for index := 1; index <= recordCount; index++ {
record, readErr := manager.command(
ctx,
client,
crsmCommand(178, index, recordLength, path.value),
)
if readErr != nil {
warnings = append(
warnings,
fmt.Sprintf(
"read EF_MSISDN record %d (path %q): %v",
index,
path.value,
readErr,
),
)
continue
}
if number := decodeMSISDNRecord(record); number != "" {
return number, warnings
}
}
}
return "", warnings
}
func crsmCommand(operation, index, length int, path string) string {
p2 := 0
if operation == 178 {
p2 = 4
}
command := fmt.Sprintf("AT+CRSM=%d,28480,%d,%d,%d", operation, index, p2, length)
if path != "" {
command += fmt.Sprintf(`,"","%s"`, path)
}
return command
}
func crsmPayload(response modem.Response) []byte {
value := valueAfterPrefix(response, "+CRSM:")
values := csvValues(value)
if len(values) < 3 {
return nil
}
sw1, sw1Err := strconv.Atoi(values[0])
sw2, sw2Err := strconv.Atoi(values[1])
if sw1Err != nil || sw2Err != nil ||
!((sw1 == 144 && sw2 == 0) || sw1 == 145) {
return nil
}
payload := strings.Trim(values[2], `" `)
decoded, err := hex.DecodeString(payload)
if err != nil {
return nil
}
return decoded
}
func msisdnRecordShape(response modem.Response) (recordLength, recordCount int, ok bool) {
payload := crsmPayload(response)
for index := 0; index+1 < len(payload); index++ {
tag := payload[index]
length := int(payload[index+1])
start := index + 2
end := start + length
if end > len(payload) {
continue
}
if tag == 0x82 && length >= 5 {
recordLength = int(payload[end-3])<<8 | int(payload[end-2])
recordCount = int(payload[end-1])
if recordLength >= 14 && recordLength <= 255 && recordCount > 0 {
return recordLength, recordCount, true
}
}
}
if len(payload) >= 15 &&
payload[4] == 0x6f && payload[5] == 0x40 &&
payload[6] == 0x04 &&
(payload[13] == 0x01 || payload[13] == 0x03) {
fileSize := int(payload[2])<<8 | int(payload[3])
recordLength = int(payload[14])
if recordLength >= 14 && fileSize >= recordLength {
return recordLength, fileSize / recordLength, true
}
}
return 0, 0, false
}
func decodeMSISDNRecord(response modem.Response) string {
payload := crsmPayload(response)
if len(payload) < 14 {
return ""
}
footer := payload[len(payload)-14:]
storedLength := int(footer[0])
if storedLength < 2 || storedLength == 0xff {
return ""
}
bcdLength := storedLength - 1
if bcdLength > 10 {
bcdLength = 10
}
var result strings.Builder
if footer[1]&0x70 == 0x10 {
result.WriteByte('+')
}
for _, value := range footer[2 : 2+bcdLength] {
for _, digit := range []byte{value & 0x0f, value >> 4} {
if digit <= 9 {
result.WriteByte('0' + digit)
}
}
}
return normalizePhoneNumber(result.String())
}