signalk-mareas-ihm 2.0.2 → 2.1.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.
- package/README.md +2 -2
- package/dist/README.md +215 -0
- package/dist/build-info.json +3 -3
- package/dist/imu/manager.js +300 -0
- package/dist/imu/sources/pypilot.js +286 -0
- package/dist/imu/sources/rawi2c.js +115 -0
- package/dist/imu/sources/signalk.js +230 -0
- package/dist/imu/types.js +44 -0
- package/dist/index.js +1485 -143
- package/dist/mapafondeo.html +465 -77
- package/dist/mobile.html +9840 -0
- package/dist/sounds/ais_en.ogg +0 -0
- package/dist/sounds/ais_es.ogg +0 -0
- package/dist/sounds/anchor_down_en.ogg +0 -0
- package/dist/sounds/anchor_down_es.ogg +0 -0
- package/dist/sounds/garreo_en.ogg +0 -0
- package/dist/sounds/garreo_es.ogg +0 -0
- package/dist/sounds/grounding_en.ogg +0 -0
- package/dist/sounds/grounding_es.ogg +0 -0
- package/dist/sources/coastline.js +3 -12
- package/dist/sources/ihm.js +70 -23
- package/dist/sources/openmeteo-tides.js +186 -0
- package/dist/sources/synthetic.js +119 -0
- package/dist/types.js +7 -1
- package/package.json +1 -4
- package/public/assets/freeboard.png +0 -0
- package/public/assets/index-D_EIVUWR.css +1 -0
- package/public/assets/index-Db9bTqdz.js +9 -0
- package/public/index.html +32 -3
- package/public/mapafondeo.html +465 -77
- package/public/mobile.html +9840 -0
- package/public/assets/index-Bx9W9sDs.css +0 -1
- package/public/assets/index-DBqAtYPk.js +0 -9
- package/public/assets/index-kT_SYLTG.js +0 -9
package/README.md
CHANGED
|
@@ -30,7 +30,7 @@ Plugin de Signal K que convierte tu OpenPlotter / Raspberry Pi en un **visor com
|
|
|
30
30
|
- **El grado de abrigo se ajusta automáticamente** cuando la ola medida supera la prevista.
|
|
31
31
|
|
|
32
32
|
#### Visor de fondeo
|
|
33
|
-
- **Mapa Leaflet** con posición GPS en tiempo real y capas múltiples (ESRI, Bing, Google, IHM S-52,
|
|
33
|
+
- **Mapa Leaflet** con posición GPS en tiempo real y capas múltiples (ESRI, Bing, Google, IHM S-52, Batimetría, MBTiles offline).
|
|
34
34
|
- **Anchor Watch** con alarma de garreo visual y sonora.
|
|
35
35
|
- **Cadena largada** con slider bidireccional y cálculo automático (métodos tradicional y Vicente).
|
|
36
36
|
- **Radio de borneo y alarma** con etiquetas en la carta y predicción según marea.
|
|
@@ -138,7 +138,7 @@ Signal K plugin that turns your OpenPlotter / Raspberry Pi into a **complete anc
|
|
|
138
138
|
- **Shelter grade auto-adjusts** when measured wave exceeds forecast.
|
|
139
139
|
|
|
140
140
|
#### Anchor watch viewer
|
|
141
|
-
- **Leaflet map** with real-time GPS position and multiple layers (ESRI, Bing, Google, IHM S-52,
|
|
141
|
+
- **Leaflet map** with real-time GPS position and multiple layers (ESRI, Bing, Google, IHM S-52, Bathymetry, offline MBTiles).
|
|
142
142
|
- **Anchor Watch** with visual and audible drag alarm.
|
|
143
143
|
- **Chain deployed** with bidirectional slider and automatic calculation (traditional and Vicente methods).
|
|
144
144
|
- **Swing and alarm radius** with chart labels and tide-aware prediction.
|
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, Batimetría, 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, Bathymetry, 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
|
package/dist/build-info.json
CHANGED
|
@@ -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
|
+
}
|