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
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "socket"
|
|
4
|
+
|
|
5
|
+
module MyChron
|
|
6
|
+
module Protocol
|
|
7
|
+
# UDP Discovery protocol for finding MyChron devices
|
|
8
|
+
# Sends "aim-ka" beacon on port 36002, receives 236-byte device info
|
|
9
|
+
class Discovery
|
|
10
|
+
include Logging
|
|
11
|
+
|
|
12
|
+
DISCOVERY_PORT = 36002
|
|
13
|
+
BEACON = "aim-ka"
|
|
14
|
+
RESPONSE_SIZE = 236
|
|
15
|
+
DEFAULT_TIMEOUT = 2.0
|
|
16
|
+
|
|
17
|
+
DeviceInfo = Struct.new(
|
|
18
|
+
:ip,
|
|
19
|
+
:device_name,
|
|
20
|
+
:wifi_name,
|
|
21
|
+
:serial_number,
|
|
22
|
+
:firmware_version,
|
|
23
|
+
:raw_data,
|
|
24
|
+
keyword_init: true
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
def initialize(timeout: DEFAULT_TIMEOUT)
|
|
28
|
+
@timeout = timeout
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
# Discover devices via UDP broadcast
|
|
32
|
+
# Returns array of DeviceInfo
|
|
33
|
+
def discover(target_ip: nil, broadcast: true)
|
|
34
|
+
devices = []
|
|
35
|
+
socket = UDPSocket.new
|
|
36
|
+
|
|
37
|
+
begin
|
|
38
|
+
socket.setsockopt(Socket::SOL_SOCKET, Socket::SO_BROADCAST, true) if broadcast
|
|
39
|
+
socket.setsockopt(Socket::SOL_SOCKET, Socket::SO_REUSEADDR, true)
|
|
40
|
+
socket.bind("0.0.0.0", 0)
|
|
41
|
+
|
|
42
|
+
# Send beacon
|
|
43
|
+
begin
|
|
44
|
+
if target_ip
|
|
45
|
+
log_debug("Sending beacon to #{target_ip}:#{DISCOVERY_PORT}")
|
|
46
|
+
socket.send(BEACON, 0, target_ip, DISCOVERY_PORT)
|
|
47
|
+
else
|
|
48
|
+
log_debug("Sending broadcast beacon to 255.255.255.255:#{DISCOVERY_PORT}")
|
|
49
|
+
socket.send(BEACON, 0, "255.255.255.255", DISCOVERY_PORT)
|
|
50
|
+
end
|
|
51
|
+
rescue Errno::EHOSTUNREACH, Errno::ENETUNREACH, Errno::EADDRNOTAVAIL => e
|
|
52
|
+
log_warn("Network error sending beacon: #{e.message}")
|
|
53
|
+
return devices
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
# Wait for responses
|
|
57
|
+
deadline = Time.now + @timeout
|
|
58
|
+
while Time.now < deadline
|
|
59
|
+
remaining = deadline - Time.now
|
|
60
|
+
break if remaining <= 0
|
|
61
|
+
|
|
62
|
+
readable = IO.select([socket], nil, nil, [remaining, 0.5].min)
|
|
63
|
+
next unless readable
|
|
64
|
+
|
|
65
|
+
begin
|
|
66
|
+
data, addr = socket.recvfrom_nonblock(RESPONSE_SIZE + 100)
|
|
67
|
+
sender_ip = addr[3]
|
|
68
|
+
|
|
69
|
+
if data.bytesize >= RESPONSE_SIZE
|
|
70
|
+
device = parse_response(data, sender_ip)
|
|
71
|
+
devices << device if device
|
|
72
|
+
log_info("Found device: #{device.device_name} at #{sender_ip}")
|
|
73
|
+
end
|
|
74
|
+
rescue IO::WaitReadable
|
|
75
|
+
# No data yet, continue
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
ensure
|
|
79
|
+
socket.close
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
devices
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
# Probe a specific IP address
|
|
86
|
+
def probe(ip)
|
|
87
|
+
devices = discover(target_ip: ip, broadcast: false)
|
|
88
|
+
devices.first
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
private
|
|
92
|
+
|
|
93
|
+
def parse_response(data, ip)
|
|
94
|
+
# Response structure (236 bytes) - verified from actual device capture:
|
|
95
|
+
# Offset 0x00 (0): Magic (0xEC000000)
|
|
96
|
+
# Offset 0x04 (4): Protocol version (0x02000000)
|
|
97
|
+
# Offset 0x08 (8): IP address (little-endian)
|
|
98
|
+
# Offset 0x0C (12): Flags/status
|
|
99
|
+
# Offset 0x14 (20): Device name (null-terminated string)
|
|
100
|
+
# Offset 0x54 (84): "idn" marker + version byte
|
|
101
|
+
# Offset 0x58 (88): Firmware version components (3x uint16 LE)
|
|
102
|
+
# Offset 0x60 (96): Serial number (uint32 LE)
|
|
103
|
+
# Offset 0x94 (148): WiFi config name (null-terminated string)
|
|
104
|
+
|
|
105
|
+
# Extract device name at offset 0x14 (20 decimal)
|
|
106
|
+
device_name = extract_string(data, 0x14, 48)
|
|
107
|
+
|
|
108
|
+
# Extract WiFi name at offset 0x94 (148 decimal)
|
|
109
|
+
wifi_name = extract_string(data, 0x94, 48)
|
|
110
|
+
|
|
111
|
+
# Extract serial number at offset 0x60 (96 decimal) - uint32 little-endian
|
|
112
|
+
serial_number = extract_serial_number(data, 0x60)
|
|
113
|
+
|
|
114
|
+
# Extract firmware version from offset 0x58 (88 decimal) - 3x uint16 LE
|
|
115
|
+
firmware_version = extract_firmware_version(data, 0x58)
|
|
116
|
+
|
|
117
|
+
DeviceInfo.new(
|
|
118
|
+
ip: ip,
|
|
119
|
+
device_name: device_name,
|
|
120
|
+
wifi_name: wifi_name,
|
|
121
|
+
serial_number: serial_number,
|
|
122
|
+
firmware_version: firmware_version,
|
|
123
|
+
raw_data: data
|
|
124
|
+
)
|
|
125
|
+
rescue StandardError => e
|
|
126
|
+
log_warn("Failed to parse response from #{ip}: #{e.message}")
|
|
127
|
+
nil
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
def extract_string(data, offset, max_length)
|
|
131
|
+
return "" if offset >= data.bytesize
|
|
132
|
+
|
|
133
|
+
str_bytes = data.byteslice(offset, max_length)
|
|
134
|
+
return "" if str_bytes.nil? || str_bytes.empty?
|
|
135
|
+
|
|
136
|
+
# Find null terminator
|
|
137
|
+
null_idx = str_bytes.index("\x00")
|
|
138
|
+
str = null_idx ? str_bytes[0...null_idx] : str_bytes
|
|
139
|
+
|
|
140
|
+
# Clean up - remove non-printable chars
|
|
141
|
+
str.encode("UTF-8", invalid: :replace, undef: :replace, replace: "")
|
|
142
|
+
.gsub(/[^[:print:]]/, "")
|
|
143
|
+
.strip
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
def extract_serial_number(data, offset)
|
|
147
|
+
# Serial number is a 32-bit little-endian unsigned integer
|
|
148
|
+
return nil if offset + 4 > data.bytesize
|
|
149
|
+
|
|
150
|
+
data.byteslice(offset, 4).unpack1("V")
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
def extract_firmware_version(data, offset)
|
|
154
|
+
# Firmware version is 3x uint16 little-endian values
|
|
155
|
+
# Format appears to be: major.minor.patch or similar
|
|
156
|
+
return nil if offset + 6 > data.bytesize
|
|
157
|
+
|
|
158
|
+
v1, v2, v3 = data.byteslice(offset, 6).unpack("v3")
|
|
159
|
+
"#{v1}.#{v2}.#{v3}"
|
|
160
|
+
end
|
|
161
|
+
end
|
|
162
|
+
end
|
|
163
|
+
end
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "date"
|
|
4
|
+
|
|
5
|
+
module MyChron
|
|
6
|
+
# Represents a telemetry session recorded on a MyChron device
|
|
7
|
+
# Wraps the raw SessionInfo struct with convenience methods
|
|
8
|
+
class Session
|
|
9
|
+
attr_reader :filename, :size, :date, :time, :laps, :best_lap,
|
|
10
|
+
:best_time_ms, :driver, :device_name, :speed_type,
|
|
11
|
+
:status, :stop_mode, :timestamp, :longitude, :latitude, :duration
|
|
12
|
+
|
|
13
|
+
# Create a Session from a Protocol::Client::SessionInfo struct
|
|
14
|
+
# @param session_info [Protocol::Client::SessionInfo] Raw session data
|
|
15
|
+
def initialize(session_info)
|
|
16
|
+
@filename = session_info.filename
|
|
17
|
+
@size = session_info.size || 0
|
|
18
|
+
@date = session_info.date
|
|
19
|
+
@time = session_info.time
|
|
20
|
+
@laps = session_info.laps || 0
|
|
21
|
+
@best_lap = session_info.best_lap
|
|
22
|
+
@best_time_ms = session_info.best_time
|
|
23
|
+
@driver = session_info.driver
|
|
24
|
+
@device_name = session_info.device_name
|
|
25
|
+
@speed_type = session_info.speed_type
|
|
26
|
+
@status = session_info.status
|
|
27
|
+
@stop_mode = session_info.stop_mode
|
|
28
|
+
@timestamp = session_info.timestamp
|
|
29
|
+
@longitude = session_info.longitude
|
|
30
|
+
@latitude = session_info.latitude
|
|
31
|
+
@duration = session_info.duration
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
# Parse the recording date and time into a DateTime object
|
|
35
|
+
# @return [DateTime, nil] The recording timestamp
|
|
36
|
+
def recorded_at
|
|
37
|
+
return nil unless @date && @time
|
|
38
|
+
|
|
39
|
+
DateTime.strptime("#{@date} #{@time}", "%d/%m/%Y %H:%M:%S")
|
|
40
|
+
rescue Date::Error
|
|
41
|
+
nil
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
# Format the best lap time as "M:SS.mmm"
|
|
45
|
+
# @return [String, nil] Formatted time or nil if not available
|
|
46
|
+
def best_time_formatted
|
|
47
|
+
return nil unless @best_time_ms&.positive?
|
|
48
|
+
|
|
49
|
+
total_ms = @best_time_ms
|
|
50
|
+
minutes = total_ms / 60_000
|
|
51
|
+
remaining_ms = total_ms % 60_000
|
|
52
|
+
seconds = remaining_ms / 1000
|
|
53
|
+
milliseconds = remaining_ms % 1000
|
|
54
|
+
|
|
55
|
+
format("%d:%02d.%03d", minutes, seconds, milliseconds)
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
# Format file size in human-readable format
|
|
59
|
+
# @return [String] Formatted size (e.g., "512.5 KB")
|
|
60
|
+
def size_formatted
|
|
61
|
+
return "0 B" if @size.zero?
|
|
62
|
+
|
|
63
|
+
units = %w[B KB MB GB]
|
|
64
|
+
exp = (Math.log(@size) / Math.log(1024)).to_i
|
|
65
|
+
exp = [exp, units.length - 1].min
|
|
66
|
+
format("%.1f %s", @size.to_f / (1024**exp), units[exp])
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
# Check if this is a valid telemetry file
|
|
70
|
+
# @return [Boolean]
|
|
71
|
+
def valid?
|
|
72
|
+
filename&.match?(/\.(xrz|xrk|hrz|drk)$/i)
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
# Get the file extension
|
|
76
|
+
# @return [String, nil]
|
|
77
|
+
def extension
|
|
78
|
+
File.extname(@filename).downcase if @filename
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
# Duration in seconds (if available)
|
|
82
|
+
# @return [Float, nil]
|
|
83
|
+
def duration_seconds
|
|
84
|
+
@duration.to_f / 1000 if @duration&.positive?
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
# Hash representation for serialization
|
|
88
|
+
# @return [Hash]
|
|
89
|
+
def to_h
|
|
90
|
+
{
|
|
91
|
+
filename: @filename,
|
|
92
|
+
size: @size,
|
|
93
|
+
date: @date,
|
|
94
|
+
time: @time,
|
|
95
|
+
laps: @laps,
|
|
96
|
+
best_lap: @best_lap,
|
|
97
|
+
best_time_ms: @best_time_ms,
|
|
98
|
+
driver: @driver,
|
|
99
|
+
device_name: @device_name,
|
|
100
|
+
recorded_at: recorded_at&.iso8601
|
|
101
|
+
}
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
# JSON representation
|
|
105
|
+
# @return [String]
|
|
106
|
+
def to_json(*args)
|
|
107
|
+
to_h.to_json(*args)
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
def inspect
|
|
111
|
+
"#<MyChron::Session #{@filename} (#{size_formatted}, #{@laps} laps)>"
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
def to_s
|
|
115
|
+
"#{@filename} - #{size_formatted} - #{@laps} laps"
|
|
116
|
+
end
|
|
117
|
+
end
|
|
118
|
+
end
|
data/lib/mychron.rb
ADDED
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "json"
|
|
4
|
+
require "socket"
|
|
5
|
+
require "timeout"
|
|
6
|
+
require "ipaddr"
|
|
7
|
+
require "open3"
|
|
8
|
+
require "time"
|
|
9
|
+
require "fileutils"
|
|
10
|
+
|
|
11
|
+
# Core modules
|
|
12
|
+
require_relative "mychron/version"
|
|
13
|
+
require_relative "mychron/errors"
|
|
14
|
+
require_relative "mychron/logging"
|
|
15
|
+
require_relative "mychron/configuration"
|
|
16
|
+
require_relative "mychron/session"
|
|
17
|
+
require_relative "mychron/device"
|
|
18
|
+
|
|
19
|
+
# Protocol implementation
|
|
20
|
+
require_relative "mychron/protocol/discovery"
|
|
21
|
+
require_relative "mychron/protocol/client"
|
|
22
|
+
|
|
23
|
+
# Network utilities (for advanced discovery)
|
|
24
|
+
require_relative "mychron/network/arp"
|
|
25
|
+
require_relative "mychron/network/scanner"
|
|
26
|
+
require_relative "mychron/network/http_probe"
|
|
27
|
+
|
|
28
|
+
# Discovery orchestration
|
|
29
|
+
require_relative "mychron/discovery/scorer"
|
|
30
|
+
require_relative "mychron/discovery/detector"
|
|
31
|
+
|
|
32
|
+
# MyChron - Ruby library for AiM MyChron 6 telemetry devices
|
|
33
|
+
#
|
|
34
|
+
# This gem provides a clean API for:
|
|
35
|
+
# - Discovering MyChron devices on your network
|
|
36
|
+
# - Listing recorded telemetry sessions
|
|
37
|
+
# - Downloading session files (.xrz format)
|
|
38
|
+
#
|
|
39
|
+
# @example Basic discovery and download
|
|
40
|
+
# # Find devices on the network
|
|
41
|
+
# devices = MyChron.discover
|
|
42
|
+
# puts "Found #{devices.count} device(s)"
|
|
43
|
+
#
|
|
44
|
+
# # Work with a specific device
|
|
45
|
+
# device = MyChron.device("192.168.1.29")
|
|
46
|
+
# sessions = device.sessions
|
|
47
|
+
# data = device.download(sessions.first.filename)
|
|
48
|
+
#
|
|
49
|
+
# @example Rails integration
|
|
50
|
+
# # In an initializer or controller
|
|
51
|
+
# MyChron.configure do |config|
|
|
52
|
+
# config.download_dir = Rails.root.join("storage/telemetry")
|
|
53
|
+
# config.logger = Rails.logger
|
|
54
|
+
# end
|
|
55
|
+
#
|
|
56
|
+
# # In a service or job
|
|
57
|
+
# device = MyChron.device(params[:device_ip])
|
|
58
|
+
# sessions = device.sessions
|
|
59
|
+
#
|
|
60
|
+
module MyChron
|
|
61
|
+
class << self
|
|
62
|
+
# Configure the MyChron gem
|
|
63
|
+
# @yield [Configuration] Configuration object
|
|
64
|
+
# @return [Configuration]
|
|
65
|
+
#
|
|
66
|
+
# @example
|
|
67
|
+
# MyChron.configure do |config|
|
|
68
|
+
# config.download_dir = "/path/to/downloads"
|
|
69
|
+
# config.discovery_timeout = 5.0
|
|
70
|
+
# config.logger = Rails.logger
|
|
71
|
+
# end
|
|
72
|
+
def configure
|
|
73
|
+
yield(configuration) if block_given?
|
|
74
|
+
configuration
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
# Get the current configuration
|
|
78
|
+
# @return [Configuration]
|
|
79
|
+
def configuration
|
|
80
|
+
@configuration ||= Configuration.new
|
|
81
|
+
end
|
|
82
|
+
alias config configuration
|
|
83
|
+
|
|
84
|
+
# Get the logger
|
|
85
|
+
# @return [Logger]
|
|
86
|
+
def logger
|
|
87
|
+
configuration.logger
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
# Set a custom logger
|
|
91
|
+
# @param logger [Logger] A Logger-compatible instance
|
|
92
|
+
def logger=(logger)
|
|
93
|
+
Logging.logger = logger
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
# Reset configuration to defaults
|
|
97
|
+
def reset!
|
|
98
|
+
@configuration = nil
|
|
99
|
+
Logging.reset!
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
# ----- Primary API: Device Discovery -----
|
|
103
|
+
|
|
104
|
+
# Discover all MyChron devices on the network via UDP broadcast
|
|
105
|
+
# @param timeout [Float] Timeout in seconds (default: 2.0)
|
|
106
|
+
# @return [Array<Protocol::Discovery::DeviceInfo>] Array of discovered devices
|
|
107
|
+
#
|
|
108
|
+
# @example
|
|
109
|
+
# devices = MyChron.discover
|
|
110
|
+
# devices.each do |device|
|
|
111
|
+
# puts "Found: #{device.device_name} at #{device.ip}"
|
|
112
|
+
# end
|
|
113
|
+
def discover(timeout: nil)
|
|
114
|
+
timeout ||= configuration.discovery_timeout
|
|
115
|
+
Protocol::Discovery.new(timeout: timeout).discover
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
# Probe a specific IP address to check for a MyChron device
|
|
119
|
+
# @param ip [String] IP address to probe
|
|
120
|
+
# @param timeout [Float] Timeout in seconds (default: 2.0)
|
|
121
|
+
# @return [Protocol::Discovery::DeviceInfo, nil] Device info or nil if not found
|
|
122
|
+
#
|
|
123
|
+
# @example
|
|
124
|
+
# device_info = MyChron.find_device("192.168.1.29")
|
|
125
|
+
# if device_info
|
|
126
|
+
# puts "Found: #{device_info.device_name}"
|
|
127
|
+
# end
|
|
128
|
+
def find_device(ip, timeout: nil)
|
|
129
|
+
timeout ||= configuration.discovery_timeout
|
|
130
|
+
Protocol::Discovery.new(timeout: timeout).probe(ip)
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
# Create a Device instance for interacting with a specific device
|
|
134
|
+
# @param ip [String] IP address of the MyChron device
|
|
135
|
+
# @return [Device] Device instance
|
|
136
|
+
#
|
|
137
|
+
# @example
|
|
138
|
+
# device = MyChron.device("192.168.1.29")
|
|
139
|
+
# sessions = device.sessions
|
|
140
|
+
# data = device.download("a_0077.xrz")
|
|
141
|
+
def device(ip)
|
|
142
|
+
Device.new(ip)
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
# ----- Convenience Methods -----
|
|
146
|
+
|
|
147
|
+
# List sessions on a device (convenience method)
|
|
148
|
+
# @param ip [String] IP address of the device
|
|
149
|
+
# @return [Array<Session>] Array of sessions
|
|
150
|
+
def sessions(ip)
|
|
151
|
+
device(ip).sessions
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
# Download a session file (convenience method)
|
|
155
|
+
# @param ip [String] IP address of the device
|
|
156
|
+
# @param filename [String] Session filename
|
|
157
|
+
# @yield [bytes_received, total_bytes] Progress callback
|
|
158
|
+
# @return [String] Binary file data
|
|
159
|
+
def download(ip, filename, &progress_block)
|
|
160
|
+
device(ip).download(filename, &progress_block)
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
# Download a session to a directory (convenience method)
|
|
164
|
+
# @param ip [String] IP address of the device
|
|
165
|
+
# @param filename [String] Session filename
|
|
166
|
+
# @param destination_dir [String] Directory to save the file
|
|
167
|
+
# @yield [bytes_received, total_bytes] Progress callback
|
|
168
|
+
# @return [String] Full path to saved file
|
|
169
|
+
def download_to(ip, filename, destination_dir, &progress_block)
|
|
170
|
+
device(ip).download_to(filename, destination_dir, &progress_block)
|
|
171
|
+
end
|
|
172
|
+
|
|
173
|
+
# ----- Advanced Discovery -----
|
|
174
|
+
|
|
175
|
+
# Perform network-based discovery using ARP + port scanning
|
|
176
|
+
# Use this if UDP discovery doesn't work (firewall, etc.)
|
|
177
|
+
# @param subnet [String, nil] Subnet to scan (auto-detected if nil)
|
|
178
|
+
# @param scan_ports [Boolean] Whether to scan ports
|
|
179
|
+
# @param probe_http [Boolean] Whether to probe HTTP endpoints
|
|
180
|
+
# @return [Array<Hash>] Array of candidate device hashes
|
|
181
|
+
def network_discover(subnet: nil, scan_ports: true, probe_http: true)
|
|
182
|
+
detector = Discovery::Detector.new(subnet: subnet)
|
|
183
|
+
# Convert boolean to nil (use default ports) or pass array through
|
|
184
|
+
ports = case scan_ports
|
|
185
|
+
when true then nil
|
|
186
|
+
when false then []
|
|
187
|
+
when Array then scan_ports
|
|
188
|
+
else nil
|
|
189
|
+
end
|
|
190
|
+
detector.run(scan_ports: ports, probe_http: probe_http)
|
|
191
|
+
end
|
|
192
|
+
end
|
|
193
|
+
end
|
data/mychron.gemspec
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "lib/mychron/version"
|
|
4
|
+
|
|
5
|
+
Gem::Specification.new do |spec|
|
|
6
|
+
spec.name = "mychron"
|
|
7
|
+
spec.version = MyChron::VERSION
|
|
8
|
+
spec.authors = ["Eugeniu"]
|
|
9
|
+
spec.email = ["eugeniu.rtj@gmail.com"]
|
|
10
|
+
|
|
11
|
+
spec.summary = "Ruby library for AiM MyChron 6 telemetry devices"
|
|
12
|
+
spec.description = <<~DESC
|
|
13
|
+
A Ruby gem for discovering and downloading telemetry data from AiM MyChron 6
|
|
14
|
+
kart data loggers over Wi-Fi. Works with Ruby on Rails and any Ruby application.
|
|
15
|
+
|
|
16
|
+
Features:
|
|
17
|
+
- UDP device discovery
|
|
18
|
+
- Session listing with lap times and metadata
|
|
19
|
+
- File download with progress callbacks
|
|
20
|
+
- Rails.logger integration
|
|
21
|
+
DESC
|
|
22
|
+
spec.homepage = "https://gitlab.com/eugeniu-rtj/mychron"
|
|
23
|
+
spec.license = "MIT"
|
|
24
|
+
spec.required_ruby_version = ">= 3.0.0"
|
|
25
|
+
|
|
26
|
+
spec.metadata["source_code_uri"] = spec.homepage
|
|
27
|
+
spec.metadata["changelog_uri"] = "#{spec.homepage}/blob/main/CHANGELOG.md"
|
|
28
|
+
|
|
29
|
+
spec.files = Dir.glob(%w[
|
|
30
|
+
lib/**/*.rb
|
|
31
|
+
Gemfile
|
|
32
|
+
mychron.gemspec
|
|
33
|
+
README.md
|
|
34
|
+
LICENSE
|
|
35
|
+
PROTOCOL.md
|
|
36
|
+
IMPLEMENTATION_NOTES.md
|
|
37
|
+
]).reject { |f| f.match?(%r{spec/|test/|\.git|exe/|cli\.rb}) }
|
|
38
|
+
|
|
39
|
+
spec.require_paths = ["lib"]
|
|
40
|
+
|
|
41
|
+
# No runtime dependencies - pure Ruby implementation
|
|
42
|
+
# The gem uses only standard library: socket, json, timeout, etc.
|
|
43
|
+
|
|
44
|
+
spec.add_development_dependency "rspec", "~> 3.13"
|
|
45
|
+
spec.add_development_dependency "rubocop", "~> 1.60"
|
|
46
|
+
spec.add_development_dependency "webmock", "~> 3.23"
|
|
47
|
+
spec.add_development_dependency "yard", "~> 0.9"
|
|
48
|
+
end
|
metadata
ADDED
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: mychron
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.3.2
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- Eugeniu
|
|
8
|
+
bindir: bin
|
|
9
|
+
cert_chain: []
|
|
10
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
|
11
|
+
dependencies:
|
|
12
|
+
- !ruby/object:Gem::Dependency
|
|
13
|
+
name: rspec
|
|
14
|
+
requirement: !ruby/object:Gem::Requirement
|
|
15
|
+
requirements:
|
|
16
|
+
- - "~>"
|
|
17
|
+
- !ruby/object:Gem::Version
|
|
18
|
+
version: '3.13'
|
|
19
|
+
type: :development
|
|
20
|
+
prerelease: false
|
|
21
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
22
|
+
requirements:
|
|
23
|
+
- - "~>"
|
|
24
|
+
- !ruby/object:Gem::Version
|
|
25
|
+
version: '3.13'
|
|
26
|
+
- !ruby/object:Gem::Dependency
|
|
27
|
+
name: rubocop
|
|
28
|
+
requirement: !ruby/object:Gem::Requirement
|
|
29
|
+
requirements:
|
|
30
|
+
- - "~>"
|
|
31
|
+
- !ruby/object:Gem::Version
|
|
32
|
+
version: '1.60'
|
|
33
|
+
type: :development
|
|
34
|
+
prerelease: false
|
|
35
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
36
|
+
requirements:
|
|
37
|
+
- - "~>"
|
|
38
|
+
- !ruby/object:Gem::Version
|
|
39
|
+
version: '1.60'
|
|
40
|
+
- !ruby/object:Gem::Dependency
|
|
41
|
+
name: webmock
|
|
42
|
+
requirement: !ruby/object:Gem::Requirement
|
|
43
|
+
requirements:
|
|
44
|
+
- - "~>"
|
|
45
|
+
- !ruby/object:Gem::Version
|
|
46
|
+
version: '3.23'
|
|
47
|
+
type: :development
|
|
48
|
+
prerelease: false
|
|
49
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
50
|
+
requirements:
|
|
51
|
+
- - "~>"
|
|
52
|
+
- !ruby/object:Gem::Version
|
|
53
|
+
version: '3.23'
|
|
54
|
+
- !ruby/object:Gem::Dependency
|
|
55
|
+
name: yard
|
|
56
|
+
requirement: !ruby/object:Gem::Requirement
|
|
57
|
+
requirements:
|
|
58
|
+
- - "~>"
|
|
59
|
+
- !ruby/object:Gem::Version
|
|
60
|
+
version: '0.9'
|
|
61
|
+
type: :development
|
|
62
|
+
prerelease: false
|
|
63
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
64
|
+
requirements:
|
|
65
|
+
- - "~>"
|
|
66
|
+
- !ruby/object:Gem::Version
|
|
67
|
+
version: '0.9'
|
|
68
|
+
description: |
|
|
69
|
+
A Ruby gem for discovering and downloading telemetry data from AiM MyChron 6
|
|
70
|
+
kart data loggers over Wi-Fi. Works with Ruby on Rails and any Ruby application.
|
|
71
|
+
|
|
72
|
+
Features:
|
|
73
|
+
- UDP device discovery
|
|
74
|
+
- Session listing with lap times and metadata
|
|
75
|
+
- File download with progress callbacks
|
|
76
|
+
- Rails.logger integration
|
|
77
|
+
email:
|
|
78
|
+
- eugeniu.rtj@gmail.com
|
|
79
|
+
executables: []
|
|
80
|
+
extensions: []
|
|
81
|
+
extra_rdoc_files: []
|
|
82
|
+
files:
|
|
83
|
+
- Gemfile
|
|
84
|
+
- IMPLEMENTATION_NOTES.md
|
|
85
|
+
- LICENSE
|
|
86
|
+
- PROTOCOL.md
|
|
87
|
+
- README.md
|
|
88
|
+
- lib/mychron.rb
|
|
89
|
+
- lib/mychron/configuration.rb
|
|
90
|
+
- lib/mychron/device.rb
|
|
91
|
+
- lib/mychron/discovery/detector.rb
|
|
92
|
+
- lib/mychron/discovery/scorer.rb
|
|
93
|
+
- lib/mychron/errors.rb
|
|
94
|
+
- lib/mychron/logging.rb
|
|
95
|
+
- lib/mychron/monitor/watcher.rb
|
|
96
|
+
- lib/mychron/network/arp.rb
|
|
97
|
+
- lib/mychron/network/http_probe.rb
|
|
98
|
+
- lib/mychron/network/scanner.rb
|
|
99
|
+
- lib/mychron/protocol/client.rb
|
|
100
|
+
- lib/mychron/protocol/discovery.rb
|
|
101
|
+
- lib/mychron/session.rb
|
|
102
|
+
- lib/mychron/version.rb
|
|
103
|
+
- mychron.gemspec
|
|
104
|
+
homepage: https://gitlab.com/eugeniu-rtj/mychron
|
|
105
|
+
licenses:
|
|
106
|
+
- MIT
|
|
107
|
+
metadata:
|
|
108
|
+
source_code_uri: https://gitlab.com/eugeniu-rtj/mychron
|
|
109
|
+
changelog_uri: https://gitlab.com/eugeniu-rtj/mychron/blob/main/CHANGELOG.md
|
|
110
|
+
rdoc_options: []
|
|
111
|
+
require_paths:
|
|
112
|
+
- lib
|
|
113
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
114
|
+
requirements:
|
|
115
|
+
- - ">="
|
|
116
|
+
- !ruby/object:Gem::Version
|
|
117
|
+
version: 3.0.0
|
|
118
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
119
|
+
requirements:
|
|
120
|
+
- - ">="
|
|
121
|
+
- !ruby/object:Gem::Version
|
|
122
|
+
version: '0'
|
|
123
|
+
requirements: []
|
|
124
|
+
rubygems_version: 3.6.9
|
|
125
|
+
specification_version: 4
|
|
126
|
+
summary: Ruby library for AiM MyChron 6 telemetry devices
|
|
127
|
+
test_files: []
|