aca-device-modules 1.0.3 → 1.0.4

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.
@@ -0,0 +1,648 @@
1
+ module Nec; end
2
+ module Nec::Projector; end
3
+
4
+
5
+ # :title:All NEC Control Module (default port - )
6
+ #
7
+ # Controls all NEC projectors as of 9/01/2011
8
+ # Status information avaliable:
9
+ # -----------------------------
10
+ #
11
+ # (built in)
12
+ # connected
13
+ #
14
+ # (module defined)
15
+ # error (array of strings)
16
+ #
17
+ # lamp_status
18
+ # lamp_target
19
+ # lamp_warming
20
+ # lamp_cooling
21
+ # lamp_usage (array of integers representing hours)
22
+ # filter_usage
23
+ #
24
+ # volume
25
+ # volume_min == 0
26
+ # volume_max == 63
27
+ #
28
+ # zoom
29
+ # zoom_min
30
+ # zoom_max
31
+ #
32
+ # mute (picture and audio)
33
+ # picture_mute
34
+ # audio_mute
35
+ # onscreen_mute
36
+ # picture_freeze
37
+ #
38
+ # target_input
39
+ # input_selected
40
+ #
41
+ # model_name
42
+ # model_series
43
+ #
44
+ #
45
+
46
+
47
+ class Nec::Projector::NpSeries
48
+ include ::Orchestrator::Constants
49
+ include ::Orchestrator::Transcoder
50
+
51
+ def on_unload
52
+ end
53
+
54
+ def on_update
55
+ self[:power_stable] = true
56
+ self[:input_stable] = true
57
+ end
58
+
59
+
60
+
61
+ #
62
+ # Sets up any constants
63
+ #
64
+ def on_load
65
+
66
+ #
67
+ # Setup constants
68
+ #
69
+ self[:volume_min] = 0
70
+ self[:volume_max] = 63
71
+ self[:lamp_usage] = []
72
+ self[:filter_usage] = []
73
+ self[:error] = []
74
+
75
+ self[:power_stable] = true
76
+ self[:input_stable] = true
77
+ end
78
+
79
+ #
80
+ # Connect and request projector status
81
+ # NOTE:: Only connected and disconnected are threadsafe
82
+ # Access of other variables should be protected outside of these functions
83
+ #
84
+ def connected
85
+ #
86
+ # Get current state of the projector
87
+ #
88
+ do_poll
89
+
90
+ #
91
+ # Get the state every 50 seconds :)
92
+ #
93
+ @polling_timer = schedule.every('50s') do
94
+ do_poll
95
+ end
96
+ end
97
+
98
+ def disconnected
99
+ #
100
+ # Perform any cleanup functions here
101
+ #
102
+ @polling_timer.cancel unless @polling_timer.nil?
103
+ @polling_timer = nil
104
+ end
105
+
106
+
107
+ #
108
+ # Command Listing
109
+ # Second byte used to detect command type
110
+ #
111
+ COMMAND = {
112
+ # Mute controls
113
+ :mute_picture => "$02,$10,$00,$00,$00,$12",
114
+ :unmute_picture => "$02,$11,$00,$00,$00,$13",
115
+ :mute_audio => "02H 12H 00H 00H 00H 14H",
116
+ :unmute_audio => "02H 13H 00H 00H 00H 15H",
117
+ :mute_onscreen => "02H 14H 00H 00H 00H 16H",
118
+ :unmute_onscreen => "02H 15H 00H 00H 00H 17H",
119
+
120
+ :freeze_picture => "$01,$98,$00,$00,$01,$01,$9B",
121
+ :unfreeze_picture =>"$01,$98,$00,$00,$01,$02,$9C",
122
+
123
+ :status_lamp => "00H 81H 00H 00H 00H 81H", # Running sense (ret 81)
124
+ :status_input => "$00,$85,$00,$00,$01,$02,$88", # Input status (ret 85)
125
+ :status_mute => "00H 85H 00H 00H 01H 03H 89H", # MUTE STATUS REQUEST (Check 10H on byte 5)
126
+ :status_error => "00H 88H 00H 00H 00H 88H", # ERROR STATUS REQUEST (ret 88)
127
+ :status_model => "00H 85H 00H 00H 01H 04H 8A", # request model name (both of these are related)
128
+
129
+ # lamp hours / remaining information
130
+ :lamp_information => "03H 8AH 00H 00H 00H 8DH", # LAMP INFORMATION REQUEST
131
+ :filter_information => "03H 8AH 00H 00H 00H 8DH",
132
+ :projector_information => "03H 8AH 00H 00H 00H 8DH",
133
+
134
+ :background_black =>"$03,$B1,$00,$00,$02,$0B,$01,$C2", # set mute to be a black screen
135
+ :background_blue => "$03,$B1,$00,$00,$02,$0B,$00,$C1", # set mute to be a blue screen
136
+ :background_logo => "$03,$B1,$00,$00,$02,$0B,$02,$C3" # set mute to be the company logo
137
+ }
138
+
139
+
140
+ #
141
+ # Automatically creates a callable function for each command
142
+ # http://blog.jayfields.com/2007/10/ruby-defining-class-methods.html
143
+ # http://blog.jayfields.com/2008/02/ruby-dynamically-define-method.html
144
+ #
145
+ COMMAND.each_key do |command|
146
+ define_method command do
147
+ send(COMMAND[command], :hex_string => true, :name => command)
148
+ end
149
+ end
150
+
151
+
152
+ #
153
+ # Volume Modification
154
+ #
155
+ def volume(vol)
156
+ # volume base command D1 D2 D3 D4 D5 + CKS
157
+ command = [0x03, 0x10, 0x00, 0x00, 0x05, 0x05, 0x00, 0x00, 0x00, 0x00]
158
+ # D3 = 00 (absolute vol) or 01 (relative vol)
159
+ # D4 = value (lower bits 0 to 63)
160
+ # D5 = value (higher bits always 00h)
161
+
162
+ vol = 63 if vol > 63
163
+ vol = 0 if vol < 0
164
+ command[-2] = vol
165
+
166
+ self[:volume] = vol
167
+
168
+ send_checksum(command)
169
+ end
170
+
171
+ #
172
+ # Mutes everything
173
+ #
174
+ def mute(state = true)
175
+ if state
176
+ mute_picture
177
+ mute_onscreen
178
+ else
179
+ unmute
180
+ end
181
+ end
182
+
183
+ #
184
+ # unmutes everything desirable
185
+ #
186
+ def unmute
187
+ unmute_picture
188
+ end
189
+
190
+ #
191
+ # Sets the lamp power value
192
+ #
193
+ def power(power)
194
+ #:lamp_on => "$02,$00,$00,$00,$00,$02",
195
+ #:lamp_off => "$02,$01,$00,$00,$00,$03",
196
+ self[:power_stable] = false
197
+
198
+ command = [0x02, 0x00, 0x00, 0x00, 0x00, 0x02]
199
+ if is_affirmative?(power)
200
+ command[1] += 1 # power off
201
+ command[-1] += 1 # checksum
202
+ self[:power_target] = Off
203
+ else
204
+ self[:power_target] = On
205
+ end
206
+
207
+ send(command, :name => :power)
208
+ end
209
+
210
+ def power?(options = {}, &block)
211
+ options[:emit] = block if block_given?
212
+ options[:hex_string] = true
213
+ send(COMMAND[:status_lamp], options)
214
+ end
215
+
216
+
217
+ INPUTS = {
218
+ :vga1 => 0x01,
219
+ :vga => 0x01,
220
+ :rgbhv => 0x02, # \
221
+ :dvi_a => 0x02, # } - all of these are the same
222
+ :vga2 => 0x02, # /
223
+
224
+ :composite => 0x06,
225
+ :svideo => 0x0B,
226
+
227
+ :component1 => 0x10,
228
+ :component => 0x10,
229
+ :component2 => 0x11,
230
+
231
+ :hdmi => 0x1A, # \
232
+ :dvi => 0x1A, # | - These are the same
233
+ :hdmi2 => 0x1B,
234
+
235
+ :lan => 0x20,
236
+ :viewer => 0x1F
237
+ }
238
+ def switch_to(input)
239
+ input = input.to_sym if input.class == String
240
+
241
+ #
242
+ # Input status update
243
+ # As much for internal use as external
244
+ # and with the added benefit of being thread safe
245
+ #
246
+ self[:target_input] = input # should do this for power on and off (ensures correct state)
247
+ self[:input_stable] = false
248
+
249
+ command = [0x02, 0x03, 0x00, 0x00, 0x02, 0x01]
250
+ command << INPUTS[input]
251
+ send_checksum(command, :name => :input)
252
+ end
253
+
254
+
255
+ #
256
+ # Return true if command success, nil if still waiting, false if fail
257
+ #
258
+ def received(data, resolve, command)
259
+ response = data
260
+ data = str_to_array(data)
261
+ command[:data] = str_to_array(command[:data]) unless command[:data].nil?
262
+
263
+ logger.info "NEC projector sent: 0x#{byte_to_hex(response)}"
264
+
265
+ #
266
+ # Command failed
267
+ #
268
+ if data[0] & 0xA0 == 0xA0
269
+ #
270
+ # We were changing power state at time of failure we should keep trying
271
+ #
272
+ if [0x00, 0x01].include?(command[:data][1])
273
+ command[:delay_on_receive] = 6
274
+ power?
275
+ return true
276
+ end
277
+ logger.info "-- NEC projector, sent fail code for command: 0x#{byte_to_hex(command[:data])}"
278
+ logger.info "-- NEC projector, response was: 0x#{byte_to_hex(response)}"
279
+ return false
280
+ end
281
+
282
+ #
283
+ # Check checksum
284
+ #
285
+ if !check_checksum(data)
286
+ logger.debug "-- NEC projector, checksum failed for command: 0x#{byte_to_hex(command[:data])}"
287
+ return false
288
+ end
289
+
290
+ #
291
+ # Process a successful command
292
+ # add 0x20 to the first byte of the send command
293
+ # Then match the second byte to the second byte of the send command
294
+ #
295
+ case data[0]
296
+ when 0x20
297
+ case data[1]
298
+ when 0x81
299
+ process_power_status(data, command)
300
+ return true
301
+ when 0x88
302
+ process_error_status(data, command)
303
+ return true
304
+ when 0x85
305
+ case command[:data][-2]
306
+ when 0x02
307
+ process_input_state(data, command)
308
+ return true
309
+ when 0x03
310
+ process_mute_state(data, command)
311
+ return true
312
+ end
313
+ end
314
+ when 0x22
315
+ case data[1]
316
+ when 0x03
317
+ return process_input_switch(data, command)
318
+ when 0x00, 0x01
319
+ process_lamp_command(data, command)
320
+ return true
321
+ when 0x10, 0x11, 0x12, 0x13, 0x14, 0x15
322
+ status_mute # update mute status's (dry)
323
+ return true
324
+ end
325
+ when 0x23
326
+ case data[1]
327
+ when 0x10
328
+ #
329
+ # Picture, Volume, Keystone, Image adjust mode
330
+ # how to play this?
331
+ #
332
+ # TODO:: process volume control
333
+ #
334
+ return true
335
+ when 0x8A
336
+ process_projector_information(data, command)
337
+ return true
338
+ end
339
+ end
340
+
341
+ logger.warn "-- NEC projector, no status updates defined for response: #{byte_to_hex(response)}"
342
+ logger.warn "-- NEC projector, command was: 0x#{byte_to_hex(command[:data])}"
343
+ return true # to prevent retries on commands we were not expecting
344
+ end
345
+
346
+
347
+ private # All response handling functions should be private so they cannot be called from the outside world
348
+
349
+
350
+ #
351
+ # The polling routine for the projector
352
+ #
353
+ def do_poll
354
+ power?({:priority => 0})
355
+ #status_input
356
+ #projector_information
357
+ #status_error
358
+ end
359
+
360
+
361
+ #
362
+ # Process the lamp on/off command response
363
+ #
364
+ def process_lamp_command(data, command)
365
+ logger.debug "-- NEC projector sent a response to a power command"
366
+
367
+ #
368
+ # Ensure a change of power state was the last command sent
369
+ #
370
+ #self[:power] = data[1] == 0x00
371
+ if command.present?
372
+ last = command[:data]
373
+ if [0x00, 0x01].include?(last[1])
374
+ power? # Queues the status power command
375
+ end
376
+ end
377
+ end
378
+
379
+ #
380
+ # Process the lamp status response
381
+ # Intimately entwinded with the power power command
382
+ # (as we need to control ensure we are in the correct target state)
383
+ #
384
+ def process_power_status(data, command)
385
+ logger.debug "-- NEC projector sent a response to a power status command"
386
+
387
+ self[:power] = (data[-2] & 0b10) > 0x0 # Power on?
388
+
389
+ if (data[-2] & 0b100000) > 0 || (data[-2] & 0b10000000) > 0
390
+ # Projector cooling || power on off processing
391
+
392
+ if self[:power_target] == On
393
+ self[:cooling] = false
394
+ self[:warming] = true
395
+
396
+ logger.debug "power warming..."
397
+
398
+
399
+ elsif self[:power_target] == Off
400
+ self[:warming] = false
401
+ self[:cooling] = true
402
+
403
+ logger.debug "power cooling..."
404
+ end
405
+
406
+
407
+ command[:delay_on_receive] = 4
408
+ power? # Then re-queue this command
409
+
410
+
411
+ # Signal processing
412
+ elsif (data[-2] & 0b1000000) > 0
413
+ command[:delay_on_receive] = 3
414
+ power? # Then re-queue this command
415
+ else
416
+ #
417
+ # We are in a stable state!
418
+ #
419
+ if (self[:power] != self[:power_target]) && !self[:power_stable]
420
+ if self[:power_target].nil?
421
+ self[:power_target] = self[:power] # setup initial state if the control system is just coming online
422
+ self[:power_stable] = true
423
+ else
424
+ #
425
+ # if we are in an undesirable state then correct it
426
+ #
427
+ logger.debug "NEC projector in an undesirable power state... (Correcting)"
428
+ power(self[:power_target])
429
+
430
+ #
431
+ # ensures lamp targets are set in case of disconnect
432
+ #
433
+ command[:delay_on_receive] = 15
434
+ end
435
+ else
436
+ logger.debug "NEC projector is in a good power state..."
437
+
438
+ self[:warming] = false
439
+ self[:cooling] = false
440
+ self[:power_stable] = true
441
+
442
+ #
443
+ # Ensure the input is in the correct state unless the lamp is off
444
+ #
445
+ status_input unless self[:power] == Off # calls status mute
446
+ end
447
+ end
448
+ end
449
+
450
+
451
+ #
452
+ # NEC has different values for the input status when compared to input selection
453
+ #
454
+ INPUT_MAP = {
455
+ 0x01 => {
456
+ 0x01 => [:vga, :vga1],
457
+ 0x02 => [:composite],
458
+ 0x03 => [:svideo],
459
+ 0x06 => [:hdmi, :dvi],
460
+ 0x07 => [:viewer]
461
+ },
462
+ 0x02 => {
463
+ 0x01 => [:vga2, :dvi_a, :rgbhv],
464
+ 0x04 => [:component2],
465
+ 0x06 => [:hdmi2],
466
+ 0x07 => [:lan]
467
+ },
468
+ 0x03 => {
469
+ 0x04 => [:component, :component1]
470
+ }
471
+ }
472
+ def process_input_state(data, command)
473
+ logger.debug "-- NEC projector sent a response to an input state command"
474
+
475
+
476
+ return if self[:power] == Off # no point doing anything here if the projector is off
477
+
478
+ self[:input_selected] = INPUT_MAP[data[-15]][data[-14]]
479
+ self[:input] = self[:input_selected].nil? ? :unknown : self[:input_selected][0]
480
+ if data[-17] == 0x01
481
+ command[:delay_on_receive] = 3 # still processing signal
482
+ status_input
483
+ else
484
+ status_mute # get mute status one signal has settled
485
+ end
486
+
487
+ #
488
+ # Notify of bad input selection for debugging
489
+ # We ensure at the very least power state and input are always correct
490
+ #
491
+ if !self[:input_selected].include?(self[:target_input]) && !self[:input_stable]
492
+ if self[:target_input].nil?
493
+ self[:target_input] = self[:input_selected][0]
494
+ self[:input_stable] = true
495
+ else
496
+ switch_to(self[:target_input])
497
+ logger.debug "-- NEC input state may not be correct, desired: #{self[:target_input]} current: #{self[:input_selected]}"
498
+ end
499
+ else
500
+ self[:input_stable] = true
501
+ end
502
+ end
503
+
504
+
505
+ #
506
+ # Check the input switching command was successful
507
+ #
508
+ def process_input_switch(data, command)
509
+ logger.debug "-- NEC projector responded to switch input command"
510
+
511
+ if data[-2] != 0xFF
512
+ status_input # Double check with a status update
513
+ return true
514
+ end
515
+
516
+ logger.debug "-- NEC projector failed to switch input with command: #{byte_to_hex(command[:data])}"
517
+ return false # retry the command
518
+ end
519
+
520
+
521
+ #
522
+ # Process the mute state response
523
+ #
524
+ def process_mute_state(data, command)
525
+ logger.debug "-- NEC projector responded to mute state command"
526
+
527
+ self[:picture_mute] = data[-17] == 0x01
528
+ self[:audio_mute] = data[-16] == 0x01
529
+ self[:onscreen_mute] = data[-15] == 0x01
530
+
531
+ #if !self[:onscreen_mute] && self[:power]
532
+ #
533
+ # Always mute onscreen
534
+ #
535
+ # mute_onscreen
536
+ #end
537
+
538
+ self[:mute] = data[-17] == 0x01 # Same as picture mute
539
+ end
540
+
541
+
542
+ #
543
+ # Process projector information response
544
+ # lamp1 hours + filter hours
545
+ #
546
+ def process_projector_information(data, command)
547
+ logger.debug "-- NEC projector sent a response to a projector information command"
548
+
549
+ lamp = 0
550
+ filter = 0
551
+
552
+ #
553
+ # get lamp usage
554
+ #
555
+ shift = 0
556
+ data[87..90].each do |byte|
557
+ lamp += byte << shift
558
+ shift += 8
559
+ end
560
+
561
+ #
562
+ # get filter usage
563
+ #
564
+ shift = 0
565
+ data[91..94].each do |byte|
566
+ filter += byte << shift
567
+ shift += 8
568
+ end
569
+
570
+ self[:lamp_usage] = [lamp / 3600] # Lamp usage in hours
571
+ self[:filter_usage] = [filter / 3600]
572
+ end
573
+
574
+
575
+ #
576
+ # provide all the error information required
577
+ #
578
+ ERROR_CODES = [{
579
+ 0b1 => "Lamp cover error",
580
+ 0b10 => "Temperature error (Bimetal)",
581
+ #0b100 == not used
582
+ 0b1000 => "Fan Error",
583
+ 0b10000 => "Fan Error",
584
+ 0b100000 => "Power Error",
585
+ 0b1000000 => "Lamp Error",
586
+ 0b10000000 => "Lamp has reached its end of life"
587
+ }, {
588
+ 0b1 => "Lamp has been used beyond its limit",
589
+ 0b10 => "Formatter error",
590
+ 0b100 => "Lamp no.2 Error"
591
+ }, {
592
+ #0b1 => "not used",
593
+ 0b10 => "FPGA error",
594
+ 0b100 => "Temperature error (Sensor)",
595
+ 0b1000 => "Lamp housing error",
596
+ 0b10000 => "Lamp data error",
597
+ 0b100000 => "Mirror cover error",
598
+ 0b1000000 => "Lamp no.2 has reached its end of life",
599
+ 0b10000000 => "Lamp no.2 has been used beyond its limit"
600
+ }, {
601
+ 0b1 => "Lamp no.2 housing error",
602
+ 0b10 => "Lamp no.2 data error",
603
+ 0b100 => "High temperature due to dust pile-up",
604
+ 0b1000 => "A foreign object sensor error"
605
+ }]
606
+ def process_error_status(data, command)
607
+ logger.debug "-- NEC projector sent a response to an error status command"
608
+
609
+ errors = []
610
+ error = data[5..8]
611
+ error.each_index do |byte_no|
612
+ if error[byte_no] > 0 # run throught each byte
613
+ ERROR_CODES[byte_no].each_key do |key| # if error indicated run though each key
614
+ if (key & error[byte_no]) > 0 # check individual bits
615
+ errors << ERROR_CODES[byte_no][key] # add errors to the error list
616
+ end
617
+ end
618
+ end
619
+ end
620
+ self[:error] = errors
621
+ end
622
+
623
+
624
+ #
625
+ # For commands that require a checksum (volume, zoom)
626
+ #
627
+ def send_checksum(command, options = {})
628
+ #
629
+ # Prepare command for sending
630
+ #
631
+ command = str_to_array(hex_to_byte(command)) unless command.class == Array
632
+ check = 0
633
+ command.each do |byte| # Loop through the first to second last element
634
+ check = (check + byte) & 0xFF
635
+ end
636
+ command << check
637
+ send(command, options)
638
+ end
639
+
640
+ def check_checksum(data)
641
+ check = 0
642
+ data[0..-2].each do |byte| # Loop through the first to second last element
643
+ check = (check + byte) & 0xFF
644
+ end
645
+ return check == data[-1] # Check the check sum equals the last element
646
+ end
647
+ end
648
+