Part of the Librescoot open-source platform.
The Librescoot Modem Service is a Go-based monitoring tool designed to track and manage cellular modem connectivity using ModemManager, with state synchronization through Redis.
It is a free and open replacement for the unu-modem service on unu's Scooter Pro.
- Real-time modem state monitoring
- Retrieval of modem connectivity information
- Public and interface IP address tracking
- Signal quality and access technology reporting
- SMS send and receive via Redis
- Redis-based state synchronization and pub/sub notifications
- ModemManager (
mmcli) - Redis
- gpsd
Assuming your mdb is connected via USB Ethernet as 192.168.7.1 and aliased as mdb;
make dist
scp modem-service-arm-dist root@mdb:/usr/bin/modem-serviceThe service supports the following command-line flags:
| Flag | Default Value | Description |
|---|---|---|
-redis-url |
redis://127.0.0.1:6379 |
Redis URL |
-gpsd-server |
localhost:2947 |
GPSD server address |
-polling-time |
5s |
Polling interval for modem checks |
-internet-check-time |
30s |
Interval for local layer checks, and the base interval for the reachability probe |
-internet-check-max-interval |
5m |
Upper bound for probe backoff while healthy. Local layer checks are unaffected and still run every tick |
-connectivity-targets |
8.8.8.8:53,1.1.1.1:53,9.9.9.9:53,208.67.222.222:53 |
Fallback probe targets, used only when no network-assigned resolver answers. Fleets on a private APN should point this at something actually reachable |
-interface |
wwan0 |
Network interface to monitor |
-sms-keepalive |
false |
Keep the CS (SGs) registration alive for SMS delivery via periodic self-calls |
The service monitors and publishes the following modem state attributes:
- Connection status
- Public IP address
- Interface IP address
- Access technology
- Signal quality
- IMEI
- ICCID
The service configures the modem location ports to unmanaged mode for gpsd use. Position, altitude, speed and course information is forwarded from gpsd to Redis for consumption by other components.
The service uses Redis to:
- Store current modem state
- Publish state change notifications on the
internetchannel - Publish GPS information
- Register a power inhibitor in
power:inhibitswhile the modem is powered
The service maintains various Redis hashes:
modem-health(normal,recovering,recovery-failed-waiting-reboot,permanent-failure-needs-replacement)reachability— why the probe reports what it doesok— a target answeredunreachable— nothing answered, but the local stack is healthy. Normal and permanent on a fleet whose APN only routes to a private range; never a reason to touch the modemno-path— a local layer is failing, which is a real fault
link-layer—ok, or the lowest failing layer and the reason (e.g.bearer: bearer not connected)modem-state(off,disconnected,connected, orUNKNOWN)ip-address(external IPv4 address orUNKNOWN)access-tech(access tech, depending on modem & SIM support)"UNKNOWN""2G"/"GSM""3G"/"UMTS""4G"/"LTE""5G"
signal-quality(0-100, in %, or 255 if UNKNOWN)sim-imeiIMEI (unique hardware identifier) (this actually identifies the modem, but the name is kept for backward compatibility)sim-imsiIMSI (unique subscriber identity)sim-iccidICCID (unique SIM card identifier)
power-state(on/off/low-power)sim-state(active/locked/missing/unknown)sim-lock(enabled/disabled/unknown)pin-actionoutcome of the last SIM PIN reconcile cycle:unconfigured- nocellular.sim-pinsettingok- SIM is in the desired stateunlocked- service just sent the PIN to unlocklock-enabled- service just enabled PIN lock with the configured PINwrong-pin- last attempt was rejected; service will not retry until restartlow-retries-bail- retries < 3, refusing to attempt to avoid PUK lockpuk-required- SIM is in PUK state, manual recovery needederror- D-Bus call failed (see journal)
operator-name(current network operator name)operator-code(current network operator code)is-roaming(true/false)registration-fail(reason if registration failed)
When cellular.sim-pin is set in the settings Redis hash (typically via
/data/settings.toml or the mobile app), modem-service will:
- send the PIN to unlock the SIM if it's PIN-locked, or
- enable PIN lock with the configured PIN if the SIM has the lock disabled.
A wrong PIN attempt consumes one retry. To prevent the SIM from ever entering
PUK state through the service, modem-service refuses to attempt a PIN unless
unlock-retries["sim-pin"] is at the maximum (3) and the service has not
already failed an attempt this run. To recover after a wrong-PIN configuration,
fix the value, then unlock the SIM once manually (e.g.
mmcli -i 0 --send-pin=CORRECT) — that restores the retry counter and
modem-service will resume normally.
Location information is tracked in a Redis hash gps with the keys:
latitude,longitudeCurrent GPS lat/lon with 6 decimalsaltitudeCurrent GPS altitudespeedCurrent GPS speed in km/hcourseCurrent GPS course (heading) in degreestimestampGPS timestampstateGPS state with the following possible values:offGPS is disabled (initial state)searchingActively searching for GPS signalfix-establishedValid GPS fix obtained (2D or 3D)errorGPS configuration or connection failed
GPS state transitions occur when:
- Enabling GPS:
off→searching - Getting first fix:
searching→fix-established - Losing fix:
fix-established→searching - Configuration failure:
searching→error - Disabling GPS: any state →
off
Latest-value convenience state only. Per-message data lives on the
sms:received / sms:sent streams (see below); don't build anything that
needs every message on this hash.
statesend state of the last outbound message, published as astatenotification on thesmschannel:idle- no send in progress (also the post-success state)sending- an outbound message is being transmittederror- the last send failed (see thesms:sentstream for the reason)
last-sent-torecipient number of the last successfully sent SMSlast-sent-attimestamp (RFC 3339) the last send completedlast-received-fromsender number of the last inbound SMSlast-received-textbody of the last inbound SMSlast-received-attimestamp (RFC 3339) the last inbound SMS arrivedunread-countnumber of inbound messages received since service start
The last-received-* fields are refreshed silently; the per-message
notification is the dedicated sms:received channel.
Per-message events use Redis streams (capped at 100 entries each) plus a
dedicated pub/sub channel per direction. The channel payload is the stream
entry as JSON, including its stream id, so a subscriber can act on it
directly; a consumer that wants catch-up semantics instead XREADs from its
last-seen ID. Redis is not persistent on librescoot, so the streams are a
live window, not an archive - both are empty after a reboot.
One entry per inbound message: from, text, timestamp (RFC 3339).
Channel payload: {"id":"<stream-id>","from":...,"text":...,"timestamp":...}.
The XADD is the delivery commit point: a message is only deleted from modem storage after it has landed on the stream. If Redis is unreachable, the message stays in the modem and the periodic drain retries, so nothing is lost to a Redis hiccup. Messages that arrived while the service was offline are drained on startup.
One entry per terminal send outcome: request-id (the caller's id token
from the scooter:sms payload, empty if none), to, text, outcome
(sent/error), error (empty on success), timestamp. Channel payload is
the same entry as JSON including the stream id.
Some operators (seen on O2/Lebara DE) run a ~15-minute implicit-detach timer
on the CS registration that SMS delivery over LTE depends on; once it expires,
inbound SMS silently stops until the next combined attach. With
-sms-keepalive enabled, the service places a brief call to the scooter's own
number whenever no CS activity has happened for ~13 minutes, which resets the
operator's timer. The call never connects and costs nothing as long as the
SIM's mailbox is not set to pick up busy calls; check that before enabling.
Each self-call briefly falls back to 2G, and SIMs without a stored MSISDN
can't use the keepalive at all (the service logs this once and disables it).
Push a JSON payload onto the scooter:sms Redis list:
redis-cli LPUSH scooter:sms '{"to":"+4915112345678","text":"Hello from the scooter"}'
# With a correlation token, echoed back as request-id on sms:sent:
redis-cli LPUSH scooter:sms '{"id":"trip-42","to":"+4915112345678","text":"Hello"}'The service creates, transmits, and deletes the message, then records the
outcome on the sms:sent stream/channel and updates the sms hash (state,
last-sent-to, last-sent-at). This is the only command queue that takes a
JSON payload rather than a bare string, because a send needs both a recipient
and a body.
While the modem is powered, modem-service registers a block inhibitor in the
power:inhibits hash (who=librescoot-modem). This holds pm-service off from
suspending the MDB until the modem has been told to shut down and confirmed off:
on a disable command modem-service powers the modem down and only then removes
the inhibitor. The inhibitor is also dropped on clean shutdown, so a restart
cannot leave a stale entry blocking suspend.
modem-service \
-redis-url redis://redis.example.com:6379 \
-interface wwan0 \
-internet-check-time 45s \
-gpsd-server localhost:2947The service logs errors and attempts to maintain the most recent known state. If modem information cannot be retrieved, internet connectivity is lost, or GPS connection fails, the service will update the state accordingly and automatically attempt to recover.
This project is dual-licensed. The source code is available under the GNU Affero General Public License v3.0. The maintainers reserve the right to grant separate licenses for commercial distribution; please contact the maintainers to discuss commercial licensing.
