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.
@@ -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