signalk-czone 0.3.0-beta.14 → 0.3.0-beta.15
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 +7 -0
- package/README.md +34 -3
- package/index.js +57 -10
- package/lib/inputs.js +94 -0
- package/package.json +3 -3
- package/public/index.html +227 -4
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,12 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.3.0-beta.15
|
|
4
|
+
|
|
5
|
+
- Preserves plugin settings when uploading a ZCF or selecting a network configuration (#32).
|
|
6
|
+
- Publishes switch-input states for Signal Interface, COI and Control X PLUS modules (#30).
|
|
7
|
+
- Adds the Navico chartplotter touch layout, drag scrolling and reduced redraw cost (#31).
|
|
8
|
+
- Updates the canonical ZCF parser to 0.1.0-beta.2 for newer Configuration Tool layouts and corrected tank settings.
|
|
9
|
+
|
|
3
10
|
## 0.3.0-beta.14
|
|
4
11
|
|
|
5
12
|
- 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.
|
|
@@ -215,7 +237,7 @@ The switch is read every 2 s; a CZone change is acted on as its status message a
|
|
|
215
237
|
|
|
216
238
|
## Installation
|
|
217
239
|
|
|
218
|
-
The plugin installs the pinned `signalk-czone-zcf@0.1.0-beta.
|
|
240
|
+
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
241
|
|
|
220
242
|
### Signal K App Store
|
|
221
243
|
|
|
@@ -239,11 +261,11 @@ npm install signalk-czone@latest --omit=dev --ignore-scripts
|
|
|
239
261
|
|
|
240
262
|
Restart Signal K and follow the configuration steps above. For Docker or appliance installations, use the corresponding container/configuration directory and restart method.
|
|
241
263
|
|
|
242
|
-
For installation from a maintainer-provided npm tarball, run `npm install /path/to/signalk-czone-0.3.0-beta.
|
|
264
|
+
For installation from a maintainer-provided npm tarball, run `npm install /path/to/signalk-czone-0.3.0-beta.15.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
265
|
|
|
244
266
|
### Publishing (maintainers)
|
|
245
267
|
|
|
246
|
-
Publish `signalk-czone-zcf@0.1.0-beta.
|
|
268
|
+
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
269
|
|
|
248
270
|
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
271
|
|
|
@@ -442,6 +464,15 @@ Only the installed path is stored in plugin configuration; the binary ZCF is not
|
|
|
442
464
|
|
|
443
465
|
The beta is intended for extended real-world testing before a stable `0.3.0` release.
|
|
444
466
|
|
|
467
|
+
## On a Navico chartplotter (B&G, Simrad, Lowrance)
|
|
468
|
+
|
|
469
|
+
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.
|
|
470
|
+
|
|
471
|
+
- **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.
|
|
472
|
+
- **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.
|
|
473
|
+
- **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.
|
|
474
|
+
- **Diagnosis:** five taps on the connection line show the last input events, the frame time and the time to redraw the list.
|
|
475
|
+
|
|
445
476
|
## Fullscreen and tablet home-screen launch
|
|
446
477
|
|
|
447
478
|
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')
|
|
@@ -171,6 +172,10 @@ module.exports = function (app) {
|
|
|
171
172
|
// Circuits nominated to confirm before switching, by circuit name.
|
|
172
173
|
let confirmOff = new Set()
|
|
173
174
|
let confirmOn = new Set()
|
|
175
|
+
// Switch inputs of the loaded configuration (lib/inputs) and their last
|
|
176
|
+
// decoded state, by Signal K path.
|
|
177
|
+
let switchInputs = []
|
|
178
|
+
let inputState = new Map()
|
|
174
179
|
|
|
175
180
|
const configRoot = app.config && app.config.configPath
|
|
176
181
|
? path.join(app.config.configPath, 'plugin-config-data')
|
|
@@ -213,12 +218,16 @@ module.exports = function (app) {
|
|
|
213
218
|
function loadConfiguredZcf () {
|
|
214
219
|
if (!fs.existsSync(zcfPath())) {
|
|
215
220
|
mapping = null
|
|
221
|
+
switchInputs = []
|
|
222
|
+
inputState = new Map()
|
|
216
223
|
return null
|
|
217
224
|
}
|
|
218
225
|
mapping = zcf.load(zcfPath())
|
|
219
226
|
mapping.commandDeviceId = selectCommandDeviceId(mapping)
|
|
220
227
|
publishedCircuitValues.clear()
|
|
221
228
|
monitoringRuntime = monitoring.createRuntime(mapping.meters || [])
|
|
229
|
+
switchInputs = inputs.switchInputs(mapping)
|
|
230
|
+
inputState = new Map(switchInputs.map(i => [i.path, { state: null, since: null }]))
|
|
222
231
|
runtimeState = new Map(mapping.circuits.map(c => [c.name, {
|
|
223
232
|
state: null,
|
|
224
233
|
percent: null,
|
|
@@ -892,6 +901,25 @@ module.exports = function (app) {
|
|
|
892
901
|
}
|
|
893
902
|
}
|
|
894
903
|
|
|
904
|
+
// Switch inputs: PGN 65284 (module status) and 65308 (input message), each
|
|
905
|
+
// input read only from the message its kind of module carries it in.
|
|
906
|
+
function decodeSwitchInputs (frame) {
|
|
907
|
+
if (!switchInputs.length) return
|
|
908
|
+
const decoded = inputs.decodeFrame(frame.data)
|
|
909
|
+
if (!decoded) return
|
|
910
|
+
for (const input of switchInputs) {
|
|
911
|
+
if (input.module !== decoded.module) continue
|
|
912
|
+
const on = inputs.inputState(input.moduleKind, decoded.bitmap, input.input, frame.pgn, input.wiring)
|
|
913
|
+
if (on === undefined) continue
|
|
914
|
+
const entry = inputState.get(input.path)
|
|
915
|
+
if (entry.state === on) continue
|
|
916
|
+
entry.state = on
|
|
917
|
+
entry.since = new Date().toISOString()
|
|
918
|
+
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')})`)
|
|
919
|
+
publishDelta(input.path, on)
|
|
920
|
+
}
|
|
921
|
+
}
|
|
922
|
+
|
|
895
923
|
function handleRawFrame (line) {
|
|
896
924
|
const frame = nmea.parseRawLine(line)
|
|
897
925
|
if (!frame) return
|
|
@@ -901,7 +929,12 @@ module.exports = function (app) {
|
|
|
901
929
|
handleConfigFastPacket(frame)
|
|
902
930
|
return
|
|
903
931
|
}
|
|
932
|
+
if (frame.pgn === inputs.INPUT_PGN) {
|
|
933
|
+
decodeSwitchInputs(frame)
|
|
934
|
+
return
|
|
935
|
+
}
|
|
904
936
|
if (frame.pgn === 65284) {
|
|
937
|
+
decodeSwitchInputs(frame)
|
|
905
938
|
decodeCzoneCircuitStatus(frame)
|
|
906
939
|
return
|
|
907
940
|
}
|
|
@@ -1401,6 +1434,8 @@ module.exports = function (app) {
|
|
|
1401
1434
|
mapping = null
|
|
1402
1435
|
runtimeState.clear()
|
|
1403
1436
|
publishedCircuitValues.clear()
|
|
1437
|
+
switchInputs = []
|
|
1438
|
+
inputState = new Map()
|
|
1404
1439
|
// Signal K removes PUT handlers when the plugin stops. The plugin
|
|
1405
1440
|
// factory can be reused on a subsequent enable, so allow start() to
|
|
1406
1441
|
// register the handlers again instead of treating the stale paths as
|
|
@@ -1409,6 +1444,22 @@ module.exports = function (app) {
|
|
|
1409
1444
|
},
|
|
1410
1445
|
|
|
1411
1446
|
registerWithRouter: router => {
|
|
1447
|
+
router.get('/inputs', (_req, res) => {
|
|
1448
|
+
res.json({
|
|
1449
|
+
inputs: switchInputs.map(i => ({
|
|
1450
|
+
name: i.name,
|
|
1451
|
+
path: i.path,
|
|
1452
|
+
module: i.module,
|
|
1453
|
+
input: i.input,
|
|
1454
|
+
wiring: i.wiring || null,
|
|
1455
|
+
moduleType: i.decoded ? inputs.LAYOUT[i.moduleKind].name : null,
|
|
1456
|
+
decoded: i.decoded,
|
|
1457
|
+
state: inputState.get(i.path).state,
|
|
1458
|
+
since: inputState.get(i.path).since
|
|
1459
|
+
}))
|
|
1460
|
+
})
|
|
1461
|
+
})
|
|
1462
|
+
|
|
1412
1463
|
router.get('/status', (_req, res) => {
|
|
1413
1464
|
res.json({
|
|
1414
1465
|
sendingEnabled: settings.allowCzoneWrite === true,
|
|
@@ -1547,11 +1598,9 @@ module.exports = function (app) {
|
|
|
1547
1598
|
fs.copyFileSync(source, temp)
|
|
1548
1599
|
const next = zcf.load(temp)
|
|
1549
1600
|
try { fs.unlinkSync(temp) } catch (_) {}
|
|
1550
|
-
|
|
1551
|
-
|
|
1552
|
-
|
|
1553
|
-
networkConfigFile: filename
|
|
1554
|
-
}
|
|
1601
|
+
// Only the configuration source changes; every other setting (confirm
|
|
1602
|
+
// before on/off, trends, third-party switches, …) is kept.
|
|
1603
|
+
const newSettings = { ...settings, configurationSource: 'networkCache', networkConfigFile: filename }
|
|
1555
1604
|
const restartResult = await saveAndRestart(newSettings)
|
|
1556
1605
|
res.json({ ok: true, file: filename, bytes: next.fileSize, vesselName: next.vesselName || null, configuration: newSettings, restartRequested: restartResult.restartRequested })
|
|
1557
1606
|
} catch (err) { res.status(400).json({ ok: false, error: err.message }) }
|
|
@@ -1579,11 +1628,9 @@ module.exports = function (app) {
|
|
|
1579
1628
|
savedAt: new Date().toISOString()
|
|
1580
1629
|
}, null, 2))
|
|
1581
1630
|
} catch (_) {}
|
|
1582
|
-
|
|
1583
|
-
|
|
1584
|
-
|
|
1585
|
-
networkConfigFile: ''
|
|
1586
|
-
}
|
|
1631
|
+
// Only the configuration source changes; every other setting (confirm
|
|
1632
|
+
// before on/off, trends, third-party switches, …) is kept.
|
|
1633
|
+
const newSettings = { ...settings, configurationSource: 'installedZcf', networkConfigFile: '' }
|
|
1587
1634
|
const restartResult = await saveAndRestart(newSettings)
|
|
1588
1635
|
res.json({
|
|
1589
1636
|
ok: true,
|
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 }
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "signalk-czone",
|
|
3
|
-
"version": "0.3.0-beta.
|
|
3
|
+
"version": "0.3.0-beta.15",
|
|
4
4
|
"description": "BETA: Signal K CZone monitoring and control using dynamically parsed ZCF configuration and NMEA 2000",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"keywords": [
|
|
@@ -42,10 +42,10 @@
|
|
|
42
42
|
"icon.png"
|
|
43
43
|
],
|
|
44
44
|
"scripts": {
|
|
45
|
-
"test": "node test/zcf.test.js && node test/zcf-fixtures.test.js && node test/circuit-state.test.js && node test/czone.test.js && node test/nmea2000.test.js && node test/control-x-plus.test.js && node test/signalk.test.js && node test/write.test.js && node test/circuit-status.test.js && node test/status-mapping-regression.test.js && node test/state.test.js && node test/config-network.test.js && node test/config-namespace.test.js && node test/monitoring.test.js && node test/confirm-off.test.js && node test/webapp-current.test.js && node test/bridge.test.js && node test/trends.test.js && node --require ./test/helpers/gx-low-space.cjs test/trends.test.js"
|
|
45
|
+
"test": "node test/zcf.test.js && node test/zcf-fixtures.test.js && node test/circuit-state.test.js && node test/czone.test.js && node test/nmea2000.test.js && node test/control-x-plus.test.js && node test/signalk.test.js && node test/write.test.js && node test/circuit-status.test.js && node test/status-mapping-regression.test.js && node test/state.test.js && node test/config-network.test.js && node test/config-namespace.test.js && node test/monitoring.test.js && node test/confirm-off.test.js && node test/webapp-current.test.js && node test/bridge.test.js && node test/trends.test.js && node test/inputs.test.js && node --require ./test/helpers/gx-low-space.cjs test/trends.test.js"
|
|
46
46
|
},
|
|
47
47
|
"dependencies": {
|
|
48
|
-
"signalk-czone-zcf": "
|
|
48
|
+
"signalk-czone-zcf": "0.1.0-beta.2"
|
|
49
49
|
},
|
|
50
50
|
"optionalDependencies": {
|
|
51
51
|
"dbus-native": "^0.4.0"
|
package/public/index.html
CHANGED
|
@@ -247,6 +247,72 @@
|
|
|
247
247
|
.no-flexgap .trend-stats > * { margin-right:28px; }
|
|
248
248
|
.no-flexgap .fullscreen-toggle, .no-flexgap button > .nav-count, .no-flexgap .section-head .search, .no-flexgap .trend-panel .section-head .trend-close { margin-left:auto; }
|
|
249
249
|
@media (max-width: 780px) { .no-flexgap .section-head .sort { margin-left:auto; } }
|
|
250
|
+
/* Chartplotter layout. A Navico plotter (B&G / Simrad / Lowrance)
|
|
251
|
+
shows the page in its own browser, Chromium 69, driven by touch that
|
|
252
|
+
arrives as mouse events. It has no flex gap, no colour-mix, few symbol
|
|
253
|
+
fonts and no hover; a drag selects text instead of scrolling. The page
|
|
254
|
+
gets the class "mfd" when the plotter's own parameters are on the address
|
|
255
|
+
(or ?layout=mfd, for trying it in a desktop browser). Nothing here
|
|
256
|
+
applies anywhere else. */
|
|
257
|
+
.mfd, .mfd * { -webkit-user-select:none; user-select:none; -webkit-tap-highlight-color:transparent; }
|
|
258
|
+
.mfd input[type=search] { -webkit-user-select:text; user-select:text; }
|
|
259
|
+
.mfd body { font-size:16px; }
|
|
260
|
+
.mfd svg.ico { width:22px; height:22px; display:block; fill:none; stroke:currentColor; stroke-width:2; stroke-linecap:round; stroke-linejoin:round; }
|
|
261
|
+
.mfd .app { grid-template-columns:230px minmax(0,1fr); }
|
|
262
|
+
.mfd .hero, .mfd .overview-card, .mfd .rightbar > .right-card:last-child, .mfd #sectionIcon, .mfd .search > span { display:none; }
|
|
263
|
+
.mfd .modes-card.none { display:none; }
|
|
264
|
+
.mfd .main { padding:10px 12px 30px; }
|
|
265
|
+
.mfd .connection { padding:0 4px 8px; }
|
|
266
|
+
.mfd .sidebar { padding:12px 10px; }
|
|
267
|
+
.mfd .rightbar { padding:0 10px 14px; }
|
|
268
|
+
.mfd .brand { padding:2px 8px 12px; }
|
|
269
|
+
.mfd .fullscreen-toggle { display:none !important; } /* the plotter's tile is already full screen */
|
|
270
|
+
.mfd .brand > * + *, .mfd .category-card > * + * { margin-left:11px; }
|
|
271
|
+
.mfd .nav button { min-height:50px; font-size:16px; padding:10px 12px; margin-bottom:4px; }
|
|
272
|
+
.mfd .nav-icon { width:24px; margin-right:12px; flex:none; }
|
|
273
|
+
.mfd .nav-count { font-size:14px; }
|
|
274
|
+
.mfd .category-nav .category-card { min-height:60px; margin-bottom:6px; }
|
|
275
|
+
.mfd .category-nav .category-card strong { font-size:14px; }
|
|
276
|
+
.mfd .category-nav .category-card span { font-size:12px; }
|
|
277
|
+
.mfd .cat-icon { background:rgba(120,170,220,.14); }
|
|
278
|
+
.mfd .section-head { padding:12px 14px; }
|
|
279
|
+
.mfd .section-head > * + * { margin-left:10px; }
|
|
280
|
+
.mfd .section-head .search { margin-left:auto; }
|
|
281
|
+
.mfd .leftwell { background:rgba(5,16,29,.78); border-right:1px solid var(--line); }
|
|
282
|
+
.mfd .sidebar, .mfd .rightbar { background:none; border:0; -webkit-backdrop-filter:none; backdrop-filter:none; }
|
|
283
|
+
.mfd .section-head h2 { font-size:20px; }
|
|
284
|
+
.mfd .section-head .subtle { font-size:13px; }
|
|
285
|
+
.mfd .search input { padding:12px 15px; font-size:15px; }
|
|
286
|
+
.mfd .section-head .sort { padding:11px 10px; font-size:14px; }
|
|
287
|
+
.mfd .circuit { grid-template-columns:44px minmax(0,1fr) auto 34px; min-height:74px; padding:10px 12px; }
|
|
288
|
+
.mfd .tags, .mfd .status { display:none; }
|
|
289
|
+
.mfd .circuit > div:nth-last-child(2) { justify-self:end; }
|
|
290
|
+
.mfd .fav { font-size:24px; padding:8px; }
|
|
291
|
+
.mfd .circuit-name strong { font-size:17px; }
|
|
292
|
+
.mfd .circuit-name small { font-size:12px; }
|
|
293
|
+
.mfd .toggle { min-width:104px; min-height:54px; font-size:17px; }
|
|
294
|
+
.mfd .ask-box { max-width:520px; padding:26px; box-shadow:none; }
|
|
295
|
+
.mfd .ask-box h3 { font-size:24px; }
|
|
296
|
+
.mfd .ask-box p { font-size:17px; }
|
|
297
|
+
.mfd .ask-buttons button { min-height:64px; font-size:19px; }
|
|
298
|
+
.mfd .ask-mark svg { width:16px; height:16px; }
|
|
299
|
+
.mfd .control > * + *, .mfd .range-wrap > * + * { margin-left:9px; }
|
|
300
|
+
.mfd .connection > * + * { margin-left:7px; }
|
|
301
|
+
.mfd .arrow { font-size:26px; padding:8px 4px; }
|
|
302
|
+
.mfd .mode-button { min-height:50px; font-size:15px; }
|
|
303
|
+
.mfd .empty { font-size:15px; }
|
|
304
|
+
/* Cheap to draw: the plotter redraws the page in software as it scrolls. */
|
|
305
|
+
.mfd, .mfd body { background:#06101d; }
|
|
306
|
+
.mfd * { box-shadow:none !important; text-shadow:none !important; transition:none !important; }
|
|
307
|
+
.mfd .section { background:#0a1a2b; }
|
|
308
|
+
.mfd .leftwell { background:#07121f; }
|
|
309
|
+
.mfd .toggle.on { background:#1aa56a; }
|
|
310
|
+
.mfd .nav button.active { background:#12456e; }
|
|
311
|
+
.mfd .category-card, .mfd .category-nav .category-card.active { background:#0f2740; }
|
|
312
|
+
.mfd .category-card:hover { transform:none; }
|
|
313
|
+
.mfd .circuit:hover, .mfd .nav button:hover, .mfd .category-nav button:hover { background:transparent; }
|
|
314
|
+
.mfd .nav button.active:hover { background:#12456e; }
|
|
315
|
+
.mfd-probe { position:fixed; left:8px; bottom:8px; z-index:9; margin:0; padding:8px 10px; max-width:70%; font:12px/1.35 monospace; color:#d6f5d6; background:rgba(0,0,0,.88); border:1px solid #2c6; border-radius:8px; white-space:pre; pointer-events:none; }
|
|
250
316
|
</style>
|
|
251
317
|
<script>
|
|
252
318
|
(() => {
|
|
@@ -306,6 +372,7 @@
|
|
|
306
372
|
<script src="fullscreen.js"></script>
|
|
307
373
|
<script>
|
|
308
374
|
(() => {
|
|
375
|
+
const OWN_SCRIPT = document.currentScript ? document.currentScript.textContent : null; // see "a newer page" below
|
|
309
376
|
const API = '/plugins/signalk-czone';
|
|
310
377
|
const SK = `${location.protocol === 'https:' ? 'wss:' : 'ws:'}//${location.host}/signalk/v1/stream?subscribe=none`;
|
|
311
378
|
const categoryIcons = {
|
|
@@ -336,6 +403,39 @@
|
|
|
336
403
|
'User Definable 5', 'Bilge Pumps'
|
|
337
404
|
];
|
|
338
405
|
const topLevelCategories = new Set(['AC', 'DC', 'Favorites']);
|
|
406
|
+
// On a Navico chartplotter (see the .mfd styles). The plotter adds
|
|
407
|
+
// mfd_name, mfd_model_detail, brand, lang and mode to the address.
|
|
408
|
+
// signalk-navico-embedder, the proxy that puts Signal K webapps on those
|
|
409
|
+
// plotters, also leaves window.SK_TOKEN on the page; either sign will do.
|
|
410
|
+
const MFD = /[?&](mfd_name|mfd_model_detail|brand|layout=mfd)\b/.test(location.search) || typeof window.SK_TOKEN === 'string';
|
|
411
|
+
if (MFD) document.documentElement.classList.add('mfd');
|
|
412
|
+
// Drawn icons for the plotter, whose fonts lack most of the symbol
|
|
413
|
+
// characters the desktop page uses (they show as empty boxes there).
|
|
414
|
+
const MFD_ICONS = {
|
|
415
|
+
home:'<path d="M3 11l9-8 9 8"/><path d="M5 10v10h14V10"/>', dot:'<circle cx="12" cy="12" r="5" fill="currentColor"/>',
|
|
416
|
+
star:'<path d="M12 3l2.8 6 6.2.6-4.7 4.2 1.5 6.2L12 16.6 6.2 20l1.5-6.2L3 9.6 9.2 9z"/>',
|
|
417
|
+
ac:'<path d="M2 12c2.5-9 6-9 8.5 0s6 9 8.5 0"/><path d="M21 12h1"/>', battery:'<rect x="3" y="7" width="16" height="10" rx="2"/><path d="M22 10v4"/>',
|
|
418
|
+
gauge:'<path d="M4 18a9 9 0 1 1 16 0"/><path d="M12 14l4-6"/>', bolt:'<path d="M13 2L5 14h6l-1 8 8-12h-6z"/>',
|
|
419
|
+
bulb:'<path d="M9 18h6M10 21h4"/><path d="M12 3a6 6 0 0 0-4 10.5V15h8v-1.5A6 6 0 0 0 12 3z"/>',
|
|
420
|
+
compass:'<circle cx="12" cy="12" r="9"/><path d="M15.5 8.5l-2 5-5 2 2-5z"/>', drop:'<path d="M12 3c4 5 6 8 6 11a6 6 0 0 1-12 0c0-3 2-6 6-11z"/>',
|
|
421
|
+
music:'<path d="M9 18V6l10-2v12"/><circle cx="7" cy="18" r="2"/><circle cx="17" cy="16" r="2"/>',
|
|
422
|
+
fan:'<circle cx="12" cy="12" r="2"/><path d="M12 10c-1-4 1-7 4-7 1 3-1 6-4 7zM13.7 13c4-1 7 1 7 4-3 1-6-1-7-4zM10.3 13c-1 4-4 6-7 5 0-3 3-5 7-5z"/>',
|
|
423
|
+
chip:'<rect x="7" y="7" width="10" height="10" rx="1"/><path d="M10 3v4M14 3v4M10 17v4M14 17v4M3 10h4M3 14h4M17 10h4M17 14h4"/>',
|
|
424
|
+
snow:'<path d="M12 3v18M4.2 7.5l15.6 9M19.8 7.5l-15.6 9"/>', phone:'<path d="M5 4h4l2 5-2.5 1.5a11 11 0 0 0 5 5L15 13l5 2v4a2 2 0 0 1-2 2A15 15 0 0 1 3 6a2 2 0 0 1 2-2z"/>',
|
|
425
|
+
warn:'<path d="M12 3l10 18H2z"/><path d="M12 10v5M12 18v.500"/>', clock:'<circle cx="12" cy="12" r="9"/><path d="M12 7v5l3 2"/>', plus:'<path d="M12 5v14M5 12h14"/>',
|
|
426
|
+
gear:'<circle cx="12" cy="12" r="4"/><path d="M12 2v4M12 18v4M2 12h4M18 12h4M4.9 4.9l2.8 2.8M16.3 16.3l2.8 2.8M4.9 19.1l2.8-2.8M16.3 7.7l2.8-2.8"/>',
|
|
427
|
+
anchor:'<circle cx="12" cy="5" r="2"/><path d="M12 7v14M5 13a7 7 0 0 0 14 0M9 10h6"/>', sun:'<circle cx="12" cy="12" r="4"/><path d="M12 2v3M12 19v3M2 12h3M19 12h3M5 5l2 2M17 17l2 2M5 19l2-2M17 7l2-2"/>',
|
|
428
|
+
swap:'<path d="M4 9h15l-3-3M20 15H5l3 3"/>', tank:'<rect x="5" y="6" width="14" height="14" rx="2"/><path d="M5 13h14M9 6V3h6v3"/>',
|
|
429
|
+
temp:'<path d="M12 3a2 2 0 0 0-2 2v9a4 4 0 1 0 4 0V5a2 2 0 0 0-2-2z"/>', cloud:'<path d="M7 18a4 4 0 0 1 0-8 5 5 0 0 1 9.6-1A4.5 4.5 0 0 1 17 18z"/>',
|
|
430
|
+
toggle:'<rect x="3" y="8" width="18" height="8" rx="4"/><circle cx="8" cy="12" r="2"/>', house:'<path d="M3 11l9-8 9 8"/><path d="M5 10v10h5v-6h4v6h5V10"/>',
|
|
431
|
+
appliance:'<rect x="5" y="3" width="14" height="18" rx="2"/><circle cx="12" cy="14" r="4"/><path d="M8 6h2"/>'
|
|
432
|
+
};
|
|
433
|
+
const MFD_ICON_FOR = { all:'home', inuse:'dot', favorites:'star', ac:'ac', dc:'battery', monitor:'gauge',
|
|
434
|
+
Lighting:'bulb', Navigation:'compass', Pumps:'drop', 'Bilge Pumps':'drop', Power:'bolt', Entertainment:'music', 'Fans/Ventilation':'fan', Electronics:'chip', Climate:'snow', Refrigeration:'snow',
|
|
435
|
+
Communications:'phone', 'Vessel Critical':'warn', 'Indicators and Alarms':'warn', '24-Hour Circuits':'clock', Accessories:'plus', 'Engine Management':'gear', 'Vessel Management':'gear',
|
|
436
|
+
'Propulsion Management':'fan', 'House/Habitat':'house', Appliances:'appliance', 'Deck Equipment':'anchor',
|
|
437
|
+
Batteries:'battery', Solar:'sun', Alternators:'gear', Converters:'swap', 'Wind Generators':'fan', 'AC Power':'ac', Tanks:'tank', Temperatures:'temp', Environment:'cloud', 'Circuit current':'bolt', Inputs:'toggle' };
|
|
438
|
+
const mfdIcon = key => `<svg class="ico" viewBox="0 0 24 24">${MFD_ICONS[MFD_ICON_FOR[key]] || MFD_ICONS.dot}</svg>`;
|
|
339
439
|
let circuits = [], status = {}, modes = [], activeMode = null, vesselName = '', view = 'all', category = null, query = '', sort = 'name', socket = null;
|
|
340
440
|
let socketReconnectTimer = null, watchdogTimer = null, lastSocketUpdateAt = 0, socketConnected = false, socketEverOpen = false;
|
|
341
441
|
const WS_WATCHDOG_MS = 120000;
|
|
@@ -369,7 +469,7 @@
|
|
|
369
469
|
const activeCount = () => circuits.filter(stateOf).length;
|
|
370
470
|
const masterCount = m => circuits.filter(c => (categoryData(c).master || []).includes(m)).length;
|
|
371
471
|
const subCategoryList = c => [...new Set([...(categoryData(c).sub || []), ...(categoryData(c).userDefined || [])])];
|
|
372
|
-
const icon = c => categoryIcons[c] || '•';
|
|
472
|
+
const icon = c => MFD ? mfdIcon(c) : (categoryIcons[c] || '•');
|
|
373
473
|
const color = c => categoryColors[c] || '#4bb5ff';
|
|
374
474
|
|
|
375
475
|
function saveFavorites() { localStorage.setItem(favoritesKey, JSON.stringify([...favorites])); }
|
|
@@ -382,7 +482,7 @@
|
|
|
382
482
|
['all','⌂','All Circuits',circuits.length], ['inuse','●','In Use',activeCount()], ['favorites','☆','Favorites',circuits.filter(c=>favorites.has(nameKey(c))).length],
|
|
383
483
|
['ac','♆','AC',masterCount('AC')], ['dc','▣','DC',masterCount('DC')]
|
|
384
484
|
];
|
|
385
|
-
document.querySelector('#mainNav').innerHTML = items.map(([v,i,t,n]) => `<button class="${view===v && !category?'active':''}" onclick="setView('${v}')"><span class="nav-icon">${i}</span>${t}<span class="nav-count">${n}</span></button>`).join('');
|
|
485
|
+
document.querySelector('#mainNav').innerHTML = items.map(([v,i,t,n]) => `<button class="${view===v && !category?'active':''}" onclick="setView('${v}')"><span class="nav-icon">${MFD ? mfdIcon(v) : i}</span>${t}<span class="nav-count">${n}</span></button>`).join('');
|
|
386
486
|
}
|
|
387
487
|
|
|
388
488
|
function renderCategories() {
|
|
@@ -412,7 +512,7 @@
|
|
|
412
512
|
const title = category || ({all:'All Circuits',inuse:'In Use',favorites:'Favorites',ac:'AC Circuits',dc:'DC Circuits',search:'Search Results'}[view] || 'Circuits');
|
|
413
513
|
document.querySelector('#sectionTitle').textContent = title;
|
|
414
514
|
document.querySelector('#sectionCount').textContent = `${list.length} circuit${list.length===1?'':'s'}`;
|
|
415
|
-
document.querySelector('#sectionIcon').textContent = category ? icon(category) : view==='favorites' ? '☆' : view==='inuse' ? '●' : '⚡';
|
|
515
|
+
document.querySelector('#sectionIcon').textContent = MFD ? '' : category ? icon(category) : view==='favorites' ? '☆' : view==='inuse' ? '●' : '⚡';
|
|
416
516
|
document.querySelector('#circuits').innerHTML = list.length ? list.map(circuitRow).join('') : `<div class="empty">No circuits match this view.</div>`;
|
|
417
517
|
}
|
|
418
518
|
|
|
@@ -982,6 +1082,7 @@
|
|
|
982
1082
|
function renderModes() {
|
|
983
1083
|
const el = document.querySelector('#modes');
|
|
984
1084
|
if (!el) return;
|
|
1085
|
+
if (el.parentElement) el.parentElement.classList.toggle('none', !modes.length); // hidden on a plotter when there are none
|
|
985
1086
|
if (!modes.length) { el.innerHTML = '<div class="empty" style="padding:10px 4px">No CZone modes found.</div>'; return; }
|
|
986
1087
|
el.innerHTML = modes.map(m => {
|
|
987
1088
|
const active = activeMode && activeMode.slug === m.slug;
|
|
@@ -1025,6 +1126,128 @@
|
|
|
1025
1126
|
document.querySelector('#connectionCard').innerHTML = `<div style="color:#a9bfd2;font-size:12px;line-height:1.7"><span style="color:${status.sendingEnabled?'#64e4aa':'#ffcc77'}">● NMEA 2000 sending ${status.sendingEnabled?'enabled':'disabled'}</span><br><span style="color:${readyColor}">● ${readyText}</span>${lastOut}</div>`;
|
|
1026
1127
|
}
|
|
1027
1128
|
|
|
1129
|
+
// Scrolling on a chartplotter. Its touch arrives as mouse events, so a
|
|
1130
|
+
// drag would select text and nothing would scroll. So a press that moves
|
|
1131
|
+
// more than a few pixels scrolls the nearest scrolling box (the list or the
|
|
1132
|
+
// left column). Positions are applied once per frame, and the tap that ends
|
|
1133
|
+
// a drag is not passed on as a click.
|
|
1134
|
+
// The plotter is slow, so redraws caused by live data wait while a finger is
|
|
1135
|
+
// down (see liveRender).
|
|
1136
|
+
let mfdBusyUntil = 0; // no data-driven redraw before this time
|
|
1137
|
+
if (MFD) {
|
|
1138
|
+
let drag = null, swallow = false, frame = 0;
|
|
1139
|
+
const scrollerOf = el => {
|
|
1140
|
+
for (let n = el; n && n !== document.body && n !== document.documentElement; n = n.parentElement) {
|
|
1141
|
+
const oy = getComputedStyle(n).overflowY;
|
|
1142
|
+
if ((oy === 'auto' || oy === 'scroll') && n.scrollHeight > n.clientHeight + 1) return n;
|
|
1143
|
+
}
|
|
1144
|
+
return document.scrollingElement || document.documentElement;
|
|
1145
|
+
};
|
|
1146
|
+
const applyDrag = () => { frame = 0; if (drag && drag.moved) drag.box.scrollTop = drag.want; };
|
|
1147
|
+
document.addEventListener('mousedown', e => {
|
|
1148
|
+
if (e.button !== 0 || (e.target.closest && e.target.closest('input, select, canvas, .ask'))) return;
|
|
1149
|
+
const box = scrollerOf(e.target);
|
|
1150
|
+
drag = { box, y: e.clientY, top: box.scrollTop, want: box.scrollTop, moved: false, held: false };
|
|
1151
|
+
mfdBusyUntil = Date.now() + 1500;
|
|
1152
|
+
}, true);
|
|
1153
|
+
document.addEventListener('mousemove', e => {
|
|
1154
|
+
if (!drag) return;
|
|
1155
|
+
// Released outside the page. Only trusted once this drag has reported a
|
|
1156
|
+
// held button, since some touch layers never report one.
|
|
1157
|
+
if (e.buttons) drag.held = true; else if (drag.held) { endDrag(); return; }
|
|
1158
|
+
const dy = e.clientY - drag.y;
|
|
1159
|
+
if (!drag.moved && Math.abs(dy) < 10) return;
|
|
1160
|
+
drag.moved = true;
|
|
1161
|
+
drag.want = drag.top - dy;
|
|
1162
|
+
mfdBusyUntil = Date.now() + 1500;
|
|
1163
|
+
if (!frame) frame = requestAnimationFrame(applyDrag);
|
|
1164
|
+
e.preventDefault();
|
|
1165
|
+
}, true);
|
|
1166
|
+
function endDrag() {
|
|
1167
|
+
if (drag && drag.moved) { applyDrag(); swallow = true; setTimeout(() => { swallow = false; }, 80); }
|
|
1168
|
+
drag = null;
|
|
1169
|
+
mfdBusyUntil = Date.now() + 400;
|
|
1170
|
+
}
|
|
1171
|
+
document.addEventListener('mouseup', endDrag, true);
|
|
1172
|
+
window.addEventListener('blur', endDrag);
|
|
1173
|
+
document.addEventListener('click', e => { if (swallow) { e.stopPropagation(); e.preventDefault(); swallow = false; } }, true);
|
|
1174
|
+
document.addEventListener('contextmenu', e => e.preventDefault());
|
|
1175
|
+
document.addEventListener('selectstart', e => { if (!(e.target.closest && e.target.closest('input'))) e.preventDefault(); });
|
|
1176
|
+
|
|
1177
|
+
// A newer page. The plotter keeps showing the page it first loaded, even
|
|
1178
|
+
// after the tile is closed and reopened, so an update never appeared
|
|
1179
|
+
// without restarting the plotter. Once a minute the page fetches itself
|
|
1180
|
+
// and, if its own script has changed on the server, reloads. One attempt
|
|
1181
|
+
// per version, so a plotter that will not let go cannot loop.
|
|
1182
|
+
const checkPage = async () => {
|
|
1183
|
+
try {
|
|
1184
|
+
if (!OWN_SCRIPT) return;
|
|
1185
|
+
const r = await fetch(location.pathname + '?fresh=' + Date.now(), { cache: 'no-store', credentials: 'include' });
|
|
1186
|
+
if (!r.ok) return;
|
|
1187
|
+
const text = await r.text();
|
|
1188
|
+
const found = text.split('<scr' + 'ipt>').map(part => part.split('</scr' + 'ipt>')[0]).find(part => part.indexOf("const API = '/plugins/signalk-czone'") >= 0);
|
|
1189
|
+
if (!found || found === OWN_SCRIPT) return;
|
|
1190
|
+
const tried = String(found.length) + ':' + found.slice(-200).length + ':' + found.split('\n').length;
|
|
1191
|
+
let before = null;
|
|
1192
|
+
try { before = sessionStorage.getItem('czone-reload'); sessionStorage.setItem('czone-reload', tried); } catch (_) { return; }
|
|
1193
|
+
if (before !== tried) location.reload(true);
|
|
1194
|
+
} catch (_) {}
|
|
1195
|
+
};
|
|
1196
|
+
setTimeout(checkPage, 15000);
|
|
1197
|
+
setInterval(checkPage, 60000);
|
|
1198
|
+
|
|
1199
|
+
// What the plotter actually sends: five taps on the connection line show
|
|
1200
|
+
// the last input events and how long frames and redraws take. For
|
|
1201
|
+
// diagnosing a plotter we cannot connect a debugger to.
|
|
1202
|
+
let taps = [], probe = null;
|
|
1203
|
+
document.querySelector('#connection').addEventListener('click', () => {
|
|
1204
|
+
const now = Date.now();
|
|
1205
|
+
taps = taps.filter(t => now - t < 3000).concat(now);
|
|
1206
|
+
if (taps.length < 5) return;
|
|
1207
|
+
taps = [];
|
|
1208
|
+
if (probe) { probe.remove(); probe = null; return; }
|
|
1209
|
+
probe = document.createElement('pre');
|
|
1210
|
+
probe.className = 'mfd-probe';
|
|
1211
|
+
document.body.appendChild(probe);
|
|
1212
|
+
const log = [];
|
|
1213
|
+
let last = performance.now(), frames = [];
|
|
1214
|
+
const note = e => {
|
|
1215
|
+
const t = performance.now();
|
|
1216
|
+
log.push(`${String(Math.round(t - last)).padStart(5)} ms ${e.type.padEnd(11)} b=${e.buttons === undefined ? '-' : e.buttons} ${e.pointerType || ''} ${e.touches ? 't' + e.touches.length : ''} ${e.deltaY !== undefined ? 'dy=' + Math.round(e.deltaY) : ''}`);
|
|
1217
|
+
last = t;
|
|
1218
|
+
if (log.length > 16) log.shift();
|
|
1219
|
+
};
|
|
1220
|
+
for (const type of ['mousedown', 'mousemove', 'mouseup', 'click', 'touchstart', 'touchmove', 'touchend', 'pointerdown', 'pointermove', 'pointerup', 'wheel', 'scroll']) document.addEventListener(type, note, true);
|
|
1221
|
+
// Frame time: the gap between two frames in a row. Redraw: rebuilding the list.
|
|
1222
|
+
const tick = () => {
|
|
1223
|
+
if (!probe) return;
|
|
1224
|
+
requestAnimationFrame(t1 => requestAnimationFrame(t2 => {
|
|
1225
|
+
if (!probe) return;
|
|
1226
|
+
frames.push(t2 - t1);
|
|
1227
|
+
if (frames.length > 10) frames.shift();
|
|
1228
|
+
const t0 = performance.now(); renderCircuits(); const redraw = performance.now() - t0;
|
|
1229
|
+
probe.textContent = `${window.innerWidth}x${window.innerHeight} frame ${Math.round(frames.reduce((a, b) => a + b, 0) / frames.length)} ms redraw ${Math.round(redraw)} ms\n${navigator.userAgent.replace(/^.*(Chrome\/[\d.]+).*$/, '$1')}\n` + log.join('\n');
|
|
1230
|
+
setTimeout(tick, 700);
|
|
1231
|
+
}));
|
|
1232
|
+
};
|
|
1233
|
+
tick();
|
|
1234
|
+
});
|
|
1235
|
+
}
|
|
1236
|
+
// A redraw asked for by live data. Anywhere else it runs at once. On a
|
|
1237
|
+
// plotter it waits until no finger is down, and runs at most twice a second.
|
|
1238
|
+
let liveTimer = null, liveLast = 0;
|
|
1239
|
+
function liveRender() {
|
|
1240
|
+
if (!MFD) { render(); return; }
|
|
1241
|
+
if (liveTimer) return;
|
|
1242
|
+
const run = () => {
|
|
1243
|
+
const now = Date.now();
|
|
1244
|
+
const wait = Math.max(mfdBusyUntil - now, liveLast + 500 - now);
|
|
1245
|
+
if (wait > 0) { liveTimer = setTimeout(run, wait); return; }
|
|
1246
|
+
liveTimer = null; liveLast = now; render();
|
|
1247
|
+
};
|
|
1248
|
+
run();
|
|
1249
|
+
}
|
|
1250
|
+
|
|
1028
1251
|
function render() { renderNav(); renderCategories(); renderCircuits(); renderStats(); renderModes(); renderHero(); }
|
|
1029
1252
|
|
|
1030
1253
|
document.querySelector('#search').addEventListener('input', e => { query=e.target.value.trim(); view=query?'search':'all'; category=null; render(); });
|
|
@@ -1094,7 +1317,7 @@
|
|
|
1094
1317
|
console.debug('[CZone WS] delta received', { valuesSeen, circuitValues, changed, updateSource: msg.updates.map(u => u.source || null), updateTimestamp: msg.updates.map(u => u.timestamp || null), timestamp: new Date(lastSocketUpdateAt).toISOString() });
|
|
1095
1318
|
updateSocketIndicator(circuitValues ? 'Connected · CZone updates live' : 'Connected · WebSocket live', true);
|
|
1096
1319
|
scheduleWatchdog();
|
|
1097
|
-
if (changed)
|
|
1320
|
+
if (changed) liveRender();
|
|
1098
1321
|
} catch (e) { console.debug('[CZone WS] message parse error', e); }
|
|
1099
1322
|
};
|
|
1100
1323
|
socket.onclose = () => {
|