scan.go 3.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131
  1. package device
  2. import (
  3. "context"
  4. "strings"
  5. "vocat/internal/modem"
  6. )
  7. // ScannedOperator is one network reported by an operator scan (AT+COPS=?).
  8. type ScannedOperator struct {
  9. // Status is "current", "available", "forbidden", or "unknown".
  10. Status string `json:"status"`
  11. Name string `json:"name"`
  12. Short string `json:"shortName,omitempty"`
  13. Numeric string `json:"numeric"`
  14. Act string `json:"act,omitempty"`
  15. }
  16. // OperatorScanResult is the outcome of a full operator scan.
  17. type OperatorScanResult struct {
  18. Status string `json:"status"` // "complete" or "failed"
  19. Operators []ScannedOperator `json:"operators"`
  20. }
  21. // ScanOperators runs AT+COPS=? to list the networks the modem can currently
  22. // see. The command is slow (tens of seconds, up to the modem's documented
  23. // ceiling), so it uses the manager's scan timeout rather than the normal
  24. // command timeout. It is abortable through the caller's context.
  25. func (manager *Manager) ScanOperators(
  26. ctx context.Context,
  27. id string,
  28. ) (OperatorScanResult, error) {
  29. state, err := manager.lookup(id)
  30. if err != nil {
  31. return OperatorScanResult{}, err
  32. }
  33. state.opMu.Lock()
  34. defer state.opMu.Unlock()
  35. if err := manager.validateActive(id, state); err != nil {
  36. return OperatorScanResult{}, err
  37. }
  38. client, err := manager.clientLocked(ctx, state, manager.candidateFor(state))
  39. if err != nil {
  40. manager.setResult(id, state, nil, err)
  41. return OperatorScanResult{}, err
  42. }
  43. scanContext, cancel := manager.withTimeout(ctx, manager.scanTimeout)
  44. defer cancel()
  45. response, err := client.Execute(scanContext, "AT+COPS=?")
  46. if err != nil {
  47. manager.setResult(id, state, nil, err)
  48. return OperatorScanResult{Status: "failed", Operators: []ScannedOperator{}}, err
  49. }
  50. result := OperatorScanResult{
  51. Status: "complete",
  52. Operators: parseOperatorScan(response),
  53. }
  54. manager.setResult(id, state, nil, nil)
  55. return result, nil
  56. }
  57. // parseOperatorScan parses the +COPS: list returned by AT+COPS=?. Each entry is
  58. // a parenthesised tuple (stat,"long","short","numeric"[,act]).
  59. func parseOperatorScan(response modem.Response) []ScannedOperator {
  60. operators := make([]ScannedOperator, 0)
  61. for _, line := range response.Lines {
  62. trimmed := strings.TrimSpace(line)
  63. if !strings.HasPrefix(strings.ToUpper(trimmed), "+COPS:") {
  64. continue
  65. }
  66. payload := strings.TrimSpace(trimmed[len("+COPS:"):])
  67. for _, tuple := range extractScanTuples(payload) {
  68. fields := csvValues(tuple)
  69. if len(fields) < 4 {
  70. continue
  71. }
  72. operator := ScannedOperator{
  73. Status: operatorScanStatus(fields[0]),
  74. Name: fields[1],
  75. Short: fields[2],
  76. Numeric: fields[3],
  77. }
  78. if len(fields) >= 5 {
  79. operator.Act = accessTechnology(fields[4])
  80. }
  81. operators = append(operators, operator)
  82. }
  83. }
  84. return operators
  85. }
  86. // extractScanTuples returns the contents of each top-level parenthesised group,
  87. // ignoring parentheses inside quoted strings.
  88. func extractScanTuples(payload string) []string {
  89. tuples := make([]string, 0)
  90. depth := 0
  91. start := -1
  92. inQuote := false
  93. for index, r := range payload {
  94. switch {
  95. case r == '"':
  96. inQuote = !inQuote
  97. case r == '(' && !inQuote:
  98. if depth == 0 {
  99. start = index + 1
  100. }
  101. depth++
  102. case r == ')' && !inQuote:
  103. depth--
  104. if depth == 0 && start >= 0 {
  105. tuples = append(tuples, payload[start:index])
  106. start = -1
  107. }
  108. }
  109. }
  110. return tuples
  111. }
  112. func operatorScanStatus(code string) string {
  113. switch strings.TrimSpace(code) {
  114. case "1":
  115. return "available"
  116. case "2":
  117. return "current"
  118. case "3":
  119. return "forbidden"
  120. default:
  121. return "unknown"
  122. }
  123. }