aca-device-modules 1.0.3 → 1.0.4

Sign up to get free protection for your applications and to get access to all the features.
@@ -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
+