librescoot-bluetooth (bluetooth-service)¶
Description¶
The Bluetooth service provides the BLE (Bluetooth Low Energy) interface for the scooter and manages communication with the nRF52840 chip via UART. It acts as a communication bridge between the nRF52 BLE chip and Redis-based backend system, handling bidirectional translation of messages. The service exposes BLE GATT characteristics for remote control and monitoring (via nRF firmware), processes UART messages from the nRF chip, and publishes battery/vehicle state updates to Redis.
Command-Line Options¶
--serial string Serial device path (default "/dev/ttymxc1")
--baud int Serial baud rate (default 115200)
--redis-addr string Redis server address (default "localhost:6379")
--redis-pass string Redis password (default "")
--redis-db int Redis database number (default 0)
--log-level int Log level (0=none, 1=error, 2=warning, 3=info, 4=debug) (default 3)
--firmware-dir string Directory containing nRF firmware files (default "/usr/share/nrf-fw")
--auto-update Automatically update nRF firmware on startup if newer version available (default true)
Redis Operations¶
Hash: ble¶
Fields written:
mac-address- Bluetooth MAC address (received from nRF52)status- BLE connection status stringpin-code- Temporary pairing PIN code (when pairing is active)
Published channel: ble (when pin-code field changes)
Hash: cb-battery¶
Fields written:
charge- Charge level (0-100%)current- Current in μAremaining-capacity- Remaining capacity in μWhfull-capacity- Full capacity in μWhcell-voltage- Cell voltage in μVtemperature- Temperature in °Ccycle-count- Battery cycle counttime-to-empty- Time until battery is empty in secondstime-to-full- Time until battery is full in secondsstate-of-health- Battery health percentage (0-100)part-number- Part identification stringserial-number- Serial numberunique-id- Unique identifierpresent- Battery presence ("true", "false")charge-status- Charging status ("not-charging", "charging")
Published channel: None (fields are written but not published by this service)
Hash: aux-battery¶
Fields written:
voltage- Voltage in mVcharge- Charge level in 25% steps (0, 25, 50, 75, 100)charge-status- Charging status ("absorption-charge", "not-charging", "float-charge", "bulk-charge")data-stream-enable- Data streaming enable flag ("0", "1")
Published channel: None (fields are written but not published by this service)
Hash: power-manager¶
Fields written:
nrf-reset-count- Reset count from nRF52nrf-reset-reason- Nordic RESETREAS register value (integer)wake-timer-armed- "true"/"false". Echo of the nRF wake-timer ACK: non-zero seconds echoed back means the timer is armed ("true"), zero means disarmed ("false").power-state-sent- "suspending". Written when the nRF ACKs the suspending power-management state (state ACK value 0). pm-service gates the actual suspend-to-RAM on this confirmation, because the ACK reply has to fully drain off the UART before the iMX6 suspends.
Published channel: power-manager (when nrf-reset-reason changes)
Hash: power-mux¶
Fields written:
selected-input- Selected power input source ("cb", "aux")
Published channel: power-mux
Hash: system¶
Fields written:
nrf-fw-version- nRF52 firmware version string (received from nRF52 during initialization)
Fields read (not written by this service):
mdb-version- MDB firmware version string (forwarded to nRF52 when it changes)
Hash: engine-ecu¶
Fields written:
odometer- Odometer/mileage value in meters (received from nRF52)
Hash: navigation¶
Fields written:
latitude- Navigation destination latitude (from extended commandnav:dest)longitude- Navigation destination longitude (from extended commandnav:dest)destination- Legacy combined coordinates "lat,lon" (from extended commandnav:destor BLE eventnavi:start)address- Destination name/address (from extended commandnav:dest lat,lon,name)
Fields cleared: nav:clear sets all navigation fields (latitude, longitude, destination, address, timestamp) to empty strings (does not HDEL).
Published channel: navigation (when fields change)
Hash: settings¶
Fields written:
cellular.apn- Cellular APN (from extended commandconfig:apnor legacy BLE eventapn <value>)hibernation-timer- Hibernation timeout in seconds (from extended commandconfig:hibernate-timer)updates.mdb.channel- MDB OTA update channel (from extended commandconfig:update-channel)updates.dbc.channel- DBC OTA update channel (from extended commandconfig:update-channel)dashboard.saved-locations.<id>.latitude- Saved location lat (fromnav:fav:add)dashboard.saved-locations.<id>.longitude- Saved location lon (fromnav:fav:add)dashboard.saved-locations.<id>.label- Saved location name (fromnav:fav:add)
Published channel: settings (when fields change)
Hash: usb¶
Fields written:
mode- USB mode ("ums" or "normal", from extended commandusb:ums/usb:normalor legacy BLE events)
Published channel: usb (when mode field changes)
Hash: scooter (written)¶
Fields written:
temperature- Ambient temperature in °C to one decimal place (the nRF sends tenths; bluetooth-service divides before writing)
Published channel: scooter
Hash: dashboard¶
Fields read (not written):
maps-available- Offline display maps installed (set by scootui-qt, queried viastatus:maps-available)navigation-available- Routing engine available (set by scootui-qt, queried viastatus:navigation-available)
Hash: ota:ble¶
Transfer status of the BLE OTA receiver (see BLE OTA Firmware Transfer and the field reference):
Fields written:
state-idle,receiving, orinstallingbundle-id,component,received-bytes,total-bytes,rate-bps- Active session detailsupdated-at- Unix timestamp of the last update
Hash: power:inhibits¶
Fields written:
ble-ota- Block-type power inhibitor held during BLE OTA transfers and installs, so pm-service does not suspend mid-transfer. Removed when the OTA session ends (or after safety timeouts: 10 min transfer idle, 30 min without install feedback).
Hashes read (not written)¶
The service reads but does not write to these hashes:
battery:0- Reads battery state and chargebattery:1- Reads battery state and chargevehicle- Reads vehicle state for nRF synchronizationpower-manager- Reads power stateota- Watched during a BLE-initiated install;status:<component>,install-progress:<component>, anderror-message:<component>(written by update-service) are relayed to the phone as INSTALL_PROGRESS notifications
Lists consumed (BRPOP)¶
The service consumes commands from:
scooter:bluetooth- BLE management commands with indefinite blocking
Commands recognized:
advertising-start-with-whitelisting- Start advertising with whitelistadvertising-restart-no-whitelisting- Restart advertising without whitelistadvertising-stop- Stop BLE advertisingdelete-bond- Delete current bonddelete-all-bonds- Delete all bonded devicesremove- Remove pairing PIN from displayltc-enable- Enable LTC4020 aux charger (safe mode)ltc-disable- Disable LTC4020 aux chargerltc-force-enable- Force-enable LTC4020 aux charger (bypasses safety check)ltc-force-disable- Force-disable LTC4020 aux chargerltc-status- Query LTC4020 charger statusfirmware-update- Trigger immediate nRF firmware update
Lists produced (LPUSH)¶
The service writes requests to:
scooter:state- State change requests ("lock", "unlock", "lock-hibernate")scooter:power- Power requests ("hibernate", "hibernate-manual", "reboot", "hibernate-for:", "hibernate-cancel") scooter:seatbox- Seatbox commands ("open")scooter:blinker- Blinker commands ("left", "right", "both", "off")scooter:keycard- Keycard management commands ("list", "count", "add:", "remove: ") scooter:alarm- Alarm commands ("enable", "disable", "arm", "disarm", "start:", "stop") scooter:update:mdb-update-from-file:<path>after a verified BLE OTA transfer of an MDB bundlescooter:update-start-dbc(lock claim + heartbeats) /complete-dbcduring the BLE OTA DBC handoffscooter:update:dbc-update-from-file:<path>after delivering a DBC bundle to the dashboard
These are triggered by BLE characteristic writes received from the nRF52 (BLE events or extended commands).
Pub/Sub published¶
bmx:interrupt- Accelerometer wake-up events from nRF ("wake-suspend", "wake-hibernation")
Hardware Interfaces¶
UART Interface¶
- Device: Configurable via
--serialoption (default:/dev/ttymxc1) - Baud rate: Configurable via
--baudoption (default: 115200) - Protocol: Custom "USOCK" protocol with CBOR payloads
- Connected to: nRF52840 BLE chip
USOCK Protocol Details¶
The USOCK protocol is a frame-based protocol for reliable serial communication:
Frame Structure:
- Sync Byte 1:
0xF6 - Sync Byte 2:
0xD9 - Frame ID: 1 byte (lower byte of message type)
- Payload Length: 2 bytes (little-endian, max 1024 bytes)
- Header CRC: 2 bytes (CRC-16/ARC over sync+frameID+length)
- Payload: Variable length CBOR-encoded data
- Payload CRC: 2 bytes (CRC-16/ARC over payload)
CBOR Message Format: Messages are CBOR-encoded maps with structure:
Where:
messageTypeis a 16-bit message category (e.g., 0xA000 for BLE version)absoluteSubtypeismessageType + relativeSubtypevaluecan be integer, string, or array
CRC Calculation:
- Algorithm: CRC-16/ARC (also known as CRC-16/IBM)
- Polynomial: 0x8005
- Initial value: 0x0000
- Reflected input/output
See nRF UART Protocol for message types and protocol details.
BLE Interface (via nRF52840)¶
The service does not directly manage BLE - this is handled by the nRF52840 firmware. The bluetooth-service only communicates with the nRF52 via UART to:
- Receive BLE connection events and characteristic writes
- Send state updates to be exposed via BLE characteristics
- Control advertising and bonding
BLE services and characteristics are defined in the nRF52 firmware.
Configuration¶
Systemd Unit¶
- Unit file:
/etc/systemd/system/bluetooth-service.service(or/usr/lib/systemd/system/bluetooth-service.service) - Type: idle (delayed until other services have started)
- Requires:
valkey.service(redis.servicebefore Librescoot 1.2) - After:
valkey.service - Started by: systemd at boot
- Restart policy: Always
Observable Behavior¶
Startup Sequence¶
- Parses command-line flags
- Connects to Redis server
- Opens UART connection to nRF52840 (USOCK protocol initialization)
- Starts Redis command watcher goroutine (BRPOP on
scooter:bluetooth) - Subscribes to Redis pub/sub channels for state updates
- Sends nRF52 initialization sequence:
- Disable data streaming
- Request BLE firmware version
- Request BLE MAC address
- Enable data streaming
- Sync data stream
- Start advertising (no whitelist)
- Sends initial state updates to nRF52:
- Vehicle state (locked/unlocked)
- Seatbox lock state
- Handlebar lock state
- Mileage (odometer)
- Firmware version
- Battery states (both slots: present, active, cycle count, charge)
- Power management state
- Enters main loop handling UART messages and Redis updates
Runtime Behavior¶
- UART message processing: Continuous reception of USOCK frames with CBOR-encoded payloads from nRF52
- Battery updates: Writes CB battery and auxiliary battery data to Redis hashes
- Vehicle state synchronization: Monitors Redis pub/sub channels and forwards state changes to nRF52
- BLE events: Receives BLE characteristic writes from nRF52 as event strings and converts to Redis LPUSH commands
- Command watching: Blocks on
scooter:bluetoothlist waiting for BLE management commands - Bidirectional translation: Redis → USOCK/CBOR for state updates; USOCK/CBOR → Redis for sensor data and events
Message Flow¶
BLE App → nRF52840 → UART/USOCK/CBOR → bluetooth-service → Redis → other services
other services → Redis (pub/sub) → bluetooth-service → UART/USOCK/CBOR → nRF52840 → BLE App
Inbound (from BLE to system):
- User interacts with BLE app
- nRF52 receives BLE characteristic write
- nRF52 sends USOCK frame with CBOR-encoded event string to bluetooth-service
- bluetooth-service parses event and performs LPUSH to appropriate Redis list
- Other services consume from Redis lists and perform actions
Outbound (from system to BLE):
- Services update Redis hash fields
- Redis publishes change notification on pub/sub channel
- bluetooth-service receives pub/sub message
- bluetooth-service reads updated value from Redis
- bluetooth-service encodes as CBOR and sends USOCK frame to nRF52
- nRF52 updates BLE characteristic value
- BLE app receives notification of characteristic change
UART Message Types Processed¶
The service handles various USOCK message types from the nRF52. See nRF UART Protocol for complete list.
Key message types received:
0x0000- Generic events (BLE characteristic writes as event strings)0x0020- Vehicle state updates (acknowledgments)0x0040- Auxiliary battery data0x0060- CB battery detailed information (MAX1730X fuel gauge data)0x0100- Power mux status0x0120- LTC4020 aux charger control (bidirectional)0x0200- Accelerometer wake-up events (suspend/hibernation)0x0400- Extended commands (string commands from phone app)0x0800- Power management (hibernation requests from nRF)0x00C0- Data stream control acknowledgments0x00E0- Battery slot status (slot 1 and 2)0xA000- BLE firmware version0xA020- BLE reset information (reason and count)0xA040- Scooter info (mileage, firmware version, system time from nRF)0xA080- BLE parameters (MAC address, PIN code, connection status)0xAA00- BLE command acknowledgments
Key message types sent:
0x0020- Vehicle state (locked/unlocked, seatbox, handlebar)0x0400- Extended response (response string to phone app, up to 512 bytes)0x0800- Power management state updates0x00C0- Data stream enable/disable/sync0x00E0- Battery status (presence, charge, cycle count for both slots)0x0120- LTC4020 aux charger control (bidirectional)0xA000- Request BLE firmware version0xA040- Scooter info (firmware version, mileage, navigation active, UMS status)0xA080- Request BLE MAC address, remove PIN0xAA00- BLE commands (advertising control, bond management)
BLE Event Strings Processed¶
Event strings received from nRF52 (message type 0x0000) are parsed and converted to Redis operations:
Vehicle control:
"scooter:state lock"→LPUSH scooter:state lock"scooter:state unlock"→LPUSH scooter:state unlock
Seatbox:
"scooter:seatbox open"→LPUSH scooter:seatbox open
Blinker control:
"scooter:blinker left"→LPUSH scooter:blinker left"scooter:blinker right"→LPUSH scooter:blinker right"scooter:blinker both"→LPUSH scooter:blinker both"scooter:blinker off"→LPUSH scooter:blinker off
Navigation:
"navi:start <coords>"→HSET navigation destination <coords>+ publish
Settings:
"apn <value>"→HSET settings cellular.apn <value>+ publish
USB mode:
"usb:ums"→HSET usb mode ums+ publish"usb:normal"→HSET usb mode normal+ publish
Extended Commands (message type 0x0400)¶
Extended commands arrive as string payloads via the EXTENDED_COMMAND BLE characteristic (0x0401). The bluetooth-service routes them by prefix:
Navigation:
nav:dest lat,lon[,name]→ setsnavigationhash fields (latitude, longitude, destination, address)nav:clear→ deletes allnavigationhash fieldsnav:fav:add lat,lon,name→ adds tosettings:dashboard.saved-locations.<id>.*nav:fav:delete <id>→ removes saved locationnav:fav:navigate <id>→ sets navigation destination from saved locationnav:fav:list→ responds with count + one notification per saved location
USB mode:
usb:ums→HSET usb mode umsusb:normal→HSET usb mode normal
Keycard management:
keycard:list,keycard:count,keycard:add:<uid>,keycard:remove:<uid>→ forwarded toscooter:keycardRedis list; response returned asynchronously viakeycardhashcommand-resultfield
Time:
time:set <unix_timestamp>→ sets system clock viatimedatectl set-time
Configuration:
config:apn <value>→HSET settings cellular.apn <value>config:hibernate-timer <seconds>→HSET settings hibernation-timer <value>config:update-channel <stable|testing|nightly>→ setssettings:updates.mdb.channelandsettings:updates.dbc.channelconfig:auto-standby-seconds <seconds>→HSET settings scooter.auto-standby-seconds <value>(auto-lock idle timeout when parked, 0=disabled, 0-3600; last 60s shown as a cancellable countdown on the dashboard)
Alarm:
alarm:enable→LPUSH scooter:alarm enablealarm:disable→LPUSH scooter:alarm disablealarm:arm→LPUSH scooter:alarm armalarm:disarm→LPUSH scooter:alarm disarmalarm:start→LPUSH scooter:alarm start(duration default from settings)alarm:start:<N>→LPUSH scooter:alarm start:<N>alarm:stop→LPUSH scooter:alarm stop
The alarm-service processes the command and the response (alarm:ok) is returned via EXTENDED_RESPONSE (0x0402).
Power management:
pm:hibernate-for <duration>-> arms a one-shot hibernation that wakes after the given duration. Duration uses Gotime.ParseDurationsyntax (e.g. "8h", "30m", "120s"); it is converted to whole seconds and forwarded asLPUSH scooter:power hibernate-for:<seconds>.pm:hibernate-cancel-> cancels a pending wake timer and returns the system to run, forwarded asLPUSH scooter:power hibernate-cancel.
Responses via EXTENDED_RESPONSE (0x0402):
pm:okon successpm:error:invalid durationif the duration cannot be parsedpm:error:duration must be positiveif the parsed duration is <= 0 secondspm:error:redisif forwarding toscooter:powerfailspm:error:unknown commandfor any otherpm:payload
LTC4020 aux charger control:
ltc:enable— safe-enable LTC4020 charger (rejected if unsafe)ltc:disable— disable LTC4020 chargerltc:force-enable— force-enable LTC4020 charger (bypasses safety check)ltc:force-disable— force-disable LTC4020 chargerltc:status— query charger state; responds withltc:status:onorltc:status:off
Response codes: ltc:ok (success), ltc:error:unsafe (rejected as unsafe), ltc:error:invalid (invalid state).
Status queries (read-only):
status:maps-available→ readsdashboard:maps-available(set by scootui-qt)status:navigation-available→ readsdashboard:navigation-available(set by scootui-qt)
Responses are sent back via EXTENDED_RESPONSE (0x0402) as string notifications to the phone app.
BLE OTA Firmware Transfer¶
The service hosts the scooter side of the BLE OTA transfer (pkg/ota): a windowed, resumable receiver for firmware bundles pushed by the phone app through the nRF's transparent OTA tunnel (raw USOCK frames 0xB0 data / 0xB1 control in, 0xB2 status out — no CBOR).
- Bundles are staged under
/data/ota/<mdb|dbc>/with JSON sidecars so interrupted transfers resume across disconnects and reboots. - After SHA-256 verification, MDB bundles are queued with the local update-service (
scooter:update:mdb); DBC bundles are delivered to the dashboard (HTTP PUT to librescoot-data-server, scp fallback) and queued onscooter:update:dbcunder the vehicle-service DBC update lock. - Install progress from the
otahash is relayed to the phone as notifications; able-otapower inhibitor blocks suspend during transfer and install.
See BLE OTA Firmware Transfer for the protocol and lifecycle.
Accelerometer Wake-up Events¶
The service handles accelerometer wake-up messages from the nRF (message type 0x0200):
- Suspend wake-up (0x0201): Published to Redis
bmx:interruptas"wake-suspend". The nRF sends this immediately when movement is detected during iMX suspend. - Hibernation wake-up (0x0202): Published to Redis
bmx:interruptas"wake-hibernation". The nRF sends this after the VERSION handshake on the next boot, if the wake-up from hibernation was caused by accelerometer movement.
The alarm-service subscribes to bmx:interrupt to trigger alarm escalation.
Power Management¶
The service handles hibernation requests from the nRF52:
Automatic hibernation:
- nRF sends hibernation request (type=automatic)
- Service forwards:
LPUSH scooter:power hibernate
Manual hibernation:
- nRF sends hibernation request (type=manual)
- If vehicle state is "parked":
LPUSH scooter:state lock-hibernate - Otherwise:
LPUSH scooter:power hibernate-manual
Soft reboot:
- nRF receives "reboot" from the BLE power control characteristic
- Service forwards:
LPUSH scooter:power reboot - pm-service triggers a Linux-only reboot (same path as post-OTA reboots)
Hard reboot:
- nRF receives "hard-reboot" from the BLE power control characteristic
- nRF notifies iMX that a power cycle is imminent, then controls the power rails directly
- iMX reboots automatically when power is restored
When power-manager enters hibernation state, the service:
- Disables data streaming to nRF52
- Sends hibernation level request (L1 or L2)
- Sends power management state update to nRF52
Log Output¶
The service logs to journald. Common log patterns include:
- UART frame parsing errors
- CBOR decoding errors
- Redis connection status
- BLE command processing
Use journalctl -u bluetooth-service to view logs.
Dependencies¶
- nRF52840 firmware - Must be running compatible firmware with USOCK/CBOR protocol support
- Redis server - Must be accessible at configured address (default: localhost:6379)
- UART device - nRF52840 must be accessible via serial device (default: /dev/ttymxc1)
- Go runtime - Compiled as static binary, no runtime dependencies
CB Battery Monitoring¶
The service receives detailed battery information from the nRF52 (which reads from MAX1730X fuel gauge) and processes alerts and faults:
CB Battery Alert Conditions (Status Register)¶
Written to cb-battery:alert hash field alert:
- Minimum Current Alert Threshold Exceeded
- Maximum Current Alert Threshold Exceeded
- Minimum Voltage Alert Threshold Exceeded
- Maximum Voltage Alert Threshold Exceeded
- Minimum Temperature Alert Threshold Exceeded
- Maximum Temperature Alert Threshold Exceeded
- Minimum SOC Alert Threshold Exceeded
- Maximum SOC Alert Threshold Exceeded
CB Battery Fault Conditions¶
Written to cb-battery:fault hash field fault:
From Protection Status Register:
- Discharging fault (ODCP, UVP, TOOHOTD, DIEHOT)
- Charging fault (TOOCOLDC, OVP, OCCP, QOVFLW, TOOHOTC, FULL, DIEHOT)
From Battery Status Register:
- ChargeFET Failure-Short Detected
- DischargeFET Failure-Short Detected
- FET Failure open
Alerts and faults are cleared automatically when the condition is resolved (all relevant bits clear).
Redis Pub/Sub Subscriptions¶
The service subscribes to these Redis channels to monitor for state changes:
vehicle- Monitors for vehicle state, seatbox lock, and handlebar lock changesbattery:0- Monitors for battery slot 1 state, presence, charge, and cycle count changesbattery:1- Monitors for battery slot 2 state, presence, charge, and cycle count changespower-manager- Monitors for power management state changesengine-ecu- Monitors for odometer/mileage changessystem- Monitors for MDB firmware version changesble- Monitors for pin-code removal notificationsnavigation- Monitors for destination changes → sends navigation active status (0/1) to nRFusb- Monitors for USB mode changes → sends UMS status (0/1) to nRFkeycard- Monitors forcommand-resultfield → relays response to phone via extended response
When a subscribed field changes, the service:
- Receives the field name via pub/sub
- Reads the updated value from the Redis hash
- Encodes and sends the update to nRF52 via USOCK