live_f1-core 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- data/.autotest +23 -0
- data/.gitignore +6 -0
- data/.rspec +1 -0
- data/Gemfile +3 -0
- data/Gemfile.lock +65 -0
- data/Guardfile +11 -0
- data/README.rdoc +62 -0
- data/Rakefile +11 -0
- data/bin/live_f1_example +74 -0
- data/features/fixtures/sessions/2012.03.china.qualifying/7136/keyframes.yaml +32722 -0
- data/features/fixtures/sessions/2012.03.china.qualifying/7136/session.key +1 -0
- data/features/fixtures/sessions/2012.03.china.qualifying/session.bin +0 -0
- data/features/fixtures/sessions/2012.04.bahrain.practice.2/7164/keyframes.yaml +22403 -0
- data/features/fixtures/sessions/2012.04.bahrain.practice.2/7164/session.key +1 -0
- data/features/fixtures/sessions/2012.04.bahrain.practice.2/session.bin +0 -0
- data/features/fixtures/sessions/2012.05.bahrain.race.post/7167/keyframes.yaml +1204 -0
- data/features/fixtures/sessions/2012.05.bahrain.race.post/7167/session.key +1 -0
- data/features/fixtures/sessions/2012.05.bahrain.race.post/session.bin +0 -0
- data/features/live_f1.feature +14 -0
- data/features/step_definitions/live_f1_steps.rb +69 -0
- data/features/support/env.rb +9 -0
- data/lib/live_f1/debug.rb +6 -0
- data/lib/live_f1/enum.rb +9 -0
- data/lib/live_f1/event.rb +23 -0
- data/lib/live_f1/packet/car/best_lap_time.rb +10 -0
- data/lib/live_f1/packet/car/driver.rb +14 -0
- data/lib/live_f1/packet/car/gap.rb +10 -0
- data/lib/live_f1/packet/car/interval.rb +10 -0
- data/lib/live_f1/packet/car/lap_count.rb +10 -0
- data/lib/live_f1/packet/car/lap_time.rb +10 -0
- data/lib/live_f1/packet/car/num_pits.rb +10 -0
- data/lib/live_f1/packet/car/number.rb +14 -0
- data/lib/live_f1/packet/car/period_1.rb +11 -0
- data/lib/live_f1/packet/car/period_2.rb +11 -0
- data/lib/live_f1/packet/car/period_3.rb +11 -0
- data/lib/live_f1/packet/car/pit_count.rb +18 -0
- data/lib/live_f1/packet/car/pit_lap_1.rb +10 -0
- data/lib/live_f1/packet/car/pit_lap_2.rb +10 -0
- data/lib/live_f1/packet/car/pit_lap_3.rb +10 -0
- data/lib/live_f1/packet/car/position.rb +14 -0
- data/lib/live_f1/packet/car/position_history.rb +14 -0
- data/lib/live_f1/packet/car/position_update.rb +13 -0
- data/lib/live_f1/packet/car/sector_1.rb +11 -0
- data/lib/live_f1/packet/car/sector_2.rb +11 -0
- data/lib/live_f1/packet/car/sector_3.rb +11 -0
- data/lib/live_f1/packet/car.rb +13 -0
- data/lib/live_f1/packet/decryptable.rb +22 -0
- data/lib/live_f1/packet/header.rb +127 -0
- data/lib/live_f1/packet/sector_time.rb +28 -0
- data/lib/live_f1/packet/sys/commentary.rb +33 -0
- data/lib/live_f1/packet/sys/copyright.rb +9 -0
- data/lib/live_f1/packet/sys/key_frame.rb +17 -0
- data/lib/live_f1/packet/sys/notice.rb +10 -0
- data/lib/live_f1/packet/sys/reset.rb +9 -0
- data/lib/live_f1/packet/sys/session_start.rb +25 -0
- data/lib/live_f1/packet/sys/speed.rb +29 -0
- data/lib/live_f1/packet/sys/timestamp.rb +23 -0
- data/lib/live_f1/packet/sys/track_status.rb +18 -0
- data/lib/live_f1/packet/sys/weather.rb +33 -0
- data/lib/live_f1/packet/sys.rb +10 -0
- data/lib/live_f1/packet.rb +129 -0
- data/lib/live_f1/source/keyframe.rb +30 -0
- data/lib/live_f1/source/live.rb +163 -0
- data/lib/live_f1/source/log.rb +29 -0
- data/lib/live_f1/source/session.rb +41 -0
- data/lib/live_f1/source.rb +83 -0
- data/lib/live_f1/version.rb +3 -0
- data/lib/live_f1.rb +32 -0
- data/live_f1-core.gemspec +26 -0
- data/spec/live_f1/event_spec.rb +5 -0
- data/spec/live_f1/packet/car/best_lap_time_spec.rb +19 -0
- data/spec/live_f1/packet/car/driver_spec.rb +20 -0
- data/spec/live_f1/packet/car/gap_spec.rb +20 -0
- data/spec/live_f1/packet/car/interval_spec.rb +20 -0
- data/spec/live_f1/packet/car/lap_count_spec.rb +20 -0
- data/spec/live_f1/packet/car/lap_time_spec.rb +20 -0
- data/spec/live_f1/packet/car/number_spec.rb +20 -0
- data/spec/live_f1/packet/car/period_1_spec.rb +21 -0
- data/spec/live_f1/packet/car/period_2_spec.rb +21 -0
- data/spec/live_f1/packet/car/period_3_spec.rb +21 -0
- data/spec/live_f1/packet/car/pit_count_spec.rb +20 -0
- data/spec/live_f1/packet/car/pit_lap_1_spec.rb +20 -0
- data/spec/live_f1/packet/car/pit_lap_2_spec.rb +20 -0
- data/spec/live_f1/packet/car/pit_lap_3_spec.rb +20 -0
- data/spec/live_f1/packet/car/position_history_spec.rb +20 -0
- data/spec/live_f1/packet/car/position_spec.rb +20 -0
- data/spec/live_f1/packet/car/position_update_spec.rb +19 -0
- data/spec/live_f1/packet/car/sector_1_spec.rb +21 -0
- data/spec/live_f1/packet/car/sector_2_spec.rb +21 -0
- data/spec/live_f1/packet/car/sector_3_spec.rb +21 -0
- data/spec/live_f1/packet/header_spec.rb +148 -0
- data/spec/live_f1/packet/sys/commentary_spec.rb +45 -0
- data/spec/live_f1/packet/sys/copyright_spec.rb +19 -0
- data/spec/live_f1/packet/sys/key_frame_spec.rb +39 -0
- data/spec/live_f1/packet/sys/notice_spec.rb +20 -0
- data/spec/live_f1/packet/sys/reset_spec.rb +26 -0
- data/spec/live_f1/packet/sys/session_start_spec.rb +31 -0
- data/spec/live_f1/packet/sys/speed_spec.rb +20 -0
- data/spec/live_f1/packet/sys/timestamp_spec.rb +34 -0
- data/spec/live_f1/packet/sys/track_status_spec.rb +20 -0
- data/spec/live_f1/packet/sys/weather_spec.rb +20 -0
- data/spec/live_f1/packet_spec.rb +112 -0
- data/spec/live_f1/source/keyframe_spec.rb +56 -0
- data/spec/live_f1/source/live_spec.rb +106 -0
- data/spec/live_f1/source/session_spec.rb +39 -0
- data/spec/live_f1/source_spec.rb +140 -0
- data/spec/spec_helper.rb +10 -0
- data/spec/support/packet_type_examples.rb +122 -0
- metadata +337 -0
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
module LiveF1
|
|
2
|
+
class Packet
|
|
3
|
+
class Sys
|
|
4
|
+
# A SessionStart packet is the first packet emitted for a session.
|
|
5
|
+
class SessionStart < Sys
|
|
6
|
+
include Packet::Type::Short
|
|
7
|
+
|
|
8
|
+
# The formula1.com unique identifier for this session
|
|
9
|
+
def session_number
|
|
10
|
+
data[1..-1].to_i
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
# The type of event this is (practise, qualifying or race) as defined
|
|
14
|
+
# by the constants in Event::Type
|
|
15
|
+
def event_type
|
|
16
|
+
header.data & 0b0111
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def inspect
|
|
20
|
+
"Event #{session_number} Start (#{Event::Type.name_for event_type})"
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
module LiveF1
|
|
2
|
+
class Packet
|
|
3
|
+
class Sys
|
|
4
|
+
class Speed < Sys
|
|
5
|
+
include Packet::Type::Long
|
|
6
|
+
include Packet::Decryptable
|
|
7
|
+
|
|
8
|
+
def trap
|
|
9
|
+
data.bytes.first
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def speeds
|
|
13
|
+
data[1..-1].split(/\s+/).each_slice(2).map { |d,s| "%s: %d" % [d,s] }.join(", ")
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def to_s
|
|
17
|
+
out = case trap
|
|
18
|
+
when 1..4
|
|
19
|
+
speeds
|
|
20
|
+
else
|
|
21
|
+
data[1..-1]
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
"[%d] %s" % [trap, out]
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
module LiveF1
|
|
2
|
+
class Packet
|
|
3
|
+
class Sys
|
|
4
|
+
# Individual packets don't include any timestamp data to indicate when
|
|
5
|
+
# they happened. However, at selected (but irregular) intervals Timestamp
|
|
6
|
+
# packets are emitted containing the number of seconds since the start of
|
|
7
|
+
# the session.
|
|
8
|
+
class Timestamp < Sys
|
|
9
|
+
include Packet::Type::Timestamp
|
|
10
|
+
include Packet::Decryptable
|
|
11
|
+
|
|
12
|
+
# The number of seconds since the start of the session.
|
|
13
|
+
def number
|
|
14
|
+
data.unpack("v").first
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def to_s
|
|
18
|
+
number
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
module LiveF1
|
|
2
|
+
class Packet
|
|
3
|
+
class Sys
|
|
4
|
+
class TrackStatus < Sys
|
|
5
|
+
include Packet::Type::Short
|
|
6
|
+
include Packet::Decryptable
|
|
7
|
+
|
|
8
|
+
def status
|
|
9
|
+
data.to_i
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def to_s
|
|
13
|
+
Event::TrackStatus.name_for(status)
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
end
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
require 'live_f1/enum'
|
|
2
|
+
|
|
3
|
+
module LiveF1
|
|
4
|
+
class Packet
|
|
5
|
+
class Sys
|
|
6
|
+
class Weather < Sys
|
|
7
|
+
module Metric
|
|
8
|
+
extend LiveF1::Enum
|
|
9
|
+
SESSION_CLOCK = 0
|
|
10
|
+
TRACK_TEMPERATURE = 1
|
|
11
|
+
AIR_TEMPERATURE = 2
|
|
12
|
+
WET_TRACK = 3
|
|
13
|
+
WIND_SPEED = 4
|
|
14
|
+
HUMIDITY = 5
|
|
15
|
+
AIR_PRESSURE = 6
|
|
16
|
+
WIND_DIRECTION = 7
|
|
17
|
+
end
|
|
18
|
+
include Metric
|
|
19
|
+
|
|
20
|
+
include Packet::Type::Short
|
|
21
|
+
include Packet::Decryptable
|
|
22
|
+
|
|
23
|
+
def to_s
|
|
24
|
+
"%-18s - %s" % [Metric.name_for(metric), data]
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def metric
|
|
28
|
+
header.data & 0b0111
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
require 'live_f1/debug'
|
|
2
|
+
|
|
3
|
+
module LiveF1
|
|
4
|
+
# A Packet represents a raw instruction sent from the live timing server to
|
|
5
|
+
# the live timing applet.
|
|
6
|
+
#
|
|
7
|
+
# It is represented in the data stream by a variable-length series of bytes,
|
|
8
|
+
# always starting with a 2-byte "header" and then a number of other bytes
|
|
9
|
+
# depending on the specific data being represented.
|
|
10
|
+
class Packet
|
|
11
|
+
# There are 4 broad categories of packet, where the only difference is how
|
|
12
|
+
# we work out how many bytes of data are expected from the stream.
|
|
13
|
+
#
|
|
14
|
+
# As described in the Header documentation, the 2 header bytes are split
|
|
15
|
+
# into information about the packet type and corresponding car, along with
|
|
16
|
+
# up to 7 bits of payload data.
|
|
17
|
+
module Type
|
|
18
|
+
module Short
|
|
19
|
+
# Short packets use 4 bits of the header data to represent the packet
|
|
20
|
+
# length. Normally this would mean a maximum length of 15, except if a
|
|
21
|
+
# short packet says it has a length of 15 it actually has a length of 0
|
|
22
|
+
def length
|
|
23
|
+
l = (header.data >> 3)
|
|
24
|
+
l == 0x0f ? 0 : l
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def spare_bits
|
|
28
|
+
3
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
module Long
|
|
33
|
+
# Long packets use all 7 bits of the header data to represent the packet
|
|
34
|
+
# length. This allows for a maximum packet length of 127 bytes.
|
|
35
|
+
def length
|
|
36
|
+
header.data
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def spare_bits
|
|
40
|
+
0
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
module Special
|
|
45
|
+
# Special packets never have any additional data.
|
|
46
|
+
def length
|
|
47
|
+
0
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def spare_bits
|
|
51
|
+
7
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
module Timestamp
|
|
56
|
+
# Timestamp packets always contain 2 bytes of data.
|
|
57
|
+
def length
|
|
58
|
+
2
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def spare_bits
|
|
62
|
+
0
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
attr_reader :source
|
|
68
|
+
attr_reader :header
|
|
69
|
+
attr_accessor :data
|
|
70
|
+
|
|
71
|
+
alias bytes data
|
|
72
|
+
|
|
73
|
+
# First extracts a Header from the given source and then extracts the
|
|
74
|
+
# packet it represents, based on the given event type.
|
|
75
|
+
def self.from_source source, event_type
|
|
76
|
+
header = Header.from_source(source, event_type)
|
|
77
|
+
packet = header.packet_klass.new source, header
|
|
78
|
+
data = source.read_bytes(packet.length)
|
|
79
|
+
packet.data = data
|
|
80
|
+
packet
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def initialize source, header
|
|
84
|
+
@source = source
|
|
85
|
+
@header = header
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
def to_s
|
|
89
|
+
data.inspect
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
def spare_bits
|
|
93
|
+
0
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
# Returns the bits of the header data which aren't used to determine the
|
|
97
|
+
# packet length
|
|
98
|
+
def spare_data
|
|
99
|
+
"%0#{spare_bits}b" % (header.data & (2 ** spare_bits - 1))
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
def inspect
|
|
103
|
+
if LiveF1.debug
|
|
104
|
+
"[%7s] %-23s %s" % [spare_data, leader, to_s]
|
|
105
|
+
else
|
|
106
|
+
"%-23s %s" % [leader, to_s]
|
|
107
|
+
end
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
def leader
|
|
111
|
+
self.class.name.sub(/LiveF1::Packet::/, '')
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
private
|
|
115
|
+
# Since some classes override +data=+ to deal with encrypted data, here's a
|
|
116
|
+
# method that can be used in rare cases (e.g. testing) where we need to
|
|
117
|
+
# bypass that process
|
|
118
|
+
def set_data new_data
|
|
119
|
+
@data = new_data
|
|
120
|
+
end
|
|
121
|
+
end
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
require_relative 'packet/header'
|
|
126
|
+
require_relative 'packet/decryptable'
|
|
127
|
+
require_relative 'packet/sector_time'
|
|
128
|
+
require_relative 'packet/sys'
|
|
129
|
+
require_relative 'packet/car'
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
require 'open-uri'
|
|
2
|
+
|
|
3
|
+
module LiveF1
|
|
4
|
+
class Source
|
|
5
|
+
class Keyframe < Source
|
|
6
|
+
attr_reader :io, :parent
|
|
7
|
+
|
|
8
|
+
def initialize io, parent
|
|
9
|
+
@io = io
|
|
10
|
+
@parent = parent
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def read_bytes num
|
|
14
|
+
io.read(num) or raise EOFError
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def session
|
|
18
|
+
parent.session
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def session= new_session
|
|
22
|
+
parent.session = new_session
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def decryption_key session_number
|
|
26
|
+
parent.decryption_key(session_number)
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
require 'net/http'
|
|
2
|
+
require 'cgi'
|
|
3
|
+
require 'timeout'
|
|
4
|
+
require 'fileutils'
|
|
5
|
+
require 'yaml'
|
|
6
|
+
require_relative 'keyframe'
|
|
7
|
+
|
|
8
|
+
module LiveF1
|
|
9
|
+
class Source
|
|
10
|
+
class Live < Source
|
|
11
|
+
|
|
12
|
+
HOST = "80.231.178.249"
|
|
13
|
+
PORT = 4321
|
|
14
|
+
|
|
15
|
+
attr_reader :username, :password
|
|
16
|
+
|
|
17
|
+
def initialize username, password
|
|
18
|
+
@username = username
|
|
19
|
+
@password = password
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def read_bytes num
|
|
23
|
+
begin
|
|
24
|
+
Timeout.timeout(0.5) do
|
|
25
|
+
socket.read(num) or raise EOFError
|
|
26
|
+
end
|
|
27
|
+
rescue Timeout::Error, Errno::ETIMEDOUT => e
|
|
28
|
+
log.flush
|
|
29
|
+
socket.write("\n")
|
|
30
|
+
socket.flush
|
|
31
|
+
retry
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def keyframe number = nil
|
|
36
|
+
io = open("http://#{HOST}/#{keyframe_filename(number)}")
|
|
37
|
+
log.keyframe(number, io.read) if number
|
|
38
|
+
io.rewind
|
|
39
|
+
Source::Keyframe.new io, self
|
|
40
|
+
rescue SocketError
|
|
41
|
+
raise ConnectionError, "Unable to connect to live timing server #{HOST}"
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def decryption_key session_number
|
|
45
|
+
key = open("http://#{HOST}/reg/getkey/#{session_number}.asp?auth=#{auth}").read.to_i(16)
|
|
46
|
+
raise ConnectionError, "Unable to access session key for session #{session_number}. This could indicate incorrect credentials or an issue with the formula1.com key server" if key.zero?
|
|
47
|
+
key
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
# Sets the base directory for live data log files to be stored
|
|
51
|
+
def log_dir= log_directory
|
|
52
|
+
Log.dir = log_directory
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def run
|
|
56
|
+
super do |packet|
|
|
57
|
+
case packet
|
|
58
|
+
when LiveF1::Packet::Sys::SessionStart
|
|
59
|
+
log.start packet.session_number
|
|
60
|
+
log.key session.decryption_key
|
|
61
|
+
when LiveF1::Packet::Sys::KeyFrame
|
|
62
|
+
keyframe(packet.number)
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
log.packet packet
|
|
66
|
+
|
|
67
|
+
yield packet
|
|
68
|
+
end
|
|
69
|
+
rescue Errno::ECONNRESET
|
|
70
|
+
log.flush
|
|
71
|
+
retry
|
|
72
|
+
rescue Exception
|
|
73
|
+
log.flush
|
|
74
|
+
raise
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def session= new_session
|
|
78
|
+
super
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def log
|
|
82
|
+
LogProxy
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
private
|
|
86
|
+
def socket
|
|
87
|
+
@socket ||= TCPSocket.open(HOST, PORT)
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
def auth
|
|
91
|
+
response = Net::HTTP.post_form URI.parse("http://#{HOST}/reg/login"), {"email" => username, "password" => password}
|
|
92
|
+
CGI::Cookie.parse(response["Set-Cookie"])["USER"].first
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
def keyframe_filename number
|
|
96
|
+
"keyframe#{ "_%05d" % number if number}.bin"
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
class LogProxy
|
|
100
|
+
class << self
|
|
101
|
+
def start session_number
|
|
102
|
+
flush
|
|
103
|
+
@log = Log.new session_number
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
[:key, :packet, :keyframe].each do |m|
|
|
107
|
+
define_method m do |*args|
|
|
108
|
+
@log.send(m, *args) if @log
|
|
109
|
+
end
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
def flush
|
|
113
|
+
@log.flush if @log
|
|
114
|
+
@log = nil
|
|
115
|
+
end
|
|
116
|
+
end
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
class Log
|
|
120
|
+
class << self
|
|
121
|
+
attr_reader :dir
|
|
122
|
+
|
|
123
|
+
def dir= log_directory
|
|
124
|
+
@dir = Pathname.new(log_directory).join(Date.today.strftime("%Y%m%d"))
|
|
125
|
+
FileUtils.mkdir_p(@dir)
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
def active?
|
|
129
|
+
!!@dir
|
|
130
|
+
end
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
def initialize session_number
|
|
134
|
+
@filename = Log.dir.join("%s.%s.f1" % [session_number, Time.now.strftime("%H%M%S")]) if Log.active?
|
|
135
|
+
@data = {
|
|
136
|
+
key: nil,
|
|
137
|
+
bytes: "",
|
|
138
|
+
keyframes: {},
|
|
139
|
+
}
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
def key k
|
|
143
|
+
@data[:key] = k
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
def packet p
|
|
147
|
+
@data[:bytes] << p.header.bytes << p.bytes
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
def keyframe n, k
|
|
151
|
+
@data[:keyframes][n] = k
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
def flush
|
|
155
|
+
File.open(@filename, "w") { |f| f.write YAML.dump(@data) } if @filename
|
|
156
|
+
end
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
class ConnectionError < RuntimeError
|
|
160
|
+
end
|
|
161
|
+
end
|
|
162
|
+
end
|
|
163
|
+
end
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
require_relative 'keyframe'
|
|
2
|
+
|
|
3
|
+
module LiveF1
|
|
4
|
+
class Source
|
|
5
|
+
class Log < Source
|
|
6
|
+
|
|
7
|
+
attr_reader :file, :data
|
|
8
|
+
|
|
9
|
+
def initialize file
|
|
10
|
+
@file = file
|
|
11
|
+
@data = YAML.load(@file.read)
|
|
12
|
+
@bytes = StringIO.new(@data[:bytes])
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def read_bytes num
|
|
16
|
+
@bytes.read(num)
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def keyframe number = nil
|
|
20
|
+
keyframe_data = data[:keyframes][number.to_s] || ""
|
|
21
|
+
Source::Keyframe.new StringIO.new(keyframe_data), self
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def decryption_key session_number
|
|
25
|
+
data[:key].to_i
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
module LiveF1
|
|
2
|
+
class Source
|
|
3
|
+
# A source's Session object holds information about the current state of
|
|
4
|
+
# the data stream, and can use that to decrypt the encrypted packets which
|
|
5
|
+
# are retrieved this way.
|
|
6
|
+
#
|
|
7
|
+
# = Decryption
|
|
8
|
+
#
|
|
9
|
+
# Decrypting a string of bytes from the timing stream relies on knowing two
|
|
10
|
+
# pieces of information, a decryption key which is specific to the session
|
|
11
|
+
# in progress, and a decryption salt which starts at a known value but is
|
|
12
|
+
# mutated with every byte that is decrypted.
|
|
13
|
+
#
|
|
14
|
+
# The decryption key can only be obtained from the live timing servers with
|
|
15
|
+
# a valid formula1.com Live Timing account, as described in
|
|
16
|
+
# LiveF1::Source::Live
|
|
17
|
+
class Session < Struct.new(:number, :event_type, :decryption_key)
|
|
18
|
+
INITIAL_DECRYPTION_SALT = 0x55555555
|
|
19
|
+
|
|
20
|
+
attr_accessor :decryption_salt
|
|
21
|
+
|
|
22
|
+
def initialize number, event_type, decryption_key
|
|
23
|
+
super
|
|
24
|
+
reset_decryption_salt!
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
# Decrypts the given string using this session's decryption_key and the
|
|
28
|
+
# current state of the decryption_salt.
|
|
29
|
+
def decrypt input
|
|
30
|
+
input.bytes.map do |b|
|
|
31
|
+
self.decryption_salt = (decryption_salt >> 1) ^ ((decryption_salt & 0x01).zero? ? 0 : decryption_key)
|
|
32
|
+
b ^ (decryption_salt & 0xff)
|
|
33
|
+
end.pack("c*")
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def reset_decryption_salt!
|
|
37
|
+
self.decryption_salt = INITIAL_DECRYPTION_SALT
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
require_relative 'source/live'
|
|
2
|
+
require_relative 'source/log'
|
|
3
|
+
require_relative 'source/session'
|
|
4
|
+
require_relative 'packet'
|
|
5
|
+
|
|
6
|
+
module LiveF1
|
|
7
|
+
# A Source reads raw live timing data packets from the live timing stream and
|
|
8
|
+
# processes them into semantic packets representing discrete instructions
|
|
9
|
+
# sent from the live timing server.
|
|
10
|
+
#
|
|
11
|
+
# These instructions relate to a low-level manipulation of the screen which
|
|
12
|
+
# would be displayed by the live timing Java applet. For example, when a
|
|
13
|
+
# car's sector 1 time is received, the applet needs to clear the other two
|
|
14
|
+
# sector times from the previous lap:
|
|
15
|
+
#
|
|
16
|
+
# Driver Lap S1 S2 S3
|
|
17
|
+
# L. HAMILTON 1:35.324 38.4 22.7 34.2
|
|
18
|
+
#
|
|
19
|
+
# becomes
|
|
20
|
+
#
|
|
21
|
+
# Driver Lap S1 S2 S3
|
|
22
|
+
# L. HAMILTON 1:35.324 39.1
|
|
23
|
+
#
|
|
24
|
+
# However, the live timing stream achieves this by sending three "packets" of
|
|
25
|
+
# data: a sector 1 packet containing "39.1", and sector 2 and 3 packets both
|
|
26
|
+
# containing the empty string "".
|
|
27
|
+
#
|
|
28
|
+
# The methods on a Source instance output all of these packets, even the ones
|
|
29
|
+
# which are useless from a data point of view.
|
|
30
|
+
class Source
|
|
31
|
+
attr_accessor :session
|
|
32
|
+
|
|
33
|
+
# Starts processing data from the relevant location, and yields all packets
|
|
34
|
+
# which are generated from the data stream.
|
|
35
|
+
def run
|
|
36
|
+
# We load packets from a couple of places below, but however we get them
|
|
37
|
+
# we treat them the same, hence this proc.
|
|
38
|
+
packet_processor = proc do |packet|
|
|
39
|
+
case packet
|
|
40
|
+
when LiveF1::Packet::Sys::SessionStart
|
|
41
|
+
self.session = Source::Session.new(packet.session_number, packet.event_type, decryption_key(packet.session_number))
|
|
42
|
+
when LiveF1::Packet::Sys::KeyFrame
|
|
43
|
+
session.reset_decryption_salt!
|
|
44
|
+
end
|
|
45
|
+
yield packet
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
# The live timing stream starts by loading the most recent Keyframe and
|
|
49
|
+
# reading its data. The keyframe contains "setup" information like driver
|
|
50
|
+
# names and numbers to get the app ready to receive live packets.
|
|
51
|
+
#
|
|
52
|
+
# Importantly we check to see whether the source *knows* about other
|
|
53
|
+
# keyframes. This is because the keyframe is itself another source (the
|
|
54
|
+
# +.run+ we call here is actually this very method on a different
|
|
55
|
+
# subclass)
|
|
56
|
+
if self.respond_to? :keyframe
|
|
57
|
+
keyframe.run(&packet_processor)
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
# Now that any keyframe has been parsed, we start streaming raw data
|
|
61
|
+
while packet = read_packet
|
|
62
|
+
packet_processor.call(packet)
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def decrypt bytes
|
|
67
|
+
session.decrypt bytes
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def read_packet
|
|
71
|
+
Packet.from_source(self, (session.event_type if session))
|
|
72
|
+
rescue EOFError
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def read_bytes num
|
|
76
|
+
raise NotImplementedError, "#read_bytes should be implemented by #{self.class}"
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def decryption_key session_number
|
|
80
|
+
raise NotImplementedError, "#decryption_key should be implemented by #{self.class}"
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
end
|
data/lib/live_f1.rb
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
require_relative 'live_f1/source'
|
|
2
|
+
require_relative 'live_f1/debug'
|
|
3
|
+
|
|
4
|
+
# =Formula 1 live timing
|
|
5
|
+
#
|
|
6
|
+
# The LiveF1 library allows realtime parsing of data from the F1 live
|
|
7
|
+
# timing stream. It connects to the binary stream and turns it into a series of
|
|
8
|
+
# objects describing the stream
|
|
9
|
+
#
|
|
10
|
+
# ==Basics
|
|
11
|
+
#
|
|
12
|
+
# The live timing service is primarily used to control the live timing Java
|
|
13
|
+
# applet at http://www.formula1.com/live_timing. However, the richness
|
|
14
|
+
# of the data it provides means that the stream could be used to provide a much
|
|
15
|
+
# deeper view of a session than the applet itself provides. This library
|
|
16
|
+
# provides the very basic toolkit allowing such an application to be built using
|
|
17
|
+
# Ruby, but when using it it's important to remember the service was built
|
|
18
|
+
# around this one visual use.
|
|
19
|
+
#
|
|
20
|
+
# The stream generates packets from the start of every practice, qualifying and
|
|
21
|
+
# race session. However anyone connecting to the stream after the start of a
|
|
22
|
+
# session doesn't get sent the entire packet history. Instead, keyframes
|
|
23
|
+
# containing the current live timing state are regularly generated throughout a
|
|
24
|
+
# session, and new connections are given the latest keyframe followed by the
|
|
25
|
+
# packets generated since that keyframe.
|
|
26
|
+
#
|
|
27
|
+
# ==Usage
|
|
28
|
+
#
|
|
29
|
+
# See bin/live-f1 for usage examples
|
|
30
|
+
#
|
|
31
|
+
module LiveF1
|
|
32
|
+
end
|