@blueyerobotics/blueye-ts 3.4.0 → 3.6.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
@@ -1,6 +1,6 @@
1
1
  # blueye-ts
2
2
 
3
- A TypeScript client for interacting with Blueye underwater drones.
3
+ A TypeScript package for interacting with Blueye underwater drones and parsing binlog files.
4
4
 
5
5
  ## Installation
6
6
 
package/dist/index.d.ts CHANGED
@@ -1,3 +1,4 @@
1
1
  export * from "@blueyerobotics/protocol-definitions";
2
+ export * from "./src/binlog-parser";
2
3
  export * from "./src/client";
3
4
  export * from "./src/schema";
package/dist/index.js CHANGED
@@ -15,5 +15,6 @@ var __exportStar = (this && this.__exportStar) || function(m, exports) {
15
15
  };
16
16
  Object.defineProperty(exports, "__esModule", { value: true });
17
17
  __exportStar(require("@blueyerobotics/protocol-definitions"), exports);
18
+ __exportStar(require("./src/binlog-parser"), exports);
18
19
  __exportStar(require("./src/client"), exports);
19
20
  __exportStar(require("./src/schema"), exports);
@@ -0,0 +1,4 @@
1
+ export declare class AsyncQueue {
2
+ private lastPromise;
3
+ enqueue<T>(fn: () => Promise<T>): Promise<T>;
4
+ }
@@ -0,0 +1,12 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.AsyncQueue = void 0;
4
+ class AsyncQueue {
5
+ lastPromise = Promise.resolve();
6
+ enqueue(fn) {
7
+ const run = this.lastPromise.then(() => fn());
8
+ this.lastPromise = run.finally(() => { });
9
+ return run;
10
+ }
11
+ }
12
+ exports.AsyncQueue = AsyncQueue;
@@ -0,0 +1,38 @@
1
+ import { Buffer } from "buffer";
2
+ import { type Protocol, type ProtocolKey, type ProtocolType } from "./client";
3
+ export type Message = {
4
+ [K in ProtocolKey]: {
5
+ monotonicTime: number;
6
+ time: number;
7
+ type: ProtocolType;
8
+ key: K;
9
+ data: ReturnType<Protocol[K]["decode"]>;
10
+ innerData?: object;
11
+ };
12
+ }[ProtocolKey];
13
+ /**
14
+ * Parse a binlog file from raw (gzipped) data into structured messages..
15
+ * @param rawData The raw binary data of the binlog file (gzip format).
16
+ * @param fixTimes Fix the message times based on the last message's monotonic and unix timestamps. Useful for ensuring the times are in sync.
17
+ * @returns A promise that resolves to an array of parsed messages.
18
+ */
19
+ export declare const parse: (rawData: Blob, fixTimes?: boolean) => Promise<Message[]>;
20
+ /**
21
+ * Decompress the gzipped binlog data (.bez).
22
+ * @param rawData The compressed binary data to decompress (gzip format).
23
+ * @returns A promise that resolves to the decompressed data as a Buffer.
24
+ */
25
+ export declare const decompress: (rawData: Blob) => Promise<Buffer>;
26
+ /**
27
+ * Parse the decompressed binlog data into structured messages.
28
+ * @param decompressed The gunzipped binlog data.
29
+ * @param fixTimes Fix the message times based on the last message's monotonic and unix timestamps. Useful for ensuring the times are in sync.
30
+ * @returns An array of parsed messages with their timestamps, types, keys, and data.
31
+ */
32
+ export declare const parseMessages: (decompressed: Uint8Array, fixTimes?: boolean) => Message[];
33
+ /**
34
+ * Fix the message times based on the last message's monotonic and unix timestamps.
35
+ * @param messages The messages to fix the times for.
36
+ * @returns The messages with corrected times.
37
+ */
38
+ export declare const fixMessageTimes: (messages: Message[]) => Message[];
@@ -0,0 +1,125 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.fixMessageTimes = exports.parseMessages = exports.decompress = exports.parse = void 0;
4
+ const protocol_definitions_1 = require("@blueyerobotics/protocol-definitions");
5
+ const wire_1 = require("@bufbuild/protobuf/wire");
6
+ const buffer_1 = require("buffer");
7
+ const fflate_1 = require("fflate");
8
+ const client_1 = require("./client");
9
+ /**
10
+ * Parse a binlog file from raw (gzipped) data into structured messages..
11
+ * @param rawData The raw binary data of the binlog file (gzip format).
12
+ * @param fixTimes Fix the message times based on the last message's monotonic and unix timestamps. Useful for ensuring the times are in sync.
13
+ * @returns A promise that resolves to an array of parsed messages.
14
+ */
15
+ const parse = async (rawData, fixTimes = true) => {
16
+ const decompressed = await (0, exports.decompress)(rawData);
17
+ const messages = (0, exports.parseMessages)(decompressed, fixTimes);
18
+ return messages;
19
+ };
20
+ exports.parse = parse;
21
+ /**
22
+ * Decompress the gzipped binlog data (.bez).
23
+ * @param rawData The compressed binary data to decompress (gzip format).
24
+ * @returns A promise that resolves to the decompressed data as a Buffer.
25
+ */
26
+ const decompress = async (rawData) => {
27
+ const gunzip = new fflate_1.Gunzip();
28
+ const blobReader = rawData.stream().getReader();
29
+ const chunks = [];
30
+ gunzip.ondata = (chunk) => {
31
+ chunks.push(chunk);
32
+ };
33
+ while (true) {
34
+ const { done, value } = await blobReader.read();
35
+ if (done) {
36
+ try {
37
+ gunzip.push(new Uint8Array(0), true);
38
+ }
39
+ catch (err) {
40
+ console.error("Error pushing end-of-stream marker:", err);
41
+ }
42
+ break;
43
+ }
44
+ gunzip.push(value);
45
+ }
46
+ return buffer_1.Buffer.concat(chunks);
47
+ };
48
+ exports.decompress = decompress;
49
+ /**
50
+ * Parse the decompressed binlog data into structured messages.
51
+ * @param decompressed The gunzipped binlog data.
52
+ * @param fixTimes Fix the message times based on the last message's monotonic and unix timestamps. Useful for ensuring the times are in sync.
53
+ * @returns An array of parsed messages with their timestamps, types, keys, and data.
54
+ */
55
+ const parseMessages = (decompressed, fixTimes = true) => {
56
+ const reader = new wire_1.BinaryReader(decompressed);
57
+ let messages = [];
58
+ while (reader.pos < reader.len) {
59
+ const length = reader.uint32();
60
+ const start = reader.pos;
61
+ const end = start + length;
62
+ if (end > reader.len) {
63
+ console.error("Unexpected EOF while reading message bytes");
64
+ break;
65
+ }
66
+ const msgBytes = decompressed.buffer.slice(start, end);
67
+ reader.pos = end;
68
+ const msg = protocol_definitions_1.blueye.protocol.BinlogRecord.decode(new Uint8Array(msgBytes), length);
69
+ const key = msg.payload.typeUrl.split(".").at(-1);
70
+ if (!key || !(0, client_1.isInProtocol)(key)) {
71
+ console.warn(`Unknown protocol key: ${key}`);
72
+ continue;
73
+ }
74
+ const data = protocol_definitions_1.blueye.protocol[key].decode(msg.payload.value);
75
+ let innerData;
76
+ if (key === "GetTelemetryRep") {
77
+ const telRep = data;
78
+ const innerKey = telRep.payload?.typeUrl.split(".").at(-1);
79
+ if (!innerKey || !(0, client_1.isInProtocol)(innerKey)) {
80
+ console.warn(`Unknown inner protocol key: ${innerKey}`);
81
+ continue;
82
+ }
83
+ innerData = protocol_definitions_1.blueye.protocol[innerKey].decode(telRep.payload.value);
84
+ }
85
+ let type = "Tel";
86
+ if (key.endsWith("Ctrl"))
87
+ type = "Ctrl";
88
+ else if (key.endsWith("Rep"))
89
+ type = "Rep";
90
+ else if (key.endsWith("Req"))
91
+ type = "Req";
92
+ // @ts-expect-error 2345
93
+ messages.push({
94
+ monotonicTime: msg.clockMonotonic.getTime(),
95
+ time: msg.unixTimestamp.getTime(),
96
+ type,
97
+ key,
98
+ data,
99
+ innerData,
100
+ });
101
+ }
102
+ if (fixTimes) {
103
+ messages = (0, exports.fixMessageTimes)(messages);
104
+ }
105
+ return messages;
106
+ };
107
+ exports.parseMessages = parseMessages;
108
+ /**
109
+ * Fix the message times based on the last message's monotonic and unix timestamps.
110
+ * @param messages The messages to fix the times for.
111
+ * @returns The messages with corrected times.
112
+ */
113
+ const fixMessageTimes = (messages) => {
114
+ if (messages.length === 0)
115
+ return messages;
116
+ const last = messages.at(-1);
117
+ const ssbLast = last.monotonicTime;
118
+ const unixLast = last.time;
119
+ for (const message of messages) {
120
+ const delta = ssbLast - message.monotonicTime;
121
+ message.time = unixLast - delta;
122
+ }
123
+ return messages;
124
+ };
125
+ exports.fixMessageTimes = fixMessageTimes;
@@ -1,5 +1,5 @@
1
1
  import { blueye } from "@blueyerobotics/protocol-definitions";
2
- import { LogLevel } from "consola";
2
+ import { type LogLevel } from "consola";
3
3
  import { Emitter } from "strict-event-emitter";
4
4
  export type Protocol = typeof blueye.protocol;
5
5
  export type ProtocolType = "Req" | "Rep" | "Tel" | "Ctrl";
@@ -25,20 +25,23 @@ type Options = Partial<{
25
25
  rpcUrl: string;
26
26
  pubUrl: string;
27
27
  timeout: number;
28
+ reconnectInterval: number;
28
29
  logLevel: LogLevel;
29
30
  autoConnect: boolean;
30
31
  }>;
31
32
  export declare class BlueyeClient extends Emitter<Events> {
32
33
  state: State;
33
34
  timeout: number;
35
+ reconnectInterval: number;
34
36
  private subUrl;
35
37
  private rpcUrl;
36
38
  private pubUrl;
37
39
  private sub;
38
40
  private rpc;
39
41
  private pub;
42
+ private queue;
40
43
  private logger;
41
- constructor({ subUrl, rpcUrl, pubUrl, timeout, logLevel, autoConnect, }?: Options);
44
+ constructor({ subUrl, rpcUrl, pubUrl, timeout, reconnectInterval, logLevel, autoConnect, }?: Options);
42
45
  private updateState;
43
46
  connect(): void;
44
47
  disconnect(): void;
@@ -6,6 +6,7 @@ const buffer_1 = require("buffer");
6
6
  const consola_1 = require("consola");
7
7
  const jszmq_1 = require("jszmq");
8
8
  const strict_event_emitter_1 = require("strict-event-emitter");
9
+ const async_queue_1 = require("./async-queue");
9
10
  const schema_1 = require("./schema");
10
11
  const DEFAULT_SUB_URL = "ws://192.168.1.101:9985";
11
12
  const DEFAULT_RPC_URL = "ws://192.168.1.101:9986";
@@ -17,26 +18,30 @@ exports.isInProtocol = isInProtocol;
17
18
  class BlueyeClient extends strict_event_emitter_1.Emitter {
18
19
  state = "disconnected";
19
20
  timeout;
21
+ reconnectInterval;
20
22
  subUrl;
21
23
  rpcUrl;
22
24
  pubUrl;
23
25
  sub;
24
26
  rpc;
25
27
  pub;
28
+ queue;
26
29
  logger;
27
- constructor({ subUrl = DEFAULT_SUB_URL, rpcUrl = DEFAULT_RPC_URL, pubUrl = DEFAULT_PUB_URL, timeout = 2000, logLevel = consola_1.LogLevels.info, autoConnect = false, } = {}) {
30
+ constructor({ subUrl = DEFAULT_SUB_URL, rpcUrl = DEFAULT_RPC_URL, pubUrl = DEFAULT_PUB_URL, timeout = 2000, reconnectInterval = 2000, logLevel = consola_1.LogLevels.info, autoConnect = false, } = {}) {
28
31
  super();
29
32
  this.timeout = timeout;
30
- this.logger = (0, consola_1.createConsola)({
31
- level: logLevel,
32
- formatOptions: { colors: true, compact: false },
33
- });
33
+ this.reconnectInterval = reconnectInterval;
34
34
  this.subUrl = subUrl;
35
35
  this.rpcUrl = rpcUrl;
36
36
  this.pubUrl = pubUrl;
37
37
  this.sub = new jszmq_1.Sub();
38
38
  this.rpc = new jszmq_1.Req();
39
39
  this.pub = new jszmq_1.Pub();
40
+ this.queue = new async_queue_1.AsyncQueue();
41
+ this.logger = (0, consola_1.createConsola)({
42
+ level: logLevel,
43
+ formatOptions: { colors: true, compact: false },
44
+ });
40
45
  // @ts-ignore
41
46
  this.sub.on("message", (topic, msg) => {
42
47
  const { key, data } = schema_1.responseSchema.parse({ key: topic, data: msg });
@@ -67,6 +72,9 @@ class BlueyeClient extends strict_event_emitter_1.Emitter {
67
72
  this.logger.warn("[client] already connecting");
68
73
  return;
69
74
  }
75
+ this.sub.options.reconnectInterval = this.reconnectInterval;
76
+ this.rpc.options.reconnectInterval = this.reconnectInterval;
77
+ this.pub.options.reconnectInterval = this.reconnectInterval;
70
78
  this.updateState("connecting");
71
79
  this.sub.subscribe("");
72
80
  this.sub.connect(this.subUrl);
@@ -96,19 +104,21 @@ class BlueyeClient extends strict_event_emitter_1.Emitter {
96
104
  const protocol = protocol_definitions_1.blueye.protocol[req];
97
105
  const message = protocol.create(opts);
98
106
  const encoded = protocol.encode(message).finish();
99
- const { key, data } = await Promise.race([
100
- new Promise((_, reject) => setTimeout(() => reject(new Error("[rpc] request timed out")), this.timeout)),
101
- new Promise((resolve) => {
107
+ const request = () => {
108
+ return new Promise((resolve, reject) => {
109
+ const timer = setTimeout(() => reject(new Error("[rpc] request timed out")), this.timeout);
102
110
  // @ts-ignore
103
111
  this.rpc.once("message", (topic, msg) => {
112
+ clearTimeout(timer);
104
113
  resolve({ key: topic.toString().split(".").at(-1), data: msg });
105
114
  });
106
115
  this.rpc.send([
107
116
  buffer_1.Buffer.from(`blueye.protocol.${req}`),
108
117
  buffer_1.Buffer.from(encoded),
109
118
  ]);
110
- }),
111
- ]);
119
+ });
120
+ };
121
+ const { key, data } = await this.queue.enqueue(request);
112
122
  if (key === "Empty") {
113
123
  return null;
114
124
  }
@@ -1,34 +1,12 @@
1
1
  import { Buffer } from "buffer";
2
2
  import z from "zod";
3
3
  export declare const responseSchema: z.ZodObject<{
4
- key: z.ZodEffects<z.ZodType<Buffer, z.ZodTypeDef, Buffer>, string, Buffer>;
5
- data: z.ZodType<Buffer, z.ZodTypeDef, Buffer>;
6
- }, "strip", z.ZodTypeAny, {
7
- key: string;
8
- data: Buffer;
9
- }, {
10
- key: Buffer;
11
- data: Buffer;
12
- }>;
4
+ key: z.ZodPipe<z.ZodCustom<Buffer, Buffer>, z.ZodTransform<string, Buffer>>;
5
+ data: z.ZodCustom<Buffer, Buffer>;
6
+ }, z.core.$strip>;
13
7
  export declare const telemetrySchema: z.ZodObject<{
14
8
  payload: z.ZodObject<{
15
- typeUrl: z.ZodEffects<z.ZodString, string, string>;
16
- value: z.ZodEffects<z.ZodEffects<z.ZodAny, Buffer | Uint8Array<ArrayBufferLike>, any>, Buffer, any>;
17
- }, "strip", z.ZodTypeAny, {
18
- value: Buffer;
19
- typeUrl: string;
20
- }, {
21
- typeUrl: string;
22
- value?: any;
23
- }>;
24
- }, "strip", z.ZodTypeAny, {
25
- payload: {
26
- value: Buffer;
27
- typeUrl: string;
28
- };
29
- }, {
30
- payload: {
31
- typeUrl: string;
32
- value?: any;
33
- };
34
- }>;
9
+ typeUrl: z.ZodPipe<z.ZodString, z.ZodTransform<string, string>>;
10
+ value: z.ZodPipe<z.ZodAny, z.ZodTransform<Buffer, any>>;
11
+ }, z.core.$strip>;
12
+ }, z.core.$strip>;
@@ -7,15 +7,17 @@ exports.telemetrySchema = exports.responseSchema = void 0;
7
7
  const buffer_1 = require("buffer");
8
8
  const zod_1 = __importDefault(require("zod"));
9
9
  exports.responseSchema = zod_1.default.object({
10
- key: zod_1.default.instanceof(buffer_1.Buffer).transform(val => val.toString().split(".").at(-1)),
11
- data: zod_1.default.instanceof(buffer_1.Buffer)
10
+ key: zod_1.default
11
+ .instanceof(buffer_1.Buffer)
12
+ .transform((val) => val.toString().split(".").at(-1)),
13
+ data: zod_1.default.instanceof(buffer_1.Buffer),
12
14
  });
13
15
  exports.telemetrySchema = zod_1.default.object({
14
16
  payload: zod_1.default.object({
15
- typeUrl: zod_1.default.string().transform(val => val.split(".").at(-1)),
17
+ typeUrl: zod_1.default.string().transform((val) => val.split(".").at(-1)),
16
18
  value: zod_1.default
17
19
  .any()
18
- .refine(val => val instanceof buffer_1.Buffer || val instanceof Uint8Array)
19
- .transform(val => (buffer_1.Buffer.isBuffer(val) ? val : buffer_1.Buffer.from(val)))
20
- })
20
+ .refine((val) => val instanceof buffer_1.Buffer || val instanceof Uint8Array)
21
+ .transform((val) => (buffer_1.Buffer.isBuffer(val) ? val : buffer_1.Buffer.from(val))),
22
+ }),
21
23
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@blueyerobotics/blueye-ts",
3
- "version": "3.4.0",
3
+ "version": "3.6.0",
4
4
  "description": "A TypeScript client for interacting with Blueye underwater drones.",
5
5
  "repository": {
6
6
  "type": "git",
@@ -16,14 +16,17 @@
16
16
  "license": "LGPL-3.0-only",
17
17
  "dependencies": {
18
18
  "@blueyerobotics/protocol-definitions": "3.2.0-b1918def",
19
+ "@bufbuild/protobuf": "^2.10.0",
19
20
  "buffer": "^6.0.3",
20
21
  "consola": "^3.4.2",
22
+ "fflate": "^0.8.2",
21
23
  "jszmq": "^0.1.2",
22
24
  "strict-event-emitter": "^0.5.1",
23
- "zod": "^3.25.67"
25
+ "zod": "^4.1.12"
24
26
  },
25
27
  "devDependencies": {
26
- "typescript": "^5.9.2"
28
+ "@biomejs/biome": "2.3.2",
29
+ "typescript": "^5.9.3"
27
30
  },
28
31
  "scripts": {
29
32
  "start": "node dist/example.js",