@springfield/ham-radio-utils 2.0.2 → 2.2.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.
@@ -0,0 +1,203 @@
1
+ import type { ILogLayer } from 'loglayer';
2
+
3
+ /**
4
+ * UI Logger: Command-Level Logging for UI Display
5
+ *
6
+ * This module provides a specialized logger for capturing command-level information
7
+ * that can be easily parsed and displayed in a UI. It captures details about
8
+ * commands, data sent, data expected, and data received in a structured format.
9
+ *
10
+ * Purpose:
11
+ * - Captures command-level information for UI display
12
+ * - Provides structured JSON logging for easy parsing
13
+ * - Tracks command execution details including timing
14
+ * - Supports protocol debugging in UI environments
15
+ * - Maintains separation from debug logging
16
+ *
17
+ * Design Rationale:
18
+ * - Single log entry per command provides clean UI display
19
+ * - JSON structure enables easy parsing and filtering
20
+ * - Command-level granularity is appropriate for UI debugging
21
+ * - Structured data supports rich UI representations
22
+ * - Separate from debug logging avoids UI noise
23
+ */
24
+
25
+ /**
26
+ * Generic protocol step interface for UI logging
27
+ */
28
+ export interface UIProtocolStep {
29
+ [key: string]: any;
30
+ }
31
+
32
+ /**
33
+ * UI Logger for capturing command-level information for UI display
34
+ */
35
+ export class UILogger {
36
+ private logger: ILogLayer;
37
+ private commandStartTimes = new Map<string, number>();
38
+
39
+ constructor(logger: ILogLayer) {
40
+ this.logger = logger;
41
+ }
42
+
43
+ // eslint-disable-next-line max-params
44
+ startCommand(stepIndex: number, totalSteps: number, operation: string, step: UIProtocolStep): void {
45
+ const commandId = `${operation}-${stepIndex}`;
46
+ const startTime = Date.now();
47
+ this.commandStartTimes.set(commandId, startTime);
48
+
49
+ const commandType = this.getCommandType(step);
50
+ const description = this.getStepDescription(step);
51
+
52
+ this.logger
53
+ .withMetadata({
54
+ commandId,
55
+ commandType,
56
+ description,
57
+ operation,
58
+ startTime,
59
+ stepIndex,
60
+ totalSteps,
61
+ })
62
+ .info('Command started');
63
+ }
64
+
65
+ // eslint-disable-next-line max-params
66
+ logCommandSuccess(stepIndex: number, totalSteps: number, operation: string, step: UIProtocolStep, context: any): void {
67
+ const commandId = `${operation}-${stepIndex}`;
68
+ const startTime = this.commandStartTimes.get(commandId) || Date.now();
69
+ const endTime = Date.now();
70
+ const duration = endTime - startTime;
71
+
72
+ const commandType = this.getCommandType(step);
73
+ const description = this.getStepDescription(step);
74
+
75
+ // Extract sent and received data from context
76
+ const dataSent = context.variables?.get('lastSentData');
77
+ const dataReceived = context.variables?.get('lastReceivedData');
78
+ const dataExpected = this.getExpectedData(step);
79
+
80
+ // For readSegment, also extract chunk logs if present
81
+ let dataChunks = undefined;
82
+ if ('readSegment' === commandType) {
83
+ dataChunks = context.variables?.get('lastReadSegmentChunks');
84
+ // For readSegment, we don't need the flattened dataSent/dataReceived since we have dataChunks
85
+ }
86
+
87
+ // Convert dataSent and dataReceived to byte array format if they exist
88
+ const dataSentArray = dataSent ? [...dataSent] : undefined;
89
+ const dataReceivedArray = dataReceived ? [...dataReceived] : undefined;
90
+
91
+ this.logger
92
+ .withMetadata({
93
+ commandType,
94
+ dataChunks,
95
+ dataExpected,
96
+ dataReceived: dataReceivedArray,
97
+ dataSent: dataSentArray,
98
+ description,
99
+ duration,
100
+ endTime,
101
+ operation,
102
+ startTime,
103
+ stepIndex,
104
+ success: true,
105
+ totalSteps,
106
+ })
107
+ .info('Command completed successfully');
108
+ }
109
+
110
+ // eslint-disable-next-line max-params
111
+ logCommandFailure(stepIndex: number, totalSteps: number, operation: string, step: UIProtocolStep, error: Error, context: any): void {
112
+ const commandId = `${operation}-${stepIndex}`;
113
+ const startTime = this.commandStartTimes.get(commandId) || Date.now();
114
+ const endTime = Date.now();
115
+ const duration = endTime - startTime;
116
+
117
+ const commandType = this.getCommandType(step);
118
+ const description = this.getStepDescription(step);
119
+
120
+ // Extract sent data from context
121
+ const dataSent = context.variables?.get('lastSentData');
122
+ const dataExpected = this.getExpectedData(step);
123
+
124
+ // Convert dataSent to byte array format if it exists
125
+ const dataSentArray = dataSent ? [...dataSent] : undefined;
126
+
127
+ this.logger
128
+ .withMetadata({
129
+ commandType,
130
+ dataExpected,
131
+ dataSent: dataSentArray,
132
+ description,
133
+ duration,
134
+ endTime,
135
+ error: error.message,
136
+ operation,
137
+ startTime,
138
+ stepIndex,
139
+ success: false,
140
+ totalSteps,
141
+ })
142
+ .info('Command failed');
143
+ }
144
+
145
+ private getCommandType(step: UIProtocolStep): string {
146
+ if ('sendReceive' in step) {
147
+ return 'sendReceive';
148
+ }
149
+ if ('send' in step) {
150
+ return 'send';
151
+ }
152
+ if ('receive' in step) {
153
+ return 'receive';
154
+ }
155
+ if ('readSegment' in step) {
156
+ return 'readSegment';
157
+ }
158
+ if ('writeSegment' in step) {
159
+ return 'writeSegment';
160
+ }
161
+ if ('setVariable' in step) {
162
+ return 'setVariable';
163
+ }
164
+ return 'unknown';
165
+ }
166
+
167
+ private getStepDescription(step: UIProtocolStep): string {
168
+ if ('sendReceive' in step && step.sendReceive.description) {
169
+ return step.sendReceive.description;
170
+ }
171
+ if ('send' in step && step.send.description) {
172
+ return step.send.description;
173
+ }
174
+ if ('receive' in step && step.receive.description) {
175
+ return step.receive.description;
176
+ }
177
+ if ('readSegment' in step && step.readSegment.description) {
178
+ return step.readSegment.description;
179
+ }
180
+ if ('writeSegment' in step && step.writeSegment.description) {
181
+ return step.writeSegment.description;
182
+ }
183
+ return 'No description';
184
+ }
185
+
186
+ private getExpectedData(step: UIProtocolStep): any {
187
+ if ('sendReceive' in step && step.sendReceive.receive) {
188
+ return step.sendReceive.receive;
189
+ }
190
+ if ('receive' in step) {
191
+ return step.receive;
192
+ }
193
+ if ('readSegment' in step) {
194
+ // For readSegment, return both start and end chunk receive patterns
195
+ return {
196
+ endChunk: step.readSegment.endChunk.receive,
197
+ startChunk: step.readSegment.startChunk.receive,
198
+ type: 'readSegment',
199
+ };
200
+ }
201
+ return undefined;
202
+ }
203
+ }