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
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: de882c005d17be9699b196963985e0943f5ee13772d93c66b060856ad51e10ed
|
|
4
|
+
data.tar.gz: da81ac312aa45a6cac77b20efef3496d83e5d717da38838cbf8ba1157965c459
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: 432ef0662a41526af7ea02b91e9c86ae4783918ed818f2e3e81557662cea4bfdfc5d9a39ca6815bda829b5228ac71b845ef4c31c1cbbc7056f467f4abf604b57
|
|
7
|
+
data.tar.gz: '089f0204b3ebf293894d235b445ceff31d0fbf046f35f72108bda0e21b5fc5c07c2149067cfa326dd98bf54f4e5a1cbbed2952feab5f6654114924e731af210c'
|
data/Gemfile
ADDED
|
@@ -0,0 +1,539 @@
|
|
|
1
|
+
# MyChron 6 Implementation Notes
|
|
2
|
+
|
|
3
|
+
This document captures everything learned during reverse engineering and implementation.
|
|
4
|
+
Use this as the authoritative reference when working on this gem.
|
|
5
|
+
|
|
6
|
+
**Last updated: 2026-02-07 (v0.3.1)**
|
|
7
|
+
|
|
8
|
+
## Table of Contents
|
|
9
|
+
|
|
10
|
+
1. [Overview](#overview)
|
|
11
|
+
2. [Critical Discoveries](#critical-discoveries)
|
|
12
|
+
3. [UDP Discovery](#udp-discovery)
|
|
13
|
+
4. [TCP Packet Format](#tcp-packet-format)
|
|
14
|
+
5. [Session Listing Protocol](#session-listing-protocol)
|
|
15
|
+
6. [File Download Protocol](#file-download-protocol)
|
|
16
|
+
7. [Gotchas and Pitfalls](#gotchas-and-pitfalls)
|
|
17
|
+
8. [Data Structures](#data-structures)
|
|
18
|
+
9. [Performance](#performance)
|
|
19
|
+
10. [Debugging Guide](#debugging-guide)
|
|
20
|
+
|
|
21
|
+
---
|
|
22
|
+
|
|
23
|
+
## Overview
|
|
24
|
+
|
|
25
|
+
### What This Library Does
|
|
26
|
+
|
|
27
|
+
1. **Discovers** MyChron 6 devices on the local network via UDP broadcast
|
|
28
|
+
2. **Lists** recorded sessions (lap data files) stored on the device
|
|
29
|
+
3. **Downloads** session files (`.xrz`, `.hrz`) from the device
|
|
30
|
+
|
|
31
|
+
### Network Architecture
|
|
32
|
+
|
|
33
|
+
```
|
|
34
|
+
Ruby Client MyChron 6 (ESP32)
|
|
35
|
+
│ │
|
|
36
|
+
│── UDP 36002: "aim-ka" ────────────▶│
|
|
37
|
+
│◀── 236-byte device info ───────────│
|
|
38
|
+
│ │
|
|
39
|
+
│══ TCP 2000: session listing ═══════│ (8-step protocol)
|
|
40
|
+
│══ TCP 2000: file download ════════│ (fresh connection required)
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
### Core Constants
|
|
44
|
+
|
|
45
|
+
```ruby
|
|
46
|
+
DISCOVERY_PORT = 36002 # UDP
|
|
47
|
+
DATA_PORT = 2000 # TCP
|
|
48
|
+
BEACON = "aim-ka" # 6 bytes
|
|
49
|
+
RESPONSE_SIZE = 236 # bytes (UDP response)
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
---
|
|
53
|
+
|
|
54
|
+
## Critical Discoveries
|
|
55
|
+
|
|
56
|
+
These are the hardest-won findings. If you forget everything else, remember these:
|
|
57
|
+
|
|
58
|
+
### 1. Tail Bytes are a 16-bit Payload Checksum (v0.3.0)
|
|
59
|
+
|
|
60
|
+
**THE most important discovery.** Every packet's tail bytes are NOT constants - they
|
|
61
|
+
are a checksum calculated from the payload bytes.
|
|
62
|
+
|
|
63
|
+
```
|
|
64
|
+
checksum = sum_of_all_payload_bytes (as a simple integer sum)
|
|
65
|
+
tail = [tail_marker] [checksum & 0xFF] [(checksum >> 8) & 0xFF] [0x3E]
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
This was hardcoded to `0x2F 0x05` in v0.2.1, which happened to be the correct
|
|
69
|
+
checksum for `a_0077.xrz` only. ALL other files silently failed because the device
|
|
70
|
+
rejects packets with wrong checksums without any error response.
|
|
71
|
+
|
|
72
|
+
**Verified across ALL packet types:**
|
|
73
|
+
- Handshake: payload sum=14 -> tail `0E 00 3E`
|
|
74
|
+
- Init (0x10): payload sum=82 -> tail `52 00 3E`
|
|
75
|
+
- List query (0x51): payload sum=1104 -> tail `50 04 3E`
|
|
76
|
+
- Finalize (0x24): payload sum=39 -> tail `27 00 3E`
|
|
77
|
+
- Download a_0077.xrz: payload sum=1327 -> tail `2F 05 3E`
|
|
78
|
+
- Download a_0065.xrz: payload sum=1324 -> tail `2C 05 3E`
|
|
79
|
+
- Data ACK #0: payload sum=447 -> tail `BF 01 3E`
|
|
80
|
+
|
|
81
|
+
### 2. Session Listing is an 8-Step Protocol (v0.2.0)
|
|
82
|
+
|
|
83
|
+
NOT a single request/response. The full sequence is:
|
|
84
|
+
|
|
85
|
+
```
|
|
86
|
+
1. Handshake (CMD 0x08)
|
|
87
|
+
2. Init request (type 0x10 in STNC frame)
|
|
88
|
+
3. Date query (CMD 0x44 with type 0x07)
|
|
89
|
+
4. ACK
|
|
90
|
+
5. List query (type 0x51 - requests CSV)
|
|
91
|
+
6. Finalize query (type 0x24 - triggers CSV output)
|
|
92
|
+
7. ACK
|
|
93
|
+
8. Receive CSV data
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
Skipping any step = empty results.
|
|
97
|
+
|
|
98
|
+
### 3. Date Query Requires Current Date (v0.2.0)
|
|
99
|
+
|
|
100
|
+
The date query (step 3) uses the current year/month. Using a wide range like
|
|
101
|
+
2020-2030 returns 0 sessions. RS3 always uses the first day of the current month.
|
|
102
|
+
|
|
103
|
+
### 4. Downloads Need Fresh Connections (v0.2.1)
|
|
104
|
+
|
|
105
|
+
Session listing leaves the device in a state incompatible with downloads.
|
|
106
|
+
**Always disconnect and reconnect before downloading a file.**
|
|
107
|
+
|
|
108
|
+
### 5. File Info Response Has TWO Blocks (v0.2.1)
|
|
109
|
+
|
|
110
|
+
The download response contains two 84-byte info blocks:
|
|
111
|
+
- Block 1: has the file path but size field is zeros
|
|
112
|
+
- Block 2: has the file path AND the actual file size at offset +8 from flags
|
|
113
|
+
|
|
114
|
+
Parse the SECOND `02 00 04 00` occurrence to find the size.
|
|
115
|
+
|
|
116
|
+
### 6. Device IP Changes via DHCP
|
|
117
|
+
|
|
118
|
+
The device gets a different IP each time it connects to the network.
|
|
119
|
+
Known IPs so far: .29, .30, .23. **Always discover via UDP broadcast first.**
|
|
120
|
+
|
|
121
|
+
### 7. Device Gets Stuck on Incomplete Downloads
|
|
122
|
+
|
|
123
|
+
If a download starts but doesn't complete (e.g., wrong checksum, network error),
|
|
124
|
+
the device enters "DATA DOWNLOAD" state and won't respond to new requests.
|
|
125
|
+
Race Studio 3 can unstick it by connecting.
|
|
126
|
+
|
|
127
|
+
---
|
|
128
|
+
|
|
129
|
+
## UDP Discovery
|
|
130
|
+
|
|
131
|
+
### Protocol
|
|
132
|
+
|
|
133
|
+
1. Send `aim-ka` (6 bytes) to UDP port 36002 (broadcast or unicast)
|
|
134
|
+
2. Device responds with 236 bytes
|
|
135
|
+
|
|
136
|
+
### Response Layout
|
|
137
|
+
|
|
138
|
+
```
|
|
139
|
+
Offset 0x14 (20): Device Name (48 bytes, null-terminated)
|
|
140
|
+
Offset 0x58 (88): Firmware Version (3x uint16 LE: major.minor.patch)
|
|
141
|
+
Offset 0x60 (96): Serial Number (uint32 LE)
|
|
142
|
+
Offset 0x94 (148): WiFi Config Name (48 bytes, null-terminated)
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
### Implementation
|
|
146
|
+
|
|
147
|
+
```ruby
|
|
148
|
+
socket = UDPSocket.new
|
|
149
|
+
socket.setsockopt(Socket::SOL_SOCKET, Socket::SO_BROADCAST, true)
|
|
150
|
+
socket.bind("0.0.0.0", 0)
|
|
151
|
+
socket.send("aim-ka", 0, "255.255.255.255", 36002)
|
|
152
|
+
|
|
153
|
+
# Wait for response
|
|
154
|
+
if IO.select([socket], nil, nil, timeout)
|
|
155
|
+
data, addr = socket.recvfrom(512)
|
|
156
|
+
# data.length should be 236
|
|
157
|
+
# addr[3] is the device IP
|
|
158
|
+
end
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
---
|
|
162
|
+
|
|
163
|
+
## TCP Packet Format
|
|
164
|
+
|
|
165
|
+
### Structure
|
|
166
|
+
|
|
167
|
+
```
|
|
168
|
+
[Header: 6 bytes] [CMD: 1] [0x00: 1] [Suffix: 4] [Payload: N] [Tail: 5] [Checksum: 2] [0x3E: 1]
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
Total overhead: 6 + 2 + 4 + 5 + 2 + 1 = 20 bytes per packet
|
|
172
|
+
|
|
173
|
+
### Header Types
|
|
174
|
+
|
|
175
|
+
| Type | Marker | Hex | Usage |
|
|
176
|
+
|------|--------|-----|-------|
|
|
177
|
+
| STCP | `<hSTCP` | `3C 68 53 54 43 50` | Handshake, ACK |
|
|
178
|
+
| STNC | `<hSTNC` | `3C 68 53 54 4E 43` | Init, queries, downloads |
|
|
179
|
+
|
|
180
|
+
### Tail Types
|
|
181
|
+
|
|
182
|
+
| Type | Marker | Hex |
|
|
183
|
+
|------|--------|-----|
|
|
184
|
+
| STCP | `<STCP` | `3C 53 54 43 50` |
|
|
185
|
+
| STNC | `<STNC` | `3C 53 54 4E 43` |
|
|
186
|
+
|
|
187
|
+
### Checksum Calculation (CRITICAL)
|
|
188
|
+
|
|
189
|
+
```ruby
|
|
190
|
+
def build_packet(header_marker, cmd, suffix, payload, tail_marker)
|
|
191
|
+
checksum = payload.sum # Simple sum of all byte values
|
|
192
|
+
packet = header_marker.dup
|
|
193
|
+
packet << cmd.chr << 0x00.chr
|
|
194
|
+
suffix.each { |b| packet << b.chr }
|
|
195
|
+
payload.each { |b| packet << b.chr }
|
|
196
|
+
packet << tail_marker
|
|
197
|
+
packet << (checksum & 0xFF).chr # Low byte
|
|
198
|
+
packet << ((checksum >> 8) & 0xFF).chr # High byte
|
|
199
|
+
packet << 0x3E.chr # Always '>'
|
|
200
|
+
packet
|
|
201
|
+
end
|
|
202
|
+
```
|
|
203
|
+
|
|
204
|
+
### Handshake (Required Before Everything)
|
|
205
|
+
|
|
206
|
+
```
|
|
207
|
+
Request: <hSTCP 08 00 [00 00 00 3E] [00 00 00 00 06 08 00 00] <STCP [0E 00 3E]
|
|
208
|
+
Response: <hSTCP 08 00 00 00 00 3E (12 bytes min)
|
|
209
|
+
+ additional data: [00 00 00 00 06 09 00 00] <STCP [0F 00 3E]
|
|
210
|
+
```
|
|
211
|
+
|
|
212
|
+
### ACK Packet
|
|
213
|
+
|
|
214
|
+
```
|
|
215
|
+
Simple: <hSTCP 04 00 [00 00 00 3E] [00 00 00 00] <STCP [00 00 3E]
|
|
216
|
+
```
|
|
217
|
+
|
|
218
|
+
---
|
|
219
|
+
|
|
220
|
+
## Session Listing Protocol
|
|
221
|
+
|
|
222
|
+
### Full 8-Step Sequence
|
|
223
|
+
|
|
224
|
+
**Step 1-2: Handshake** (see above)
|
|
225
|
+
|
|
226
|
+
**Step 3: Init Request (type 0x10)**
|
|
227
|
+
```
|
|
228
|
+
Frame: <hSTNC 40 00 [00 00 00 3E] [64-byte payload] <STNC [checksum] 3E
|
|
229
|
+
Payload offsets:
|
|
230
|
+
8-9: 0x10 0x00 (type = init)
|
|
231
|
+
10-11: 0x01 0x00 (flags)
|
|
232
|
+
16-19: 0x40 0x00 0x00 0x00
|
|
233
|
+
24-27: 0x01 0x00 0x00 0x00 (count)
|
|
234
|
+
```
|
|
235
|
+
|
|
236
|
+
**Step 4: Date Query (CMD 0x44)**
|
|
237
|
+
```
|
|
238
|
+
Frame: <hSTCP 44 00 [00 00 00 3E] [68-byte payload] <STCP [checksum] 3E
|
|
239
|
+
Payload offsets:
|
|
240
|
+
12-15: year (uint32 LE, current year)
|
|
241
|
+
16-19: month (uint32 LE, current month)
|
|
242
|
+
20-23: day (uint32 LE, always 1)
|
|
243
|
+
24-27: 0x15 (constant param1)
|
|
244
|
+
28-31: 0x07 (type = session list)
|
|
245
|
+
44-47: year (repeat)
|
|
246
|
+
48-51: month (repeat)
|
|
247
|
+
52-55: day (repeat)
|
|
248
|
+
56-59: 0x16 (constant param2 = param1 + 1)
|
|
249
|
+
60-63: 0x07 (type repeat)
|
|
250
|
+
```
|
|
251
|
+
|
|
252
|
+
**Step 5: ACK**
|
|
253
|
+
|
|
254
|
+
**Step 6: List Query (type 0x51)**
|
|
255
|
+
```
|
|
256
|
+
Frame: <hSTNC 40 00 [00 00 00 3E] [64-byte payload] <STNC [checksum] 3E
|
|
257
|
+
Payload offsets:
|
|
258
|
+
8-9: 0x51 0x00 (type = query)
|
|
259
|
+
10-11: 0x02 0x00 (flags)
|
|
260
|
+
24-27: 0x01 0x00 0x00 0x00 (count)
|
|
261
|
+
32-35: 0xFF 0xFF 0xFF 0xFF (all dates)
|
|
262
|
+
```
|
|
263
|
+
|
|
264
|
+
**Step 7: Finalize Query (type 0x24)**
|
|
265
|
+
```
|
|
266
|
+
Frame: <hSTNC 40 00 [00 00 00 3E] [64-byte payload] <STNC [checksum] 3E
|
|
267
|
+
Payload offsets:
|
|
268
|
+
8-9: 0x24 0x00 (type = finalize)
|
|
269
|
+
10-11: 0x02 0x00 (flags)
|
|
270
|
+
24-27: 0x01 0x00 0x00 0x00 (count)
|
|
271
|
+
```
|
|
272
|
+
|
|
273
|
+
**Step 8: ACK + Receive CSV**
|
|
274
|
+
|
|
275
|
+
### CSV Format
|
|
276
|
+
|
|
277
|
+
```
|
|
278
|
+
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,...
|
|
279
|
+
a_0079.xrz,2518552,25/01/2026,13:21:40,13,3,56789,Driver1,,,,,,,,0,JD-AT,0,0,0,...
|
|
280
|
+
```
|
|
281
|
+
|
|
282
|
+
Field indices: 0=name, 1=size, 2=date, 3=hour, 4=nlap, 5=nbest, 6=best(ms),
|
|
283
|
+
7=pilota, 12=mode, 13=trk_type, 14=motivolap, 16=device, 17=lat, 18=lon, 19=dur
|
|
284
|
+
|
|
285
|
+
---
|
|
286
|
+
|
|
287
|
+
## File Download Protocol
|
|
288
|
+
|
|
289
|
+
### Connection Requirements
|
|
290
|
+
|
|
291
|
+
- MUST use a fresh TCP connection (disconnect after session listing)
|
|
292
|
+
- MUST send handshake before download request
|
|
293
|
+
- Device accepts only ONE concurrent connection
|
|
294
|
+
|
|
295
|
+
### Download Request
|
|
296
|
+
|
|
297
|
+
```
|
|
298
|
+
Frame: <hSTNC 40 00 [00 00 00 3E] [64-byte payload] <STNC [checksum] 3E
|
|
299
|
+
|
|
300
|
+
Payload:
|
|
301
|
+
Offset 8-11: 02 00 04 00 (flags - 0x04 marks as "downloaded" in RS3)
|
|
302
|
+
Offset 24-27: 01 00 00 00 (count)
|
|
303
|
+
Offset 32-55: "1:/mem/<filename>" (null-terminated, max 24 bytes)
|
|
304
|
+
```
|
|
305
|
+
|
|
306
|
+
The checksum varies per filename since the path bytes are part of the payload.
|
|
307
|
+
|
|
308
|
+
### File Info Response (168 bytes)
|
|
309
|
+
|
|
310
|
+
Two 84-byte blocks:
|
|
311
|
+
|
|
312
|
+
```
|
|
313
|
+
Block 1 (size field = 0):
|
|
314
|
+
[12-byte header] [8 zeros] [02 00 04 00] [00 00 00 00] [C0 FF 00 00] ... [path] [8-byte tail]
|
|
315
|
+
|
|
316
|
+
Block 2 (has actual size):
|
|
317
|
+
[12-byte header] [8 zeros] [02 00 04 00] [00 00 00 00] [size: 4 bytes LE] [C0 FF 00 00] ... [path] [8-byte tail]
|
|
318
|
+
```
|
|
319
|
+
|
|
320
|
+
To extract size: find SECOND occurrence of `02 00 04 00`, read 4 bytes at offset +8.
|
|
321
|
+
|
|
322
|
+
### Data Transfer with ACKs
|
|
323
|
+
|
|
324
|
+
After sending initial ACK, device streams data in ~64KB chunks with protocol markers
|
|
325
|
+
interspersed. Send a data ACK every ~64KB to keep the transfer going.
|
|
326
|
+
|
|
327
|
+
**Data ACK format:**
|
|
328
|
+
```
|
|
329
|
+
<hSTCP 04 00 [00 00 00 3E] [counter_lo, counter_hi, seq_lo, 0x00] <STCP [checksum] 3E
|
|
330
|
+
```
|
|
331
|
+
|
|
332
|
+
Counter starts at `0xFFC0`, decrements by `0x40` per ACK:
|
|
333
|
+
- ACK 0: counter=0xFFC0, payload=[C0 FF 00 00]
|
|
334
|
+
- ACK 1: counter=0xFF80, payload=[80 FF 01 00]
|
|
335
|
+
- ACK 2: counter=0xFF40, payload=[40 FF 02 00]
|
|
336
|
+
- etc.
|
|
337
|
+
|
|
338
|
+
The tail checksum is auto-calculated from the 4-byte payload, same as all other packets.
|
|
339
|
+
|
|
340
|
+
### Marker Stripping
|
|
341
|
+
|
|
342
|
+
Data stream contains `<hSTCP` headers (**16 bytes**) and `<STCP` tails
|
|
343
|
+
(8 bytes) that must be stripped to get the raw file data.
|
|
344
|
+
|
|
345
|
+
**CRITICAL**: Data frame headers during download are **16 bytes, not 12**.
|
|
346
|
+
The standard 12-byte header is followed by a 4-byte counter/sequence field:
|
|
347
|
+
|
|
348
|
+
```text
|
|
349
|
+
Data frame header (16 bytes):
|
|
350
|
+
<hSTCP (6) + cmd (1) + 0xFF (1) + suffix (4) + counter (4)
|
|
351
|
+
|
|
352
|
+
Counter values:
|
|
353
|
+
- 00 00 00 00 for the initial data frame
|
|
354
|
+
- C0 FF 00 00 after chunk 0 ACK (counter echoes send_data_ack values)
|
|
355
|
+
- 80 FF 01 00 after chunk 1 ACK
|
|
356
|
+
- etc.
|
|
357
|
+
```
|
|
358
|
+
|
|
359
|
+
Each ~65KB download chunk has this exact structure (confirmed by raw TCP capture):
|
|
360
|
+
- **16-byte header** + **65,472 bytes file data** + **8-byte tail** = 65,496 bytes total
|
|
361
|
+
- One frame per chunk, no intermediate frames
|
|
362
|
+
- No separate ACK response packets (counter is embedded in the header)
|
|
363
|
+
|
|
364
|
+
---
|
|
365
|
+
|
|
366
|
+
## Gotchas and Pitfalls
|
|
367
|
+
|
|
368
|
+
### Things That Silently Fail (No Error Response)
|
|
369
|
+
|
|
370
|
+
1. **Wrong checksum** -> device returns 0 bytes (discovered v0.3.0)
|
|
371
|
+
2. **Wrong date range in query** -> device returns 0 sessions (discovered v0.2.0)
|
|
372
|
+
3. **Downloading on reused connection** -> device returns 0 bytes (discovered v0.2.1)
|
|
373
|
+
4. **Incomplete download** -> device gets stuck in DATA DOWNLOAD state
|
|
374
|
+
5. **Wrong header strip size** -> correct file size but corrupted content (discovered v0.3.1)
|
|
375
|
+
|
|
376
|
+
### Things That Must Be Exact
|
|
377
|
+
|
|
378
|
+
1. Payload checksum in tail bytes
|
|
379
|
+
2. `1:/mem/` prefix for file paths
|
|
380
|
+
3. Current date (year/month) in date query
|
|
381
|
+
4. Handshake before every operation
|
|
382
|
+
5. Fresh connection for downloads
|
|
383
|
+
|
|
384
|
+
### Common Mistakes
|
|
385
|
+
|
|
386
|
+
- Using wide date ranges (2020-2030) instead of current month
|
|
387
|
+
- Expecting CSV from a single request (need 8-step sequence)
|
|
388
|
+
- Hardcoding tail bytes instead of calculating checksums
|
|
389
|
+
- Reusing a connection after session listing for downloads
|
|
390
|
+
- Not sending periodic ACKs during data transfer (~64KB intervals)
|
|
391
|
+
- Looking at the FIRST info block for file size (it's in the SECOND)
|
|
392
|
+
|
|
393
|
+
### Device Behavior
|
|
394
|
+
|
|
395
|
+
- **DHCP**: IP address changes every connection (always discover first)
|
|
396
|
+
- **Single client**: Only one TCP connection at a time
|
|
397
|
+
- **Stuck state**: Incomplete downloads leave device stuck (RS3 can recover)
|
|
398
|
+
- **Sleep mode**: Device must be awake for UDP discovery
|
|
399
|
+
- **File marking**: Flag 0x04 marks files as "downloaded" in RS3 UI (doesn't delete)
|
|
400
|
+
- **Download speed**: ~23-34 KB/s (ESP32 WiFi limitation)
|
|
401
|
+
|
|
402
|
+
---
|
|
403
|
+
|
|
404
|
+
## Data Structures
|
|
405
|
+
|
|
406
|
+
### SessionInfo (from CSV)
|
|
407
|
+
|
|
408
|
+
```ruby
|
|
409
|
+
SessionInfo = Struct.new(
|
|
410
|
+
:filename, # "a_0077.xrz"
|
|
411
|
+
:size, # 2224965 (bytes)
|
|
412
|
+
:date, # "25/01/2026"
|
|
413
|
+
:time, # "10:45:25"
|
|
414
|
+
:laps, # 10
|
|
415
|
+
:best_lap, # 3 (lap number)
|
|
416
|
+
:best_time, # 56789 (milliseconds)
|
|
417
|
+
:driver, # "Driver1"
|
|
418
|
+
:speed_type, # field 12
|
|
419
|
+
:status, # field 13
|
|
420
|
+
:stop_mode, # field 14
|
|
421
|
+
:timestamp, # field 15
|
|
422
|
+
:device_name, # "JD-AT"
|
|
423
|
+
:longitude, # field 17
|
|
424
|
+
:latitude, # field 18
|
|
425
|
+
:duration, # field 19 (milliseconds)
|
|
426
|
+
keyword_init: true
|
|
427
|
+
)
|
|
428
|
+
```
|
|
429
|
+
|
|
430
|
+
### DeviceInfo (from UDP)
|
|
431
|
+
|
|
432
|
+
```ruby
|
|
433
|
+
DeviceInfo = Struct.new(
|
|
434
|
+
:ip, # "192.168.1.23"
|
|
435
|
+
:device_name, # "JD-AT"
|
|
436
|
+
:wifi_name, # "Wifi P2"
|
|
437
|
+
:serial_number, # 35016462
|
|
438
|
+
:firmware_version, # "56.355.437"
|
|
439
|
+
keyword_init: true
|
|
440
|
+
)
|
|
441
|
+
```
|
|
442
|
+
|
|
443
|
+
---
|
|
444
|
+
|
|
445
|
+
## Performance
|
|
446
|
+
|
|
447
|
+
### Measured (Real Device, v0.3.0)
|
|
448
|
+
|
|
449
|
+
| Operation | Time | Notes |
|
|
450
|
+
|-----------|------|-------|
|
|
451
|
+
| UDP Discovery | <1s | Broadcast or unicast |
|
|
452
|
+
| Session Listing | ~3s | 60 sessions, 8-step protocol |
|
|
453
|
+
| Download 28KB | 0.8s | 34 KB/s |
|
|
454
|
+
| Download 45KB | 1.3s | 34 KB/s |
|
|
455
|
+
| Download 709KB | 30.7s | 23 KB/s |
|
|
456
|
+
| Download 2.2MB | ~90s | 23 KB/s |
|
|
457
|
+
| Download 2.5MB | ~106s | 23 KB/s |
|
|
458
|
+
|
|
459
|
+
Small files download faster (~34 KB/s), large files settle at ~23 KB/s.
|
|
460
|
+
|
|
461
|
+
### Recommendations
|
|
462
|
+
|
|
463
|
+
- Use background jobs for downloads >500KB
|
|
464
|
+
- Cache session lists (refresh on demand)
|
|
465
|
+
- Always discover device IP first (DHCP changes it)
|
|
466
|
+
- Show progress callbacks for files >100KB
|
|
467
|
+
|
|
468
|
+
---
|
|
469
|
+
|
|
470
|
+
## Debugging Guide
|
|
471
|
+
|
|
472
|
+
### Enable Logging
|
|
473
|
+
|
|
474
|
+
```ruby
|
|
475
|
+
require 'logger'
|
|
476
|
+
MyChron::Logging.logger = Logger.new(STDOUT).tap do |l|
|
|
477
|
+
l.level = Logger::DEBUG
|
|
478
|
+
l.formatter = proc { |sev, time, _, msg| "[#{sev}] #{msg}\n" }
|
|
479
|
+
end
|
|
480
|
+
```
|
|
481
|
+
|
|
482
|
+
### Compare Packet Hex Dumps
|
|
483
|
+
|
|
484
|
+
```ruby
|
|
485
|
+
packet = build_nc_packet(...)
|
|
486
|
+
puts packet.bytes.map { |b| format('%02X', b) }.join(' ')
|
|
487
|
+
# Compare with pcap capture in Wireshark
|
|
488
|
+
```
|
|
489
|
+
|
|
490
|
+
### Verify Checksum
|
|
491
|
+
|
|
492
|
+
```ruby
|
|
493
|
+
payload = [...] # Your payload array
|
|
494
|
+
sum = payload.sum
|
|
495
|
+
puts "Checksum: 0x#{format('%04X', sum)} -> tail bytes: #{format('%02X', sum & 0xFF)} #{format('%02X', (sum >> 8) & 0xFF)}"
|
|
496
|
+
```
|
|
497
|
+
|
|
498
|
+
### Test Single Download
|
|
499
|
+
|
|
500
|
+
```ruby
|
|
501
|
+
ruby -I lib -r mychron -e "
|
|
502
|
+
MyChron::Logging.logger = Logger.new(STDOUT)
|
|
503
|
+
device = MyChron.device('192.168.1.23')
|
|
504
|
+
data = device.download('a_0065.xrz')
|
|
505
|
+
puts data.bytesize
|
|
506
|
+
"
|
|
507
|
+
```
|
|
508
|
+
|
|
509
|
+
### pcap Captures
|
|
510
|
+
|
|
511
|
+
Reference captures in project root:
|
|
512
|
+
- `rs3_session_list.pcap` - Full session listing sequence
|
|
513
|
+
- `download_capture.pcap` - Download of a_0077.xrz (tail 0x2F 0x05)
|
|
514
|
+
- `download_capture2.pcap` - Download of a_0065.xrz (tail 0x2C 0x05)
|
|
515
|
+
|
|
516
|
+
Open with: `tcpdump -r capture.pcap -X` or Wireshark
|
|
517
|
+
|
|
518
|
+
### Capture New Traffic
|
|
519
|
+
|
|
520
|
+
```bash
|
|
521
|
+
sudo tcpdump -i any host <device_ip> -w capture.pcap -s 0
|
|
522
|
+
```
|
|
523
|
+
|
|
524
|
+
---
|
|
525
|
+
|
|
526
|
+
## Version History of Discoveries
|
|
527
|
+
|
|
528
|
+
| Version | Date | Discovery |
|
|
529
|
+
|---------|------|-----------|
|
|
530
|
+
| v0.1.0 | Jan 2026 | UDP discovery, basic TCP framing |
|
|
531
|
+
| v0.2.0 | Feb 4, 2026 | 8-step session listing, date query, CSV parsing |
|
|
532
|
+
| v0.2.1 | Feb 4, 2026 | Fresh connections for downloads, dual info blocks, read_available fix |
|
|
533
|
+
| v0.3.0 | Feb 7, 2026 | **Tail byte checksum** - all files downloadable |
|
|
534
|
+
| v0.3.1 | Feb 7, 2026 | **16-byte data frame headers** - byte-identical downloads |
|
|
535
|
+
|
|
536
|
+
---
|
|
537
|
+
|
|
538
|
+
*This document contains everything needed to rebuild the MyChron client from scratch.*
|
|
539
|
+
*Last updated: 2026-03-24*
|
data/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Eugeniu
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|