@v5x/serial 0.5.6 → 0.5.7

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
@@ -29,7 +29,10 @@ if (opened.isErr()) {
29
29
  console.error(opened.error); // VexSerialError with a stable .kind
30
30
  return;
31
31
  }
32
- // opened.value: true | false | undefined (opened / busy / no port selected)
32
+ if (opened.value !== "opened") {
33
+ console.error(`Unable to open a compatible port: ${opened.value}`);
34
+ return;
35
+ }
33
36
 
34
37
  const status = await connection.getSystemStatus();
35
38
  if (status.isOk()) {
@@ -81,9 +84,9 @@ removed. Migrate to the corresponding `setMatchMode()` / `setActiveProgram()`
81
84
  methods and handle the returned `Result`.
82
85
 
83
86
  Legacy methods that resolved to `false`, `null`, or `undefined` on failure now
84
- return a typed `Err` instead, preserving the prior success value semantics on
85
- the `Ok` channel (e.g. `open()` still resolves to `Ok(true | false |
86
- undefined)`).
87
+ return a typed `Err` instead. `open()` reports non-error connection outcomes on
88
+ the `Ok` channel as `"opened"`, `"busy"`, or `"no-port"`; only `"opened"`
89
+ indicates an established connection.
87
90
 
88
91
  ## Transfers and timeouts
89
92
 
@@ -112,3 +115,33 @@ bun run build
112
115
  ```
113
116
 
114
117
  The build emits JavaScript bundles and TypeScript declarations under `dist/`.
118
+
119
+ ### Packet registry and browser bundle measurement
120
+
121
+ The complete entry point registers every reply class explicitly. Packet
122
+ decoding no longer discovers classes by enumerating the packet-model module at
123
+ runtime. Consumers that only need framing, CRC, packet base classes, and a
124
+ custom reply registry can import `@v5x/serial/packet-core`; that entry point
125
+ does not retain file-transfer, firmware, screenshot, or dashboard models.
126
+ Register only the reply classes the application accepts with
127
+ `PacketEncoder.getInstance().registerPacketTypes(...)`.
128
+
129
+ Run `bun run measure:bundles` in this package to report reproducible minified
130
+ byte sizes for the complete browser entry and the packet-core entry.
131
+
132
+ ### File-transfer window semantics
133
+
134
+ The `windowSize` returned by `InitFileTransferReplyD2HPacket` is the maximum
135
+ data size for one `ReadFile` or `WriteFile` block. It is not a count of blocks
136
+ that may be outstanding. This follows from the wire model: read and write
137
+ replies carry only the shared CDC2 command and function IDs; write replies have
138
+ no address, sequence number, or request identifier with which to correlate an
139
+ individual outstanding block. A read address can validate a reply after it is
140
+ routed, but cannot make write failures unambiguous.
141
+
142
+ Transfers therefore intentionally remain stop-and-wait. The advertised value
143
+ is clamped to the library's 4096-byte block limit and used only as the chunk
144
+ size. There is no safe before/after pipelining benchmark to run because the
145
+ protocol does not expose the correlation needed to enable pipelining without
146
+ risking data corruption. Hardware throughput remains dependent on one USB
147
+ round trip per block.
package/dist/Vex.d.ts CHANGED
@@ -1,3 +1,4 @@
1
+ /// <reference types="dom-serial" preserve="true" />
1
2
  import { type VexFirmwareVersion } from "./VexFirmwareVersion.js";
2
3
  import { type HostBoundPacket } from "./VexPacket.js";
3
4
  export declare const USER_PROG_CHUNK_SIZE = 4096;
@@ -134,7 +135,8 @@ export declare enum AckType {
134
135
  CDC2_NACK_FILE_EXISTS = 219,
135
136
  CDC2_NACK_FILE_SYS_FULL = 220,
136
137
  TIMEOUT = 256,
137
- WRITE_ERROR = 257
138
+ WRITE_ERROR = 257,
139
+ NOT_CONNECTED = 258
138
140
  }
139
141
  export declare enum SmartDeviceType {
140
142
  EMPTY = 0,
@@ -1,3 +1,4 @@
1
+ /// <reference types="dom-serial" preserve="true" />
1
2
  import { AckType, FileDownloadTarget, type IFileBasicInfo, type IFileWriteRequest, type IPacketCallback, type MatchMode, type SlotNumber, type SelectDashScreen } from "./Vex.js";
2
3
  import { VexSerialError } from "./VexError.js";
3
4
  import { VexEventTarget } from "./VexEvent.js";
@@ -34,17 +35,29 @@ export declare class VexSerialConnection extends VexEventTarget<VexSerialConnect
34
35
  reader: ReadableStreamDefaultReader<unknown> | undefined;
35
36
  port: SerialPort | undefined;
36
37
  serial: Serial;
37
- callbacksQueue: IPacketCallback[];
38
+ private pendingCallbacks;
39
+ private rawCallbacks;
40
+ private pendingCommandTails;
38
41
  private _onPortDisconnect;
39
42
  private _closePromise;
43
+ private _openPromise;
44
+ private _isClosing;
40
45
  private _wasConnected;
41
46
  protected fileTransferTail: Promise<unknown>;
42
47
  protected fileTransferDepth: number;
48
+ /** Pending callbacks, exposed as a snapshot for backwards compatibility. */
49
+ get callbacksQueue(): IPacketCallback[];
43
50
  get isConnected(): boolean;
44
51
  get isFileTransferring(): boolean;
45
52
  constructor(serial: Serial);
46
53
  protected reportWarning(message: string, details?: unknown): void;
54
+ /**
55
+ * Connection events are notifications only: a consumer callback must not
56
+ * alter the lifecycle of the serial transport that produced it.
57
+ */
58
+ private emitSafely;
47
59
  close(): Promise<void>;
60
+ private _closeAfterOpen;
48
61
  private _hasOpenResources;
49
62
  private _doClose;
50
63
  /**
@@ -52,14 +65,31 @@ export declare class VexSerialConnection extends VexEventTarget<VexSerialConnect
52
65
  * `"busy"` when the matching port is already held elsewhere, and
53
66
  * `"no-port"` when no matching port was selected. The result is `Err`
54
67
  * when a connection is already open (programmer error) or when the
55
- * port fails to open (permissions, dead device, ...).
68
+ * port fails to open (permissions, dead device, ...). Concurrent calls
69
+ * join the same open attempt and receive its result.
56
70
  */
57
71
  open(use?: number, askUser?: boolean): ResultAsync<OpenResult, VexSerialError>;
58
72
  private _open;
59
73
  writeDataAsync(rawData: DeviceBoundPacket | Uint8Array, timeout?: number): Promise<HostBoundPacket | ArrayBuffer | AckType>;
74
+ /**
75
+ * A command ID identifies a reply packet type, not an individual request.
76
+ * Keep requests with the same reply identifiers off the wire until the
77
+ * preceding request has resolved, while allowing unrelated commands to run
78
+ * concurrently.
79
+ */
80
+ private serializeCommand;
81
+ private writeDataAsyncUnserialized;
60
82
  request<T extends HostBoundPacket>(packet: DeviceBoundPacket, ReplyType: HostBoundPacketType<T>, timeout?: number): ResultAsync<T, VexSerialError>;
61
- protected readData(cache: Uint8Array, expectedSize: number): Promise<Uint8Array>;
83
+ protected readData(cache: ReceiveBuffer, expectedSize: number): Promise<void>;
62
84
  protected startReader(): Promise<void>;
85
+ private pendingKey;
86
+ private getPendingQueue;
87
+ private enqueuePendingCallback;
88
+ private removePendingCallback;
89
+ private shiftPendingCallback;
90
+ private shiftRawCallback;
91
+ private hasPendingCallbacks;
92
+ private drainPendingCallbacks;
63
93
  query1(): ResultAsync<Query1ReplyD2HPacket, VexSerialError>;
64
94
  getSystemVersion(): ResultAsync<VexFirmwareVersion, VexSerialError>;
65
95
  }
@@ -122,4 +152,19 @@ export declare class V5SerialConnection extends VexSerialConnection {
122
152
  mockTouch(x: number, y: number, press: boolean): ResultAsync<SendDashTouchReplyD2HPacket, VexSerialError>;
123
153
  openScreen(screen: number | SelectDashScreen, port: number): ResultAsync<SelectDashReplyD2HPacket, VexSerialError>;
124
154
  }
155
+ /**
156
+ * A growable receive queue. Parsed packets are copied out before their bytes
157
+ * are discarded, so views into this reusable storage never escape the reader.
158
+ */
159
+ declare class ReceiveBuffer {
160
+ private storage;
161
+ private start;
162
+ private end;
163
+ get byteLength(): number;
164
+ get bytes(): Uint8Array<ArrayBuffer>;
165
+ append(chunk: Uint8Array): void;
166
+ copy(length: number): Uint8Array<ArrayBuffer>;
167
+ discard(length: number): void;
168
+ private reserve;
169
+ }
125
170
  export {};
@@ -1,3 +1,4 @@
1
+ /// <reference types="dom-serial" preserve="true" />
1
2
  import { type MatchMode } from "./Vex.js";
2
3
  import { V5SerialConnection } from "./VexConnection.js";
3
4
  import { V5Brain, V5Controller, V5Radio, V5SerialDeviceState, V5SmartDevice, VexSerialDevice } from "./VexDeviceState.js";
@@ -5,6 +6,10 @@ import { VexSerialError } from "./VexError.js";
5
6
  import { ResultAsync } from "neverthrow";
6
7
  export { VexSerialDevice, V5Brain, V5Battery, V5BrainButton, V5BrainSettings, V5Controller, V5SmartDevice, V5Radio, V5SerialDeviceState, } from "./VexDeviceState.js";
7
8
  export { sleep, sleepUntil, sleepUntilAsync, downloadFileFromInternet, uploadFirmware, } from "./VexFirmware.js";
9
+ export interface V5SerialDeviceOptions {
10
+ autoRefresh?: boolean;
11
+ refreshIntervalMs?: number;
12
+ }
8
13
  export declare class V5SerialDevice extends VexSerialDevice {
9
14
  autoReconnect: boolean;
10
15
  pauseRefreshOnFileTransfer: boolean;
@@ -13,16 +18,26 @@ export declare class V5SerialDevice extends VexSerialDevice {
13
18
  private _refreshInterval;
14
19
  state: V5SerialDeviceState;
15
20
  private _disposed;
21
+ private _lifecycleGeneration;
22
+ private _disconnectListener;
16
23
  private _refreshGeneration;
17
24
  private _autoRefresh;
25
+ private _refreshIntervalMs;
18
26
  private _isLastRefreshComplete;
19
27
  private readonly _brain;
20
28
  private readonly _controllers;
21
29
  private readonly _radio;
22
30
  private readonly _deviceFacades;
23
- constructor(defaultSerial: Serial, autoRefresh?: boolean);
31
+ /**
32
+ * Device lifecycle events are notifications only: consumer callbacks must
33
+ * not alter automatic refresh or reconnect work that produced them.
34
+ */
35
+ private _emitSafely;
36
+ constructor(defaultSerial: Serial, options?: boolean | V5SerialDeviceOptions);
24
37
  get autoRefresh(): boolean;
25
38
  set autoRefresh(value: boolean);
39
+ get refreshIntervalMs(): number;
40
+ set refreshIntervalMs(value: number);
26
41
  private _startRefreshInterval;
27
42
  private _stopRefreshInterval;
28
43
  get isV5Controller(): boolean;
@@ -56,7 +71,13 @@ export declare class V5SerialDevice extends VexSerialDevice {
56
71
  reconnect(timeout?: number): ResultAsync<void, VexSerialError>;
57
72
  private _reconnect;
58
73
  private waitForReconnectToFinish;
74
+ protected createConnection(): V5SerialConnection;
75
+ private _isLifecycleCurrent;
76
+ private _staleLifecycleResult;
77
+ private _commitConnection;
78
+ private _detachDisconnectListener;
59
79
  private doAfterConnect;
80
+ private _discardConnection;
60
81
  /**
61
82
  * Refresh the high-level device snapshot. All required replies are
62
83
  * collected before any public state is mutated, so callers never see
@@ -1,3 +1,4 @@
1
+ /// <reference types="dom-serial" preserve="true" />
1
2
  import { type MatchMode, SerialDeviceType, type SlotNumber, type ISmartDeviceInfo, SmartDeviceType, FileVendor, type IProgramInfo, type IFileHandle, type IFileBasicInfo, type IFileWriteRequest, FileDownloadTarget, RadioChannelType } from "./Vex.js";
2
3
  import { type V5SerialConnection } from "./VexConnection.js";
3
4
  import { VexEventTarget } from "./VexEvent.js";
@@ -64,11 +65,18 @@ export declare class V5SerialDeviceState {
64
65
  systemVersion: VexFirmwareVersion;
65
66
  uniqueId: number;
66
67
  };
67
- controllers: {
68
- battery: number;
69
- isAvailable: boolean;
70
- isCharging: boolean;
71
- }[];
68
+ controllers: [
69
+ {
70
+ battery: number;
71
+ isAvailable: boolean;
72
+ isCharging: boolean | undefined;
73
+ },
74
+ {
75
+ battery: number;
76
+ isAvailable: boolean;
77
+ isCharging: boolean | undefined;
78
+ }
79
+ ];
72
80
  devices: Array<ISmartDeviceInfo | undefined>;
73
81
  isFieldControllerConnected: boolean;
74
82
  matchMode: MatchMode;
@@ -169,6 +177,11 @@ export declare class V5Controller {
169
177
  get batteryPercent(): number;
170
178
  get isMasterController(): boolean;
171
179
  get isAvailable(): boolean;
180
+ /**
181
+ * Whether the controller is charging. The V5 system-status response only
182
+ * reports this state for the primary controller, so the partner controller
183
+ * returns `undefined`.
184
+ */
172
185
  get isCharging(): boolean | undefined;
173
186
  }
174
187
  export declare class V5SmartDevice {
@@ -1,3 +1,4 @@
1
+ /// <reference types="dom-serial" preserve="true" />
1
2
  import { type AckType } from "./Vex.js";
2
3
  /**
3
4
  * Typed error hierarchy for the serial protocol package.
@@ -16,6 +16,5 @@ export declare class VexEventTarget<TEvents extends object = Record<EventName, u
16
16
  on<K extends EventMapKey<TEvents>>(eventName: K, listener: EventListener<TEvents[K]>): void;
17
17
  remove<K extends EventMapKey<TEvents>>(eventName: K, listener: EventListener<TEvents[K]>): void;
18
18
  clearListeners(): void;
19
- private normalizeEventName;
20
19
  }
21
20
  export {};
@@ -1,3 +1,4 @@
1
+ /// <reference types="dom-serial" preserve="true" />
1
2
  import type { V5SerialDeviceState } from "./VexDeviceState.js";
2
3
  import { VexSerialError } from "./VexError.js";
3
4
  import { ResultAsync } from "neverthrow";
@@ -0,0 +1,5 @@
1
+ export * from "./VexCRC.js";
2
+ export * from "./VexFirmwareVersion.js";
3
+ export * from "./VexPacketBase.js";
4
+ export * from "./VexPacketEncoder.js";
5
+ export * from "./VexPacketView.js";
@@ -11,6 +11,8 @@ export declare class PacketEncoder {
11
11
  allPacketsTable: Map<number, Map<number | undefined, typeof HostBoundPacket>>;
12
12
  static getInstance(): PacketEncoder;
13
13
  private constructor();
14
+ /** Register reply packet classes without retaining an entire module object. */
15
+ registerPacketTypes(types: Iterable<typeof HostBoundPacket>): void;
14
16
  /** Look up the reply class registered for a command ID pair. */
15
17
  getPacketType(commandId: number | undefined, commandExtendedId: number | undefined): typeof HostBoundPacket | undefined;
16
18
  /** Create the vex CDC header. */
@@ -235,7 +235,7 @@ export declare class ReadFileReplyD2HPacket extends HostBoundPacket {
235
235
  static COMMAND_EXTENDED_ID: number;
236
236
  addr: number;
237
237
  length: number;
238
- buf: ArrayBuffer;
238
+ buf: Uint8Array;
239
239
  constructor(data: DataArray);
240
240
  }
241
241
  export declare class LinkFileReplyD2HPacket extends HostBoundPacket {
@@ -0,0 +1,3 @@
1
+ import { type HostBoundPacket } from "./VexPacketBase.js";
2
+ /** Default reply registry used by the complete serial entry point. */
3
+ export declare const defaultReplyPacketTypes: readonly (typeof HostBoundPacket)[];
@@ -0,0 +1,2 @@
1
+ export declare const SCREEN_CAPTURE_FRAMEBUFFER_SIZE: number;
2
+ export declare function convertScreenCapture(framebuffer: Uint8Array): Uint8Array;
@@ -1,3 +1,4 @@
1
+ /// <reference types="dom-serial" preserve="true" />
1
2
  import { type IFileBasicInfo, type IFileHandle, FileVendor, type IProgramInfo, type IFileWriteRequest, FileDownloadTarget } from "./Vex.js";
2
3
  import { type ProgramIniConfig } from "./VexIniConfig.js";
3
4
  import type { V5SerialDeviceState } from "./VexDeviceState.js";