ffi-hdhomerun 0.4 → 0.5
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/lib/ffi-hdhomerun.rb +136 -11
- data/lib/ffi/hdhomerun.rb +23 -1
- data/spec/hdhomerun_spec.rb +81 -1
- metadata +2 -2
data/lib/ffi-hdhomerun.rb
CHANGED
@@ -5,12 +5,18 @@ require 'pry'
|
|
5
5
|
# Documentation can be found under HDHomeRun::Tuner
|
6
6
|
#
|
7
7
|
module HDHomeRun
|
8
|
-
VERSION = "0.
|
8
|
+
VERSION = "0.5"
|
9
9
|
ERROR_STRING_UNKNOWN_VARIABLE = "ERROR: unknown getset variable"
|
10
|
+
ERROR_REGEX_TUNER_LOCKED = /^ERROR: resource locked by /
|
11
|
+
ERROR_STRING_TUNER_LOCK_EXPIRED = "ERROR: lock no longer held"
|
10
12
|
MAX_TUNERS = 8
|
11
13
|
|
14
|
+
class InvalidDeviceIdError < Exception; end
|
12
15
|
class ConnectionError < Exception; end
|
16
|
+
class InvalidTunerError < Exception; end
|
13
17
|
class UnknownVariableError < Exception; end
|
18
|
+
class TunerLockedError < Exception; end
|
19
|
+
class LockExpiredError < Exception; end
|
14
20
|
|
15
21
|
#
|
16
22
|
# Discover tuners on the network.
|
@@ -24,7 +30,8 @@ module HDHomeRun
|
|
24
30
|
# :tuner_count -- number of tuners on the device (int)
|
25
31
|
#
|
26
32
|
def self.discover
|
27
|
-
results_ptr = FFI::MemoryPointer.new(
|
33
|
+
results_ptr = FFI::MemoryPointer.new(
|
34
|
+
FFI::HDHomeRun::DiscoverDevice, 64)
|
28
35
|
count = FFI::HDHomeRun::discover_find_devices_custom(
|
29
36
|
FFI::HDHomeRun::DEVICE_ID_WILDCARD,
|
30
37
|
FFI::HDHomeRun::DEVICE_TYPE_TUNER,
|
@@ -38,7 +45,7 @@ module HDHomeRun
|
|
38
45
|
(0..count-1).map do |i|
|
39
46
|
|
40
47
|
# construct our hash
|
41
|
-
p = FFI::HDHomeRun::
|
48
|
+
p = FFI::HDHomeRun::DiscoverDevice.new(results_ptr[i])
|
42
49
|
out = { :id => "%08X" % p.device_id,
|
43
50
|
:type => p.device_type,
|
44
51
|
:ip_addr => p.ip_addr }
|
@@ -74,9 +81,11 @@ module HDHomeRun
|
|
74
81
|
p[:id] = "%08X" % p[:id] if p[:id].is_a? Fixnum
|
75
82
|
|
76
83
|
@hd = create_from_str(p[:id].to_s, nil)
|
77
|
-
raise
|
84
|
+
raise InvalidDeviceIdError, "Invalid device id: %s" % p[:id] \
|
85
|
+
if @hd.null?
|
86
|
+
@hd = FFI::HDHomeRun::Device.new(@hd)
|
78
87
|
@id = get_device_id_requested(@hd)
|
79
|
-
raise
|
88
|
+
raise InvalidDeviceIdError, "Invalid device id: %08X" % @id \
|
80
89
|
unless validate_device_id(@id)
|
81
90
|
raise ConnectionError, "Unable to connect to device" \
|
82
91
|
unless get_model_str(@hd)
|
@@ -104,7 +113,7 @@ module HDHomeRun
|
|
104
113
|
error = error.read_string
|
105
114
|
raise UnknownVariableError, "unknown variable '%s'" % key if
|
106
115
|
error == ERROR_STRING_UNKNOWN_VARIABLE
|
107
|
-
|
116
|
+
|
108
117
|
# otherwise, it's a generic runtime error.
|
109
118
|
raise RuntimeError, error
|
110
119
|
end
|
@@ -126,9 +135,14 @@ module HDHomeRun
|
|
126
135
|
|
127
136
|
# Throw a useful exception if this get was for an unknown variable.
|
128
137
|
error = error.read_string
|
129
|
-
raise UnknownVariableError, "unknown variable '%s'" % key
|
130
|
-
error == ERROR_STRING_UNKNOWN_VARIABLE
|
131
|
-
|
138
|
+
raise UnknownVariableError, "unknown variable '%s'" % key \
|
139
|
+
if error == ERROR_STRING_UNKNOWN_VARIABLE
|
140
|
+
|
141
|
+
# Check for locking exceptions
|
142
|
+
raise TunerLockedError, error if error =~ ERROR_REGEX_TUNER_LOCKED
|
143
|
+
raise LockExpiredError, error \
|
144
|
+
if error == ERROR_STRING_TUNER_LOCK_EXPIRED
|
145
|
+
|
132
146
|
# otherwise, it's a generic runtime error.
|
133
147
|
raise RuntimeError, error
|
134
148
|
end
|
@@ -200,6 +214,7 @@ module HDHomeRun
|
|
200
214
|
# Arguments:
|
201
215
|
# [:id] HDHomeRun id (default: "FFFFFFFF")
|
202
216
|
# [:tuner] Tuner number (default: "/tuner0")
|
217
|
+
# [:key] lockkey for tuner, see lock (default: nil)
|
203
218
|
#
|
204
219
|
# Create a tuner instance for the first tuner on any HDHomeRun box
|
205
220
|
# on the network:
|
@@ -213,9 +228,17 @@ module HDHomeRun
|
|
213
228
|
super(p)
|
214
229
|
@tuner = p[:tuner] || 0
|
215
230
|
@tuner = "/tuner%d" % @tuner if @tuner.is_a? Fixnum
|
231
|
+
self.key = p[:key] if p[:key]
|
232
|
+
|
233
|
+
# Verify the format of the tuner argument
|
234
|
+
raise InvalidTunerError, "invalid tuner: %s" % @tuner \
|
235
|
+
if set_tuner_from_str(@hd, @tuner) <= 0
|
216
236
|
|
217
|
-
if
|
218
|
-
|
237
|
+
# Final check to see if the tuner exists on our device.
|
238
|
+
begin
|
239
|
+
get_tuner("status")
|
240
|
+
rescue UnknownVariableError => e
|
241
|
+
raise InvalidTunerError, "invalid tuner: %s, %s" % [@tuner, e]
|
219
242
|
end
|
220
243
|
end
|
221
244
|
|
@@ -292,6 +315,108 @@ module HDHomeRun
|
|
292
315
|
end
|
293
316
|
end
|
294
317
|
|
318
|
+
# Lock the tuner. Optionally a key can be provided to allow
|
319
|
+
# locking across non-persistent connections.
|
320
|
+
#
|
321
|
+
# [:k:] optional key for non-persistent connections
|
322
|
+
#
|
323
|
+
# Examples:
|
324
|
+
#
|
325
|
+
# Locking with a persistent connection:
|
326
|
+
#
|
327
|
+
# t = Tuner.new(:id => 0xFEFEFEFE, :tuner => 0)
|
328
|
+
# t.lock # => true
|
329
|
+
# t.channel = 3 # => 3
|
330
|
+
# # Create a second instance of the tuner and try to change
|
331
|
+
# # the channel.
|
332
|
+
# t2 = Tuner.new(:id => 0xFEFEFEFE, :tuner => 0)
|
333
|
+
# t2.channel = 4 # => TunerLockedError exception
|
334
|
+
# # first tuner instance releases lock
|
335
|
+
# t.unlock # => true
|
336
|
+
# # Now second tuner can make changes.
|
337
|
+
# t2.channel = 4 # => 4
|
338
|
+
#
|
339
|
+
# Locking across non-persistent connections:
|
340
|
+
#
|
341
|
+
# # Grab a tuner and lock it.
|
342
|
+
# t = Tuner.new(:id => 0xFEFEFEFE, :tuner => 0)
|
343
|
+
# t.lock 0xFFFF # => true
|
344
|
+
# t.channel = 3 # => 3
|
345
|
+
# t = nil # => nil
|
346
|
+
#
|
347
|
+
# # Create a new instance referencing the same tuner, cannot
|
348
|
+
# # set tuner variables without setting the key first.
|
349
|
+
# t2 = Tuner.new(:id => 0xFEFEFEFE, :tuner => 0)
|
350
|
+
# t2.channel = 4 # => TunerLockedError exception
|
351
|
+
# t2.key = 0xFFFF # => true
|
352
|
+
# t2.channel = 4 # => 4
|
353
|
+
# t2 = nil # => nil
|
354
|
+
#
|
355
|
+
# # tuner key can be passed in instansiation arguments.
|
356
|
+
# t3 = Tuner.new(:id => 0xFEFEFEFE, :tuner => 0, :key => 0xFFFF)
|
357
|
+
# t3.channel = 5 # => 5
|
358
|
+
#
|
359
|
+
# # Invalid keys (or expired keys) result in a LockExpiredError
|
360
|
+
# t3.key = 0x1234 # => true
|
361
|
+
# t3.channel = 6 # => LockExpiredError exception
|
362
|
+
# t3 = nil # => nil
|
363
|
+
#
|
364
|
+
def lock(k = nil)
|
365
|
+
# If no key was provided, generate a random key for this
|
366
|
+
# session.
|
367
|
+
k ||= rand(0xFFFFFFFF)
|
368
|
+
set_tuner("lockkey", k)
|
369
|
+
self.key = k
|
370
|
+
true
|
371
|
+
end
|
372
|
+
|
373
|
+
# Unlock the tuner. Requires you to already have the correct
|
374
|
+
# key set or use the [:force:] argument.
|
375
|
+
#
|
376
|
+
# Example:
|
377
|
+
# t = Tuner.new(:id => 0xFEFEFEFE, :tuner => 0)
|
378
|
+
# t2 = Tuner.new(:id => 0xFEFEFEFE, :tuner => 0)
|
379
|
+
#
|
380
|
+
# # Lock the tuner and try to unlock it without the key
|
381
|
+
# t.lock # => true
|
382
|
+
# t2.unlock # => TunerLockedError exception
|
383
|
+
# t.locked? # => true
|
384
|
+
#
|
385
|
+
# # force the tuner to unlock
|
386
|
+
# t2.unlock :force # => true
|
387
|
+
# t.locked? # => false
|
388
|
+
#
|
389
|
+
def unlock(force=false)
|
390
|
+
# FIXME uncommenting the following line causes the tests to core
|
391
|
+
# dump.
|
392
|
+
# return true unless locked?
|
393
|
+
set_tuner("lockkey", force != false ? "force" : "none")
|
394
|
+
self.key = 0
|
395
|
+
|
396
|
+
# Strange behavior here, setting the lockkey to "none" without
|
397
|
+
# the correct lockkey does not generate an error. Instead the
|
398
|
+
# new value is ignored by the tuner. So we have to check here
|
399
|
+
# to see if the tuner is still locked here to tell if our unlock
|
400
|
+
# was successful.
|
401
|
+
raise TunerLockedError if locked?
|
402
|
+
true
|
403
|
+
end
|
404
|
+
|
405
|
+
# Specify the key to use when communicating with this tuner.
|
406
|
+
def key=(key)
|
407
|
+
lockkey_use_value(@hd, key.to_i)
|
408
|
+
end
|
409
|
+
|
410
|
+
# check to see if the tuner is locked
|
411
|
+
def locked?
|
412
|
+
get_tuner("lockkey") != "none"
|
413
|
+
end
|
414
|
+
|
415
|
+
# Return the ip address that holds the lock on the tuner
|
416
|
+
def lock_owner
|
417
|
+
get_tuner("lockkey")
|
418
|
+
end
|
419
|
+
|
295
420
|
def to_s
|
296
421
|
"<%s:0x%08x @id=%08X, @tuner=%s>" %
|
297
422
|
[ self.class, object_id, @id, @tuner ]
|
data/lib/ffi/hdhomerun.rb
CHANGED
@@ -47,6 +47,9 @@ module FFI # :nodoc:
|
|
47
47
|
:hdhomerun_discover_find_devices_custom,
|
48
48
|
[ :uint32, :uint32, :uint32, :pointer, :int ],
|
49
49
|
:int
|
50
|
+
attach_function :lockkey_use_value,
|
51
|
+
:hdhomerun_device_tuner_lockkey_use_value,
|
52
|
+
[:pointer, :ulong], :void
|
50
53
|
|
51
54
|
# HDHomeRun capture statistics for a given tuner
|
52
55
|
#
|
@@ -71,6 +74,25 @@ module FFI # :nodoc:
|
|
71
74
|
end
|
72
75
|
end
|
73
76
|
|
77
|
+
class Device < FFI::Struct
|
78
|
+
layout :cs, :pointer,
|
79
|
+
:vs, :pointer,
|
80
|
+
:dbg, :pointer,
|
81
|
+
:scan, :pointer,
|
82
|
+
:multicast_ip, :uint32,
|
83
|
+
:multicast_port, :uint16,
|
84
|
+
:device_id, :uint,
|
85
|
+
:lockkey, :uint32,
|
86
|
+
:name, [:char, 32],
|
87
|
+
:model, [:char, 32]
|
88
|
+
|
89
|
+
members.each do |name|
|
90
|
+
define_method name.to_s do
|
91
|
+
send(:[], name.to_sym)
|
92
|
+
end
|
93
|
+
end
|
94
|
+
end
|
95
|
+
|
74
96
|
# Discover device structure
|
75
97
|
#
|
76
98
|
# Silicon dust changed this structure at some point between
|
@@ -82,7 +104,7 @@ module FFI # :nodoc:
|
|
82
104
|
# FFI_HDHOMERUN_OLD_DEVICE_STRUCT environment variable has been
|
83
105
|
# set.
|
84
106
|
#
|
85
|
-
class
|
107
|
+
class DiscoverDevice < FFI::Struct
|
86
108
|
if ENV['FFI_HDHOMERUN_OLD_DEVICE_STRUCT']
|
87
109
|
layout :ip_addr, :uint32,
|
88
110
|
:device_type, :uint32,
|
data/spec/hdhomerun_spec.rb
CHANGED
@@ -19,7 +19,7 @@ describe HDHomeRun::Device do
|
|
19
19
|
|
20
20
|
it "throws exception on invalid name" do
|
21
21
|
lambda { HDHomeRun::Device.new("WOOF WOOF, I'm a puppy!") } \
|
22
|
-
.should raise_error(
|
22
|
+
.should raise_error(HDHomeRun::InvalidDeviceIdError)
|
23
23
|
end
|
24
24
|
|
25
25
|
it "throws exception on unreachable device" do
|
@@ -74,6 +74,13 @@ describe HDHomeRun::Tuner do
|
|
74
74
|
"to a channel that comes in clearly on your tuner" unless @channel
|
75
75
|
end
|
76
76
|
|
77
|
+
it "throws exception for invalid tuners" do
|
78
|
+
lambda { HDHomeRun::Tuner.new(:tuner => "woof") } \
|
79
|
+
.should raise_error(HDHomeRun::InvalidTunerError)
|
80
|
+
lambda { HDHomeRun::Tuner.new(:tuner => 9999) } \
|
81
|
+
.should raise_error(HDHomeRun::InvalidTunerError)
|
82
|
+
end
|
83
|
+
|
77
84
|
it "can set and get the channel" do
|
78
85
|
@tuner.channel = "none"
|
79
86
|
@tuner.channel.should eq("none")
|
@@ -100,5 +107,78 @@ describe HDHomeRun::Tuner do
|
|
100
107
|
|
101
108
|
bytes.size.should be > 0
|
102
109
|
end
|
110
|
+
|
111
|
+
it "can be locked non-persistently" do
|
112
|
+
tuner2 = HDHomeRun::Tuner.new(:id => @tuner.id, :tuner => @tuner.tuner)
|
113
|
+
@tuner.lock.should be true
|
114
|
+
@tuner.channel = 33
|
115
|
+
|
116
|
+
lambda { tuner2.channel = 3 } \
|
117
|
+
.should raise_error(HDHomeRun::TunerLockedError)
|
118
|
+
|
119
|
+
@tuner.unlock
|
120
|
+
end
|
121
|
+
|
122
|
+
it "can be locked persistently" do
|
123
|
+
t = HDHomeRun::Tuner.new(:id => @tuner.id, :tuner => @tuner.tuner)
|
124
|
+
t.lock(0xFACE).should be true
|
125
|
+
t.key = 0xFACE
|
126
|
+
t.channel = 11
|
127
|
+
|
128
|
+
lambda { @tuner.channel = 3 } \
|
129
|
+
.should raise_error(HDHomeRun::TunerLockedError)
|
130
|
+
|
131
|
+
t = nil
|
132
|
+
t = HDHomeRun::Tuner.new(:id => @tuner.id, :tuner => @tuner.tuner)
|
133
|
+
t.key = 0xFACE
|
134
|
+
|
135
|
+
t.channel = 7
|
136
|
+
|
137
|
+
lambda { @tuner.channel = 3 } \
|
138
|
+
.should raise_error(HDHomeRun::TunerLockedError)
|
139
|
+
|
140
|
+
t.unlock
|
141
|
+
|
142
|
+
@tuner.channel = 3
|
143
|
+
end
|
144
|
+
|
145
|
+
it "cannot be double locked" do
|
146
|
+
t = HDHomeRun::Tuner.new(:id => @tuner.id, :tuner => @tuner.tuner)
|
147
|
+
@tuner.lock
|
148
|
+
lambda { t.lock(0x3333) } \
|
149
|
+
.should raise_error(HDHomeRun::TunerLockedError)
|
150
|
+
@tuner.unlock
|
151
|
+
end
|
152
|
+
|
153
|
+
it "rejects invalid keys" do
|
154
|
+
@tuner.unlock(:force)
|
155
|
+
@tuner.key = 0xFEEDFACE
|
156
|
+
lambda { @tuner.channel = 3 } \
|
157
|
+
.should raise_error(HDHomeRun::LockExpiredError)
|
158
|
+
end
|
159
|
+
|
160
|
+
it "can check to see if the tuner is locked" do
|
161
|
+
@tuner.locked?.should be false
|
162
|
+
t = HDHomeRun::Tuner.new(:id => @tuner.id, :tuner => @tuner.tuner)
|
163
|
+
t.lock
|
164
|
+
@tuner.locked?.should be true
|
165
|
+
t.unlock
|
166
|
+
end
|
167
|
+
|
168
|
+
it "cannot unlock without the correct key" do
|
169
|
+
t = HDHomeRun::Tuner.new(:id => @tuner.id, :tuner => @tuner.tuner)
|
170
|
+
@tuner.lock
|
171
|
+
lambda { t.unlock } \
|
172
|
+
.should raise_error(HDHomeRun::TunerLockedError)
|
173
|
+
@tuner.locked?.should be true
|
174
|
+
@tuner.unlock
|
175
|
+
end
|
176
|
+
|
177
|
+
it "can force a tuner to unlock" do
|
178
|
+
t = HDHomeRun::Tuner.new(:id => @tuner.id, :tuner => @tuner.tuner)
|
179
|
+
@tuner.lock
|
180
|
+
t.unlock(:force)
|
181
|
+
@tuner.locked?.should be false
|
182
|
+
end
|
103
183
|
end
|
104
184
|
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: ffi-hdhomerun
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: '0.
|
4
|
+
version: '0.5'
|
5
5
|
prerelease:
|
6
6
|
platform: ruby
|
7
7
|
authors:
|
@@ -9,7 +9,7 @@ authors:
|
|
9
9
|
autorequire:
|
10
10
|
bindir: bin
|
11
11
|
cert_chain: []
|
12
|
-
date: 2012-
|
12
|
+
date: 2012-09-15 00:00:00.000000000 Z
|
13
13
|
dependencies:
|
14
14
|
- !ruby/object:Gem::Dependency
|
15
15
|
name: ffi
|