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.
- checksums.yaml +7 -0
- data/Gemfile +5 -0
- data/IMPLEMENTATION_NOTES.md +539 -0
- data/LICENSE +21 -0
- data/PROTOCOL.md +848 -0
- data/README.md +413 -0
- data/lib/mychron/configuration.rb +112 -0
- data/lib/mychron/device.rb +196 -0
- data/lib/mychron/discovery/detector.rb +242 -0
- data/lib/mychron/discovery/scorer.rb +123 -0
- data/lib/mychron/errors.rb +27 -0
- data/lib/mychron/logging.rb +79 -0
- data/lib/mychron/monitor/watcher.rb +165 -0
- data/lib/mychron/network/arp.rb +257 -0
- data/lib/mychron/network/http_probe.rb +121 -0
- data/lib/mychron/network/scanner.rb +167 -0
- data/lib/mychron/protocol/client.rb +946 -0
- data/lib/mychron/protocol/discovery.rb +163 -0
- data/lib/mychron/session.rb +118 -0
- data/lib/mychron/version.rb +5 -0
- data/lib/mychron.rb +193 -0
- data/mychron.gemspec +48 -0
- metadata +127 -0
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*
|