live_f1-core 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- 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
|