| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131 |
- package device
- import (
- "context"
- "strings"
- "vocat/internal/modem"
- )
- // ScannedOperator is one network reported by an operator scan (AT+COPS=?).
- type ScannedOperator struct {
- // Status is "current", "available", "forbidden", or "unknown".
- Status string `json:"status"`
- Name string `json:"name"`
- Short string `json:"shortName,omitempty"`
- Numeric string `json:"numeric"`
- Act string `json:"act,omitempty"`
- }
- // OperatorScanResult is the outcome of a full operator scan.
- type OperatorScanResult struct {
- Status string `json:"status"` // "complete" or "failed"
- Operators []ScannedOperator `json:"operators"`
- }
- // ScanOperators runs AT+COPS=? to list the networks the modem can currently
- // see. The command is slow (tens of seconds, up to the modem's documented
- // ceiling), so it uses the manager's scan timeout rather than the normal
- // command timeout. It is abortable through the caller's context.
- func (manager *Manager) ScanOperators(
- ctx context.Context,
- id string,
- ) (OperatorScanResult, error) {
- state, err := manager.lookup(id)
- if err != nil {
- return OperatorScanResult{}, err
- }
- state.opMu.Lock()
- defer state.opMu.Unlock()
- if err := manager.validateActive(id, state); err != nil {
- return OperatorScanResult{}, err
- }
- client, err := manager.clientLocked(ctx, state, manager.candidateFor(state))
- if err != nil {
- manager.setResult(id, state, nil, err)
- return OperatorScanResult{}, err
- }
- scanContext, cancel := manager.withTimeout(ctx, manager.scanTimeout)
- defer cancel()
- response, err := client.Execute(scanContext, "AT+COPS=?")
- if err != nil {
- manager.setResult(id, state, nil, err)
- return OperatorScanResult{Status: "failed", Operators: []ScannedOperator{}}, err
- }
- result := OperatorScanResult{
- Status: "complete",
- Operators: parseOperatorScan(response),
- }
- manager.setResult(id, state, nil, nil)
- return result, nil
- }
- // parseOperatorScan parses the +COPS: list returned by AT+COPS=?. Each entry is
- // a parenthesised tuple (stat,"long","short","numeric"[,act]).
- func parseOperatorScan(response modem.Response) []ScannedOperator {
- operators := make([]ScannedOperator, 0)
- for _, line := range response.Lines {
- trimmed := strings.TrimSpace(line)
- if !strings.HasPrefix(strings.ToUpper(trimmed), "+COPS:") {
- continue
- }
- payload := strings.TrimSpace(trimmed[len("+COPS:"):])
- for _, tuple := range extractScanTuples(payload) {
- fields := csvValues(tuple)
- if len(fields) < 4 {
- continue
- }
- operator := ScannedOperator{
- Status: operatorScanStatus(fields[0]),
- Name: fields[1],
- Short: fields[2],
- Numeric: fields[3],
- }
- if len(fields) >= 5 {
- operator.Act = accessTechnology(fields[4])
- }
- operators = append(operators, operator)
- }
- }
- return operators
- }
- // extractScanTuples returns the contents of each top-level parenthesised group,
- // ignoring parentheses inside quoted strings.
- func extractScanTuples(payload string) []string {
- tuples := make([]string, 0)
- depth := 0
- start := -1
- inQuote := false
- for index, r := range payload {
- switch {
- case r == '"':
- inQuote = !inQuote
- case r == '(' && !inQuote:
- if depth == 0 {
- start = index + 1
- }
- depth++
- case r == ')' && !inQuote:
- depth--
- if depth == 0 && start >= 0 {
- tuples = append(tuples, payload[start:index])
- start = -1
- }
- }
- }
- return tuples
- }
- func operatorScanStatus(code string) string {
- switch strings.TrimSpace(code) {
- case "1":
- return "available"
- case "2":
- return "current"
- case "3":
- return "forbidden"
- default:
- return "unknown"
- }
- }
|