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 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,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ source "https://rubygems.org"
4
+
5
+ gemspec
@@ -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.