signalk-siparu 0.2.14 → 0.2.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "signalk-siparu",
3
- "version": "0.2.14",
3
+ "version": "0.2.16",
4
4
  "description": "Kept aboard, proven ashore. Position, wind, depth and voyage history, recorded on the boat and readable from anywhere: sealed to the devices you name, signed on the way out. A read-only Signal K plugin with a built-in dashboard.",
5
5
  "keywords": [
6
6
  "signalk-node-server-plugin",
@@ -2,6 +2,7 @@ import type { RemoteLink } from './remotelink';
2
2
  import type { PathSeriesResult, SnapshotsQuery, SnapshotsResult, TrackResult, VoyageListResult, PhaseListResult, RollupsResult, VoyageStatsResult, FuelPathsView, AisFeed, HealthResult } from './contract';
3
3
  export declare const FRAME_EVERY_MS = 2000;
4
4
  export declare const STILL_FRAME_EVERY_MS = 60000;
5
+ export declare const PROMPTED_FRAME_FLOOR_MS = 5000;
5
6
  export declare const REPORTING_STALE_MS: number;
6
7
  export declare const PING_EVERY_MS = 25000;
7
8
  export interface LiveSocket {
@@ -87,6 +88,7 @@ export declare class LiveUplink {
87
88
  private kill;
88
89
  private dispatch;
89
90
  private openSealedAsk;
91
+ private greet;
90
92
  private sendFrame;
91
93
  private scheduleFrame;
92
94
  private wayStreak;
@@ -1,10 +1,11 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.LiveUplink = exports.PING_EVERY_MS = exports.REPORTING_STALE_MS = exports.STILL_FRAME_EVERY_MS = exports.FRAME_EVERY_MS = void 0;
3
+ exports.LiveUplink = exports.PING_EVERY_MS = exports.REPORTING_STALE_MS = exports.PROMPTED_FRAME_FLOOR_MS = exports.STILL_FRAME_EVERY_MS = exports.FRAME_EVERY_MS = void 0;
4
4
  exports.rpcEnvelope = rpcEnvelope;
5
5
  exports.isWatchingNote = isWatchingNote;
6
6
  exports.FRAME_EVERY_MS = 2_000;
7
7
  exports.STILL_FRAME_EVERY_MS = 60_000;
8
+ exports.PROMPTED_FRAME_FLOOR_MS = 5_000;
8
9
  exports.REPORTING_STALE_MS = exports.STILL_FRAME_EVERY_MS * 3;
9
10
  const UNDERWAY_SOG_MS = 0.3 * 0.514444;
10
11
  const CLEARLY_UNDERWAY_SOG_MS = 1.5 * 0.514444;
@@ -140,6 +141,7 @@ class LiveUplink {
140
141
  }
141
142
  if (isWatchingNote(data)) {
142
143
  this.deps.onWatching?.();
144
+ this.greet(gen);
143
145
  return;
144
146
  }
145
147
  const opened = this.openSealedAsk(data);
@@ -207,6 +209,12 @@ class LiveUplink {
207
209
  }
208
210
  return open(envelope);
209
211
  }
212
+ greet(gen) {
213
+ if (this.lastFrameTs !== null && Date.now() - this.lastFrameTs < exports.PROMPTED_FRAME_FLOOR_MS) {
214
+ return;
215
+ }
216
+ this.sendFrame(gen);
217
+ }
210
218
  sendFrame(gen) {
211
219
  if (gen !== this.gen || !this.sock)
212
220
  return;
@@ -1,7 +1,7 @@
1
1
  import { MetricField, Snapshot } from './contract';
2
2
  import { Options } from './config';
3
3
  export declare const SUBSCRIBED_PATHS: string[];
4
- export declare const DYNAMIC_PREFIXES: readonly ["propulsion.", "tanks.", "electrical.generators."];
4
+ export declare const DYNAMIC_PREFIXES: readonly ["propulsion.", "tanks.", "electrical.ac.", "electrical.alternators.", "electrical.batteries.", "electrical.chargers.", "electrical.generators.", "electrical.inverters.", "electrical.solar."];
5
5
  export interface Diagnosis {
6
6
  code: 'ok' | 'instruments-off' | 'energy-only' | 'no-data';
7
7
  message: string;
@@ -22,6 +22,7 @@ export declare class MetricsState {
22
22
  private conceptNumeric;
23
23
  snapshot(now: number, flushGust: boolean, freshnessMs?: number): Snapshot;
24
24
  private dynamicPathCount;
25
+ private evictFiller;
25
26
  dynamicPaths(now: number): Record<string, number | string>;
26
27
  dynamicPathAges(now: number): Record<string, number>;
27
28
  coreFieldAges(now: number): Partial<Record<MetricField, number>>;
@@ -34,10 +34,20 @@ exports.SUBSCRIBED_PATHS = [
34
34
  ...DEPTH_PATHS,
35
35
  ...Object.keys(DIRECT_PATHS)
36
36
  ];
37
- exports.DYNAMIC_PREFIXES = ['propulsion.', 'tanks.', 'electrical.generators.'];
37
+ exports.DYNAMIC_PREFIXES = [
38
+ 'propulsion.',
39
+ 'tanks.',
40
+ 'electrical.ac.',
41
+ 'electrical.alternators.',
42
+ 'electrical.batteries.',
43
+ 'electrical.chargers.',
44
+ 'electrical.generators.',
45
+ 'electrical.inverters.',
46
+ 'electrical.solar.'
47
+ ];
38
48
  const CORE_PATH_SET = new Set(exports.SUBSCRIBED_PATHS);
39
49
  const TEXT_MAX = 32;
40
- const MAX_DYNAMIC_PATHS = 64;
50
+ const MAX_DYNAMIC_PATHS = 128;
41
51
  const PATH_MAX_LEN = 128;
42
52
  const PATH_RE = /^[A-Za-z][A-Za-z0-9]*(\.[A-Za-z0-9_-]+)+$/;
43
53
  function isDynamicPath(path) {
@@ -46,6 +56,41 @@ function isDynamicPath(path) {
46
56
  PATH_RE.test(path) &&
47
57
  exports.DYNAMIC_PREFIXES.some((p) => path.startsWith(p)));
48
58
  }
59
+ const PRIORITY_LEAVES = new Set([
60
+ 'revolutions',
61
+ 'temperature',
62
+ 'oilPressure',
63
+ 'runTime',
64
+ 'fuel.rate',
65
+ 'alternatorVoltage',
66
+ 'engineLoad',
67
+ 'exhaustTemperature',
68
+ 'state',
69
+ 'voltage',
70
+ 'current',
71
+ 'frequency',
72
+ 'power',
73
+ 'realPower',
74
+ 'lineNeutralVoltage',
75
+ 'lineLineVoltage',
76
+ 'acin.voltage',
77
+ 'acin.current',
78
+ 'currentLevel',
79
+ 'capacity',
80
+ 'capacity.stateOfCharge',
81
+ 'capacity.timeRemaining',
82
+ 'chargingMode',
83
+ 'controllerMode',
84
+ 'inverterMode',
85
+ 'panelPower',
86
+ 'yieldToday'
87
+ ]);
88
+ function isPriorityPath(path) {
89
+ const seg = path.split('.');
90
+ if (PRIORITY_LEAVES.has(seg[seg.length - 1]))
91
+ return true;
92
+ return seg.length >= 2 && PRIORITY_LEAVES.has(`${seg[seg.length - 2]}.${seg[seg.length - 1]}`);
93
+ }
49
94
  const STRING_FIELDS = new Set(['nav_state', 'ais_class']);
50
95
  const TWS_SET = new Set(TWS_PATHS);
51
96
  class MetricsState {
@@ -82,8 +127,10 @@ class MetricsState {
82
127
  stored = value.slice(0, TEXT_MAX);
83
128
  else
84
129
  return false;
85
- if (!this.paths.has(path) && this.dynamicPathCount() >= MAX_DYNAMIC_PATHS)
86
- return false;
130
+ if (!this.paths.has(path) && this.dynamicPathCount() >= MAX_DYNAMIC_PATHS) {
131
+ if (!isPriorityPath(path) || !this.evictFiller())
132
+ return false;
133
+ }
87
134
  }
88
135
  else {
89
136
  if (typeof value !== 'number' || !Number.isFinite(value))
@@ -230,6 +277,25 @@ class MetricsState {
230
277
  n++;
231
278
  return n;
232
279
  }
280
+ evictFiller() {
281
+ let victim = null;
282
+ let victimTs = Infinity;
283
+ for (const [path, bySource] of this.paths) {
284
+ if (!isDynamicPath(path) || isPriorityPath(path))
285
+ continue;
286
+ let last = 0;
287
+ for (const entry of bySource.values())
288
+ last = Math.max(last, entry.ts);
289
+ if (last < victimTs) {
290
+ victimTs = last;
291
+ victim = path;
292
+ }
293
+ }
294
+ if (victim === null)
295
+ return false;
296
+ this.paths.delete(victim);
297
+ return true;
298
+ }
233
299
  dynamicPaths(now) {
234
300
  const out = {};
235
301
  for (const path of this.paths.keys()) {