eventbus

package module
v0.1.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Aug 9, 2026 License: AGPL-3.0 Imports: 5 Imported by: 0

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

View Source
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.

View Source
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.

View Source
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.

View Source
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

func ButtonTopic(source, gesture string) string

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

func ValidTrigger(s string) bool

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.

func New

func New(topic, src string) Event

New returns an Event with topic and src set. The caller fills in the rest. TS is left zero so callers can stamp it from their own clock, which keeps this package free of time dependencies and makes tests deterministic.

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

func (p *Publisher) Emit(e Event) error

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.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL