signalk-mareas-ihm 2.0.2 → 2.1.0

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.
package/dist/README.md ADDED
@@ -0,0 +1,215 @@
1
+ # signalk-mareas-ihm
2
+
3
+ 🇪🇸 [Español](#español) · 🇬🇧 [English](#english)
4
+
5
+ ---
6
+
7
+ ## Español
8
+
9
+ Plugin de Signal K que convierte tu OpenPlotter / Raspberry Pi en un **visor completo de fondeo**: mareas oficiales del **IHM (Instituto Hidrográfico de la Marina)**, previsión de abrigo, medición de olas a bordo, alarmas inteligentes y soporte para domótica.
10
+
11
+ ### Características principales
12
+
13
+ #### Mareas
14
+ - **Predicción de mareas** automática por GPS o selección manual entre 70 estaciones del IHM.
15
+ - **Curvas de marea interactivas** con HAT/LAT anuales, cursor y etiquetas de pleamar/bajamar.
16
+ - **Coeficientes oficiales IHM** con descarga automática.
17
+ - **Caché offline** de al menos 2 meses de predicciones.
18
+ - **Tendencia y coeficiente** visibles en la barra superior del visor.
19
+
20
+ #### Previsión de abrigo
21
+ - **Rosa de 16 sectores** con detección automática del abrigo según la costa (OpenStreetMap).
22
+ - **Grado A-F y porcentaje de protección** combinando viento y olas previstos.
23
+ - **Strip de exposición** próximas 12 h con previsión hora a hora.
24
+ - **Resumen "AHORA / PREDICCIÓN"** con datos en tiempo real de sensores y previsión.
25
+ - **Edición manual** de sectores cuando el detector automático no convence.
26
+
27
+ #### Medición de olas en fondeo
28
+ - **Dirección, período y altura** de ola calculados a bordo desde sensores de actitud y aceleración (pypilot IMU u otros compatibles).
29
+ - **Historial de las últimas 24 h** en barras de 15 minutos.
30
+ - **El grado de abrigo se ajusta automáticamente** cuando la ola medida supera la prevista.
31
+
32
+ #### Visor de fondeo
33
+ - **Mapa Leaflet** con posición GPS en tiempo real y capas múltiples (ESRI, Bing, Google, IHM S-52, SonarChart, MBTiles offline).
34
+ - **Anchor Watch** con alarma de garreo visual y sonora.
35
+ - **Cadena largada** con slider bidireccional y cálculo automático (métodos tradicional y Vicente).
36
+ - **Radio de borneo y alarma** con etiquetas en la carta y predicción según marea.
37
+ - **Sincronización multi-dispositivo** en tiempo real (SSE).
38
+ - **Cartas náuticas locales** vía servidor MBTiles integrado.
39
+
40
+ #### Sensores en tiempo real
41
+ - Lectura directa de **viento (veleta)**, **temperatura aire/agua** y **presión atmosférica** de Signal K.
42
+ - Etiquetas **"Veleta"** y **"Sensor"** distinguen datos reales de previsión.
43
+ - **Fallback automático a Open-Meteo** cuando un sensor falla.
44
+ - **Sonda inteligente**: detecta congelación / spikes / valores absurdos y deja de mostrar lecturas inventadas.
45
+
46
+ #### AIS inteligente
47
+ - **Detección de colisión** con targets AIS en zona de borneo.
48
+ - **ACK por target individual** — silencia alarma de un barco sin desactivar la alarma general.
49
+ - **Detección de garreo ajeno** — si un target ACKed se acerca >2 m/min, la alarma se reactiva.
50
+ - **Estimación de ancla** de otros barcos mediante análisis de track (centroide + radio máximo).
51
+
52
+ #### Alarmas y audio
53
+ - **Alarmas independientes**: garreo, varada, AIS y sonda con control individual.
54
+ - **Voces pregrabadas (OGG)** por idioma — más naturales que el sintetizador.
55
+ - **Detección automática** de la salida de audio del Raspberry Pi (USB → analog → HDMI).
56
+ - **Soporte móvil** fiable (audio funciona incluso con la pestaña en segundo plano).
57
+ - **Patrones distintos** por evento ("tuc tuc tuc" para AIS, sirena para garreo, etc.).
58
+
59
+ #### Domótica / KIP
60
+ - **Botones KIP** para fondear/levar y activar/desactivar alarmas.
61
+ - **Endpoints REST** para Alexa, Google Home, Node-RED, MQTT.
62
+ - **Endpoint toggle** para mandos a distancia con un solo botón.
63
+
64
+ #### Bilingüe completo
65
+ - Interfaz en **español e inglés** en todas las vistas.
66
+ - Compass cardinal en español correcto (N/NE/E/SE/S/SO/O/NO).
67
+ - Banderas de idioma en la barra inferior del visor.
68
+
69
+ ### URLs
70
+
71
+ | URL | Función |
72
+ |-----|---------|
73
+ | `/signalk-mareas-ihm/` | Landing — selector Mareas / Visor de Fondeo |
74
+ | `/signalk-mareas-ihm/mareas` | Vista de Mareas (directo) |
75
+ | `/signalk-mareas-ihm/visorfondeo` | Visor de Fondeo (directo) |
76
+
77
+ ### Domótica — Endpoints REST
78
+
79
+ ```bash
80
+ # Fondear/Levar con un solo botón
81
+ curl -X POST http://openplotter.local:3000/signalk-mareas-ihm/api/anchor-watch/toggle
82
+
83
+ # Fondear en posición GPS actual
84
+ curl -X POST http://openplotter.local:3000/signalk-mareas-ihm/api/anchor-watch/drop
85
+
86
+ # Levar ancla
87
+ curl -X POST http://openplotter.local:3000/signalk-mareas-ihm/api/anchor-watch/lift
88
+
89
+ # Estado simple (para monitorización)
90
+ curl http://openplotter.local:3000/signalk-mareas-ihm/api/anchor-watch/simple
91
+ ```
92
+
93
+ ### KIP — Paths para Boolean Control
94
+
95
+ | Path | Función |
96
+ |------|---------|
97
+ | `environment.anchor.mareasIhm.anchorCommand` | Fondear (true) / Levar (false) |
98
+ | `environment.anchor.mareasIhm.garreoAlarmCommand` | Alarma garreo ON/OFF |
99
+ | `environment.anchor.mareasIhm.aisAlarmCommand` | Alarma AIS ON/OFF |
100
+
101
+ ### Instalación
102
+
103
+ Desde el **Signal K Appstore**: buscar `signalk-mareas-ihm` e instalar.
104
+
105
+ O por línea de comandos:
106
+
107
+ ```bash
108
+ cd ~/.signalk
109
+ npm install signalk-mareas-ihm@latest --save
110
+ sudo systemctl restart signalk
111
+ ```
112
+
113
+ ---
114
+
115
+ ## English
116
+
117
+ Signal K plugin that turns your OpenPlotter / Raspberry Pi into a **complete anchor watch viewer**: official Spanish tides from **IHM (Instituto Hidrográfico de la Marina)**, shelter forecast, on-board wave measurement, smart alarms and home automation support.
118
+
119
+ ### Main Features
120
+
121
+ #### Tides
122
+ - **Automatic tide prediction** by GPS or manual selection among 70 IHM stations.
123
+ - **Interactive tide curves** with annual HAT/LAT, cursor and high/low tide labels.
124
+ - **Official IHM coefficients** with automatic download.
125
+ - **Offline cache** of at least 2 months of predictions.
126
+ - **Tendency and coefficient** visible in the viewer's top bar.
127
+
128
+ #### Shelter forecast
129
+ - **16-sector rose** with automatic shelter detection from coastline (OpenStreetMap).
130
+ - **Grade A-F and protection percentage** combining forecast wind and waves.
131
+ - **12 h exposure strip** with hourly forecast.
132
+ - **"NOW / FORECAST" summary** with real-time sensor and forecast data.
133
+ - **Manual sector override** when the automatic detector is wrong.
134
+
135
+ #### On-board wave measurement
136
+ - **Direction, period and height** of waves computed on board from attitude and acceleration sensors (pypilot IMU or compatible).
137
+ - **24 h history** in 15-minute bars.
138
+ - **Shelter grade auto-adjusts** when measured wave exceeds forecast.
139
+
140
+ #### Anchor watch viewer
141
+ - **Leaflet map** with real-time GPS position and multiple layers (ESRI, Bing, Google, IHM S-52, SonarChart, offline MBTiles).
142
+ - **Anchor Watch** with visual and audible drag alarm.
143
+ - **Chain deployed** with bidirectional slider and automatic calculation (traditional and Vicente methods).
144
+ - **Swing and alarm radius** with chart labels and tide-aware prediction.
145
+ - **Multi-device real-time sync** (SSE).
146
+ - **Local nautical charts** via integrated MBTiles server.
147
+
148
+ #### Real-time sensors
149
+ - Direct reading of **wind (anemometer)**, **air/water temperature** and **atmospheric pressure** from Signal K.
150
+ - **"Veleta"** and **"Sensor"** badges distinguish real data from forecast.
151
+ - **Automatic Open-Meteo fallback** when a sensor drops.
152
+ - **Smart depth sounder**: detects frozen / spike / absurd values and stops showing made-up readings.
153
+
154
+ #### Smart AIS
155
+ - **Collision detection** with AIS targets in swing zone.
156
+ - **Per-target ACK** — silence alarm for one boat without disabling the general alarm.
157
+ - **External dragging detection** — if an ACKed target approaches >2 m/min, alarm reactivates.
158
+ - **Anchor estimation** for other boats via track analysis (centroid + max radius).
159
+
160
+ #### Alarms and audio
161
+ - **Independent alarms**: drag, grounding, AIS and depth with individual control.
162
+ - **Pre-recorded voices (OGG)** per language — more natural than synthesizer.
163
+ - **Automatic detection** of Raspberry Pi audio output (USB → analog → HDMI).
164
+ - **Reliable mobile support** (audio works even when the tab is in background).
165
+ - **Distinct patterns** per event (rapid beep for AIS, siren for drag, etc.).
166
+
167
+ #### Home Automation / KIP
168
+ - **KIP buttons** for drop/lift and alarm enable/disable.
169
+ - **REST endpoints** for Alexa, Google Home, Node-RED, MQTT.
170
+ - **Toggle endpoint** for single-button remote controls.
171
+
172
+ #### Full Bilingual
173
+ - Interface in **Spanish and English** across all views.
174
+ - Correct Spanish cardinal compass (N/NE/E/SE/S/SO/O/NO).
175
+ - Language flags in the viewer's bottom bar.
176
+
177
+ ### URLs
178
+
179
+ | URL | Function |
180
+ |-----|----------|
181
+ | `/signalk-mareas-ihm/` | Landing — Tides / Anchor Watch selector |
182
+ | `/signalk-mareas-ihm/mareas` | Tides view (direct) |
183
+ | `/signalk-mareas-ihm/visorfondeo` | Anchor Watch Viewer (direct) |
184
+
185
+ ### Home Automation — REST Endpoints
186
+
187
+ ```bash
188
+ # Drop/Lift with single button
189
+ curl -X POST http://openplotter.local:3000/signalk-mareas-ihm/api/anchor-watch/toggle
190
+
191
+ # Drop anchor at current GPS
192
+ curl -X POST http://openplotter.local:3000/signalk-mareas-ihm/api/anchor-watch/drop
193
+
194
+ # Lift anchor
195
+ curl -X POST http://openplotter.local:3000/signalk-mareas-ihm/api/anchor-watch/lift
196
+
197
+ # Simple status (for monitoring)
198
+ curl http://openplotter.local:3000/signalk-mareas-ihm/api/anchor-watch/simple
199
+ ```
200
+
201
+ ### Installation
202
+
203
+ From the **Signal K Appstore**: search `signalk-mareas-ihm` and install.
204
+
205
+ Or via command line:
206
+
207
+ ```bash
208
+ cd ~/.signalk
209
+ npm install signalk-mareas-ihm@latest --save
210
+ sudo systemctl restart signalk
211
+ ```
212
+
213
+ ---
214
+
215
+ © IHM — Official data from Spain's Instituto Hidrográfico de la Marina
@@ -1,7 +1,7 @@
1
1
  {
2
- "version": "2.0.2",
3
- "timestamp": "20260523-1548",
2
+ "version": "2.1.0",
3
+ "timestamp": "20260602-0142",
4
4
  "gitHash": null,
5
5
  "gitDirty": true,
6
- "builtAt": "2026-05-23T13:48:04.276Z"
6
+ "builtAt": "2026-06-01T23:42:06.561Z"
7
7
  }
@@ -0,0 +1,300 @@
1
+ /**
2
+ * Rev147 — ImuManager.
3
+ *
4
+ * Orquesta las fuentes IMU según un orden profesional:
5
+ * A. Signal K existente (paths attitude/heading ya publicados por otro plugin).
6
+ * B. Pypilot local (localhost:23322).
7
+ * C. Pypilot remoto (host configurado, o lista candidata).
8
+ * D. MacArthur HAT — asumido por pypilot, no acceso I2C directo.
9
+ * E. Raw I2C (último recurso, sólo si `imu.rawI2c.enabled === true`).
10
+ *
11
+ * Reglas:
12
+ * - `preferredSource` manual fijado por usuario gana mientras esté sano.
13
+ * - Auto-detect respeta `imu.autoDetect`. Si false, sólo arranca la fuente
14
+ * fijada (o todas las habilitadas por config y deja al manager elegir).
15
+ * - Si la fuente activa pasa a stale durante `staleAfterSeconds`, busca
16
+ * fallback dentro de las disponibles y loguea la transición.
17
+ * - El manager NO publica datos a SK — los consumers (wave detection, etc.)
18
+ * reciben muestras vía callback `onSample`.
19
+ * - El manager NO compite con plugins que ya emiten attitude — la
20
+ * fuente SignalKAttitudeSource simplemente LEE de SK, no escribe.
21
+ *
22
+ * Robustez:
23
+ * - Timeouts cortos en probes (3 s).
24
+ * - Errores de conexión NO lanzan excepciones — se reflejan como `status`.
25
+ * - El manager corre un tick periódico (100 ms) que poll-ea la fuente activa
26
+ * y, si está stale, intenta failover.
27
+ */
28
+ import { SignalKAttitudeSource } from "./sources/signalk.js";
29
+ import { PypilotKeyValueSource } from "./sources/pypilot.js";
30
+ import { RawI2cImuSource } from "./sources/rawi2c.js";
31
+ import { IMU_LOG_MESSAGES, DEFAULT_IMU_CONFIG, } from "./types.js";
32
+ export class ImuManager {
33
+ app;
34
+ config;
35
+ tickMs;
36
+ onSample;
37
+ onSourceChange;
38
+ /** Todas las fuentes registradas (algunas pueden estar disabled). */
39
+ sources = [];
40
+ /** Fuente activa actualmente entregando muestras al consumer. */
41
+ active = null;
42
+ tickTimer = null;
43
+ bootstrapDone = false;
44
+ lastFailoverLogMs = 0;
45
+ constructor(opts) {
46
+ this.app = opts.app;
47
+ this.config = { ...DEFAULT_IMU_CONFIG, ...opts.config };
48
+ this.tickMs = opts.tickMs ?? 100;
49
+ this.onSample = opts.onSample;
50
+ this.onSourceChange = opts.onSourceChange;
51
+ }
52
+ /**
53
+ * Inicializa: construye las fuentes según config, ejecuta auto-detect si
54
+ * está habilitado, y arranca el tick.
55
+ */
56
+ async start() {
57
+ if (this.bootstrapDone)
58
+ return;
59
+ // A. Signal K (siempre disponible — es sólo lectura)
60
+ const skSource = new SignalKAttitudeSource(this.app, {
61
+ staleAfterMs: this.config.staleAfterSeconds * 1000,
62
+ });
63
+ this.sources.push(skSource);
64
+ // B. Pypilot local
65
+ const localPypilot = new PypilotKeyValueSource({
66
+ app: this.app,
67
+ host: "localhost",
68
+ port: this.config.pypilot.port,
69
+ isLocal: true,
70
+ staleAfterMs: this.config.staleAfterSeconds * 1000,
71
+ });
72
+ this.sources.push(localPypilot);
73
+ // C. Pypilot remoto (host configurado + lista candidata)
74
+ const remoteHosts = new Set();
75
+ if (this.config.pypilot.host && this.config.pypilot.host !== "localhost" && this.config.pypilot.host !== "127.0.0.1") {
76
+ remoteHosts.add(this.config.pypilot.host);
77
+ }
78
+ for (const h of this.config.pypilot.remoteHosts || []) {
79
+ if (h && h !== "localhost")
80
+ remoteHosts.add(h);
81
+ }
82
+ for (const host of remoteHosts) {
83
+ this.sources.push(new PypilotKeyValueSource({
84
+ app: this.app,
85
+ host,
86
+ port: this.config.pypilot.port,
87
+ isLocal: false,
88
+ staleAfterMs: this.config.staleAfterSeconds * 1000,
89
+ }));
90
+ }
91
+ // D. MacArthur HAT — sin acción directa, asumido vía pypilot.
92
+ // Solo log para que el usuario vea que lo conocemos.
93
+ if (this._looksLikeMacArthur()) {
94
+ this.app.debug?.(IMU_LOG_MESSAGES.macarthurAssumed);
95
+ }
96
+ // E. Raw I2C — siempre instanciado, pero arranca disabled salvo opt-in
97
+ const rawI2c = new RawI2cImuSource({
98
+ app: this.app,
99
+ bus: this.config.rawI2c.bus,
100
+ enabled: this.config.rawI2c.enabled,
101
+ allowedModels: this.config.rawI2c.allowedModels,
102
+ });
103
+ this.sources.push(rawI2c);
104
+ if (!this.config.rawI2c.enabled) {
105
+ this.app.debug?.(IMU_LOG_MESSAGES.rawI2cDisabled);
106
+ }
107
+ // Iniciar la fuente SK siempre (es barata y solo lee).
108
+ skSource.start();
109
+ // Si autoDetect, probar fuentes pypilot en orden de prioridad.
110
+ if (this.config.autoDetect) {
111
+ await this._autoDetect();
112
+ }
113
+ else {
114
+ // Sin auto-detect: arrancar solo la fuente que coincida con preferredSource.
115
+ this._startPreferredOnly();
116
+ }
117
+ // Tick periódico
118
+ this.tickTimer = setInterval(() => this._tick(), this.tickMs);
119
+ this.bootstrapDone = true;
120
+ }
121
+ async stop() {
122
+ if (this.tickTimer) {
123
+ clearInterval(this.tickTimer);
124
+ this.tickTimer = null;
125
+ }
126
+ for (const s of this.sources) {
127
+ try {
128
+ await s.stop();
129
+ }
130
+ catch { /* defensive */ }
131
+ }
132
+ this.active = null;
133
+ this.bootstrapDone = false;
134
+ }
135
+ /** Estado para el endpoint /api/imu/status. */
136
+ status() {
137
+ return {
138
+ active: this.active
139
+ ? { id: this.active.id, type: this.active.type, ageMs: this.active.quality.ageMs }
140
+ : null,
141
+ available: this.sources.map((s) => ({
142
+ id: s.id,
143
+ type: s.type,
144
+ status: s.status,
145
+ priority: s.priority,
146
+ ageMs: s.quality.ageMs,
147
+ sampleRateHz: s.quality.sampleRateHz,
148
+ provides: s.provides,
149
+ host: s.host,
150
+ port: s.port,
151
+ lastError: s.lastError,
152
+ })),
153
+ config: this.config,
154
+ };
155
+ }
156
+ /**
157
+ * Lanza auto-detect manual desde la UI (botón "Buscar IMU"). Re-prueba
158
+ * pypilot local + remoto y re-selecciona la mejor fuente.
159
+ */
160
+ async triggerAutoDetect() {
161
+ await this._autoDetect();
162
+ }
163
+ // ─────────────────────────── internals ───────────────────────────
164
+ async _autoDetect() {
165
+ // A. SK: ya arrancada. Esperamos un tick por si llega data inmediata.
166
+ await this._sleep(200);
167
+ const skSource = this.sources.find((s) => s.type === "signalk");
168
+ if (skSource) {
169
+ skSource.poll(); // refresca estado
170
+ if (skSource.quality.coherent) {
171
+ this.app.debug?.(IMU_LOG_MESSAGES.usingSignalK);
172
+ this._setActive(skSource);
173
+ return;
174
+ }
175
+ }
176
+ // B+C. Probar pypilot local + remotos en paralelo (3 s timeout cada uno)
177
+ const pypilots = this.sources.filter((s) => s instanceof PypilotKeyValueSource);
178
+ const probes = await Promise.all(pypilots.map(async (p) => ({ src: p, ok: await p.probe() })));
179
+ // Local gana sobre remoto. Dentro de cada grupo, primer host que responde.
180
+ const localOk = probes.find((p) => p.src.type === "pypilot-local" && p.ok);
181
+ if (localOk) {
182
+ this.app.debug?.(IMU_LOG_MESSAGES.usingPypilotLocal);
183
+ localOk.src.start();
184
+ this._setActive(localOk.src);
185
+ return;
186
+ }
187
+ const remoteOk = probes.find((p) => p.src.type === "pypilot-network" && p.ok);
188
+ if (remoteOk) {
189
+ this.app.debug?.(`${IMU_LOG_MESSAGES.usingPypilotRemote} @ ${remoteOk.src.host}:${remoteOk.src.port}`);
190
+ remoteOk.src.start();
191
+ this._setActive(remoteOk.src);
192
+ return;
193
+ }
194
+ // E. Raw I2C como último recurso (sólo si enabled — y aun así es stub).
195
+ const rawi2c = this.sources.find((s) => s.type === "raw-i2c");
196
+ if (rawi2c && this.config.rawI2c.enabled) {
197
+ rawi2c.start();
198
+ // No lo marcamos como activo — el stub no entrega samples.
199
+ }
200
+ // Nada disponible
201
+ if (!this.active) {
202
+ this.app.debug?.(IMU_LOG_MESSAGES.noSource);
203
+ }
204
+ }
205
+ _startPreferredOnly() {
206
+ const pref = this.config.preferredSource;
207
+ if (pref === "auto" || pref === "none")
208
+ return;
209
+ const src = this.sources.find((s) => s.type === pref);
210
+ if (!src)
211
+ return;
212
+ src.start();
213
+ this._setActive(src);
214
+ }
215
+ /** Tick: poll de la fuente activa; si stale, intenta failover. */
216
+ _tick() {
217
+ if (this.active) {
218
+ const sample = this.active.poll();
219
+ if (sample) {
220
+ try {
221
+ this.onSample?.(sample, this.active);
222
+ }
223
+ catch { /* never propagate */ }
224
+ }
225
+ else {
226
+ // Activa cayó a stale/error: failover
227
+ const now = Date.now();
228
+ if (now - this.lastFailoverLogMs > 5_000) {
229
+ this.app.debug?.(IMU_LOG_MESSAGES.sourceStale);
230
+ this.lastFailoverLogMs = now;
231
+ }
232
+ const fallback = this._chooseBestSource(this.active);
233
+ if (fallback && fallback !== this.active) {
234
+ this._setActive(fallback);
235
+ }
236
+ }
237
+ }
238
+ else {
239
+ // Sin fuente activa — intentamos elegir una entre las disponibles
240
+ const cand = this._chooseBestSource(null);
241
+ if (cand)
242
+ this._setActive(cand);
243
+ }
244
+ }
245
+ /** Elige la mejor fuente disponible distinta de `exclude`. */
246
+ _chooseBestSource(exclude) {
247
+ const candidates = this.sources
248
+ .filter((s) => s !== exclude)
249
+ .filter((s) => s.status === "available" || s.status === "active")
250
+ .filter((s) => {
251
+ if (this.config.minQuality === "ok")
252
+ return s.quality.coherent;
253
+ return true;
254
+ })
255
+ .sort((a, b) => b.priority - a.priority);
256
+ return candidates[0] ?? null;
257
+ }
258
+ _setActive(src) {
259
+ const prev = this.active;
260
+ if (prev === src)
261
+ return;
262
+ if (prev)
263
+ prev.status = prev.status === "active" ? "available" : prev.status;
264
+ src.status = "active";
265
+ this.active = src;
266
+ try {
267
+ this.onSourceChange?.(src, prev);
268
+ }
269
+ catch { /* ignore */ }
270
+ }
271
+ /**
272
+ * Heurística simple: si /sys/firmware/devicetree/base/model contiene la
273
+ * cadena "MacArthur" (o "OpenPlotter"), consideramos MacArthur HAT
274
+ * probable. Tomar como hint, no como hecho.
275
+ */
276
+ _looksLikeMacArthur() {
277
+ try {
278
+ // Sólo en Linux/Pi; cualquier error → asume no.
279
+ // eslint-disable-next-line @typescript-eslint/no-var-requires
280
+ const fs = require("fs");
281
+ const candidates = [
282
+ "/sys/firmware/devicetree/base/model",
283
+ "/proc/device-tree/model",
284
+ ];
285
+ for (const p of candidates) {
286
+ try {
287
+ const txt = fs.readFileSync(p, "utf-8");
288
+ if (/macarthur|openplotter/i.test(txt))
289
+ return true;
290
+ }
291
+ catch { /* path missing */ }
292
+ }
293
+ }
294
+ catch { /* fs missing */ }
295
+ return false;
296
+ }
297
+ _sleep(ms) {
298
+ return new Promise((resolve) => setTimeout(resolve, ms));
299
+ }
300
+ }