3 Commits
Author SHA1 Message Date
Joachim Wiberg 600483c35b modem: route NMEA from cellular modems to gpsd
Cellular modems (Quectel EM05/EM06/EM12, Sierra EM7565, ...) expose
GPS data as NMEA on a dedicated USB serial interface.  Until now we
relied on ModemManager's --location-enable-gps-nmea, which makes MM
hold the NMEA port and parse the stream itself — useful for mmcli
but a dead end for the rest of the system.  The result was that
modem GPS was visible only via 'show modem' and could not act as an
NTP fallback time source on devices without dedicated GPS hardware.

This commit reroutes the NMEA stream into the existing
gpsd → chronyd pipeline so cellular modems behave just like any
other NMEA-over-USB GPS dongle.  No changes to gpsd, chronyd, or
the ietf-hardware:gps component — the user activates it the same
way as for a dedicated receiver, by adding a 'gps' hardware
component that references /dev/gpsN.

This is Phase 2 of the modem GPS / location integration plan
(.notes/modem-gps-plan.md).

udev (src/modemd/77-mm-modem-gps.rules):
  - For known vendor:product:interface tuples, set ID_MM_PORT_IGNORE
    so ModemManager keeps off the NMEA port, and add
    SYMLINK+="gps%n" so gpsd's existing udev hook picks up the
    device automatically.
  - Interface-number match uses ENV{ID_USB_INTERFACE_NUM} rather
    than ATTRS{bInterfaceNumber}: udev requires all ATTRS{} matches
    in a rule to share one parent, but idVendor/idProduct live on
    the USB device while bInterfaceNumber lives on the USB
    interface.  ENV{} matching has no parent constraint and udev
    already populates ID_USB_INTERFACE_NUM from bInterfaceNumber.
  - Initial entries: Quectel EM05 (2c7c:0125), EM06-E (2c7c:0306),
    EM12-G (2c7c:0512), Sierra Wireless EM7565 (1199:9091).
    Easy to extend.

modemd (modemd/__init__.py):
  - GPS_AT_COMMANDS table maps manufacturer -> (enable, disable) AT
    command pair.  prepare_location() issues the vendor AT command
    via 'mmcli --command=AT+QGPS=1' (or AT+CGPS=1 for Sierra)
    instead of --location-enable-gps-{nmea,raw}.  Vendor lookup is
    substring-based ('Quectel' matches both 'Quectel' and 'Quectel
    Incorporated') because ModemManager normalises differently
    across firmware revisions.  For unrecognised vendors the
    high-level MM path is still used as a fallback, so nothing
    regresses for hardware not yet in the table.
  - The AT path runs based on the udev/vendor table, not on MM's
    --location-status capabilities.  Setting ID_MM_PORT_IGNORE on
    the NMEA port removes 'gps' from MM's capability list even
    though the GPS hardware is still there, so a capability gate
    would mistakenly skip GPS for the very modems we're targeting.
  - _location_capabilities() filters MM-managed sources so a single
    unsupported flag (e.g. CDMA on an LTE-only modem) doesn't fail
    the whole batched call.  Sources the modem doesn't support are
    silently skipped; a warning is logged only if the user's YANG
    config explicitly lists one.
  - Subprocess fan-out collapsed: agps-msa, agps-msb, 3gpp and cdma
    flags share a single batched mmcli invocation.
    prepare_location() drops from 5 subprocess calls to 1 (unknown
    vendor) or 2 (known vendor, AT + batched MM).
  - Stringly-typed if/elif source dispatch replaced with a
    dict-driven table (LOCATION_MM_FLAGS).
  - Two helpers eliminate the ['mmcli', '-m', self.path, ...]
    boilerplate at ~30 ModemThread call sites:
      self._mmcli(*args, check=True)   # runcmd wrapper
      self._mmclij(*args)              # runcmdj wrapper

build (package/feature-modem/Config.in):
  - Select BR2_PACKAGE_MODEM_MANAGER_ATVIADBUS so ModemManager
    accepts raw AT commands over D-Bus without being started in
    --debug mode.  Required by the AT path above; without it
    'mmcli --command' is rejected with MM_CORE_ERROR_UNAUTHORIZED.

Signed-off-by: Joachim Wiberg <troglobit@gmail.com>
2026-06-07 20:51:03 +02:00
Joachim Wiberg 1168c6c8e2 modemd: package Python scripts for compiled deployment
Python scripts without a .py extension are never cached as bytecode by
CPython — every invocation re-parses the source.  Packaging as a wheel
pre-compiles all modules to .pyc and installs thin import stubs as the
executables, matching the approach used by bin/show and statd.

Restructure the modemd Python scripts into a proper Python package
(src/modemd/modemd/): each script becomes a module with a main() entry
point.  pyproject.toml declares 11 entry points; the Buildroot
MODEMD_BUILD_PYTHON hook builds a wheel via PEP 517, then pyinstaller.py
installs compiled stubs to /usr/libexec/modemd/ and pre-compiled .pyc
modules to site-packages.

/sbin/modemd is now a symlink to the compiled stub.

Signed-off-by: Joachim Wiberg <troglobit@gmail.com>
2026-06-07 20:51:01 +02:00
Joachim Wiberg 314b0b425f Add cellular modem support via modemd daemon
Integrates the modem management subsystem, donated by Avvero Pty Ltd,
adapted to work on any platform without proprietary hardware (simctrl,
modules.json, /sys/class/sim/).

Components:
- package/feature-modem: Buildroot feature package pulling in modemd,
  ModemManager, libmbim, libqmi, and USB serial/MBIM/QMI kernel drivers
- package/modemd: Buildroot package for the modemd daemon and helpers
- src/modemd: modem management daemon — detects modems via ModemManager,
  connects bearers, configures IP addresses, handles SMS and location
- src/confd/src/modem.c: confd plugin — enables/disables modemd and
  modem-manager via sysrepo, forwards RPCs (restart/reset/send-sms)
- src/confd/src/interfaces.{c,h}: IFT_MODEM type for wwan* interfaces;
  confd manages address/route assignment, modem daemon owns link state
- src/confd/yang/confd/infix-modem.yang: YANG model for modem config and
  operational state (bearers, APN, bands, modes, location, SMS)
- src/statd/python/yanger/infix_modem.py: operational state collector
- patches/modem-manager: upstream patches for udev install path,
  multiplexed interface naming, and MBIM port trust from udev hint
  (the last fixes Dell DW5811e / Sierra EM7455 on cold boot)
- patches/libqmi: ignore sysfs in download mode

Platform portability fixes for boards without simctrl hardware:
- modem-info: fall back to /run/modems.json when /run/modules.json is
  absent; discover wwan interfaces via sysfs scan; provide synthetic SIM
  entry when /sys/class/sim/ is not present; fix dbg() to respect flag
- modemd: make --set-allowed-modes=any best-effort (some modems only
  support one mode combination and reject any change); skip mode set
  when current mode already matches requested mode
- sim-setup: guard simctrl open, return None when modules.json absent
  to skip SIM slot switching entirely on non-simctrl hardware

Signed-off-by: Joachim Wiberg <troglobit@gmail.com>
2026-06-07 20:50:28 +02:00