@ceralive/modem-control 1.2.1 → 1.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +79 -0
- package/dist/backend/cell-info.d.ts +2 -0
- package/dist/backend/cell-info.js +18 -2
- package/dist/backend/device-classifier.d.ts +56 -0
- package/dist/backend/device-classifier.js +88 -0
- package/dist/backend/mm-mutations/bands.d.ts +4 -0
- package/dist/backend/mm-mutations/bands.js +49 -0
- package/dist/backend/mm-mutations/context.d.ts +10 -0
- package/dist/backend/mm-mutations/context.js +3 -0
- package/dist/backend/mm-mutations/inhibit.d.ts +4 -0
- package/dist/backend/mm-mutations/inhibit.js +19 -0
- package/dist/backend/mm-mutations/modes.d.ts +5 -0
- package/dist/backend/mm-mutations/modes.js +44 -0
- package/dist/backend/mm-mutations/scan.d.ts +3 -0
- package/dist/backend/mm-mutations/scan.js +46 -0
- package/dist/backend/mm-mutations/sim.d.ts +5 -0
- package/dist/backend/mm-mutations/sim.js +49 -0
- package/dist/backend/mm-mutations.d.ts +0 -16
- package/dist/backend/mm-mutations.js +27 -234
- package/dist/backend/observer/bus-lifecycle.d.ts +7 -0
- package/dist/backend/observer/bus-lifecycle.js +12 -0
- package/dist/backend/observer/epoch-reconcile.d.ts +4 -0
- package/dist/backend/observer/epoch-reconcile.js +28 -0
- package/dist/backend/observer/signal-routing.d.ts +11 -0
- package/dist/backend/observer/signal-routing.js +11 -0
- package/dist/backend/observer.d.ts +1 -1
- package/dist/backend/observer.js +18 -35
- package/dist/backend/router-ethernet.d.ts +7 -0
- package/dist/backend/router-ethernet.js +70 -18
- package/dist/backend/signal-setup.d.ts +21 -2
- package/dist/backend/signal-setup.js +24 -2
- package/dist/backend/usage/accounting.d.ts +7 -0
- package/dist/backend/usage/accounting.js +8 -2
- package/dist/backend/usage/persistence.d.ts +4 -0
- package/dist/backend/usage/persistence.js +59 -0
- package/dist/backend/usage/policy.d.ts +24 -0
- package/dist/backend/usage/policy.js +38 -0
- package/dist/backend/usage/sampler.d.ts +7 -1
- package/dist/backend/usage/sampler.js +33 -119
- package/dist/backend/usage/sampling.d.ts +38 -0
- package/dist/backend/usage/sampling.js +72 -0
- package/dist/backend/usb-mode-transition/admission.d.ts +5 -0
- package/dist/backend/usb-mode-transition/admission.js +12 -0
- package/dist/backend/usb-mode-transition/at.d.ts +13 -0
- package/dist/backend/usb-mode-transition/at.js +15 -0
- package/dist/backend/usb-mode-transition/outcome.d.ts +4 -0
- package/dist/backend/usb-mode-transition/outcome.js +17 -0
- package/dist/backend/usb-mode-transition/reenumeration.d.ts +8 -0
- package/dist/backend/usb-mode-transition/reenumeration.js +37 -0
- package/dist/backend/usb-mode-transition.d.ts +2 -2
- package/dist/backend/usb-mode-transition.js +25 -71
- package/dist/domain/mm-enums.d.ts +37 -0
- package/dist/domain/mm-enums.js +34 -0
- package/dist/fcc/coverage.d.ts +7 -0
- package/dist/fcc/coverage.js +7 -0
- package/dist/hardware/router-parsers.js +3 -32
- package/dist/json-boundary.d.ts +4 -0
- package/dist/json-boundary.js +19 -0
- package/dist/observations/model.d.ts +45 -0
- package/dist/observations/raw.d.ts +12 -0
- package/dist/observations/raw.js +31 -0
- package/dist/observations/sources/hilink.js +7 -1
- package/dist/observations/sources/modemmanager.d.ts +25 -0
- package/dist/observations/sources/modemmanager.js +123 -15
- package/dist/observations/sources/router-shared.d.ts +35 -1
- package/dist/observations/sources/router-shared.js +39 -0
- package/dist/observations/sources/ufi.js +7 -1
- package/dist/observations/sources/zte.js +7 -1
- package/dist/providers/modem-manager/provider.d.ts +10 -0
- package/dist/providers/modem-manager/provider.js +3 -0
- package/dist/providers/modem-manager/snapshot.js +6 -0
- package/dist/providers/network-manager/adapter.js +15 -136
- package/dist/providers/network-manager/divergence.d.ts +4 -0
- package/dist/providers/network-manager/divergence.js +7 -0
- package/dist/providers/network-manager/observe-fold.d.ts +9 -0
- package/dist/providers/network-manager/observe-fold.js +109 -0
- package/dist/providers/network-manager/projection.d.ts +5 -0
- package/dist/providers/network-manager/projection.js +14 -0
- package/dist/providers/network-manager/state.d.ts +10 -0
- package/dist/providers/network-manager/state.js +16 -0
- package/dist/providers/ufi-himi/session.js +2 -16
- package/dist/providers/zte-goform/session.js +2 -16
- package/dist/safety/flock-resource-ownership.js +73 -13
- package/dist/transport/transport.d.ts +20 -1
- package/dist/transport/transport.js +133 -16
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -199,6 +199,85 @@ are, so the preferred mode and the measurement-recency flag survive normalizatio
|
|
|
199
199
|
`NormalizedSignal.qualityRecent` claims that flag, and the router sources answer
|
|
200
200
|
`unsupported` for it.
|
|
201
201
|
|
|
202
|
+
### Extended signal — the `Modem.Signal` per-RAT dicts
|
|
203
|
+
|
|
204
|
+
`rsrp`, `rsrq`, `snr` and `sinr` are claimed for MM-managed modems from the
|
|
205
|
+
`Modem.Signal` interface's own `a{sv}` properties, so a ModemManager device reports the
|
|
206
|
+
same detail a Huawei or ZTE dongle always did. `rsrp` / `rsrq` / `snr` read `Nr5g` first
|
|
207
|
+
and `Lte` second — on a 5G NSA attach both are populated with different measurements, so
|
|
208
|
+
`MetricProvenance.rawFields` names the dict that answered (`Signal.Nr5g.rsrp`) rather than
|
|
209
|
+
merging the two. `dbm` reads `rssi` across `Lte → Umts → Gsm → Evdo → Cdma`.
|
|
210
|
+
|
|
211
|
+
`sinr` comes from the `Evdo` dict and from no other, because that is the only dict
|
|
212
|
+
ModemManager defines it on; `Lte` and `Nr5g` publish `snr`, a different quantity that
|
|
213
|
+
never populates it. An LTE/5G modem reporting no SINR therefore answers `not-reported` —
|
|
214
|
+
a claim about this read — and not `unsupported`, which would be a false claim about the
|
|
215
|
+
source. An exported-but-silent `Modem.Signal` yields `not-reported` for every extended
|
|
216
|
+
metric and a modem without the interface yields `not-observed`; neither ever yields a
|
|
217
|
+
zero. Every member the normalized model has no slot for (`error-rate`, `ecio`, `io`,
|
|
218
|
+
`rscp`) stays verbatim in the diagnostics block.
|
|
219
|
+
|
|
220
|
+
The `Signal.Setup` reporting rate is injectable — `signalIntervalSeconds` on
|
|
221
|
+
`createModemManagerProvider`, on `createMmDbusBackend`, or `intervalSeconds` on
|
|
222
|
+
`SignalSetupManager` — and defaults to `DEFAULT_SIGNAL_INTERVAL_SECONDS` (5 seconds) at
|
|
223
|
+
each. `Setup` takes an unsigned integer, so a fractional or non-positive rate is refused
|
|
224
|
+
at construction rather than marshalled.
|
|
225
|
+
|
|
226
|
+
### Registration and cell context
|
|
227
|
+
|
|
228
|
+
`NormalizedRadio` carries `operatorName` and `operatorCode`, and every observation carries
|
|
229
|
+
an additive `cell` block (`cellId` + `tac`). Both operator fields come from
|
|
230
|
+
`Modem3gpp` — the operator the modem is **registered with** — and never from
|
|
231
|
+
`Sim.OperatorName`, which is the *home* operator written into the SIM and differs for the
|
|
232
|
+
whole time a device is roaming. `operatorCode` stays text: the MNC is two or three digits
|
|
233
|
+
and the width is significant, so `31001` and `310001` are different networks.
|
|
234
|
+
|
|
235
|
+
`cellId` and `tac` come from the existing `3gpp-lac-ci` location source, whose value is one
|
|
236
|
+
five-token string (`MCC,MNC,LAC,CI,TAC`, the last three in uppercase hex). Both fields are
|
|
237
|
+
read out of that single value, so they always describe the same reported cell; a value in
|
|
238
|
+
any other shape reads `malformed` for both rather than being partially decoded, and the hex
|
|
239
|
+
is kept as text so an identifier matches what `mmcli` shows. This is **coarse cell context,
|
|
240
|
+
not a GNSS fix** — it carries no coordinate, `3gpp-lac-ci` is not a member of
|
|
241
|
+
`GNSS_SOURCES`, and nothing on this path enables a location source or sets
|
|
242
|
+
`Location.Setup`'s `signal_location`.
|
|
243
|
+
|
|
244
|
+
Because ModemManager masks the `Location` property unless `signal_location` is true — which
|
|
245
|
+
this package never sets — nothing populates the optional `location` input today, and an MM
|
|
246
|
+
observation honestly reads `not-observed` for `cell`. Cell identity that is wired comes from
|
|
247
|
+
`Modem.GetCellInfo`, where `CellReading` now also reports `tac` and reads ModemManager's real
|
|
248
|
+
`ci` key (with the older `cell-id` spelling kept as a fallback).
|
|
249
|
+
|
|
250
|
+
**No EARFCN is claimed.** ModemManager exposes no generic ARFCN; the only occurrences are
|
|
251
|
+
inside per-cell `GetCellInfo` dicts under two different keys for two different quantities —
|
|
252
|
+
`earfcn` for LTE and `nrarfcn` for 5GNR. One normalized field would have to merge them or
|
|
253
|
+
pick a RAT, so neither `NormalizedCell` nor `CellReading` claims one and the raw keys stay
|
|
254
|
+
available.
|
|
255
|
+
|
|
256
|
+
Router sources claim only what their migrated parsers already decode: ZTE reports the
|
|
257
|
+
operator name and its cell id, UFI reports its cell id, HiLink reports neither. No router
|
|
258
|
+
source derives an operator code from separate unpadded MCC/MNC fields, and `tac` reads
|
|
259
|
+
`not-reported` rather than `unsupported` — nothing here decodes one, which says nothing
|
|
260
|
+
about what the firmware could report.
|
|
261
|
+
|
|
262
|
+
### Data-usage throughput — absent is not zero
|
|
263
|
+
|
|
264
|
+
`SlotUsageSnapshot.rateBytesPerSecond` reports throughput over the last **measured**
|
|
265
|
+
sampling interval, and the key is **omitted** rather than set to `0` whenever there was no
|
|
266
|
+
interval to measure. That happens on a first sample, a rebaseline, a paused (ambiguous
|
|
267
|
+
identity) slot, a remap or reboot, an interface missing from `/proc/net/dev`, a clock that
|
|
268
|
+
did not advance — and, most importantly, on a **counter reset**.
|
|
269
|
+
|
|
270
|
+
Interface counters restart at zero when the interface is re-created. Clamping the resulting
|
|
271
|
+
negative delta to zero would show an idle link that was in fact carrying traffic, and using
|
|
272
|
+
the raw post-reset value would show every byte since the interface came up as if it moved
|
|
273
|
+
inside one interval. So a backwards counter reports **no rate at all**, and the baseline is
|
|
274
|
+
rebased in the same pass so the *next* interval is measured correctly rather than inheriting
|
|
275
|
+
the gap.
|
|
276
|
+
|
|
277
|
+
Rates are never persisted. A same-boot reload resumes the counter baseline — a cumulative
|
|
278
|
+
total is still true after a restart — but the rate restarts unmeasured, because this process
|
|
279
|
+
did not observe the start of that interval.
|
|
280
|
+
|
|
202
281
|
### Mutation safety ports
|
|
203
282
|
|
|
204
283
|
The root export includes `MutationAdmissionPort`, `ResourceOwnershipPort`,
|
|
@@ -12,6 +12,8 @@ export interface CellReading {
|
|
|
12
12
|
readonly serving: boolean;
|
|
13
13
|
/** The cell identifier, when supplied (serving-cell tiebreak key). */
|
|
14
14
|
readonly cellId?: string;
|
|
15
|
+
/** Tracking-area code — from the `tac` key ONLY, never derived from the cell id. */
|
|
16
|
+
readonly tac?: string;
|
|
15
17
|
/** Physical cell id — from the `physical-ci` key ONLY. */
|
|
16
18
|
readonly pci?: number;
|
|
17
19
|
readonly rsrp?: number;
|
|
@@ -8,11 +8,24 @@
|
|
|
8
8
|
// - `rsrp`, `rsrq` pass through as numbers
|
|
9
9
|
// - `sinr` the REAL NR SINR key. A dict carrying `snr` (the
|
|
10
10
|
// WRONG name) is IGNORED — `sinr` stays undefined.
|
|
11
|
-
// - `cell-id`
|
|
11
|
+
// - `ci`, else `cell-id` the cell identifier (serving-cell tiebreak)
|
|
12
|
+
// - `tac` the tracking-area code, when the RAT reports one
|
|
12
13
|
// - `band` surfaced ONLY when the source supplies it directly;
|
|
13
14
|
// never inferred from earfcn / frequency / anything.
|
|
14
15
|
// - `serving` / `cell-type` whether this is the serving cell.
|
|
15
16
|
//
|
|
17
|
+
// `ci` IS THE REAL MM KEY and `cell-id` is the fallback, not the other way round.
|
|
18
|
+
// ModemManager 1.24.2 spells it `PROPERTY_CI "ci"` in both `libmm-glib/mm-cell-info-lte.c`
|
|
19
|
+
// and `mm-cell-info-nr5g.c`; `cell-id` is kept behind it so a source that already
|
|
20
|
+
// flattened to that older spelling still decodes rather than silently losing its id.
|
|
21
|
+
//
|
|
22
|
+
// NO ARFCN IS READ HERE, and that is deliberate rather than pending. MM does publish
|
|
23
|
+
// one — but under TWO different keys for two different quantities: `earfcn` in an LTE
|
|
24
|
+
// cell dict and `nrarfcn` in a 5GNR one. There is no generic ARFCN property anywhere on
|
|
25
|
+
// the `Modem`, `Modem3gpp` or `Location` interfaces, so a single `earfcn` field on this
|
|
26
|
+
// reading would have to either merge the two or quietly pick a RAT. `CellReading` makes
|
|
27
|
+
// no ARFCN claim instead, and an unread key stays available to a caller that needs it.
|
|
28
|
+
//
|
|
16
29
|
// Every reading also carries `source` + `observedAt` provenance, so a consumer can
|
|
17
30
|
// tell a fresh reading from a cached one and know where it came from. Pure — no I/O.
|
|
18
31
|
import { numberProp, stringProp } from './managed-objects.js';
|
|
@@ -27,7 +40,8 @@ function readServing(cell) {
|
|
|
27
40
|
}
|
|
28
41
|
/** Normalize ONE cell's `a{sv}` dict into a `CellReading`, pinning the known keys. */
|
|
29
42
|
export function normalizeCellReading(cell, provenance) {
|
|
30
|
-
const cellId = stringProp(cell, 'cell-id');
|
|
43
|
+
const cellId = stringProp(cell, 'ci') ?? stringProp(cell, 'cell-id');
|
|
44
|
+
const tac = stringProp(cell, 'tac');
|
|
31
45
|
const pci = numberProp(cell, 'physical-ci');
|
|
32
46
|
const rsrp = numberProp(cell, 'rsrp');
|
|
33
47
|
const rsrq = numberProp(cell, 'rsrq');
|
|
@@ -38,6 +52,7 @@ export function normalizeCellReading(cell, provenance) {
|
|
|
38
52
|
return {
|
|
39
53
|
serving: readServing(cell),
|
|
40
54
|
...(cellId !== undefined ? { cellId } : {}),
|
|
55
|
+
...(tac !== undefined ? { tac } : {}),
|
|
41
56
|
...(pci !== undefined ? { pci } : {}),
|
|
42
57
|
...(rsrp !== undefined ? { rsrp } : {}),
|
|
43
58
|
...(rsrq !== undefined ? { rsrq } : {}),
|
|
@@ -105,6 +120,7 @@ function stableTag(reading) {
|
|
|
105
120
|
return JSON.stringify([
|
|
106
121
|
reading.serving,
|
|
107
122
|
reading.cellId ?? null,
|
|
123
|
+
reading.tac ?? null,
|
|
108
124
|
reading.pci ?? null,
|
|
109
125
|
reading.rsrp ?? null,
|
|
110
126
|
reading.rsrq ?? null,
|
|
@@ -13,6 +13,61 @@ export interface UsbNetClassification {
|
|
|
13
13
|
readonly deviceClass: UsbNetClass;
|
|
14
14
|
readonly reason: string;
|
|
15
15
|
}
|
|
16
|
+
export type CellularModelEvidenceTier = 'modemmanager-1.24.2-fcc' | 'mainline-kernel' | 'usb-ids-registry';
|
|
17
|
+
/**
|
|
18
|
+
* A POSITIVE, sourced claim that a labelled family is a router appliance whose only
|
|
19
|
+
* control surface is a vendor web UI — no ModemManager plugin, no kernel modem driver
|
|
20
|
+
* claiming its application PID. ABSENCE OF THIS FIELD IS NOT A CLAIM: it does not say
|
|
21
|
+
* a family is a modem module, only that nothing here asserts otherwise. Same tri-state
|
|
22
|
+
* discipline as `fcc/coverage.ts` — `unknown` is never folded into `absent`.
|
|
23
|
+
*
|
|
24
|
+
* It also does not, and must not, decide a DEVICE CLASS. `classifyDevice` reads live
|
|
25
|
+
* interfaces and bound drivers; this label is model-family evidence sitting beside it.
|
|
26
|
+
*/
|
|
27
|
+
export type CellularModelFamilyKind = 'router-webui';
|
|
28
|
+
export interface CellularModelEvidence {
|
|
29
|
+
readonly vendor: string;
|
|
30
|
+
readonly family: string;
|
|
31
|
+
readonly evidenceTier: CellularModelEvidenceTier;
|
|
32
|
+
readonly familyKind?: CellularModelFamilyKind;
|
|
33
|
+
}
|
|
34
|
+
/** Where each evidence tier's rows were read from, so a reviewer can re-check them. */
|
|
35
|
+
export declare const CELLULAR_MODEL_EVIDENCE_SOURCES: {
|
|
36
|
+
readonly 'modemmanager-1.24.2-fcc': {
|
|
37
|
+
readonly source: 'ModemManager 1.24.2 data/dispatcher-fcc-unlock/meson.build';
|
|
38
|
+
readonly pin: 'f2b9ab1ad78d322f32134a444b5b54c6e8160e19';
|
|
39
|
+
};
|
|
40
|
+
readonly 'mainline-kernel': {
|
|
41
|
+
readonly source: 'Linux drivers/net/usb/qmi_wwan.c + drivers/usb/serial/option.c';
|
|
42
|
+
readonly pin: '45c13f3f9e3bb15fd89ff2864c6f627a3b4b4229';
|
|
43
|
+
};
|
|
44
|
+
readonly 'usb-ids-registry': {
|
|
45
|
+
readonly source: 'The USB ID Repository (linux-usb.org/usb.ids)';
|
|
46
|
+
readonly pin: '2026.06.26';
|
|
47
|
+
};
|
|
48
|
+
};
|
|
49
|
+
/**
|
|
50
|
+
* Exact model-family evidence. This table may label a known VID:PID, but it never
|
|
51
|
+
* decides whether the device is MM-managed: live interface/driver evidence below
|
|
52
|
+
* remains authoritative. The pinned ModemManager FCC map is the strongest tier;
|
|
53
|
+
* `mainline-kernel` rows are application-mode PIDs named by Linux's own qmi_wwan /
|
|
54
|
+
* qcserial / option device tables; `usb-ids-registry` is the weakest and is used only
|
|
55
|
+
* where NO kernel modem driver claims the id at all — which is itself the evidence
|
|
56
|
+
* that the device is a router appliance rather than a controllable module.
|
|
57
|
+
*
|
|
58
|
+
* EVERY ROW IS AN EXACT VID:PID. A vendor range is never inferred, and that is not a
|
|
59
|
+
* stylistic preference: NETGEAR's `0846` and u-blox's `1546` both carry non-cellular
|
|
60
|
+
* products (Wi-Fi/Ethernet adapters and GNSS receivers respectively), so a
|
|
61
|
+
* vendor-keyed rule on either would label hardware that has no radio.
|
|
62
|
+
*/
|
|
63
|
+
export declare const CELLULAR_USB_MODEL_ROWS: ReadonlyMap<string, CellularModelEvidence>;
|
|
64
|
+
/**
|
|
65
|
+
* Vendor ids whose ENTIRE USB range is cellular, so the vendor id alone is honest
|
|
66
|
+
* evidence of a radio. Membership is a claim about the whole range, which is why
|
|
67
|
+
* NETGEAR's `0846` is deliberately ABSENT despite having a row above: the USB ID
|
|
68
|
+
* Repository lists that vendor's Wi-Fi and Ethernet adapters under it, so a
|
|
69
|
+
* vendor-keyed rule there would report a Wi-Fi dongle as a cellular uplink.
|
|
70
|
+
*/
|
|
16
71
|
export declare const CELLULAR_USB_VENDOR_IDS: ReadonlyMap<string, string>;
|
|
17
72
|
/**
|
|
18
73
|
* Classify one USB device from its udev/sysfs snapshot. Precedence:
|
|
@@ -23,6 +78,7 @@ export declare const CELLULAR_USB_VENDOR_IDS: ReadonlyMap<string, string>;
|
|
|
23
78
|
*/
|
|
24
79
|
export declare function classifyDevice(snapshot: UsbDeviceSnapshot): DeviceClassification;
|
|
25
80
|
export declare function cellularVendorName(vendorId: string): string | undefined;
|
|
81
|
+
export declare function cellularModelEvidence(device: Pick<UsbDeviceSnapshot, 'vendorId' | 'productId'>): CellularModelEvidence | undefined;
|
|
26
82
|
export declare function cellularEvidence(device: UsbDeviceSnapshot): string | undefined;
|
|
27
83
|
export declare function classifyUsbNetDevice(device: UsbDeviceSnapshot): UsbNetClassification;
|
|
28
84
|
export declare function publishesGenericIdentity(device: UsbDeviceSnapshot): boolean;
|
|
@@ -35,6 +35,90 @@ const AT_DRIVERS = new Set(['option', 'qcserial', 'cdc_acm']);
|
|
|
35
35
|
const ECM_NCM_DRIVERS = new Set(['cdc_ether', 'cdc_ncm']);
|
|
36
36
|
const RNDIS_DRIVERS = new Set(['rndis_host']);
|
|
37
37
|
const STORAGE_DRIVERS = new Set(['usb-storage', 'uas']);
|
|
38
|
+
/** Where each evidence tier's rows were read from, so a reviewer can re-check them. */
|
|
39
|
+
export const CELLULAR_MODEL_EVIDENCE_SOURCES = {
|
|
40
|
+
'modemmanager-1.24.2-fcc': {
|
|
41
|
+
source: 'ModemManager 1.24.2 data/dispatcher-fcc-unlock/meson.build',
|
|
42
|
+
pin: 'f2b9ab1ad78d322f32134a444b5b54c6e8160e19',
|
|
43
|
+
},
|
|
44
|
+
'mainline-kernel': {
|
|
45
|
+
source: 'Linux drivers/net/usb/qmi_wwan.c + drivers/usb/serial/option.c',
|
|
46
|
+
pin: '45c13f3f9e3bb15fd89ff2864c6f627a3b4b4229',
|
|
47
|
+
},
|
|
48
|
+
'usb-ids-registry': {
|
|
49
|
+
source: 'The USB ID Repository (linux-usb.org/usb.ids)',
|
|
50
|
+
pin: '2026.06.26',
|
|
51
|
+
},
|
|
52
|
+
};
|
|
53
|
+
/**
|
|
54
|
+
* Exact model-family evidence. This table may label a known VID:PID, but it never
|
|
55
|
+
* decides whether the device is MM-managed: live interface/driver evidence below
|
|
56
|
+
* remains authoritative. The pinned ModemManager FCC map is the strongest tier;
|
|
57
|
+
* `mainline-kernel` rows are application-mode PIDs named by Linux's own qmi_wwan /
|
|
58
|
+
* qcserial / option device tables; `usb-ids-registry` is the weakest and is used only
|
|
59
|
+
* where NO kernel modem driver claims the id at all — which is itself the evidence
|
|
60
|
+
* that the device is a router appliance rather than a controllable module.
|
|
61
|
+
*
|
|
62
|
+
* EVERY ROW IS AN EXACT VID:PID. A vendor range is never inferred, and that is not a
|
|
63
|
+
* stylistic preference: NETGEAR's `0846` and u-blox's `1546` both carry non-cellular
|
|
64
|
+
* products (Wi-Fi/Ethernet adapters and GNSS receivers respectively), so a
|
|
65
|
+
* vendor-keyed rule on either would label hardware that has no radio.
|
|
66
|
+
*/
|
|
67
|
+
export const CELLULAR_USB_MODEL_ROWS = new Map([
|
|
68
|
+
[
|
|
69
|
+
'03f0:4e1d',
|
|
70
|
+
{ vendor: 'Sierra Wireless', family: 'EM74xx', evidenceTier: 'modemmanager-1.24.2-fcc' },
|
|
71
|
+
],
|
|
72
|
+
['1199:9071', { vendor: 'Sierra Wireless', family: 'EM74xx', evidenceTier: 'mainline-kernel' }],
|
|
73
|
+
[
|
|
74
|
+
'1199:9079',
|
|
75
|
+
{ vendor: 'Sierra Wireless', family: 'EM74xx', evidenceTier: 'modemmanager-1.24.2-fcc' },
|
|
76
|
+
],
|
|
77
|
+
['1199:907b', { vendor: 'Sierra Wireless', family: 'EM74xx', evidenceTier: 'mainline-kernel' }],
|
|
78
|
+
['1199:9091', { vendor: 'Sierra Wireless', family: 'EM75xx', evidenceTier: 'mainline-kernel' }],
|
|
79
|
+
['1199:90d3', { vendor: 'Sierra Wireless', family: 'EM919x', evidenceTier: 'mainline-kernel' }],
|
|
80
|
+
['1199:c081', { vendor: 'Sierra Wireless', family: 'EM75xx', evidenceTier: 'mainline-kernel' }],
|
|
81
|
+
[
|
|
82
|
+
'413c:81a3',
|
|
83
|
+
{ vendor: 'Sierra Wireless', family: 'EM74xx', evidenceTier: 'modemmanager-1.24.2-fcc' },
|
|
84
|
+
],
|
|
85
|
+
[
|
|
86
|
+
'413c:81a8',
|
|
87
|
+
{ vendor: 'Sierra Wireless', family: 'EM74xx', evidenceTier: 'modemmanager-1.24.2-fcc' },
|
|
88
|
+
],
|
|
89
|
+
['1bc7:1031', { vendor: 'Telit', family: 'LE910C1-EUX', evidenceTier: 'mainline-kernel' }],
|
|
90
|
+
['1bc7:1034', { vendor: 'Telit', family: 'LE910C4-WWX', evidenceTier: 'mainline-kernel' }],
|
|
91
|
+
['1bc7:1040', { vendor: 'Telit', family: 'LE922A', evidenceTier: 'mainline-kernel' }],
|
|
92
|
+
['1bc7:1050', { vendor: 'Telit', family: 'FN980', evidenceTier: 'mainline-kernel' }],
|
|
93
|
+
['1bc7:1060', { vendor: 'Telit', family: 'LN920', evidenceTier: 'mainline-kernel' }],
|
|
94
|
+
['1bc7:1070', { vendor: 'Telit', family: 'FN990A', evidenceTier: 'mainline-kernel' }],
|
|
95
|
+
['1bc7:1080', { vendor: 'Telit', family: 'FE990A', evidenceTier: 'mainline-kernel' }],
|
|
96
|
+
['1bc7:10a0', { vendor: 'Telit', family: 'FN920C04', evidenceTier: 'mainline-kernel' }],
|
|
97
|
+
['1bc7:1100', { vendor: 'Telit', family: 'ME910', evidenceTier: 'mainline-kernel' }],
|
|
98
|
+
['1bc7:1200', { vendor: 'Telit', family: 'LE920', evidenceTier: 'mainline-kernel' }],
|
|
99
|
+
['1546:1311', { vendor: 'u-blox', family: 'LARA-R6', evidenceTier: 'mainline-kernel' }],
|
|
100
|
+
['1546:1312', { vendor: 'u-blox', family: 'LARA-R6', evidenceTier: 'mainline-kernel' }],
|
|
101
|
+
['1546:1313', { vendor: 'u-blox', family: 'LARA-R6', evidenceTier: 'mainline-kernel' }],
|
|
102
|
+
['1546:1341', { vendor: 'u-blox', family: 'LARA-L6', evidenceTier: 'mainline-kernel' }],
|
|
103
|
+
['1546:1342', { vendor: 'u-blox', family: 'LARA-L6', evidenceTier: 'mainline-kernel' }],
|
|
104
|
+
['1546:1343', { vendor: 'u-blox', family: 'LARA-L6', evidenceTier: 'mainline-kernel' }],
|
|
105
|
+
[
|
|
106
|
+
'0846:68e1',
|
|
107
|
+
{
|
|
108
|
+
vendor: 'NETGEAR',
|
|
109
|
+
family: 'LB1120',
|
|
110
|
+
evidenceTier: 'usb-ids-registry',
|
|
111
|
+
familyKind: 'router-webui',
|
|
112
|
+
},
|
|
113
|
+
],
|
|
114
|
+
]);
|
|
115
|
+
/**
|
|
116
|
+
* Vendor ids whose ENTIRE USB range is cellular, so the vendor id alone is honest
|
|
117
|
+
* evidence of a radio. Membership is a claim about the whole range, which is why
|
|
118
|
+
* NETGEAR's `0846` is deliberately ABSENT despite having a row above: the USB ID
|
|
119
|
+
* Repository lists that vendor's Wi-Fi and Ethernet adapters under it, so a
|
|
120
|
+
* vendor-keyed rule there would report a Wi-Fi dongle as a cellular uplink.
|
|
121
|
+
*/
|
|
38
122
|
export const CELLULAR_USB_VENDOR_IDS = new Map([
|
|
39
123
|
['05c6', 'Qualcomm'],
|
|
40
124
|
['0af0', 'Option'],
|
|
@@ -43,6 +127,7 @@ export const CELLULAR_USB_VENDOR_IDS = new Map([
|
|
|
43
127
|
['1546', 'u-blox'],
|
|
44
128
|
['19d2', 'ZTE'],
|
|
45
129
|
['1bbb', 'TCL/Alcatel'],
|
|
130
|
+
['1bc7', 'Telit'],
|
|
46
131
|
['1c9e', 'Longcheer'],
|
|
47
132
|
['1e0e', 'SIMCom'],
|
|
48
133
|
['2c7c', 'Quectel'],
|
|
@@ -154,6 +239,9 @@ export function classifyDevice(snapshot) {
|
|
|
154
239
|
export function cellularVendorName(vendorId) {
|
|
155
240
|
return CELLULAR_USB_VENDOR_IDS.get(vendorId.toLowerCase());
|
|
156
241
|
}
|
|
242
|
+
export function cellularModelEvidence(device) {
|
|
243
|
+
return CELLULAR_USB_MODEL_ROWS.get(`${device.vendorId}:${device.productId}`.toLowerCase());
|
|
244
|
+
}
|
|
157
245
|
export function cellularEvidence(device) {
|
|
158
246
|
const vendor = cellularVendorName(device.vendorId);
|
|
159
247
|
if (vendor !== undefined)
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import type { BandReadResult, ModemRef, Receipt } from '../../ports/index.js';
|
|
2
|
+
import type { MmMutationContext } from './context.js';
|
|
3
|
+
export declare function readBands(context: MmMutationContext, modem: ModemRef): Promise<BandReadResult>;
|
|
4
|
+
export declare function setCurrentBands(context: MmMutationContext, modem: ModemRef, bands: readonly string[]): Promise<Receipt>;
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { decodeBandList, encodeBandList, isResetSelection } from '../../band/index.js';
|
|
2
|
+
import { receipt } from '../../ports/index.js';
|
|
3
|
+
import { MODEM_IFACE } from '../constants.js';
|
|
4
|
+
import { fetchManagedObjects, findInterface, propValue } from '../managed-objects.js';
|
|
5
|
+
import { describeMutationError } from './context.js';
|
|
6
|
+
export async function readBands(context, modem) {
|
|
7
|
+
try {
|
|
8
|
+
const tree = await fetchManagedObjects(context.transport, context.destination);
|
|
9
|
+
const props = findInterface(tree, modem, MODEM_IFACE);
|
|
10
|
+
if (props === undefined) {
|
|
11
|
+
return { ok: false, reason: 'the modem exports no Modem interface' };
|
|
12
|
+
}
|
|
13
|
+
return {
|
|
14
|
+
ok: true,
|
|
15
|
+
bands: {
|
|
16
|
+
supported: decodeBandList(propValue(props, 'SupportedBands')),
|
|
17
|
+
current: decodeBandList(propValue(props, 'CurrentBands')),
|
|
18
|
+
},
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
catch (error) {
|
|
22
|
+
return { ok: false, reason: `reading bands failed: ${describeMutationError(error)}` };
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
export function setCurrentBands(context, modem, bands) {
|
|
26
|
+
if (bands.length === 0) {
|
|
27
|
+
return Promise.resolve(receipt('band', 'failed', 'no bands were requested'));
|
|
28
|
+
}
|
|
29
|
+
const encoded = encodeBandList(bands);
|
|
30
|
+
if (!encoded.ok) {
|
|
31
|
+
return Promise.resolve(receipt('band', 'unsupported', `this build does not know the band "${encoded.unknown}"`));
|
|
32
|
+
}
|
|
33
|
+
return context.actor.runQuiesced({ stableKey: context.resolveStableKey(modem) }, async () => {
|
|
34
|
+
try {
|
|
35
|
+
await context.transport.callMethod({
|
|
36
|
+
destination: context.destination,
|
|
37
|
+
path: modem,
|
|
38
|
+
interface: MODEM_IFACE,
|
|
39
|
+
member: 'SetCurrentBands',
|
|
40
|
+
signature: 'au',
|
|
41
|
+
args: [encoded.values],
|
|
42
|
+
});
|
|
43
|
+
return receipt('band', 'applied', isResetSelection(bands) ? 'band lock released' : `bands set to ${bands.join(', ')}`);
|
|
44
|
+
}
|
|
45
|
+
catch (error) {
|
|
46
|
+
return receipt('band', 'failed', `SetCurrentBands failed: ${describeMutationError(error)}`);
|
|
47
|
+
}
|
|
48
|
+
});
|
|
49
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { ModemRef } from '../../ports/index.js';
|
|
2
|
+
import type { DbusTransport } from '../../transport/index.js';
|
|
3
|
+
import type { ModemActor } from '../modem-actor.js';
|
|
4
|
+
export interface MmMutationContext {
|
|
5
|
+
readonly transport: DbusTransport;
|
|
6
|
+
readonly actor: ModemActor;
|
|
7
|
+
readonly destination: string;
|
|
8
|
+
readonly resolveStableKey: (modem: ModemRef) => string;
|
|
9
|
+
}
|
|
10
|
+
export declare function describeMutationError(error: unknown): string;
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import type { InhibitLease } from '../../ports/index.js';
|
|
2
|
+
import type { MmMutationContext } from './context.js';
|
|
3
|
+
export declare function inhibitDevice(context: MmMutationContext, uid: string, now: () => number): Promise<InhibitLease>;
|
|
4
|
+
export declare function uninhibitDevice(context: MmMutationContext, lease: InhibitLease): Promise<void>;
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { epochMillis } from '../../domain/index.js';
|
|
2
|
+
import { MM_MANAGER_IFACE, MM_ROOT_PATH } from '../constants.js';
|
|
3
|
+
export async function inhibitDevice(context, uid, now) {
|
|
4
|
+
await setInhibited(context, uid, true);
|
|
5
|
+
return { uid, acquiredAt: epochMillis(now()) };
|
|
6
|
+
}
|
|
7
|
+
export async function uninhibitDevice(context, lease) {
|
|
8
|
+
await setInhibited(context, lease.uid, false);
|
|
9
|
+
}
|
|
10
|
+
function setInhibited(context, uid, inhibit) {
|
|
11
|
+
return context.transport.callMethod({
|
|
12
|
+
destination: context.destination,
|
|
13
|
+
path: MM_ROOT_PATH,
|
|
14
|
+
interface: MM_MANAGER_IFACE,
|
|
15
|
+
member: 'InhibitDevice',
|
|
16
|
+
signature: 'sb',
|
|
17
|
+
args: [uid, inhibit],
|
|
18
|
+
});
|
|
19
|
+
}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import type { DesiredRadio } from '../../domain/index.js';
|
|
2
|
+
import type { ModemRef, Receipt } from '../../ports/index.js';
|
|
3
|
+
import type { MmMutationContext } from './context.js';
|
|
4
|
+
export declare function setRadioModes(context: MmMutationContext, modem: ModemRef, preference: DesiredRadio): Promise<Receipt>;
|
|
5
|
+
export declare function setModeCombination(context: MmMutationContext, modem: ModemRef, allowed: number, preferred: number): Promise<Receipt>;
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { receipt } from '../../ports/index.js';
|
|
2
|
+
import { MODEM_IFACE } from '../constants.js';
|
|
3
|
+
import { describeMutationError } from './context.js';
|
|
4
|
+
const MODE_BIT = { gsm: 2, umts: 4, lte: 8, '5gnr': 16 };
|
|
5
|
+
export function setRadioModes(context, modem, preference) {
|
|
6
|
+
const allowed = maskOf(preference.allowedSet ?? new Set(preference.preferenceOrdered));
|
|
7
|
+
const preferred = preference.preferenceOrdered[0];
|
|
8
|
+
const preferredMask = preferred !== undefined ? MODE_BIT[preferred] : 0;
|
|
9
|
+
if (allowed === 0) {
|
|
10
|
+
return Promise.resolve(receipt('radio', 'failed', 'no radio modes were requested'));
|
|
11
|
+
}
|
|
12
|
+
return setModeMasks(context, modem, allowed, preferredMask, 'radio mode preference applied');
|
|
13
|
+
}
|
|
14
|
+
export function setModeCombination(context, modem, allowed, preferred) {
|
|
15
|
+
if (allowed === 0) {
|
|
16
|
+
return Promise.resolve(receipt('radio', 'failed', 'no radio modes were requested'));
|
|
17
|
+
}
|
|
18
|
+
return setModeMasks(context, modem, allowed, preferred, 'radio mode combination applied');
|
|
19
|
+
}
|
|
20
|
+
function setModeMasks(context, modem, allowed, preferred, successMessage) {
|
|
21
|
+
return context.actor.runQuiesced({ stableKey: context.resolveStableKey(modem) }, async () => {
|
|
22
|
+
try {
|
|
23
|
+
await context.transport.callMethod({
|
|
24
|
+
destination: context.destination,
|
|
25
|
+
path: modem,
|
|
26
|
+
interface: MODEM_IFACE,
|
|
27
|
+
member: 'SetCurrentModes',
|
|
28
|
+
signature: '(uu)',
|
|
29
|
+
args: [[allowed, preferred]],
|
|
30
|
+
});
|
|
31
|
+
return receipt('radio', 'applied', successMessage);
|
|
32
|
+
}
|
|
33
|
+
catch (error) {
|
|
34
|
+
return receipt('radio', 'failed', `SetCurrentModes failed: ${describeMutationError(error)}`);
|
|
35
|
+
}
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
function maskOf(rats) {
|
|
39
|
+
let mask = 0;
|
|
40
|
+
for (const rat of rats) {
|
|
41
|
+
mask |= MODE_BIT[rat];
|
|
42
|
+
}
|
|
43
|
+
return mask;
|
|
44
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { MODEM3GPP_IFACE } from '../constants.js';
|
|
2
|
+
import { numberProp, stringProp } from '../managed-objects.js';
|
|
3
|
+
import { describeMutationError } from './context.js';
|
|
4
|
+
const AVAILABILITY = {
|
|
5
|
+
0: 'unknown',
|
|
6
|
+
1: 'available',
|
|
7
|
+
2: 'current',
|
|
8
|
+
3: 'forbidden',
|
|
9
|
+
};
|
|
10
|
+
export function scanNetworks(context, modem, timeoutMs) {
|
|
11
|
+
return context.actor.run(context.resolveStableKey(modem), async () => {
|
|
12
|
+
try {
|
|
13
|
+
const reply = await context.transport.callMethod({
|
|
14
|
+
destination: context.destination,
|
|
15
|
+
path: modem,
|
|
16
|
+
interface: MODEM3GPP_IFACE,
|
|
17
|
+
member: 'Scan',
|
|
18
|
+
timeoutMs,
|
|
19
|
+
});
|
|
20
|
+
return { ok: true, networks: parseScan(reply.body[0]) };
|
|
21
|
+
}
|
|
22
|
+
catch (error) {
|
|
23
|
+
return { ok: false, reason: `network scan failed: ${describeMutationError(error)}` };
|
|
24
|
+
}
|
|
25
|
+
});
|
|
26
|
+
}
|
|
27
|
+
function parseScan(value) {
|
|
28
|
+
if (!Array.isArray(value)) {
|
|
29
|
+
return [];
|
|
30
|
+
}
|
|
31
|
+
const networks = [];
|
|
32
|
+
for (const entry of value) {
|
|
33
|
+
const operatorCode = stringProp(entry, 'operator-code');
|
|
34
|
+
if (operatorCode === undefined) {
|
|
35
|
+
continue;
|
|
36
|
+
}
|
|
37
|
+
const name = stringProp(entry, 'operator-long') ?? stringProp(entry, 'operator-short');
|
|
38
|
+
const availability = AVAILABILITY[numberProp(entry, 'status') ?? 0] ?? 'unknown';
|
|
39
|
+
networks.push({
|
|
40
|
+
operatorCode,
|
|
41
|
+
...(name !== undefined ? { operatorName: name } : {}),
|
|
42
|
+
availability,
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
return networks;
|
|
46
|
+
}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import type { ModemRef, Receipt, SimPukUnlockResult, SimUnlockResult } from '../../ports/index.js';
|
|
2
|
+
import type { MmMutationContext } from './context.js';
|
|
3
|
+
export declare function setPrimarySimSlot(context: MmMutationContext, modem: ModemRef, slotIndex: number): Promise<Receipt>;
|
|
4
|
+
export declare function unlockWithPin(context: MmMutationContext, modem: ModemRef, pin: string): Promise<SimUnlockResult>;
|
|
5
|
+
export declare function unlockWithPuk(context: MmMutationContext, modem: ModemRef, puk: string, newPin: string): Promise<SimPukUnlockResult>;
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { receipt } from '../../ports/index.js';
|
|
2
|
+
import { MODEM_IFACE } from '../constants.js';
|
|
3
|
+
import { fetchManagedObjects, findInterface, propValue } from '../managed-objects.js';
|
|
4
|
+
import { sendSimPin, sendSimPuk } from '../sim-unlock.js';
|
|
5
|
+
import { describeMutationError } from './context.js';
|
|
6
|
+
export async function setPrimarySimSlot(context, modem, slotIndex) {
|
|
7
|
+
const slots = await readSlotCount(context, modem);
|
|
8
|
+
if (slots === undefined) {
|
|
9
|
+
return receipt('simSlot', 'failed', 'could not read the modem SIM-slot list');
|
|
10
|
+
}
|
|
11
|
+
if (slots <= 1) {
|
|
12
|
+
return receipt('simSlot', 'unsupported', 'single-slot modem has no primary slot to select');
|
|
13
|
+
}
|
|
14
|
+
if (slotIndex < 1 || slotIndex > slots) {
|
|
15
|
+
return receipt('simSlot', 'failed', `slot ${slotIndex} is out of range (1..${slots})`);
|
|
16
|
+
}
|
|
17
|
+
return context.actor.runQuiesced({ stableKey: context.resolveStableKey(modem) }, async () => {
|
|
18
|
+
try {
|
|
19
|
+
await context.transport.callMethod({
|
|
20
|
+
destination: context.destination,
|
|
21
|
+
path: modem,
|
|
22
|
+
interface: MODEM_IFACE,
|
|
23
|
+
member: 'SetPrimarySimSlot',
|
|
24
|
+
signature: 'u',
|
|
25
|
+
args: [slotIndex],
|
|
26
|
+
});
|
|
27
|
+
return receipt('simSlot', 'applied', `primary SIM slot set to ${slotIndex}`);
|
|
28
|
+
}
|
|
29
|
+
catch (error) {
|
|
30
|
+
return receipt('simSlot', 'failed', `SetPrimarySimSlot failed: ${describeMutationError(error)}`);
|
|
31
|
+
}
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
export function unlockWithPin(context, modem, pin) {
|
|
35
|
+
return context.actor.run(context.resolveStableKey(modem), () => sendSimPin(context.transport, context.destination, modem, pin));
|
|
36
|
+
}
|
|
37
|
+
export function unlockWithPuk(context, modem, puk, newPin) {
|
|
38
|
+
return context.actor.run(context.resolveStableKey(modem), () => sendSimPuk(context.transport, context.destination, modem, puk, newPin));
|
|
39
|
+
}
|
|
40
|
+
async function readSlotCount(context, modem) {
|
|
41
|
+
try {
|
|
42
|
+
const tree = await fetchManagedObjects(context.transport, context.destination);
|
|
43
|
+
const slots = propValue(findInterface(tree, modem, MODEM_IFACE), 'SimSlots');
|
|
44
|
+
return Array.isArray(slots) ? slots.length : undefined;
|
|
45
|
+
}
|
|
46
|
+
catch {
|
|
47
|
+
return undefined;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
@@ -6,30 +6,14 @@ export interface MmMutationsDeps {
|
|
|
6
6
|
readonly transport: DbusTransport;
|
|
7
7
|
readonly actor: ModemActor;
|
|
8
8
|
readonly destination?: string;
|
|
9
|
-
/** Map a live modem path to its stable actor key (survives replug). */
|
|
10
9
|
readonly resolveStableKey: (modem: ModemRef) => string;
|
|
11
10
|
readonly scanTimeoutMs?: number;
|
|
12
11
|
readonly now?: () => number;
|
|
13
12
|
}
|
|
14
|
-
/** The disruptive + SIM mutations of `ModemManagerPort`, serialized per modem. */
|
|
15
13
|
export declare class MmMutations {
|
|
16
14
|
#private;
|
|
17
15
|
constructor(deps: MmMutationsDeps);
|
|
18
16
|
setRadioModes(modem: ModemRef, preference: DesiredRadio): Promise<Receipt>;
|
|
19
|
-
/**
|
|
20
|
-
* `SetCurrentModes` over the RAW `(uu)` masks the modem itself advertised.
|
|
21
|
-
*
|
|
22
|
-
* `setRadioModes` above speaks the reconciler's vocabulary — an ordered RAT
|
|
23
|
-
* preference — and structurally cannot express `MM_MODEM_MODE_NONE`: its preferred
|
|
24
|
-
* mask is derived from `preferenceOrdered[0]`, so "allow this set and prefer
|
|
25
|
-
* nothing within it" has no spelling. That is the combination the bench FM350-GL
|
|
26
|
-
* actually advertises, so a caller selecting an advertised combination verbatim
|
|
27
|
-
* needs this entry point. It quiesces for the same reason `setRadioModes` does: a
|
|
28
|
-
* mode change re-registers the radio and drops the bearer underneath NM.
|
|
29
|
-
*
|
|
30
|
-
* An `allowed` mask of 0 is refused — that is not a selection, it is a radio with
|
|
31
|
-
* nothing switched on — while a `preferred` mask of 0 is passed through untouched.
|
|
32
|
-
*/
|
|
33
17
|
setModeCombination(modem: ModemRef, allowed: number, preferred: number): Promise<Receipt>;
|
|
34
18
|
readBands(modem: ModemRef): Promise<BandReadResult>;
|
|
35
19
|
setCurrentBands(modem: ModemRef, bands: readonly string[]): Promise<Receipt>;
|