| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292 |
- package device
- import (
- "context"
- "errors"
- "fmt"
- "regexp"
- "strconv"
- "strings"
- "vocat/internal/modem"
- )
- var apnPattern = regexp.MustCompile(`^[A-Za-z0-9](?:[A-Za-z0-9._-]{0,98}[A-Za-z0-9])?$`)
- func (manager *Manager) SetNetwork(
- ctx context.Context,
- id string,
- request NetworkRequest,
- ) (NetworkResult, error) {
- state, err := manager.lookup(id)
- if err != nil {
- return NetworkResult{}, err
- }
- apn := strings.TrimSpace(request.APN)
- if request.Enabled && !apnPattern.MatchString(apn) {
- return NetworkResult{}, ErrInvalidNetworkAPN
- }
- ipVersion := normalizeIPVersion(request.IPVersion)
- if ipVersion == "" {
- return NetworkResult{}, errors.New("IP version must be IP, IPV6, or IPV4V6")
- }
- state.opMu.Lock()
- defer state.opMu.Unlock()
- if err := manager.validateActive(id, state); err != nil {
- return NetworkResult{}, err
- }
- if request.Enabled {
- if err := manager.regionBlockError(state); err != nil {
- manager.setResult(id, state, nil, err)
- return NetworkResult{}, err
- }
- }
- candidate := manager.candidateFor(state)
- if candidate.QMIControl != "" && candidate.NetworkInterface != "" {
- return setQMINetwork(ctx, candidate, request.Enabled, apn, ipVersion)
- }
- client, err := manager.clientLocked(ctx, state, candidate)
- if err != nil {
- manager.setResult(id, state, nil, err)
- return NetworkResult{}, err
- }
- if request.Enabled {
- commands := []string{
- fmt.Sprintf(`AT+CGDCONT=1,"%s","%s"`, ipVersion, apn),
- "AT+CGATT=1",
- "AT+CGACT=1,1",
- }
- for _, command := range commands {
- if _, err := manager.command(ctx, client, command); err != nil {
- manager.setResult(id, state, nil, err)
- return NetworkResult{}, err
- }
- }
- } else {
- if _, err := manager.command(ctx, client, "AT+CGACT=0,1"); err != nil {
- manager.setResult(id, state, nil, err)
- return NetworkResult{}, err
- }
- }
- manager.setResult(id, state, nil, nil)
- return NetworkResult{
- Enabled: request.Enabled,
- Backend: "at",
- Interface: candidate.NetworkInterface,
- APN: apn,
- IPVersion: ipVersion,
- Detail: map[bool]string{true: "PDP context activated", false: "PDP context deactivated"}[request.Enabled],
- }, nil
- }
- func normalizeIPVersion(value string) string {
- switch strings.ToUpper(strings.TrimSpace(value)) {
- case "", "IP", "IPV4":
- return "IP"
- case "IPV6":
- return "IPV6"
- case "IPV4V6", "IPV6V4":
- return "IPV4V6"
- default:
- return ""
- }
- }
- func (manager *Manager) USBNetMode(ctx context.Context, id string) (USBNetMode, error) {
- response, err := manager.ExecuteAT(ctx, id, `AT+QCFG="usbnet"`)
- if err != nil {
- return USBNetMode{}, err
- }
- for _, line := range response.Lines {
- upper := strings.ToUpper(strings.TrimSpace(line))
- if !strings.HasPrefix(upper, `+QCFG: "USBNET",`) {
- continue
- }
- value := strings.TrimSpace(strings.TrimPrefix(upper, `+QCFG: "USBNET",`))
- mode, parseErr := strconv.Atoi(value)
- if parseErr == nil {
- return USBNetMode{Mode: mode, Name: usbNetModeName(mode)}, nil
- }
- }
- return USBNetMode{}, errors.New("modem did not return a valid USB network mode")
- }
- func (manager *Manager) SetUSBNetMode(ctx context.Context, id string, mode int) (USBNetMode, error) {
- if mode < 0 || mode > 3 {
- return USBNetMode{}, errors.New("USB network mode must be between 0 and 3")
- }
- response, err := manager.ExecuteSensitiveAT(ctx, id, fmt.Sprintf(`AT+QCFG="usbnet",%d`, mode))
- if err != nil {
- return USBNetMode{}, err
- }
- if !response.OK() {
- return USBNetMode{}, &modem.CommandError{Command: response.Command, Final: response.Final, Lines: response.Lines}
- }
- return USBNetMode{Mode: mode, Name: usbNetModeName(mode)}, nil
- }
- // SetUSBNetModeByPort sets the USB network mode on a device that has only been
- // discovered (not yet taken over), addressed by its AT port path. The port must
- // belong to a currently discovered candidate, so the endpoint cannot be used to
- // open arbitrary host paths.
- func (manager *Manager) SetUSBNetModeByPort(
- ctx context.Context,
- atPortPath string,
- mode int,
- ) (USBNetMode, error) {
- if mode < 0 || mode > 3 {
- return USBNetMode{}, errors.New("USB network mode must be between 0 and 3")
- }
- atPortPath = strings.TrimSpace(atPortPath)
- if atPortPath == "" {
- return USBNetMode{}, errors.New("an AT port path is required")
- }
- manager.mu.RLock()
- var candidate modem.Candidate
- found := false
- for _, state := range manager.devices {
- if state.discovered &&
- (state.candidate.ATPort.OpenPath() == atPortPath || state.candidate.ATPort.Path == atPortPath) {
- candidate = copyCandidate(state.candidate)
- found = true
- break
- }
- }
- manager.mu.RUnlock()
- if !found {
- return USBNetMode{}, fmt.Errorf("no discovered device owns AT port %q", atPortPath)
- }
- client, err := manager.opener.Open(ctx, candidate.ATPort)
- if err != nil {
- return USBNetMode{}, err
- }
- defer func() { _ = client.Close() }()
- commandCtx, cancel := manager.withTimeout(ctx, manager.commandTimeout)
- defer cancel()
- response, err := client.Execute(commandCtx, fmt.Sprintf(`AT+QCFG="usbnet",%d`, mode))
- if err != nil {
- return USBNetMode{}, err
- }
- if !response.OK() {
- return USBNetMode{}, &modem.CommandError{Command: response.Command, Final: response.Final, Lines: response.Lines}
- }
- return USBNetMode{Mode: mode, Name: usbNetModeName(mode)}, nil
- }
- func usbNetModeName(mode int) string {
- switch mode {
- case 0:
- return "QMI"
- case 1:
- return "ECM"
- case 2:
- return "MBIM"
- case 3:
- return "RNDIS"
- default:
- return "unknown"
- }
- }
- func (manager *Manager) OperatorSelection(ctx context.Context, id string) (OperatorSelection, error) {
- response, err := manager.ExecuteAT(ctx, id, "AT+COPS?")
- if err != nil {
- return OperatorSelection{}, err
- }
- return parseOperatorSelection(response)
- }
- func parseOperatorSelection(response modem.Response) (OperatorSelection, error) {
- values := csvValues(valueAfterPrefix(response, "+COPS:"))
- if len(values) < 1 {
- return OperatorSelection{}, errors.New("modem did not return operator selection state")
- }
- result := OperatorSelection{}
- result.Mode, _ = strconv.Atoi(values[0])
- if len(values) > 1 {
- result.Format, _ = strconv.Atoi(values[1])
- }
- if len(values) > 2 {
- result.Operator = strings.Trim(values[2], `"`)
- }
- if len(values) > 3 {
- result.AccessTechnology = accessTechnology(values[3])
- }
- return result, nil
- }
- func (manager *Manager) SetOperatorSelection(
- ctx context.Context,
- id string,
- automatic bool,
- plmn string,
- accessTechnologyValue *int,
- ) (OperatorSelection, error) {
- result := OperatorSelection{Mode: 0}
- command := "AT+COPS=0"
- if !automatic {
- plmn = strings.TrimSpace(plmn)
- if len(plmn) < 5 || len(plmn) > 6 || strings.IndexFunc(plmn, func(r rune) bool { return r < '0' || r > '9' }) >= 0 {
- return OperatorSelection{}, errors.New("operator PLMN must contain 5 or 6 digits")
- }
- // Mode 1 is a real manual lock. Mode 4 is only a manual attempt with
- // automatic fallback; using it made a rejected registration silently
- // return to COPS=0 while the UI incorrectly reported a successful lock.
- command = fmt.Sprintf(`AT+COPS=1,2,"%s"`, plmn)
- actName := ""
- if accessTechnologyValue != nil {
- if *accessTechnologyValue < 0 || *accessTechnologyValue > 9 {
- return OperatorSelection{}, errors.New("invalid operator access technology")
- }
- command += fmt.Sprintf(",%d", *accessTechnologyValue)
- actName = accessTechnology(strconv.Itoa(*accessTechnologyValue))
- }
- result = OperatorSelection{Mode: 1, Format: 2, Operator: plmn, AccessTechnology: actName}
- }
- state, err := manager.lookup(id)
- if err != nil {
- return OperatorSelection{}, err
- }
- state.opMu.Lock()
- defer state.opMu.Unlock()
- if err := manager.validateActive(id, state); err != nil {
- return OperatorSelection{}, err
- }
- client, err := manager.clientLocked(ctx, state, manager.candidateFor(state))
- if err != nil {
- manager.setResult(id, state, nil, err)
- return OperatorSelection{}, err
- }
- // Manual PLMN selection makes the modem search for and register on the
- // requested network, which can take tens of seconds — far longer than the
- // normal command timeout. Use the same deadline budget as operator scan so
- // the lock is not aborted while registration is still in progress.
- lockCtx, cancel := manager.withTimeout(ctx, manager.scanTimeout)
- defer cancel()
- if _, err := client.Execute(lockCtx, command); err != nil {
- manager.setResult(id, state, nil, errors.New("operator selection command failed"))
- return OperatorSelection{}, err
- }
- if !automatic {
- response, err := client.Execute(lockCtx, "AT+COPS?")
- if err != nil {
- manager.setResult(id, state, nil, err)
- return OperatorSelection{}, fmt.Errorf("verify manual operator selection: %w", err)
- }
- actual, err := parseOperatorSelection(response)
- if err != nil {
- manager.setResult(id, state, nil, err)
- return OperatorSelection{}, err
- }
- if actual.Mode != 1 || actual.Operator != plmn {
- err := fmt.Errorf("network %s did not accept registration; modem reports mode=%d operator=%q", plmn, actual.Mode, actual.Operator)
- manager.setResult(id, state, nil, err)
- return OperatorSelection{}, err
- }
- result = actual
- }
- manager.setResult(id, state, nil, nil)
- return result, nil
- }
|