mychron 0.3.2

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.
data/PROTOCOL.md ADDED
@@ -0,0 +1,848 @@
1
+ # AiM MyChron 6 Protocol Documentation
2
+
3
+ This document describes the reverse-engineered communication protocols used by AiM MyChron 6 kart data loggers. The protocols were discovered through packet capture analysis of Race Studio 3 communications.
4
+
5
+ ## Table of Contents
6
+
7
+ 1. [Device Overview](#device-overview)
8
+ 2. [UDP Discovery Protocol](#udp-discovery-protocol)
9
+ 3. [TCP Data Protocol](#tcp-data-protocol)
10
+ 4. [Session Listing](#session-listing)
11
+ 5. [File Download Protocol](#file-download-protocol)
12
+ 6. [Protocol State Machine](#protocol-state-machine)
13
+ 7. [Magic Bytes Reference](#magic-bytes-reference)
14
+
15
+ ---
16
+
17
+ ## Device Overview
18
+
19
+ ### Hardware Specifications
20
+
21
+ | Property | Value |
22
+ |----------|-------|
23
+ | Device | AiM MyChron 6 Kart Data Logger |
24
+ | WiFi Chip | Espressif ESP32 |
25
+ | MAC OUI | `88:57:21` (Espressif) |
26
+ | Discovery Port | UDP 36002 |
27
+ | Data Port | TCP 2000 |
28
+ | File Formats | `.xrz`, `.xrk`, `.hrz`, `.drk` |
29
+
30
+ ### WiFi Modes
31
+
32
+ 1. **AP Mode**: Device creates hotspot with SSID `AiM-MYC6-XXXXXX`
33
+ 2. **WLAN Mode**: Device joins existing network (recommended for this tool)
34
+
35
+ ### Known Test Device
36
+
37
+ - Device Name: `JD-AT`
38
+ - WiFi Config: `Wifi P2`
39
+ - IP Address: `192.168.1.29`
40
+
41
+ ---
42
+
43
+ ## UDP Discovery Protocol
44
+
45
+ **Port**: 36002
46
+ **Purpose**: Discover MyChron devices on the local network
47
+
48
+ ### Request
49
+
50
+ Send a 6-byte ASCII beacon:
51
+
52
+ ```
53
+ Payload: "aim-ka"
54
+ Hex: 61 69 6D 2D 6B 61
55
+ ```
56
+
57
+ **Transmission Methods**:
58
+ - **Broadcast**: Send to `255.255.255.255:36002`
59
+ - **Unicast**: Send to specific IP address
60
+
61
+ ### Response
62
+
63
+ The device responds with a 236-byte binary packet:
64
+
65
+ ```
66
+ Offset | Length | Field | Description
67
+ --------|--------|--------------------|---------------------------------
68
+ 0x00 | 4 | Magic | 0xEC000000
69
+ 0x04 | 4 | Version | 0x02000000
70
+ 0x08 | 4 | Device IP | Little-endian 32-bit IP address
71
+ 0x0C | 8 | Flags/Status | Device state information
72
+ 0x14 | 48 | Device Name | Null-terminated ASCII (e.g., "JD-AT")
73
+ 0x44 | 16 | Reserved | Unknown data
74
+ 0x54 | 4 | IDN Marker | "idn" + protocol version byte (0x01)
75
+ 0x58 | 6 | Firmware Version | 3x uint16 LE (e.g., 56.355.437)
76
+ 0x5E | 2 | Reserved | Unknown data
77
+ 0x60 | 4 | Serial Number | uint32 LE (e.g., 35016462)
78
+ 0x64 | 48 | Reserved | Unknown data
79
+ 0x94 | 48 | WiFi Config Name | Null-terminated ASCII (e.g., "Wifi P2")
80
+ 0xC4 | 52 | Reserved | Unknown data (total: 236 bytes)
81
+ ```
82
+
83
+ ### Response Parsing
84
+
85
+ ```ruby
86
+ def parse_response(data)
87
+ return nil unless data.length >= 236
88
+
89
+ device_name = data[0x14, 48].unpack1('Z*').gsub(/[^[:print:]]/, '')
90
+ wifi_name = data[0x94, 48].unpack1('Z*').gsub(/[^[:print:]]/, '')
91
+ serial_number = data[0x60, 4].unpack1('V') # uint32 little-endian
92
+ fw_v1, fw_v2, fw_v3 = data[0x58, 6].unpack('v3') # 3x uint16 LE
93
+ firmware_version = "#{fw_v1}.#{fw_v2}.#{fw_v3}"
94
+
95
+ { name: device_name, wifi: wifi_name, serial: serial_number, firmware: firmware_version }
96
+ end
97
+ ```
98
+
99
+ ### Implementation Notes
100
+
101
+ - Use `UDPSocket` with `SO_BROADCAST` for broadcast discovery
102
+ - Timeout: 2 seconds is sufficient for local network
103
+ - Multiple devices may respond; collect all responses
104
+ - Handle `EHOSTUNREACH`, `ENETUNREACH` gracefully (device may be asleep)
105
+
106
+ ---
107
+
108
+ ## TCP Data Protocol
109
+
110
+ **Port**: 2000
111
+ **Purpose**: Session listing and file downloads
112
+
113
+ ### Framing Structure
114
+
115
+ All TCP packets use consistent binary framing with ASCII markers:
116
+
117
+ ```
118
+ [Header Marker] [CMD] [00] [4-byte suffix] [Payload] [Tail Marker] [tail bytes]
119
+ ```
120
+
121
+ #### Header Markers (6 bytes)
122
+
123
+ | Marker | Hex | Usage |
124
+ |--------|-----|-------|
125
+ | `<hSTCP` | `3C 68 53 54 43 50` | Standard commands |
126
+ | `<hSTNC` | `3C 68 53 54 4E 43` | File/data commands |
127
+
128
+ #### Tail Markers (8 bytes: 5 marker + 2 checksum + 1 close)
129
+
130
+ | Marker | Hex | Usage |
131
+ |--------|-----|-------|
132
+ | `<STCP` | `3C 53 54 43 50` | Standard commands |
133
+ | `<STNC` | `3C 53 54 4E 43` | File/data commands |
134
+
135
+ #### Tail Checksum
136
+
137
+ The 2 bytes after the tail marker are a **16-bit little-endian checksum** of the payload:
138
+
139
+ ```
140
+ checksum = sum_of_all_payload_bytes
141
+ tail = [Marker] [checksum & 0xFF] [(checksum >> 8) & 0xFF] [0x3E]
142
+ ```
143
+
144
+ The final byte is always `0x3E` (`>`).
145
+
146
+ **Examples** (verified across all packet types):
147
+
148
+ - Handshake payload `[00 00 00 00 06 08 00 00]` → sum=14 → tail `0E 00 3E`
149
+ - Download `a_0077.xrz` → sum=1327=0x052F → tail `2F 05 3E`
150
+ - Download `a_0065.xrz` → sum=1324=0x052C → tail `2C 05 3E`
151
+ - List query (type 0x51) → sum=1104=0x0450 → tail `50 04 3E`
152
+
153
+ ### Command Bytes
154
+
155
+ | Command | Value | Description |
156
+ |---------|-------|-------------|
157
+ | `CMD_PING` | `0x08` | Handshake/ping |
158
+ | `CMD_DATA` | `0x40` | Data operations |
159
+ | `CMD_LIST` | `0x44` | List operations |
160
+ | `CMD_ACK` | `0x04` | Acknowledgment |
161
+
162
+ ### Connection Handshake
163
+
164
+ Before any operation, a handshake must be performed:
165
+
166
+ **Handshake Request** (32 bytes):
167
+ ```
168
+ <hSTCP 08 00 00 00 00 3E Header (12 bytes)
169
+ 00 00 00 00 06 08 00 00 Payload (8 bytes)
170
+ <STCP 0E 00 3E Tail (8 bytes)
171
+ ```
172
+
173
+ **Handshake Response**:
174
+ ```
175
+ <hSTCP 08 00 00 00 00 3E (minimum expected)
176
+ [additional data may follow]
177
+ ```
178
+
179
+ ---
180
+
181
+ ## Session Listing
182
+
183
+ **Purpose**: Retrieve list of recorded sessions on device
184
+
185
+ > **IMPORTANT**: Session listing requires a specific multi-step protocol sequence.
186
+ > A single request does NOT work - the full sequence below must be followed.
187
+
188
+ ### Protocol Sequence (Verified 2026-02-04)
189
+
190
+ The session listing protocol requires the following sequence:
191
+
192
+ | Step | Type | Frame | Description |
193
+ |------|------|-------|-------------|
194
+ | 1 | Handshake | `<hSTCP` 0x08 | Connection handshake (28 bytes) |
195
+ | 2 | Init | `<hSTNC` 0x10 | Initialize session (84 bytes) |
196
+ | 3 | Date Query | `<hSTCP` 0x44 | Date range query with current date (88 bytes) |
197
+ | 4 | ACK | `<hSTCP` 0x04 | Acknowledge date query (24 bytes) |
198
+ | 5 | Query | `<hSTNC` 0x51 | Request session list (84 bytes) |
199
+ | 6 | Finalize | `<hSTNC` 0x24 | Trigger CSV output (84 bytes) |
200
+ | 7 | ACK | `<hSTCP` 0x04 | Signal ready for data (24 bytes) |
201
+ | 8 | Response | `<hSTCPJ` 0x4A | Device sends CSV data |
202
+
203
+ ### Step 2: Init Request (Type 0x10)
204
+
205
+ **Total Size**: 84 bytes (12 header + 64 payload + 8 tail)
206
+
207
+ ```
208
+ Header: <hSTNC 40 00 00 00 00 3E
209
+ Payload (64 bytes):
210
+ Offset 8-9: 0x10 0x00 (type = init)
211
+ Offset 10-11: 0x01 0x00 (flags)
212
+ Offset 16-19: 0x40 0x00 0x00 0x00
213
+ Offset 24-27: 0x01 0x00 0x00 0x00 (count)
214
+ Tail: <STNC 52 00 3E
215
+ ```
216
+
217
+ ### Step 3: Date Query (Type 0x44)
218
+
219
+ **Total Size**: 88 bytes (12 header + 68 payload + 8 tail)
220
+
221
+ > **CRITICAL**: Must use current date values, not wide ranges like 2020-2030.
222
+ > Wide date ranges return 0 sessions.
223
+
224
+ ```
225
+ Header: <hSTCP 44 00 00 00 00 3E
226
+ Payload (68 bytes):
227
+ Offset 12-15: year (uint32 LE) - current year (e.g., 0xEA07 = 2026)
228
+ Offset 16-19: month (uint32 LE) - current month
229
+ Offset 20-23: day (uint32 LE) - 1 (first day of month)
230
+ Offset 24-27: param1 (uint32 LE) - 0x15 (21)
231
+ Offset 28-31: type (uint32 LE) - 0x07
232
+ Offset 44-47: year (uint32 LE) - same year
233
+ Offset 48-51: month (uint32 LE) - same month
234
+ Offset 52-55: day (uint32 LE) - 1
235
+ Offset 56-59: param2 (uint32 LE) - 0x16 (22)
236
+ Offset 60-63: type (uint32 LE) - 0x07
237
+ Tail: <STCP 21 02 3E
238
+ ```
239
+
240
+ ### Step 5: Query Request (Type 0x51)
241
+
242
+ **Total Size**: 84 bytes
243
+
244
+ ```
245
+ Header: <hSTNC 40 00 00 00 00 3E
246
+ Payload (64 bytes):
247
+ Offset 8-9: 0x51 0x00 (type = Query)
248
+ Offset 10-11: 0x02 0x00 (flags)
249
+ Offset 24-27: 0x01 0x00 0x00 0x00 (count)
250
+ Offset 32-35: 0xFF 0xFF 0xFF 0xFF (all dates)
251
+ Tail: <STNC 50 04 3E
252
+ ```
253
+
254
+ ### Step 6: Finalize Query (Type 0x24)
255
+
256
+ **Total Size**: 84 bytes
257
+
258
+ > **CRITICAL**: Type 0x51 alone does NOT return CSV. Must send Type 0x24 after.
259
+
260
+ ```
261
+ Header: <hSTNC 40 00 00 00 00 3E
262
+ Payload (64 bytes):
263
+ Offset 8-9: 0x24 0x00 (type = finalize)
264
+ Offset 10-11: 0x02 0x00 (flags)
265
+ Offset 24-27: 0x01 0x00 0x00 0x00 (count)
266
+ Tail: <STNC 27 00 3E
267
+ ```
268
+
269
+ ### Response Format
270
+
271
+ After the finalize query and ACK, the device sends `<hSTCPJ` (type 0x4A) followed by CSV:
272
+
273
+ ```
274
+ <hSTCPJ....>
275
+ name,size,date,hour,nlap,nbest,best,pilota,track_name,veicolo,campionato,venue_type,mode,trk_type,motivolap,maxvel,device,track_lat,track_lon,test_dur,...
276
+ a_0079.xrz,2518552,25/01/2026,13:21:40,13,10,66282,,GuerreroK,,,,speed,closed,stop,1079590912,JD-AT,395074512,-7469880,822949,,,,,,
277
+ a_0078.xrz,733838,25/01/2026,12:53:13,1,,,,GuerreroK,,,,speed,closed,stop,1072064102,JD-AT,395074512,-7469880,270928,,,,,,
278
+ ...
279
+ ```
280
+
281
+ ### CSV Field Definitions
282
+
283
+ | Index | Field | Type | Description |
284
+ |-------|-------|------|-------------|
285
+ | 0 | `name` | string | Filename (e.g., `a_0052.xrz`) |
286
+ | 1 | `size` | integer | File size in bytes |
287
+ | 2 | `date` | string | Recording date (DD/MM/YYYY) |
288
+ | 3 | `hour` | string | Recording time (HH:MM:SS) |
289
+ | 4 | `nlap` | integer | Number of laps |
290
+ | 5 | `nbest` | integer | Best lap number |
291
+ | 6 | `best` | integer | Best lap time (milliseconds) |
292
+ | 7 | `pilota` | string | Driver name (usually empty) |
293
+ | 8 | `track_name` | string | Track name |
294
+ | 9 | `veicolo` | string | Vehicle name |
295
+ | 10 | `campionato` | string | Championship |
296
+ | 11 | `venue_type` | string | Venue type |
297
+ | 12 | `mode` | string | Speed type (`speed`, etc.) |
298
+ | 13 | `trk_type` | string | Track type (`closed`, `open`) |
299
+ | 14 | `motivolap` | string | Stop mode (`stop`, `pause`) |
300
+ | 15 | `maxvel` | integer | Max velocity (float as int) |
301
+ | 16 | `device` | string | Device name (e.g., `JD-AT`) |
302
+ | 17 | `track_lat` | integer | Track latitude (scaled) |
303
+ | 18 | `track_lon` | integer | Track longitude (scaled) |
304
+ | 19 | `test_dur` | integer | Test duration (milliseconds) |
305
+
306
+ ### Parsing Sessions
307
+
308
+ ```ruby
309
+ SessionInfo = Struct.new(
310
+ :filename, :size, :date, :time, :laps, :best_lap,
311
+ :best_time, :driver, :device_name, :timestamp,
312
+ keyword_init: true
313
+ )
314
+
315
+ def parse_csv_line(line)
316
+ fields = line.split(',')
317
+ SessionInfo.new(
318
+ filename: fields[0],
319
+ size: fields[1].to_i,
320
+ date: fields[2],
321
+ time: fields[3],
322
+ laps: fields[4].to_i,
323
+ best_lap: fields[5].to_i,
324
+ best_time: fields[6].to_i,
325
+ driver: fields[8],
326
+ device_name: fields[16],
327
+ timestamp: fields[17].to_i
328
+ )
329
+ end
330
+ ```
331
+
332
+ ---
333
+
334
+ ## File Download Protocol
335
+
336
+ **Command**: `0x40` with `<hSTNC>` markers
337
+ **Purpose**: Download session files from device
338
+
339
+ ### IMPORTANT: Connection State
340
+
341
+ **CRITICAL**: Download MUST use a fresh connection. The session listing protocol
342
+ (8-step sequence with init, date query, 0x51, 0x24) leaves the device in a
343
+ different state that is incompatible with the download protocol.
344
+
345
+ If you call `list_sessions` then try to download on the same connection,
346
+ the device will return 0 bytes. **Always disconnect and reconnect before downloading.**
347
+
348
+ ```ruby
349
+ # WRONG - download fails after session listing
350
+ client.connect
351
+ sessions = client.list_sessions
352
+ data = client.download_session(filename) # Returns 0 bytes!
353
+
354
+ # CORRECT - use separate connections
355
+ client.connect
356
+ sessions = client.list_sessions
357
+ client.disconnect
358
+
359
+ client.connect # Fresh connection
360
+ data = client.download_session(filename) # Works!
361
+ ```
362
+
363
+ ### Download Request Packet
364
+
365
+ **Total Size**: 85 bytes
366
+
367
+ ```
368
+ Header (12 bytes):
369
+ 3C 68 53 54 4E 43 <hSTNC
370
+ 40 00 CMD_DATA, 0x00
371
+ 00 00 00 3E Suffix
372
+
373
+ Payload (64 bytes):
374
+ 00 00 00 00 00 00 00 00 Offset 0-7: Zeros
375
+ 02 00 04 00 Offset 8-11: Flags (CRITICAL - see below)
376
+ 00 00 00 00 00 00 00 00 Offset 12-19: Zeros
377
+ 00 00 00 00 Offset 20-23: Zeros
378
+ 01 00 00 00 Offset 24-27: Count (1)
379
+ 00 00 00 00 Offset 28-31: Zeros
380
+ [file_path, 24 bytes] Offset 32-55: File path
381
+ 00 00 00 00 00 00 00 00 Offset 56-63: Zeros
382
+
383
+ Tail (8 bytes):
384
+ 3C 53 54 4E 43 <STNC
385
+ 2C 05 3E Tail bytes
386
+ ```
387
+
388
+ ### File Path Format
389
+
390
+ ```
391
+ Path: "1:/mem/<filename>\0"
392
+ Example: "1:/mem/a_0077.xrz\0"
393
+ Max Length: 24 bytes (including null terminator)
394
+ ```
395
+
396
+ ### Download Flags
397
+
398
+ ```
399
+ Offset 8-11: 02 00 04 00
400
+
401
+ Byte 0 (0x02): Type indicator - file download
402
+ Byte 1 (0x00): Reserved
403
+ Byte 2 (0x04): CRITICAL - Marks file as "downloaded" in Race Studio 3
404
+ This flag does NOT delete the file from the device.
405
+ Without this flag, RS3 continues to show file as "new"
406
+ Byte 3 (0x00): Reserved
407
+ ```
408
+
409
+ ### File Info Response
410
+
411
+ After the download request, the device sends file metadata. Extract file size:
412
+
413
+ **Pattern 1**: Look for marker `0xC0 0xFF 0x00 0x00`
414
+ - File size is 4 bytes BEFORE this marker
415
+ - Unpack as little-endian 32-bit unsigned integer
416
+
417
+ **Pattern 2**: Look for flags `0x02 0x00 0x04 0x00`
418
+ - File size is at offset +8 from flags
419
+ - Unpack as little-endian 32-bit unsigned integer
420
+
421
+ ```ruby
422
+ def extract_file_size(data)
423
+ # Pattern 1: Look for 0xC0FF0000 marker
424
+ idx = data.index("\xC0\xFF\x00\x00")
425
+ if idx && idx >= 4
426
+ return data[idx - 4, 4].unpack1('V')
427
+ end
428
+
429
+ # Pattern 2: Look for flags 0x02000400
430
+ idx = data.index("\x02\x00\x04\x00")
431
+ if idx
432
+ return data[idx + 8, 4].unpack1('V')
433
+ end
434
+
435
+ nil
436
+ end
437
+ ```
438
+
439
+ ### Data Transfer with ACK Protocol
440
+
441
+ **CRITICAL**: The device sends data in ~64KB chunks and waits for acknowledgment before sending more. Failure to send ACKs causes timeout and connection reset.
442
+
443
+ #### ACK Packet Structure
444
+
445
+ ```
446
+ Header (12 bytes):
447
+ 3C 68 53 54 43 50 <hSTCP
448
+ 04 00 CMD_ACK, 0x00
449
+ 00 00 00 3E Suffix
450
+
451
+ Payload (4 bytes):
452
+ [counter_low] [counter_high] [seq_low] [seq_high]
453
+
454
+ Tail (8 bytes):
455
+ 3C 53 54 43 50 <STCP
456
+ [tail_byte] 01 3E
457
+ ```
458
+
459
+ #### ACK Counter Mechanics
460
+
461
+ ```
462
+ Initial counter: 0xFFC0
463
+ Decrement: 0x40 per ACK
464
+ Sequence: Starts at 0, increments by 1 per ACK
465
+
466
+ Counter progression:
467
+ ACK #0: 0xFFC0
468
+ ACK #1: 0xFF80
469
+ ACK #2: 0xFF40
470
+ ACK #3: 0xFF00
471
+ ACK #4: 0xFEC0 (high byte wraps)
472
+ ACK #5: 0xFE80
473
+ ...
474
+
475
+ Tail byte calculation:
476
+ high_byte_drops = 0xFF - counter_high
477
+ tail_byte = (counter_low + seq - 1 - high_byte_drops) & 0xFF
478
+ ```
479
+
480
+ #### ACK Examples from Capture
481
+
482
+ | ACK # | Counter | Seq | Payload | Tail |
483
+ |-------|---------|-----|---------|------|
484
+ | 0 | 0xFFC0 | 0 | `C0 FF 00 00` | `BF 01 3E` |
485
+ | 1 | 0xFF80 | 1 | `80 FF 01 00` | `80 01 3E` |
486
+ | 2 | 0xFF40 | 2 | `40 FF 02 00` | `41 01 3E` |
487
+ | 3 | 0xFF00 | 3 | `00 FF 03 00` | `02 01 3E` |
488
+ | 4 | 0xFEC0 | 4 | `C0 FE 04 00` | `C2 01 3E` |
489
+ | 5 | 0xFE80 | 5 | `80 FE 05 00` | `83 01 3E` |
490
+
491
+ #### Building ACK Packets
492
+
493
+ ```ruby
494
+ def build_ack_packet(chunk_number)
495
+ # Calculate counter (starts at 0xFFC0, decrements by 0x40)
496
+ counter = 0xFFC0 - (chunk_number * 0x40)
497
+ counter_low = counter & 0xFF
498
+ counter_high = (counter >> 8) & 0xFF
499
+
500
+ # Sequence number
501
+ seq_low = chunk_number & 0xFF
502
+ seq_high = (chunk_number >> 8) & 0xFF
503
+
504
+ # Calculate tail byte
505
+ high_byte_drops = 0xFF - counter_high
506
+ tail_byte = (counter_low + chunk_number - 1 - high_byte_drops) & 0xFF
507
+
508
+ # Build packet
509
+ header = "<hSTCP\x04\x00\x00\x00\x00\x3E"
510
+ payload = [counter_low, counter_high, seq_low, seq_high].pack('CCCC')
511
+ tail = "<STCP" + [tail_byte, 0x01, 0x3E].pack('CCC')
512
+
513
+ header + payload + tail
514
+ end
515
+ ```
516
+
517
+ ### Data Reception Algorithm
518
+
519
+ ```ruby
520
+ def download_file(socket, filename)
521
+ file_size = receive_file_info(socket)
522
+ return nil unless file_size
523
+
524
+ data = ""
525
+ chunk_number = 0
526
+ bytes_since_ack = 0
527
+
528
+ # Send initial ACK
529
+ socket.write(build_ack_packet(chunk_number))
530
+ chunk_number += 1
531
+
532
+ while data.length < file_size
533
+ chunk = socket.recv_nonblock(65536) rescue nil
534
+
535
+ if chunk && !chunk.empty?
536
+ # Strip protocol markers (16-byte data frame headers + 8-byte tails)
537
+ clean_data = strip_markers(chunk)
538
+ data << clean_data
539
+ bytes_since_ack += clean_data.length
540
+
541
+ # Send ACK every ~64KB
542
+ if bytes_since_ack >= 65536
543
+ socket.write(build_ack_packet(chunk_number))
544
+ chunk_number += 1
545
+ bytes_since_ack = 0
546
+ end
547
+ else
548
+ # No data - send ACK if pending
549
+ if bytes_since_ack > 0
550
+ socket.write(build_ack_packet(chunk_number))
551
+ chunk_number += 1
552
+ bytes_since_ack = 0
553
+ end
554
+ end
555
+ end
556
+
557
+ data
558
+ end
559
+ ```
560
+
561
+ ### Marker Stripping
562
+
563
+ Remove protocol overhead from received data.
564
+
565
+ **IMPORTANT**: Data frame headers during download are **16 bytes**, not 12.
566
+ The standard 12-byte header is followed by a 4-byte counter field:
567
+
568
+ ```text
569
+ Data frame header (16 bytes):
570
+ <hSTCP (6) + cmd (1) + flag (1) + suffix (4) + counter (4)
571
+
572
+ Counter values:
573
+ - 00 00 00 00 for the initial frame
574
+ - C0 FF 00 00 after chunk 0 ACK (counter = 0xFFC0)
575
+ - 80 FF 01 00 after chunk 1 ACK (counter = 0xFF80)
576
+ - etc.
577
+
578
+ Each ~65KB download chunk structure:
579
+ 16-byte header + ~65,472 bytes data + 8-byte tail = 65,496 bytes
580
+ ```
581
+
582
+ ```ruby
583
+ def strip_markers(data)
584
+ result = data.dup
585
+
586
+ # Remove header markers (16 bytes each for data frames)
587
+ while (idx = result.index("<hSTCP"))
588
+ result.slice!(idx, 16)
589
+ end
590
+ while (idx = result.index("<hSTNC"))
591
+ result.slice!(idx, 16)
592
+ end
593
+
594
+ # Remove tail markers (8 bytes each)
595
+ while (idx = result.index("<STCP"))
596
+ result.slice!(idx, 8)
597
+ end
598
+ while (idx = result.index("<STNC"))
599
+ result.slice!(idx, 8)
600
+ end
601
+
602
+ result
603
+ end
604
+ ```
605
+
606
+ ---
607
+
608
+ ## Protocol State Machine
609
+
610
+ ```
611
+ ┌─────────────┐
612
+ │ START │
613
+ └──────┬──────┘
614
+
615
+
616
+ ┌──────────────────┐
617
+ │ TCP Connect :2000│
618
+ └────────┬─────────┘
619
+
620
+
621
+ ┌──────────────────┐
622
+ │ Send Handshake │
623
+ │ <hSTCP 08 00...> │
624
+ └────────┬─────────┘
625
+
626
+
627
+ ┌──────────────────┐
628
+ │ Recv Handshake │
629
+ │ Response │
630
+ └────────┬─────────┘
631
+
632
+ ┌────────────────┴────────────────┐
633
+ │ │
634
+ ▼ ▼
635
+ ┌────────────────┐ ┌────────────────┐
636
+ │ LIST SESSIONS │ │ DOWNLOAD FILE │
637
+ └───────┬────────┘ └───────┬────────┘
638
+ │ │
639
+ ▼ ▼
640
+ ┌────────────────┐ ┌────────────────┐
641
+ │ Send List Req │ │ Send Download │
642
+ │ <hSTNC 40 00..>│ │ Request │
643
+ └───────┬────────┘ └───────┬────────┘
644
+ │ │
645
+ ▼ ▼
646
+ ┌────────────────┐ ┌────────────────┐
647
+ │ Recv CSV Data │ │ Recv File Info │
648
+ │ (in <STNC>) │ │ (extract size) │
649
+ └───────┬────────┘ └───────┬────────┘
650
+ │ │
651
+ ▼ ▼
652
+ ┌────────────────┐ ┌────────────────┐
653
+ │ Parse Sessions │ │ Send ACK #0 │
654
+ └───────┬────────┘ └───────┬────────┘
655
+ │ │
656
+ │ ▼
657
+ │ ┌────────────────┐
658
+ │ ┌───▶│ Recv Data Chunk│
659
+ │ │ └───────┬────────┘
660
+ │ │ │
661
+ │ │ ▼
662
+ │ │ ┌────────────────┐
663
+ │ │ │ Strip Markers │
664
+ │ │ │ Accumulate Data│
665
+ │ │ └───────┬────────┘
666
+ │ │ │
667
+ │ │ ▼
668
+ │ │ ┌────────────────┐
669
+ │ │ │ >= 64KB chunk? │
670
+ │ │ └───────┬────────┘
671
+ │ │ │
672
+ │ │ Yes │ No
673
+ │ │ ┌───────┴───────┐
674
+ │ │ ▼ │
675
+ │ │ ┌────────────────┐ │
676
+ │ │ │ Send ACK #N │ │
677
+ │ │ └───────┬────────┘ │
678
+ │ │ │ │
679
+ │ │ ┌────┴──────────┘
680
+ │ │ ▼
681
+ │ │ ┌────────────────┐
682
+ │ │ │ All data recv? │
683
+ │ │ └───────┬────────┘
684
+ │ │ │
685
+ │ │ No │ Yes
686
+ │ └─────────┘ │
687
+ │ ▼
688
+ │ ┌────────────────┐
689
+ │ │ Verify Size │
690
+ │ └───────┬────────┘
691
+ │ │
692
+ └──────────────┬───────────────────┘
693
+
694
+
695
+ ┌────────────────┐
696
+ │ Close Socket │
697
+ └───────┬────────┘
698
+
699
+
700
+ ┌────────────────┐
701
+ │ END │
702
+ └────────────────┘
703
+ ```
704
+
705
+ ---
706
+
707
+ ## Magic Bytes Reference
708
+
709
+ ### Protocol Headers
710
+
711
+ | Marker | ASCII | Hex | Length | Usage |
712
+ |--------|-------|-----|--------|-------|
713
+ | Header Standard | `<hSTCP` | `3C 68 53 54 43 50` | 6 | Handshake, ACK |
714
+ | Header Data | `<hSTNC` | `3C 68 53 54 4E 43` | 6 | List, Download |
715
+ | Tail Standard | `<STCP` | `3C 53 54 43 50` | 5 | Handshake, ACK |
716
+ | Tail Data | `<STNC` | `3C 53 54 4E 43` | 5 | List, Download |
717
+
718
+ ### UDP Discovery
719
+
720
+ | Name | Value | Notes |
721
+ |------|-------|-------|
722
+ | Beacon | `aim-ka` | 6 bytes ASCII |
723
+ | Port | `36002` | UDP |
724
+ | Response Size | `236` | bytes |
725
+ | Device Name Offset | `0x14` | 48 bytes max |
726
+ | Firmware Version Offset | `0x58` | 3x uint16 LE |
727
+ | Serial Number Offset | `0x60` | uint32 LE |
728
+ | WiFi Name Offset | `0x94` | 48 bytes max |
729
+
730
+ ### TCP Commands
731
+
732
+ | Command | Value | Description |
733
+ |---------|-------|-------------|
734
+ | Ping/Handshake | `0x08` | Connection handshake |
735
+ | Data | `0x40` | Data operations |
736
+ | List | `0x44` | List operations |
737
+ | ACK | `0x04` | Data acknowledgment |
738
+
739
+ ### Download Flags
740
+
741
+ | Offset | Value | Purpose |
742
+ |--------|-------|---------|
743
+ | 8-11 | `02 00 04 00` | Standard download, mark as downloaded |
744
+
745
+ ### ACK Constants
746
+
747
+ | Name | Value | Notes |
748
+ |------|-------|-------|
749
+ | Initial Counter | `0xFFC0` | Decrements by 0x40 |
750
+ | Chunk Size | `65536` | ~64KB triggers ACK |
751
+ | Tail Byte 2 | `0x01` | Always 0x01 |
752
+ | Tail Byte 3 | `0x3E` | Always 0x3E |
753
+
754
+ ---
755
+
756
+ ## Error Handling
757
+
758
+ ### Connection Errors
759
+
760
+ | Error | Cause | Recovery |
761
+ |-------|-------|----------|
762
+ | `EHOSTUNREACH` | Device in sleep mode | Wake device, retry |
763
+ | `ECONNREFUSED` | Port 2000 not responding | Check device state |
764
+ | `ENETUNREACH` | Network unreachable | Check WiFi connection |
765
+ | `ETIMEDOUT` | Connection timeout | Increase timeout, retry |
766
+ | `ECONNRESET` | Connection reset by peer | Missing ACK, restart |
767
+
768
+ ### Protocol Errors
769
+
770
+ | Error | Cause | Recovery |
771
+ |-------|-------|----------|
772
+ | Invalid handshake | Wrong marker in response | Reconnect |
773
+ | File size 0 | Parse failure | Try alternate pattern |
774
+ | Incomplete download | Missing ACKs | Implement proper ACK |
775
+ | CSV parse error | Unexpected format | Skip malformed lines |
776
+
777
+ ---
778
+
779
+ ## Timeouts
780
+
781
+ | Operation | Timeout | Notes |
782
+ |-----------|---------|-------|
783
+ | UDP Discovery | 2 seconds | Per broadcast |
784
+ | TCP Connect | 5 seconds | Initial connection |
785
+ | Read Timeout | 30 seconds | Per recv operation |
786
+ | Total Transfer | 300 seconds | 5 minutes max |
787
+ | ACK Response | 1-2 seconds | Device wait time |
788
+
789
+ ---
790
+
791
+ ## Appendix: Packet Captures
792
+
793
+ ### Handshake Exchange
794
+
795
+ **Client Request:**
796
+ ```
797
+ 3C 68 53 54 43 50 08 00 00 00 00 3E <hSTCP....>
798
+ 00 00 00 00 06 08 00 00 ........
799
+ 3C 53 54 43 50 0E 00 3E <STCP..>
800
+ ```
801
+
802
+ **Device Response:**
803
+ ```
804
+ 3C 68 53 54 43 50 08 00 00 00 00 3E <hSTCP....>
805
+ [additional bytes...]
806
+ ```
807
+
808
+ ### Session List Request
809
+
810
+ **Client Request:**
811
+ ```
812
+ 3C 68 53 54 4E 43 40 00 00 00 00 3E <hSTNC@...>
813
+ 00 00 00 00 00 00 00 00 07 00 01 00 ............
814
+ 00 00 00 00 00 00 00 00 00 00 00 00 ............
815
+ 00 00 01 00 00 00 00 00 00 00 00 00 ............
816
+ 00 00 00 00 00 00 00 00 00 00 00 00 ............
817
+ 00 00 00 00 00 00 00 00 00 00 00 00 ............
818
+ 00 00 00 00 ....
819
+ 3C 53 54 4E 43 09 00 3E <STNC..>
820
+ ```
821
+
822
+ ### Download Request (file: a_0077.xrz)
823
+
824
+ **Client Request:**
825
+ ```
826
+ 3C 68 53 54 4E 43 40 00 00 00 00 3E <hSTNC@...>
827
+ 00 00 00 00 00 00 00 00 02 00 04 00 ............
828
+ 00 00 00 00 00 00 00 00 00 00 00 00 ............
829
+ 01 00 00 00 00 00 00 00 ........
830
+ 31 3A 2F 6D 65 6D 2F 61 5F 30 30 37 1:/mem/a_007
831
+ 37 2E 78 72 7A 00 00 00 00 00 00 00 7.xrz.......
832
+ 00 00 00 00 00 00 00 00 ........
833
+ 3C 53 54 4E 43 2C 05 3E <STNC,.>
834
+ ```
835
+
836
+ ### ACK Packet (chunk 0)
837
+
838
+ **Client ACK:**
839
+ ```
840
+ 3C 68 53 54 43 50 04 00 00 00 00 3E <hSTCP....>
841
+ C0 FF 00 00 ....
842
+ 3C 53 54 43 50 BF 01 3E <STCP..>
843
+ ```
844
+
845
+ ---
846
+
847
+ *Documentation generated from reverse engineering of Race Studio 3 protocol communications.*
848
+ *Last updated: 2026-03-24*