-
Notifications
You must be signed in to change notification settings - Fork 366
Expand file tree
/
Copy pathash.ts
More file actions
1933 lines (1640 loc) · 68.1 KB
/
ash.ts
File metadata and controls
1933 lines (1640 loc) · 68.1 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
/* v8 ignore start */
import {EventEmitter} from "node:events";
import {Socket} from "node:net";
import {wait} from "../../../utils";
import {logger} from "../../../utils/logger";
import {SerialPort} from "../../serialPort";
import type {SerialPortOptions} from "../../tstype";
import {isTcpPath, parseTcpPath} from "../../utils";
import {EzspStatus} from "../enums";
import {halCommonCrc16, inc8, withinRange} from "../utils/math";
import {
ASH_ACKNUM_BIT,
ASH_ACKNUM_MASK,
ASH_CRC_LEN,
ASH_DFRAME_MASK,
ASH_FLIP,
ASH_FRAME_LEN_ACK,
ASH_FRAME_LEN_DATA_MIN,
ASH_FRAME_LEN_ERROR,
ASH_FRAME_LEN_NAK,
ASH_FRAME_LEN_RSTACK,
ASH_FRMNUM_BIT,
ASH_FRMNUM_MASK,
ASH_MAX_DATA_FIELD_LEN,
ASH_MAX_FRAME_WITH_CRC_LEN,
ASH_MAX_TIMEOUTS,
ASH_MIN_DATA_FIELD_LEN,
ASH_MIN_FRAME_WITH_CRC_LEN,
ASH_NFLAG_MASK,
ASH_RFLAG_MASK,
ASH_SHFRAME_MASK,
ASH_VERSION,
ASH_WAKE,
EZSP_HOST_RX_POOL_SIZE,
LFSR_POLY,
LFSR_SEED,
SH_RX_BUFFER_LEN,
SH_TX_BUFFER_LEN,
TX_POOL_BUFFERS,
} from "./consts";
import {AshFrameType, AshReservedByte, NcpFailedCode} from "./enums";
import {AshParser} from "./parser";
import {EzspBuffer, EzspFreeList, EzspQueue} from "./queues";
import {AshWriter} from "./writer";
const NS = "zh:ember:uart:ash";
type UartAshCounters = {
/** DATA frame data fields bytes transmitted */
txData: number;
/** frames of all types transmitted */
txAllFrames: number;
/** DATA frames transmitted */
txDataFrames: number;
/** ACK frames transmitted */
txAckFrames: number;
/** NAK frames transmitted */
txNakFrames: number;
/** DATA frames retransmitted */
txReDataFrames: number;
/** ACK and NAK frames with nFlag 0 transmitted */
// txN0Frames: number,
/** ACK and NAK frames with nFlag 1 transmitted */
txN1Frames: number;
/** frames cancelled (with ASH_CAN byte) */
txCancelled: number;
/** DATA frame data fields bytes received */
rxData: number;
/** frames of all types received */
rxAllFrames: number;
/** DATA frames received */
rxDataFrames: number;
/** ACK frames received */
rxAckFrames: number;
/** NAK frames received */
rxNakFrames: number;
/** retransmitted DATA frames received */
rxReDataFrames: number;
/** ACK and NAK frames with nFlag 0 received */
// rxN0Frames: number,
/** ACK and NAK frames with nFlag 1 received */
rxN1Frames: number;
/** frames cancelled (with ASH_CAN byte) */
rxCancelled: number;
/** frames with CRC errors */
rxCrcErrors: number;
/** frames with comm errors (with ASH_SUB byte) */
rxCommErrors: number;
/** frames shorter than minimum */
rxTooShort: number;
/** frames longer than maximum */
rxTooLong: number;
/** frames with illegal control byte */
rxBadControl: number;
/** frames with illegal length for type of frame */
rxBadLength: number;
/** frames with bad ACK numbers */
rxBadAckNumber: number;
/** DATA frames discarded due to lack of buffers */
rxNoBuffer: number;
/** duplicate retransmitted DATA frames */
rxDuplicates: number;
/** DATA frames received out of sequence */
rxOutOfSequence: number;
/** received ACK timeouts */
rxAckTimeouts: number;
};
const enum SendState {
IDLE = 0,
SHFRAME = 1,
TX_DATA = 2,
RETX_DATA = 3,
}
// Bits in ashFlags
const enum Flag {
/** Reject Condition */
REJ = 0x01,
/** Retransmit Condition */
RETX = 0x02,
/** send NAK */
NAK = 0x04,
/** send ACK */
ACK = 0x08,
/** send RST */
RST = 0x10,
/** send immediate CAN */
CAN = 0x20,
/** in CONNECTED state, else ERROR */
CONNECTED = 0x40,
/** not ready to receive DATA frames */
NR = 0x100,
/** last transmitted NR status */
NRTX = 0x200,
}
/** max frames sent without being ACKed (1-7) */
export const CONFIG_TX_K = 3;
/** adaptive rec'd ACK timeout initial value */
const CONFIG_ACK_TIME_INIT = 800;
/** " " " " " minimum value */
const CONFIG_ACK_TIME_MIN = 400;
/** " " " " " maximum value */
const CONFIG_ACK_TIME_MAX = 2400;
/** time allowed to receive RSTACK after ncp is reset */
const CONFIG_TIME_RST = 5000;
/** time between checks for received RSTACK (CONNECTED status) */
const CONFIG_TIME_RST_CHECK = 100;
/** if free buffers < limit, host receiver isn't ready, will hold off the ncp from sending normal priority frames */
const CONFIG_NR_LOW_LIMIT = 8; // RX_FREE_LW
/** if free buffers > limit, host receiver is ready */
const CONFIG_NR_HIGH_LIMIT = 12; // RX_FREE_HW
/** time until a set nFlag must be resent (max 2032) */
const CONFIG_NR_TIME = 480;
/** Read/write max bytes count at stream level */
const CONFIG_HIGHWATER_MARK = 256;
interface UartAshEventMap {
fatalError: [status: EzspStatus];
frame: [];
}
/**
* ASH Protocol handler.
*/
export class UartAsh extends EventEmitter<UartAshEventMap> {
private readonly portOptions: SerialPortOptions;
private serialPort?: SerialPort;
private socketPort?: Socket;
private writer: AshWriter;
private parser: AshParser;
/** True when serial/socket is currently closing. */
private closing: boolean;
/** time ackTimer started: 0 means not ready uint16_t */
private ackTimer: number;
/** time used to check ackTimer expiry (msecs) uint16_t */
private ackPeriod: number;
/** not ready timer (16 msec units). Set to (now + config.nrTime) when started. uint8_t */
private nrTimer: number;
/** frame decode in progress */
private decodeInProgress: boolean;
// Variables used in encoding frames
/** true when preceding byte was escaped */
private encodeEscFlag: boolean;
/** byte to send after ASH_ESC uint8_t */
private encodeFlip: number;
/** uint16_t */
private encodeCrc: number;
/** encoder state: 0 = control/data bytes, 1 = crc low byte, 2 = crc high byte, 3 = flag. uint8_t */
private encodeState: number;
/** bytes remaining to encode. uint8_t */
private encodeCount: number;
// Variables used in decoding frames
/** bytes in frame, plus CRC, clamped to limit +1: high values also used to record certain errors. uint8_t */
private decodeLen: number;
/** ASH_FLIP if previous byte was ASH_ESC. uint8_t */
private decodeFlip: number;
/** a 2 byte queue to avoid outputting crc bytes. uint8_t */
private decodeByte1: number;
/** at frame end, they contain the received crc. uint8_t */
private decodeByte2: number;
/** uint16_t */
private decodeCrc: number;
private decodeOutByte = 0x00;
/** outgoing short frames */
private txSHBuffer: Buffer;
/** incoming short frames */
private rxSHBuffer: Buffer;
/** bit flags for top-level logic. uint16_t */
private flags: number;
/** frame ack'ed from remote peer. uint8_t */
private ackRx: number;
/** frame ack'ed to remote peer. uint8_t */
private ackTx: number;
/** next frame to be transmitted. uint8_t */
private frmTx: number;
/** next frame to be retransmitted. uint8_t */
private frmReTx: number;
/** next frame expected to be rec'd. uint8_t */
private frmRx: number;
/** frame at retx queue's head. uint8_t */
private frmReTxHead: number;
/** consecutive timeout counter. uint8_t */
private timeouts: number;
/** rec'd DATA frame buffer. uint8_t */
private rxDataBuffer?: EzspBuffer;
/** rec'd frame length. uint8_t */
private rxLen: number;
/** tx frame offset. uint8_t */
private txOffset: number;
public counters: UartAshCounters;
/**
* Errors reported by the NCP.
* The `NcpFailedCode` from the frame reporting this is logged before this is set to make it clear where it failed:
* - The NCP sent an ERROR frame during the initial reset sequence (before CONNECTED state)
* - The NCP sent an ERROR frame
* - The NCP sent an unexpected RSTACK
*/
private ncpError: EzspStatus;
/** Errors reported by the Host. */
private hostError: EzspStatus;
/** sendExec() state variable */
private sendState: SendState;
/** NCP is enabled to sleep, set by EZSP, not supported atm, always false */
public ncpSleepEnabled: boolean;
/**
* Set when the ncp has indicated it has a pending callback by seting the callback flag in the frame control byte
* or (uart version only) by sending an an ASH_WAKE byte between frames.
*/
public ncpHasCallbacks: boolean;
/** Transmit buffers */
private readonly txPool: EzspBuffer[];
public readonly txQueue: EzspQueue;
public readonly reTxQueue: EzspQueue;
public readonly txFree: EzspFreeList;
/** Receive buffers */
private readonly rxPool: EzspBuffer[];
public readonly rxQueue: EzspQueue;
public readonly rxFree: EzspFreeList;
constructor(options: SerialPortOptions) {
super();
this.portOptions = options;
this.serialPort = undefined;
this.socketPort = undefined;
this.writer = new AshWriter({highWaterMark: CONFIG_HIGHWATER_MARK});
this.parser = new AshParser({readableHighWaterMark: CONFIG_HIGHWATER_MARK});
this.txPool = new Array<EzspBuffer>(TX_POOL_BUFFERS);
this.txQueue = new EzspQueue();
this.reTxQueue = new EzspQueue();
this.txFree = new EzspFreeList();
this.rxPool = new Array<EzspBuffer>(EZSP_HOST_RX_POOL_SIZE);
this.rxQueue = new EzspQueue();
this.rxFree = new EzspFreeList();
this.closing = false;
this.txSHBuffer = Buffer.alloc(SH_TX_BUFFER_LEN);
this.rxSHBuffer = Buffer.alloc(SH_RX_BUFFER_LEN);
this.ackTimer = 0;
this.ackPeriod = 0;
this.nrTimer = 0;
this.flags = 0;
this.decodeInProgress = false;
this.ackRx = 0;
this.ackTx = 0;
this.frmTx = 0;
this.frmReTx = 0;
this.frmRx = 0;
this.frmReTxHead = 0;
this.timeouts = 0;
this.rxDataBuffer = undefined;
this.rxLen = 0;
// init to "start of frame" default
this.encodeCount = 0;
this.encodeState = 0;
this.encodeEscFlag = false;
this.encodeFlip = 0;
this.encodeCrc = 0xffff;
this.txOffset = 0;
// init to "start of frame" default
this.decodeLen = 0;
this.decodeByte1 = 0;
this.decodeByte2 = 0;
this.decodeFlip = 0;
this.decodeCrc = 0xffff;
this.ncpError = EzspStatus.NO_ERROR;
this.hostError = EzspStatus.NO_ERROR;
this.sendState = SendState.IDLE;
this.ncpSleepEnabled = false;
this.ncpHasCallbacks = false;
this.stopAckTimer();
this.stopNrTimer();
this.counters = {
txData: 0,
txAllFrames: 0,
txDataFrames: 0,
txAckFrames: 0,
txNakFrames: 0,
txReDataFrames: 0,
// txN0Frames: 0,
txN1Frames: 0,
txCancelled: 0,
rxData: 0,
rxAllFrames: 0,
rxDataFrames: 0,
rxAckFrames: 0,
rxNakFrames: 0,
rxReDataFrames: 0,
// rxN0Frames: 0,
rxN1Frames: 0,
rxCancelled: 0,
rxCrcErrors: 0,
rxCommErrors: 0,
rxTooShort: 0,
rxTooLong: 0,
rxBadControl: 0,
rxBadLength: 0,
rxBadAckNumber: 0,
rxNoBuffer: 0,
rxDuplicates: 0,
rxOutOfSequence: 0,
rxAckTimeouts: 0,
};
// All transmit buffers are put into txFree, and txQueue and reTxQueue are empty.
this.txQueue.tail = undefined;
this.reTxQueue.tail = undefined;
this.txFree.link = undefined;
for (let i = 0; i < TX_POOL_BUFFERS; i++) {
this.txPool[i] = new EzspBuffer();
this.txFree.freeBuffer(this.txPool[i]);
}
// All receive buffers are put into rxFree, and rxQueue is empty.
this.rxQueue.tail = undefined;
this.rxFree.link = undefined;
for (let i = 0; i < EZSP_HOST_RX_POOL_SIZE; i++) {
this.rxPool[i] = new EzspBuffer();
this.rxFree.freeBuffer(this.rxPool[i]);
}
}
/**
* Check if port is valid, open, and not closing.
*/
get portOpen(): boolean {
if (this.closing) {
return false;
}
// biome-ignore lint/style/noNonNullAssertion: ignored using `--suppress`
if (isTcpPath(this.portOptions.path!)) {
return this.socketPort ? !this.socketPort.closed : false;
}
return this.serialPort ? this.serialPort.isOpen : false;
}
/**
* Get max wait time before response is considered timed out.
*/
get responseTimeout(): number {
return ASH_MAX_TIMEOUTS * CONFIG_ACK_TIME_MAX;
}
/**
* Indicates if the host is in the Connected state.
* If not, the host and NCP cannot exchange DATA frames.
* Note that this function does not actively confirm that communication with NCP is healthy, but simply returns its last known status.
*
* @returns
* - true - host and NCP can exchange DATA frames
* - false - host and NCP cannot now exchange DATA frames
*/
get connected(): boolean {
return (this.flags & Flag.CONNECTED) !== 0;
}
/**
* Has nothing to do...
*/
get idle(): boolean {
return (
!this.decodeInProgress && // don't have a partial frame
// && (this.serial.readAvailable() === EzspStatus.NO_RX_DATA) // no rx data
this.rxQueue.empty && // no rx frames to process
!this.ncpHasCallbacks && // no pending callbacks
this.flags === Flag.CONNECTED && // no pending ACKs, NAKs, etc.
this.ackTx === this.frmRx && // do not need to send an ACK
this.ackRx === this.frmTx && // not waiting to receive an ACK
this.sendState === SendState.IDLE && // nothing being transmitted now
this.txQueue.empty // nothing waiting to transmit
// && this.serial.outputIsIdle() // nothing in OS buffers or UART FIFO
);
}
/**
* Init the serial or socket port and hook parser/writer.
* NOTE: This is the only function that throws/rejects in the ASH layer (caught by resetNcp and turned into an EzspStatus).
*/
private async initPort(): Promise<void> {
await this.closePort(); // will do nothing if nothing's open
// biome-ignore lint/style/noNonNullAssertion: ignored using `--suppress`
if (!isTcpPath(this.portOptions.path!)) {
const serialOpts = {
// biome-ignore lint/style/noNonNullAssertion: ignored using `--suppress`
path: this.portOptions.path!,
baudRate: typeof this.portOptions.baudRate === "number" ? this.portOptions.baudRate : 115200,
rtscts: typeof this.portOptions.rtscts === "boolean" ? this.portOptions.rtscts : false,
autoOpen: false,
parity: "none" as const,
stopBits: 1 as const,
xon: false,
xoff: false,
};
// enable software flow control if RTS/CTS not enabled in config
if (!serialOpts.rtscts) {
logger.info("RTS/CTS config is off, enabling software flow control.", NS);
serialOpts.xon = true;
serialOpts.xoff = true;
}
// @ts-expect-error Jest testing
if (this.portOptions.binding !== undefined) {
// @ts-expect-error Jest testing
serialOpts.binding = this.portOptions.binding;
}
logger.debug(() => `Opening serial port with ${JSON.stringify(serialOpts)}`, NS);
this.serialPort = new SerialPort(serialOpts);
this.writer.pipe(this.serialPort);
this.serialPort.pipe(this.parser);
this.parser.on("data", this.onFrame.bind(this));
try {
await this.serialPort.asyncOpen();
logger.info("Serial port opened", NS);
this.serialPort.once("close", this.onPortClose.bind(this));
this.serialPort.on("error", this.onPortError.bind(this));
} catch (error) {
await this.stop();
throw error;
}
} else {
// biome-ignore lint/style/noNonNullAssertion: ignored using `--suppress`
const info = parseTcpPath(this.portOptions.path!);
logger.debug(() => `Opening TCP socket with ${info.host}:${info.port}`, NS);
this.socketPort = new Socket();
this.socketPort.setNoDelay(true);
this.socketPort.setKeepAlive(true, 15000);
this.writer.pipe(this.socketPort);
this.socketPort.pipe(this.parser);
this.parser.on("data", this.onFrame.bind(this));
return await new Promise((resolve, reject): void => {
const openError = async (err: Error): Promise<void> => {
await this.stop();
reject(err);
};
// biome-ignore lint/style/noNonNullAssertion: ignored using `--suppress`
this.socketPort!.on("connect", () => {
logger.debug(() => "Socket connected", NS);
});
// biome-ignore lint/style/noNonNullAssertion: ignored using `--suppress`
this.socketPort!.on("ready", (): void => {
logger.info("Socket ready", NS);
// biome-ignore lint/style/noNonNullAssertion: ignored using `--suppress`
this.socketPort!.removeListener("error", openError);
// biome-ignore lint/style/noNonNullAssertion: ignored using `--suppress`
this.socketPort!.once("close", this.onPortClose.bind(this));
// biome-ignore lint/style/noNonNullAssertion: ignored using `--suppress`
this.socketPort!.on("error", this.onPortError.bind(this));
resolve();
});
// biome-ignore lint/style/noNonNullAssertion: ignored using `--suppress`
this.socketPort!.once("error", openError);
// biome-ignore lint/style/noNonNullAssertion: ignored using `--suppress`
this.socketPort!.connect(info.port, info.host);
});
}
}
/**
* Handle port closing
* @param err A boolean for Socket, an Error for serialport
*/
private onPortClose(error: boolean | Error): void {
logger.info(`Port closed, error=${error}`, NS);
if (this.flags !== 0) {
this.flags = 0;
this.emit("fatalError", EzspStatus.ERROR_SERIAL_INIT);
}
}
/**
* Handle port error
* @param error
*/
private onPortError(error: Error): void {
logger.error(`Port ${error}`, NS);
}
/**
* Handle received frame from AshParser.
* @param buf
*/
private onFrame(buffer: Buffer): void {
const iCAN = buffer.lastIndexOf(AshReservedByte.CANCEL); // should only be one, but just in case...
if (iCAN !== -1) {
// ignore the cancel before RSTACK
if (this.flags & Flag.CONNECTED) {
this.counters.rxCancelled += 1;
logger.warning(`Frame(s) in progress cancelled in [${buffer.toString("hex")}]`, NS);
}
// get rid of everything up to the CAN flag and start reading frame from there, no need to loop through bytes in vain
buffer = buffer.subarray(iCAN + 1);
}
if (!buffer.length) {
// skip any CANCEL that results in empty frame (have yet to see one, but just in case...)
// shouldn't happen for any other reason, unless receiving bad stuff from port?
logger.debug(() => "Received empty frame. Skipping.", NS);
return;
}
const status = this.receiveFrame(buffer);
this.sendExec(); // always trigger to cover all cases
if (status !== EzspStatus.SUCCESS && status !== EzspStatus.ASH_IN_PROGRESS && status !== EzspStatus.NO_RX_DATA) {
this.emit("fatalError", status);
return;
}
}
/**
* Initializes the ASH protocol, and waits until the NCP finishes rebooting, or a non-recoverable error occurs.
*
* @returns
* - EzspStatus.SUCCESS
* - EzspStatus.HOST_FATAL_ERROR
* - EzspStatus.ASH_NCP_FATAL_ERROR)
*/
public async start(): Promise<EzspStatus> {
if (!this.portOpen || this.flags & Flag.CONNECTED) {
return EzspStatus.ERROR_INVALID_CALL;
}
logger.info("======== ASH starting ========", NS);
try {
if (this.serialPort) {
await this.serialPort.asyncFlush(); // clear read/write buffers
} else {
// XXX: Socket equiv?
}
} catch (err) {
logger.error(`Error while flushing before start: ${err}`, NS);
}
// block til RSTACK, fatal error or timeout
// NOTE: on average, this seems to take around 1000ms when successful
for (let i = 0; i < CONFIG_TIME_RST; i += CONFIG_TIME_RST_CHECK) {
this.sendExec();
if (this.flags & Flag.CONNECTED) {
logger.info("======== ASH started ========", NS);
return EzspStatus.SUCCESS;
}
if (this.hostError !== EzspStatus.NO_ERROR || this.ncpError !== EzspStatus.NO_ERROR) {
// don't wait for inevitable fail, bail early, let retry logic in EZSP layer do its thing
break;
}
logger.debug(() => `Waiting for RSTACK... ${i}/${CONFIG_TIME_RST}`, NS);
await wait(CONFIG_TIME_RST_CHECK);
}
return EzspStatus.HOST_FATAL_ERROR;
}
/**
* Stops the ASH protocol - flushes and closes the serial port, clears all queues, stops timers, etc.
*/
public async stop(): Promise<void> {
this.closing = true;
this.logCounters();
await this.closePort();
logger.info("======== ASH stopped ========", NS);
}
/**
* Close port and remove listeners.
* Does nothing if port not defined/open.
*/
public async closePort(): Promise<void> {
this.flags = 0;
if (this.serialPort?.isOpen) {
try {
await this.serialPort.asyncFlushAndClose();
} catch (err) {
logger.error(`Failed to close serial port ${err}.`, NS);
}
this.serialPort.removeAllListeners();
} else if (this.socketPort != null && !this.socketPort.closed) {
this.socketPort.destroy();
this.socketPort.removeAllListeners();
}
}
/**
* Initializes the ASH serial port and (if enabled) resets the NCP.
* The method used to do the reset is specified by the the host configuration parameter resetMethod.
*
* When the reset method is sending a RST frame, the caller should retry NCP resets a few times if it fails.
*
* @returns
* - EzspStatus.SUCCESS
* - EzspStatus.HOST_FATAL_ERROR
*/
public async resetNcp(): Promise<EzspStatus> {
if (this.closing) {
return EzspStatus.ERROR_INVALID_CALL;
}
logger.info("======== ASH Adapter reset ========", NS);
// ask ncp to reset itself using RST frame
try {
if (!this.portOpen) {
await this.initPort();
}
this.flags = Flag.RST | Flag.CAN;
return EzspStatus.SUCCESS;
} catch (err) {
logger.error(`Failed to init port with error ${err}`, NS);
this.hostError = EzspStatus.HOST_FATAL_ERROR;
return this.hostError;
}
}
/**
* Adds a DATA frame to the transmit queue to send to the NCP.
* Frames that are too long or too short will not be sent, and frames will not be added to the queue
* if the host is not in the Connected state, or the NCP is not ready to receive a DATA frame or if there
* is no room in the queue;
*
* @param len length of data field
* @param inBuf array containing the data to be sent
*
* @returns
* - EzspStatus.SUCCESS
* - EzspStatus.NO_TX_SPACE
* - EzspStatus.DATA_FRAME_TOO_SHORT
* - EzspStatus.DATA_FRAME_TOO_LONG
* - EzspStatus.NOT_CONNECTED
*/
public send(len: number, inBuf: Buffer): EzspStatus {
// Check for errors that might have been detected
if (this.hostError !== EzspStatus.NO_ERROR) {
return EzspStatus.HOST_FATAL_ERROR;
}
if (this.ncpError !== EzspStatus.NO_ERROR) {
return EzspStatus.ASH_NCP_FATAL_ERROR;
}
// After verifying that the data field length is within bounds,
// copies data frame to a buffer and appends it to the transmit queue.
if (len < ASH_MIN_DATA_FIELD_LEN) {
return EzspStatus.DATA_FRAME_TOO_SHORT;
}
if (len > ASH_MAX_DATA_FIELD_LEN) {
return EzspStatus.DATA_FRAME_TOO_LONG;
}
if (!(this.flags & Flag.CONNECTED)) {
return EzspStatus.NOT_CONNECTED;
}
const buffer = this.txFree.allocBuffer();
if (buffer === undefined) {
return EzspStatus.NO_TX_SPACE;
}
inBuf.copy(buffer.data, 0, 0, len);
buffer.len = len;
this.randomizeBuffer(buffer.data, buffer.len); // IN/OUT data
this.txQueue.addTail(buffer);
this.sendExec();
return EzspStatus.SUCCESS;
}
/**
* Manages outgoing communication to the NCP, including DATA frames as well as the frames used for
* initialization and error detection and recovery.
*/
public sendExec(): void {
let outByte = 0x00;
let inByte = 0x00;
let len = 0;
let buffer: EzspBuffer | undefined;
// Check for received acknowledgement timer expiry
if (this.ackTimerHasExpired()) {
if (this.flags & Flag.CONNECTED) {
const reTx = this.flags & Flag.RETX;
const expectedFrm = reTx ? this.frmReTx : this.frmTx;
if (this.ackRx !== expectedFrm) {
this.counters.rxAckTimeouts += 1;
this.adjustAckPeriod(true);
logger.debug(() => `Timer expired waiting for ACK for ${reTx ? "frmReTx" : "frmTx"}=${expectedFrm}, ackRx=${this.ackRx}`, NS);
if (++this.timeouts >= ASH_MAX_TIMEOUTS) {
this.hostDisconnect(EzspStatus.ASH_ERROR_TIMEOUTS);
return;
}
this.startRetransmission();
} else {
this.stopAckTimer();
}
} /* else {
this.hostDisconnect(EzspStatus.ASH_ERROR_RESET_FAIL);
}*/
// let Ezsp layer retry logic handle timeout
}
while (this.writer.writeAvailable()) {
// Send ASH_CAN character immediately, ahead of any other transmit data
if (this.flags & Flag.CAN) {
if (this.sendState === SendState.IDLE) {
// sending RST or just woke NCP
this.writer.writeByte(AshReservedByte.CANCEL);
} else if (this.sendState === SendState.TX_DATA) {
// cancel frame in progress
this.counters.txCancelled += 1;
this.writer.writeByte(AshReservedByte.CANCEL);
this.stopAckTimer();
this.sendState = SendState.IDLE;
}
this.flags &= ~Flag.CAN;
continue;
}
switch (this.sendState) {
case SendState.IDLE: {
// In between frames - do some housekeeping and decide what to send next
// If retransmitting, set the next frame to send to the last ackNum
// received, then check to see if retransmission is now complete.
if (this.flags & Flag.RETX) {
if (withinRange(this.frmReTx, this.ackRx, this.frmTx)) {
this.frmReTx = this.ackRx;
}
if (this.frmReTx === this.frmTx) {
this.flags &= ~Flag.RETX;
this.scrubReTxQueue();
}
}
// restrain ncp if needed
this.dataFrameFlowControl();
// See if a short frame is flagged to be sent
// The order of the tests below - RST, NAK and ACK -
// sets the relative priority of sending these frame types.
if (this.flags & Flag.RST) {
this.txSHBuffer[0] = AshFrameType.RST;
this.setAndStartAckTimer(CONFIG_TIME_RST);
len = 1;
this.flags &= ~(Flag.RST | Flag.NAK | Flag.ACK);
this.sendState = SendState.SHFRAME;
logger.debug(() => "---> [FRAME type=RST]", NS);
} else if (this.flags & (Flag.NAK | Flag.ACK)) {
if (this.flags & Flag.NAK) {
this.txSHBuffer[0] = AshFrameType.NAK + (this.frmRx << ASH_ACKNUM_BIT);
this.flags &= ~(Flag.NRTX | Flag.NAK | Flag.ACK);
logger.debug(() => `---> [FRAME type=NAK frmRx=${this.frmRx}](ackRx=${this.ackRx})`, NS);
} else {
this.txSHBuffer[0] = AshFrameType.ACK + (this.frmRx << ASH_ACKNUM_BIT);
this.flags &= ~(Flag.NRTX | Flag.ACK);
logger.debug(() => `---> [FRAME type=ACK frmRx=${this.frmRx}](ackRx=${this.ackRx})`, NS);
}
if (this.flags & Flag.NR) {
this.txSHBuffer[0] |= ASH_NFLAG_MASK;
this.flags |= Flag.NRTX;
this.startNrTimer();
}
this.ackTx = this.frmRx;
len = 1;
this.sendState = SendState.SHFRAME;
} else if (this.flags & Flag.RETX) {
// Retransmitting DATA frames for error recovery
// buffer assumed valid from loop logic
// biome-ignore lint/style/noNonNullAssertion: ignored using `--suppress`
buffer = this.reTxQueue.getNthEntry((this.frmTx - this.frmReTx) & 7)!;
len = buffer.len + 1;
this.txSHBuffer[0] = AshFrameType.DATA | (this.frmReTx << ASH_FRMNUM_BIT) | (this.frmRx << ASH_ACKNUM_BIT) | ASH_RFLAG_MASK;
this.sendState = SendState.RETX_DATA;
logger.debug(
() => `---> [FRAME type=DATA_RETX frmReTx=${this.frmReTx} frmRx=${this.frmRx}](ackRx=${this.ackRx} frmTx=${this.frmTx})`,
NS,
);
} else if (this.ackTx !== this.frmRx) {
// An ACK should be generated
this.flags |= Flag.ACK;
break;
} else if (!this.txQueue.empty && withinRange(this.ackRx, this.frmTx, this.ackRx + CONFIG_TX_K - 1)) {
// Send a DATA frame if ready
buffer = this.txQueue.head;
len = buffer.len + 1;
this.counters.txData += len - 1;
this.txSHBuffer[0] = AshFrameType.DATA | (this.frmTx << ASH_FRMNUM_BIT) | (this.frmRx << ASH_ACKNUM_BIT);
this.sendState = SendState.TX_DATA;
logger.debug(() => `---> [FRAME type=DATA frmTx=${this.frmTx} frmRx=${this.frmRx}](ackRx=${this.ackRx})`, NS);
} else {
// Otherwise there's nothing to send
this.writer.writeFlush();
return;
}
this.countFrame(true);
// Start frame - encodeByte() is inited by a non-zero length argument
outByte = this.encodeByte(len, this.txSHBuffer[0]);
this.writer.writeByte(outByte);
break;
}
case SendState.SHFRAME: {
// sending short frame
if (this.txOffset !== 0xff) {
inByte = this.txSHBuffer[this.txOffset];
outByte = this.encodeByte(0, inByte);
this.writer.writeByte(outByte);
} else {
this.sendState = SendState.IDLE;
}
break;
}
case SendState.TX_DATA:
case SendState.RETX_DATA: {
// sending OR resending data frame
if (this.txOffset !== 0xff) {
// buffer assumed valid from loop logic
// biome-ignore lint/style/noNonNullAssertion: ignored using `--suppress`
inByte = this.txOffset ? buffer!.data[this.txOffset - 1] : this.txSHBuffer[0];
outByte = this.encodeByte(0, inByte);
this.writer.writeByte(outByte);
} else {
if (this.sendState === SendState.TX_DATA) {
this.frmTx = inc8(this.frmTx);
buffer = this.txQueue.removeHead();
this.reTxQueue.addTail(buffer);
} else {
this.frmReTx = inc8(this.frmReTx);
}
if (this.ackTimerIsNotRunning()) {
this.startAckTimer();
}
this.ackTx = this.frmRx;
this.sendState = SendState.IDLE;
}
break;
}
}
}
this.writer.writeFlush();
}
/**
* Retrieve a frame and accept, reTx, reject, fail based on type & validity in current state.
* @returns
* - EzspStatus.SUCCESS On valid RSTACK or valid DATA frame.
* - EzspStatus.ASH_IN_PROGRESS
* - EzspStatus.NO_RX_DATA
* - EzspStatus.NO_RX_SPACE
* - EzspStatus.HOST_FATAL_ERROR
* - EzspStatus.ASH_NCP_FATAL_ERROR
*/
private receiveFrame(buffer: Buffer): EzspStatus {
// Check for errors that might have been detected
if (this.hostError !== EzspStatus.NO_ERROR) {
return EzspStatus.HOST_FATAL_ERROR;
}
if (this.ncpError !== EzspStatus.NO_ERROR) {
return EzspStatus.ASH_NCP_FATAL_ERROR;
}
let ackNum = 0;
let frmNum = 0;