nl-knd_client 0.0.0.pre.usegit
Sign up to get free protection for your applications and to get access to all the features.
- 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
|