device_api.go 50 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430143114321433143414351436143714381439144014411442144314441445144614471448144914501451145214531454145514561457145814591460146114621463146414651466146714681469147014711472147314741475147614771478147914801481148214831484148514861487148814891490149114921493149414951496149714981499150015011502150315041505150615071508150915101511151215131514151515161517
  1. package server
  2. import (
  3. "context"
  4. "encoding/json"
  5. "errors"
  6. "fmt"
  7. "net/http"
  8. "net/url"
  9. "strconv"
  10. "strings"
  11. "time"
  12. "vocat/internal/device"
  13. "vocat/internal/i18n"
  14. "vocat/internal/modem"
  15. "vocat/internal/store"
  16. "vocat/internal/vowifi"
  17. vowifiruntime "vocat/internal/vowifi/runtime"
  18. )
  19. // DeviceController is the narrow hardware boundary used by the HTTP layer.
  20. // device.Manager implements it; tests can provide a transcript-backed fake.
  21. type DeviceController interface {
  22. Discover(context.Context) ([]device.Device, error)
  23. List() []device.Device
  24. Get(string) (device.Device, error)
  25. Refresh(context.Context, string) (device.Snapshot, error)
  26. ExecuteAT(context.Context, string, string) (modem.Response, error)
  27. Reboot(context.Context, string) error
  28. USSD(context.Context, string, string) (device.USSDResult, error)
  29. ContinueUSSD(context.Context, string, string) (device.USSDResult, error)
  30. CancelUSSD(context.Context, string) error
  31. SetFlight(context.Context, string, bool) (device.FlightResult, error)
  32. SetNetwork(context.Context, string, device.NetworkRequest) (device.NetworkResult, error)
  33. USBNetMode(context.Context, string) (device.USBNetMode, error)
  34. SetUSBNetMode(context.Context, string, int) (device.USBNetMode, error)
  35. SetUSBNetModeByPort(context.Context, string, int) (device.USBNetMode, error)
  36. OperatorSelection(context.Context, string) (device.OperatorSelection, error)
  37. SetOperatorSelection(context.Context, string, bool, string, *int) (device.OperatorSelection, error)
  38. ScanOperators(context.Context, string) (device.OperatorScanResult, error)
  39. SendSMS(context.Context, string, string, string) (device.SMSSendResult, error)
  40. ListSMS(context.Context, string) ([]device.SMSMessage, error)
  41. ReadSMS(context.Context, string, int) (device.SMSMessage, error)
  42. DeleteSMS(context.Context, string, int) error
  43. ESIMInventory(context.Context, string) ([]device.EsimInventoryEntry, error)
  44. ESIMListProfiles(context.Context, string) (device.EsimInfo, error)
  45. ESIMSwitchProfile(context.Context, string, string, string) error
  46. ESIMDisableProfile(context.Context, string, string, string) error
  47. ESIMRenameProfile(context.Context, string, string, string, string) error
  48. ESIMDownloadProfile(context.Context, string, device.EsimDownloadParams, func(device.EsimProgress)) (*device.EsimDownloadResult, error)
  49. ESIMDeleteProfile(context.Context, string, string, string) (*device.EsimDeleteResult, error)
  50. ESIMChipInfo(context.Context, string) (*device.EsimChipInfo, error)
  51. }
  52. type deviceConfigPayload struct {
  53. ID string `json:"id"`
  54. Name string `json:"name"`
  55. Interface string `json:"interface"`
  56. ControlDevice string `json:"control_device"`
  57. ATPort string `json:"at_port"`
  58. USBPath string `json:"usb_path"`
  59. AudioDevice string `json:"audio_device"`
  60. ModemIMEI string `json:"modem_imei"`
  61. APN string `json:"apn"`
  62. ProxyPort int `json:"proxy_port"`
  63. BaudRate int `json:"baud_rate"`
  64. DataBits int `json:"data_bits"`
  65. StopBits int `json:"stop_bits"`
  66. Parity string `json:"parity"`
  67. DeviceBackend string `json:"device_backend"`
  68. ESIMTransport string `json:"esim_transport"`
  69. QMIUseProxy bool `json:"qmi_use_proxy"`
  70. QMIProxyPath string `json:"qmi_proxy_path"`
  71. QMIProxyExecutable string `json:"qmi_proxy_executable"`
  72. NetworkEnabled bool `json:"network_enabled"`
  73. SMSEnabled bool `json:"sms_enabled"`
  74. VoWiFiEnabled bool `json:"vowifi_enabled"`
  75. }
  76. func (payload deviceConfigPayload) toStoreDevice() store.Device {
  77. name := strings.TrimSpace(payload.Name)
  78. if name == "" {
  79. name = payload.ID
  80. }
  81. return store.Device{
  82. ID: strings.TrimSpace(payload.ID),
  83. Name: name,
  84. Interface: strings.TrimSpace(payload.Interface),
  85. ControlDevice: strings.TrimSpace(payload.ControlDevice),
  86. ATPort: strings.TrimSpace(payload.ATPort),
  87. USBPath: strings.TrimSpace(payload.USBPath),
  88. AudioDevice: strings.TrimSpace(payload.AudioDevice),
  89. ModemIMEI: strings.TrimSpace(payload.ModemIMEI),
  90. APN: strings.TrimSpace(payload.APN),
  91. ProxyPort: payload.ProxyPort,
  92. BaudRate: payload.BaudRate,
  93. DataBits: payload.DataBits,
  94. StopBits: payload.StopBits,
  95. Parity: payload.Parity,
  96. DeviceBackend: payload.DeviceBackend,
  97. ESIMTransport: payload.ESIMTransport,
  98. QMIUseProxy: payload.QMIUseProxy,
  99. QMIProxyPath: strings.TrimSpace(payload.QMIProxyPath),
  100. QMIProxyExecutable: strings.TrimSpace(payload.QMIProxyExecutable),
  101. NetworkEnabled: payload.NetworkEnabled,
  102. SMSEnabled: payload.SMSEnabled,
  103. VoWiFiEnabled: payload.VoWiFiEnabled,
  104. }
  105. }
  106. func validDeviceID(value string) bool {
  107. value = strings.TrimSpace(value)
  108. if len(value) < 1 || len(value) > 64 {
  109. return false
  110. }
  111. for index, character := range value {
  112. if character >= 'a' && character <= 'z' ||
  113. character >= 'A' && character <= 'Z' ||
  114. character >= '0' && character <= '9' ||
  115. (index > 0 && (character == '.' || character == '_' || character == '-')) {
  116. continue
  117. }
  118. return false
  119. }
  120. return true
  121. }
  122. func (s *Server) routeDeviceAPI(w http.ResponseWriter, r *http.Request) bool {
  123. cleanPath := strings.Trim(strings.TrimPrefix(r.URL.Path, "/api"), "/")
  124. switch cleanPath {
  125. case "dashboard/devices":
  126. if !requireMethod(w, r, http.MethodGet) {
  127. return true
  128. }
  129. writeJSON(w, http.StatusOK, map[string]any{"data": s.dashboardDevices()})
  130. return true
  131. case "devices":
  132. return s.handleDevices(w, r)
  133. case "devices/discovered":
  134. return s.handleDiscoveredDevices(w, r)
  135. case "devices/actions/rescan":
  136. return s.handleDeviceRescan(w, r)
  137. case "device-mgmt/discovered/fix-usbnet":
  138. return s.handleFixUSBNet(w, r)
  139. }
  140. segments := splitAPIPath(cleanPath)
  141. if len(segments) >= 2 && segments[0] == "devices" {
  142. id := segments[1]
  143. if id == "" {
  144. writeError(w, http.StatusBadRequest, "invalid_device", "device ID is empty")
  145. return true
  146. }
  147. return s.handleDevicePath(w, r, id, segments[2:])
  148. }
  149. return false
  150. }
  151. func splitAPIPath(value string) []string {
  152. raw := strings.Split(value, "/")
  153. result := make([]string, 0, len(raw))
  154. for _, segment := range raw {
  155. decoded, err := url.PathUnescape(segment)
  156. if err != nil {
  157. decoded = segment
  158. }
  159. result = append(result, decoded)
  160. }
  161. return result
  162. }
  163. // maxDeviceLimit 是设备数量的软上限:达到上限后禁止再添加新设备。
  164. const maxDeviceLimit = 5
  165. func (s *Server) handleDevices(w http.ResponseWriter, r *http.Request) bool {
  166. switch r.Method {
  167. case http.MethodGet:
  168. writeJSON(w, http.StatusOK, map[string]any{
  169. "data": map[string]any{
  170. "device_limit": maxDeviceLimit,
  171. "devices": s.deviceSummaries(),
  172. },
  173. })
  174. case http.MethodPost:
  175. if s.devices == nil {
  176. writeError(w, http.StatusServiceUnavailable, "device_manager_unavailable", "device manager is unavailable")
  177. return true
  178. }
  179. var request struct {
  180. Config json.RawMessage `json:"config"`
  181. }
  182. if err := s.decodeJSON(w, r, &request); err != nil {
  183. writeError(w, http.StatusBadRequest, "invalid_request", err.Error())
  184. return true
  185. }
  186. var payload deviceConfigPayload
  187. if err := json.Unmarshal(request.Config, &payload); err != nil {
  188. writeError(w, http.StatusBadRequest, "invalid_device_config", "device config must be a valid JSON object")
  189. return true
  190. }
  191. if !validDeviceID(payload.ID) {
  192. writeError(w, http.StatusBadRequest, "invalid_device_id", "device ID must use 1-64 letters, digits, dots, underscores, or hyphens")
  193. return true
  194. }
  195. if _, err := s.store.Device(r.Context(), payload.ID); err == nil {
  196. writeError(w, http.StatusConflict, "device_exists", "a device with this ID already exists")
  197. return true
  198. } else if !errors.Is(err, store.ErrNotFound) {
  199. s.writeStoreError(w, err)
  200. return true
  201. }
  202. configured, err := s.store.ListDevices(r.Context())
  203. if err != nil {
  204. s.writeStoreError(w, err)
  205. return true
  206. }
  207. if len(configured) >= maxDeviceLimit {
  208. writeError(w, http.StatusConflict, "device_limit_reached", i18n.Tf("设备数量已达上限,最多只能添加 %d 台设备", maxDeviceLimit))
  209. return true
  210. }
  211. devices, err := s.devices.Discover(r.Context())
  212. if err != nil {
  213. s.writeDeviceError(w, err)
  214. return true
  215. }
  216. selected := findDiscoveredDevice(devices, payload)
  217. if selected == nil {
  218. writeError(w, http.StatusNotFound, "device_not_found", "the selected Linux modem was not discovered")
  219. return true
  220. }
  221. config := payload.toStoreDevice()
  222. fillConfigFromPhysical(&config, *selected)
  223. if err := s.store.UpsertDevice(r.Context(), config); err != nil {
  224. s.writeStoreError(w, err)
  225. return true
  226. }
  227. writeJSON(w, http.StatusCreated, map[string]any{
  228. "data": map[string]any{
  229. "status": "created",
  230. "id": config.ID,
  231. "discovery_key": selected.ID,
  232. "physical_device": s.configuredDeviceSummary(config, selected),
  233. },
  234. })
  235. default:
  236. w.Header().Set("Allow", "GET, POST")
  237. writeError(w, http.StatusMethodNotAllowed, "method_not_allowed", "method not allowed")
  238. }
  239. return true
  240. }
  241. func findDiscoveredDevice(devices []device.Device, config deviceConfigPayload) *device.Device {
  242. for index := range devices {
  243. candidate := devices[index].Candidate
  244. if config.ATPort != "" &&
  245. (candidate.ATPort.Path == config.ATPort || candidate.ATPort.OpenPath() == config.ATPort) {
  246. return &devices[index]
  247. }
  248. if config.ControlDevice != "" && candidate.QMIControl == config.ControlDevice {
  249. return &devices[index]
  250. }
  251. if config.USBPath != "" && candidate.USBPath == config.USBPath {
  252. return &devices[index]
  253. }
  254. }
  255. return nil
  256. }
  257. func (s *Server) handleDiscoveredDevices(w http.ResponseWriter, r *http.Request) bool {
  258. if !requireMethod(w, r, http.MethodGet) {
  259. return true
  260. }
  261. if s.devices == nil {
  262. writeJSON(w, http.StatusOK, map[string]any{"data": map[string]any{"devices": []any{}}})
  263. return true
  264. }
  265. devices := s.devices.List()
  266. configured, err := s.store.ListDevices(r.Context())
  267. if err != nil {
  268. s.writeStoreError(w, err)
  269. return true
  270. }
  271. result := make([]map[string]any, 0, len(devices))
  272. for _, entry := range devices {
  273. candidate := entry.Candidate
  274. atPorts := make([]string, 0, len(candidate.Ports))
  275. for _, port := range candidate.Ports {
  276. if port.Role != modem.PortRoleDiagnostic {
  277. atPorts = append(atPorts, port.OpenPath())
  278. }
  279. }
  280. controlPath := candidate.QMIControl
  281. if controlPath == "" {
  282. controlPath = candidate.ATPort.OpenPath()
  283. }
  284. configuredID := ""
  285. for _, config := range configured {
  286. if physicalMatchesConfig(entry, config) {
  287. configuredID = config.ID
  288. break
  289. }
  290. }
  291. result = append(result, map[string]any{
  292. "discovery_key": entry.ID,
  293. "control_path": controlPath,
  294. "net_interface": candidate.NetworkInterface,
  295. "usb_path": candidate.USBPath,
  296. "vendor_id": parseHexID(candidate.VendorID),
  297. "product_id": parseHexID(candidate.ProductID),
  298. "driver_name": "",
  299. "at_ports": atPorts,
  300. "at_port": candidate.ATPort.OpenPath(),
  301. "imei": snapshotString(entry.Snapshot, func(snapshot *device.Snapshot) string { return snapshot.IMEI }),
  302. "mode": backendMode(candidate),
  303. "network_capable": candidate.NetworkInterface != "" || candidate.QMIControl != "",
  304. "configured": configuredID != "",
  305. "configured_id": configuredID,
  306. "degraded": !candidate.HasATPort(),
  307. })
  308. }
  309. writeJSON(w, http.StatusOK, map[string]any{"data": map[string]any{"devices": result}})
  310. return true
  311. }
  312. func parseHexID(value string) int64 {
  313. value = strings.TrimPrefix(strings.ToLower(strings.TrimSpace(value)), "0x")
  314. number, _ := strconv.ParseInt(value, 16, 64)
  315. return number
  316. }
  317. func (s *Server) handleDeviceRescan(w http.ResponseWriter, r *http.Request) bool {
  318. if !requireMethod(w, r, http.MethodPost) {
  319. return true
  320. }
  321. if s.devices == nil {
  322. writeError(w, http.StatusServiceUnavailable, "device_manager_unavailable", "device manager is unavailable")
  323. return true
  324. }
  325. devices, err := s.devices.Discover(r.Context())
  326. if err != nil {
  327. s.writeDeviceError(w, err)
  328. return true
  329. }
  330. writeJSON(w, http.StatusOK, map[string]any{
  331. "data": map[string]any{"status": "ok", "devices": len(devices)},
  332. })
  333. return true
  334. }
  335. func (s *Server) handleDevicePath(
  336. w http.ResponseWriter,
  337. r *http.Request,
  338. id string,
  339. tail []string,
  340. ) bool {
  341. config, err := s.store.Device(r.Context(), id)
  342. if err != nil {
  343. s.writeStoreError(w, err)
  344. return true
  345. }
  346. if len(tail) == 0 {
  347. switch r.Method {
  348. case http.MethodDelete:
  349. if err := s.store.DeleteDevice(r.Context(), id); err != nil {
  350. s.writeStoreError(w, err)
  351. return true
  352. }
  353. writeJSON(w, http.StatusOK, map[string]any{
  354. "data": map[string]any{"deleted": true, "physical_device_untouched": true},
  355. })
  356. case http.MethodPut:
  357. var request struct {
  358. Config json.RawMessage `json:"config"`
  359. }
  360. if err := s.decodeJSON(w, r, &request); err != nil {
  361. writeError(w, http.StatusBadRequest, "invalid_request", err.Error())
  362. return true
  363. }
  364. var payload deviceConfigPayload
  365. if err := json.Unmarshal(request.Config, &payload); err != nil {
  366. writeError(w, http.StatusBadRequest, "invalid_device_config", "device config must be a valid JSON object")
  367. return true
  368. }
  369. if payload.ID != "" && payload.ID != id {
  370. writeError(w, http.StatusConflict, "immutable_device_id", "device ID cannot be changed")
  371. return true
  372. }
  373. next := payload.toStoreDevice()
  374. next.ID = id
  375. next.CreatedAt = config.CreatedAt
  376. if next.Name == id && strings.TrimSpace(payload.Name) == "" {
  377. next.Name = config.Name
  378. }
  379. if err := s.store.UpsertDevice(r.Context(), next); err != nil {
  380. s.writeStoreError(w, err)
  381. return true
  382. }
  383. writeJSON(w, http.StatusOK, map[string]any{
  384. "data": map[string]any{"status": "saved", "config": storedDeviceConfig(next)},
  385. })
  386. default:
  387. w.Header().Set("Allow", "DELETE, PUT")
  388. writeError(w, http.StatusMethodNotAllowed, "method_not_allowed", "method not allowed")
  389. }
  390. return true
  391. }
  392. entry, physicalID, physicalPresent := s.physicalForConfig(config)
  393. if len(tail) > 0 && tail[0] == "esim" {
  394. return s.handleESIM(w, r, tail[1:], physicalID, physicalPresent)
  395. }
  396. switch strings.Join(tail, "/") {
  397. case "overview":
  398. if !requireMethod(w, r, http.MethodGet) {
  399. return true
  400. }
  401. writeJSON(w, http.StatusOK, map[string]any{
  402. "data": map[string]any{"devices": []any{s.configuredDeviceOverview(config, entry, physicalPresent)}},
  403. })
  404. case "overview/stream":
  405. return s.handleOverviewStream(w, r, config, entry, physicalPresent)
  406. case "status":
  407. if !requireMethod(w, r, http.MethodGet) {
  408. return true
  409. }
  410. writeJSON(w, http.StatusOK, map[string]any{"data": s.configuredDeviceStatus(config, entry, physicalPresent)})
  411. case "config":
  412. if !requireMethod(w, r, http.MethodGet) {
  413. return true
  414. }
  415. writeJSON(w, http.StatusOK, map[string]any{
  416. "data": map[string]any{"config": storedDeviceConfig(config)},
  417. })
  418. case "actions/refresh":
  419. if !requireMethod(w, r, http.MethodPost) {
  420. return true
  421. }
  422. if !s.requirePhysicalDevice(w, physicalPresent) {
  423. return true
  424. }
  425. snapshot, err := s.devices.Refresh(r.Context(), physicalID)
  426. if err != nil {
  427. s.writeDeviceError(w, err)
  428. return true
  429. }
  430. writeJSON(w, http.StatusOK, map[string]any{"data": snapshot})
  431. case "actions/at":
  432. if !s.requirePhysicalDevice(w, physicalPresent) {
  433. return true
  434. }
  435. return s.handleAT(w, r, physicalID)
  436. case "actions/ussd":
  437. if !s.requirePhysicalDevice(w, physicalPresent) {
  438. return true
  439. }
  440. return s.handleUSSD(w, r, physicalID)
  441. case "actions/ussd/continue":
  442. return s.handleUSSDContinue(w, r)
  443. case "actions/ussd/cancel":
  444. return s.handleUSSDCancel(w, r)
  445. case "actions/reboot":
  446. if !requireMethod(w, r, http.MethodPost) {
  447. return true
  448. }
  449. if !s.requirePhysicalDevice(w, physicalPresent) {
  450. return true
  451. }
  452. if err := s.devices.Reboot(r.Context(), physicalID); err != nil {
  453. s.writeDeviceError(w, err)
  454. return true
  455. }
  456. writeJSON(w, http.StatusAccepted, map[string]any{"data": map[string]any{"status": "rebooting"}})
  457. case "flight-mode":
  458. if !s.requirePhysicalDevice(w, physicalPresent) {
  459. return true
  460. }
  461. return s.handleFlightMode(w, r, physicalID)
  462. case "usbnet-mode":
  463. if !s.requirePhysicalDevice(w, physicalPresent) {
  464. return true
  465. }
  466. return s.handleUSBNetMode(w, r, physicalID)
  467. case "operator_selection":
  468. if !s.requirePhysicalDevice(w, physicalPresent) {
  469. return true
  470. }
  471. return s.handleOperatorSelection(w, r, physicalID)
  472. case "operator_selection/scan":
  473. if !s.requirePhysicalDevice(w, physicalPresent) {
  474. return true
  475. }
  476. return s.handleOperatorScan(w, r, physicalID)
  477. case "operator_selection/scan/stream":
  478. if !s.requirePhysicalDevice(w, physicalPresent) {
  479. return true
  480. }
  481. return s.handleOperatorScanStream(w, r, physicalID)
  482. case "vowifi":
  483. return s.handleVoWiFiEnabled(w, r, config, physicalPresent)
  484. case "vowifi/actions/reconnect":
  485. return s.handleVoWiFiReconnect(w, r, config, physicalPresent)
  486. case "vowifi/e911/websheet":
  487. return s.handleE911Websheet(w, r, config)
  488. default:
  489. return false
  490. }
  491. return true
  492. }
  493. func (s *Server) handleUSBNetMode(w http.ResponseWriter, r *http.Request, physicalID string) bool {
  494. switch r.Method {
  495. case http.MethodGet:
  496. result, err := s.devices.USBNetMode(r.Context(), physicalID)
  497. if err != nil {
  498. s.writeDeviceError(w, err)
  499. return true
  500. }
  501. writeJSON(w, http.StatusOK, map[string]any{"data": result})
  502. case http.MethodPatch:
  503. var request struct {
  504. Mode int `json:"mode"`
  505. }
  506. if err := s.decodeJSON(w, r, &request); err != nil {
  507. writeError(w, http.StatusBadRequest, "invalid_request", err.Error())
  508. return true
  509. }
  510. result, err := s.devices.SetUSBNetMode(r.Context(), physicalID, request.Mode)
  511. if err != nil {
  512. s.writeDeviceError(w, err)
  513. return true
  514. }
  515. writeJSON(w, http.StatusOK, map[string]any{
  516. "data": map[string]any{
  517. "mode": result.Mode, "name": result.Name,
  518. "reboot_required": true,
  519. },
  520. })
  521. default:
  522. w.Header().Set("Allow", "GET, PATCH")
  523. writeError(w, http.StatusMethodNotAllowed, "method_not_allowed", "method not allowed")
  524. }
  525. return true
  526. }
  527. // operatorSelectionWire is the current-selection shape the SPA reads.
  528. func operatorSelectionWire(sel device.OperatorSelection) map[string]any {
  529. mode := "automatic"
  530. if sel.Mode != 0 {
  531. mode = "manual"
  532. }
  533. return map[string]any{
  534. "mode": mode,
  535. "plmn": sel.Operator,
  536. "access_technology": sel.AccessTechnology,
  537. }
  538. }
  539. // accessTechnologyValue maps a RAT name (as surfaced to the UI by the scan) back
  540. // to its numeric AT+COPS access technology. Returns nil for an unknown name.
  541. func accessTechnologyValue(name string) *int {
  542. var value int
  543. switch strings.ToUpper(strings.TrimSpace(name)) {
  544. case "GSM":
  545. value = 0
  546. case "UTRAN", "UMTS", "WCDMA":
  547. value = 2
  548. case "EDGE":
  549. value = 3
  550. case "HSDPA":
  551. value = 4
  552. case "HSUPA":
  553. value = 5
  554. case "HSPA":
  555. value = 6
  556. case "LTE":
  557. value = 7
  558. case "NR5G", "NR":
  559. value = 9
  560. default:
  561. return nil
  562. }
  563. return &value
  564. }
  565. func (s *Server) handleOperatorSelection(w http.ResponseWriter, r *http.Request, physicalID string) bool {
  566. switch r.Method {
  567. case http.MethodGet:
  568. result, err := s.devices.OperatorSelection(r.Context(), physicalID)
  569. if err != nil {
  570. s.writeDeviceError(w, err)
  571. return true
  572. }
  573. writeJSON(w, http.StatusOK, map[string]any{"data": operatorSelectionWire(result)})
  574. case http.MethodPost, http.MethodPut, http.MethodPatch:
  575. // The SPA posts {mode, plmn, includes_pcs_digit, rat}; the legacy shape is
  576. // {automatic, plmn, access_technology}. Accept both. includes_pcs_digit is
  577. // accepted for contract compatibility but not currently applied to the command.
  578. var request struct {
  579. Mode string `json:"mode"`
  580. Automatic *bool `json:"automatic"`
  581. PLMN string `json:"plmn"`
  582. AccessTechnology *int `json:"access_technology"`
  583. Rat string `json:"rat"`
  584. IncludesPcsDigit bool `json:"includes_pcs_digit"`
  585. }
  586. if err := s.decodeJSON(w, r, &request); err != nil {
  587. writeError(w, http.StatusBadRequest, "invalid_request", err.Error())
  588. return true
  589. }
  590. automatic := false
  591. switch strings.ToLower(strings.TrimSpace(request.Mode)) {
  592. case "manual":
  593. automatic = false
  594. case "automatic":
  595. automatic = true
  596. default:
  597. if request.Automatic != nil {
  598. automatic = *request.Automatic
  599. }
  600. }
  601. accessTechnology := request.AccessTechnology
  602. if rat := strings.TrimSpace(request.Rat); rat != "" {
  603. accessTechnology = accessTechnologyValue(rat)
  604. }
  605. // Manual PLMN selection can take tens of seconds while the modem
  606. // searches for and registers on the requested network. The server's
  607. // WriteTimeout would otherwise cut the response off, so clear the
  608. // connection's write deadline for this request like the SSE paths do.
  609. controller := http.NewResponseController(w)
  610. _ = controller.SetWriteDeadline(time.Time{})
  611. result, err := s.devices.SetOperatorSelection(r.Context(), physicalID, automatic, request.PLMN, accessTechnology)
  612. if err != nil {
  613. s.writeDeviceError(w, err)
  614. return true
  615. }
  616. writeJSON(w, http.StatusOK, map[string]any{"data": operatorSelectionWire(result)})
  617. default:
  618. w.Header().Set("Allow", "GET, POST, PUT, PATCH")
  619. writeError(w, http.StatusMethodNotAllowed, "method_not_allowed", "method not allowed")
  620. }
  621. return true
  622. }
  623. func (s *Server) handleVoWiFiEnabled(
  624. w http.ResponseWriter,
  625. r *http.Request,
  626. config store.Device,
  627. physicalPresent bool,
  628. ) bool {
  629. if !requireMethod(w, r, http.MethodPatch) {
  630. return true
  631. }
  632. var request struct {
  633. Enabled bool `json:"enabled"`
  634. }
  635. if err := s.decodeJSON(w, r, &request); err != nil {
  636. writeError(w, http.StatusBadRequest, "invalid_request", err.Error())
  637. return true
  638. }
  639. if s.vowifi == nil {
  640. writeError(w, http.StatusServiceUnavailable, "vowifi_provider_unavailable", "the VoWiFi runtime is unavailable")
  641. return true
  642. }
  643. if request.Enabled && !physicalPresent {
  644. writeError(w, http.StatusServiceUnavailable, "physical_device_missing", "the configured modem is not present on this Linux host")
  645. return true
  646. }
  647. if request.Enabled {
  648. entry, _, _ := s.physicalForConfig(config)
  649. imsi := snapshotString(entry.Snapshot, func(snapshot *device.Snapshot) string { return snapshot.IMSI })
  650. if reason := device.RegionBlockReason(imsi); reason != "" {
  651. writeError(w, http.StatusForbidden, "region_blocked", reason)
  652. return true
  653. }
  654. }
  655. previous := config.VoWiFiEnabled
  656. config.VoWiFiEnabled = request.Enabled
  657. if err := s.store.UpsertDevice(r.Context(), config); err != nil {
  658. s.writeStoreError(w, err)
  659. return true
  660. }
  661. state, err := s.vowifi.RequestEnabled(config.ID, request.Enabled)
  662. if err != nil {
  663. // Repeating the same desired state while its asynchronous transaction is
  664. // already running is idempotent. Treat it as accepted instead of making a
  665. // harmless double-click (or a stale browser refresh) surface vowifi_busy.
  666. if errors.Is(err, vowifiruntime.ErrOperationInProgress) &&
  667. (state.Enabled == request.Enabled || previous == request.Enabled) {
  668. writeJSON(w, http.StatusAccepted, map[string]any{
  669. "data": map[string]any{
  670. "accepted": true,
  671. "enabled": request.Enabled,
  672. "status": "in_progress",
  673. "runtime": state,
  674. },
  675. })
  676. return true
  677. }
  678. config.VoWiFiEnabled = previous
  679. if restoreErr := s.store.UpsertDevice(r.Context(), config); restoreErr != nil {
  680. s.logger.Error(
  681. "restore VoWiFi policy after rejected runtime operation",
  682. "device_id", config.ID,
  683. "error", restoreErr,
  684. )
  685. }
  686. s.writeVoWiFiError(w, err)
  687. return true
  688. }
  689. writeJSON(w, http.StatusAccepted, map[string]any{
  690. "data": map[string]any{
  691. "accepted": true,
  692. "enabled": request.Enabled,
  693. "status": map[bool]string{true: "starting", false: "stopping"}[request.Enabled],
  694. "runtime": state,
  695. },
  696. })
  697. return true
  698. }
  699. func (s *Server) handleVoWiFiReconnect(
  700. w http.ResponseWriter,
  701. r *http.Request,
  702. config store.Device,
  703. physicalPresent bool,
  704. ) bool {
  705. if !requireMethod(w, r, http.MethodPost) {
  706. return true
  707. }
  708. if s.vowifi == nil {
  709. writeError(w, http.StatusServiceUnavailable, "vowifi_provider_unavailable", "the VoWiFi runtime is unavailable")
  710. return true
  711. }
  712. if !config.VoWiFiEnabled {
  713. writeError(w, http.StatusConflict, "vowifi_disabled", "enable VoWiFi before requesting a reconnect")
  714. return true
  715. }
  716. if !physicalPresent {
  717. writeError(w, http.StatusServiceUnavailable, "physical_device_missing", "the configured modem is not present on this Linux host")
  718. return true
  719. }
  720. state, err := s.vowifi.RequestReconnect(config.ID)
  721. if err != nil {
  722. s.writeVoWiFiError(w, err)
  723. return true
  724. }
  725. writeJSON(w, http.StatusAccepted, map[string]any{
  726. "data": map[string]any{
  727. "accepted": true,
  728. "status": "reconnecting",
  729. "runtime": state,
  730. },
  731. })
  732. return true
  733. }
  734. func (s *Server) writeVoWiFiError(w http.ResponseWriter, err error) {
  735. switch {
  736. case errors.Is(err, vowifiruntime.ErrNotRegistered):
  737. writeError(w, http.StatusServiceUnavailable, "vowifi_device_unavailable", "the configured device has no VoWiFi runtime")
  738. case errors.Is(err, vowifiruntime.ErrOperationInProgress):
  739. writeError(w, http.StatusConflict, "vowifi_busy", "another VoWiFi operation is still in progress")
  740. case errors.Is(err, vowifiruntime.ErrClosed):
  741. writeError(w, http.StatusServiceUnavailable, "vowifi_runtime_stopped", "the VoWiFi runtime is stopping")
  742. case errors.Is(err, vowifi.ErrNotRunning):
  743. writeError(w, http.StatusConflict, "vowifi_not_running", "VoWiFi is not running")
  744. default:
  745. s.logger.Warn("VoWiFi action rejected", "error", err)
  746. writeError(w, http.StatusBadGateway, "vowifi_error", err.Error())
  747. }
  748. }
  749. // maxActionTimeoutMs bounds a client-supplied per-request timeout so a terminal
  750. // action cannot hold the device operation lock indefinitely.
  751. const maxActionTimeoutMs = 120000
  752. // actionRequestContext applies the frontend's optional per-request timeout
  753. // (timeout_ms) to the request context. Manager.withTimeout preserves an
  754. // existing deadline, so setting one here makes the requested timeout take
  755. // effect; a non-positive or absent value leaves the manager's defaults in place.
  756. func actionRequestContext(parent context.Context, timeoutMs int) (context.Context, context.CancelFunc) {
  757. if parent == nil {
  758. parent = context.Background()
  759. }
  760. if timeoutMs <= 0 {
  761. return context.WithCancel(parent)
  762. }
  763. if timeoutMs > maxActionTimeoutMs {
  764. timeoutMs = maxActionTimeoutMs
  765. }
  766. return context.WithTimeout(parent, time.Duration(timeoutMs)*time.Millisecond)
  767. }
  768. func (s *Server) handleAT(w http.ResponseWriter, r *http.Request, id string) bool {
  769. if !requireMethod(w, r, http.MethodPost) {
  770. return true
  771. }
  772. var request struct {
  773. Command string `json:"cmd"`
  774. TimeoutMs int `json:"timeout_ms"`
  775. }
  776. if err := s.decodeJSON(w, r, &request); err != nil {
  777. writeError(w, http.StatusBadRequest, "invalid_request", err.Error())
  778. return true
  779. }
  780. command := strings.TrimSpace(request.Command)
  781. if err := validateATCommand(command); err != nil {
  782. writeError(w, http.StatusBadRequest, "unsafe_at_command", err.Error())
  783. return true
  784. }
  785. ctx, cancel := actionRequestContext(r.Context(), request.TimeoutMs)
  786. defer cancel()
  787. response, err := s.devices.ExecuteAT(ctx, id, command)
  788. if err != nil {
  789. s.writeDeviceError(w, err)
  790. return true
  791. }
  792. text := response.Text()
  793. if response.Final != "" {
  794. if text != "" {
  795. text += "\n"
  796. }
  797. text += response.Final
  798. }
  799. writeJSON(w, http.StatusOK, map[string]any{
  800. "data": map[string]any{
  801. "response": text,
  802. "final": response.Final,
  803. "duration_ms": response.Duration.Milliseconds(),
  804. "urcs": response.URCs,
  805. },
  806. })
  807. return true
  808. }
  809. func validateATCommand(command string) error {
  810. upper := strings.ToUpper(command)
  811. if len(command) < 2 || len(command) > 512 || !strings.HasPrefix(upper, "AT") {
  812. return errors.New("AT command must start with AT and contain at most 512 characters")
  813. }
  814. if strings.ContainsAny(command, "\r\n\x00") {
  815. return errors.New("AT command must contain exactly one line")
  816. }
  817. canonical := strings.NewReplacer(" ", "", "\t", "").Replace(upper)
  818. for _, blocked := range []string{
  819. `+QCFG="USBNET"`,
  820. `+QCFG="USBCFG"`,
  821. "+QPOWD",
  822. "+CFUN=",
  823. "+CGATT=",
  824. "+CGACT=",
  825. "+CGDATA",
  826. "+QNETDEVCTL=",
  827. "+QIACT",
  828. "+QIDEACT",
  829. "+CMGS",
  830. "+CMSS",
  831. "+CMGC",
  832. "+QCMGS",
  833. "+CUSD=",
  834. "D",
  835. "A",
  836. "H",
  837. } {
  838. for _, segment := range strings.Split(canonical[2:], ";") {
  839. if strings.HasPrefix(segment, blocked) {
  840. return fmt.Errorf("AT%s is reserved for a guarded device action", blocked)
  841. }
  842. }
  843. }
  844. return nil
  845. }
  846. func (s *Server) handleUSSD(w http.ResponseWriter, r *http.Request, id string) bool {
  847. if !requireMethod(w, r, http.MethodPost) {
  848. return true
  849. }
  850. var request struct {
  851. Command string `json:"command"`
  852. TimeoutMs int `json:"timeout_ms"`
  853. }
  854. if err := s.decodeJSON(w, r, &request); err != nil {
  855. writeError(w, http.StatusBadRequest, "invalid_request", err.Error())
  856. return true
  857. }
  858. ctx, cancel := actionRequestContext(r.Context(), request.TimeoutMs)
  859. defer cancel()
  860. result, err := s.devices.USSD(ctx, id, request.Command)
  861. if err != nil {
  862. s.writeDeviceError(w, err)
  863. return true
  864. }
  865. writeJSON(w, http.StatusOK, map[string]any{
  866. "data": map[string]any{
  867. "result": result.Text,
  868. "raw": result.Raw,
  869. "dcs": result.DCS,
  870. },
  871. })
  872. return true
  873. }
  874. func (s *Server) handleFlightMode(w http.ResponseWriter, r *http.Request, id string) bool {
  875. if !requireMethod(w, r, http.MethodPatch) {
  876. return true
  877. }
  878. var request struct {
  879. Enabled bool `json:"enabled"`
  880. }
  881. if err := s.decodeJSON(w, r, &request); err != nil {
  882. writeError(w, http.StatusBadRequest, "invalid_request", err.Error())
  883. return true
  884. }
  885. result, err := s.devices.SetFlight(r.Context(), id, request.Enabled)
  886. if err != nil {
  887. s.writeDeviceError(w, err)
  888. return true
  889. }
  890. writeJSON(w, http.StatusOK, map[string]any{"data": result})
  891. return true
  892. }
  893. func (s *Server) requirePhysicalDevice(w http.ResponseWriter, present bool) bool {
  894. if s.devices == nil {
  895. writeError(w, http.StatusServiceUnavailable, "device_manager_unavailable", "device manager is unavailable")
  896. return false
  897. }
  898. if !present {
  899. writeError(w, http.StatusServiceUnavailable, "physical_device_missing", "the configured modem is not present on this Linux host")
  900. return false
  901. }
  902. return true
  903. }
  904. func (s *Server) writeDeviceError(w http.ResponseWriter, err error) {
  905. switch {
  906. case errors.Is(err, device.ErrNotFound):
  907. writeError(w, http.StatusNotFound, "device_not_found", "device was not found or is no longer present")
  908. case errors.Is(err, device.ErrNotStarted):
  909. writeError(w, http.StatusServiceUnavailable, "device_manager_not_started", "device manager is not started")
  910. case errors.Is(err, device.ErrNoATPort):
  911. writeError(w, http.StatusServiceUnavailable, "at_port_unavailable", "device has no usable AT port")
  912. case errors.Is(err, device.ErrDataBackendUnavailable):
  913. writeError(w, http.StatusNotImplemented, "data_backend_unavailable", err.Error())
  914. case errors.Is(err, device.ErrEUICCChannelStuck):
  915. writeError(w, http.StatusServiceUnavailable, "euicc_channel_stuck", err.Error())
  916. case errors.Is(err, device.ErrESIMDeleteProfileNotFound):
  917. writeError(w, http.StatusNotFound, "esim_profile_not_found", "The profile is no longer present on this eUICC. Refresh the profile list and try again.")
  918. case errors.Is(err, device.ErrESIMDeleteProfileNotDisabled):
  919. writeError(w, http.StatusConflict, "esim_profile_enabled", "The active profile cannot be deleted. Enable another profile first, then delete this disabled profile.")
  920. case errors.Is(err, device.ErrESIMDeleteDisallowedByPolicy):
  921. writeError(w, http.StatusConflict, "esim_delete_disallowed_by_policy", "This profile's policy does not allow it to be deleted.")
  922. case errors.Is(err, device.ErrESIMNicknameTooLong):
  923. writeError(w, http.StatusBadRequest, "esim_nickname_too_long", "Profile nickname must not exceed 64 characters.")
  924. case errors.Is(err, device.ErrESIMNicknameProfileNotFound):
  925. writeError(w, http.StatusNotFound, "esim_profile_not_found", "The profile is no longer present on this eUICC. Refresh the profile list and try again.")
  926. case errors.Is(err, device.ErrESIMDisableProfileNotFound):
  927. writeError(w, http.StatusNotFound, "esim_profile_not_found", "The profile is no longer present on this eUICC. Refresh the profile list and try again.")
  928. case errors.Is(err, device.ErrESIMProfileNotEnabled):
  929. writeError(w, http.StatusConflict, "esim_profile_not_enabled", "This profile is already disabled. Refresh the profile list before retrying.")
  930. case errors.Is(err, device.ErrESIMDisableDisallowedByPolicy):
  931. writeError(w, http.StatusConflict, "esim_disable_disallowed_by_policy", "This profile's policy does not allow it to be disabled.")
  932. case errors.Is(err, device.ErrESIMDisableCATBusy):
  933. writeError(w, http.StatusConflict, "esim_cat_busy", "The eUICC is busy with a SIM Toolkit operation. Wait a moment and retry disabling the profile.")
  934. case errors.Is(err, device.ErrInvalidNetworkAPN):
  935. writeError(w, http.StatusBadRequest, "invalid_apn", "APN must contain only letters, digits, dots, underscores, or hyphens")
  936. case errors.Is(err, device.ErrRegionBlocked):
  937. writeError(w, http.StatusForbidden, "region_blocked", err.Error())
  938. case errors.Is(err, context.DeadlineExceeded), errors.Is(err, modem.ErrCommandTimeout):
  939. writeError(w, http.StatusGatewayTimeout, "modem_timeout", "the modem did not answer before the command timeout")
  940. case errors.Is(err, context.Canceled):
  941. writeError(w, http.StatusRequestTimeout, "request_canceled", "the modem request was canceled")
  942. default:
  943. s.logger.Warn("device operation failed", "error", err)
  944. writeError(w, http.StatusBadGateway, "modem_error", err.Error())
  945. }
  946. }
  947. func (s *Server) deviceSummaries() []map[string]any {
  948. configs, err := s.store.ListDevices(context.Background())
  949. if err != nil {
  950. s.logger.Error("list configured devices", "error", err)
  951. return []map[string]any{}
  952. }
  953. result := make([]map[string]any, 0, len(configs))
  954. for _, config := range configs {
  955. entry, _, present := s.physicalForConfig(config)
  956. if present {
  957. result = append(result, s.configuredDeviceSummary(config, &entry))
  958. } else {
  959. result = append(result, s.configuredDeviceSummary(config, nil))
  960. }
  961. }
  962. return result
  963. }
  964. func (s *Server) dashboardDevices() []map[string]any {
  965. devices := s.deviceSummaries()
  966. result := make([]map[string]any, 0, len(devices))
  967. for _, entry := range devices {
  968. modemStatus, _ := entry["modem"].(map[string]any)
  969. runtime, _ := entry["vowifi_runtime"].(map[string]any)
  970. vowifiActive, _ := runtime["tunnel_ready"].(bool)
  971. result = append(result, map[string]any{
  972. "id": entry["id"],
  973. "name": entry["name"],
  974. "interface": entry["interface"],
  975. "proxy_port": entry["proxy_port"],
  976. "public_ip": entry["public_ip"],
  977. "healthy": entry["healthy"],
  978. "operator": modemStatus["operator"],
  979. "signal_dbm": modemStatus["signal_dbm"],
  980. "network_mode": modemStatus["network_mode"],
  981. "network_duplex": modemStatus["network_duplex"],
  982. "vowifi_active": vowifiActive,
  983. "vowifi_runtime": runtime,
  984. "network_connected": false,
  985. "model": modemStatus["model"],
  986. })
  987. }
  988. return result
  989. }
  990. func (s *Server) physicalForConfig(config store.Device) (device.Device, string, bool) {
  991. if s.devices == nil {
  992. return device.Device{ID: config.ID}, "", false
  993. }
  994. if entry, err := s.devices.Get(config.ID); err == nil && entry.Discovered {
  995. return entry, entry.ID, true
  996. }
  997. for _, entry := range s.devices.List() {
  998. if entry.Discovered && physicalMatchesConfig(entry, config) {
  999. return entry, entry.ID, true
  1000. }
  1001. }
  1002. return device.Device{ID: config.ID}, "", false
  1003. }
  1004. func physicalMatchesConfig(entry device.Device, config store.Device) bool {
  1005. candidate := entry.Candidate
  1006. if entry.ID == config.ID {
  1007. return true
  1008. }
  1009. if config.ATPort != "" &&
  1010. (config.ATPort == candidate.ATPort.Path || config.ATPort == candidate.ATPort.OpenPath()) {
  1011. return true
  1012. }
  1013. if config.ControlDevice != "" && config.ControlDevice == candidate.QMIControl {
  1014. return true
  1015. }
  1016. if config.USBPath != "" && config.USBPath == candidate.USBPath {
  1017. return true
  1018. }
  1019. return config.ModemIMEI != "" &&
  1020. entry.Snapshot != nil &&
  1021. config.ModemIMEI == entry.Snapshot.IMEI
  1022. }
  1023. func (s *Server) configuredDeviceSummary(
  1024. config store.Device,
  1025. entry *device.Device,
  1026. ) map[string]any {
  1027. var result map[string]any
  1028. if entry != nil {
  1029. result = deviceSummary(*entry)
  1030. } else {
  1031. result = deviceSummary(device.Device{ID: config.ID})
  1032. }
  1033. result["id"] = config.ID
  1034. result["name"] = config.Name
  1035. result["interface"] = config.Interface
  1036. result["proxy_port"] = config.ProxyPort
  1037. result["esim_transport"] = config.ESIMTransport
  1038. result["sms_enabled"] = config.SMSEnabled
  1039. result["network_enabled"] = false
  1040. result["vowifi_enabled"] = config.VoWiFiEnabled
  1041. if runtime, err := s.store.VoWiFiRuntime(context.Background(), config.ID); err == nil {
  1042. runtimeResponse := storedVoWiFiRuntime(runtime)
  1043. result["vowifi_runtime"] = runtimeResponse
  1044. result["vowifi_active"] = runtime.TunnelReady
  1045. if runtime.LocalPhone != "" {
  1046. // The SIM panel reads the top-level local_phone; keep modem.phone_number
  1047. // in sync for the summary/overview consumers that read it there.
  1048. result["local_phone"] = runtime.LocalPhone
  1049. result["phone_number_source"] = runtime.PhoneNumberSource
  1050. if modemStatus, ok := result["modem"].(map[string]any); ok {
  1051. modemStatus["phone_number"] = runtime.LocalPhone
  1052. modemStatus["phone_number_source"] = runtime.PhoneNumberSource
  1053. }
  1054. }
  1055. }
  1056. return result
  1057. }
  1058. func (s *Server) configuredDeviceOverview(
  1059. config store.Device,
  1060. entry device.Device,
  1061. present bool,
  1062. ) map[string]any {
  1063. var physical *device.Device
  1064. if present {
  1065. physical = &entry
  1066. }
  1067. result := s.configuredDeviceSummary(config, physical)
  1068. result["id"] = config.ID
  1069. result["name"] = config.Name
  1070. result["interface"] = config.Interface
  1071. result["at_port"] = config.ATPort
  1072. result["audio_device"] = config.AudioDevice
  1073. result["backend_mode"] = config.DeviceBackend
  1074. result["control_device"] = config.ControlDevice
  1075. result["esim_transport"] = config.ESIMTransport
  1076. result["sms_enabled"] = config.SMSEnabled
  1077. result["network_enabled"] = false
  1078. result["vowifi_enabled"] = config.VoWiFiEnabled
  1079. result["radio_live_ok"] = present && entry.Snapshot != nil && entry.Snapshot.Responsive
  1080. result["traffic"] = map[string]string{}
  1081. result["traffic_raw"] = map[string]int64{}
  1082. result["traffic_meta"] = map[string]any{}
  1083. return result
  1084. }
  1085. func (s *Server) configuredDeviceStatus(
  1086. config store.Device,
  1087. entry device.Device,
  1088. present bool,
  1089. ) map[string]any {
  1090. var physical *device.Device
  1091. if present {
  1092. physical = &entry
  1093. }
  1094. summary := s.configuredDeviceSummary(config, physical)
  1095. lastUpdated := time.Time{}
  1096. if physical != nil {
  1097. lastUpdated = physical.LastUpdated
  1098. }
  1099. result := map[string]any{
  1100. "healthy": summary["healthy"],
  1101. "public_ip": summary["public_ip"],
  1102. "network_connected": false,
  1103. "modem": summary["modem"],
  1104. "vowifi": summary["vowifi_runtime"],
  1105. "sim_service_table": map[string]any{},
  1106. "pnn": []any{},
  1107. "opl": []any{},
  1108. "last_hardware_refresh": lastUpdated,
  1109. }
  1110. result["id"] = config.ID
  1111. result["name"] = config.Name
  1112. result["interface"] = config.Interface
  1113. result["proxy_port"] = config.ProxyPort
  1114. return result
  1115. }
  1116. func storedVoWiFiRuntime(runtime store.VoWiFiRuntime) map[string]any {
  1117. return map[string]any{
  1118. "device_id": runtime.DeviceID,
  1119. "phase": runtime.Phase,
  1120. "dataplane_mode": runtime.DataplaneMode,
  1121. "iccid": runtime.ICCID,
  1122. "imsi": runtime.IMSI,
  1123. "sim_ready": runtime.SIMReady,
  1124. "access_ready": runtime.AccessReady,
  1125. "tunnel_ready": runtime.TunnelReady,
  1126. "ims_ready": runtime.IMSReady,
  1127. "sms_ready": runtime.SMSReady,
  1128. "reg_status": runtime.RegStatus,
  1129. "reg_status_text": runtime.RegStatusText,
  1130. "network_mode": runtime.NetworkMode,
  1131. "local_phone": runtime.LocalPhone,
  1132. "phone_number_source": runtime.PhoneNumberSource,
  1133. "last_error_class": runtime.LastErrorClass,
  1134. "last_error": runtime.LastError,
  1135. "last_reason": runtime.LastReason,
  1136. "updated_at": runtime.UpdatedAt,
  1137. "tunnel": rawJSONObject(runtime.Tunnel),
  1138. "imscore": rawJSONObject(runtime.IMSCore),
  1139. "smsip": rawJSONObject(runtime.SMSIP),
  1140. }
  1141. }
  1142. func rawJSONObject(value json.RawMessage) any {
  1143. var result any
  1144. if len(value) != 0 && json.Unmarshal(value, &result) == nil {
  1145. return result
  1146. }
  1147. return map[string]any{}
  1148. }
  1149. func deviceSummary(entry device.Device) map[string]any {
  1150. snapshot := entry.Snapshot
  1151. healthy := entry.Discovered && snapshot != nil && snapshot.Responsive && entry.LastError == ""
  1152. phone := ""
  1153. phoneSource := ""
  1154. if snapshot != nil {
  1155. phone = snapshot.Phone.Number
  1156. phoneSource = snapshot.Phone.Source
  1157. }
  1158. mode := 0
  1159. modeKnown := false
  1160. if snapshot != nil {
  1161. mode = snapshot.OperatingMode
  1162. modeKnown = snapshot.ModeKnown
  1163. }
  1164. runtime := idleVoWiFiRuntime(entry.ID, snapshot)
  1165. summary := modemSummary(snapshot, phone, phoneSource)
  1166. if model, _ := summary["model"].(string); strings.TrimSpace(model) == "" {
  1167. if p := strings.TrimSpace(entry.Candidate.Product); p != "" {
  1168. summary["model"] = p
  1169. } else if id := strings.TrimSpace(entry.ID); id != "" {
  1170. summary["model"] = id
  1171. }
  1172. }
  1173. return map[string]any{
  1174. "id": entry.ID,
  1175. "name": deviceName(entry),
  1176. "running": entry.Discovered,
  1177. "healthy": healthy,
  1178. "control_online": healthy,
  1179. "physical_present": entry.Discovered,
  1180. "worker_running": entry.Discovered,
  1181. "data_connected": false,
  1182. "radio_registered": snapshot != nil && snapshot.OperatorName != "",
  1183. "lifecycle_phase": lifecyclePhase(entry),
  1184. "lifecycle_reason": entry.LastError,
  1185. "public_ip": "",
  1186. "private_ip": "",
  1187. "interface": entry.Candidate.NetworkInterface,
  1188. "esim_transport": backendMode(entry.Candidate),
  1189. "sms_enabled": true,
  1190. "network_enabled": false,
  1191. "vowifi_enabled": false,
  1192. "vowifi_active": false,
  1193. "vowifi_runtime": runtime,
  1194. "modem": summary,
  1195. "local_phone": phone,
  1196. "phone_number_source": phoneSource,
  1197. "network_connected": false,
  1198. "registration_state_label": registrationLabel(snapshot),
  1199. "flight_mode": modeKnown && (mode == 0 || mode == 4),
  1200. }
  1201. }
  1202. func deviceOverview(entry device.Device) map[string]any {
  1203. result := deviceSummary(entry)
  1204. result["at_port"] = entry.Candidate.ATPort.OpenPath()
  1205. result["audio_device"] = ""
  1206. result["backend_mode"] = backendMode(entry.Candidate)
  1207. result["control_device"] = firstNonEmpty(entry.Candidate.QMIControl, entry.Candidate.ATPort.OpenPath())
  1208. result["radio_live_ok"] = entry.Snapshot != nil && entry.Snapshot.Responsive
  1209. result["traffic"] = map[string]string{}
  1210. result["traffic_raw"] = map[string]int64{}
  1211. result["traffic_meta"] = map[string]any{}
  1212. return result
  1213. }
  1214. func deviceStatus(entry device.Device) map[string]any {
  1215. summary := deviceSummary(entry)
  1216. return map[string]any{
  1217. "id": entry.ID,
  1218. "name": summary["name"],
  1219. "healthy": summary["healthy"],
  1220. "interface": summary["interface"],
  1221. "public_ip": "",
  1222. "proxy_port": 1080,
  1223. "network_connected": false,
  1224. "modem": summary["modem"],
  1225. "vowifi": summary["vowifi_runtime"],
  1226. "sim_service_table": map[string]any{},
  1227. "pnn": []any{},
  1228. "opl": []any{},
  1229. "last_hardware_refresh": entry.LastUpdated,
  1230. }
  1231. }
  1232. func storedDeviceConfig(config store.Device) map[string]any {
  1233. return map[string]any{
  1234. "id": config.ID,
  1235. "name": config.Name,
  1236. "interface": config.Interface,
  1237. "control_device": config.ControlDevice,
  1238. "at_port": config.ATPort,
  1239. "usb_path": config.USBPath,
  1240. "audio_device": config.AudioDevice,
  1241. "modem_imei": config.ModemIMEI,
  1242. "apn": config.APN,
  1243. "proxy_port": config.ProxyPort,
  1244. "baud_rate": config.BaudRate,
  1245. "data_bits": config.DataBits,
  1246. "stop_bits": config.StopBits,
  1247. "parity": config.Parity,
  1248. "device_backend": config.DeviceBackend,
  1249. "esim_transport": config.ESIMTransport,
  1250. "qmi_use_proxy": config.QMIUseProxy,
  1251. "qmi_proxy_path": config.QMIProxyPath,
  1252. "qmi_proxy_executable": config.QMIProxyExecutable,
  1253. "network_enabled": false,
  1254. "sms_enabled": config.SMSEnabled,
  1255. "vowifi_enabled": config.VoWiFiEnabled,
  1256. }
  1257. }
  1258. func fillConfigFromPhysical(config *store.Device, entry device.Device) {
  1259. candidate := entry.Candidate
  1260. if config.Interface == "" {
  1261. config.Interface = candidate.NetworkInterface
  1262. }
  1263. if config.ControlDevice == "" {
  1264. config.ControlDevice = firstNonEmpty(candidate.QMIControl, candidate.ATPort.OpenPath())
  1265. }
  1266. if config.ATPort == "" {
  1267. config.ATPort = candidate.ATPort.OpenPath()
  1268. }
  1269. if config.USBPath == "" {
  1270. config.USBPath = candidate.USBPath
  1271. }
  1272. if config.ModemIMEI == "" && entry.Snapshot != nil {
  1273. config.ModemIMEI = entry.Snapshot.IMEI
  1274. }
  1275. if config.DeviceBackend == "" {
  1276. config.DeviceBackend = backendMode(candidate)
  1277. }
  1278. if config.ESIMTransport == "" {
  1279. config.ESIMTransport = config.DeviceBackend
  1280. }
  1281. }
  1282. func modemSummary(snapshot *device.Snapshot, phone string, phoneSource string) map[string]any {
  1283. if snapshot == nil {
  1284. return map[string]any{
  1285. "operator": "",
  1286. "native_mcc": "",
  1287. "native_mnc": "",
  1288. "card_mcc": "",
  1289. "card_mnc": "",
  1290. "card_country": "",
  1291. "service_blocked": false,
  1292. "blocked_reason": "",
  1293. "network_mode": "",
  1294. "radio_band": "",
  1295. "radio_channel": 0,
  1296. "signal_dbm": 0,
  1297. "signal_sinr": 0,
  1298. "imei": "",
  1299. "iccid": "",
  1300. "reg_status": 0,
  1301. "reg_status_text": "not refreshed",
  1302. "sim_inserted": false,
  1303. "phone_number": phone,
  1304. "phone_number_source": phoneSource,
  1305. "model": "",
  1306. }
  1307. }
  1308. mcc, mnc := splitPLMN(snapshot.OperatorCode)
  1309. cardMCC, cardMNC := device.CardMCCMNC(snapshot.IMSI)
  1310. blockedReason := device.RegionBlockReason(snapshot.IMSI)
  1311. return map[string]any{
  1312. "operator": snapshot.OperatorName,
  1313. "native_mcc": mcc,
  1314. "native_mnc": mnc,
  1315. "card_mcc": cardMCC,
  1316. "card_mnc": cardMNC,
  1317. "card_country": countryNameForMCC(cardMCC),
  1318. "service_blocked": blockedReason != "",
  1319. "blocked_reason": blockedReason,
  1320. "network_mode": snapshot.AccessTech,
  1321. "network_duplex": "",
  1322. "radio_band": snapshot.Band,
  1323. "radio_channel": parseDecimal(snapshot.Channel),
  1324. "signal_dbm": pointerInt(snapshot.RSSIDBm),
  1325. "signal_rsrp": pointerInt(snapshot.RSRP),
  1326. "signal_rsrq": pointerInt(snapshot.RSRQ),
  1327. "signal_sinr": pointerInt(snapshot.SINR),
  1328. "imei": snapshot.IMEI,
  1329. "iccid": snapshot.ICCID,
  1330. "imsi": snapshot.IMSI,
  1331. "firmware": snapshot.Firmware,
  1332. "model": snapshot.Model,
  1333. "reg_status": boolInt(snapshot.OperatorName != ""),
  1334. "reg_status_text": registrationText(snapshot),
  1335. "ps_attached": false,
  1336. "sim_inserted": snapshot.SIMStatus != "",
  1337. "operating_mode": snapshot.OperatingMode,
  1338. "phone_number": phone,
  1339. "phone_number_source": phoneSource,
  1340. }
  1341. }
  1342. func idleVoWiFiRuntime(id string, snapshot *device.Snapshot) map[string]any {
  1343. iccid := ""
  1344. imsi := ""
  1345. phone := ""
  1346. source := ""
  1347. simReady := false
  1348. if snapshot != nil {
  1349. iccid = snapshot.ICCID
  1350. imsi = snapshot.IMSI
  1351. phone = snapshot.Phone.Number
  1352. source = snapshot.Phone.Source
  1353. simReady = snapshot.SIMReady
  1354. }
  1355. return map[string]any{
  1356. "device_id": id,
  1357. "phase": "idle",
  1358. "dataplane_mode": "",
  1359. "iccid": iccid,
  1360. "imsi": imsi,
  1361. "sim_ready": simReady,
  1362. "access_ready": false,
  1363. "tunnel_ready": false,
  1364. "ims_ready": false,
  1365. "sms_ready": false,
  1366. "reg_status": 0,
  1367. "reg_status_text": "not started",
  1368. "network_mode": "",
  1369. "local_phone": phone,
  1370. "phone_number_source": source,
  1371. "last_error_class": "",
  1372. "last_error": "",
  1373. "last_reason": "disabled",
  1374. "updated_at": time.Now().UTC(),
  1375. }
  1376. }
  1377. func deviceName(entry device.Device) string {
  1378. if entry.Snapshot != nil && strings.TrimSpace(entry.Snapshot.Model) != "" {
  1379. return entry.Snapshot.Model
  1380. }
  1381. if strings.TrimSpace(entry.Candidate.Product) != "" {
  1382. return entry.Candidate.Product
  1383. }
  1384. return entry.ID
  1385. }
  1386. func backendMode(candidate modem.Candidate) string {
  1387. if candidate.QMIControl != "" {
  1388. return "qmi"
  1389. }
  1390. return "at"
  1391. }
  1392. func lifecyclePhase(entry device.Device) string {
  1393. switch {
  1394. case !entry.Discovered:
  1395. return "missing"
  1396. case entry.LastError != "":
  1397. return "degraded"
  1398. case entry.Snapshot == nil:
  1399. return "discovered"
  1400. case entry.Snapshot.Responsive:
  1401. return "ready"
  1402. default:
  1403. return "unresponsive"
  1404. }
  1405. }
  1406. func registrationLabel(snapshot *device.Snapshot) string {
  1407. if snapshot == nil || snapshot.OperatorName == "" {
  1408. return "unknown"
  1409. }
  1410. return "registered"
  1411. }
  1412. func registrationText(snapshot *device.Snapshot) string {
  1413. if snapshot.OperatorName != "" {
  1414. return "registered"
  1415. }
  1416. return "unknown"
  1417. }
  1418. func splitPLMN(value string) (string, string) {
  1419. value = strings.TrimSpace(value)
  1420. if len(value) < 5 {
  1421. return "", ""
  1422. }
  1423. return value[:3], value[3:]
  1424. }
  1425. func parseDecimal(value string) int {
  1426. number, _ := strconv.Atoi(strings.TrimSpace(value))
  1427. return number
  1428. }
  1429. func pointerInt(value *int) int {
  1430. if value == nil {
  1431. return 0
  1432. }
  1433. return *value
  1434. }
  1435. func boolInt(value bool) int {
  1436. if value {
  1437. return 1
  1438. }
  1439. return 0
  1440. }
  1441. func snapshotString(
  1442. snapshot *device.Snapshot,
  1443. selector func(*device.Snapshot) string,
  1444. ) string {
  1445. if snapshot == nil {
  1446. return ""
  1447. }
  1448. return selector(snapshot)
  1449. }
  1450. func firstNonEmpty(values ...string) string {
  1451. for _, value := range values {
  1452. if strings.TrimSpace(value) != "" {
  1453. return value
  1454. }
  1455. }
  1456. return ""
  1457. }