Skip to content

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, dashboard, vehicle:fault, events:faults
 battery-service ──writes──> battery:0, battery:1, battery:N:fault
 ecu-service ────writes──> engine-ecu
 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──> version:mdb (MDB), version:dbc (DBC)  (one-shot at boot, no pub/sub notification)
 settings-service writes──> settings  (publishes one notification per field at load too)
 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 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, vehicle-service, update-service, uplink-service, alarm-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 by settings-service (one notification per field, including at boot), scootui, UMS updates
 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, ecu-service, battery-service, modem-service, bluetooth-service; read by uplink-service (stream consumer)

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, handlebar:lock-state, dashboard:power, main-power, engine-power, horn:button, seatbox:button, dbc-updating, update:status, auto-standby-remaining, auto-standby-deadline | vehicle | | system | cpu:governor, usb0-gate | system | | dashboard | ready (deletes on standby), backlight-enabled | dashboard | | vehicle:fault | fault codes (Redis Set) | vehicle (payload: "fault") | | events:faults | fault events (Redis Stream) | — |

Publishes (channel only, no hash behind them):

  • buttons - button edges, e.g. horn:on, seatbox:off, brake:left:on, blinker:right:off
  • input-events - synthesized gestures, e.g. brake:left:hold, brake:right:tap

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)
  • power-manager/state (suspend/hibernate handshake)
  • ble/status (link state for lock-on-disconnect)

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 index
  • scooter:led:fade → "channel:index" string
  • scooter:update → "start", "complete", "start-dbc", "complete-dbc"
  • scooter:hardware → "dashboard:on", "dashboard:off", "engine:on", "engine:off", "handlebar:lock", "handlebar:unlock" (and :force variants)
  • scooter:hop-on → "engage", "engage-learning", "release"
  • scooter:dbc-hold → "map-download", "release"

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 watchdog bounds that hold, reset on any ota hash field ending in :dbc:

condition timeout
a heartbeat:dbc has been seen during this update 3 min
no heartbeat seen 15 min

The longer fallback exists for a DBC whose update-service is too old to publish heartbeat:{component}; cutting its power early could interrupt an install. The short timeout applies only once the DBC has proven it publishes heartbeats. The flag is seeded on vehicle-service startup from heartbeat:dbc, because the ota watcher does not replay field values after a restart.

Dashboard power has a second holder: scootui-qt holds it for the duration of a map or routing tile download, tracked by vehicle-service alongside the update hold. When the update watchdog expires it will not cut power while a map download is still holding the rail.

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-open
  • settings/scooter.dual-battery

Subscribes to:

  • vehicle channel (payload: "state", "seatbox:lock")
  • settings channel (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 | (throttle publishes on engine-ecu; the rest of this row does not) | | engine-ecu | temperature, fault:code, fault:description | (no publish) | | engine-ecu | odometer | engine-ecu | | engine-ecu | kers, boost | (kers publishes on engine-ecu; boost does not) | | engine-ecu | kers-reason-off | engine-ecu | | engine-ecu | gear, fw-version | (no publish) |

Reads:

  • vehicle/state (for engine ready state)
  • battery:0/state, battery:0/temperature-state
  • battery:1/state, battery:1/temperature-state
  • settings/engine-ecu.boost

Subscribes to:

  • vehicle channel (payload: "state")
  • battery:0, battery:1 channels (all fields → HGETALL)
  • settings channel (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") | | keycard | command-result | keycard | | system | keycard-master-count, keycard-authorized-count | system |

Note: Sets 10-second TTL on keycard hash after publish.

Publishes:

  • keycard:events - teach-in progress (mode-entered, mode-exited, card-learned:<uid>, rejected:already-authorized:<uid>, error:save-failed:<uid>)

Reads: None (hardware-driven)

Consumes queues:

  • scooter:keycard → "list", "count", "add:", "remove:" (and the master commands)

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") | | cb-battery:alert, cb-battery:fault | auxiliary-battery alerts and faults | same-named channels | | navigation | destination fields from the phone (destination, latitude, longitude, address) | navigation | | scooter | temperature (nRF die temperature, tenths of a degree) | scooter | | settings, usb, power-mux, power:inhibits | read or written in passing (settings values, UMS mode, mux input, the DFU inhibitor) | respective channels |

Reads:

  • vehicle/state, vehicle/seatbox:lock, vehicle/handlebar:lock-sensor
  • battery:0/state, battery:0/present, battery:0/charge, battery:0/cycle-count
  • battery:1/state, battery:1/present, battery:1/charge, battery:1/cycle-count
  • power-manager/state
  • engine-ecu/odometer
  • system/mdb-version
  • ble/pin-code

Subscribes to:

  • vehicle, battery:0, battery:1, power-manager, engine-ecu, system, ble channels

Consumes queues:

  • scooter:bluetooth → BLE commands ("advertising-start-with-whitelisting", "advertising-restart-no-whitelisting", "advertising-stop", "delete-bond", "delete-all-bonds", "remove", "firmware-update", "ltc-enable", "ltc-disable", "ltc-force-enable", "ltc-force-disable", "ltc-status", "data-stream-sync")

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:

  • vehicle channel via HashWatcher
  • battery:0 channel via HashWatcher
  • settings channel (for hibernation-timer)
  • power:inhibits hash (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/status:<component> (self-read)
  • ota/update-version:<component>, ota/update-method:<component>
  • version:<component>/version_id, version:<component>/variant_id
  • settings/updates.<component>.method
  • settings/updates.<component>.last-check-time

Subscribes to:

  • vehicle channel (via HashWatcher)
  • ota channel (via HashWatcher)
  • settings channel (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") | | cell-location | cell-based location estimate (silent write) | — | | internet-usage | rx-bytes, tx-bytes, rx-bytes-roaming, tx-bytes-roaming, since (silent write) | — | | sms | last SMS state | sms | | events:faults | modem fault events (Redis Stream) | — |

Reads:

  • vehicle/state (via HashWatcher, for hibernation behavior)
  • settings (SIM, APN and connectivity fields, watched at startup)

Publishes (channel only):

  • gps:tpv - a fresh GPS fix as JSON
  • sms:received, sms:sent - SMS payloads (also appended to the same-named streams)

Consumes queues:

  • scooter:modem → "disable", "enable"
  • scooter:sms → JSON {"to", "text"}

Hardware access: ModemManager via D-Bus, GPIO modem power, USB


version-service

Writes (one-shot at boot, no pub/sub publish): | Hash | Fields | |------|--------| | version:mdb (MDB), version:dbc (DBC) | All fields from /etc/os-release (lowercase), serial_number, serial_number_real |

Note: version-service does not publish on any channel. It runs once at boot via systemd (Type=oneshot) and exits. The hash name comes from the -hash flag; the installed units pass version:mdb on the MDB and version:dbc on the DBC, so the compiled-in default os-release is never used on a vehicle. 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 | settings |

Reads:

  • settings/* (all fields, to save back to TOML)

Subscribes to:

  • settings channel (to detect changes and flush to TOML)

Consumes queues (BRPOP):

  • settings:overlay → apply:service, clear:service — owns the overlay command list; the service overlay fans out to pm-service, vehicle-service, alarm-service, and scootui purely via existing settings-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-active, trigger:source, trigger:timestamp | alarm | | settings | alarm.enabled, alarm.honk, alarm.duration, alarm.seatbox-trigger, alarm.hairtrigger, alarm.hairtrigger-duration, alarm.l1-cooldown | settings | | power:inhibits | alarm-active (block) | power:inhibits |

Produces queues (LPUSH):

  • scooter:horn → "on", "off"
  • scooter:blinker → "both", "off"

Publishes:

  • alarm hash status field — 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.duration
  • settings/alarm.seatbox-trigger, settings/alarm.hairtrigger, settings/alarm.hairtrigger-duration
  • settings/alarm.l1-cooldown
  • motion/wake-cause (once on startup — durable backstop for wake-from-hibernation indicator)
  • usb/mode (via HashWatcher — "ums" / "ums-by-dbc" hold the FSM disarmed for the session)

Subscribes to:

  • vehicle channel (state, seatbox:opened event, seatbox:lock)
  • settings channel
  • power-manager channel (state field — drives hibernation handshake)
  • motion:interrupt channel (JSON envelope {type, timestamp, engine})
  • buttons channel (button events that can trigger the alarm)
  • usb channel (mode field — suppresses the alarm during mass-storage sessions)

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-qt (Qt/QML 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
  • scooter:blinker → off, left, right, both
  • scooter:bluetooth → delete-all-bonds
  • scooter:hop-on → engage-learning, engage, release
  • scooter:dbc-hold → map-download, release
  • scooter:update:mdb, scooter:update:dbc → check-now, preview-channel:<channel>
  • settings:overlay → apply:service, clear:service

Publishes:

  • buttons channel (button events)

Hardware access: Display via Qt EGLFS (eglfs_kms) on the imx-drm KMS device


Reads (telemetry, HGETALL):

  • vehicle, battery:0, battery:1, aux-battery, cb-battery, engine-ecu, power-manager, power-mux, internet, modem, gps, keycard, ble, dashboard, system, alarm, navigation, ota, scooter

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"
  • scooter:alarm → "arm", "disarm", "enable", "disable", "stop"

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, scooter:modem, scooter:update:<component>, settings:overlay (apply:service, clear:service via lsc 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, usb0-gate), bluetooth-service (mdb-version, nrf-fw-version), pm-service (cpu:governor), keycard-service (keycard counts) bluetooth-service, uplink-service, scootui, vehicle-service
ota update-service (incl. heartbeat:<component>) vehicle-service (reads status), update-service (self), scootui
internet modem-service; radio-gaga/uplink-service (unu-cloud legacy) scootui, uplink-service
modem modem-service scootui, uplink-service
gps modem-service scootui, uplink-service
alarm alarm-service lsc, monitoring
maps scootui-qt (metadata), ums-service (tile transfer) scootui, 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, alarm-service
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
engine-ecu:fault ecu-service scootui, monitoring

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), lsc
scooter:hardware vehicle-service lsc, scootui, uplink-service
scooter:power pm-service lsc, vehicle-service, update-service, uplink-service, alarm-service
scooter:governor pm-service vehicle-service (sends cpu governor changes)
scooter:modem modem-service pm-service, lsc
scooter:bluetooth bluetooth-service external/lsc
scooter:alarm alarm-service lsc, uplink-service
scooter:keycard keycard-service lsc, bluetooth-service
scooter:hop-on vehicle-service scootui
scooter:dbc-hold vehicle-service scootui
scooter:sms modem-service external clients
settings:overlay settings-service lsc
power:inhibits pm-service inhibitor manager update-service, vehicle-service, modem-service, alarm-service, bluetooth-service (hold pm inhibitors)

Inhibitor ids worth knowing, because fleet tooling matches on their prefixes:

id type held by while
download-transfer:{component} suspend-only update-service an OTA transfer is in flight, both components
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
map-download suspend-only vehicle-service the dashboard is mid map or tile download
alarm-active block alarm-service an alarm is sounding
modem-active block modem-service the modem is powered

download-transfer is what keeps the MDB out of suspend during an OTA. Without it pm-service suspends roughly a minute into stand-by and takes the modem with it, so a download in stand-by would never finish. 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 published to wrong channel names (Resolved)

File: ecu-service/ipc_tx.go

ecu-service published its hash change notifications to channel names with a space embedded, "engine-ecu throttle" and friends, with an empty payload. The convention is channel = hash name, payload = field name, so nothing subscribing to "engine-ecu" ever saw them.

Corrected once in b4066e5, reintroduced by the flat-package rewrite d89a036, and corrected again in e32d9db (ecu-service v0.8.2). All five notifications now publish the field name on the engine-ecu channel, and there is a comment on the channel constant recording the convention.

GAP-3: 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-5: Startup race, vehicle-service subscribes after reading initial dashboard state (Low)

File: vehicle-service/internal/messaging/redis.go:114 (the read, in Connect()) and :165 (dashboardWatcher.Start(), in StartListening())

vehicle-service reads dashboard/ready in Connect() and subscribes later in StartListening(). HashWatcher.Start() only subscribes; it does not re-read the hash. A change landing between the read and the subscribe is therefore missed. redis-ipc's other entry point, StartWithSync(), subscribes first and then runs the HGETALL — the settings watcher uses it for exactly this reason — so the window here is a choice rather than something the library forces.

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-7: 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-8: 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
4 Low vehicle-service Tiny startup race between initial dashboard state read and subscription
6 Low vehicle+pm Double-write to system/cpu:governor, minor inconsistency window
7 Low keycard-service 10s TTL with no keyspace notification support for expiry