-
-
Notifications
You must be signed in to change notification settings - Fork 2
en Module Bus Protocol
SelenaCore Module Bus is a CAN-bus inspired communication layer where the core acts as the master node and external modules connect as peers over a single WebSocket endpoint. This document is the authoritative protocol reference for module developers.
ws://<host>/api/v1/bus?token=<module_token>
All module-to-core communication goes through this single endpoint. There are no per-module ports.
Module Core
| |
| WebSocket connect ?token=TOKEN |
|---------------------------------------------->|
| token validation |
| (reject -> close 4001)|
| |
| WebSocket accept() |
|<----------------------------------------------|
| |
| announce {...capabilities} |
|---------------------------------------------->|
| |
| announce_ack {bus_id} |
|<----------------------------------------------|
| |
| bidirectional message loop |
|<--------------------------------------------->|
| |
| ping (every 15s) |
|<----------------------------------------------|
| pong |
|---------------------------------------------->|
| |
| shutdown {drain_ms} |
|<----------------------------------------------|
| (finish work, close connection) |
|---------------------------------------------->|
-
Connect -- Module opens a WebSocket connection with
?token=TOKENas a query parameter. -
Authentication -- Core validates the token before calling
accept(). An invalid token results in an immediate close with code4001. -
Announce -- Module sends an
announcemessage declaring its name, version, and capabilities. -
Registration -- Core validates the announcement, registers the module, and replies with
announce_ackcontaining the assignedbus_id. - Message loop -- Bidirectional communication begins. The module can send and receive all supported message types.
-
Health checks -- Core sends
pingevery 15 seconds. The module must reply withpong. Three consecutive missed pings result in a disconnect (close code4004). -
Shutdown -- Core sends a
shutdownmessage with adrain_mswindow. The module should finish in-flight work within that window and exit gracefully.
Every message is a JSON object with a required type field:
{"type": "<message_type>", ...}The following sections define each message type, its direction, and its schema.
Direction: module -> core
Sent immediately after the WebSocket connection is accepted. Declares the module identity and its capabilities.
{
"type": "announce",
"module": "weather-module",
"version": "1.0.0",
"capabilities": {
"intents": [
{
"patterns": {
"en": ["weather", "forecast"]
},
"priority": 50,
"description": "Weather queries"
}
],
"subscriptions": ["device.state_changed"],
"publishes": ["weather.module_started"]
}
}| Field | Type | Description |
|---|---|---|
module |
string | Unique module identifier, must match the registered manifest name. |
version |
string | Semver version of the module. |
capabilities.intents |
array | List of intent declarations this module can handle. |
capabilities.intents[].patterns |
object | Map of language code to trigger phrases. Only the en key is honoured — non-English speech falls through to the LLM tier, which understands any language and returns an English intent name. Other language keys are ignored at index time. |
capabilities.intents[].priority |
integer | Routing priority. Lower values are matched first. |
capabilities.intents[].description |
string | Human-readable description of the intent group. |
capabilities.subscriptions |
array | Event types the module wants to receive. Supports wildcards (e.g. device.*). |
capabilities.publishes |
array | Event types the module is permitted to emit. |
If the module does not send announce within the configured timeout, the connection is closed with code 4002.
Direction: module -> core
Identical schema to announce but with "type": "re_announce". Allows a module to hot-reload its capabilities (add/remove intents, change subscriptions) without dropping the WebSocket connection.
{
"type": "re_announce",
"module": "weather-module",
"version": "1.1.0",
"capabilities": { ... }
}The core replaces the module's registered capabilities atomically and responds with a new announce_ack.
Direction: core -> module
Confirms successful registration or re-registration.
{
"type": "announce_ack",
"bus_id": "uuid-1234",
"warnings": []
}| Field | Type | Description |
|---|---|---|
bus_id |
string | UUID assigned by the core. Used internally for routing. |
warnings |
array | List of non-fatal warning strings (e.g. unknown subscription patterns). |
Direction: core -> module
Dispatched when a user query matches one of the module's registered intent patterns.
{
"type": "intent",
"id": "uuid-request",
"payload": {
"text": "what's the weather?",
"lang": "en",
"context": {}
}
}| Field | Type | Description |
|---|---|---|
id |
string | Unique request identifier. Must be echoed back in intent_response. |
payload.text |
string | The raw user query text. |
payload.lang |
string | Detected language code (en, uk, etc.). |
payload.context |
object | Arbitrary context from the originating session. |
The module must respond within 10 seconds or the request is considered timed out.
Direction: module -> core
Response to an intent message. The id field must match the original request.
{
"type": "intent_response",
"id": "uuid-request",
"payload": {
"handled": true,
"tts_text": "It's currently 12°C and cloudy",
"data": {
"temperature": 12,
"condition": "cloudy"
}
}
}| Field | Type | Description |
|---|---|---|
id |
string | Must match the id from the corresponding intent message. |
payload.handled |
boolean |
true if the module successfully handled the intent. false triggers fallthrough to the next matching module. |
payload.tts_text |
string | Text-to-speech response for the user. |
payload.data |
object | Structured data accompanying the response. Schema is module-specific. |
If handled is false, the core routes the intent to the next eligible module (up to 3 fallthrough attempts total).
Direction: bidirectional
Used for publish/subscribe event broadcasting.
{
"type": "event",
"payload": {
"event_type": "device.state_changed",
"data": {
"device_id": "xxx",
"state": {"power": true}
}
}
}| Field | Type | Description |
|---|---|---|
payload.event_type |
string | Dot-separated event type identifier. |
payload.data |
object | Arbitrary event payload. |
Module -> core: The event_type is validated against the module's declared publishes list. Events not in the list are rejected.
Core -> module: Delivered only if the event_type matches one of the module's subscriptions patterns. Wildcard matching is supported -- device.* matches device.state_changed, device.added, etc.
Direction: bidirectional
Health check mechanism.
{"type": "ping", "ts": 1711900000}{"type": "pong", "ts": 1711900000}| Field | Type | Description |
|---|---|---|
ts |
integer | Unix timestamp (seconds) of the ping origin. Echoed back in pong. |
The core sends ping every 15 seconds. The module must respond with pong containing the same ts value. After 3 consecutive missed pings, the core closes the connection with code 4004.
Direction: module -> core
Allows a module to call SelenaCore REST API endpoints over the bus without making a separate HTTP connection. Permissions are enforced via the ACL system.
{
"type": "api_request",
"id": "req-uuid",
"payload": {
"method": "GET",
"path": "/devices",
"body": null
}
}| Field | Type | Description |
|---|---|---|
id |
string | Unique request identifier. Echoed in the corresponding api_response. |
payload.method |
string | HTTP method: GET, POST, PATCH, DELETE. |
payload.path |
string | API path (without the /api/v1 prefix). |
payload.body |
object or null | JSON request body. null for GET/DELETE. |
Direction: core -> module
Response to an api_request.
{
"type": "api_response",
"id": "req-uuid",
"payload": {
"status": 200,
"body": [
{"device_id": "...", "name": "Kitchen Light"}
]
}
}| Field | Type | Description |
|---|---|---|
id |
string | Matches the id from the originating api_request. |
payload.status |
integer | HTTP-equivalent status code. |
payload.body |
any | Response body. Structure matches the corresponding REST API endpoint. |
Unauthorized requests receive status 403.
Direction: core -> module
Sent when the core is shutting down or explicitly disconnecting the module.
{
"type": "shutdown",
"drain_ms": 5000
}| Field | Type | Description |
|---|---|---|
drain_ms |
integer | Milliseconds the module has to finish in-flight work before the connection is terminated. |
The module should complete any pending operations within the specified window and then close its end of the connection.
Each WebSocket connection maintains two internal queues to separate traffic by priority:
| Queue | Max Size | Overflow Policy | Message Types |
|---|---|---|---|
| Critical | 100 | Backpressure (blocks sender) |
intent, intent_response, api_request, api_response
|
| Event | 1000 | Drop oldest | event |
The writer coroutine always drains the critical queue first. This guarantees that intent handling and API calls are never starved by a burst of event traffic.
Module permissions are declared in the module's manifest.json and enforced on every api_request. The permission-to-endpoint mapping:
| Permission | Allowed Operations |
|---|---|
devices.read |
GET /devices, GET /devices/{id}
|
devices.write |
POST /devices, PATCH /devices/{id}/state, DELETE /devices/{id}
|
events.subscribe |
Receive events matching subscription patterns |
events.publish |
POST /events/publish, emit event messages on the bus |
Requests that exceed the module's granted permissions receive a 403 status in the api_response.
A per-module circuit breaker protects the system from unresponsive modules:
- Closed (normal) -- Intents are routed to the module as usual.
- Open (tripped) -- The module is excluded from intent routing. Triggered when the module consistently times out on intent requests.
-
Recovery -- After 30 seconds in the open state, the breaker allows a trial request. A successful
intent_responseresets the breaker to closed.
The circuit breaker only affects intent routing. Events and API requests continue to flow normally while the breaker is open.
When a user query arrives, the core resolves it through the bus:
-
route_intent(text, lang, context)is called. - The input text is matched against a compiled regex index built from all connected modules' intent patterns. Matching is case-insensitive.
- All matches are sorted by
priority(lower value = higher priority). - The
intentmessage is sent to the first matching module. - If the module responds with
handled: false, the next match is tried. - Maximum 3 fallthrough attempts before the query is considered unhandled.
- Each module has a 10-second timeout to respond.
| Code | Name | Description |
|---|---|---|
4001 |
invalid_token |
Authentication failed. The provided token is missing, expired, or invalid. |
4002 |
announce_timeout |
Module did not send an announce message within the required timeout after connection. |
4003 |
invalid_json / expected_announce
|
Protocol violation. The first message was not valid JSON or was not an announce message. |
4004 |
ping_timeout |
Health check failed. Three consecutive pings went unanswered. |
1001 |
core_shutdown |
The core is shutting down gracefully. |
A minimal module session:
1. Connect: ws://localhost/api/v1/bus?token=abc123
2. Send: {"type":"announce","module":"my-module","version":"0.1.0","capabilities":{"intents":[],"subscriptions":["device.*"],"publishes":[]}}
3. Receive: {"type":"announce_ack","bus_id":"550e8400-e29b-41d4-a716-446655440000","warnings":[]}
4. Receive: {"type":"ping","ts":1711900000}
5. Send: {"type":"pong","ts":1711900000}
6. Receive: {"type":"event","payload":{"event_type":"device.state_changed","data":{"device_id":"light-1","state":{"power":true}}}}
7. Send: {"type":"api_request","id":"r1","payload":{"method":"GET","path":"/devices","body":null}}
8. Receive: {"type":"api_response","id":"r1","payload":{"status":200,"body":[{"device_id":"light-1","name":"Kitchen Light"}]}}
🤖 This wiki is auto-synced from docs/ in the main repo. Hand-edits on the wiki UI get overwritten on the next push. Open a PR against the main repo instead.
MIT License · Sponsor · Ko-fi
SelenaCore
🇬🇧 English
Getting started
Architecture
Voice & translation
Hardware integration
Development
- Modules overview
- Module development
- System module development
- Module API guide
- Module bus protocol
- Widget development
- User manager / auth
Reference
🇺🇦 Українська
Початок
Архітектура
Голос і переклад
Інтеграція заліза
Розробка
- Розробка модулів
- Розробка системних модулів
- Module API
- Module bus
- Widget development
- User manager / auth
Довідник