@sixfathoms/lplex 0.2.0 → 0.3.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/README.md CHANGED
@@ -58,7 +58,7 @@ for (const d of devices) {
58
58
  }
59
59
  ```
60
60
 
61
- ### `client.values(signal?): Promise<DeviceValues[]>`
61
+ ### `client.values(filter?, signal?): Promise<DeviceValues[]>`
62
62
 
63
63
  Returns the last-seen value for each (device, PGN) pair, grouped by device. Useful for getting a snapshot of current bus state without subscribing to SSE.
64
64
 
@@ -72,6 +72,15 @@ for (const device of snapshot) {
72
72
  }
73
73
  ```
74
74
 
75
+ Pass a `Filter` to narrow results by PGN and/or device criteria:
76
+
77
+ ```typescript
78
+ const positions = await client.values({
79
+ pgn: [129025],
80
+ manufacturer: ["Garmin"],
81
+ });
82
+ ```
83
+
75
84
  ### `client.subscribe(filter?, signal?): Promise<AsyncIterable<Event>>`
76
85
 
77
86
  Opens an ephemeral SSE stream. No session state, no replay. Frames flow until you stop reading or abort.
@@ -326,7 +335,7 @@ interface DeviceValues {
326
335
  | `/clients/{id}/ack` | PUT | ACK sequence number. JSON body: `{ "seq": N }`. Returns 204. |
327
336
  | `/send` | POST | Transmit CAN frame. JSON body: `pgn`, `src`, `dst`, `prio`, `data`. Returns 202. |
328
337
  | `/devices` | GET | Device snapshot. Returns JSON array. |
329
- | `/values` | GET | Last-seen value per (device, PGN). Returns JSON array grouped by device. |
338
+ | `/values` | GET | Last-seen value per (device, PGN). Query params: `pgn`, `manufacturer`, `instance`, `name` (repeatable). Returns JSON array grouped by device. |
330
339
 
331
340
  ## License
332
341
 
package/dist/index.cjs CHANGED
@@ -171,8 +171,10 @@ var Client = class {
171
171
  return resp.json();
172
172
  }
173
173
  /** Fetch the last-seen value for each (device, PGN) pair. */
174
- async values(signal) {
175
- const url = `${this.#baseURL}/values`;
174
+ async values(filter, signal) {
175
+ let url = `${this.#baseURL}/values`;
176
+ const qs = filterToQueryString(filter);
177
+ if (qs) url += `?${qs}`;
176
178
  const resp = await this.#fetch(url, { signal });
177
179
  if (!resp.ok) {
178
180
  const body = await resp.text();
@@ -201,6 +203,18 @@ var Client = class {
201
203
  }
202
204
  return parseSSE(resp.body);
203
205
  }
206
+ /** Fetch the last-seen decoded values for each (device, PGN) pair. */
207
+ async decodedValues(filter, signal) {
208
+ let url = `${this.#baseURL}/values/decoded`;
209
+ const qs = filterToQueryString(filter);
210
+ if (qs) url += `?${qs}`;
211
+ const resp = await this.#fetch(url, { signal });
212
+ if (!resp.ok) {
213
+ const body = await resp.text();
214
+ throw new HttpError("GET", url, resp.status, body);
215
+ }
216
+ return resp.json();
217
+ }
204
218
  /** Transmit a CAN frame through the server. */
205
219
  async send(params, signal) {
206
220
  const url = `${this.#baseURL}/send`;
@@ -215,6 +229,44 @@ var Client = class {
215
229
  throw new HttpError("POST", url, resp.status, body);
216
230
  }
217
231
  }
232
+ /**
233
+ * Send an ISO Request (PGN 59904) and wait for the response frame.
234
+ * Returns the response frame, or throws HttpError with status 504 on timeout.
235
+ */
236
+ async query(params, signal) {
237
+ const url = `${this.#baseURL}/query`;
238
+ const resp = await this.#fetch(url, {
239
+ method: "POST",
240
+ headers: { "Content-Type": "application/json" },
241
+ body: JSON.stringify(params),
242
+ signal
243
+ });
244
+ if (!resp.ok) {
245
+ const body = await resp.text();
246
+ throw new HttpError("POST", url, resp.status, body);
247
+ }
248
+ return resp.json();
249
+ }
250
+ /** Check server health. */
251
+ async health(signal) {
252
+ const url = `${this.#baseURL}/healthz`;
253
+ const resp = await this.#fetch(url, { signal });
254
+ if (!resp.ok) {
255
+ const body = await resp.text();
256
+ throw new HttpError("GET", url, resp.status, body);
257
+ }
258
+ return resp.json();
259
+ }
260
+ /** Fetch boat-side replication status (only available when replication is configured). */
261
+ async replicationStatus(signal) {
262
+ const url = `${this.#baseURL}/replication/status`;
263
+ const resp = await this.#fetch(url, { signal });
264
+ if (!resp.ok) {
265
+ const body = await resp.text();
266
+ throw new HttpError("GET", url, resp.status, body);
267
+ }
268
+ return resp.json();
269
+ }
218
270
  /** Create or reconnect a buffered session on the server. */
219
271
  async createSession(config, signal) {
220
272
  const url = `${this.#baseURL}/clients/${config.clientId}`;
@@ -239,23 +291,28 @@ var Client = class {
239
291
  }
240
292
  };
241
293
  function filterIsEmpty(f) {
242
- return !f.pgn?.length && !f.manufacturer?.length && !f.instance?.length && !f.name?.length;
294
+ return !f.pgn?.length && !f.exclude_pgn?.length && !f.manufacturer?.length && !f.instance?.length && !f.name?.length && !f.exclude_name?.length;
243
295
  }
244
296
  function filterToQueryString(f) {
245
297
  if (!f || filterIsEmpty(f)) return "";
246
298
  const params = new URLSearchParams();
247
299
  for (const p of f.pgn ?? []) params.append("pgn", p.toString());
300
+ for (const p of f.exclude_pgn ?? [])
301
+ params.append("exclude_pgn", p.toString());
248
302
  for (const m of f.manufacturer ?? []) params.append("manufacturer", m);
249
303
  for (const i of f.instance ?? []) params.append("instance", i.toString());
250
304
  for (const n of f.name ?? []) params.append("name", n);
305
+ for (const n of f.exclude_name ?? []) params.append("exclude_name", n);
251
306
  return params.toString();
252
307
  }
253
308
  function filterToJSON(f) {
254
309
  const m = {};
255
310
  if (f.pgn?.length) m.pgn = f.pgn;
311
+ if (f.exclude_pgn?.length) m.exclude_pgn = f.exclude_pgn;
256
312
  if (f.manufacturer?.length) m.manufacturer = f.manufacturer;
257
313
  if (f.instance?.length) m.instance = f.instance;
258
314
  if (f.name?.length) m.name = f.name;
315
+ if (f.exclude_name?.length) m.exclude_name = f.exclude_name;
259
316
  return m;
260
317
  }
261
318
 
package/dist/index.d.cts CHANGED
@@ -42,9 +42,11 @@ type Event = {
42
42
  */
43
43
  interface Filter {
44
44
  pgn?: number[];
45
+ exclude_pgn?: number[];
45
46
  manufacturer?: string[];
46
47
  instance?: number[];
47
48
  name?: string[];
49
+ exclude_name?: string[];
48
50
  }
49
51
  /** Configuration for creating a buffered session. */
50
52
  interface SessionConfig {
@@ -82,6 +84,43 @@ interface DeviceValues {
82
84
  model_id?: string;
83
85
  values: PGNValue[];
84
86
  }
87
+ /** A single PGN's decoded value for a device. */
88
+ interface DecodedPGNValue {
89
+ pgn: number;
90
+ ts: string;
91
+ data: string;
92
+ seq: number;
93
+ decoded: Record<string, unknown>;
94
+ }
95
+ /** Decoded values grouped by device. */
96
+ interface DecodedDeviceValues {
97
+ name: string;
98
+ src: number;
99
+ manufacturer?: string;
100
+ model_id?: string;
101
+ values: DecodedPGNValue[];
102
+ }
103
+ /** Parameters for an ISO Request query (POST /query). */
104
+ interface QueryParams {
105
+ pgn: number;
106
+ dst: number;
107
+ timeout?: string;
108
+ }
109
+ /** Health check response from GET /healthz. */
110
+ interface HealthStatus {
111
+ status: string;
112
+ }
113
+ /** Boat-side replication status from GET /replication/status. */
114
+ interface ReplicationStatus {
115
+ connected: boolean;
116
+ instance_id: string;
117
+ local_head_seq: number;
118
+ cloud_cursor: number;
119
+ holes: SeqRange[];
120
+ live_lag: number;
121
+ backfill_remaining_seqs: number;
122
+ last_ack: string;
123
+ }
85
124
  /** Summary of a cloud instance, returned by GET /instances. */
86
125
  interface InstanceSummary {
87
126
  id: string;
@@ -143,14 +182,25 @@ declare class Client {
143
182
  /** Fetch a snapshot of all NMEA 2000 devices discovered by the server. */
144
183
  devices(signal?: AbortSignal): Promise<Device[]>;
145
184
  /** Fetch the last-seen value for each (device, PGN) pair. */
146
- values(signal?: AbortSignal): Promise<DeviceValues[]>;
185
+ values(filter?: Filter, signal?: AbortSignal): Promise<DeviceValues[]>;
147
186
  /**
148
187
  * Open an ephemeral SSE stream with optional filtering.
149
188
  * No session, no replay, no ACK.
150
189
  */
151
190
  subscribe(filter?: Filter, signal?: AbortSignal): Promise<AsyncIterable<Event>>;
191
+ /** Fetch the last-seen decoded values for each (device, PGN) pair. */
192
+ decodedValues(filter?: Filter, signal?: AbortSignal): Promise<DecodedDeviceValues[]>;
152
193
  /** Transmit a CAN frame through the server. */
153
194
  send(params: SendParams, signal?: AbortSignal): Promise<void>;
195
+ /**
196
+ * Send an ISO Request (PGN 59904) and wait for the response frame.
197
+ * Returns the response frame, or throws HttpError with status 504 on timeout.
198
+ */
199
+ query(params: QueryParams, signal?: AbortSignal): Promise<Frame>;
200
+ /** Check server health. */
201
+ health(signal?: AbortSignal): Promise<HealthStatus>;
202
+ /** Fetch boat-side replication status (only available when replication is configured). */
203
+ replicationStatus(signal?: AbortSignal): Promise<ReplicationStatus>;
154
204
  /** Create or reconnect a buffered session on the server. */
155
205
  createSession(config: SessionConfig, signal?: AbortSignal): Promise<Session>;
156
206
  }
@@ -191,4 +241,4 @@ declare class HttpError extends LplexError {
191
241
  constructor(method: string, path: string, status: number, body: string);
192
242
  }
193
243
 
194
- export { Client, type ClientOptions, CloudClient, type CloudClientOptions, type Device, type DeviceValues, type Event, type Filter, type Frame, HttpError, type InstanceStatus, type InstanceSummary, LplexError, type PGNValue, type ReplicationEvent, type ReplicationEventType, type SendParams, type SeqRange, Session, type SessionConfig, type SessionInfo };
244
+ export { Client, type ClientOptions, CloudClient, type CloudClientOptions, type DecodedDeviceValues, type DecodedPGNValue, type Device, type DeviceValues, type Event, type Filter, type Frame, type HealthStatus, HttpError, type InstanceStatus, type InstanceSummary, LplexError, type PGNValue, type QueryParams, type ReplicationEvent, type ReplicationEventType, type ReplicationStatus, type SendParams, type SeqRange, Session, type SessionConfig, type SessionInfo };
package/dist/index.d.ts CHANGED
@@ -42,9 +42,11 @@ type Event = {
42
42
  */
43
43
  interface Filter {
44
44
  pgn?: number[];
45
+ exclude_pgn?: number[];
45
46
  manufacturer?: string[];
46
47
  instance?: number[];
47
48
  name?: string[];
49
+ exclude_name?: string[];
48
50
  }
49
51
  /** Configuration for creating a buffered session. */
50
52
  interface SessionConfig {
@@ -82,6 +84,43 @@ interface DeviceValues {
82
84
  model_id?: string;
83
85
  values: PGNValue[];
84
86
  }
87
+ /** A single PGN's decoded value for a device. */
88
+ interface DecodedPGNValue {
89
+ pgn: number;
90
+ ts: string;
91
+ data: string;
92
+ seq: number;
93
+ decoded: Record<string, unknown>;
94
+ }
95
+ /** Decoded values grouped by device. */
96
+ interface DecodedDeviceValues {
97
+ name: string;
98
+ src: number;
99
+ manufacturer?: string;
100
+ model_id?: string;
101
+ values: DecodedPGNValue[];
102
+ }
103
+ /** Parameters for an ISO Request query (POST /query). */
104
+ interface QueryParams {
105
+ pgn: number;
106
+ dst: number;
107
+ timeout?: string;
108
+ }
109
+ /** Health check response from GET /healthz. */
110
+ interface HealthStatus {
111
+ status: string;
112
+ }
113
+ /** Boat-side replication status from GET /replication/status. */
114
+ interface ReplicationStatus {
115
+ connected: boolean;
116
+ instance_id: string;
117
+ local_head_seq: number;
118
+ cloud_cursor: number;
119
+ holes: SeqRange[];
120
+ live_lag: number;
121
+ backfill_remaining_seqs: number;
122
+ last_ack: string;
123
+ }
85
124
  /** Summary of a cloud instance, returned by GET /instances. */
86
125
  interface InstanceSummary {
87
126
  id: string;
@@ -143,14 +182,25 @@ declare class Client {
143
182
  /** Fetch a snapshot of all NMEA 2000 devices discovered by the server. */
144
183
  devices(signal?: AbortSignal): Promise<Device[]>;
145
184
  /** Fetch the last-seen value for each (device, PGN) pair. */
146
- values(signal?: AbortSignal): Promise<DeviceValues[]>;
185
+ values(filter?: Filter, signal?: AbortSignal): Promise<DeviceValues[]>;
147
186
  /**
148
187
  * Open an ephemeral SSE stream with optional filtering.
149
188
  * No session, no replay, no ACK.
150
189
  */
151
190
  subscribe(filter?: Filter, signal?: AbortSignal): Promise<AsyncIterable<Event>>;
191
+ /** Fetch the last-seen decoded values for each (device, PGN) pair. */
192
+ decodedValues(filter?: Filter, signal?: AbortSignal): Promise<DecodedDeviceValues[]>;
152
193
  /** Transmit a CAN frame through the server. */
153
194
  send(params: SendParams, signal?: AbortSignal): Promise<void>;
195
+ /**
196
+ * Send an ISO Request (PGN 59904) and wait for the response frame.
197
+ * Returns the response frame, or throws HttpError with status 504 on timeout.
198
+ */
199
+ query(params: QueryParams, signal?: AbortSignal): Promise<Frame>;
200
+ /** Check server health. */
201
+ health(signal?: AbortSignal): Promise<HealthStatus>;
202
+ /** Fetch boat-side replication status (only available when replication is configured). */
203
+ replicationStatus(signal?: AbortSignal): Promise<ReplicationStatus>;
154
204
  /** Create or reconnect a buffered session on the server. */
155
205
  createSession(config: SessionConfig, signal?: AbortSignal): Promise<Session>;
156
206
  }
@@ -191,4 +241,4 @@ declare class HttpError extends LplexError {
191
241
  constructor(method: string, path: string, status: number, body: string);
192
242
  }
193
243
 
194
- export { Client, type ClientOptions, CloudClient, type CloudClientOptions, type Device, type DeviceValues, type Event, type Filter, type Frame, HttpError, type InstanceStatus, type InstanceSummary, LplexError, type PGNValue, type ReplicationEvent, type ReplicationEventType, type SendParams, type SeqRange, Session, type SessionConfig, type SessionInfo };
244
+ export { Client, type ClientOptions, CloudClient, type CloudClientOptions, type DecodedDeviceValues, type DecodedPGNValue, type Device, type DeviceValues, type Event, type Filter, type Frame, type HealthStatus, HttpError, type InstanceStatus, type InstanceSummary, LplexError, type PGNValue, type QueryParams, type ReplicationEvent, type ReplicationEventType, type ReplicationStatus, type SendParams, type SeqRange, Session, type SessionConfig, type SessionInfo };
package/dist/index.js CHANGED
@@ -141,8 +141,10 @@ var Client = class {
141
141
  return resp.json();
142
142
  }
143
143
  /** Fetch the last-seen value for each (device, PGN) pair. */
144
- async values(signal) {
145
- const url = `${this.#baseURL}/values`;
144
+ async values(filter, signal) {
145
+ let url = `${this.#baseURL}/values`;
146
+ const qs = filterToQueryString(filter);
147
+ if (qs) url += `?${qs}`;
146
148
  const resp = await this.#fetch(url, { signal });
147
149
  if (!resp.ok) {
148
150
  const body = await resp.text();
@@ -171,6 +173,18 @@ var Client = class {
171
173
  }
172
174
  return parseSSE(resp.body);
173
175
  }
176
+ /** Fetch the last-seen decoded values for each (device, PGN) pair. */
177
+ async decodedValues(filter, signal) {
178
+ let url = `${this.#baseURL}/values/decoded`;
179
+ const qs = filterToQueryString(filter);
180
+ if (qs) url += `?${qs}`;
181
+ const resp = await this.#fetch(url, { signal });
182
+ if (!resp.ok) {
183
+ const body = await resp.text();
184
+ throw new HttpError("GET", url, resp.status, body);
185
+ }
186
+ return resp.json();
187
+ }
174
188
  /** Transmit a CAN frame through the server. */
175
189
  async send(params, signal) {
176
190
  const url = `${this.#baseURL}/send`;
@@ -185,6 +199,44 @@ var Client = class {
185
199
  throw new HttpError("POST", url, resp.status, body);
186
200
  }
187
201
  }
202
+ /**
203
+ * Send an ISO Request (PGN 59904) and wait for the response frame.
204
+ * Returns the response frame, or throws HttpError with status 504 on timeout.
205
+ */
206
+ async query(params, signal) {
207
+ const url = `${this.#baseURL}/query`;
208
+ const resp = await this.#fetch(url, {
209
+ method: "POST",
210
+ headers: { "Content-Type": "application/json" },
211
+ body: JSON.stringify(params),
212
+ signal
213
+ });
214
+ if (!resp.ok) {
215
+ const body = await resp.text();
216
+ throw new HttpError("POST", url, resp.status, body);
217
+ }
218
+ return resp.json();
219
+ }
220
+ /** Check server health. */
221
+ async health(signal) {
222
+ const url = `${this.#baseURL}/healthz`;
223
+ const resp = await this.#fetch(url, { signal });
224
+ if (!resp.ok) {
225
+ const body = await resp.text();
226
+ throw new HttpError("GET", url, resp.status, body);
227
+ }
228
+ return resp.json();
229
+ }
230
+ /** Fetch boat-side replication status (only available when replication is configured). */
231
+ async replicationStatus(signal) {
232
+ const url = `${this.#baseURL}/replication/status`;
233
+ const resp = await this.#fetch(url, { signal });
234
+ if (!resp.ok) {
235
+ const body = await resp.text();
236
+ throw new HttpError("GET", url, resp.status, body);
237
+ }
238
+ return resp.json();
239
+ }
188
240
  /** Create or reconnect a buffered session on the server. */
189
241
  async createSession(config, signal) {
190
242
  const url = `${this.#baseURL}/clients/${config.clientId}`;
@@ -209,23 +261,28 @@ var Client = class {
209
261
  }
210
262
  };
211
263
  function filterIsEmpty(f) {
212
- return !f.pgn?.length && !f.manufacturer?.length && !f.instance?.length && !f.name?.length;
264
+ return !f.pgn?.length && !f.exclude_pgn?.length && !f.manufacturer?.length && !f.instance?.length && !f.name?.length && !f.exclude_name?.length;
213
265
  }
214
266
  function filterToQueryString(f) {
215
267
  if (!f || filterIsEmpty(f)) return "";
216
268
  const params = new URLSearchParams();
217
269
  for (const p of f.pgn ?? []) params.append("pgn", p.toString());
270
+ for (const p of f.exclude_pgn ?? [])
271
+ params.append("exclude_pgn", p.toString());
218
272
  for (const m of f.manufacturer ?? []) params.append("manufacturer", m);
219
273
  for (const i of f.instance ?? []) params.append("instance", i.toString());
220
274
  for (const n of f.name ?? []) params.append("name", n);
275
+ for (const n of f.exclude_name ?? []) params.append("exclude_name", n);
221
276
  return params.toString();
222
277
  }
223
278
  function filterToJSON(f) {
224
279
  const m = {};
225
280
  if (f.pgn?.length) m.pgn = f.pgn;
281
+ if (f.exclude_pgn?.length) m.exclude_pgn = f.exclude_pgn;
226
282
  if (f.manufacturer?.length) m.manufacturer = f.manufacturer;
227
283
  if (f.instance?.length) m.instance = f.instance;
228
284
  if (f.name?.length) m.name = f.name;
285
+ if (f.exclude_name?.length) m.exclude_name = f.exclude_name;
229
286
  return m;
230
287
  }
231
288
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sixfathoms/lplex",
3
- "version": "0.2.0",
3
+ "version": "0.3.0",
4
4
  "description": "TypeScript client for lplex CAN bus HTTP bridge",
5
5
  "type": "module",
6
6
  "main": "./dist/index.cjs",