Librescoot Service Interaction Map¶
Generated from source analysis of all service repositories.
Architecture Overview¶
Redis (192.168.7.1:6379)
========================
Hashes (state storage + pub/sub notification)
─────────────────────────────────────────────
vehicle-service ──writes──> vehicle, system, ota, dashboard, buttons, vehicle:fault, events:faults
battery-service ──writes──> battery:0, battery:1, battery:N:fault
ecu-service ────writes──> engine-ecu (WARNING: publishes to broken channels, see gaps)
keycard-service ─writes──> keycard (with 10s TTL)
bluetooth-service writes──> ble, ble:fault, system (mdb-version, nrf-fw-version), engine-ecu (odometer)
pm-service ─────writes──> power-manager, power-manager:busy-services, system (cpu:governor)
modem-service ──writes──> internet, modem, gps, internet:fault, events:faults
dbc-backlight ──writes──> dashboard (backlight, brightness, from the OPT3001)
alarm-service ──writes──> alarm
version-service writes──> os-release (one-shot at boot, no pub/sub notification)
settings-service writes──> settings (WARNING: no PUBLISH on initial load, see gaps)
update-service ──writes──> ota (status:*, update-version:*, download-progress:*, etc.)
ums-service ────writes──> usb
scootui ────────writes──> dashboard (ready, serial-number)
Lists (command queues, LPUSH/BRPOP)
────────────────────────────────────
scooter:state ── vehicle-service reads ── lsc, scootui, uplink-service, alarm-service write
scooter:seatbox ── vehicle-service reads ── lsc, scootui, uplink-service write
scooter:horn ── vehicle-service reads ── lsc, scootui, uplink-service, alarm-service write
scooter:blinker ── vehicle-service reads ── lsc, scootui, uplink-service, alarm-service write
scooter:led:cue ── vehicle-service reads ── lsc writes
scooter:led:fade── vehicle-service reads ── lsc writes
scooter:update ── vehicle-service reads ── update-service writes (start-dbc, complete-dbc)
scooter:hardware── vehicle-service reads ── lsc, scootui, uplink-service write
scooter:power ── pm-service reads ── lsc, update-service, uplink-service write
scooter:governor── pm-service reads ── vehicle-service writes
scooter:modem ── modem-service reads ── pm-service writes ("disable")
scooter:bluetooth─ bluetooth-service reads─ (firmware-update and BLE commands)
scooter:alarm ── alarm-service reads ── lsc writes
settings:overlay── settings-service reads ── lsc writes (apply:service, clear:service)
scooter:update:<component> ── update-service reads ── update-service writes (check-now)
power:inhibits ── pm-service inhibitor redis listener reads ── (external inhibitors write)
Pub/Sub channels (notifications only; consumers HGET after receiving)
──────────────────────────────────────────────────────────────────────
vehicle ← published by vehicle-service (payload = field name changed)
battery:0 ← published by battery-service (payload = field name or "fault")
battery:1 ← published by battery-service
engine-ecu ← published by bluetooth-service (odometer), NOT by ecu-service (see gaps)
keycard ← published by keycard-service
dashboard ← published by scootui (ready, serial-number, backlight-enabled), dbc-backlight
power-manager ← published by pm-service
system ← published by vehicle-service (cpu:governor), bluetooth-service (mdb-version, nrf-fw-version)
ota ← published by update-service
internet ← published by modem-service
modem ← published by modem-service
gps ← published by modem-service
alarm ← published by alarm-service
ble ← published by bluetooth-service
usb ← published by ums-service
settings ← published externally (scootui, UMS updates); NOT by settings-service on load
motion:sensors ← published by motion-service (10 Hz IMU stream)
motion:heading ← published by motion-service (5 Hz tilt-compensated mag heading)
motion:interrupt ← published by motion-service (motion-engine edges + wake-hibernation)
motion:ready ← published by motion-service on startup
motion:rpc ← request channel for motion-service RPC (alarm-service Calls prepare-hibernation)
buttons ← published by vehicle-service (physical button events)
Sets (fault tracking)
─────────────────────
vehicle:fault ── written by vehicle-service
battery:0:fault── written by battery-service
battery:1:fault── written by battery-service
internet:fault ── written by modem-service
ble:fault ── written by bluetooth-service
Streams (event log)
───────────────────
events:faults ── written by vehicle-service, modem-service; read by uplink-service (HGetAll)
Per-Service Detail¶
vehicle-service¶
Writes (hash → pub/sub channel):
| Hash | Fields | Channel |
|------|--------|---------|
| vehicle | state, state:timestamp, blinker:switch, blinker:state, brake:left, brake:right, seatbox:lock, kickstand, handlebar:lock-sensor, handlebar:position, dashboard:power, dbc-updating, update:status, auto-standby-remaining (deprecated), auto-standby-deadline | vehicle |
| system | cpu:governor | system |
| ota | standby-timer-start | ota |
| dashboard | ready (deletes on standby) | dashboard |
| buttons | horn:on, horn:off, seatbox:on, seatbox:off | buttons |
| vehicle:fault | fault codes (Redis Set) | vehicle (payload: "fault") |
| events:faults | fault events (Redis Stream) | — |
Reads:
vehicle/state(self-reads for restart recovery)vehicle/dbc-updating,vehicle/dashboard:power(self-reads)dashboard/ready(from scootui)keycard/authentication(from keycard-service)settings/*(any field, for scooter.*, alarm settings via watch)ota/status:mdb,ota/status:dbc(checks OTA status on update commands)ota/standby-timer-start(indirectly via update-service)
Consumes queues (BRPOP):
scooter:state→ "unlock", "lock", "lock-hibernate", "force-lock"scooter:seatbox→ "open"scooter:horn→ "on", "off"scooter:blinker→ "off", "left", "right", "both"scooter:led:cue→ integer cue indexscooter:led:fade→ "channel:index" stringscooter:update→ "start", "complete", "start-dbc", "complete-dbc"scooter:hardware→ "dashboard:on", "dashboard:off", "engine:on", "engine:off", "handlebar:lock", "handlebar:unlock" (and :force variants)
DBC update power hold. start-dbc makes vehicle-service keep dashboard power up and
defer the power cut that entering stand-by would normally perform, until complete-dbc
arrives. A single watchdog bounds that hold: it expires 15 minutes after the last change to any
ota hash field ending in :dbc, and then clears the stuck state. There is no
heartbeat-driven short timeout in this release.
Produces queues (LPUSH):
scooter:governor→ "ondemand"/"powersave" (send to pm-service via intermediate; actually uses its own queue handler — see note)
Hardware access: GPIO via /dev/gpiochip*, PWM LED via kernel driver
battery-service¶
Writes:
| Hash | Fields | Channel |
|------|--------|---------|
| battery:0 | present, state, voltage, current, charge, temperature:0–3, temperature-state, cycle-count, state-of-health, serial-number, manufacturing-date, fw-version | battery:0 (on change: present, state, charge, temperature-state, fault) |
| battery:1 | same fields | battery:1 |
| battery:N:fault | fault codes (Redis Set) | battery:N (payload: "fault") |
Reads:
vehicle/state(vehicle state for active/standby mode)vehicle/seatbox:lock(seatbox state for enabling battery readers)settings/scooter.battery-keep-active-on-seatbox-opensettings/scooter.dual-battery
Subscribes to:
vehiclechannel (payload: "state", "seatbox:lock")settingschannel (payload: "scooter.battery-keep-active-on-seatbox-open", "scooter.dual-battery")
Hardware access: NFC via /dev/i2c-*, PN7150 controller
ecu-service¶
Writes:
| Hash | Fields | Channel |
|------|--------|---------|
| engine-ecu | motor:voltage, motor:current, rpm, speed, raw-speed, throttle, brake, power, energy:consumed, energy:recovered | (broken: see gaps) |
| engine-ecu | temperature, fault:code, fault:description | (no publish) |
| engine-ecu | odometer | (broken: see gaps) |
| engine-ecu | kers, boost | (broken: see gaps) |
| engine-ecu | kers-reason-off | (broken: see gaps) |
| engine-ecu | gear, fw-version | (no publish) |
Reads:
vehicle/state(for engine ready state)battery:0/state,battery:0/temperature-statebattery:1/state,battery:1/temperature-statesettings/engine-ecu.boost
Subscribes to:
vehiclechannel (payload: "state")battery:0,battery:1channels (all fields → HGETALL)settingschannel (payload: "engine-ecu.boost")
Hardware access: CAN bus via /dev/can*
keycard-service¶
Writes:
| Hash | Fields | Channel |
|------|--------|---------|
| keycard | authentication ("passed"), type ("scooter"), uid | keycard (payload: "authentication") |
Note: Sets 10-second TTL on keycard hash after publish.
Reads: None (hardware-driven)
Hardware access: NFC via PN7150, I2C LED controller LP5562
bluetooth-service¶
Writes:
| Hash | Fields | Channel |
|------|--------|---------|
| system | mdb-version (from nRF), nrf-fw-version | system |
| engine-ecu | odometer (from nRF) | engine-ecu |
| ble | status fields, pin-code, firmware-update-status | ble |
| ble:fault | fault codes (Redis Set) | ble (payload: "fault") |
Reads:
vehicle/state,vehicle/seatbox:lock,vehicle/handlebar:lock-sensorbattery:0/state,battery:0/present,battery:0/charge,battery:0/cycle-countbattery:1/state,battery:1/present,battery:1/charge,battery:1/cycle-countpower-manager/stateengine-ecu/odometersystem/mdb-versionble/pin-code
Subscribes to:
vehicle,battery:0,battery:1,power-manager,engine-ecu,system,blechannels
Consumes queues:
scooter:bluetooth→ BLE commands ("advertising-start-with-whitelisting", "advertising-restart-no-whitelisting", "advertising-stop", "delete-bond", "delete-all-bonds", "remove", "firmware-update")
Hardware access: UART via usock to nRF52 at /var/run/bluetooth-service.sock or similar
pm-service¶
Writes:
| Hash | Fields | Channel |
|------|--------|---------|
| power-manager | state ("running", "suspending", "hibernating", etc.), wakeup-source | power-manager |
| power-manager:busy-services | inhibitor entries (field = "who why what", value = type) | power-manager:busy-services (payload: "replaced"/"cleared") |
| system | cpu:governor | system |
Reads:
vehicle/state(watches for standby/parked/ready-to-drive transitions)battery:0/state(watches for active/inactive)settings/hibernation-timer
Subscribes to:
vehiclechannel via HashWatcherbattery:0channel via HashWatchersettingschannel (for hibernation-timer)power:inhibitshash (inhibitor redis listener)
Consumes queues:
scooter:power→ "run", "suspend", "hibernate", "hibernate-manual", "hibernate-timer", "reboot"scooter:governor→ "ondemand", "powersave", "performance"
Produces queues:
scooter:modem→ "disable" (to disable modem before power state change)
Hardware access: /sys/power/pm_wakeup_irq, /sys/class/tty/*/power/wakeup, systemd D-Bus
update-service¶
Writes:
| Hash | Fields | Channel |
|------|--------|---------|
| ota | status:<component>, update-version:<component>, download-progress:<component>, download-bytes:<component>, download-total:<component>, install-progress:<component>, error:<component>, error-message:<component>, update-method:<component> | ota |
| settings | updates.<component>.last-check-time | settings |
Reads:
vehicle/state(watches; state-based update triggering)vehicle/state:timestamp(for standby duration check)ota/standby-timer-start(from vehicle-service, for timing)ota/status:<component>(self-read)ota/update-version:<component>,ota/update-method:<component>version:<component>/version_id,version:<component>/variant_idsettings/updates.<component>.methodsettings/updates.<component>.last-check-time
Subscribes to:
vehiclechannel (via HashWatcher)otachannel (via HashWatcher)settingschannel (via HashWatcher)
Consumes queues:
scooter:update:<component>→ "check-now"
Produces queues:
scooter:update→ "start-dbc", "complete-dbc" (to vehicle-service)scooter:power→ "reboot" (after MDB update)
External: GitHub Releases API, Mender
modem-service¶
Writes:
| Hash | Fields | Channel |
|------|--------|---------|
| internet | connectivity fields | internet |
| modem | modem status fields | modem |
| gps | location data (lat, lon, accuracy, speed, heading, altitude, updated, timestamp) | gps (payload: "timestamp" on recovery, no publish on regular update) |
| internet:fault | fault codes (Redis Set) | internet (payload: "fault") |
| events:faults | modem fault events (Redis Stream) | — |
Reads:
vehicle/state(via HashWatcher, for hibernation behavior)
Consumes queues:
scooter:modem→ "disable", "enable"
Hardware access: ModemManager via D-Bus, GPIO modem power, USB
version-service¶
Writes (one-shot at boot, no pub/sub publish):
| Hash | Fields |
|------|--------|
| os-release | All fields from /etc/os-release (lowercase), serial_number, serial_number_real |
Note: version-service does NOT publish to the os-release channel. It runs once at boot via systemd (Type=oneshot) and exits. The system hash for mdb-version is written by bluetooth-service (from nRF).
Hardware access: /sys/bus/nvmem/devices/imx-ocotp0/nvmem, /sys/fsl_otp/HW_OCOTP_CFG0/CFG1
settings-service¶
Writes:
| Hash | Fields | Channel |
|------|--------|---------|
| settings | All TOML fields: scooter.*, cellular.*, updates.*, dashboard.*, alarm.*; overlay-injected values (in-memory only, not persisted); dashboard.service-mode-active status field | NONE (critical gap — see gaps section) |
Reads:
settings/*(all fields, to save back to TOML)
Subscribes to:
settingschannel (to detect changes and flush to TOML)internetchannel (for WireGuard manager)
Consumes queues (BRPOP):
settings:overlay→apply:service,clear:service— owns the overlay command list; theserviceoverlay fans out to pm-service, vehicle-service, alarm-service, and scootui purely via existingsettings-channel reactions
Hardware access: /data/settings.toml, NetworkManager D-Bus
alarm-service¶
Writes:
| Hash | Fields | Channel |
|------|--------|---------|
| alarm | status ("disabled", "disarmed", "armed", "level-1-triggered", "level-2-triggered") | alarm |
Produces queues (LPUSH):
scooter:horn→ "on", "off"scooter:blinker→ "both", "off"
Publishes:
alarmhashstatusfield — motion-service watches this and reactively re-derives the BMX055 chip profile
Calls (synchronous RPC via redis-ipc CallMethod):
motion:rpc/prepare-hibernation— gates pm-service's hibernation entry on motion-service confirming the chip is in armed-hibernation profile
Reads:
vehicle/state(via HashWatcher)vehicle/seatbox:lock(via HashWatcher)settings/alarm.enabled,settings/alarm.honk,settings/alarm.durationsettings/alarm.seatbox-trigger,settings/alarm.hairtrigger,settings/alarm.hairtrigger-durationsettings/alarm.l1-cooldownmotion/wake-cause(once on startup — durable backstop for wake-from-hibernation indicator)
Subscribes to:
vehiclechannel (state, seatbox:opened event, seatbox:lock)settingschannelpower-managerchannel (state field — drives hibernation handshake)motion:interruptchannel (JSON envelope{type, timestamp, engine})
Consumes queues:
scooter:alarm→ "enable", "disable", "start:N", "stop"
Hardware access: I2C BMX055 accelerometer/gyroscope at /dev/i2c-3
dbc-backlight-service¶
Writes:
| Hash | Fields | Channel |
|------|--------|---------|
| dashboard | backlight (int 0-10240), brightness (float lux) | dashboard |
Reads:
dashboard/backlight-enabled(override; absent means enabled)settings/dashboard.backlight-mode(auto, low, medium, high; absent means auto)
Subscribes: dashboard (payload backlight-enabled), settings (payload dashboard.backlight-mode)
Hardware access: OPT3001 ambient light sensor via IIO, and /sys/class/backlight/backlight/brightness
The service reads the sensor itself; there is no separate illumination service on
the DBC. It publishes the reading as dashboard/brightness for scootui-qt's auto
light/dark theme, which is why the field keeps updating even when the backlight is
overridden off or pinned to a fixed level.
scootui (Flutter dashboard)¶
Writes:
| Hash | Fields | Channel |
|------|--------|---------|
| dashboard | ready ("true"), serial-number | dashboard |
Reads (HGETALL / HGET):
vehicle,battery:0,battery:1,engine-ecu,power-manager,internet,modem,gps,keycard,ble,dashboard,system
Subscribes to (pub/sub):
- Multiple channels for real-time updates (subscription details in app UI layers)
Produces queues (LPUSH):
scooter:state→ "lock", "unlock", "lock-hibernate"scooter:seatbox→ "open"scooter:horn→ "on", "off"scooter:hardware→ various- (via
push()method)
Publishes:
buttonschannel (button events)
Hardware access: Display via Flutter framebuffer/DRM backend
uplink-service¶
Reads (telemetry, HGETALL):
vehicle,battery:0,battery:1,aux-battery,cb-battery,engine-ecu,power-manager,internet,modem,gps,keycard,ble,dashboard,system
Produces queues (LPUSH) — from remote server commands:
scooter:state→ "unlock", "lock", "lock-hibernate", "force-lock"scooter:seatbox→ "open"scooter:horn→ "on", "off"scooter:blinker→ "left", "right", "both", "off"scooter:hardware→ "dashboard:on/off", "engine:on/off", "handlebar:lock/unlock"scooter:power→ "reboot", "hibernate", "hibernate-manual"
ums-service¶
Writes:
| Hash | Fields | Channel |
|------|--------|---------|
| usb | mode ("normal", "ums", "ums-by-dbc") | usb |
Reads:
usb/mode(via HashWatcher, to trigger mode changes)
Hardware access: USB gadget via configfs, /data/dbc/
lsc (CLI tool)¶
Reads (HGETALL / HGET): All state hashes for display
Produces queues (LPUSH):
scooter:state,scooter:seatbox,scooter:horn,scooter:blinker,scooter:hardware,scooter:power,scooter:alarm,scooter:led:cue,scooter:led:fade,settings:overlay(apply:service,clear:servicevialsc service-mode on|off)
Complete Redis Key Inventory¶
Hash Keys¶
| Key | Owner (Writer) | Primary Readers |
|---|---|---|
vehicle |
vehicle-service | battery-service, ecu-service, pm-service, bluetooth-service, modem-service, alarm-service, update-service, uplink-service, scootui, lsc |
battery:0 |
battery-service | ecu-service, pm-service, bluetooth-service, uplink-service, scootui |
battery:1 |
battery-service | ecu-service, bluetooth-service, uplink-service, scootui |
engine-ecu |
ecu-service (data), bluetooth-service (odometer) | bluetooth-service, uplink-service, scootui |
keycard |
keycard-service (10s TTL) | vehicle-service |
ble |
bluetooth-service | scootui, uplink-service |
dashboard |
scootui (ready/serial-number/backlight-enabled), dbc-backlight (backlight/brightness), vehicle-service (backlight-enabled) | vehicle-service (ready), dbc-backlight (backlight-enabled), scootui (brightness) |
power-manager |
pm-service | bluetooth-service, scootui, uplink-service |
power-manager:busy-services |
pm-service | monitoring only |
system |
vehicle-service (cpu:governor), bluetooth-service (mdb-version, nrf-fw-version), pm-service (cpu:governor) | bluetooth-service, uplink-service, scootui |
ota |
update-service | vehicle-service (reads status), update-service (self), scootui |
internet |
modem-service | scootui, uplink-service, pm-service |
modem |
modem-service | scootui, uplink-service |
gps |
modem-service | scootui, uplink-service |
alarm |
alarm-service | lsc, monitoring |
settings |
settings-service (from TOML), update-service (last-check-time) | vehicle-service, battery-service, ecu-service, alarm-service, pm-service |
os-release |
version-service (one-shot) | Nothing (dead key — see gaps) |
usb |
ums-service | ums-service (self), scootui |
motion |
motion-service | alarm-service (wake-cause), scootui (heading), monitoring |
Set Keys¶
| Key | Owner | Readers |
|---|---|---|
vehicle:fault |
vehicle-service | scootui, uplink-service |
battery:0:fault |
battery-service | scootui |
battery:1:fault |
battery-service | scootui |
internet:fault |
modem-service | scootui |
ble:fault |
bluetooth-service | scootui |
Stream Keys¶
| Key | Writers | Readers |
|---|---|---|
events:faults |
vehicle-service, modem-service | uplink-service (via HGetAll — see gaps) |
List Keys (Command Queues)¶
| Key | Consumer | Producers |
|---|---|---|
scooter:state |
vehicle-service | lsc, scootui, uplink-service |
scooter:seatbox |
vehicle-service | lsc, scootui, uplink-service |
scooter:horn |
vehicle-service | lsc, scootui, uplink-service, alarm-service |
scooter:blinker |
vehicle-service | lsc, scootui, uplink-service, alarm-service |
scooter:led:cue |
vehicle-service | lsc |
scooter:led:fade |
vehicle-service | lsc |
scooter:update |
vehicle-service | update-service |
scooter:update:<component> |
update-service | update-service (self check-now) |
scooter:hardware |
vehicle-service | lsc, scootui, uplink-service |
scooter:power |
pm-service | lsc, update-service, uplink-service |
scooter:governor |
pm-service | vehicle-service (sends cpu governor changes) |
scooter:modem |
modem-service | pm-service |
scooter:bluetooth |
bluetooth-service | external/lsc |
scooter:alarm |
alarm-service | lsc |
settings:overlay |
settings-service | lsc |
power:inhibits |
pm-service inhibitor manager | update-service, vehicle-service, modem-service (hold pm inhibitors) |
Inhibitor ids worth knowing, because fleet tooling matches on their prefixes:
| id | type | held by | while |
|---|---|---|---|
download:{component}, preparing:{component}, install:{component} |
delay | update-service | the corresponding update phase (advisory only, see below) |
dbc-update |
suspend-only | vehicle-service | a DBC update holds dashboard power |
It is suspend-only rather than block so a |
|||
| hibernate is never obstructed, and it is bounded by the download's own wall-clock cap. | |||
| update-service declines to take it at all when that cap is disabled | |||
(download-max-duration set to 0), since an unbounded suspend hold is the battery |
|||
| drain the cap exists to prevent. |
Its release needs three owners, because the holder is not always alive to release it.
The DBC writes download-transfer:dbc into the MDB's Redis, and pm-service applies
no TTL to Redis inhibitors, so a DBC that loses power mid-transfer would otherwise
orphan a suspend inhibit that nothing ever clears:
| who releases it | when |
|---|---|
| update-service | normally, on every exit path of the transfer |
| update-service | at startup, clearing a stale one left by a crash or power cut |
| vehicle-service | whenever it cuts dashboard power, since that is what orphans it |
The delay-typed inhibits are advisory: pm-service's blocking check honours only
block and suspend-only, and the duration carried in the JSON is not read. They
appear in power-manager:busy-services for observability and have no effect on power
transitions.
Identified Gaps and Issues¶
GAP-1: ecu-service publishes to wrong channel names (Critical Bug)¶
File: ecu-service/ipc_tx.go
The ecu-service publishes notifications using channel names with spaces embedded:
tx.redis.Publish(tx.ctx, "engine-ecu throttle", nil) // line 53
tx.redis.Publish(tx.ctx, "engine-ecu odometer", nil) // line 94
tx.redis.Publish(tx.ctx, "engine-ecu kers", nil) // line 116
tx.redis.Publish(tx.ctx, "engine-ecu kers-reason-off", nil) // line 165
The correct pattern (used everywhere else) is: publish to "engine-ecu" with payload being the field name (e.g., "throttle", "odometer", "kers"). Instead, ecu-service publishes to channels named "engine-ecu throttle" etc., which no subscriber listens to.
Additionally, the message payload is nil. Other services send the field name as the payload.
Impact: bluetooth-service subscribes to "engine-ecu" watching for "odometer" field updates. It won't receive notifications from ecu-service's own data. Bluetooth-service works around this by writing odometer itself (from nRF UART). But any future subscriber watching "engine-ecu" for throttle, kers, or speed data changes will never receive notifications.
Severity: High — all throttle/kers/speed pub/sub notifications from ecu-service are silently dropped.
GAP-2: settings-service does not PUBLISH when loading settings at boot (High Bug)¶
File: settings-service/internal/redis/client.go
ReplaceSettings() and SetSettings() use DEL + HSET in a pipeline but never call PUBLISH. Services that subscribe to settings for startup configuration (vehicle-service, battery-service, ecu-service, alarm-service, pm-service) won't receive notifications when settings-service loads the TOML on boot.
The services handle this by reading settings on startup directly (HGET), so they don't miss the initial values. However, if settings-service starts after these services (due to race or restart), the services will miss any new settings values because there's no PUBLISH to trigger a re-read.
Severity: High — services may silently use stale or default settings if settings-service restarts after them.
GAP-3: version-service writes to os-release but no service reads it (Normal)¶
File: version-service/cmd/version-service/main.go
version-service writes OS release data to the os-release Redis hash. The hash name is configurable (--hash flag, default "os-release"). No other service in the codebase reads HGET os-release or subscribes to an os-release channel.
The bluetooth-service looks for "system" hash with field "mdb-version" (written by nRF via UART). The uplink-service telemetry reads "system" but not "os-release". lsc does not appear to read it either.
Impact: The version information is lost. Intended consumer not implemented.
Severity: Normal — the data is written but never consumed; functionality gap.
GAP-4: version:<component> hash written by nobody, read by update-service (Normal)¶
File: update-service/internal/redis/client.go lines 113–130
update-service reads version:<component>/version_id and version/<component>/variant_id to check if an update is needed. Nothing in the analyzed codebase writes these hashes. The hash appears intended to be populated by the Mender boot updater or an init script, not a running service.
If the version:* hashes are absent (new device, or after factory reset), update-service falls back gracefully (returns empty string, treats it as "no version installed" → always updates). This is documented behavior but creates unnecessary update cycles.
Severity: Normal — by-design but undocumented dependency on external init scripts.
GAP-5: events:faults stream is written correctly but consumed incorrectly (Normal)¶
File: uplink-service/internal/telemetry/collector.go
The events:faults key is a Redis Stream (written via XADD by vehicle-service and modem-service). The uplink-service telemetry collector does HGetAll("events:faults") which is a hash operation against a stream key. This will always return empty and silently fail — HGETALL on a stream returns nothing.
Impact: Fault event history is never included in telemetry snapshots sent to the uplink server.
Severity: Normal — telemetry data is incomplete, faults not forwarded to fleet management.
GAP-6: battery-service subscribes to vehicle channel but misses seatbox:opened event (Normal)¶
File: battery-service/battery/service.go lines 145–148
battery-service handles msg.Payload == "seatbox:lock" (the field update) but not "seatbox:opened" (the event from vehicle-service/PublishSeatboxOpened). The alarm-service correctly handles the "seatbox:opened" event via its OnEvent() handler. Battery-service uses seatbox:lock field changes instead.
This is mostly fine in practice since seatbox:lock changes from "closed" to "open" on the same operation. But the battery-service will see the lock change after the opened event, potentially creating a small timing window.
Severity: Low — functional but subtly different timing from intended protocol.
GAP-7: pm-service reads battery:0 only — ignores battery:1 state (Normal)¶
File: pm-service/internal/service/service.go line 134
pm-service only subscribes to battery:0 state changes. For dual-battery configurations, if battery:0 becomes inactive but battery:1 is still active, pm-service will believe "battery is inactive" and may allow suspend. This is a potential data integrity risk in dual-battery setups.
Severity: Normal — relevant only for dual-battery mode, but could cause data loss on incorrect suspend.
GAP-8: Startup race — vehicle-service subscribes after reading initial dashboard state (Low)¶
File: vehicle-service/internal/messaging/redis.go lines 102–115
vehicle-service reads dashboard/ready in Connect() and then starts the dashboardWatcher later in StartListening(). There is a window between the initial read and the subscription where dashboard/ready could change (scootui publishes) without vehicle-service noticing.
This is not critical because scootui publishes ready=true only once at startup, and vehicle-service's Connect() runs very early. But if scootui restarts after vehicle-service has already initialized, vehicle-service subscribes correctly via dashboardWatcher and will catch the new publication.
Severity: Low — timing window is small and consequences are benign (vehicle would need a keycard auth to advance state anyway).
GAP-9: ecu-service uses go-redis v8, all others use v9 (Low)¶
File: ecu-service/ipc_rx.go, ipc_tx.go
All other services use github.com/redis/go-redis/v9 (the renamed, actively maintained version). v8 uses a different import path and context handling. This creates a maintenance burden and prevents ecu-service from using the shared redis-ipc library.
Severity: Low — functional but creates maintenance debt.
GAP-10: system hash has two conflicting writers for cpu:governor (Low)¶
Both vehicle-service (redis.go:576) and pm-service (service.go:812) write to system/cpu:governor. If they write different values concurrently, or if one doesn't know about the other's write, the hash value could be misleading. Currently pm-service handles the actual sysfs write and publishes to the hash; vehicle-service also writes to the hash when it sends the governor command to pm-service via scooter:governor. This double-write is redundant at best.
Severity: Low — the value in Redis may be briefly inconsistent between the command send and the pm-service processing it.
GAP-11: keycard hash has 10s TTL — no consumer handles key expiry (Low)¶
File: keycard-service/keycard/redis.go line 51
keycard-service sets a 10-second TTL on the keycard hash. vehicle-service subscribes to keycard/authentication changes. When the key expires, there is no TTL expiry notification in the pub/sub protocol used (keyspace notifications are not enabled by default in Redis). If vehicle-service somehow reads keycard/authentication after expiry, it would get a redis.Nil error, which it handles gracefully. But a consumer polling this value could be confused.
Severity: Low — edge case, current usage (event-driven) is safe.
Summary of Critical Issues¶
| # | Severity | Service | Issue |
|---|---|---|---|
| 1 | High | ecu-service | Publishes to broken channel names with spaces — all throttle/kers/speed notifications silently dropped |
| 2 | High | settings-service | No PUBLISH when loading settings from TOML — services miss initial settings on settings-service restart |
| 3 | Normal | version-service | os-release hash written but never read — intended consumer not implemented |
| 4 | Normal | update-service | Reads version:<component> hash that nothing writes — depends on undocumented external init |
| 5 | Normal | uplink-service | Uses HGETALL on events:faults stream — always returns empty, fault history never telemetrized |
| 6 | Normal | pm-service | Only tracks battery:0 state — dual-battery suspend guard incomplete |
| 7 | Low | vehicle-service | Tiny startup race between initial dashboard state read and subscription |
| 8 | Low | ecu-service | Uses go-redis v8 while all others use v9 — maintenance burden |
| 9 | Low | vehicle+pm | Double-write to system/cpu:governor — minor inconsistency window |
| 10 | Low | keycard-service | 10s TTL with no keyspace notification support for expiry |