mirror of
https://github.com/MengMengCode/VoCat.git
synced 2026-08-15 04:13:42 +08:00
337 lines
8.3 KiB
Go
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())
|
|
}
|