@ceralive/modem-control 1.2.0 → 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 +84 -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/runtime-composition-operation.js +2 -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
|
@@ -8,35 +8,45 @@
|
|
|
8
8
|
// is `not-reported`, an interface nobody read is `not-observed`, and a field that WAS
|
|
9
9
|
// present but decoded to nothing is `malformed` with a diagnostic note. Collapsing
|
|
10
10
|
// those three into one absent value is precisely the loss this layer exists to stop.
|
|
11
|
-
import { decodeEsimStatus, decodeMmAccessTechnologies, decodeMmState, decodeRegistrationState, decodeSimType, decodeStateFailedReason, decodeUnlockRequired, modeMaskToLabel, modemHardwareLabel, modemHardwareName, } from '../../domain/index.js';
|
|
11
|
+
import { decode3gppLacCi, decodeEsimStatus, decodeMmAccessTechnologies, decodeMmState, decodeRegistrationState, decodeSimType, decodeStateFailedReason, decodeUnlockRequired, modeMaskToLabel, modemHardwareLabel, modemHardwareName, } from '../../domain/index.js';
|
|
12
12
|
import { readSimPresence } from '../../hardware/router-parsers.js';
|
|
13
13
|
import { freshObservation, metricProvenance } from '../envelope.js';
|
|
14
14
|
import { knownMetric, unknownMetric } from '../metric.js';
|
|
15
15
|
import { createObservationDiagnostics, } from '../provenance.js';
|
|
16
|
-
import { hasRawField, mergeRawRecords, prefixRawRecord, rawBooleanAt, rawKey, rawNumber, rawNumberAt, rawString, rawStringArray, } from '../raw.js';
|
|
16
|
+
import { hasRawDictMember, hasRawField, mergeRawRecords, prefixRawRecord, rawBooleanAt, rawDictNumber, rawKey, rawNumber, rawNumberAt, rawString, rawStringArray, } from '../raw.js';
|
|
17
17
|
const SOURCE = 'modemmanager';
|
|
18
18
|
const MODEM = 'Modem';
|
|
19
19
|
const MODEM3GPP = 'Modem3gpp';
|
|
20
20
|
const SIM = 'Sim';
|
|
21
21
|
const SIGNAL = 'Signal';
|
|
22
|
+
const LOCATION = 'Location';
|
|
22
23
|
export function normalizeModemManagerObservation(input, context) {
|
|
23
|
-
const raw = mergeRawRecords(prefixRawRecord(MODEM, input.modem), prefixRawRecord(MODEM3GPP, input.modem3gpp), prefixRawRecord(SIM, input.sim), prefixRawRecord(SIGNAL, input.signal));
|
|
24
|
+
const raw = mergeRawRecords(prefixRawRecord(MODEM, input.modem), prefixRawRecord(MODEM3GPP, input.modem3gpp), prefixRawRecord(SIM, input.sim), prefixRawRecord(SIGNAL, input.signal), prefixRawRecord(LOCATION, input.location));
|
|
24
25
|
const notes = [];
|
|
25
26
|
const consumed = [];
|
|
26
27
|
const provenance = (...fields) => {
|
|
27
28
|
consumed.push(...fields);
|
|
28
29
|
return metricProvenance(SOURCE, context, fields);
|
|
29
30
|
};
|
|
31
|
+
// A dict-sourced metric CONSUMES the whole `a{sv}` property (that is the raw key it
|
|
32
|
+
// actually has) while NAMING the exact member it read, so provenance stays precise
|
|
33
|
+
// without `unmapped` gaining a key that was never in the payload.
|
|
34
|
+
const dictProvenance = (dict, member) => {
|
|
35
|
+
consumed.push(dict);
|
|
36
|
+
return metricProvenance(SOURCE, context, [`${dict}.${member}`]);
|
|
37
|
+
};
|
|
30
38
|
const hardware = normalizeHardware(raw, provenance);
|
|
31
39
|
const radio = normalizeRadio(raw, provenance, notes);
|
|
32
|
-
const signal = normalizeSignal(raw, input.signal !== undefined, provenance);
|
|
40
|
+
const signal = normalizeSignal(raw, input.signal !== undefined, provenance, dictProvenance, notes);
|
|
33
41
|
const sim = normalizeSim(raw, input.sim !== undefined, provenance, notes);
|
|
42
|
+
const cell = normalizeCell(raw, input.location !== undefined, provenance, notes);
|
|
34
43
|
return freshObservation(SOURCE, context, {
|
|
35
44
|
source: SOURCE,
|
|
36
45
|
hardware,
|
|
37
46
|
radio,
|
|
38
47
|
signal,
|
|
39
48
|
sim,
|
|
49
|
+
cell,
|
|
40
50
|
diagnostics: createObservationDiagnostics({ source: SOURCE, raw, consumed, notes }),
|
|
41
51
|
});
|
|
42
52
|
}
|
|
@@ -53,12 +63,53 @@ const SIM_SLOTS = rawKey(MODEM, 'SimSlots');
|
|
|
53
63
|
const FAILED_REASON = rawKey(MODEM, 'StateFailedReason');
|
|
54
64
|
const UNLOCK_REQUIRED = rawKey(MODEM, 'UnlockRequired');
|
|
55
65
|
const REGISTRATION_STATE = rawKey(MODEM3GPP, 'RegistrationState');
|
|
66
|
+
const OPERATOR_NAME = rawKey(MODEM3GPP, 'OperatorName');
|
|
67
|
+
const OPERATOR_CODE = rawKey(MODEM3GPP, 'OperatorCode');
|
|
56
68
|
const SIM_TYPE = rawKey(SIM, 'SimType');
|
|
57
69
|
const ESIM_STATUS = rawKey(SIM, 'EsimStatus');
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
const
|
|
61
|
-
|
|
70
|
+
// The `Modem.Location` entry for MM's coarse-cell source. Named, not bit-keyed — see
|
|
71
|
+
// `ModemManagerObservationInput.location`.
|
|
72
|
+
const LAC_CI = rawKey(LOCATION, '3gpp-lac-ci');
|
|
73
|
+
// `Modem.Signal` publishes ONE `a{sv}` per RAT, and the member sets differ (MM 1.24.2
|
|
74
|
+
// `org.freedesktop.ModemManager1.Modem.Signal.xml`):
|
|
75
|
+
// Cdma rssi/ecio/error-rate Evdo rssi/ecio/SINR/io/error-rate
|
|
76
|
+
// Gsm rssi/error-rate Umts rssi/rscp/ecio/error-rate
|
|
77
|
+
// Lte rssi/rsrq/rsrp/snr/error-rate Nr5g rsrq/rsrp/snr/error-rate
|
|
78
|
+
const SIGNAL_CDMA = rawKey(SIGNAL, 'Cdma');
|
|
79
|
+
const SIGNAL_EVDO = rawKey(SIGNAL, 'Evdo');
|
|
80
|
+
const SIGNAL_GSM = rawKey(SIGNAL, 'Gsm');
|
|
81
|
+
const SIGNAL_UMTS = rawKey(SIGNAL, 'Umts');
|
|
82
|
+
const SIGNAL_LTE = rawKey(SIGNAL, 'Lte');
|
|
83
|
+
const SIGNAL_NR5G = rawKey(SIGNAL, 'Nr5g');
|
|
84
|
+
const RSSI = {
|
|
85
|
+
member: 'rssi',
|
|
86
|
+
dicts: [SIGNAL_LTE, SIGNAL_UMTS, SIGNAL_GSM, SIGNAL_EVDO, SIGNAL_CDMA],
|
|
87
|
+
flat: rawKey(SIGNAL, 'rssi'),
|
|
88
|
+
};
|
|
89
|
+
const RSRP = {
|
|
90
|
+
member: 'rsrp',
|
|
91
|
+
dicts: [SIGNAL_NR5G, SIGNAL_LTE],
|
|
92
|
+
flat: rawKey(SIGNAL, 'rsrp'),
|
|
93
|
+
};
|
|
94
|
+
const RSRQ = {
|
|
95
|
+
member: 'rsrq',
|
|
96
|
+
dicts: [SIGNAL_NR5G, SIGNAL_LTE],
|
|
97
|
+
flat: rawKey(SIGNAL, 'rsrq'),
|
|
98
|
+
};
|
|
99
|
+
const SNR = {
|
|
100
|
+
member: 'snr',
|
|
101
|
+
dicts: [SIGNAL_NR5G, SIGNAL_LTE],
|
|
102
|
+
flat: rawKey(SIGNAL, 'snr'),
|
|
103
|
+
};
|
|
104
|
+
// SINR is a member of the `Evdo` dict and of NO other, so an LTE/NR modem reporting no
|
|
105
|
+
// SINR is a READ-class `not-reported` — ModemManager CAN express it, this modem did not.
|
|
106
|
+
// The NR SINR a device may publish through `Modem.GetCellInfo` is a different call on a
|
|
107
|
+
// different interface and is deliberately not folded in here.
|
|
108
|
+
const SINR = {
|
|
109
|
+
member: 'sinr',
|
|
110
|
+
dicts: [SIGNAL_EVDO],
|
|
111
|
+
flat: rawKey(SIGNAL, 'sinr'),
|
|
112
|
+
};
|
|
62
113
|
function hardwareIdentity(raw) {
|
|
63
114
|
const model = rawString(raw, MODEL);
|
|
64
115
|
const manufacturer = rawString(raw, MANUFACTURER);
|
|
@@ -93,6 +144,53 @@ function normalizeRadio(raw, provenance, notes) {
|
|
|
93
144
|
registration: decodedLabel(raw, REGISTRATION_STATE, decodeRegistrationState, provenance, notes),
|
|
94
145
|
accessTechnologies: normalizeAccessTechnologies(raw, provenance, notes),
|
|
95
146
|
modeLabel: normalizeModeLabel(raw, provenance, notes),
|
|
147
|
+
// `Modem3gpp`, never `Sim` — the registered operator, not the SIM's home one.
|
|
148
|
+
// A modem that is not registered reports these empty, which is `not-reported`:
|
|
149
|
+
// the property exists and this read carried no value.
|
|
150
|
+
operatorName: registrationString(raw, OPERATOR_NAME, provenance),
|
|
151
|
+
operatorCode: registrationString(raw, OPERATOR_CODE, provenance),
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
/** A `Modem3gpp` registration string — present-and-empty is still no reading. */
|
|
155
|
+
function registrationString(raw, key, provenance) {
|
|
156
|
+
const source = provenance(key);
|
|
157
|
+
const value = rawString(raw, key);
|
|
158
|
+
return value === undefined
|
|
159
|
+
? unknownMetric('not-reported', source)
|
|
160
|
+
: knownMetric(value, source);
|
|
161
|
+
}
|
|
162
|
+
/**
|
|
163
|
+
* Coarse cell context from the `3gpp-lac-ci` location reading.
|
|
164
|
+
*
|
|
165
|
+
* The whole value is ONE string, so a `cellId` and a `tac` read out of it necessarily
|
|
166
|
+
* agree with each other — they came from the same reported cell, never from two reads
|
|
167
|
+
* that raced. A value in an unrecognized shape is `malformed` for both rather than
|
|
168
|
+
* partially decoded; see `decode3gppLacCi`.
|
|
169
|
+
*/
|
|
170
|
+
function normalizeCell(raw, locationRead, provenance, notes) {
|
|
171
|
+
const source = provenance(LAC_CI);
|
|
172
|
+
if (!hasRawField(raw, LAC_CI)) {
|
|
173
|
+
const reason = locationRead ? 'not-reported' : 'not-observed';
|
|
174
|
+
return {
|
|
175
|
+
cellId: unknownMetric(reason, source),
|
|
176
|
+
tac: unknownMetric(reason, source),
|
|
177
|
+
};
|
|
178
|
+
}
|
|
179
|
+
const decoded = decode3gppLacCi(rawString(raw, LAC_CI));
|
|
180
|
+
if (decoded === undefined) {
|
|
181
|
+
notes.push({ code: 'field-shape-unrecognized', field: LAC_CI });
|
|
182
|
+
return {
|
|
183
|
+
cellId: unknownMetric('malformed', source),
|
|
184
|
+
tac: unknownMetric('malformed', source),
|
|
185
|
+
};
|
|
186
|
+
}
|
|
187
|
+
return {
|
|
188
|
+
cellId: knownMetric(decoded.cellId, source),
|
|
189
|
+
// MM emits an EMPTY tracking-area code on a device with no LTE/NR attach — the
|
|
190
|
+
// reading arrived, this field of it did not.
|
|
191
|
+
tac: decoded.trackingAreaCode === ''
|
|
192
|
+
? unknownMetric('not-reported', source)
|
|
193
|
+
: knownMetric(decoded.trackingAreaCode, source),
|
|
96
194
|
};
|
|
97
195
|
}
|
|
98
196
|
/** A decoder whose "I could not place this" answer is the literal string `unknown`. */
|
|
@@ -143,13 +241,24 @@ function normalizeModeLabel(raw, provenance, notes) {
|
|
|
143
241
|
}
|
|
144
242
|
return knownMetric(label, source);
|
|
145
243
|
}
|
|
146
|
-
function normalizeSignal(raw, signalRead, provenance) {
|
|
244
|
+
function normalizeSignal(raw, signalRead, provenance, dictProvenance, notes) {
|
|
147
245
|
const missing = signalRead ? 'not-reported' : 'not-observed';
|
|
148
|
-
const extended = (
|
|
149
|
-
const
|
|
150
|
-
|
|
246
|
+
const extended = (metric) => {
|
|
247
|
+
for (const dict of metric.dicts) {
|
|
248
|
+
if (!hasRawDictMember(raw, dict, metric.member))
|
|
249
|
+
continue;
|
|
250
|
+
const source = dictProvenance(dict, metric.member);
|
|
251
|
+
const value = rawDictNumber(raw, dict, metric.member);
|
|
252
|
+
if (value === undefined) {
|
|
253
|
+
notes.push({ code: 'field-shape-unrecognized', field: `${dict}.${metric.member}` });
|
|
254
|
+
return unknownMetric('malformed', source);
|
|
255
|
+
}
|
|
256
|
+
return knownMetric(value, source);
|
|
257
|
+
}
|
|
258
|
+
const source = provenance(metric.flat);
|
|
259
|
+
const value = rawNumber(raw, metric.flat);
|
|
151
260
|
return value === undefined
|
|
152
|
-
? unknownMetric(hasRawField(raw,
|
|
261
|
+
? unknownMetric(hasRawField(raw, metric.flat) ? 'malformed' : missing, source)
|
|
153
262
|
: knownMetric(value, source);
|
|
154
263
|
};
|
|
155
264
|
// `SignalQuality` is a `(ub)`: percentage at 0, "measured recently" at 1. Both are
|
|
@@ -173,8 +282,7 @@ function normalizeSignal(raw, signalRead, provenance) {
|
|
|
173
282
|
rsrp: extended(RSRP),
|
|
174
283
|
rsrq: extended(RSRQ),
|
|
175
284
|
snr: extended(SNR),
|
|
176
|
-
|
|
177
|
-
sinr: unknownMetric('unsupported', metricProvenanceEmpty(provenance)),
|
|
285
|
+
sinr: extended(SINR),
|
|
178
286
|
};
|
|
179
287
|
}
|
|
180
288
|
/** Provenance for a metric no raw field backs, which is what `unsupported` means. */
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { type ObservationAuthority } from '../../domain/index.js';
|
|
2
2
|
import { type NormalizedMetric } from '../metric.js';
|
|
3
|
-
import type { NormalizedHardware, NormalizedSim } from '../model.js';
|
|
3
|
+
import type { NormalizedCell, NormalizedHardware, NormalizedSim } from '../model.js';
|
|
4
4
|
import type { MetricProvenance } from '../provenance.js';
|
|
5
5
|
export type RouterProvenance = (fields: readonly string[], authority?: ObservationAuthority) => MetricProvenance;
|
|
6
6
|
export declare function unsupportedRadioMetric(provenance: RouterProvenance): NormalizedMetric<string>;
|
|
@@ -14,6 +14,40 @@ export declare function unsupportedRadioMetric(provenance: RouterProvenance): No
|
|
|
14
14
|
* migrated decoder covers is precisely the invented reading this layer refuses.
|
|
15
15
|
*/
|
|
16
16
|
export declare function routerSim(provenance: RouterProvenance, vendorPresenceField?: string): NormalizedSim;
|
|
17
|
+
/**
|
|
18
|
+
* The cell block a router source produces.
|
|
19
|
+
*
|
|
20
|
+
* `cellId` is claimed only when a MIGRATED parser decoded one — `parseZteDetails` and
|
|
21
|
+
* `parseUfiDetails` both do — and the raw vendor key is named in provenance. HiLink
|
|
22
|
+
* ships a `<cell_id>` tag that no migrated parser reads, so it stays verbatim in the
|
|
23
|
+
* diagnostics block rather than being lifted here: the same rule that keeps `SimStatus`
|
|
24
|
+
* out of `sim.presence`.
|
|
25
|
+
*
|
|
26
|
+
* `tac` is always `not-reported`, never `unsupported`. None of the three migrated
|
|
27
|
+
* parsers decodes a tracking-area code, but that is a fact about what THIS package
|
|
28
|
+
* reads, not a claim that the vendor's firmware cannot report one — and `unsupported`
|
|
29
|
+
* would be the second kind of statement.
|
|
30
|
+
*/
|
|
31
|
+
export declare function routerCell(provenance: RouterProvenance, cell: {
|
|
32
|
+
readonly id: string;
|
|
33
|
+
readonly field: string;
|
|
34
|
+
} | undefined): NormalizedCell;
|
|
35
|
+
/**
|
|
36
|
+
* A registered-operator NAME a migrated parser decoded, or an honest silence.
|
|
37
|
+
*
|
|
38
|
+
* The matching CODE is deliberately never derived. `parseZteDetails` reports `mcc` and
|
|
39
|
+
* `mnc` as separate unpadded vendor fields, while `Modem3gpp.OperatorCode` is a
|
|
40
|
+
* fixed-width concatenation that MM itself splits back apart at exactly three
|
|
41
|
+
* characters — joining an unpadded 2-vs-3-digit MNC would name a different network
|
|
42
|
+
* roughly whenever the leading zero matters. So it reads `not-reported`.
|
|
43
|
+
*/
|
|
44
|
+
export declare function routerOperator(provenance: RouterProvenance, operator: {
|
|
45
|
+
readonly name: string;
|
|
46
|
+
readonly field: string;
|
|
47
|
+
} | undefined): {
|
|
48
|
+
readonly operatorName: NormalizedMetric<string>;
|
|
49
|
+
readonly operatorCode: NormalizedMetric<string>;
|
|
50
|
+
};
|
|
17
51
|
/** Router admin APIs report no measurement-recency flag; that is a source capability claim. */
|
|
18
52
|
export declare function unsupportedQualityRecent(provenance: RouterProvenance): NormalizedMetric<boolean>;
|
|
19
53
|
/**
|
|
@@ -40,6 +40,45 @@ export function routerSim(provenance, vendorPresenceField) {
|
|
|
40
40
|
esimStatus: unknownMetric('unsupported', provenance([])),
|
|
41
41
|
};
|
|
42
42
|
}
|
|
43
|
+
/**
|
|
44
|
+
* The cell block a router source produces.
|
|
45
|
+
*
|
|
46
|
+
* `cellId` is claimed only when a MIGRATED parser decoded one — `parseZteDetails` and
|
|
47
|
+
* `parseUfiDetails` both do — and the raw vendor key is named in provenance. HiLink
|
|
48
|
+
* ships a `<cell_id>` tag that no migrated parser reads, so it stays verbatim in the
|
|
49
|
+
* diagnostics block rather than being lifted here: the same rule that keeps `SimStatus`
|
|
50
|
+
* out of `sim.presence`.
|
|
51
|
+
*
|
|
52
|
+
* `tac` is always `not-reported`, never `unsupported`. None of the three migrated
|
|
53
|
+
* parsers decodes a tracking-area code, but that is a fact about what THIS package
|
|
54
|
+
* reads, not a claim that the vendor's firmware cannot report one — and `unsupported`
|
|
55
|
+
* would be the second kind of statement.
|
|
56
|
+
*/
|
|
57
|
+
export function routerCell(provenance, cell) {
|
|
58
|
+
return {
|
|
59
|
+
cellId: cell === undefined
|
|
60
|
+
? unknownMetric('not-reported', provenance([]))
|
|
61
|
+
: knownMetric(cell.id, provenance([cell.field])),
|
|
62
|
+
tac: unknownMetric('not-reported', provenance([])),
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* A registered-operator NAME a migrated parser decoded, or an honest silence.
|
|
67
|
+
*
|
|
68
|
+
* The matching CODE is deliberately never derived. `parseZteDetails` reports `mcc` and
|
|
69
|
+
* `mnc` as separate unpadded vendor fields, while `Modem3gpp.OperatorCode` is a
|
|
70
|
+
* fixed-width concatenation that MM itself splits back apart at exactly three
|
|
71
|
+
* characters — joining an unpadded 2-vs-3-digit MNC would name a different network
|
|
72
|
+
* roughly whenever the leading zero matters. So it reads `not-reported`.
|
|
73
|
+
*/
|
|
74
|
+
export function routerOperator(provenance, operator) {
|
|
75
|
+
return {
|
|
76
|
+
operatorName: operator === undefined
|
|
77
|
+
? unknownMetric('not-reported', provenance([]))
|
|
78
|
+
: knownMetric(operator.name, provenance([operator.field])),
|
|
79
|
+
operatorCode: unknownMetric('not-reported', provenance([])),
|
|
80
|
+
};
|
|
81
|
+
}
|
|
43
82
|
/** Router admin APIs report no measurement-recency flag; that is a source capability claim. */
|
|
44
83
|
export function unsupportedQualityRecent(provenance) {
|
|
45
84
|
return unknownMetric('unsupported', provenance([]));
|
|
@@ -16,7 +16,7 @@ import { freshObservation, metricProvenance } from '../envelope.js';
|
|
|
16
16
|
import { metricFromRouterSignal, unknownMetric } from '../metric.js';
|
|
17
17
|
import { createObservationDiagnostics, } from '../provenance.js';
|
|
18
18
|
import { hasRawField, mergeRawRecords, normalizeRawValue, parseJsonObject, prefixRawRecord, rawKey, } from '../raw.js';
|
|
19
|
-
import { routerHardware, routerSim, unsupportedQualityRecent, unsupportedRadioMetric, } from './router-shared.js';
|
|
19
|
+
import { routerCell, routerHardware, routerOperator, routerSim, unsupportedQualityRecent, unsupportedRadioMetric, } from './router-shared.js';
|
|
20
20
|
const SOURCE = 'ufi-himiapi';
|
|
21
21
|
const SYSINFO = 'sysinfo';
|
|
22
22
|
const OVERVIEW = 'overview';
|
|
@@ -72,6 +72,9 @@ export function normalizeUfiObservation(input, context) {
|
|
|
72
72
|
registration: unsupportedRadioMetric(provenance),
|
|
73
73
|
accessTechnologies: unknownMetric('unsupported', provenance([])),
|
|
74
74
|
modeLabel: unknownMetric('not-reported', provenance([])),
|
|
75
|
+
// `parseUfiDetails` decodes no operator field from any of the three
|
|
76
|
+
// endpoints, so neither half is claimed.
|
|
77
|
+
...routerOperator(provenance, undefined),
|
|
75
78
|
},
|
|
76
79
|
signal: {
|
|
77
80
|
quality: unknownMetric('unsupported', provenance([])),
|
|
@@ -89,6 +92,9 @@ export function normalizeUfiObservation(input, context) {
|
|
|
89
92
|
sinr: metricFromRouterSignal(signalModel.sinr, provenance([])),
|
|
90
93
|
},
|
|
91
94
|
sim: routerSim(provenance, hasRawField(raw, SIM_STATE) ? SIM_STATE : undefined),
|
|
95
|
+
cell: routerCell(provenance, details?.cell_id === undefined
|
|
96
|
+
? undefined
|
|
97
|
+
: { id: details.cell_id, field: rawKey(SYSINFO, 'cellid') }),
|
|
92
98
|
diagnostics: createObservationDiagnostics({ source: SOURCE, raw, consumed, notes }),
|
|
93
99
|
});
|
|
94
100
|
}
|
|
@@ -15,7 +15,7 @@ import { freshObservation, metricProvenance } from '../envelope.js';
|
|
|
15
15
|
import { knownMetric, metricFromRouterSignal, unknownMetric, } from '../metric.js';
|
|
16
16
|
import { createObservationDiagnostics } from '../provenance.js';
|
|
17
17
|
import { hasRawField, parseJsonRecord, prefixRawRecord, rawKey } from '../raw.js';
|
|
18
|
-
import { routerHardware, routerSim, unsupportedQualityRecent, unsupportedRadioMetric, } from './router-shared.js';
|
|
18
|
+
import { routerCell, routerHardware, routerOperator, routerSim, unsupportedQualityRecent, unsupportedRadioMetric, } from './router-shared.js';
|
|
19
19
|
const SOURCE = 'zte-goform';
|
|
20
20
|
const BODY = 'goform';
|
|
21
21
|
/** ZTE's own SIM presence code. Named as evidence, never decoded into a presence. */
|
|
@@ -44,6 +44,9 @@ export function normalizeZteObservation(input, context) {
|
|
|
44
44
|
registration: unsupportedRadioMetric(provenance),
|
|
45
45
|
accessTechnologies: unknownMetric('unsupported', provenance([])),
|
|
46
46
|
modeLabel: normalizeModeLabel(details?.network_type, parsed !== undefined, provenance),
|
|
47
|
+
...routerOperator(provenance, details?.provider === undefined
|
|
48
|
+
? undefined
|
|
49
|
+
: { name: details.provider, field: rawKey(BODY, 'network_provider_fullname') }),
|
|
47
50
|
},
|
|
48
51
|
signal: {
|
|
49
52
|
quality: unknownMetric('unsupported', provenance([])),
|
|
@@ -59,6 +62,9 @@ export function normalizeZteObservation(input, context) {
|
|
|
59
62
|
sinr: metricFromRouterSignal(signalModel.sinr, provenance([])),
|
|
60
63
|
},
|
|
61
64
|
sim: routerSim(provenance, hasRawField(raw, SIM_CARD_STATE) ? SIM_CARD_STATE : undefined),
|
|
65
|
+
cell: routerCell(provenance, details?.cell_id === undefined
|
|
66
|
+
? undefined
|
|
67
|
+
: { id: details.cell_id, field: rawKey(BODY, 'cell_id') }),
|
|
62
68
|
diagnostics: createObservationDiagnostics({ source: SOURCE, raw, consumed, notes }),
|
|
63
69
|
});
|
|
64
70
|
}
|
|
@@ -11,6 +11,16 @@ export interface ModemManagerProviderOptions {
|
|
|
11
11
|
readonly destination?: string;
|
|
12
12
|
readonly quiesce?: QuiesceHook;
|
|
13
13
|
readonly now?: () => number;
|
|
14
|
+
/**
|
|
15
|
+
* `Signal.Setup` reporting rate in whole seconds, defaulting to
|
|
16
|
+
* `DEFAULT_SIGNAL_INTERVAL_SECONDS` (5).
|
|
17
|
+
*
|
|
18
|
+
* The backend has accepted this since it was written; the provider had no way to pass
|
|
19
|
+
* it, so an embedder constructing a provider — which is every embedder — was pinned to
|
|
20
|
+
* the default with no seam to change it. That is the gap this closes, and it is why
|
|
21
|
+
* the option is threaded rather than re-implemented here.
|
|
22
|
+
*/
|
|
23
|
+
readonly signalIntervalSeconds?: number;
|
|
14
24
|
/**
|
|
15
25
|
* The band-lock certification catalog. Defaults to the one shipped in this package,
|
|
16
26
|
* which is EMPTY — so a band write is refused on every device until a human-reviewed
|
|
@@ -33,6 +33,9 @@ export class ModemManagerProvider {
|
|
|
33
33
|
destination: this.#destination,
|
|
34
34
|
actor,
|
|
35
35
|
now: this.#now,
|
|
36
|
+
...(options.signalIntervalSeconds === undefined
|
|
37
|
+
? {}
|
|
38
|
+
: { signalIntervalSeconds: options.signalIntervalSeconds }),
|
|
36
39
|
});
|
|
37
40
|
const location = new MmLocation({
|
|
38
41
|
transport: this.#transport,
|
|
@@ -98,6 +98,8 @@ export function createRuntimeCompositionOperation(deps) {
|
|
|
98
98
|
});
|
|
99
99
|
const current = await lease.run(queries.current);
|
|
100
100
|
const enumeration = await lease.run(queries.enumerate);
|
|
101
|
+
if (!current.ok || !enumeration.ok)
|
|
102
|
+
return suppressed('no-return-path', 'A runtime composition query failed');
|
|
101
103
|
const resolved = resolveRuntimeCompositionCapability({
|
|
102
104
|
vendor,
|
|
103
105
|
currentResponse: current.raw,
|
|
@@ -6,10 +6,16 @@ import { decodeStateFailedReason, epochMillis, sourceEpoch, stableKeyFromPhysica
|
|
|
6
6
|
import { readSimPresence } from '../../hardware/router-parsers.js';
|
|
7
7
|
import { normalizeModemManagerObservation } from '../../observations/index.js';
|
|
8
8
|
import { describeBandWriteCertification, readRadioModeTruth } from '../../radio/index.js';
|
|
9
|
+
import { isVariant } from '../../transport/index.js';
|
|
9
10
|
const SIGNAL_IFACE = `${MODEM_IFACE}.Signal`;
|
|
10
11
|
function rawValue(value) {
|
|
11
12
|
if (typeof value === 'string' || typeof value === 'number' || typeof value === 'boolean')
|
|
12
13
|
return value;
|
|
14
|
+
// An `a{sv}` decodes to `[key, variant][]`, so a dict member's value arrives WRAPPED.
|
|
15
|
+
// Without this unwrap a `Modem.Signal` RAT dict retained as `[['rsrp'], ['rsrq'], …]`:
|
|
16
|
+
// the key survived and the reading did not.
|
|
17
|
+
if (isVariant(value))
|
|
18
|
+
return rawValue(value.value);
|
|
13
19
|
if (Array.isArray(value)) {
|
|
14
20
|
const result = [];
|
|
15
21
|
for (const item of value) {
|
|
@@ -20,20 +20,13 @@
|
|
|
20
20
|
// deactivate. There is deliberately no delete path here: profile removal is not in
|
|
21
21
|
// that set, so this adapter cannot express it.
|
|
22
22
|
import { epochMillis } from '../../domain/index.js';
|
|
23
|
-
import { appliedConfiguration,
|
|
23
|
+
import { appliedConfiguration, desiredProfile } from '../../observations/index.js';
|
|
24
24
|
import { receipt } from '../../ports/index.js';
|
|
25
|
-
import {
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
'config',
|
|
31
|
-
'need-auth',
|
|
32
|
-
'ip-config',
|
|
33
|
-
'ip-check',
|
|
34
|
-
'secondaries',
|
|
35
|
-
'deactivating',
|
|
36
|
-
]);
|
|
25
|
+
import { projectDivergence } from './divergence.js';
|
|
26
|
+
import { foldObservation } from './observe-fold.js';
|
|
27
|
+
import { projectStateView } from './projection.js';
|
|
28
|
+
import { slotsFor, targetIfname } from './state.js';
|
|
29
|
+
import { boundBearer, unboundBearer } from './types.js';
|
|
37
30
|
export class NetworkManagerAdapter {
|
|
38
31
|
#port;
|
|
39
32
|
#now;
|
|
@@ -79,7 +72,7 @@ export class NetworkManagerAdapter {
|
|
|
79
72
|
autoConfig: request.profile.autoConfig,
|
|
80
73
|
homeOnly: request.profile.homeOnly,
|
|
81
74
|
}), epochMillis(this.#now()), request.requestedBy);
|
|
82
|
-
this.#
|
|
75
|
+
slotsFor(this.#slots, id).desired = desired;
|
|
83
76
|
return { ok: true, connectionId: id, desired };
|
|
84
77
|
}
|
|
85
78
|
/**
|
|
@@ -100,7 +93,7 @@ export class NetworkManagerAdapter {
|
|
|
100
93
|
};
|
|
101
94
|
}
|
|
102
95
|
const desired = desiredProfile(unboundBearer(ifname), epochMillis(this.#now()), requestedBy);
|
|
103
|
-
this.#
|
|
96
|
+
slotsFor(this.#slots, id).desired = desired;
|
|
104
97
|
return { ok: true, connectionId: id, desired };
|
|
105
98
|
}
|
|
106
99
|
desiredFor(id) {
|
|
@@ -151,40 +144,9 @@ export class NetworkManagerAdapter {
|
|
|
151
144
|
* was.
|
|
152
145
|
*/
|
|
153
146
|
observe(input) {
|
|
154
|
-
const
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
kind: 'refused',
|
|
158
|
-
reason: 'superseded-generation',
|
|
159
|
-
currentGeneration: this.#observedGeneration,
|
|
160
|
-
};
|
|
161
|
-
}
|
|
162
|
-
this.#observedGeneration = generation;
|
|
163
|
-
const devices = new Map(input.devices.map((device) => [device.ifname, device]));
|
|
164
|
-
const outcomes = [];
|
|
165
|
-
const losses = [];
|
|
166
|
-
for (const [id, slots] of this.#slots) {
|
|
167
|
-
const ifname = targetIfname(slots);
|
|
168
|
-
if (ifname === undefined) {
|
|
169
|
-
continue;
|
|
170
|
-
}
|
|
171
|
-
const device = devices.get(ifname);
|
|
172
|
-
slots.observed = observedState(device === undefined
|
|
173
|
-
? unavailableObservation(SOURCE, input.context, 'device-absent')
|
|
174
|
-
: freshObservation(SOURCE, input.context, observedBearer(device)));
|
|
175
|
-
const outcome = this.#classify(id, slots, ifname, device, input.context.observedAt);
|
|
176
|
-
outcomes.push({ connectionId: id, outcome });
|
|
177
|
-
if (outcome.status === 'lost') {
|
|
178
|
-
losses.push(outcome.loss);
|
|
179
|
-
}
|
|
180
|
-
}
|
|
181
|
-
return {
|
|
182
|
-
kind: 'accepted',
|
|
183
|
-
generation,
|
|
184
|
-
observedAt: input.context.observedAt,
|
|
185
|
-
outcomes,
|
|
186
|
-
losses,
|
|
187
|
-
};
|
|
147
|
+
const folded = foldObservation(this.#slots, this.#observedGeneration, input);
|
|
148
|
+
this.#observedGeneration = folded.generation;
|
|
149
|
+
return folded.result;
|
|
188
150
|
}
|
|
189
151
|
observedFor(id) {
|
|
190
152
|
return this.#slots.get(id)?.observed ?? null;
|
|
@@ -195,21 +157,11 @@ export class NetworkManagerAdapter {
|
|
|
195
157
|
* for observations, so it cannot manufacture one.
|
|
196
158
|
*/
|
|
197
159
|
stateView(id, context) {
|
|
198
|
-
|
|
199
|
-
if (slots === undefined) {
|
|
200
|
-
return null;
|
|
201
|
-
}
|
|
202
|
-
return {
|
|
203
|
-
desired: slots.desired,
|
|
204
|
-
applied: slots.applied,
|
|
205
|
-
observed: slots.observed ??
|
|
206
|
-
observedState(unavailableObservation(SOURCE, context, 'provider-unavailable')),
|
|
207
|
-
};
|
|
160
|
+
return projectStateView(this.#slots, id, context);
|
|
208
161
|
}
|
|
209
162
|
/** `desiredVsApplied` ("did our write happen") and `appliedVsObserved` ("did it stick"). */
|
|
210
163
|
divergence(id, context) {
|
|
211
|
-
|
|
212
|
-
return view === null ? null : describeStateDivergence(view, nmBearerStateEquals);
|
|
164
|
+
return projectDivergence(this.#slots, id, context);
|
|
213
165
|
}
|
|
214
166
|
// ── internals ──────────────────────────────────────────────────────────────
|
|
215
167
|
async #activate(id, desired, options) {
|
|
@@ -243,7 +195,7 @@ export class NetworkManagerAdapter {
|
|
|
243
195
|
generation: options.generation,
|
|
244
196
|
operationId: options.operationId,
|
|
245
197
|
});
|
|
246
|
-
this.#
|
|
198
|
+
slotsFor(this.#slots, id).applied = applied;
|
|
247
199
|
return { ok: true, applied, receipt: activation };
|
|
248
200
|
}
|
|
249
201
|
async #deactivate(id, ifname, options) {
|
|
@@ -263,82 +215,9 @@ export class NetworkManagerAdapter {
|
|
|
263
215
|
generation: options.generation,
|
|
264
216
|
operationId: options.operationId,
|
|
265
217
|
});
|
|
266
|
-
this.#
|
|
218
|
+
slotsFor(this.#slots, id).applied = applied;
|
|
267
219
|
return { ok: true, applied, receipt: result };
|
|
268
220
|
}
|
|
269
|
-
#classify(id, slots, ifname, device, observedAt) {
|
|
270
|
-
const applied = slots.applied;
|
|
271
|
-
if (applied === null) {
|
|
272
|
-
return { status: 'unapplied' };
|
|
273
|
-
}
|
|
274
|
-
const lose = (reason) => {
|
|
275
|
-
slots.applied = null;
|
|
276
|
-
return {
|
|
277
|
-
status: 'lost',
|
|
278
|
-
loss: {
|
|
279
|
-
connectionId: id,
|
|
280
|
-
deviceIfname: ifname,
|
|
281
|
-
reason,
|
|
282
|
-
lostAt: observedAt,
|
|
283
|
-
generation: applied.generation,
|
|
284
|
-
previous: applied,
|
|
285
|
-
},
|
|
286
|
-
};
|
|
287
|
-
};
|
|
288
|
-
if (device === undefined) {
|
|
289
|
-
return lose('interface-absent');
|
|
290
|
-
}
|
|
291
|
-
if (device.state === 'failed') {
|
|
292
|
-
return lose('activation-failed');
|
|
293
|
-
}
|
|
294
|
-
if (applied.configuration.kind === 'unbound') {
|
|
295
|
-
// We deliberately took the bearer down; anything active here is somebody else.
|
|
296
|
-
return device.activeConnection === undefined
|
|
297
|
-
? { status: 'retained', applied }
|
|
298
|
-
: lose('connection-replaced');
|
|
299
|
-
}
|
|
300
|
-
if (device.activeConnection === undefined) {
|
|
301
|
-
return lose('interface-detached');
|
|
302
|
-
}
|
|
303
|
-
if (device.activeConnection.connectionId !== id) {
|
|
304
|
-
return lose('connection-replaced');
|
|
305
|
-
}
|
|
306
|
-
if (device.state === 'activated') {
|
|
307
|
-
return { status: 'retained', applied };
|
|
308
|
-
}
|
|
309
|
-
return TRANSITIONAL_STATES.has(device.state)
|
|
310
|
-
? { status: 'pending', applied, deviceState: device.state }
|
|
311
|
-
: lose('interface-detached');
|
|
312
|
-
}
|
|
313
|
-
#slotsFor(id) {
|
|
314
|
-
const existing = this.#slots.get(id);
|
|
315
|
-
if (existing !== undefined) {
|
|
316
|
-
return existing;
|
|
317
|
-
}
|
|
318
|
-
const created = { desired: null, applied: null, observed: null };
|
|
319
|
-
this.#slots.set(id, created);
|
|
320
|
-
return created;
|
|
321
|
-
}
|
|
322
|
-
}
|
|
323
|
-
/** The device the slots are about: where the bearer IS, else where it was asked for. */
|
|
324
|
-
function targetIfname(slots) {
|
|
325
|
-
const state = slots.applied?.configuration ?? slots.desired?.profile;
|
|
326
|
-
if (state === undefined) {
|
|
327
|
-
return undefined;
|
|
328
|
-
}
|
|
329
|
-
return state.kind === 'bound' ? state.binding.deviceIfname : state.deviceIfname;
|
|
330
|
-
}
|
|
331
|
-
function observedBearer(device) {
|
|
332
|
-
const active = device.activeConnection;
|
|
333
|
-
return active === undefined
|
|
334
|
-
? unboundBearer(device.ifname)
|
|
335
|
-
: boundBearer({
|
|
336
|
-
connectionId: active.connectionId,
|
|
337
|
-
deviceIfname: device.ifname,
|
|
338
|
-
apn: active.apn,
|
|
339
|
-
autoConfig: active.autoConfig,
|
|
340
|
-
homeOnly: active.homeOnly,
|
|
341
|
-
});
|
|
342
221
|
}
|
|
343
222
|
function refuse(reason, message) {
|
|
344
223
|
return { ok: false, reason, receipt: receipt('connection', 'failed', message) };
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import type { NormalizationContext, StateDivergence } from '../../observations/index.js';
|
|
2
|
+
import type { ConnectionId } from '../../ports/index.js';
|
|
3
|
+
import type { ConnectionSlots } from './state.js';
|
|
4
|
+
export declare function projectDivergence(slotsByConnection: ReadonlyMap<ConnectionId, ConnectionSlots>, id: ConnectionId, context: NormalizationContext): StateDivergence | null;
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { describeStateDivergence } from '../../observations/index.js';
|
|
2
|
+
import { projectStateView } from './projection.js';
|
|
3
|
+
import { nmBearerStateEquals } from './types.js';
|
|
4
|
+
export function projectDivergence(slotsByConnection, id, context) {
|
|
5
|
+
const view = projectStateView(slotsByConnection, id, context);
|
|
6
|
+
return view === null ? null : describeStateDivergence(view, nmBearerStateEquals);
|
|
7
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { DeviceGeneration } from '../../domain/index.js';
|
|
2
|
+
import type { ConnectionId } from '../../ports/index.js';
|
|
3
|
+
import type { ConnectionSlots } from './state.js';
|
|
4
|
+
import type { NmObservationInput, NmObservationResult } from './types.js';
|
|
5
|
+
export interface ObservationFold {
|
|
6
|
+
readonly generation: DeviceGeneration;
|
|
7
|
+
readonly result: NmObservationResult;
|
|
8
|
+
}
|
|
9
|
+
export declare function foldObservation(slotsByConnection: Map<ConnectionId, ConnectionSlots>, observedGeneration: DeviceGeneration | null, input: NmObservationInput): ObservationFold;
|