nl-knd_client 0.0.0.pre.usegit
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/.gitignore +25 -0
- data/.rspec +1 -0
- data/Gemfile +8 -0
- data/Gemfile.lock +53 -0
- data/LICENSE +661 -0
- data/README.md +59 -0
- data/Rakefile +10 -0
- data/bin/console +7 -0
- data/bin/setup +8 -0
- data/ext/Makefile +36 -0
- data/ext/front.c +26 -0
- data/ext/front_grid.c +43 -0
- data/ext/kinutils/extconf.rb +8 -0
- data/ext/kinutils/kinutils.c +411 -0
- data/ext/kinutils/unpack.c +212 -0
- data/ext/kinutils/unpack.h +108 -0
- data/ext/overhead.c +26 -0
- data/ext/overhead_grid.c +42 -0
- data/ext/side.c +26 -0
- data/ext/side_grid.c +42 -0
- data/ext/unpacktest.c +69 -0
- data/lib/nl/knd_client.rb +21 -0
- data/lib/nl/knd_client/em_knd_client.rb +904 -0
- data/lib/nl/knd_client/em_knd_command.rb +77 -0
- data/lib/nl/knd_client/simple_knd_client.rb +106 -0
- data/lib/nl/knd_client/version.rb +5 -0
- data/lib/nl/knd_client/zone.rb +227 -0
- data/nl-knd_client.gemspec +37 -0
- metadata +145 -0
data/ext/unpacktest.c
ADDED
@@ -0,0 +1,69 @@
|
|
1
|
+
/*
|
2
|
+
* Experimental Kinect depth image unpacking code.
|
3
|
+
* (C)2011 Mike Bourgeous
|
4
|
+
*/
|
5
|
+
#include <stdio.h>
|
6
|
+
#include <string.h>
|
7
|
+
#include <stdint.h>
|
8
|
+
#include <stdlib.h>
|
9
|
+
|
10
|
+
#include "unpack.h"
|
11
|
+
|
12
|
+
int main(int argc, char *argv[])
|
13
|
+
{
|
14
|
+
uint8_t buf11[11];
|
15
|
+
uint8_t buf8out[8];
|
16
|
+
uint16_t buf16out[8];
|
17
|
+
enum { TO_16, TO_8, PIX_TO_16 } mode = TO_16;
|
18
|
+
|
19
|
+
if(argc >= 2) {
|
20
|
+
if(!strcmp(argv[1], "-8")) {
|
21
|
+
mode = TO_8;
|
22
|
+
} else if(!strcmp(argv[1], "-i")) {
|
23
|
+
mode = PIX_TO_16;
|
24
|
+
}
|
25
|
+
}
|
26
|
+
|
27
|
+
if(mode == PIX_TO_16) {
|
28
|
+
uint8_t in[640 * 480 * 11 / 8];
|
29
|
+
uint16_t val;
|
30
|
+
int idx;
|
31
|
+
|
32
|
+
if(fread(in, 1, sizeof(in), stdin) != sizeof(in)) {
|
33
|
+
fprintf(stderr, "Must provide %zu bytes on stdin.\n", sizeof(in));
|
34
|
+
return -1;
|
35
|
+
}
|
36
|
+
|
37
|
+
for(idx = 0; idx < 640 * 480; idx++) {
|
38
|
+
val = 65535 - (pxval_11(in, idx) << 5);
|
39
|
+
fwrite(&val, 2, 1, stdout);
|
40
|
+
}
|
41
|
+
|
42
|
+
return 0;
|
43
|
+
}
|
44
|
+
|
45
|
+
while(!feof(stdin)) {
|
46
|
+
memset(buf11, 0, 11);
|
47
|
+
if(fread(buf11, 1, 11, stdin) <= 0) {
|
48
|
+
break;
|
49
|
+
}
|
50
|
+
|
51
|
+
switch(mode) {
|
52
|
+
case TO_16:
|
53
|
+
unpack11_to_16(buf11, buf16out);
|
54
|
+
fwrite(buf16out, 16, 1, stdout);
|
55
|
+
break;
|
56
|
+
|
57
|
+
case TO_8:
|
58
|
+
unpack11_to_8(buf11, buf8out);
|
59
|
+
fwrite(buf8out, 8, 1, stdout);
|
60
|
+
break;
|
61
|
+
|
62
|
+
default:
|
63
|
+
break;
|
64
|
+
}
|
65
|
+
}
|
66
|
+
|
67
|
+
return 0;
|
68
|
+
}
|
69
|
+
|
@@ -0,0 +1,21 @@
|
|
1
|
+
require "nl/knd_client/version"
|
2
|
+
|
3
|
+
module NL
|
4
|
+
module KndClient
|
5
|
+
end
|
6
|
+
end
|
7
|
+
|
8
|
+
require_relative 'knd_client/kinutils'
|
9
|
+
require_relative 'knd_client/simple_knd_client'
|
10
|
+
|
11
|
+
begin
|
12
|
+
require 'eventmachine'
|
13
|
+
rescue LoadError
|
14
|
+
# Ignore missing eventmachine gem
|
15
|
+
end
|
16
|
+
|
17
|
+
if defined?(EM)
|
18
|
+
require_relative 'knd_client/zone'
|
19
|
+
require_relative 'knd_client/em_knd_command'
|
20
|
+
require_relative 'knd_client/em_knd_client'
|
21
|
+
end
|
@@ -0,0 +1,904 @@
|
|
1
|
+
require 'time'
|
2
|
+
require 'nl/fast_png'
|
3
|
+
|
4
|
+
module NL
|
5
|
+
module KndClient
|
6
|
+
# An asynchronous EventMachine-based client for KND, with full support for
|
7
|
+
# all major KND features.
|
8
|
+
#
|
9
|
+
# There should only be a single global instance of this class at any given
|
10
|
+
# time (TODO: move class-level variables to instance variables, if possible
|
11
|
+
# with EventMachine).
|
12
|
+
class EMKndClient < EM::Connection
|
13
|
+
@@logger = ->(msg) { puts "#{Time.now.iso8601(6)} - #{msg}" }
|
14
|
+
@@bencher = nil
|
15
|
+
@@debug_cmd = nil
|
16
|
+
|
17
|
+
# Reads command debugging configuration from the KNC_DEBUG_CMD
|
18
|
+
# environment variable. Called automatically by .debug_cmd?.
|
19
|
+
def self.init_debug_cmd
|
20
|
+
dbg = ENV['KNC_DEBUG_CMD']
|
21
|
+
|
22
|
+
case dbg
|
23
|
+
when 'true'
|
24
|
+
@@debug_cmd = true
|
25
|
+
|
26
|
+
when 'false'
|
27
|
+
@@debug_cmd = false
|
28
|
+
|
29
|
+
when String
|
30
|
+
@@debug_cmd = dbg.split(',').map(&:strip)
|
31
|
+
|
32
|
+
else
|
33
|
+
@@debug_cmd = false
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
# Returns true if the given command should have debugging info logged.
|
38
|
+
def self.debug_cmd?(cmdname)
|
39
|
+
init_debug_cmd if @@debug_cmd.nil?
|
40
|
+
@@debug_cmd == true || (@@debug_cmd.is_a?(Array) && @@debug_cmd.include?(cmdname))
|
41
|
+
end
|
42
|
+
|
43
|
+
# Sets a block to be called for any log messages generated by EMKndClient
|
44
|
+
# and related classes. The default is to print messages to STDOUT with a
|
45
|
+
# timestamp. Calling without a block will disable logging.
|
46
|
+
def self.on_log(&block)
|
47
|
+
@@logger = block
|
48
|
+
end
|
49
|
+
|
50
|
+
# Logs a message to STDOUT, or to the logging function specified by
|
51
|
+
# .on_log.
|
52
|
+
#
|
53
|
+
# The KNC project this code was extracted from was written without
|
54
|
+
# awareness of the Ruby Logger class.
|
55
|
+
def self.log(msg)
|
56
|
+
@@logger.call(msg) if @@logger
|
57
|
+
end
|
58
|
+
|
59
|
+
# Calls the given +block+ for certain named sections of code. The
|
60
|
+
# benchmark block must accept a name parameter and a proc parameter, and
|
61
|
+
# call the proc. Disables EMKndClient benchmarking if no block is given.
|
62
|
+
# This is used by KNC to instrument
|
63
|
+
def self.on_bench(&block)
|
64
|
+
@@bencher = block
|
65
|
+
end
|
66
|
+
|
67
|
+
# Some EMKndClient functions call this method to wrap named sections of
|
68
|
+
# code with optional instrumentation. Use the .on_bench method to enable
|
69
|
+
# benchmarking/instrumentation.
|
70
|
+
def self.bench(name, &block)
|
71
|
+
if @@bencher
|
72
|
+
@@bencher.call(name, block)
|
73
|
+
else
|
74
|
+
yield
|
75
|
+
end
|
76
|
+
end
|
77
|
+
|
78
|
+
include EM::Protocols::LineText2
|
79
|
+
|
80
|
+
DEPTH_SIZE = 640 * 480 * 11 / 8
|
81
|
+
VIDEO_SIZE = 640 * 480
|
82
|
+
BLANK_IMAGE = NL::FastPng.store_png(640, 480, 8, "\x00" * (640 * 480))
|
83
|
+
|
84
|
+
def self.blank_image
|
85
|
+
BLANK_IMAGE
|
86
|
+
end
|
87
|
+
|
88
|
+
@@zones = {}
|
89
|
+
|
90
|
+
@@images = {
|
91
|
+
:depth => BLANK_IMAGE,
|
92
|
+
:linear => BLANK_IMAGE,
|
93
|
+
:ovh => BLANK_IMAGE,
|
94
|
+
:side => BLANK_IMAGE,
|
95
|
+
:front => BLANK_IMAGE,
|
96
|
+
:video => BLANK_IMAGE
|
97
|
+
}
|
98
|
+
|
99
|
+
@@connect_cb = nil
|
100
|
+
|
101
|
+
@@connection = 0
|
102
|
+
@@connected = false
|
103
|
+
@@instance = nil
|
104
|
+
@@fps = 0
|
105
|
+
|
106
|
+
# The time of the last connection/disconnection event
|
107
|
+
@@connection_time = Time.now
|
108
|
+
|
109
|
+
# Returns the most recent PNG of the given type, an empty string if no
|
110
|
+
# data, or nil if an invalid type.
|
111
|
+
def self.png_data type
|
112
|
+
@@images[type]
|
113
|
+
end
|
114
|
+
|
115
|
+
def self.clear_images
|
116
|
+
@@images.each_key do |k|
|
117
|
+
@@images[k] = BLANK_IMAGE
|
118
|
+
end
|
119
|
+
end
|
120
|
+
|
121
|
+
# Whether the client is connected to the knd server
|
122
|
+
def self.connected?
|
123
|
+
@@connected || false
|
124
|
+
end
|
125
|
+
|
126
|
+
# Sets or replaces a proc called with true or false when a connection
|
127
|
+
# is made or lost.
|
128
|
+
def self.on_connect &bl
|
129
|
+
@@connect_cb = bl
|
130
|
+
end
|
131
|
+
|
132
|
+
def self.hostname
|
133
|
+
@@hostname
|
134
|
+
end
|
135
|
+
|
136
|
+
def self.hostname= name
|
137
|
+
@@instance.close_connection_after_writing if @@connected
|
138
|
+
@@hostname = name
|
139
|
+
end
|
140
|
+
|
141
|
+
# Changes the current hostname and opens the connection loop. If
|
142
|
+
# connection fails, it will be retried automatically, so this should only
|
143
|
+
# be called once for the application.
|
144
|
+
def self.connect(hostname)
|
145
|
+
self.hostname = hostname || '127.0.0.1'
|
146
|
+
begin
|
147
|
+
EM.connect(self.hostname, 14308, EMKndClient)
|
148
|
+
rescue => e
|
149
|
+
log e, 'Error resolving KND.'
|
150
|
+
raise e
|
151
|
+
end
|
152
|
+
end
|
153
|
+
|
154
|
+
# The currently-connected client instance.
|
155
|
+
def self.instance
|
156
|
+
@@instance if @@connected
|
157
|
+
end
|
158
|
+
|
159
|
+
def self.zones
|
160
|
+
@@zones
|
161
|
+
end
|
162
|
+
|
163
|
+
def self.occupied
|
164
|
+
@@occupied
|
165
|
+
end
|
166
|
+
|
167
|
+
def self.fps
|
168
|
+
@@fps
|
169
|
+
end
|
170
|
+
|
171
|
+
def enter_depth
|
172
|
+
@binary = :depth
|
173
|
+
set_binary_mode(DEPTH_SIZE)
|
174
|
+
end
|
175
|
+
|
176
|
+
def enter_video(length = VIDEO_SIZE)
|
177
|
+
@binary = :video
|
178
|
+
set_binary_mode(length)
|
179
|
+
end
|
180
|
+
|
181
|
+
def leave_binary
|
182
|
+
@binary = :none
|
183
|
+
end
|
184
|
+
|
185
|
+
def initialize
|
186
|
+
super
|
187
|
+
@@connection = @@connection + 1
|
188
|
+
@thiscon = @@connection
|
189
|
+
@binary = :none
|
190
|
+
@quit = false
|
191
|
+
@commands = []
|
192
|
+
@active_command = nil
|
193
|
+
@@connected ||= false
|
194
|
+
@tcp_ok = false
|
195
|
+
@tcp_connected = false
|
196
|
+
|
197
|
+
@getbright_sent = false # Whether a getbright command is in the queue
|
198
|
+
@depth_sent = false # Whether a getdepth command is in the queue
|
199
|
+
@video_sent = false # Whether a getvideo command is in the queue
|
200
|
+
@requests = {
|
201
|
+
:depth => [],
|
202
|
+
:linear => [],
|
203
|
+
:ovh => [],
|
204
|
+
:side => [],
|
205
|
+
:front => [],
|
206
|
+
:video => []
|
207
|
+
}
|
208
|
+
|
209
|
+
# The Mutex isn't necessary if all signaling takes place on the event loop
|
210
|
+
@image_lock = Mutex.new
|
211
|
+
|
212
|
+
# Zone/status update callbacks (for protocol plugins like xAP)
|
213
|
+
@cbs = []
|
214
|
+
end
|
215
|
+
|
216
|
+
def connection_completed
|
217
|
+
@tcp_connected = true
|
218
|
+
|
219
|
+
log "Connected to depth camera server at #{EMKndClient.hostname} (connection #{@thiscon})."
|
220
|
+
|
221
|
+
fps_proc = proc {
|
222
|
+
do_command('fps') {|cmd|
|
223
|
+
fps = cmd.message.to_i()
|
224
|
+
if !@@connected && fps > 0
|
225
|
+
log "Depth camera server is online (connection #{@thiscon})."
|
226
|
+
|
227
|
+
@@connected = true
|
228
|
+
@@connect_cb.call true if @@connect_cb
|
229
|
+
|
230
|
+
now = Time.now
|
231
|
+
elapsed = now - @@connection_time
|
232
|
+
@@connection_time = now
|
233
|
+
call_cbs :online, true, elapsed
|
234
|
+
end
|
235
|
+
|
236
|
+
@@fps = fps
|
237
|
+
call_cbs :fps, @@fps
|
238
|
+
|
239
|
+
@fpstimer = EM::Timer.new(0.3333333) do
|
240
|
+
fps_proc.call
|
241
|
+
end
|
242
|
+
}.errback {|cmd|
|
243
|
+
if cmd == nil
|
244
|
+
log "FPS command timed out. Disconnecting from depth camera server (connection #{@thiscon})."
|
245
|
+
else
|
246
|
+
log "FPS command failed: #{cmd.message}. Disconnecting from depth camera server (connection #{@thiscon})."
|
247
|
+
end
|
248
|
+
close_connection
|
249
|
+
}
|
250
|
+
}
|
251
|
+
zone_proc = proc {
|
252
|
+
get_zones {
|
253
|
+
# TODO: Unsubscribe and defer zones.json response response when
|
254
|
+
# there is no web activity and xAP is off.
|
255
|
+
@zonetimer = EM::Timer.new(2) do
|
256
|
+
zone_proc.call
|
257
|
+
end
|
258
|
+
}
|
259
|
+
}
|
260
|
+
|
261
|
+
startup_proc = proc do
|
262
|
+
fps_proc.call
|
263
|
+
zone_proc.call
|
264
|
+
subscribe().errback { |cmd|
|
265
|
+
if cmd == nil
|
266
|
+
log "Subscribe command timed out. Disconnecting from depth camera server (connection #{@thiscon})."
|
267
|
+
else
|
268
|
+
log "Subscribe command failed: #{cmd.message}. Disconnecting from depth camera server (connection #{@thiscon})."
|
269
|
+
end
|
270
|
+
close_connection
|
271
|
+
}
|
272
|
+
end
|
273
|
+
|
274
|
+
do_command('ver') { |cmd|
|
275
|
+
@version = cmd.message.split(' ', 2).last.to_i if cmd.message
|
276
|
+
@fpstimer = EM::Timer.new(0.1, &startup_proc)
|
277
|
+
log "Protocol version is #{@version}."
|
278
|
+
}.errback { |cmd|
|
279
|
+
if cmd == nil
|
280
|
+
log "Version command timed out. Disconnecting (connection #{@thiscon})."
|
281
|
+
close_connection
|
282
|
+
else
|
283
|
+
log "Version command failed. Assuming version 1."
|
284
|
+
@version = 1
|
285
|
+
@fpstimer = EM::Timer.new(0.1, &startup_proc)
|
286
|
+
end
|
287
|
+
}
|
288
|
+
|
289
|
+
@@instance = self
|
290
|
+
rescue => e
|
291
|
+
log e
|
292
|
+
Kernel.exit
|
293
|
+
end
|
294
|
+
|
295
|
+
def receive_line(data)
|
296
|
+
# Send lines to any command waiting for data
|
297
|
+
if @active_command
|
298
|
+
@active_command = nil if @active_command.add_line data
|
299
|
+
return
|
300
|
+
end
|
301
|
+
|
302
|
+
type, message = data.split(" - ", 2)
|
303
|
+
|
304
|
+
case type
|
305
|
+
when "DEPTH"
|
306
|
+
enter_depth
|
307
|
+
|
308
|
+
when 'VIDEO'
|
309
|
+
length = message.gsub(/[^0-9 ]+/, '').to_i
|
310
|
+
enter_video length
|
311
|
+
|
312
|
+
when 'BRIGHT'
|
313
|
+
line = message.kin_kvp
|
314
|
+
name = Zone.fix_name! line['name']
|
315
|
+
if @@zones.has_key? name
|
316
|
+
match = @@zones[name]
|
317
|
+
match['bright'] = line['bright'].to_i
|
318
|
+
call_cbs :change, match
|
319
|
+
else
|
320
|
+
log "=== NOTICE - BRIGHT line for missing zone #{name}"
|
321
|
+
end
|
322
|
+
|
323
|
+
when "SUB"
|
324
|
+
zone = Zone.new(message)
|
325
|
+
name = zone["name"]
|
326
|
+
if !@@zones.has_key? name
|
327
|
+
log "=== NOTICE - SUB added zone #{name} ==="
|
328
|
+
@@zones[name] = zone
|
329
|
+
call_cbs :add, zone
|
330
|
+
else
|
331
|
+
match = @@zones[name]
|
332
|
+
match.merge_zone zone
|
333
|
+
call_cbs :change, match
|
334
|
+
end
|
335
|
+
|
336
|
+
when "ADD"
|
337
|
+
zone = Zone.new(message)
|
338
|
+
name = zone["name"]
|
339
|
+
@@zones[name] = zone
|
340
|
+
log "Zone #{name} added via ADD"
|
341
|
+
call_cbs :add, zone
|
342
|
+
|
343
|
+
when "DEL"
|
344
|
+
name = message
|
345
|
+
log "Zone #{name} removed via DEL"
|
346
|
+
if @@zones.include? message
|
347
|
+
zone = @@zones[message]
|
348
|
+
@@zones.delete message
|
349
|
+
call_cbs :del, zone
|
350
|
+
else
|
351
|
+
puts "=== ERROR - DEL received for nonexistent zone ==="
|
352
|
+
end
|
353
|
+
|
354
|
+
when "OK"
|
355
|
+
if @commands.length == 0
|
356
|
+
puts "=== ERROR - OK when no command was queued - disconnecting ==="
|
357
|
+
close_connection
|
358
|
+
else
|
359
|
+
cmd = @commands.shift
|
360
|
+
active = cmd.ok_line message
|
361
|
+
@active_command = cmd unless active
|
362
|
+
end
|
363
|
+
|
364
|
+
when "ERR"
|
365
|
+
if @commands.length == 0
|
366
|
+
puts "=== ERROR - ERR when no command was queued - disconnecting ==="
|
367
|
+
close_connection
|
368
|
+
end
|
369
|
+
@commands.shift.err_line message
|
370
|
+
|
371
|
+
else
|
372
|
+
puts "----- Unknown Response -----"
|
373
|
+
p data
|
374
|
+
end
|
375
|
+
end
|
376
|
+
|
377
|
+
def receive_binary_data(d)
|
378
|
+
case @binary
|
379
|
+
when :depth
|
380
|
+
EM.defer do
|
381
|
+
data = d
|
382
|
+
begin
|
383
|
+
@image_lock.synchronize do
|
384
|
+
@depth_sent = false
|
385
|
+
end
|
386
|
+
|
387
|
+
unpacked = nil
|
388
|
+
|
389
|
+
if(check_requests(:front) or check_requests(:ovh) or check_requests(:depth) or
|
390
|
+
check_requests(:side) or check_requests(:linear))
|
391
|
+
EMKndClient.bench('unpack') do
|
392
|
+
unpacked = Kinutils.unpack11_to_16(data)
|
393
|
+
end
|
394
|
+
else
|
395
|
+
raise "---- Received an unneeded depth image"
|
396
|
+
end
|
397
|
+
|
398
|
+
if check_requests(:depth)
|
399
|
+
EMKndClient.bench('16png') do
|
400
|
+
set_image :depth, NL::FastPng.store_png(640, 480, 16, unpacked)
|
401
|
+
end
|
402
|
+
end
|
403
|
+
|
404
|
+
if check_requests(:linear)
|
405
|
+
linbuf = nil
|
406
|
+
EMKndClient.bench('linear_plot') do
|
407
|
+
linbuf = Kinutils.plot_linear(unpacked)
|
408
|
+
end
|
409
|
+
EMKndClient.bench('linear_png') do
|
410
|
+
set_image :linear, NL::FastPng.store_png(640, 480, 8, linbuf)
|
411
|
+
end
|
412
|
+
end
|
413
|
+
|
414
|
+
if check_requests(:ovh)
|
415
|
+
ovhbuf = nil
|
416
|
+
EMKndClient.bench('ovh_plot') do
|
417
|
+
ovhbuf = Kinutils.plot_overhead(unpacked)
|
418
|
+
end
|
419
|
+
EMKndClient.bench('ovh_png') do
|
420
|
+
set_image :ovh, NL::FastPng.store_png(KNC_XPIX, KNC_ZPIX, 8, ovhbuf)
|
421
|
+
end
|
422
|
+
end
|
423
|
+
|
424
|
+
if check_requests(:side)
|
425
|
+
sidebuf = nil
|
426
|
+
EMKndClient.bench('side_plot') do
|
427
|
+
sidebuf = Kinutils.plot_side(unpacked)
|
428
|
+
end
|
429
|
+
EMKndClient.bench('side_png') do
|
430
|
+
set_image :side, NL::FastPng.store_png(KNC_ZPIX, KNC_YPIX, 8, sidebuf)
|
431
|
+
end
|
432
|
+
end
|
433
|
+
|
434
|
+
if check_requests(:front)
|
435
|
+
frontbuf = nil
|
436
|
+
EMKndClient.bench('front_plot') do
|
437
|
+
frontbuf = Kinutils.plot_front(unpacked)
|
438
|
+
end
|
439
|
+
EMKndClient.bench('front_png') do
|
440
|
+
set_image :front, NL::FastPng.store_png(KNC_XPIX, KNC_YPIX, 8, frontbuf)
|
441
|
+
end
|
442
|
+
end
|
443
|
+
rescue => e
|
444
|
+
log "Error in depth image processing task: #{e.to_s}"
|
445
|
+
log "\t#{e.backtrace.join("\n\t")}"
|
446
|
+
end
|
447
|
+
end
|
448
|
+
|
449
|
+
when :video
|
450
|
+
EM.defer do
|
451
|
+
data = d
|
452
|
+
begin
|
453
|
+
@image_lock.synchronize do
|
454
|
+
@video_sent = false
|
455
|
+
end
|
456
|
+
|
457
|
+
unpacked = nil
|
458
|
+
|
459
|
+
unless check_requests(:video)
|
460
|
+
raise "---- Received an unneeded video image"
|
461
|
+
end
|
462
|
+
|
463
|
+
if d.bytesize != VIDEO_SIZE
|
464
|
+
set_image :video, BLANK_IMAGE
|
465
|
+
raise "---- Unknown video image format with size #{d.bytesize}; expected #{VIDEO_SIZE}"
|
466
|
+
end
|
467
|
+
|
468
|
+
EMKndClient.bench('videopng') do
|
469
|
+
set_image :video, NL::FastPng.store_png(640, 480, 8, data)
|
470
|
+
end
|
471
|
+
|
472
|
+
rescue => e
|
473
|
+
log "Error in video image processing task: #{e.to_s}"
|
474
|
+
log "\t#{e.backtrace.join("\n\t")}"
|
475
|
+
end
|
476
|
+
end
|
477
|
+
end
|
478
|
+
|
479
|
+
leave_binary
|
480
|
+
end
|
481
|
+
|
482
|
+
def unbind
|
483
|
+
begin
|
484
|
+
if @tcp_connected
|
485
|
+
log "Disconnected from camera server (connection #{@thiscon})."
|
486
|
+
@@connect_cb.call false if @@connect_cb
|
487
|
+
|
488
|
+
if @@connected
|
489
|
+
now = Time.now
|
490
|
+
elapsed = now - @@connection_time
|
491
|
+
@@connection_time = now
|
492
|
+
call_cbs :online, false, elapsed
|
493
|
+
end
|
494
|
+
|
495
|
+
@cbs.clear
|
496
|
+
end
|
497
|
+
|
498
|
+
EMKndClient.clear_images
|
499
|
+
|
500
|
+
@@connected = false
|
501
|
+
@@fps = 0
|
502
|
+
@@instance = nil
|
503
|
+
@fpstimer.cancel if @fpstimer
|
504
|
+
@zonetimer.cancel if @zonetimer
|
505
|
+
@imagetimer.cancel if @imagetimer
|
506
|
+
@refreshtimer.cancel if @refreshtimer
|
507
|
+
|
508
|
+
@commands.each do |cmd|
|
509
|
+
cmd.err_line "Connection closed"
|
510
|
+
end
|
511
|
+
|
512
|
+
@requests.each do |k, v|
|
513
|
+
v.each do |req|
|
514
|
+
req.call @@images[k]
|
515
|
+
end
|
516
|
+
end
|
517
|
+
|
518
|
+
if @quit
|
519
|
+
EventMachine::stop_event_loop
|
520
|
+
else
|
521
|
+
EM::Timer.new(1) do
|
522
|
+
EM.connect(@@hostname, 14308, EMKndClient)
|
523
|
+
end
|
524
|
+
end
|
525
|
+
rescue => e
|
526
|
+
log e
|
527
|
+
Kernel.exit
|
528
|
+
end
|
529
|
+
end
|
530
|
+
|
531
|
+
# Pass a string or a EMKndCommand, returns the EMKndCommand. The block, if
|
532
|
+
# any, will be used as a EMKndCommand success callback
|
533
|
+
#
|
534
|
+
# Most of the time you should use a more specific method, e.g. #add_zone,
|
535
|
+
# #clear_zones, etc.
|
536
|
+
def do_command command, &block
|
537
|
+
if command.is_a? EMKndCommand
|
538
|
+
cmd = command
|
539
|
+
else
|
540
|
+
cmd = EMKndCommand.new command
|
541
|
+
end
|
542
|
+
|
543
|
+
if block != nil
|
544
|
+
cmd.callback do |*args|
|
545
|
+
block.call *args
|
546
|
+
end
|
547
|
+
end
|
548
|
+
|
549
|
+
log "do_command #{cmd.to_s}" if EMKndClient.debug_cmd?(cmd.name)
|
550
|
+
send_data "#{cmd.to_s}\n"
|
551
|
+
@commands << cmd
|
552
|
+
cmd
|
553
|
+
end
|
554
|
+
|
555
|
+
# TODO: Could do a multi-command function by having all deferred
|
556
|
+
# command methods return the command object used, throwing them in an
|
557
|
+
# array, passing the array to do_multi_cmd, and do_multi_cmd adding its
|
558
|
+
# own success/failure handlers to each command object.
|
559
|
+
|
560
|
+
# Defers an update of the zone list. This will run the zones command
|
561
|
+
# to see if any zones have been removed. The block will be called with
|
562
|
+
# no parameters on success, if a block is given.
|
563
|
+
def get_zones &block
|
564
|
+
# No subscription received after this zone command finishes can
|
565
|
+
# contain a zone that was removed prior to the execution of
|
566
|
+
# this command
|
567
|
+
do_command 'zones' do |cmd|
|
568
|
+
EMKndClient.bench('get_zones') do
|
569
|
+
old_zones = @@zones
|
570
|
+
zonelist = {}
|
571
|
+
cmd.lines.each do |line|
|
572
|
+
zone = Zone.new line
|
573
|
+
oldzone = @@zones[zone['name']]
|
574
|
+
zone['bright'] = oldzone['bright'] if oldzone && oldzone.include?('bright')
|
575
|
+
zonelist[zone['name']] = zone
|
576
|
+
end
|
577
|
+
|
578
|
+
@@zones = zonelist
|
579
|
+
|
580
|
+
# Notify protocol plugin callbacks about new zones
|
581
|
+
EMKndClient.bench('get_zones_callbacks') do
|
582
|
+
unless @cbs.empty?
|
583
|
+
zonelist.each do |k, v|
|
584
|
+
if !old_zones.include? k
|
585
|
+
log "Zone #{k} added in get_zones"
|
586
|
+
call_cbs :add, v
|
587
|
+
elsif v['occupied'] != @@zones[k]['occupied']
|
588
|
+
log "Zone #{k} changed in get_zones"
|
589
|
+
call_cbs :change, v
|
590
|
+
end
|
591
|
+
end
|
592
|
+
old_zones.each do |k, v|
|
593
|
+
if !zonelist.include? k
|
594
|
+
log "Zone #{k} removed in get_zones"
|
595
|
+
call_cbs :del, v
|
596
|
+
end
|
597
|
+
end
|
598
|
+
end
|
599
|
+
end
|
600
|
+
|
601
|
+
@@occupied = cmd.message.gsub(/.*, ([0-9]+) occupied.*/, '\1').to_i if cmd.message
|
602
|
+
end
|
603
|
+
|
604
|
+
if block != nil
|
605
|
+
block.call
|
606
|
+
end
|
607
|
+
end
|
608
|
+
end
|
609
|
+
|
610
|
+
# Subscribes to zone updates. The given block will be called with a
|
611
|
+
# success message on success. The return value is the command object
|
612
|
+
# that represents the subscribe command (e.g. for adding an errback).
|
613
|
+
def subscribe &block
|
614
|
+
do_command 'sub' do |cmd|
|
615
|
+
block.call cmd.message if block != nil
|
616
|
+
end
|
617
|
+
end
|
618
|
+
|
619
|
+
# Updates parameters on the given zone. The zone argument should be a
|
620
|
+
# Zone with only the changed parameters and zone name filled in. The
|
621
|
+
# changes will be merged with the existing zone data. If xmin, ymin,
|
622
|
+
# zmin, xmax, ymax, and zmax are all specified, then any other
|
623
|
+
# attributes will be ignored. Attributes will be set in the order they
|
624
|
+
# are returned by iterating over the keys in zone. The block, if
|
625
|
+
# specified, will be called with true and a message for success, false
|
626
|
+
# and a message for error. If multiple parameters are set, messages
|
627
|
+
# from individual commands will be separated by separator.
|
628
|
+
def set_zone zone, separator="\n", &block
|
629
|
+
zone = zone.clone
|
630
|
+
|
631
|
+
name = zone['name']
|
632
|
+
zone.delete 'name'
|
633
|
+
if name == nil || name.length == 0
|
634
|
+
if block != nil
|
635
|
+
block.call false, "No zone name was given."
|
636
|
+
end
|
637
|
+
return
|
638
|
+
end
|
639
|
+
if !@@zones.has_key? name
|
640
|
+
if block != nil
|
641
|
+
block.call false, "Zone #{name} doesn't exist."
|
642
|
+
end
|
643
|
+
return
|
644
|
+
end
|
645
|
+
|
646
|
+
all = ['xmin', 'ymin', 'zmin', 'xmax', 'ymax', 'zmax'].reduce(true) { |has, attr|
|
647
|
+
has &= zone.has_key? attr
|
648
|
+
}
|
649
|
+
|
650
|
+
if all
|
651
|
+
cmd = EMKndCommand.new 'setzone', name, 'all',
|
652
|
+
zone['xmin'], zone['ymin'], zone['zmin'],
|
653
|
+
zone['xmax'], zone['ymax'], zone['zmax']
|
654
|
+
|
655
|
+
if block != nil
|
656
|
+
cmd.callback { |cmd|
|
657
|
+
@@zones.has_key?(name) && @@zones[name].merge_zone(zone)
|
658
|
+
block.call true, cmd.message
|
659
|
+
}
|
660
|
+
cmd.errback {|cmd| block.call false, (cmd ? cmd.message : 'timeout')}
|
661
|
+
end
|
662
|
+
|
663
|
+
do_command cmd
|
664
|
+
|
665
|
+
['xmin', 'ymin', 'zmin', 'xmax', 'ymax', 'zmax'].each do |key|
|
666
|
+
zone.delete key
|
667
|
+
end
|
668
|
+
end
|
669
|
+
|
670
|
+
# Send values not covered by xmin/ymin/zmin/xmax/ymax/zmax combo above
|
671
|
+
unless zone.empty?
|
672
|
+
# TODO: Extract a multi-command method from this
|
673
|
+
zone = zone.clone
|
674
|
+
zone.delete 'name'
|
675
|
+
|
676
|
+
if zone.length == 0
|
677
|
+
if block != nil
|
678
|
+
block.call false, "No parameters were specified."
|
679
|
+
end
|
680
|
+
return
|
681
|
+
end
|
682
|
+
|
683
|
+
result = true
|
684
|
+
messages = []
|
685
|
+
cmds = []
|
686
|
+
count = 0
|
687
|
+
func = lambda {|msg|
|
688
|
+
messages << msg
|
689
|
+
count += 1
|
690
|
+
if block != nil and count == cmds.length
|
691
|
+
block.call result, messages.join(separator)
|
692
|
+
end
|
693
|
+
}
|
694
|
+
|
695
|
+
zone.each do |k, v|
|
696
|
+
if v == true
|
697
|
+
v = 1
|
698
|
+
elsif v == false
|
699
|
+
v = 0
|
700
|
+
end
|
701
|
+
cmd = EMKndCommand.new 'setzone', name, k, v
|
702
|
+
cmd.callback { |cmd|
|
703
|
+
@@zones.has_key?(name) && @@zones[name][k] = v
|
704
|
+
func.call cmd.message
|
705
|
+
}
|
706
|
+
cmd.errback { |cmd|
|
707
|
+
result = false
|
708
|
+
func.call (cmd ? cmd.message : 'timeout')
|
709
|
+
}
|
710
|
+
cmds << cmd
|
711
|
+
end
|
712
|
+
cmds.each do |cmd| do_command cmd end
|
713
|
+
end
|
714
|
+
end
|
715
|
+
|
716
|
+
# Adds a new zone. Calls block with true and a message for success,
|
717
|
+
# false and a message for error.
|
718
|
+
def add_zone zone, &block
|
719
|
+
zone['name'] ||= 'New_Zone'
|
720
|
+
zone['name'].gsub!(/ +/, '_')
|
721
|
+
zone['name'].gsub!(/[^A-Za-z0-9_]/, '')
|
722
|
+
|
723
|
+
if zone['name'].downcase == '__status'
|
724
|
+
block.call false, 'Cannot use "__status" for a zone name.'
|
725
|
+
else
|
726
|
+
add_zone2(zone['name'], zone['xmin'], zone['ymin'], zone['zmin'], zone['xmax'], zone['ymax'], zone['zmax']) {|*args|
|
727
|
+
block.call *args if block != nil
|
728
|
+
}
|
729
|
+
end
|
730
|
+
end
|
731
|
+
|
732
|
+
# Adds a new zone. Calls block with true and a message for success,
|
733
|
+
# false and a message for error.
|
734
|
+
def add_zone2 name, xmin, ymin, zmin, xmax, ymax, zmax, &block
|
735
|
+
cmd = EMKndCommand.new 'addzone', name, xmin, ymin, zmin, xmax, ymax, zmax
|
736
|
+
|
737
|
+
if block != nil
|
738
|
+
cmd.callback { |cmd|
|
739
|
+
block.call true, cmd.message
|
740
|
+
}
|
741
|
+
cmd.errback { |cmd|
|
742
|
+
block.call false, (cmd ? cmd.message : 'timeout')
|
743
|
+
}
|
744
|
+
end
|
745
|
+
|
746
|
+
do_command cmd
|
747
|
+
end
|
748
|
+
|
749
|
+
def remove_zone name, &block
|
750
|
+
cmd = EMKndCommand.new 'rmzone', name
|
751
|
+
|
752
|
+
if block != nil
|
753
|
+
cmd.callback { |cmd|
|
754
|
+
block.call true, cmd.message
|
755
|
+
}
|
756
|
+
cmd.errback { |cmd|
|
757
|
+
block.call false, (cmd ? cmd.message : 'timeout')
|
758
|
+
}
|
759
|
+
end
|
760
|
+
|
761
|
+
do_command cmd
|
762
|
+
end
|
763
|
+
|
764
|
+
def clear_zones &block
|
765
|
+
cmd = EMKndCommand.new 'clear'
|
766
|
+
|
767
|
+
if block != nil
|
768
|
+
cmd.callback { |cmd|
|
769
|
+
@@zones.clear
|
770
|
+
block.call true, cmd.message
|
771
|
+
}
|
772
|
+
cmd.errback { |cmd|
|
773
|
+
block.call false, (cmd ? cmd.message : 'timeout')
|
774
|
+
}
|
775
|
+
end
|
776
|
+
|
777
|
+
do_command cmd
|
778
|
+
end
|
779
|
+
|
780
|
+
def request_brightness &block
|
781
|
+
return if @getbright_sent
|
782
|
+
|
783
|
+
cmd = EMKndCommand.new 'getbright'
|
784
|
+
|
785
|
+
cmd.callback { |cmd|
|
786
|
+
block.call true, cmd.message if block
|
787
|
+
@getbright_sent = false
|
788
|
+
}
|
789
|
+
cmd.errback { |cmd|
|
790
|
+
block.call false, (cmd ? cmd.message : 'timeout') if block
|
791
|
+
@getbright_sent = false
|
792
|
+
}
|
793
|
+
|
794
|
+
@getbright_sent = true
|
795
|
+
do_command cmd
|
796
|
+
end
|
797
|
+
|
798
|
+
# Makes sure a getdepth or getvideo command is queued as appropriate
|
799
|
+
def request_image type
|
800
|
+
if type == :video
|
801
|
+
do_command 'getvideo' unless @video_sent
|
802
|
+
@video_sent = true
|
803
|
+
else
|
804
|
+
do_command 'getdepth' unless @depth_sent
|
805
|
+
@depth_sent = true
|
806
|
+
end
|
807
|
+
end
|
808
|
+
private :request_image
|
809
|
+
|
810
|
+
# Returns true if there are requests pending of the given type
|
811
|
+
def check_requests type
|
812
|
+
ret = nil
|
813
|
+
EMKndClient.bench("check_requests #{type}") do
|
814
|
+
@image_lock.synchronize do
|
815
|
+
ret = !@requests[type].empty?
|
816
|
+
end
|
817
|
+
end
|
818
|
+
return ret
|
819
|
+
end
|
820
|
+
|
821
|
+
# Sets the PNG data for the given type of image
|
822
|
+
def set_image type, pngdata
|
823
|
+
@image_lock.synchronize do
|
824
|
+
@@images[type] = pngdata
|
825
|
+
reqs = @requests[type].clone
|
826
|
+
EM.next_tick do
|
827
|
+
reqs.each do |v|
|
828
|
+
v.call pngdata
|
829
|
+
end
|
830
|
+
end
|
831
|
+
@requests[type].clear
|
832
|
+
end
|
833
|
+
end
|
834
|
+
private :set_image
|
835
|
+
|
836
|
+
# The parameter is the type of image to request (:depth, :linear, :ovh,
|
837
|
+
# :side, :front, or :video). The given block will be called with a
|
838
|
+
# string containing the corresponding PNG data, or an empty string.
|
839
|
+
def get_image type, &block
|
840
|
+
raise "Invalid image type: #{type}" if @requests[type] == nil
|
841
|
+
|
842
|
+
@image_lock.synchronize do
|
843
|
+
if @requests[type].empty?
|
844
|
+
request_image type
|
845
|
+
end
|
846
|
+
if block_given?
|
847
|
+
@requests[type].push block
|
848
|
+
else
|
849
|
+
@requests[type].push proc { }
|
850
|
+
end
|
851
|
+
|
852
|
+
length = @requests[type].length
|
853
|
+
log "There are now #{length} #{type} requests." if @@bencher && length > 1
|
854
|
+
end
|
855
|
+
end
|
856
|
+
|
857
|
+
# Adds a callback to be called when a zone is added, removed, or
|
858
|
+
# changed, the framerate changes, or the system disconnects.
|
859
|
+
#
|
860
|
+
# For zone updates, the given block/proc/lambda will be called with the
|
861
|
+
# type of operation (:add, :del, :change) and the Zone object.
|
862
|
+
#
|
863
|
+
# For status updates, the block/proc/lambda will be called with the
|
864
|
+
# status value being updated (:online, :fps) the value (true/false
|
865
|
+
# for :online, 0-30 for :fps), and for :online, the number of seconds
|
866
|
+
# since the last online/offline transition.
|
867
|
+
#
|
868
|
+
# The block will be called with :fps and :add events as soon as it is
|
869
|
+
# added, and an :online event if already online.
|
870
|
+
def add_cb block
|
871
|
+
raise 'Parameter must be callable' unless block.respond_to? :call
|
872
|
+
unless @cbs.include? block
|
873
|
+
@cbs << block
|
874
|
+
block.call :online, @@connected, (Time.now - @@connection_time) if @@fps > 0
|
875
|
+
block.call :fps, @@fps
|
876
|
+
@@zones.each do |k, v|
|
877
|
+
block.call :add, v
|
878
|
+
end
|
879
|
+
end
|
880
|
+
end
|
881
|
+
|
882
|
+
# Removes a zone callback previously added with add_zone_cb. The block
|
883
|
+
# will be called with :online, false when it is removed.
|
884
|
+
def remove_cb block
|
885
|
+
raise 'Parameter must be callable' unless block.respond_to? :call
|
886
|
+
@cbs.delete block
|
887
|
+
block.call :online, false, (Time.now - @@connection_time)
|
888
|
+
end
|
889
|
+
|
890
|
+
# Calls each callback with the given arguments.
|
891
|
+
def call_cbs *args
|
892
|
+
@cbs.each do |cb|
|
893
|
+
cb.call *args
|
894
|
+
end
|
895
|
+
end
|
896
|
+
private :call_cbs
|
897
|
+
|
898
|
+
def log(msg)
|
899
|
+
EMKndClient.log(msg)
|
900
|
+
end
|
901
|
+
private :log
|
902
|
+
end
|
903
|
+
end
|
904
|
+
end
|