aca-device-modules 1.0.4 → 1.0.5

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