aca-device-modules 1.0.4 → 1.0.5

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