signalk-ships-barometer 0.1.0 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +9 -0
- package/README.md +78 -3
- package/docs/VALIDATION-0.2.0.md +21 -0
- package/docs/publishing.md +1 -1
- package/index.js +39 -0
- package/lib/history.js +79 -0
- package/package.json +7 -4
- package/public/app.js +71 -0
- package/public/assets/ships-barometer.png +0 -0
- package/public/chart.js +14 -0
- package/public/index.html +8 -0
- package/public/style.css +3 -0
- package/public/theme.js +32 -0
- package/schema.json +6 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,14 @@
|
|
|
1
1
|
# Changes
|
|
2
2
|
|
|
3
|
+
## 0.2.0
|
|
4
|
+
|
|
5
|
+
- Add an independent, minute-bounded pressure recorder with at most 72 hours / 4,320 samples, atomic persistence and automatic expiration.
|
|
6
|
+
- Add an offline responsive webapp with current pressure, measured three-hour delta, existing tendency and forecasts, and 6/12/24/48-hour charts with visible gaps.
|
|
7
|
+
- Add Day, Night, Red and vessel-based Auto themes, matching Endurance controls; retain browser theme choice.
|
|
8
|
+
- Show freshness, source and storage status; support an explicit history pressure source.
|
|
9
|
+
- Preserve barometer.js, map.js and assessment.js calculations unchanged. No external forecast overlay or database dependency.
|
|
10
|
+
- Version 0.2.0 follows the existing 0.1.0 release, replacing the provisional 1.5.0 plan.
|
|
11
|
+
|
|
3
12
|
## 0.1.0
|
|
4
13
|
|
|
5
14
|
Independent Ship's Barometer release, derived from oyve/signalk-barometer-trend v2.3.4.
|
package/README.md
CHANGED
|
@@ -1,3 +1,71 @@
|
|
|
1
|
+
# Ship’s Barometer 0.2.0
|
|
2
|
+
|
|
3
|
+
An offline Signal K weather instrument with a responsive pressure chart. No QuestDB,
|
|
4
|
+
InfluxDB, internet connection, API key or extra history plugin is required.
|
|
5
|
+
|
|
6
|
+
Open **Ship’s Barometer** from Signal K’s Webapps after enabling the plugin. The
|
|
7
|
+
webapp shows current sensor pressure in hPa, a measured three-hour change, the
|
|
8
|
+
existing tendency and plain-language pressure forecast. Select 6, 12, 24 or 48 hours.
|
|
9
|
+
Day, Night and Red use the same controls and Auto rules as Endurance: the theme
|
|
10
|
+
button cycles the manual modes, and Auto follows vessel `environment.mode`.
|
|
11
|
+
Day selects Day; Night selects Red while `navigation.state` is sailing or motoring,
|
|
12
|
+
and Night while anchored or moored. Missing day/night state falls back to Night.
|
|
13
|
+
Your manual selection is stored in your browser.
|
|
14
|
+
|
|
15
|
+
## History and data quality
|
|
16
|
+
|
|
17
|
+
- A separate recorder retains at most **72 hours (3 days)** and **4,320 samples**.
|
|
18
|
+
- At most one actual observation per 60 seconds; no timer-generated samples.
|
|
19
|
+
- Original Signal K timestamps are retained, including across restarts. Invalid,
|
|
20
|
+
future, out-of-order and other-vessel observations are rejected. Source IDs are
|
|
21
|
+
required; known external weather sources are excluded.
|
|
22
|
+
- The recorder locks to the first accepted pressure source. Use **History pressure
|
|
23
|
+
source** to select an exact Signal K source ID when multiple sources exist. The
|
|
24
|
+
chart’s source selection is independent of the legacy forecast engine.
|
|
25
|
+
- Lines stop at gaps longer than 90 seconds. Single observations remain visible as
|
|
26
|
+
points. Missing observations are never filled. No history is imported from the
|
|
27
|
+
forecast engine or an external database.
|
|
28
|
+
- The three-hour delta compares actual readings within one minute of the three-hour
|
|
29
|
+
baseline; it is unavailable without that baseline or with stale current data.
|
|
30
|
+
It describes the change between those observations, even if there are gaps between them.
|
|
31
|
+
- Current pressure and analysis are hidden when pressure observations are over five
|
|
32
|
+
minutes old. Historical points remain available, with their actual times.
|
|
33
|
+
- `pressure-history.json` in the plugin data directory is atomically replaced every
|
|
34
|
+
three minutes when changed, and on orderly shutdown. Expired observations are
|
|
35
|
+
pruned during ingestion, reads, loading and saves. An abrupt power loss may lose
|
|
36
|
+
the latest three minutes. History storage errors appear in the webapp and server log.
|
|
37
|
+
- The chart displays raw sensor pressure. The existing analysis retains its own
|
|
38
|
+
altitude correction, sampling and source handling, so its reference can differ.
|
|
39
|
+
|
|
40
|
+
The existing pressure/tendency/forecast calculation files are unchanged. The optional
|
|
41
|
+
local weather assessment remains opt-in. Its existing explanatory reasons appear
|
|
42
|
+
when enabled and fresh. External forecast overlays are not included in this release:
|
|
43
|
+
no suitable provider was already integrated, and this instrument works entirely offline.
|
|
44
|
+
|
|
45
|
+
## Install, update and rollback
|
|
46
|
+
|
|
47
|
+
For the local release tarball, in the Signal K installation directory (commonly
|
|
48
|
+
`~/.signalk`), run `npm install /absolute/path/signalk-ships-barometer-0.2.0.tgz`.
|
|
49
|
+
Restart Signal K, enable Ship’s Barometer in Plugin Config, then open its Webapp.
|
|
50
|
+
For containers, use the installation directory inside the Signal K container and
|
|
51
|
+
retain the persistent Signal K data volume. This is local installation guidance;
|
|
52
|
+
initial onboard review on TBD confirms live observations and historical collection. Forecast accuracy has not been field-calibrated.
|
|
53
|
+
|
|
54
|
+
Before updating, back up the plugin configuration and its data directory. To roll
|
|
55
|
+
back, reinstall `signalk-ships-barometer@0.1.0` (or your saved previous tarball) and
|
|
56
|
+
restart Signal K. The new history file is independent of legacy `offline.json` and
|
|
57
|
+
`local-assessment.json`. Keep the backup until your aboard checks pass.
|
|
58
|
+
|
|
59
|
+
## Development
|
|
60
|
+
|
|
61
|
+
Run `npm ci` then `npm test`. Tests cover retention, minute bounds, original timestamps,
|
|
62
|
+
source isolation, persistence/restart, corrupt storage, write failures, chart gaps,
|
|
63
|
+
endpoint validation and plugin lifecycle, alongside the inherited forecast tests.
|
|
64
|
+
The read-only `/plugins/signalk-ships-barometer/history?hours=24` endpoint accepts
|
|
65
|
+
only 6, 12, 24 and 48 hours. Browser assets ship locally with the package.
|
|
66
|
+
|
|
67
|
+
---
|
|
68
|
+
|
|
1
69
|
<!-- Independent modifications by SCarns, 2026. Derived from oyve/signalk-barometer-trend v2.3.4; see NOTICE. -->
|
|
2
70
|
# Ship's Barometer
|
|
3
71
|
|
|
@@ -20,18 +88,25 @@ The assessment is **descriptive and experimental**. It does not provide a calibr
|
|
|
20
88
|
|
|
21
89
|
## Install
|
|
22
90
|
|
|
23
|
-
|
|
91
|
+
Available on [npm](https://www.npmjs.com/package/signalk-ships-barometer). Refresh the Signal K store and search for **Ship's Barometer**, or install from your Signal K user directory:
|
|
92
|
+
|
|
93
|
+
```sh
|
|
94
|
+
cd ~/.signalk
|
|
95
|
+
npm install --save signalk-ships-barometer
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
Download `signalk-ships-barometer-0.2.0.tgz` from [GitHub Releases](https://github.com/SCarns/signalk-ships-barometer/releases/latest) and copy it into your Signal K user directory, normally `~/.signalk`:
|
|
24
99
|
|
|
25
100
|
```sh
|
|
26
101
|
cd ~/.signalk
|
|
27
|
-
npm install --save ./signalk-ships-barometer-0.
|
|
102
|
+
npm install --save ./signalk-ships-barometer-0.2.0.tgz
|
|
28
103
|
```
|
|
29
104
|
|
|
30
105
|
**Disable Barometer Trend before enabling Ship's Barometer.** The plugins share the original forecast output paths; running both would create competing publishers. You can keep the old package installed for rollback.
|
|
31
106
|
|
|
32
107
|
Restart Signal K and enable **Ship's Barometer** in plugin settings. To use the extra assessment, enable **Local weather assessment**. Check [installation and migration](docs/installation.md) for source/depth settings and history handling.
|
|
33
108
|
|
|
34
|
-
|
|
109
|
+
Releases are published on npm under **scarns**, with the Signal K discovery keyword and app icon metadata. Store indexing/cache refresh may take time. See [publishing to the Signal K store](docs/publishing.md) for future releases.
|
|
35
110
|
|
|
36
111
|
## Local inputs and output
|
|
37
112
|
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# 0.2.0 local validation
|
|
2
|
+
|
|
3
|
+
- `npm test`: 76 passing tests, including the inherited forecast, input, assessment and wind-unit suites.
|
|
4
|
+
- New history tests verify minute and 72-hour bounds, invalid timestamps, stale current data, source locking, persistence, pruning, corrupt files, write failure/retry, real three-hour baselines and chart gap splitting.
|
|
5
|
+
- Integration test verifies the local vessel subscription, endpoint range validation, stopped status, orderly save and restart restoration.
|
|
6
|
+
- `barometer.js`, `map.js`, `assessment.js`, `meta.json` and runtime dependencies remain unchanged from repository commit `3ca70df`.
|
|
7
|
+
- Browser inspection uses explicitly synthetic data, including a pressure outage; it does not establish sensor accuracy or onboard forecasting performance.
|
|
8
|
+
- Theme tests confirm vessel-based Auto rules and manual overrides match Endurance. Phone browser checks confirm the manual cycle and 48-hour range selection.
|
|
9
|
+
- External forecasts and database connections are absent.
|
|
10
|
+
|
|
11
|
+
## Aboard acceptance
|
|
12
|
+
|
|
13
|
+
1. Back up the existing plugin data directory and configuration, then install the local tarball and restart Signal K.
|
|
14
|
+
2. Enable the plugin. Check its displayed pressure source against your onboard sensor. Select an exact source in Plugin Config if necessary.
|
|
15
|
+
3. Verify current pressure, existing forecast/tendency and all four chart ranges. Check phone and helm displays with Day, Night, Red and Auto themes.
|
|
16
|
+
4. Leave the plugin collecting for three hours; check its measured delta once a real baseline is available.
|
|
17
|
+
5. Stop the pressure feed for at least five minutes. Verify stale status, hidden current readings and a visible chart gap after observations resume.
|
|
18
|
+
6. Restart Signal K; confirm timestamps and recorded points survive. A sudden power loss may lose up to three minutes.
|
|
19
|
+
7. Compare existing Signal K forecast outputs with your prior build. The recorder never supplies inputs to the forecast engine.
|
|
20
|
+
|
|
21
|
+
Initial onboard review on TBD: the user confirmed normal use after letting the build run and supplied a 24-hour history screenshot on October 18, 2026. This establishes onboard operation and historical collection, not forecast accuracy or completion of every acceptance check.
|
package/docs/publishing.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
Signal K discovers plugins published to npm with the `signalk-node-server-plugin` keyword. Ship's Barometer already has that keyword, the weather category, its own package name (`signalk-ships-barometer`), display name and app icon metadata. GitHub publication alone does not make it available in the store.
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
Version **0.1.0 was published successfully on October 2, 2026 under scarns**. The public registry reports it as latest; its checksum matches the tested GitHub release tarball. Signal K discovery keywords and icon metadata were verified. The installed artifact passed all 64 tests. The initial authentication setup below is retained as a reference for future publishers.
|
|
6
6
|
|
|
7
7
|
## First publication
|
|
8
8
|
|
package/index.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
// Ship's Barometer modifications by SCarns, 2026. Derived from oyve/signalk-barometer-trend v2.3.4; see NOTICE and CHANGELOG.md.
|
|
2
2
|
'use strict'
|
|
3
3
|
const fs = require('fs');
|
|
4
|
+
const { History } = require('./lib/history');
|
|
4
5
|
const meta = require('./meta.json');
|
|
5
6
|
const schema = require('./schema.json');
|
|
6
7
|
const barometer = require('./barometer');
|
|
@@ -9,6 +10,9 @@ const { Assessment, PATHS } = require('./assessment');
|
|
|
9
10
|
module.exports = function (app) {
|
|
10
11
|
var plugin = { };
|
|
11
12
|
let persistTimer = null;
|
|
13
|
+
let history = null;
|
|
14
|
+
let running = false;
|
|
15
|
+
let display = {};
|
|
12
16
|
let assessment = null;
|
|
13
17
|
let assessmentTimer = null;
|
|
14
18
|
|
|
@@ -19,6 +23,10 @@ module.exports = function (app) {
|
|
|
19
23
|
var unsubscribes = [];
|
|
20
24
|
plugin.start = function (options, restartPlugin) {
|
|
21
25
|
app.debug('Plugin started');
|
|
26
|
+
display = {};
|
|
27
|
+
history = new History(app.getDataDirPath() + '/pressure-history.json', app.selfId, msg => app.error(msg), options.historyPressureSource || '');
|
|
28
|
+
history.load();
|
|
29
|
+
running = true;
|
|
22
30
|
|
|
23
31
|
try {
|
|
24
32
|
barometer.setSampleRate(options.rate);
|
|
@@ -68,14 +76,25 @@ module.exports = function (app) {
|
|
|
68
76
|
publishAssessment();
|
|
69
77
|
}
|
|
70
78
|
|
|
79
|
+
app.subscriptionmanager.subscribe(
|
|
80
|
+
{ context: 'vessels.' + app.selfId, subscribe: [{ path: 'environment.outside.pressure', period: 60000, policy: 'instant', minPeriod: 60000 }] },
|
|
81
|
+
unsubscribes, err => app.error('Pressure history subscription: ' + err),
|
|
82
|
+
delta => history && history.ingest(delta)
|
|
83
|
+
);
|
|
84
|
+
|
|
71
85
|
persistTimer = setInterval(function () {
|
|
72
86
|
saveAssessment();
|
|
87
|
+
if (history) history.save();
|
|
73
88
|
barometer.persist(write);
|
|
74
89
|
}, 1000 * 60 * 3); //every 3 minutes
|
|
75
90
|
};
|
|
76
91
|
|
|
77
92
|
plugin.stop = function () {
|
|
78
93
|
app.debug('Plugin stopping');
|
|
94
|
+
running = false;
|
|
95
|
+
if (history) history.save();
|
|
96
|
+
history = null;
|
|
97
|
+
display = {};
|
|
79
98
|
clearInterval(persistTimer);
|
|
80
99
|
clearInterval(assessmentTimer);
|
|
81
100
|
saveAssessment();
|
|
@@ -89,7 +108,27 @@ module.exports = function (app) {
|
|
|
89
108
|
|
|
90
109
|
plugin.schema = schema[0];
|
|
91
110
|
|
|
111
|
+
plugin.registerWithRouter = function (router) {
|
|
112
|
+
const viewer = typeof router.access === 'function' ? router.access('readonly') : router;
|
|
113
|
+
viewer.get('/history', (req, res) => {
|
|
114
|
+
const hours = Number(req.query && req.query.hours || 24);
|
|
115
|
+
if (![6, 12, 24, 48].includes(hours)) return res.status(400).json({ error: 'Choose 6, 12, 24 or 48 hours' });
|
|
116
|
+
res.set('Cache-Control', 'no-store');
|
|
117
|
+
const modeValue = path => {
|
|
118
|
+
const leaf = app.getSelfPath && app.getSelfPath(path);
|
|
119
|
+
const value = leaf && leaf.value !== undefined ? leaf.value : leaf;
|
|
120
|
+
return typeof value === 'string' ? value : null;
|
|
121
|
+
};
|
|
122
|
+
res.json({ running, history: history ? history.snapshot(hours) : null, analysis: display,
|
|
123
|
+
display: { environmentMode: modeValue('environment.mode'), navigationState: modeValue('navigation.state') } });
|
|
124
|
+
});
|
|
125
|
+
};
|
|
126
|
+
|
|
92
127
|
function sendDelta(deltaValues) {
|
|
128
|
+
if (Array.isArray(deltaValues)) {
|
|
129
|
+
const time = Date.now();
|
|
130
|
+
for (const v of deltaValues) display[v.path] = { value: v.value, time };
|
|
131
|
+
}
|
|
93
132
|
if (deltaValues !== null && deltaValues.length > 0) {
|
|
94
133
|
let signalk_delta = {
|
|
95
134
|
context: "vessels." + app.selfId,
|
package/lib/history.js
ADDED
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
const fs = require('fs');
|
|
3
|
+
const MINUTE = 60000;
|
|
4
|
+
const RETENTION = 72 * 60 * MINUTE;
|
|
5
|
+
const MAX_RECORDS = 4320;
|
|
6
|
+
const PRESSURE = 'environment.outside.pressure';
|
|
7
|
+
|
|
8
|
+
// Observations only: no synthetic timer samples, interpolation or forecast inputs.
|
|
9
|
+
class History {
|
|
10
|
+
constructor(file, selfId, report = () => {}, source = '') {
|
|
11
|
+
this.file = file;
|
|
12
|
+
this.context = `vessels.${selfId}`;
|
|
13
|
+
this.report = report;
|
|
14
|
+
this.source = source;
|
|
15
|
+
this.records = [];
|
|
16
|
+
this.dirty = false;
|
|
17
|
+
this.error = null;
|
|
18
|
+
}
|
|
19
|
+
prune(now = Date.now()) {
|
|
20
|
+
const kept = this.records.filter(p => p.time > now - RETENTION && p.time <= now).slice(-MAX_RECORDS);
|
|
21
|
+
if (kept.length !== this.records.length) this.dirty = true;
|
|
22
|
+
this.records = kept;
|
|
23
|
+
}
|
|
24
|
+
add(time, pressure, source, now = Date.now()) {
|
|
25
|
+
if (typeof source !== 'string' || !source || /open-meteo|noaa|weatherapi/i.test(source)) return false;
|
|
26
|
+
if (!Number.isFinite(time) || !Number.isFinite(pressure) || pressure <= 0 || time > now || time <= now - RETENTION) return false;
|
|
27
|
+
if (this.source && source !== this.source) return false;
|
|
28
|
+
const last = this.records[this.records.length - 1];
|
|
29
|
+
if (last && time < last.time + MINUTE) return false;
|
|
30
|
+
if (!this.source) this.source = source;
|
|
31
|
+
this.records.push({ time, pressure, source });
|
|
32
|
+
this.dirty = true;
|
|
33
|
+
this.prune(now);
|
|
34
|
+
return true;
|
|
35
|
+
}
|
|
36
|
+
ingest(delta, now = Date.now()) {
|
|
37
|
+
if (!delta || ![this.context, 'vessels.self'].includes(delta.context)) return;
|
|
38
|
+
for (const u of delta.updates || []) {
|
|
39
|
+
// Require a timestamp: receipt time must never make a cached reading look fresh.
|
|
40
|
+
const time = typeof u.timestamp === 'string' ? Date.parse(u.timestamp) : NaN;
|
|
41
|
+
const source = u.$source || (u.source && (u.source.label || u.source.src));
|
|
42
|
+
for (const v of u.values || []) if (v.path === PRESSURE) this.add(time, v.value, source, now);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
load(now = Date.now()) {
|
|
46
|
+
try {
|
|
47
|
+
const data = JSON.parse(fs.readFileSync(this.file, 'utf8'));
|
|
48
|
+
if (data.version !== 1 || !Array.isArray(data.records) || data.records.length > MAX_RECORDS) throw new Error('Invalid history file');
|
|
49
|
+
this.dirty = true; // Rewrite after filtering even when every saved record expired.
|
|
50
|
+
const configured = this.source;
|
|
51
|
+
if (!configured && typeof data.source === 'string') this.source = data.source;
|
|
52
|
+
for (const p of data.records) this.add(p.time, p.pressure, p.source, now);
|
|
53
|
+
this.prune(now);
|
|
54
|
+
} catch (err) {
|
|
55
|
+
if (err.code !== 'ENOENT') { this.error = err.message; this.report(`Pressure history could not be loaded: ${err.message}`); }
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
save(now = Date.now()) {
|
|
59
|
+
this.prune(now);
|
|
60
|
+
if (!this.dirty) return;
|
|
61
|
+
try {
|
|
62
|
+
fs.writeFileSync(this.file + '.tmp', JSON.stringify({ version: 1, source: this.source, records: this.records }));
|
|
63
|
+
fs.renameSync(this.file + '.tmp', this.file);
|
|
64
|
+
this.dirty = false;
|
|
65
|
+
this.error = null;
|
|
66
|
+
} catch (err) { this.error = err.message; this.report(`Pressure history could not be saved: ${err.message}`); }
|
|
67
|
+
}
|
|
68
|
+
snapshot(hours = 24, now = Date.now()) {
|
|
69
|
+
this.prune(now);
|
|
70
|
+
const latest = this.records.at(-1) || null;
|
|
71
|
+
const fresh = !!latest && now - latest.time <= 5 * MINUTE;
|
|
72
|
+
// An actual sample near three hours ago, never an interpolated baseline.
|
|
73
|
+
const baseline = latest && this.records.find(p => Math.abs(p.time - (latest.time - 3 * 60 * MINUTE)) <= MINUTE);
|
|
74
|
+
return { now, source: this.source || null, storageError: this.error, latest, fresh,
|
|
75
|
+
delta3h: fresh && baseline ? latest.pressure - baseline.pressure : null,
|
|
76
|
+
gapMs: 1.5 * MINUTE, records: this.records.filter(p => p.time >= now - hours * 60 * MINUTE) };
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
module.exports = { History, RETENTION, MINUTE, MAX_RECORDS };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "signalk-ships-barometer",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"description": "Ship's Barometer: offline pressure forecasts and local wind, moisture and water-temperature assessment",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"scripts": {
|
|
@@ -23,7 +23,8 @@
|
|
|
23
23
|
"hemisphere",
|
|
24
24
|
"prediction",
|
|
25
25
|
"weather",
|
|
26
|
-
"forecast"
|
|
26
|
+
"forecast",
|
|
27
|
+
"signalk-webapp"
|
|
27
28
|
],
|
|
28
29
|
"author": "SCarns",
|
|
29
30
|
"license": "Apache-2.0",
|
|
@@ -52,7 +53,7 @@
|
|
|
52
53
|
],
|
|
53
54
|
"signalk-plugin-enabled-by-default": false,
|
|
54
55
|
"signalk": {
|
|
55
|
-
"appIcon": "
|
|
56
|
+
"appIcon": "assets/ships-barometer.png",
|
|
56
57
|
"displayName": "Ship's Barometer"
|
|
57
58
|
},
|
|
58
59
|
"files": [
|
|
@@ -67,7 +68,9 @@
|
|
|
67
68
|
"NOTICE",
|
|
68
69
|
"README.md",
|
|
69
70
|
"CHANGELOG.md",
|
|
70
|
-
"docs"
|
|
71
|
+
"docs",
|
|
72
|
+
"lib",
|
|
73
|
+
"public"
|
|
71
74
|
],
|
|
72
75
|
"engines": {
|
|
73
76
|
"node": ">=18"
|
package/public/app.js
ADDED
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
const el = id => document.getElementById(id);
|
|
3
|
+
let hours = 24;
|
|
4
|
+
let request = 0;
|
|
5
|
+
for (const b of document.querySelectorAll('[data-hours]')) b.onclick = () => {
|
|
6
|
+
hours = Number(b.dataset.hours);
|
|
7
|
+
for (const other of document.querySelectorAll('[data-hours]')) other.setAttribute('aria-pressed', String(other === b));
|
|
8
|
+
refresh();
|
|
9
|
+
};
|
|
10
|
+
function node(tag, attrs = {}, text) {
|
|
11
|
+
const n = document.createElementNS('http://www.w3.org/2000/svg', tag);
|
|
12
|
+
for (const [key,value] of Object.entries(attrs)) n.setAttribute(key,value);
|
|
13
|
+
if (text !== undefined) n.textContent = text;
|
|
14
|
+
return n;
|
|
15
|
+
}
|
|
16
|
+
function chart(h) {
|
|
17
|
+
const svg = el('chart'); svg.replaceChildren();
|
|
18
|
+
const width = Math.max(340, svg.getBoundingClientRect().width);
|
|
19
|
+
svg.setAttribute('viewBox', `0 0 ${width} 320`);
|
|
20
|
+
const right = width - 20;
|
|
21
|
+
const data = h.records.filter(p => p.time >= h.now - hours * 3600000);
|
|
22
|
+
if (!data.length) { svg.append(node('text',{x:70,y:150},'Waiting for pressure observations')); return; }
|
|
23
|
+
const start = h.now - hours * 3600000;
|
|
24
|
+
const lo = Math.min(...data.map(p => p.pressure))/100 - 1;
|
|
25
|
+
const hi = Math.max(...data.map(p => p.pressure))/100 + 1;
|
|
26
|
+
const x = t => 70 + (t-start)/(hours*3600000)*(right-70);
|
|
27
|
+
const y = p => 260 - (p/100-lo)/(hi-lo)*235;
|
|
28
|
+
const ticks = width < 600 ? 2 : 4;
|
|
29
|
+
for (let i=0;i<=ticks;i++) {
|
|
30
|
+
const value=lo+(hi-lo)*i/ticks, cy=y(value*100);
|
|
31
|
+
svg.append(node('line',{x1:70,x2:right,y1:cy,y2:cy,class:'grid'}),node('text',{x:4,y:cy+5},value.toFixed(1)));
|
|
32
|
+
const time=start+hours*3600000*i/ticks;
|
|
33
|
+
const anchor = i===0?'start':i===ticks?'end':'middle';
|
|
34
|
+
svg.append(node('text',{x:x(time),y:290,'text-anchor':anchor},new Date(time).toLocaleTimeString([],{hour:'2-digit',minute:'2-digit'})));
|
|
35
|
+
svg.append(node('text',{x:x(time),y:313,'text-anchor':anchor},new Date(time).toLocaleDateString([],{month:'short',day:'numeric'})));
|
|
36
|
+
}
|
|
37
|
+
for (const group of pressureSegments(data,h.gapMs)) {
|
|
38
|
+
svg.append(node('path',{class:'observed',d:group.map((p,i)=>`${i?'L':'M'}${x(p.time)},${y(p.pressure)}`).join(' ')}));
|
|
39
|
+
for (const p of group) {
|
|
40
|
+
const dot=node('circle',{cx:x(p.time),cy:y(p.pressure),r:group.length===1?3:1.6});
|
|
41
|
+
dot.append(node('title',{},`${new Date(p.time).toLocaleString()}: ${(p.pressure/100).toFixed(1)} hPa`));svg.append(dot);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
function clearReadings() { for (const id of ['pressure','change','tendency']) el(id).textContent='—'; el('assessment').textContent='Waiting for fresh Barometer analysis.';el('local').textContent=''; }
|
|
46
|
+
async function refresh() {
|
|
47
|
+
const seq=++request;
|
|
48
|
+
try {
|
|
49
|
+
const response=await fetch(`../plugins/signalk-ships-barometer/history?hours=${hours}`,{cache:'no-store',signal:AbortSignal.timeout(10000)});
|
|
50
|
+
if (!response.ok) throw new Error('History unavailable');
|
|
51
|
+
const data=await response.json();
|
|
52
|
+
if(seq!==request)return;
|
|
53
|
+
if (!data.running || !data.history) throw new Error('Barometer plugin is stopped');
|
|
54
|
+
window.BarometerTheme.update(data.display || {});
|
|
55
|
+
const h=data.history;
|
|
56
|
+
clearReadings();
|
|
57
|
+
el('status').textContent = h.latest ? `${h.fresh?'Live observations':'Stale observations'} · last reading ${new Date(h.latest.time).toLocaleString()}` : 'Waiting for local pressure observations';
|
|
58
|
+
if(h.storageError)el('status').textContent+=' · history storage needs attention';
|
|
59
|
+
el('pressure').textContent=h.fresh?(h.latest.pressure/100).toFixed(1):'—';
|
|
60
|
+
el('change').textContent=h.delta3h===null?'—':`${h.delta3h>0?'+':''}${(h.delta3h/100).toFixed(1)}`;
|
|
61
|
+
const read=path=>{const v=data.analysis[path];return v&&h.fresh&&h.now-v.time<=5*60000?v.value:null;};
|
|
62
|
+
const prefix='environment.outside.pressure.';
|
|
63
|
+
el('tendency').textContent=read(prefix+'trend.tendency') || '—';
|
|
64
|
+
el('assessment').textContent=read(prefix+'prediction.quadrant') || read(prefix+'prediction.pressureOnly') || 'Waiting for existing Barometer analysis.';
|
|
65
|
+
const local=read('environment.outside.weather.assessment');
|
|
66
|
+
if(local&&Array.isArray(local.reasons))el('local').textContent=local.reasons.join(' ');
|
|
67
|
+
el('source').textContent=`Pressure source: ${h.source||'waiting'} · 3-hour change needs an observed sample within one minute of the baseline.`;
|
|
68
|
+
chart(h);
|
|
69
|
+
} catch(err) { if(seq!==request)return;clearReadings();el('status').textContent=`${err.message} · reconnecting`;el('chart').replaceChildren();el('source').textContent=''; }
|
|
70
|
+
}
|
|
71
|
+
refresh();setInterval(refresh,30000);
|
|
Binary file
|
package/public/chart.js
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/* Shared with tests: never draw across missing observations. */
|
|
2
|
+
(function(root) {
|
|
3
|
+
function segments(records, gapMs) {
|
|
4
|
+
const result = [];
|
|
5
|
+
for (const p of records) {
|
|
6
|
+
const last = result.at(-1);
|
|
7
|
+
if (!last || p.time - last.at(-1).time > gapMs || p.source !== last.at(-1).source) result.push([p]);
|
|
8
|
+
else last.push(p);
|
|
9
|
+
}
|
|
10
|
+
return result;
|
|
11
|
+
}
|
|
12
|
+
if (typeof module !== 'undefined') module.exports = { segments };
|
|
13
|
+
else root.pressureSegments = segments;
|
|
14
|
+
})(typeof window !== 'undefined' ? window : globalThis);
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><meta name="theme-color" content="#0b1218"><script src="theme.js"></script><title>Ship’s Barometer</title><link rel="stylesheet" href="style.css"></head>
|
|
3
|
+
<body><main><header><h1>Ship’s Barometer</h1><span class="theme-controls" role="group" aria-label="Display theme"><button id="theme" type="button" class="theme-control" aria-label="Change display theme">☾</button><button id="theme-auto" type="button" aria-pressed="true" title="Follow vessel day/night and navigation state">Auto</button></span></header>
|
|
4
|
+
<p id="status" role="status">Connecting to onboard observations…</p>
|
|
5
|
+
<section class="readings" aria-label="Pressure readings"><article><h2>Current pressure</h2><strong id="pressure">—</strong><p>hPa · sensor pressure</p></article><article><h2>3-hour change</h2><strong id="change">—</strong><p>hPa · observed samples</p></article><article><h2>Tendency</h2><strong id="tendency">—</strong><p>Existing Barometer analysis</p></article></section>
|
|
6
|
+
<section><h2>Weather assessment</h2><p id="assessment">Waiting for existing Barometer analysis.</p><p id="local"></p></section>
|
|
7
|
+
<section><div class="chart-heading"><h2>Pressure history</h2><div id="ranges" role="group" aria-label="History range"><button data-hours="6">6 h</button><button data-hours="12">12 h</button><button data-hours="24" aria-pressed="true">24 h</button><button data-hours="48">48 h</button></div></div><svg id="chart" viewBox="0 0 900 320" role="img" aria-label="Observed pressure history in hPa"></svg><p id="chart-note">Onboard observations · gaps indicate missing data</p><p id="source"></p></section>
|
|
8
|
+
<footer>Offline instrument · up to 72 hours (3 days) retained · local assessments are guidance</footer></main><script src="chart.js"></script><script src="app.js"></script></body></html>
|
package/public/style.css
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
:root{color-scheme:light;--bg:#edf2f4;--panel:#fff;--text:#163440;--muted:#4f6670;--line:#007b84;--grid:#d5e0e5}html[data-theme=night]{color-scheme:dark;--bg:#101b22;--panel:#1b2932;--text:#e2eced;--muted:#a3bac4;--line:#63d5ce;--grid:#3b4e59}html[data-theme=red]{color-scheme:dark;--bg:#0f0000;--panel:#1d0505;--text:#e25d53;--muted:#b7443d;--line:#e25d53;--grid:#63241e}*{box-sizing:border-box}body{margin:0;background:var(--bg);color:var(--text);font:17px/1.5 system-ui,sans-serif}main{max-width:1100px;margin:auto;padding:22px}header,.chart-heading{display:flex;align-items:center;justify-content:space-between;gap:16px;flex-wrap:wrap}h1{font-size:1.7rem}h2{font-size:1rem;margin:0 0 12px}section{background:var(--panel);padding:22px;border-radius:12px;margin:18px 0}section.readings{padding:0;background:none;display:grid;grid-template-columns:repeat(3,1fr);gap:16px}.readings article{background:var(--panel);padding:22px;border-radius:12px}.readings strong{font-size:clamp(1.5rem,4vw,2.5rem)}p{margin:8px 0;color:var(--muted)}button,select{font:inherit;background:var(--panel);color:var(--text);border:1px solid var(--muted);border-radius:6px;min-height:44px;padding:7px 12px}button{cursor:pointer}button[aria-pressed=true]{background:var(--text);color:var(--bg)}button:focus-visible,select:focus-visible{outline:3px solid var(--line);outline-offset:3px}#chart{width:100%;height:auto;display:block;margin-top:16px}#chart text{fill:var(--muted);font:15px system-ui}#chart .grid{stroke:var(--grid)}#chart .observed{stroke:var(--line);fill:none;stroke-width:2.5}#chart circle{fill:var(--line)}footer{color:var(--muted);font-size:.85rem}#source{overflow-wrap:anywhere}@media(max-width:600px){main{padding:12px}section.readings{grid-template-columns:1fr}.readings article{padding:16px}.readings p{display:inline;margin-left:12px}.readings strong{font-size:2rem}section{padding:16px}}
|
|
2
|
+
|
|
3
|
+
.theme-controls{display:inline-flex;gap:5px;align-items:center}.theme-control{min-width:42px;font-size:22px;line-height:1;padding:7px 10px}.theme-controls button[aria-pressed="true"]{box-shadow:inset 0 0 0 2px currentColor;font-weight:700}
|
package/public/theme.js
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
(function(){
|
|
3
|
+
const modes=['auto','day','night','red'];
|
|
4
|
+
const normalize=mode=>modes.includes(mode)?mode:'auto';
|
|
5
|
+
function resolve(mode,environmentMode,navigationState){
|
|
6
|
+
mode=normalize(mode);if(mode!=='auto')return mode;
|
|
7
|
+
const light=String(environmentMode||'').trim().toLowerCase();
|
|
8
|
+
const state=String(navigationState||'').trim().toLowerCase();
|
|
9
|
+
if(light==='day')return 'day';
|
|
10
|
+
if(light==='night'&&['sailing','motoring'].includes(state))return 'red';
|
|
11
|
+
return 'night';
|
|
12
|
+
}
|
|
13
|
+
const next=mode=>modes[(modes.indexOf(normalize(mode))+1)%modes.length];
|
|
14
|
+
if(typeof module==='object'&&module.exports){module.exports={resolve,next,normalize};return;}
|
|
15
|
+
let mode='auto',environmentMode=null,navigationState=null;
|
|
16
|
+
try{mode=normalize(localStorage.getItem('barometer.display.theme'));}catch{}
|
|
17
|
+
const label=value=>value[0].toUpperCase()+value.slice(1);
|
|
18
|
+
function apply(){
|
|
19
|
+
const effective=resolve(mode,environmentMode,navigationState);
|
|
20
|
+
document.documentElement.dataset.theme=effective;
|
|
21
|
+
const meta=document.querySelector('meta[name="theme-color"]');if(meta)meta.content={day:'#edf3f3',night:'#0b1218',red:'#080000'}[effective];
|
|
22
|
+
const button=document.getElementById('theme');
|
|
23
|
+
if(button){button.textContent={day:'☀︎',night:'☾',red:'◉'}[effective];button.setAttribute('aria-label',`Display theme: ${label(effective)}. Switch to ${label(next(effective)==='auto'?'day':next(effective))}.`);button.title=`${label(effective)} display. Click to select the next manual theme.`;}
|
|
24
|
+
const auto=document.getElementById('theme-auto');
|
|
25
|
+
if(auto){auto.setAttribute('aria-pressed',String(mode==='auto'));auto.title=mode==='auto'&&!['day','night'].includes(String(environmentMode||'').trim().toLowerCase())?'Auto: vessel day/night state unavailable; using Night.':'Auto follows vessel day/night; night sailing or motoring uses Red.';}
|
|
26
|
+
|
|
27
|
+
}
|
|
28
|
+
window.BarometerTheme={update(data={}){environmentMode=data.environmentMode;navigationState=data.navigationState;apply();}};
|
|
29
|
+
apply();
|
|
30
|
+
function select(value){mode=value;try{localStorage.setItem('barometer.display.theme',mode);}catch{}apply();}
|
|
31
|
+
document.addEventListener('DOMContentLoaded',()=>{apply();document.getElementById('theme').addEventListener('click',()=>{const effective=resolve(mode,environmentMode,navigationState);select({day:'night',night:'red',red:'day'}[effective]);});document.getElementById('theme-auto').addEventListener('click',()=>select('auto'));});
|
|
32
|
+
})();
|
package/schema.json
CHANGED
|
@@ -61,6 +61,12 @@
|
|
|
61
61
|
"type": "number",
|
|
62
62
|
"minimum": 0,
|
|
63
63
|
"description": "Optional depth below the surface. Leave unset if unknown. For a transducer two feet deep, enter 0.6096."
|
|
64
|
+
},
|
|
65
|
+
"historyPressureSource": {
|
|
66
|
+
"title": "History pressure source",
|
|
67
|
+
"type": "string",
|
|
68
|
+
"default": "",
|
|
69
|
+
"description": "Exact Signal K source ID for the chart. Blank locks to the first valid local pressure source and retains it across restarts. Independent of forecast inputs."
|
|
64
70
|
}
|
|
65
71
|
},
|
|
66
72
|
"$comment": "Modified by SCarns for independent Ship's Barometer settings; see NOTICE and CHANGELOG.md."
|