feat(lte): cellular telemetry, band details and roaming history (v0.23.12-beta)
Adds LTE support, requested in discussion #85 by a contributor running a MikroTik ATL 18 on a fixed antenna. Cellular was previously a blank page: the only occurrence of "lte" in the tree excluded LTE interfaces from DHCP binding. Built against two real field captures rather than documentation, which disagreed with the hardware in several places — the modem reports `data-class` not `access-technology`, `iccid` not `uicc`, `status: running` not `connected`, embeds units inside values (`-97dBm`, `17dB`), and omits `registration-status`, `pin-status` and `lac`/`tac` entirely. What an LTE interface reports depends on the modem chipset, not on RouterOS, so every field is optional by construction. Fixes a latent bug in the API client while it was in reach. A sentence carrying the same attribute twice kept only the last value, and a Cat-18 modem reports one `ca-band` per aggregated carrier. On the contributor's 4-carrier capture that yielded B3 + B20 — 30 MHz — on a device actually running 70 MHz, because the narrowest carrier happens to arrive last. Repeats are now preserved alongside the flat value, so existing callers are byte-for-byte unaffected. Signal quality is graded rather than merely displayed. RSRP alone misleads: the capture shows −97 dBm, nominally "fair", on a link running 256QAM at CQI 15 with two spatial streams. SINR leads the grade and RSRP moderates it. Surfaces the uplink anchor, which no MikroTik UI shows. Left on automatic a modem anchors to whichever band it hears loudest, often a narrow one, and aggregates wider bands for downlink only — but uplink rides the primary carrier alone, so upload stays capped. This is the contributor's actual reason for locking bands, and it reframes band selection as anchor control rather than coverage. Band changes, tower handovers and re-registrations are reconstructed by comparing consecutive polls, since scanning interrupts service and finds nothing at a site served by a single base station. Observed bands accumulate as the evidence a future band lock will be checked against. Band selection itself is deliberately absent: it needs Change Guard plus a free-space check, as the target hardware has 1.1 MB free of 16 MB flash. Also fixes an unrelated display bug in the RF Health channel map. The 2.4 GHz axis is labelled by channel, but ticks at 2400 and 2480 fall outside the channel plan and fell back to printing raw frequencies — and the 2400 label sat at exactly 0% with a centring transform, hanging half its width outside the card. 2.4 GHz is the only band with a tick at an edge. Unlabelled ticks are now dropped and edge labels anchor inward. - backend/src/utils/lte.ts — parsing, quality, change detection (59 tests) - RouterOSClient — repeated attributes preserved (6 tests) - lte_interfaces, lte_cell_history, lte_observed_bands - GET /devices/:id/lte, /lte/history, /lte/metrics - LteTab, gated on has_lte from the interface list - scripts/seed-lte-fixture.sql — a cellular device for local development Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
57d650c159
commit
2625fea8de
@@ -0,0 +1,5 @@
|
||||
-- Remove everything seed-lte-fixture.sql created. The device cascades to its
|
||||
-- interfaces, lte_interfaces, lte_cell_history and lte_observed_bands rows.
|
||||
DELETE FROM devices WHERE name LIKE 'fixture-lte-%';
|
||||
SELECT 'cleaned' AS status,
|
||||
(SELECT count(*) FROM devices WHERE name LIKE 'fixture-lte-%') AS remaining;
|
||||
@@ -0,0 +1,87 @@
|
||||
-- A cellular device for local development, reproducing a real field capture.
|
||||
--
|
||||
-- The fleet this platform is developed against contains no LTE hardware at all
|
||||
-- (49 ethernet interfaces, 7 wifi, zero cellular), which is exactly the position
|
||||
-- CAPsMAN support was written from before two bugs shipped. This seeds the shape
|
||||
-- of a real modem so the LTE paths can be exercised against actual endpoints.
|
||||
--
|
||||
-- Values come from a MikroTik ATL 18 (Quectel EG18-EA, Cat-18) on VIVACOM,
|
||||
-- contributed by @trackersoft in discussion #85. See
|
||||
-- backend/src/utils/__tests__/fixtures/lte-atl18.md for the raw capture.
|
||||
--
|
||||
-- What makes this shape worth reproducing:
|
||||
--
|
||||
-- * RSRP −97 dBm — "fair" on its own — on a link running 256QAM at CQI 15 with
|
||||
-- two spatial streams, which is the modem at its ceiling. Any UI that grades
|
||||
-- on RSRP alone gets this device visibly wrong.
|
||||
-- * a primary carrier plus an aggregated one, so carrier-aggregation rendering
|
||||
-- is exercised rather than assumed
|
||||
-- * a configured band allow-list (1,3,7) that is wider than the bands actually
|
||||
-- observed, which is the state a band-lock warning has to reason about
|
||||
-- * tower movement history, which cannot be recovered after the fact
|
||||
--
|
||||
-- Everything is prefixed `fixture-` and removed by seed-lte-fixture-clean.sql.
|
||||
|
||||
BEGIN;
|
||||
|
||||
-- A cellular CPE. Deliberately device_type 'router': cellular is a property of
|
||||
-- the hardware, and gating the LTE tab on device_type would hide it here.
|
||||
INSERT INTO devices (name, ip_address, api_username, api_password_encrypted,
|
||||
device_type, status, model, has_lte)
|
||||
VALUES ('fixture-lte-cpe-001', '10.251.0.1', 'admin', 'x',
|
||||
'router', 'online', 'ATL18', TRUE)
|
||||
ON CONFLICT DO NOTHING;
|
||||
|
||||
-- The interface row is what collectLte keys off, so the modem is only contacted
|
||||
-- on devices that actually have one.
|
||||
INSERT INTO interfaces (device_id, name, type, mtu, running, disabled, updated_at)
|
||||
SELECT id, 'lte1', 'lte', 1500, TRUE, FALSE, NOW()
|
||||
FROM devices WHERE name = 'fixture-lte-cpe-001'
|
||||
ON CONFLICT (device_id, name) DO UPDATE SET type = 'lte', running = TRUE;
|
||||
|
||||
-- Current state, verbatim from the capture.
|
||||
INSERT INTO lte_interfaces (
|
||||
device_id, interface_name, status, data_class, modem_model, modem_revision,
|
||||
operator, cell_id, enb_id, sector_id, phy_cell_id, session_uptime_s,
|
||||
primary_band, ca_bands, rssi, rsrp, rsrq, sinr, cqi, rank_indicator, mcs,
|
||||
dl_modulation, quality, allowed_bands, network_mode, apn_profiles,
|
||||
allow_roaming, updated_at)
|
||||
SELECT id, 'lte1', 'running', 'LTE', 'EG18-EA', 'EG18EAPAR01A14M4G',
|
||||
'VIVACOM', '123456', '1234', '12', '190', 285479,
|
||||
'{"band":1,"bandwidthMhz":20,"earfcn":500,"phyCellId":190,
|
||||
"raw":"B1@20Mhz earfcn: 500 phy-cellid: 190"}'::jsonb,
|
||||
'[{"band":3,"bandwidthMhz":20,"earfcn":1800,"phyCellId":190,
|
||||
"raw":"B3@20Mhz earfcn: 1800 phy-cellid: 190"}]'::jsonb,
|
||||
-69, -97, -9, 17, 15, 2, 20,
|
||||
'256qam', 'good', '1,3,7', 'lte', 'default', FALSE, NOW()
|
||||
FROM devices WHERE name = 'fixture-lte-cpe-001'
|
||||
ON CONFLICT (device_id, interface_name) DO UPDATE SET updated_at = NOW();
|
||||
|
||||
-- Bands seen serving this device. Note B7 is *allowed* but has never been
|
||||
-- observed — locking to it is the case a future pre-flight has to catch.
|
||||
INSERT INTO lte_observed_bands (device_id, interface_name, band, observations, first_seen, last_seen)
|
||||
SELECT d.id, 'lte1', b.band, b.n, NOW() - INTERVAL '6 days', NOW()
|
||||
FROM devices d, (VALUES (1, 4120), (3, 3980)) AS b(band, n)
|
||||
WHERE d.name = 'fixture-lte-cpe-001'
|
||||
ON CONFLICT (device_id, interface_name, band) DO NOTHING;
|
||||
|
||||
-- Movement over the last day: a handover, a re-registration, and an aggregated
|
||||
-- carrier dropping away and returning.
|
||||
INSERT INTO lte_cell_history (device_id, interface_name, at, kind, detail, cell_id, enb_id, bands, rsrp, sinr)
|
||||
SELECT d.id, 'lte1', NOW() - h.ago, h.kind, h.detail, h.cell, '1234', h.bands, h.rsrp, h.sinr
|
||||
FROM devices d, (VALUES
|
||||
(INTERVAL '35 minutes', 'band-change', 'Bands changed from 1 to 1,3', '123456', '1,3', -97::real, 17::real),
|
||||
(INTERVAL '2 hours', 'band-change', 'Bands changed from 1,3 to 1', '123456', '1', -101::real, 12::real),
|
||||
(INTERVAL '7 hours', 'handover', 'Cell changed from 998877 to 123456', '123456', '1,3', -95::real, 18::real),
|
||||
(INTERVAL '7 hours', 'session-reset', 'Session restarted — uptime fell from 210433s to 12s',
|
||||
'998877', '1', -104::real, 9::real),
|
||||
(INTERVAL '19 hours', 'handover', 'Cell changed from 123456 to 998877', '998877', '1', -103::real, 10::real)
|
||||
) AS h(ago, kind, detail, cell, bands, rsrp, sinr)
|
||||
WHERE d.name = 'fixture-lte-cpe-001';
|
||||
|
||||
COMMIT;
|
||||
|
||||
SELECT 'seeded' AS status,
|
||||
(SELECT count(*) FROM lte_interfaces) AS lte_interfaces,
|
||||
(SELECT count(*) FROM lte_cell_history) AS history_rows,
|
||||
(SELECT count(*) FROM lte_observed_bands) AS observed_bands;
|
||||
Reference in New Issue
Block a user