signalk-czone 0.3.0-beta.14 → 0.3.0-beta.16

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/CHANGELOG.md CHANGED
@@ -1,5 +1,18 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.3.0-beta.16
4
+
5
+ - Adds an opt-in Victron GX switch pane for CZone circuits on the GX, Remote Console and VRM (#33).
6
+ - Preserves write-control and confirmation rules, synchronises observed state, and keeps GX names and grouping settings.
7
+ - Resets on circuits when changing to momentary mode; refuses blocked changes and restores the previous type if CZone does not confirm OFF.
8
+
9
+ ## 0.3.0-beta.15
10
+
11
+ - Preserves plugin settings when uploading a ZCF or selecting a network configuration (#32).
12
+ - Publishes switch-input states for Signal Interface, COI and Control X PLUS modules (#30).
13
+ - Adds the Navico chartplotter touch layout, drag scrolling and reduced redraw cost (#31).
14
+ - Updates the canonical ZCF parser to 0.1.0-beta.2 for newer Configuration Tool layouts and corrected tank settings.
15
+
3
16
  ## 0.3.0-beta.14
4
17
 
5
18
  - Adds bidirectional CZone links to Shelly Gen2+ switches and Victron GX outputs, with startup, confirmation and relay-function safeguards.
package/README.md CHANGED
@@ -57,6 +57,28 @@ The existing electrical-monitoring definition endpoint remains:
57
57
  GET /plugins/signalk-czone/monitoring
58
58
  ```
59
59
 
60
+ ## Switch inputs
61
+
62
+ Each switch input in the ZCF (a switch, float switch or signal wired to a CZone module) is published as `electrical.czone.inputs.<input name>.state`, `true` when the input is on. The name is cleaned the same way as circuit names; a repeated name gets `2`, `3` and so on. A value is published when the plugin first hears the module and then only on change.
63
+
64
+ Where an input's state is read depends on the kind of module (its type in the ZCF):
65
+
66
+ | Module | Message read | Input `i` is at |
67
+ | --- | --- | --- |
68
+ | Signal Interface | 65284 status and 65308 input message | bit `2i+1` |
69
+ | COI | 65308 input message only (its 65284 is its outputs) | bit `2(i-16)+1` |
70
+ | Control X PLUS | 65284 status only | bit `i`, or `i+1` when wired to switch to negative |
71
+
72
+ Inputs on other kinds of module are listed but not given a state. Checked on a Signal Interface on a test bench, COIs on SugarShack and a Control X PLUS on Compass Rose (Anchor Up, Ignition, High Bilge Water Alarm). Only one Control X PLUS input wired to negative has been seen.
73
+
74
+ ```text
75
+ GET /plugins/signalk-czone/inputs
76
+ ```
77
+
78
+ lists each input with its module, input number, wiring, module type, state and when it last changed.
79
+
80
+ The input table needs `signalk-czone-zcf` with `signalInputs` (the newer Configuration Tool layout included).
81
+
60
82
  ## Design
61
83
 
62
84
  The plugin does **not** modify Signal K Server, canboatjs, n2k-signalk, or any global PGN definitions.
@@ -213,9 +235,21 @@ The switch follows the CZone circuit, and the circuit follows the switch when it
213
235
 
214
236
  The switch is read every 2 s; a CZone change is acted on as its status message arrives. `GET /plugins/signalk-czone/bridge/status` shows each link; `/bridge/outputs` lists the GX's outputs; `/bridge/shelly?host=&channel=` asks a Shelly. Tested on a bench (Shelly PM1 both ways) and on a boat (GX relay 2 switched from a CZone display; the alarm relay refused).
215
237
 
238
+ ## Victron switch pane
239
+
240
+ On a Victron GX (Venus OS 3.60 or later), tick **Show CZone circuits in the Victron switch pane** in the configuration panel and the circuits appear as switches on the GX touch screen, Remote Console and VRM. The plugin registers `com.victronenergy.switch.czone` on the GX's D-Bus, following Victron's switchable-output API.
241
+
242
+ - **Which circuits:** those the webapp lists, one card per CZone category (the first sub-category, in a fixed priority, when there are several; DC or AC when there are none). A circuit linked to a third-party switch is left out, since that switch is already in the pane.
243
+ - **Both ways:** a tap in the pane switches the circuit with the same code as a Signal K PUT, so it needs **Enable CZone read/write control**, and a circuit set to confirm before turning off (on) is not turned off (on) from the pane unless other apps are allowed to; the pane's switch goes back. A change made anywhere else shows in the pane, from the plugin's own decoded 65284 state, checked every two seconds.
244
+ - **Label:** the circuit name, plus the current while it is on ("Freezer (2.9 A)"; untick **Show circuit current** to leave it off). A reading that only wobbles does not change the label, since every rename makes the GX re-sort its lists. Names, groups, types and the device's name can be edited on the GX and are kept (`victron-switches.json` in the plugin's data folder).
245
+ - **Device list:** Settings > Devices lists each switch as "CZone: name".
246
+ - `GET /plugins/signalk-czone/victron/status` reports whether it is running, which circuits the pane has as on, any that disagree with the plugin, and the last few requests from the pane.
247
+
248
+ Off a GX, or without `dbus-native` (an optional dependency), it reports that D-Bus is not available and the rest of the plugin is unaffected.
249
+
216
250
  ## Installation
217
251
 
218
- The plugin installs the pinned `signalk-czone-zcf@0.1.0-beta.1` npm dependency automatically. No separate parser installation or Git checkout is required.
252
+ The plugin installs the pinned `signalk-czone-zcf@0.1.0-beta.2` npm dependency automatically. No separate parser installation or Git checkout is required.
219
253
 
220
254
  ### Signal K App Store
221
255
 
@@ -239,11 +273,11 @@ npm install signalk-czone@latest --omit=dev --ignore-scripts
239
273
 
240
274
  Restart Signal K and follow the configuration steps above. For Docker or appliance installations, use the corresponding container/configuration directory and restart method.
241
275
 
242
- For installation from a maintainer-provided npm tarball, run `npm install /path/to/signalk-czone-0.3.0-beta.9.tgz --omit=dev --ignore-scripts` from the same configuration directory. Its pinned parser dependency must already be published to npm. Do not simply extract the plugin into `node_modules`: that does not install dependencies.
276
+ For installation from a maintainer-provided npm tarball, run `npm install /path/to/signalk-czone-0.3.0-beta.16.tgz --omit=dev --ignore-scripts` from the same configuration directory. Its pinned parser dependency must already be published to npm. Do not simply extract the plugin into `node_modules`: that does not install dependencies.
243
277
 
244
278
  ### Publishing (maintainers)
245
279
 
246
- Publish `signalk-czone-zcf@0.1.0-beta.1` first. Then run `npm install`, `npm test`, and `npm pack --dry-run` in this repository, verify the runtime files and App Store assets, and run `npm publish --tag latest` after npm authentication.
280
+ Publish `signalk-czone-zcf@0.1.0-beta.2` first. Then run `npm install`, `npm test`, and `npm pack --dry-run` in this repository, verify the runtime files and App Store assets, and run `npm publish --tag latest` after npm authentication.
247
281
 
248
282
  The package defaults to `latest`, including versions named `-beta.N`. The GitHub publishing workflow runs when `package.json` or the workflow changes on `main`: it tests and packs the release, publishes it if the version is new, and sets and verifies the `latest` tag. If the version already exists, the workflow promotes that existing version without republishing its contents.
249
283
 
@@ -442,6 +476,15 @@ Only the installed path is stored in plugin configuration; the binary ZCF is not
442
476
 
443
477
  The beta is intended for extended real-world testing before a stable `0.3.0` release.
444
478
 
479
+ ## On a Navico chartplotter (B&G, Simrad, Lowrance)
480
+
481
+ A Navico plotter shows the webapp in its own browser (Chromium 69), driven by a touch screen whose touches arrive as mouse events. The page recognises the plotter from the parameters the plotter adds to the address (`mfd_name`, `mfd_model_detail`, `brand`), or from the login token left by `signalk-navico-embedder`, and switches to a touch layout. `?layout=mfd` shows it in a desktop browser for testing. Nothing changes anywhere else.
482
+
483
+ - **Touch layout:** larger rows, buttons and navigation; no banner, overview or category chips; drawn icons in place of emoji and symbol characters the plotter's fonts lack; no shadows, gradients, blur or transitions, which are slow to draw there.
484
+ - **Scrolling:** a drag scrolls the list or the left column instead of selecting text, applied once per frame, and the tap that ends a drag is not taken as a press. Redraws caused by live data wait while a finger is down.
485
+ - **Updates:** the plotter keeps showing the page it first loaded. Once a minute the page checks whether its script has changed on the server and reloads once if so.
486
+ - **Diagnosis:** five taps on the connection line show the last input events, the frame time and the time to redraw the list.
487
+
445
488
  ## Fullscreen and tablet home-screen launch
446
489
 
447
490
  Open `/signalk-czone/` directly in your browser. The **fullscreen icon** beside
package/index.js CHANGED
@@ -3,6 +3,7 @@
3
3
  const path = require('path')
4
4
  const fs = require('fs')
5
5
  const zcf = require('./lib/zcf')
6
+ const inputs = require('./lib/inputs')
6
7
  const czone = require('./lib/czone')
7
8
  const nmea = require('./lib/nmea2000')
8
9
  const monitoring = require('./lib/monitoring')
@@ -12,6 +13,7 @@ const confirmOffLib = require('./lib/confirm-off')
12
13
  const { createBridge, bridgeEntries } = require('./lib/bridge')
13
14
  const { createShellyDriver } = require('./lib/bridge/shelly')
14
15
  const { createVictronDriver } = require('./lib/bridge/victron')
16
+ const { createVictronSwitches } = require('./lib/victron/switches')
15
17
 
16
18
  const MAX_UPLOAD_BYTES = 1024 * 1024
17
19
  const CZONE_CONFIG_BLOCK_HEADER = 23
@@ -150,6 +152,44 @@ module.exports = function (app) {
150
152
  },
151
153
  drivers: bridgeDrivers
152
154
  })
155
+ // Victron switch pane (lib/victron/switches): the CZone circuits as
156
+ // switches on the GX touch screen, Remote Console and VRM. Off unless
157
+ // ticked. A tap there switches the circuit through the same code as a
158
+ // Signal K PUT, so "Enable CZone read/write control" and the confirm-before
159
+ // rules apply; the state shown is the plugin's own decoded 65284 state.
160
+ const switchPane = createVictronSwitches(app, {
161
+ log: m => log(m),
162
+ version: require('./package.json').version,
163
+ getCurrent: skPath => {
164
+ const m = /^electrical\.czone\.([^.]+)\.current$/.exec(skPath || '')
165
+ const c = m && mapping && mapping.circuits.find(x => x.slug === m[1])
166
+ const st = c && runtimeState.get(c.name)
167
+ return st && Number.isFinite(st.current) ? st.current : null
168
+ },
169
+ controls: () => ({
170
+ state: (slug, on) => {
171
+ const c = circuitByName(slug)
172
+ if (on) refuseUnconfirmedOn(c)
173
+ else refuseUnconfirmedOff(c)
174
+ sendCircuitState(c, on)
175
+ const st = runtimeState.get(c.name)
176
+ if (st) st.lastRequested = on ? 'ON' : 'OFF'
177
+ },
178
+ brightness: (slug, normalized) => {
179
+ const c = circuitByName(slug)
180
+ const st = runtimeState.get(c.name)
181
+ if (normalized <= 0) refuseUnconfirmedOff(c)
182
+ else if (!st || st.state !== 'ON') refuseUnconfirmedOn(c)
183
+ sendCircuitBrightness(c, normalized)
184
+ if (st) st.lastRequestedPercent = Math.round(normalized * 100)
185
+ },
186
+ getState: slug => {
187
+ const c = mapping && mapping.circuits.find(x => x.slug === slug)
188
+ const st = c && runtimeState.get(c.name)
189
+ return st && st.statusObserved ? { state: st.state, percent: st.percent } : null
190
+ }
191
+ })
192
+ })
153
193
  let monitoringRuntime = null
154
194
  // Trend recording of circuit current (see lib/trends).
155
195
  let trends = null
@@ -171,6 +211,10 @@ module.exports = function (app) {
171
211
  // Circuits nominated to confirm before switching, by circuit name.
172
212
  let confirmOff = new Set()
173
213
  let confirmOn = new Set()
214
+ // Switch inputs of the loaded configuration (lib/inputs) and their last
215
+ // decoded state, by Signal K path.
216
+ let switchInputs = []
217
+ let inputState = new Map()
174
218
 
175
219
  const configRoot = app.config && app.config.configPath
176
220
  ? path.join(app.config.configPath, 'plugin-config-data')
@@ -213,12 +257,16 @@ module.exports = function (app) {
213
257
  function loadConfiguredZcf () {
214
258
  if (!fs.existsSync(zcfPath())) {
215
259
  mapping = null
260
+ switchInputs = []
261
+ inputState = new Map()
216
262
  return null
217
263
  }
218
264
  mapping = zcf.load(zcfPath())
219
265
  mapping.commandDeviceId = selectCommandDeviceId(mapping)
220
266
  publishedCircuitValues.clear()
221
267
  monitoringRuntime = monitoring.createRuntime(mapping.meters || [])
268
+ switchInputs = inputs.switchInputs(mapping)
269
+ inputState = new Map(switchInputs.map(i => [i.path, { state: null, since: null }]))
222
270
  runtimeState = new Map(mapping.circuits.map(c => [c.name, {
223
271
  state: null,
224
272
  percent: null,
@@ -892,6 +940,25 @@ module.exports = function (app) {
892
940
  }
893
941
  }
894
942
 
943
+ // Switch inputs: PGN 65284 (module status) and 65308 (input message), each
944
+ // input read only from the message its kind of module carries it in.
945
+ function decodeSwitchInputs (frame) {
946
+ if (!switchInputs.length) return
947
+ const decoded = inputs.decodeFrame(frame.data)
948
+ if (!decoded) return
949
+ for (const input of switchInputs) {
950
+ if (input.module !== decoded.module) continue
951
+ const on = inputs.inputState(input.moduleKind, decoded.bitmap, input.input, frame.pgn, input.wiring)
952
+ if (on === undefined) continue
953
+ const entry = inputState.get(input.path)
954
+ if (entry.state === on) continue
955
+ entry.state = on
956
+ entry.since = new Date().toISOString()
957
+ log(`CZone INPUT: ${input.name} ${on ? 'ON' : 'OFF'} (PGN ${frame.pgn} module=0x${decoded.module.toString(16).padStart(2, '0')} bitmap=0x${decoded.bitmap.toString(16).padStart(8, '0')})`)
958
+ publishDelta(input.path, on)
959
+ }
960
+ }
961
+
895
962
  function handleRawFrame (line) {
896
963
  const frame = nmea.parseRawLine(line)
897
964
  if (!frame) return
@@ -901,7 +968,12 @@ module.exports = function (app) {
901
968
  handleConfigFastPacket(frame)
902
969
  return
903
970
  }
971
+ if (frame.pgn === inputs.INPUT_PGN) {
972
+ decodeSwitchInputs(frame)
973
+ return
974
+ }
904
975
  if (frame.pgn === 65284) {
976
+ decodeSwitchInputs(frame)
905
977
  decodeCzoneCircuitStatus(frame)
906
978
  return
907
979
  }
@@ -1276,6 +1348,18 @@ module.exports = function (app) {
1276
1348
  default: false,
1277
1349
  description: 'Other Signal K apps switch with a PUT and cannot ask "are you sure?". Unticked, an off from them is refused with a message; the circuit can still be turned off from the webapp or a CZone keypad.'
1278
1350
  },
1351
+ victronSwitches: {
1352
+ type: 'boolean',
1353
+ title: 'Show CZone circuits in the Victron switch pane',
1354
+ default: false,
1355
+ description: 'Victron GX only (Venus OS 3.60 or later). The circuits appear as switches on the GX touch screen, Remote Console and VRM, one card per CZone category. Switching there needs CZone read/write control enabled, and circuits set to confirm before turning off (on) are not turned off (on) from there unless other apps are allowed to.'
1356
+ },
1357
+ victronSwitchCurrent: {
1358
+ type: 'boolean',
1359
+ title: 'Show circuit current in the switch pane',
1360
+ default: true,
1361
+ description: 'Adds the current to a switch\'s label while the circuit is on, for example "Freezer (2.9 A)".'
1362
+ },
1279
1363
  bridges: {
1280
1364
  type: 'array',
1281
1365
  title: 'Third-party switches',
@@ -1317,6 +1401,7 @@ module.exports = function (app) {
1317
1401
  })
1318
1402
  trends.start()
1319
1403
  startBridge()
1404
+ switchPane.start(settings, mapping).catch(err => log(`Victron switch pane: ${err.message}`))
1320
1405
 
1321
1406
  const ready = (reason = 'nmea2000OutAvailable event') => {
1322
1407
  nmeaReady = true
@@ -1393,6 +1478,7 @@ module.exports = function (app) {
1393
1478
  if (trends) trends.stop()
1394
1479
  trends = null
1395
1480
  bridge.stop()
1481
+ switchPane.stop()
1396
1482
  closeBridgeBus()
1397
1483
  reassembler = null
1398
1484
  configFastPacket = null
@@ -1401,6 +1487,8 @@ module.exports = function (app) {
1401
1487
  mapping = null
1402
1488
  runtimeState.clear()
1403
1489
  publishedCircuitValues.clear()
1490
+ switchInputs = []
1491
+ inputState = new Map()
1404
1492
  // Signal K removes PUT handlers when the plugin stops. The plugin
1405
1493
  // factory can be reused on a subsequent enable, so allow start() to
1406
1494
  // register the handlers again instead of treating the stale paths as
@@ -1409,6 +1497,22 @@ module.exports = function (app) {
1409
1497
  },
1410
1498
 
1411
1499
  registerWithRouter: router => {
1500
+ router.get('/inputs', (_req, res) => {
1501
+ res.json({
1502
+ inputs: switchInputs.map(i => ({
1503
+ name: i.name,
1504
+ path: i.path,
1505
+ module: i.module,
1506
+ input: i.input,
1507
+ wiring: i.wiring || null,
1508
+ moduleType: i.decoded ? inputs.LAYOUT[i.moduleKind].name : null,
1509
+ decoded: i.decoded,
1510
+ state: inputState.get(i.path).state,
1511
+ since: inputState.get(i.path).since
1512
+ }))
1513
+ })
1514
+ })
1515
+
1412
1516
  router.get('/status', (_req, res) => {
1413
1517
  res.json({
1414
1518
  sendingEnabled: settings.allowCzoneWrite === true,
@@ -1493,6 +1597,7 @@ module.exports = function (app) {
1493
1597
 
1494
1598
  // Third-party switches: link status, the GX's outputs, a Shelly test.
1495
1599
  router.get('/bridge/status', (_req, res) => res.json({ links: bridge.status() }))
1600
+ router.get('/victron/status', (_req, res) => res.json(switchPane.status()))
1496
1601
  router.get('/bridge/outputs', (_req, res) => {
1497
1602
  bridgeDrivers.victron.list()
1498
1603
  .then(outputs => res.json({ available: true, outputs }))
@@ -1547,11 +1652,9 @@ module.exports = function (app) {
1547
1652
  fs.copyFileSync(source, temp)
1548
1653
  const next = zcf.load(temp)
1549
1654
  try { fs.unlinkSync(temp) } catch (_) {}
1550
- const newSettings = {
1551
- allowCzoneWrite: settings.allowCzoneWrite === true,
1552
- configurationSource: 'networkCache',
1553
- networkConfigFile: filename
1554
- }
1655
+ // Only the configuration source changes; every other setting (confirm
1656
+ // before on/off, trends, third-party switches, …) is kept.
1657
+ const newSettings = { ...settings, configurationSource: 'networkCache', networkConfigFile: filename }
1555
1658
  const restartResult = await saveAndRestart(newSettings)
1556
1659
  res.json({ ok: true, file: filename, bytes: next.fileSize, vesselName: next.vesselName || null, configuration: newSettings, restartRequested: restartResult.restartRequested })
1557
1660
  } catch (err) { res.status(400).json({ ok: false, error: err.message }) }
@@ -1579,11 +1682,9 @@ module.exports = function (app) {
1579
1682
  savedAt: new Date().toISOString()
1580
1683
  }, null, 2))
1581
1684
  } catch (_) {}
1582
- const newSettings = {
1583
- allowCzoneWrite: settings.allowCzoneWrite === true,
1584
- configurationSource: 'installedZcf',
1585
- networkConfigFile: ''
1586
- }
1685
+ // Only the configuration source changes; every other setting (confirm
1686
+ // before on/off, trends, third-party switches, …) is kept.
1687
+ const newSettings = { ...settings, configurationSource: 'installedZcf', networkConfigFile: '' }
1587
1688
  const restartResult = await saveAndRestart(newSettings)
1588
1689
  res.json({
1589
1690
  ok: true,
@@ -1686,5 +1787,7 @@ module.exports = function (app) {
1686
1787
 
1687
1788
  // For tests: run one bridge poll now instead of waiting for its timer.
1688
1789
  Object.defineProperty(plugin, '_bridgePoll', { value: () => bridge.poll(), enumerable: false })
1790
+ // For tests: the Victron switch pane.
1791
+ Object.defineProperty(plugin, '_switchPane', { value: switchPane, enumerable: false })
1689
1792
  return plugin
1690
1793
  }
package/lib/inputs.js ADDED
@@ -0,0 +1,94 @@
1
+ 'use strict'
2
+
3
+ // CZone switch inputs: ON / OFF of each switch wired to a CZone module, from
4
+ // the module's own messages. The inputs and their wiring come from the ZCF's
5
+ // signal-input table (signalk-czone-zcf parseSignalInputs).
6
+ //
7
+ // PGN 65284 (0xFF04), module status, about every two seconds and on change:
8
+ // 27 99 <module dipswitch> <module kind> <bitmap, 32 bits, low byte first>
9
+ // PGN 65308 (0xFF1C), input message, sent on change:
10
+ // 27 99 <module dipswitch> <variant> <bitmap, 32 bits, low byte first>
11
+ //
12
+ // Where an input sits depends on the kind of module (its type in the ZCF):
13
+ //
14
+ // Signal Interface (0x0D). Test bench, 6 Oct 2026: module 2, five switches on
15
+ // inputs 0-4 closed one after another gave 02 00, 0A 00, 2A 00, AA 00, AA 02:
16
+ // input n (from 0) is bit 2n+1, in 65284 and in 65308 (variant 00). The
17
+ // message comes on change too, so a tap on a momentary switch is caught.
18
+ //
19
+ // COI (0x1F). SugarShack capture, 27 Sep 2026: a COI's 65284 carries its
20
+ // outputs only; its inputs are in 65308 (variant 03), numbered from 16 in
21
+ // the ZCF, input i at bit 2(i-16)+1. Module 1A input 19 -> 80 00, 1A input
22
+ // 20 -> 00 02, 1C input 17 -> 08 00.
23
+ //
24
+ // Control X PLUS (0x36). Compass Rose, 15 Oct 2026, each input operated twice:
25
+ // its 65284 has the outputs in bits 0-15 and the inputs above, input i (from
26
+ // 16 in the ZCF) at bit i when it switches to positive, bit i+1 when it
27
+ // switches to negative. Anchor Up (input 17, to +) bit 17; Ignition (24, to +)
28
+ // bit 24; High Bilge Water Alarm (18, to -) bit 19. Only one input to
29
+ // negative was seen. Its 65308 messages come in four variants (byte 3: 00,
30
+ // 01, 03, 06) and are not read.
31
+ //
32
+ // Other kinds: not decoded (no state rather than a guess).
33
+
34
+ const STATUS_PGN = 65284
35
+ const INPUT_PGN = 65308
36
+
37
+ const oddBits = first => input => 2 * (input - first) + 1
38
+ const LAYOUT = {
39
+ 0x0d: { name: 'Signal Interface', pgns: [STATUS_PGN, INPUT_PGN], first: 0, bit: oddBits(0) },
40
+ 0x1f: { name: 'COI', pgns: [INPUT_PGN], first: 16, bit: oddBits(16) },
41
+ 0x36: { name: 'Control X PLUS', pgns: [STATUS_PGN], first: 16, bit: (input, wiring) => input + (wiring === 'switchToNeg' ? 1 : 0) }
42
+ }
43
+
44
+ // data: 8 bytes of a 65284 or 65308 frame -> { module, variant, bitmap } or null
45
+ function decodeFrame (data) {
46
+ if (!data || data.length !== 8 || data[0] !== 0x27 || data[1] !== 0x99) return null
47
+ return { module: data[2], variant: data[3], bitmap: data.readUInt32LE(4) }
48
+ }
49
+
50
+ // true / false, or undefined when this kind of module or message does not
51
+ // carry the input.
52
+ function inputState (kind, bitmap, input, pgn, wiring) {
53
+ const layout = LAYOUT[kind]
54
+ if (!layout || !Number.isInteger(input) || !layout.pgns.includes(pgn)) return undefined
55
+ if (input < layout.first) return undefined
56
+ const bit = layout.bit(input, wiring)
57
+ if (bit > 31) return undefined
58
+ return ((bitmap >>> bit) & 1) === 1
59
+ }
60
+
61
+ function slugify (name) {
62
+ const cleaned = String(name || 'Input').trim().replace(/[^A-Za-z0-9]+/g, '_').replace(/^_+|_+$/g, '')
63
+ return cleaned || 'Input'
64
+ }
65
+
66
+ // The switch inputs of a loaded configuration, each with its Signal K path
67
+ // (electrical.czone.inputs.<name>.state) and its module's kind.
68
+ function switchInputs (mapping) {
69
+ const table = mapping && mapping.signalInputs
70
+ if (!table || !Array.isArray(table.inputs)) return []
71
+ const kinds = new Map((mapping.modules || []).filter(m => Number.isInteger(m.module)).map(m => [m.module, m.type]))
72
+ const used = new Map()
73
+ return table.inputs
74
+ .filter(i => i.type === 'switch' && Number.isInteger(i.module) && i.module !== 0)
75
+ .map(i => {
76
+ const base = slugify(i.name)
77
+ const count = (used.get(base) || 0) + 1
78
+ used.set(base, count)
79
+ const slug = count === 1 ? base : `${base}${count}`
80
+ const kind = kinds.get(i.module)
81
+ return {
82
+ name: String(i.name).trim(),
83
+ slug,
84
+ path: `electrical.czone.inputs.${slug}.state`,
85
+ module: i.module,
86
+ input: i.input,
87
+ wiring: i.wiring,
88
+ moduleKind: Number.isInteger(kind) ? kind : null,
89
+ decoded: Boolean(LAYOUT[kind])
90
+ }
91
+ })
92
+ }
93
+
94
+ module.exports = { STATUS_PGN, INPUT_PGN, LAYOUT, decodeFrame, inputState, switchInputs }