Documentation
¶
Overview ¶
Package eventbus is the wire contract for the Librescoot event bus.
It is imported both by event-service, which derives events from existing Redis traffic, and by services that emit their own events directly. Keeping the struct, the topic names and the trigger vocabulary in one place is what stops the two from drifting.
Index ¶
Constants ¶
const ( // Named vehicle transitions. Emitted alongside TopicVehicleStateChanged. TopicVehicleUnlocked = "vehicle.unlocked" TopicVehicleLocked = "vehicle.locked" TopicVehicleHibernating = "vehicle.hibernating" TopicRideStarted = "ride.started" TopicRideEnded = "ride.ended" // The complete record: fires for every vehicle state transition. TopicVehicleStateChanged = "vehicle.state.changed" TopicVehicleSeatboxOpened = "vehicle.seatbox.opened" TopicVehicleSeatboxClosed = "vehicle.seatbox.closed" TopicVehicleHandlebarLocked = "vehicle.handlebar.locked" TopicVehicleHandlebarUnlocked = "vehicle.handlebar.unlocked" TopicVehicleKickstandUp = "vehicle.kickstand.up" TopicVehicleKickstandDown = "vehicle.kickstand.down" TopicVehicleBlinkerChanged = "vehicle.blinker.changed" TopicKeycardAuthPassed = "keycard.auth.passed" TopicKeycardAuthFailed = "keycard.auth.failed" TopicBatteryInserted = "battery.inserted" TopicBatteryRemoved = "battery.removed" TopicBatteryStateChanged = "battery.state.changed" TopicBatteryChargeChanged = "battery.charge.changed" TopicAuxChargeChanged = "aux.charge.changed" TopicCBBChargeChanged = "cbb.charge.changed" TopicPowerStateChanged = "power.state.changed" TopicPowerWake = "power.wake" TopicAlarmArmed = "alarm.armed" TopicAlarmDisarmed = "alarm.disarmed" TopicAlarmTriggered = "alarm.triggered" // The complete record: fires for every alarm status change. TopicAlarmStatusChanged = "alarm.status.changed" TopicMotionDetected = "motion.detected" // Not derived in v1: published vocabulary, not yet wired to the adapter. // ecu-service publishes the literal string "fault" on the engine-ecu // channel rather than the field name that changed, so the adapter's // field-based watcher never sees "fault:code" arrive as a notification. // modem-service's PublishLocationState is the only writer of the gps // hash, and it never notifies on the fix field at all, silently or // otherwise. Deriving either pair needs a producer-side change (publish // the field name for ecu.fault, notify on write for gps.fix), not an // adapter fix. See EVENT-SERVICE-DESIGN.md section 5.4. TopicECUFaultRaised = "ecu.fault.raised" TopicECUFaultCleared = "ecu.fault.cleared" TopicGPSFixAcquired = "gps.fix.acquired" TopicGPSFixLost = "gps.fix.lost" TopicNetConnectivityChanged = "net.connectivity.changed" TopicSMSReceived = "sms.received" TopicOTAStatusChanged = "ota.status.changed" TopicOTAAvailable = "ota.available" TopicOTAInstalled = "ota.installed" TopicSettingsChanged = "settings.changed" TopicSystemBooted = "system.booted" TopicSystemShutdown = "system.shutdown" TopicDashboardReady = "dashboard.ready" )
Topic names. Dotted so subscribers can filter server-side with PSUBSCRIBE ev:vehicle.* rather than decoding every message.
Battery slot is carried in data["slot"], not in the topic, so that a glob over battery events does not have to enumerate slots.
const ( // rider.*: the rider physically did something. TriggerRiderKickstand = "rider.kickstand" TriggerRiderBrake = "rider.brake" TriggerRiderSeatbox = "rider.seatbox" TriggerRiderHorn = "rider.horn" TriggerRiderBlinker = "rider.blinker" TriggerRiderThrottle = "rider.throttle" // auth.*: an identity was presented. TriggerAuthKeycard = "auth.keycard" TriggerAuthBLE = "auth.ble" // command.*: something asked for this explicitly. The command queues carry // no provenance, so these say what was asked for, never who asked. TriggerCommandUnlock = "command.unlock" TriggerCommandLock = "command.lock" TriggerCommandHibernate = "command.hibernate" TriggerCommandForceLock = "command.force-lock" TriggerCommandHopOn = "command.hop-on" TriggerCommandOpenSeatbox = "command.open-seatbox" // timer.*: a timer expired. TriggerTimerAutoStandby = "timer.auto-standby" TriggerTimerHibernation = "timer.hibernation" TriggerTimerShutdown = "timer.shutdown" TriggerTimerSeatbox = "timer.seatbox" // sensor.*: a measured condition was met. TriggerSensorMotion = "sensor.motion" TriggerSensorVoltage = "sensor.voltage" TriggerSensorTemperature = "sensor.temperature" TriggerSensorCharge = "sensor.charge" // system.*: system lifecycle. TriggerSystemBoot = "system.boot" TriggerSystemShutdown = "system.shutdown" TriggerSystemUpdate = "system.update" TriggerSystemDashboard = "system.dashboard" TriggerSystemFault = "system.fault" )
The trigger vocabulary. data["trigger"] says why an event happened, in terms that mean the same thing whichever service emitted it. Service internals (FSM event IDs and the like) never reach the bus; producers map into this vocabulary instead. See EVENT-SERVICE-DESIGN.md section 6.4.
The field is optional. A producer with no meaningful cause omits it rather than inventing one.
const ChannelPrefix = "ev:"
ChannelPrefix is prepended to the topic to form the pub/sub channel name. Keeping events under their own prefix means PSUBSCRIBE ev:* cannot collide with the hash-notification channels that services already use.
const StreamKey = "events"
StreamKey is the Redis stream every event is appended to. Redis is not persisted on the scooter, so this is a live window for consumers that restart, not an archive.
Variables ¶
This section is empty.
Functions ¶
func AllTriggers ¶
func AllTriggers() []string
AllTriggers returns a copy of the vocabulary, for tests and documentation generation. Callers must not rely on ordering.
func ButtonTopic ¶
ButtonTopic builds the topic for a gesture on input-events, for example ButtonTopic("brake:left", "hold") is "button.brake.left.hold". The colon in the source name becomes a dot so the whole topic is one consistent namespace and globs like button.brake.* work.
func ValidTrigger ¶
ValidTrigger reports whether s is in the vocabulary. Producers should call this in tests, not at runtime: an invalid trigger is a programming error, and dropping the field at runtime would hide it.
Types ¶
type Event ¶
type Event struct {
ID string `json:"id,omitempty"`
TS int64 `json:"ts"`
Topic string `json:"topic"`
Src string `json:"src"`
From string `json:"from,omitempty"`
To string `json:"to,omitempty"`
Data map[string]any `json:"data,omitempty"`
}
Event is one occurrence on the bus.
ID is assigned by Redis when the event is appended to the stream, so it is empty until then. From and To are promoted out of Data because "changed from X to Y" is the shape most rules match on.
type Publisher ¶
type Publisher struct {
// contains filtered or unexported fields
}
Publisher writes events to the bus: one append to the stream and one publish on the topic channel per event.
func NewPublisher ¶
func NewPublisher(c *ipc.Client, src string, opts ...PublisherOption) *Publisher
NewPublisher returns a Publisher that stamps every event with src.
func (*Publisher) Emit ¶
Emit writes e to the stream and publishes it on its topic channel.
The stream ID is not known until the append succeeds, so the channel payload carries it: this is why the two writes cannot simply be pipelined blind.
If the stream append succeeds and the publish then fails, the event is durably in the stream but never reaches live subscribers. Emit returns an error for that case same as any other, and a caller that retries will append a second copy of the event to the stream rather than lose it. The stream is the recovery path: a consumer that missed the publish can still pick the event up from there.
type PublisherOption ¶
type PublisherOption func(*Publisher)
PublisherOption configures a Publisher.
func WithClock ¶
func WithClock(fn func() int64) PublisherOption
WithClock replaces the millisecond clock. Tests use this to get deterministic timestamps.
func WithStreamMaxLen ¶
func WithStreamMaxLen(n int64) PublisherOption
WithStreamMaxLen overrides the approximate stream cap. Zero means the default of 2000.