signalk-wyoming 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (5) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +30 -0
  3. package/SPEC.md +332 -0
  4. package/index.js +23 -0
  5. package/package.json +29 -0
package/LICENSE ADDED
@@ -0,0 +1,201 @@
1
+ Apache License
2
+ Version 2.0, January 2004
3
+ http://www.apache.org/licenses/
4
+
5
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
6
+
7
+ 1. Definitions.
8
+
9
+ "License" shall mean the terms and conditions for use, reproduction,
10
+ and distribution as defined by Sections 1 through 9 of this document.
11
+
12
+ "Licensor" shall mean the copyright owner or entity authorized by
13
+ the copyright owner that is granting the License.
14
+
15
+ "Legal Entity" shall mean the union of the acting entity and all
16
+ other entities that control, are controlled by, or are under common
17
+ control with that entity. For the purposes of this definition,
18
+ "control" means (i) the power, direct or indirect, to cause the
19
+ direction or management of such entity, whether by contract or
20
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
21
+ outstanding shares, or (iii) beneficial ownership of such entity.
22
+
23
+ "You" (or "Your") shall mean an individual or Legal Entity
24
+ exercising permissions granted by this License.
25
+
26
+ "Source" form shall mean the preferred form for making modifications,
27
+ including but not limited to software source code, documentation
28
+ source, and configuration files.
29
+
30
+ "Object" form shall mean any form resulting from mechanical
31
+ transformation or translation of a Source form, including but
32
+ not limited to compiled object code, generated documentation,
33
+ and conversions to other media types.
34
+
35
+ "Work" shall mean the work of authorship, whether in Source or
36
+ Object form, made available under the License, as indicated by a
37
+ copyright notice that is included in or attached to the work
38
+ (an example is provided in the Appendix below).
39
+
40
+ "Derivative Works" shall mean any work, whether in Source or Object
41
+ form, that is based on (or derived from) the Work and for which the
42
+ editorial revisions, annotations, elaborations, or other modifications
43
+ represent, as a whole, an original work of authorship. For the purposes
44
+ of this License, Derivative Works shall not include works that remain
45
+ separable from, or merely link (or bind by name) to the interfaces of,
46
+ the Work and Derivative Works thereof.
47
+
48
+ "Contribution" shall mean any work of authorship, including
49
+ the original version of the Work and any modifications or additions
50
+ to that Work or Derivative Works thereof, that is intentionally
51
+ submitted to Licensor for inclusion in the Work by the copyright owner
52
+ or by an individual or Legal Entity authorized to submit on behalf of
53
+ the copyright owner. For the purposes of this definition, "submitted"
54
+ means any form of electronic, verbal, or written communication sent
55
+ to the Licensor or its representatives, including but not limited to
56
+ communication on electronic mailing lists, source code control systems,
57
+ and issue tracking systems that are managed by, or on behalf of, the
58
+ Licensor for the purpose of discussing and improving the Work, but
59
+ excluding communication that is conspicuously marked or otherwise
60
+ designated in writing by the copyright owner as "Not a Contribution."
61
+
62
+ "Contributor" shall mean Licensor and any individual or Legal Entity
63
+ on behalf of whom a Contribution has been received by Licensor and
64
+ subsequently incorporated within the Work.
65
+
66
+ 2. Grant of Copyright License. Subject to the terms and conditions of
67
+ this License, each Contributor hereby grants to You a perpetual,
68
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
69
+ copyright license to reproduce, prepare Derivative Works of,
70
+ publicly display, publicly perform, sublicense, and distribute the
71
+ Work and such Derivative Works in Source or Object form.
72
+
73
+ 3. Grant of Patent License. Subject to the terms and conditions of
74
+ this License, each Contributor hereby grants to You a perpetual,
75
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
76
+ (except as stated in this section) patent license to make, have made,
77
+ use, offer to sell, sell, import, and otherwise transfer the Work,
78
+ where such license applies only to those patent claims licensable
79
+ by such Contributor that are necessarily infringed by their
80
+ Contribution(s) alone or by combination of their Contribution(s)
81
+ with the Work to which such Contribution(s) was submitted. If You
82
+ institute patent litigation against any entity (including a
83
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
84
+ or a Contribution incorporated within the Work constitutes direct
85
+ or contributory patent infringement, then any patent licenses
86
+ granted to You under this License for that Work shall terminate
87
+ as of the date such litigation is filed.
88
+
89
+ 4. Redistribution. You may reproduce and distribute copies of the
90
+ Work or Derivative Works thereof in any medium, with or without
91
+ modifications, and in Source or Object form, provided that You
92
+ meet the following conditions:
93
+
94
+ (a) You must give any other recipients of the Work or
95
+ Derivative Works a copy of this License; and
96
+
97
+ (b) You must cause any modified files to carry prominent notices
98
+ stating that You changed the files; and
99
+
100
+ (c) You must retain, in the Source form of any Derivative Works
101
+ that You distribute, all copyright, patent, trademark, and
102
+ attribution notices from the Source form of the Work,
103
+ excluding those notices that do not pertain to any part of
104
+ the Derivative Works; and
105
+
106
+ (d) If the Work includes a "NOTICE" text file as part of its
107
+ distribution, then any Derivative Works that You distribute must
108
+ include a readable copy of the attribution notices contained
109
+ within such NOTICE file, excluding those notices that do not
110
+ pertain to any part of the Derivative Works, in at least one
111
+ of the following places: within a NOTICE text file distributed
112
+ as part of the Derivative Works; within the Source form or
113
+ documentation, if provided along with the Derivative Works; or,
114
+ within a display generated by the Derivative Works, if and
115
+ wherever such third-party notices normally appear. The contents
116
+ of the NOTICE file are for informational purposes only and
117
+ do not modify the License. You may add Your own attribution
118
+ notices within Derivative Works that You distribute, alongside
119
+ or as an addendum to the NOTICE text from the Work, provided
120
+ that such additional attribution notices cannot be construed
121
+ as modifying the License.
122
+
123
+ You may add Your own copyright statement to Your modifications and
124
+ may provide additional or different license terms and conditions
125
+ for use, reproduction, or distribution of Your modifications, or
126
+ for any such Derivative Works as a whole, provided Your use,
127
+ reproduction, and distribution of the Work otherwise complies with
128
+ the conditions stated in this License.
129
+
130
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
131
+ any Contribution intentionally submitted for inclusion in the Work
132
+ by You to the Licensor shall be under the terms and conditions of
133
+ this License, without any additional terms or conditions.
134
+ Notwithstanding the above, nothing herein shall supersede or modify
135
+ the terms of any separate license agreement you may have executed
136
+ with Licensor regarding such Contributions.
137
+
138
+ 6. Trademarks. This License does not grant permission to use the trade
139
+ names, trademarks, service marks, or product names of the Licensor,
140
+ except as required for reasonable and customary use in describing the
141
+ origin of the Work and reproducing the content of the NOTICE file.
142
+
143
+ 7. Disclaimer of Warranty. Unless required by applicable law or
144
+ agreed to in writing, Licensor provides the Work (and each
145
+ Contributor provides its Contributions) on an "AS IS" BASIS,
146
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
147
+ implied, including, without limitation, any warranties or conditions
148
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
149
+ PARTICULAR PURPOSE. You are solely responsible for determining the
150
+ appropriateness of using or redistributing the Work and assume any
151
+ risks associated with Your exercise of permissions under this License.
152
+
153
+ 8. Limitation of Liability. In no event and under no legal theory,
154
+ whether in tort (including negligence), contract, or otherwise,
155
+ unless required by applicable law (such as deliberate and grossly
156
+ negligent acts) or agreed to in writing, shall any Contributor be
157
+ liable to You for damages, including any direct, indirect, special,
158
+ incidental, or consequential damages of any character arising as a
159
+ result of this License or out of the use or inability to use the
160
+ Work (including but not limited to damages for loss of goodwill,
161
+ work stoppage, computer failure or malfunction, or any and all
162
+ other commercial damages or losses), even if such Contributor
163
+ has been advised of the possibility of such damages.
164
+
165
+ 9. Accepting Warranty or Additional Liability. While redistributing
166
+ the Work or Derivative Works thereof, You may choose to offer,
167
+ and charge a fee for, acceptance of support, warranty, indemnity,
168
+ or other liability obligations and/or rights consistent with this
169
+ License. However, in accepting such obligations, You may act only
170
+ on Your own behalf and on Your sole responsibility, not on behalf
171
+ of any other Contributor, and only if You agree to indemnify,
172
+ defend, and hold each Contributor harmless for any liability
173
+ incurred by, or claims asserted against, such Contributor by reason
174
+ of your accepting any such warranty or additional liability.
175
+
176
+ END OF TERMS AND CONDITIONS
177
+
178
+ APPENDIX: How to apply the Apache License to your work.
179
+
180
+ To apply the Apache License to your work, attach the following
181
+ boilerplate notice, with the fields enclosed by brackets "[]"
182
+ replaced with your own identifying information. (Don't include
183
+ the brackets!) The text should be enclosed in the appropriate
184
+ comment syntax for the file format. We also recommend that a
185
+ file or class name and description of purpose be included on the
186
+ same "printed page" as the copyright notice for easier
187
+ identification within third-party archives.
188
+
189
+ Copyright [yyyy] [name of copyright owner]
190
+
191
+ Licensed under the Apache License, Version 2.0 (the "License");
192
+ you may not use this file except in compliance with the License.
193
+ You may obtain a copy of the License at
194
+
195
+ http://www.apache.org/licenses/LICENSE-2.0
196
+
197
+ Unless required by applicable law or agreed to in writing, software
198
+ distributed under the License is distributed on an "AS IS" BASIS,
199
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
200
+ See the License for the specific language governing permissions and
201
+ limitations under the License.
package/README.md ADDED
@@ -0,0 +1,30 @@
1
+ # signalk-wyoming
2
+
3
+ > **Status: RFC.** This is a design under community review — nothing is functional yet. The npm package is a 0.0.1 placeholder reserving the name. **Read [SPEC.md](SPEC.md) and tell us what you think** via [issues](https://github.com/hoeken/signalk-wyoming/issues).
4
+
5
+ An offline voice assistant for your boat, built on [Signal K](https://signalk.org) and the [Wyoming protocol](https://github.com/rhasspy/wyoming) (the voice ecosystem behind Home Assistant's Assist: Whisper, Piper, openWakeWord).
6
+
7
+ - **The boat talks:** any plugin, node-red flow, or API client can speak through satellites anywhere on the boat — and Signal K notifications (anchor alarm, engine temperature, AIS proximity) are spoken automatically.
8
+ - **The boat listens:** wake-word-triggered voice commands are transcribed and published to a subscribable Signal K path (`voice.command`) for anything to consume.
9
+ - **Fully offline.** No cloud. Everything runs in containers on the boat.
10
+
11
+ ## The plugin family
12
+
13
+ | Plugin | Role | Standalone? |
14
+ |--------|------|-------------|
15
+ | [signalk-wyoming](https://github.com/hoeken/signalk-wyoming) | Orchestrator: pipelines, satellite manager, TTS routing, notification→speech bridge, `voice.*` paths, webapp | needs the services below |
16
+ | [signalk-whisper](https://github.com/hoeken/signalk-whisper) | Speech-to-text (Wyoming Whisper) | ✅ usable by any Wyoming client, incl. Home Assistant |
17
+ | [signalk-piper](https://github.com/hoeken/signalk-piper) | Text-to-speech (Wyoming Piper) | ✅ |
18
+ | [signalk-openwakeword](https://github.com/hoeken/signalk-openwakeword) | Wake word detection (Wyoming openWakeWord) | ✅ |
19
+
20
+ All services run as containers managed through signalk-container / [signalk-container-helper](https://github.com/hoeken/signalk-container-helper). Audio endpoints are [wyoming-satellite](https://github.com/rhasspy/wyoming-satellite) devices around the boat — including an optional "local satellite" on the server box itself.
21
+
22
+ ## Read the spec
23
+
24
+ **[SPEC.md](SPEC.md)** covers the architecture, decisions made (and why), plugin configuration, the `voice.*` path schema, the `say()` API surface, audio configuration strategy, and the roadmap. [IDEA.md](IDEA.md) is the original ideation doc, kept for provenance.
25
+
26
+ Feedback is very welcome — especially from anyone who has fought boat audio, runs Signal K in a container, or uses Wyoming satellites with Home Assistant.
27
+
28
+ ## License
29
+
30
+ Apache-2.0
package/SPEC.md ADDED
@@ -0,0 +1,332 @@
1
+ # signalk-wyoming — Specification
2
+
3
+ **Status:** Draft v0.2 — RFC, community comments welcome (2026-07-23)
4
+ **Scope:** A family of SignalK plugins integrating the [Wyoming protocol](https://github.com/rhasspy/wyoming) voice-assistant ecosystem (Whisper STT, Piper TTS, openWakeWord) into SignalK, delivered as containerized microservices via signalk-container / [signalk-container-helper](https://github.com/hoeken/signalk-container-helper).
5
+
6
+ ---
7
+
8
+ ## 1. Decision log
9
+
10
+ Decisions made during ideation, with the deciding rationale:
11
+
12
+ | # | Decision | Rationale |
13
+ |---|----------|-----------|
14
+ | D1 | **Orchestrator and self-satellite are one plugin** (`signalk-wyoming`), self-satellite as opt-in config, off by default | SignalK plugins share one Node process, so a separate plugin buys no isolation; merging avoids cross-plugin lifecycle handshakes, a shared library, and a second install for the common case. A `role` setting (satellite-only mode) remains a future escape hatch. |
15
+ | D2 | **Local audio via a containerized satellite** (self-maintained `wyoming-satellite`-based image), not native in-plugin audio | Many SignalK installs are themselves containerized; native `arecord`/`aplay` would require users to modify the SignalK image/compose. A sibling container launched via the host Docker socket works identically across all deployments, and a container runtime is already a hard prerequisite of this plugin family. No upstream satellite image exists, so we build one either way — and owning it lets us bake in device-test tooling. |
16
+ | D3 | **Publish-only intent model** — recognized text is published to `voice.command`; intent handling is explicitly out of scope | Clean boundary. node-red, custom plugins, or a future intents/LLM plugin consume commands and respond via the `say()` API. |
17
+ | D4 | **Notification→speech bridge is a core v1 feature** | The killer boat feature; works with zero microphones, making Piper + orchestrator alone a useful install. |
18
+ | D5 | **Flat plugin names** (`signalk-whisper`, `signalk-piper`, `signalk-openwakeword`, `signalk-wyoming`) | Shorter than family-prefixed names; npm availability confirmed for all four. Names reserved with 0.0.1 placeholder releases. |
19
+
20
+ ---
21
+
22
+ ## 2. Overview
23
+
24
+ ### 2.1 What this is
25
+
26
+ Voice in and voice out for a boat's SignalK server, fully offline:
27
+
28
+ - **Voice out:** any plugin, node-red flow, or API client can make the boat speak ("Anchor alarm: drag detected") through speakers anywhere on the boat. SignalK notifications are spoken automatically.
29
+ - **Voice in:** wake-word-triggered voice commands ("ok nabu, log my position") are transcribed and published to a subscribable SignalK path for anything to consume.
30
+
31
+ ### 2.2 The plugin family
32
+
33
+ | Plugin | Role | Wraps | Default port | Standalone? |
34
+ |--------|------|-------|--------------|-------------|
35
+ | `signalk-whisper` | Speech-to-text service | `rhasspy/wyoming-whisper` (faster-whisper) | 10300 | ✅ Usable by any Wyoming client, incl. Home Assistant |
36
+ | `signalk-piper` | Text-to-speech service | `rhasspy/wyoming-piper` | 10200 | ✅ Same |
37
+ | `signalk-openwakeword` | Wake word detection service | `rhasspy/wyoming-openwakeword` | 10400 | ✅ Same |
38
+ | `signalk-wyoming` | Orchestrator: pipelines, satellite manager, TTS router, notification bridge, SignalK paths/API, webapp. Optionally runs a **local satellite** container (mic/speaker on the server box). | our own satellite image (§7) | satellite: 10700, control: 10800 | Needs ≥1 of whisper/piper + ≥1 audio endpoint |
39
+
40
+ All four require **signalk-container** (container runtime manager, reached via `globalThis`) and use **signalk-container-helper** (`ManagedContainer`, `buildConfig()`, `startSafely()`, offline-tolerant conventions).
41
+
42
+ ### 2.3 Non-goals (v1)
43
+
44
+ - Intent handling / NLU / conversation (D3 — future separate plugin)
45
+ - ESPHome-based voice devices (HA Voice PE, ESP32-S3-BOX) — they speak ESPHome's native protocol, not Wyoming
46
+ - Cloud STT/TTS providers (offline-first; pluggable providers possible later)
47
+ - Media/music playback, ducking, multiroom audio sync
48
+ - GPU acceleration
49
+ - Self-satellite on non-Linux hosts
50
+
51
+ ---
52
+
53
+ ## 3. Architecture
54
+
55
+ ### 3.1 Topology
56
+
57
+ Wyoming services are plain TCP servers. **The orchestrator is a Wyoming TCP client to everything** — it dials out to whisper, piper, openwakeword, and every satellite (satellites *listen* on :10700; the server connects to them, as Home Assistant does).
58
+
59
+ ```
60
+ ┌──── boat LAN ────────────┐
61
+ ┌────────────────── SignalK server host ──────────────┐ │ │
62
+ │ │ │ Remote satellites │
63
+ │ SignalK server process (native OR containerized) │ │ (upstream or our image, │
64
+ │ ┌───────────────────────────────────────────────┐ │ │ each listens :10700) │
65
+ │ │ signalk-wyoming (orchestrator) │──────► cockpit │
66
+ │ │ · satellite mgr · pipelines · VAD │ │ │ ► salon │
67
+ │ │ · say() router · notification bridge │ │ │ ► cabin │
68
+ │ │ · voice.* paths · REST/PUT API · webapp │ │ └──────────────────────────┘
69
+ │ └───┬────────────┬────────────┬────────────┬────┘ │
70
+ │ │ :10300 │ :10200 │ :10400 │ :10700/:10800
71
+ │ ┌───▼──────┐ ┌───▼─────┐ ┌────▼─────────┐ ┌▼───────────────────┐
72
+ │ │ whisper │ │ piper │ │ openwakeword │ │ local satellite │
73
+ │ │ (STT) │ │ (TTS) │ │ (wake) │ │ (mic/spkr,/dev/snd)│
74
+ │ └──────────┘ └─────────┘ └──────────────┘ └────────────────────┘
75
+ │ sibling containers managed via signalk-container │
76
+ └──────────────────────────────────────────────────────────────────┘
77
+ ```
78
+
79
+ When SignalK itself runs in a container, signalk-container launches these as **sibling containers** via the host Docker socket; the SignalK container needs no audio access of any kind.
80
+
81
+ ### 3.2 The `Satellite` interface (the seam)
82
+
83
+ The orchestrator manages satellites through one internal interface:
84
+
85
+ ```ts
86
+ interface Satellite {
87
+ id: string; name: string; zone?: string;
88
+ wakeMode: 'satellite' | 'central';
89
+ state: 'disconnected' | 'idle' | 'listening' | 'transcribing' | 'speaking';
90
+ connect(): void; // maintains connection w/ backoff
91
+ play(audio: AudioStream, opts?: { priority?: 'normal' | 'urgent' }): Promise<void>;
92
+ events: /* wake detections, audio stream, state changes */;
93
+ }
94
+ ```
95
+
96
+ v1 ships one implementation: `RemoteSatellite` (Wyoming TCP client). The **local satellite is simply a `RemoteSatellite` pointed at `tcp://localhost:10700`** whose container lifecycle the plugin also owns. A future native in-process backend (bare-metal optimization) or a `role: satellite-only` mode slots in behind this interface with zero orchestrator changes.
97
+
98
+ ### 3.3 Pipeline flows
99
+
100
+ **Voice command — satellite wake mode** (satellite drives its own wake detection):
101
+ 1. Satellite streams its mic audio to a wake service (`--wake-uri`, typically the boat's central `signalk-openwakeword`).
102
+ 2. On detection, satellite plays its awake sound locally and asks the orchestrator to run a pipeline; audio streams to the orchestrator.
103
+ 3. Orchestrator opens a `transcribe` session with whisper, forwards audio, and runs **endpointing** (§3.4) to decide when the utterance ended.
104
+ 4. Whisper returns `transcript`; orchestrator publishes to `voice.command` (§5.1), updates state paths, satellite plays its done sound.
105
+
106
+ **Voice command — central wake mode** (dumb satellite, always streaming):
107
+ Same, except the satellite streams continuously to the orchestrator, which tees audio to openwakeword and watches for `detection` itself. Trades continuous LAN audio (~256 kbit/s/satellite raw PCM) and server CPU for simpler satellites.
108
+
109
+ **Announcement:**
110
+ 1. `say()` invoked (REST / PUT / PropertyValues / notification bridge).
111
+ 2. Orchestrator sends `synthesize` to piper, receives `audio-start/chunk/stop`.
112
+ 3. Audio streams to each target satellite's per-satellite FIFO queue; `priority: 'urgent'` jumps the queue. State paths update (`speaking`).
113
+
114
+ **Notification bridge:**
115
+ `notifications.*` delta → filter (state ≥ `minState`, `method` includes `sound` unless configured otherwise, only on state *transitions*) → `say(value.message ?? "Alarm: <path>", targets, urgent if emergency)`.
116
+
117
+ ### 3.4 Endpointing (utterance segmentation)
118
+
119
+ Lives in the orchestrator. v1: silero-VAD via `onnxruntime-node`, with an energy-gate + timeout fallback if the dependency proves painful. Tunables (advanced config): `silenceMs` (default 800), `maxUtteranceMs` (10000), `minUtteranceMs` (300). This is the one genuinely non-trivial implementation detail in the orchestrator — spike it early (M2).
120
+
121
+ ---
122
+
123
+ ## 4. Cross-cutting conventions
124
+
125
+ ### 4.1 Service discovery (PropertyValues)
126
+
127
+ Each service plugin advertises itself on the shared property name **`wyoming-service`**:
128
+
129
+ ```js
130
+ app.emitPropertyValue('wyoming-service', {
131
+ plugin: 'signalk-whisper', // key for latest-wins de-duplication
132
+ type: 'asr', // 'asr' | 'tts' | 'wake'
133
+ uri: 'tcp://localhost:10300',
134
+ status: 'ready', // 'starting' | 'ready' | 'stopped' | 'error'
135
+ })
136
+ ```
137
+
138
+ PropertyValues replays history to late subscribers, so start order never matters. Consumers keep the **latest value per `plugin`**. Plugins emit again on every status change (including `stopped` in their `stop()` hook). Third-party plugins may advertise additional Wyoming services under the same convention.
139
+
140
+ The orchestrator's service config defaults to `auto` (use discovery) with manual `tcp://host:port` overrides — so an off-boat GPU box or an existing Home Assistant add-on can be used instead of the sibling plugins.
141
+
142
+ ### 4.2 Model management (offline-first)
143
+
144
+ "First use at sea with no internet" is a real failure mode. Therefore:
145
+
146
+ - Models download **at plugin install/first-start**, into the plugin's `signalkDataMount` volume, and survive container recreation.
147
+ - Plugin status (`app.setPluginStatus`) reports download progress and blocks "ready" (and the `wyoming-service` `ready` emission) until models are present.
148
+ - Approximate sizes documented in each plugin README: whisper tiny ≈ 75 MB / base ≈ 150 MB / small ≈ 500 MB (int8 variants smaller); piper voice ≈ 60 MB; openWakeWord models ≈ 1–5 MB.
149
+
150
+ ### 4.3 Error surfacing
151
+
152
+ - Service unreachable / container unhealthy → SignalK notification `notifications.voice.<service>` (state `alarm`), plus plugin status text.
153
+ - `say()` with piper down → immediate error result (REST 503 / PUT failure / rejected promise), never a silent drop.
154
+ - All containers use container-helper health checks + restart policy; offline Docker-pull failures are non-fatal per helper conventions.
155
+
156
+ ---
157
+
158
+ ## 5. `signalk-wyoming` (orchestrator) — detailed spec
159
+
160
+ ### 5.1 SignalK paths (under `vessels.self`)
161
+
162
+ Custom `voice.*` branch (outside the SignalK schema — normal plugin practice):
163
+
164
+ | Path | Value | Notes |
165
+ |------|-------|-------|
166
+ | `voice.command` | `{ text, satellite, zone?, language, wakeWord?, durationMs? }` | One delta per utterance. `$source` = `signalk-wyoming.<satelliteId>` so source-based filtering also works. |
167
+ | `voice.satellites.<id>.connected` | `boolean` | |
168
+ | `voice.satellites.<id>.state` | `'idle' \| 'listening' \| 'transcribing' \| 'speaking'` | |
169
+ | `voice.say` | write-only PUT target | See §5.2 |
170
+
171
+ ### 5.2 The `say()` surface (all three delegate to one internal function)
172
+
173
+ `say(opts)` where `opts = { text, targets?: string[] /* satellite ids or zones, default all */, voice?, priority?: 'normal'|'urgent' }` → `Promise<{ ok, queued: string[], errors?: [...] }>`
174
+
175
+ 1. **REST** (respects SignalK write auth):
176
+ - `POST /plugins/signalk-wyoming/api/say` — body is `opts`; returns 202 + result
177
+ - `GET /plugins/signalk-wyoming/api/satellites` — status list
178
+ - `GET /plugins/signalk-wyoming/api/services` — discovered services + health
179
+ - `POST /plugins/signalk-wyoming/api/satellites/:id/test` — play test tone
180
+ 2. **PUT** on `voice.say` — value is a plain string or the `opts` object. Works from node-red and any SignalK client with write permission.
181
+ 3. **PropertyValues API object** (in-process, for other plugins):
182
+ ```js
183
+ app.emitPropertyValue('signalk-wyoming.api', { version: 1, say })
184
+ ```
185
+ Emitting an API object (not say-*events*) is deliberate: PropertyValues replays history, which would re-speak stale messages; replaying an API handle is instead a feature — late-loading plugins get it automatically.
186
+
187
+ ### 5.3 Notification bridge (core v1)
188
+
189
+ Config:
190
+
191
+ | Field | Default | Notes |
192
+ |-------|---------|-------|
193
+ | `enabled` | `true` | |
194
+ | `minState` | `alarm` | speak `alert`/`warn`/`alarm`/`emergency` at or above this |
195
+ | `requireSoundMethod` | `true` | only speak notifications whose `method` includes `sound` |
196
+ | `includePaths` / `excludePaths` | `[]` | glob patterns on the notification path |
197
+ | `targets` | `all` | satellite ids or zones |
198
+
199
+ Behavior: speak on state *transitions* into qualifying states (no re-speak of unchanged notifications; escalation re-speaks). `emergency` maps to `priority: 'urgent'`. Spoken text is the notification's `message`, falling back to a path-derived phrase. Quiet hours and repeat/nag policies are v1.x (§10).
200
+
201
+ ### 5.4 Satellites config
202
+
203
+ ```
204
+ satellites: [
205
+ { id: 'cockpit', name: 'Cockpit', host: '10.10.10.21', port: 10700,
206
+ zone: 'deck', wakeMode: 'satellite' | 'central', wakeWords?: ['ok_nabu'] }
207
+ ]
208
+ localSatellite: {
209
+ enabled: false, // D1: opt-in, off by default
210
+ micDevice: 'auto', sndDevice: 'auto',// populated via control-endpoint enumeration
211
+ wakeMode: 'central', wakeWords?: [],
212
+ audioMode: 'alsa' | 'pulse-socket', // §8: headless vs desktop hosts
213
+ feedbackSounds: true
214
+ }
215
+ services: { asr: 'auto'|uri, tts: 'auto'|uri, wake: 'auto'|uri }
216
+ defaults: { language: 'en', voice: /* piper voice */ }
217
+ ```
218
+
219
+ Degraded modes are first-class: TTS-only (no whisper — announcements only, the recommended starter install), STT-only (no piper — commands published, no spoken replies). `wakeMode: central` or a wake-enabled local satellite without openwakeword available → config-time warning + `notifications.voice.wake` at runtime.
220
+
221
+ ### 5.5 Webapp
222
+
223
+ One webapp, four screens. This is the answer to "audio config is frustrating":
224
+
225
+ 1. **Status** — satellites (connection/state), services (health, model info), recent commands/announcements log.
226
+ 2. **Audio setup** — device **dropdowns** (populated from the satellite image's control endpoint, §7), *record 3 s & play back* button, *play tone* button, live VU meter (websocket). No free-text ALSA device strings unless the user opens "advanced".
227
+ 3. **Test** — type-and-say box (target picker), **push-to-talk** button (browser mic → orchestrator → whisper; doubles as the STT test before any wake word exists — note browsers require HTTPS or localhost for `getUserMedia`), wake-word test with live detection feedback.
228
+ 4. **Config** — friendlier editor for the plugin config (the JSON-schema auto-UI degrades at this schema size).
229
+
230
+ ---
231
+
232
+ ## 6. Service plugins — config specs
233
+
234
+ Common to all three (via container-helper `buildConfig()`): image tag (default pinned, `auto` supported), port, restart policy, resource limits (memory cap especially — whisper), data mount for models. Each emits `wyoming-service` per §4.1. Recommended default: bind whisper/piper to localhost/docker-network only (§9).
235
+
236
+ ### 6.1 `signalk-whisper`
237
+
238
+ | Param | Default | Stretch |
239
+ |-------|---------|---------|
240
+ | `model` | `tiny-int8` | — |
241
+ | `language` | `auto` | fixing to e.g. `en` improves speed/accuracy |
242
+ | `port` | 10300 | |
243
+ | — | | `beamSize`, `computeType`, `initialPrompt` (nautical vocabulary priming — likely high value: "genoa", "windlass", "gybe"), GPU |
244
+
245
+ ### 6.2 `signalk-piper`
246
+
247
+ | Param | Default | Stretch |
248
+ |-------|---------|---------|
249
+ | `voice` | `en_US-lessac-medium` | multiple preloaded voices; per-message `voice` override honored from `say()` |
250
+ | `port` | 10200 | |
251
+ | — | | `lengthScale` (speech rate), streaming synthesis |
252
+
253
+ ### 6.3 `signalk-openwakeword`
254
+
255
+ | Param | Default | Stretch |
256
+ |-------|---------|---------|
257
+ | `wakeWords` | `['ok_nabu']` | per-satellite wake words |
258
+ | `threshold` | 0.5 | |
259
+ | `triggerLevel` | 1 | |
260
+ | `port` | 10400 | |
261
+ | — | | custom `.tflite` model upload via webapp |
262
+
263
+ ---
264
+
265
+ ## 7. The satellite container image
266
+
267
+ **The one piece we build from scratch** (no upstream image exists; upstream pushes systemd installs). Working name: `ghcr.io/hoeken/wyoming-satellite` — deliberately SignalK-agnostic, usable standalone on any Docker host (cockpit Pi, or even by Home Assistant users; a small contribution back to the Wyoming ecosystem).
268
+
269
+ - **Contents:** upstream `wyoming-satellite` (pinned release) + `alsa-utils`, on a slim Debian/Python base. MIT, with attribution.
270
+ - **Arch:** `linux/arm64`, `linux/amd64` (armv7 = open question).
271
+ - **Ports:** `:10700` Wyoming satellite protocol; `:10800` control API.
272
+ - **Control API** (the audio-UX enabler, identical across all deployment scenarios):
273
+ - `GET /devices` — parsed `arecord -l` / `aplay -l`
274
+ - `POST /test/record` — record N seconds, return WAV
275
+ - `POST /test/play` — tone or uploaded WAV
276
+ - `GET /vu` — websocket/SSE stream of mic RMS levels
277
+ - `GET /health`
278
+ - **Env config:** `MIC_DEVICE`, `SND_DEVICE`, `WAKE_URI`, `VAD`, `NOISE_SUPPRESSION`, `AUTO_GAIN`, `MIC_VOLUME`, `AWAKE_WAV`/`DONE_WAV` (defaults baked in, overridable via data mount).
279
+ - Runs upstream's VAD / noise-suppression / auto-gain — the Python audio stack we chose not to reimplement in Node (D2).
280
+
281
+ ---
282
+
283
+ ## 8. Audio configuration strategy
284
+
285
+ Principle: **audio never touches the SignalK process.** Whisper/piper/openwakeword are pure network services; all capture/playback happens in satellite containers (or remote satellites), which our image + control API make testable from the webapp.
286
+
287
+ | Deployment | Local-satellite approach |
288
+ |------------|--------------------------|
289
+ | SignalK native or containerized, **headless host** (Pi OS Lite — the common boat server) | `/dev/snd` passthrough, `audio` group. Raw ALSA, no sound server to fight. **Default path.** |
290
+ | **Desktop host** (OpenPlotter / Pi OS desktop, PipeWire owns devices) | `audioMode: pulse-socket` — mount the host sound-server socket instead of `/dev/snd`. Documented recipe; contention warning surfaced in webapp if `/dev/snd` mode fails. |
291
+
292
+ **Hardware recommendation (documented prominently):** a USB conference speakerphone (Anker PowerConf, Jabra Speak) — onboard DSP does echo cancellation / AGC / noise suppression in hardware, better than any software path, and it's one cable. Remote satellites: ship a tested "boat satellite" recipe (Pi Zero 2 W + speakerphone + our image or upstream installer).
293
+
294
+ ---
295
+
296
+ ## 9. Security
297
+
298
+ - REST/PUT respect SignalK's access control (write permission for `say`, admin for config). Webapp config screens require admin.
299
+ - **Wyoming has no authentication.** Defaults: whisper/piper bound to localhost/docker network (only the orchestrator needs them). openwakeword must be LAN-reachable only if remote satellites use `wakeMode: satellite` — call this out in config UI. Satellites are LAN-trust; document firewalling guidance for marina wifi. TLS/auth on Wyoming links: out of scope v1.
300
+
301
+ ---
302
+
303
+ ## 10. Roadmap
304
+
305
+ **M0 — Foundations (prerequisites, other repos):**
306
+ - signalk-container + helper: `devices: ['/dev/snd']`, `groupAdd: ['audio']` in `buildConfig()`; hot-plug handled properly (`device-cgroup-rule` + `/dev/snd` bind-mount rather than static `--device`) — generally useful beyond audio (serial/GPS dongles).
307
+ - Satellite image repo + multi-arch CI + control API.
308
+ - ~~Reserve npm names~~ Done — 0.0.1 placeholders published without the `signalk-node-server-plugin` keyword, so they stay out of the app store until a functional release.
309
+
310
+ **M1 — "The boat talks"** (useful with zero microphones): `signalk-piper`; orchestrator with `say()` core (REST + PUT + PropertyValues), local satellite (output-only), **notification bridge**, status paths, webapp Status + basic Test screens.
311
+
312
+ **M2 — "The boat listens":** `signalk-whisper`; pipeline engine + VAD endpointing spike; webapp push-to-talk and record/playback screens; `voice.command` publishing. (PTT makes STT testable before wake words exist.)
313
+
314
+ **M3 — "Hands-free":** `signalk-openwakeword`; both wake modes; feedback sounds; wake test UI.
315
+
316
+ **M4 — "Whole-boat":** remote satellites hardened (reconnect/backoff soak testing), zones/targets, queue polish, Pi satellite recipe docs.
317
+
318
+ **v1.x candidates:** quiet hours + repeat/nag policy for the bridge; priority queue refinement; per-satellite voices; mDNS "scan for satellites"; custom wake-word model upload.
319
+
320
+ **Stretch:** browser satellite webapp (`getUserMedia` → websocket satellite; zero audio config on any tablet — needs HTTPS); `role: satellite-only` mode; native in-process local satellite (bare-metal desktop optimization behind the `Satellite` seam); streaming TTS; Snapcast as an announce target; intents/LLM assistant as a **separate plugin** consuming `voice.command` + `signalk-wyoming.api`; GPU whisper.
321
+
322
+ ---
323
+
324
+ ## 11. Open questions
325
+
326
+ 1. **`voice.command` value shape:** object (recommended, self-contained for node-red) vs plain string + `$source`-only satellite attribution. Spec assumes object.
327
+ 2. **VAD choice:** silero via `onnxruntime-node` vs energy gate for v1 — decide after M2 spike.
328
+ 3. **Satellite image registry/name:** ghcr.io vs Docker Hub; `wyoming-satellite` vs `signalk-wyoming-satellite`.
329
+ 4. **armv7 support** for the satellite image (Pi Zero 2 W runs arm64; older Pis?).
330
+ 5. **Whisper default model:** `tiny-int8` (Pi-safe) vs `base-int8` (better accuracy, still OK on Pi 5) — benchmark on real hardware in M2.
331
+ 6. **Control API port** (10800 proposed) — confirm no conflicts with common marine stacks.
332
+ 7. Assumption to verify: signalk-container is available/public in time for M0 (spec written against the helper README's description of it).
package/index.js ADDED
@@ -0,0 +1,23 @@
1
+ module.exports = function (app) {
2
+ const plugin = {
3
+ id: 'signalk-wyoming',
4
+ name: 'Wyoming Voice Assistant',
5
+ description:
6
+ 'Wyoming protocol voice assistant orchestrator for Signal K — pre-release placeholder, not yet functional.',
7
+
8
+ schema: () => ({
9
+ type: 'object',
10
+ properties: {},
11
+ }),
12
+
13
+ start: (options) => {
14
+ app.setPluginStatus(
15
+ 'Pre-release placeholder — not yet functional. See https://github.com/hoeken/signalk-wyoming'
16
+ )
17
+ },
18
+
19
+ stop: () => {},
20
+ }
21
+
22
+ return plugin
23
+ }
package/package.json ADDED
@@ -0,0 +1,29 @@
1
+ {
2
+ "name": "signalk-wyoming",
3
+ "version": "0.0.1",
4
+ "description": "Wyoming protocol voice assistant orchestrator for Signal K — voice commands in, spoken announcements out. Pre-release placeholder.",
5
+ "main": "index.js",
6
+ "files": [
7
+ "index.js",
8
+ "README.md",
9
+ "SPEC.md",
10
+ "LICENSE"
11
+ ],
12
+ "keywords": [
13
+ "wyoming",
14
+ "voice-assistant",
15
+ "voice",
16
+ "speech",
17
+ "marine",
18
+ "boat"
19
+ ],
20
+ "author": "hoeken",
21
+ "license": "Apache-2.0",
22
+ "repository": {
23
+ "type": "git",
24
+ "url": "git+https://github.com/hoeken/signalk-wyoming.git"
25
+ },
26
+ "engines": {
27
+ "node": ">=22"
28
+ }
29
+ }