From 6737cfa0a47f80ac933962575dccc2a4513a6af4 Mon Sep 17 00:00:00 2001 From: Philip Kheav Date: Fri, 27 Nov 2020 11:44:03 +0000 Subject: [PATCH 01/27] feat(nec_proj): initial copy from display driver --- drivers/nec/np_series.cr | 281 ++++++++++++++++++++++++++++++++++ drivers/nec/np_series_spec.cr | 70 +++++++++ 2 files changed, 351 insertions(+) create mode 100644 drivers/nec/np_series.cr create mode 100644 drivers/nec/np_series_spec.cr diff --git a/drivers/nec/np_series.cr b/drivers/nec/np_series.cr new file mode 100644 index 00000000000..db900ed2192 --- /dev/null +++ b/drivers/nec/np_series.cr @@ -0,0 +1,281 @@ +require "placeos-driver/interface/powerable" +require "placeos-driver/interface/muteable" +require "placeos-driver/interface/switchable" + +class Nec::NpSeries < PlaceOS::Driver + include Interface::Powerable + include Interface::AudioMuteable + + enum Input + Vga = 1 + Rgbhv = 2 + Dvi = 3 + HdmiSet = 4 + Video1 = 5 + Video2 = 6 + Svideo = 7 + Tuner = 9 + Tv = 10 + Dvd1 = 12 + Option = 13 + Dvd2 = 14 + DisplayPort = 15 + Hdmi = 17 + Hdmi2 = 18 + Hdmi3 = 130 + Usb = 135 + end + include PlaceOS::Driver::Interface::InputSelection(Input) + + # Discovery Information + tcp_port 7142 + descriptive_name "NEC Projector" + generic_name :Display + + DELIMITER = 0x0D_u8 + + def on_load + # Communication settings + queue.delay = 100.milliseconds + transport.tokenizer = Tokenizer.new(Bytes[DELIMITER]) + end + + def connected + schedule.every(50.seconds, true) do + do_poll + end + end + + def disconnected + schedule.clear + end + + def power(state : Bool) + # Do nothing if already in desired state + return if self[:power]? == state + + if state + logger.debug { "-- NEC LCD, requested to power on" } + # 1 = Power On + data = MsgType::Command.build(Command::SetPower, 1) + send(data, name: "power", delay: 5.seconds) + else + logger.debug { "-- NEC LCD, requested to power off" } + # 4 = Power Off + data = MsgType::Command.build(Command::SetPower, 4) + send(data, name: "power", delay: 10.seconds, timeout: 10.seconds) + end + end + + def power?(**options) : Bool + data = MsgType::Command.build(Command::PowerQuery) + send(data, **options, name: "power?").get + self[:power].as_bool + end + + def switch_to(input : Input) + logger.debug { "-- NEC LCD, requested to switch to: #{input}" } + data = MsgType::SetParameter.build(Command::VideoInput, input.value) + send(data, name: "input", delay: 6.seconds) + end + + enum Audio + Audio1 = 1 + Audio2 = 2 + Audio3 = 3 + Hdmi = 4 + Tv = 6 + DisplayPort = 7 + end + + def switch_audio(input : Audio) + logger.debug { "-- NEC LCD, requested to switch audio to: #{input}" } + data = MsgType::SetParameter.build(Command::AudioInput, input.value) + send(data, name: "audio") + end + + def auto_adjust + data = MsgType::SetParameter.build(Command::AutoSetup, 1) + send(data, name: "auto_adjust") + end + + def brightness(val : Int32) + data = MsgType::SetParameter.build(Command::BrightnessStatus, val.clamp(0, 100)) + send(data, name: "brightness") + send(MsgType::Command.build(Command::Save), name: "save", priority: 0) + end + + def contrast(val : Int32) + data = MsgType::SetParameter.build(Command::ContrastStatus, val.clamp(0, 100)) + send(data, name: "contrast") + send(MsgType::Command.build(Command::Save), name: "save", priority: 0) + end + + def volume(val : Int32) + data = MsgType::SetParameter.build(Command::VolumeStatus, val.clamp(0, 100)) + send(data, name: "volume") + send(MsgType::Command.build(Command::Save), name: "save", priority: 0) + end + + def mute_audio(state : Bool = true, index : Int32 | String = 0) + logger.debug { "requested to update mute to #{state}" } + data = MsgType::SetParameter.build(Command::MuteStatus, state ? 1 : 0) + send(data, name: "mute_audio") + end + + def do_poll + current_power = power?(priority: 0) + logger.debug { "Polling, power = #{current_power}" } + + if current_power + mute_status + volume_status + video_input + audio_input + end + end + + # LCD Response code + def received(data, task) + task_name = task.try &.name + logger.debug { "task is #{task_name}" } + + ascii_string = String.new(data) + # Check for valid response + if !check_checksum(data) + return task.try &.retry("-- NEC LCD, invalid response was: #{ascii_string}") + end + + logger.debug { "NEC LCD responded with ascii_string #{ascii_string}" } + + if ascii_string[8..9] == "00" + parse_response(ascii_string) + elsif ascii_string[10..13] == "00D6" # Annoyingly unique case to deal with power status query + self[:power] = ascii_string[23] == '1' + elsif ascii_string[8..9] == "BE" # Wait response + return task.try &.retry("-- NEC LCD, response was a wait command") + else + return task.try &.abort("-- NEC LCD, command failed: #{task_name}\n-- NEC LCD, response was: #{ascii_string}") + end + + task.try &.success + end + + private def parse_response(data : String) + logger.debug { "data is #{data}" } + + if data.size >= 15 + command = ( + # For most commands + Command.from_value?(data[10..13].to_i(16)) || \ + # For Command::SetPower + Command.from_value?(data[10..15].to_i(16)) + ).not_nil! + else # This is a short command and is most likely Command::Save + # This line will error if this is not Command::Save which is fine + command = Command.from_value(data[9..10].to_i(16)) + # Don't do any processing for Command::Save + return if command.save? + end + value = (command.set_power? ? data[16..19] : data[20..23]).to_i(16) + + case command + when .video_input? + self[:input] = Input.from_value(value) + when .audio_input? + self[:audio] = Audio.from_value(value) + when .volume_status? + self[:volume] = value + self[:audio_mute] = value == 0 + when .brightness_status? + self[:brightness] = value + when .contrast_status? + self[:contrast] = value + when .mute_status? + self[:audio_mute] = value == 1 + self[:volume] = 0 if value == 1 + when .auto_setup? + # auto_setup + # nothing needed to do here (we are delaying the next command by 4 seconds) + when .set_power? + self[:power] = value == 1 + else + logger.debug { "-- NEC LCD, unknown response received: #{data}" } + end + end + + enum Command + VideoInput = 0x0060 + AudioInput = 0x022E + VolumeStatus = 0x0062 + MuteStatus = 0x008D + PowerOnDelay = 0x02D8 + ContrastStatus = 0x0012 + BrightnessStatus = 0x0010 + AutoSetup = 0x001E + PowerQuery = 0x01D6 + Save = 0x0C + SetPower = 0xC203D6 + + def to_s : String + case self + when .save? + length = 2 + when .set_power? + length = 6 + else + length = 4 + end + value.to_s(16, true).rjust(length, '0') + end + end + + {% for name in Command.constants %} + @[Security(Level::Administrator)] + def {{name.id.underscore}}(priority : Int32 = 0) + send(MsgType::GetParameter.build(Command::{{name.id}}), priority: priority, name: {{name.id.underscore.stringify}}) + end + {% end %} + + private def check_checksum(data : Bytes) + # Loop through the second to the third last element + checksum = data[1..-3].reduce { |a, b| a ^ b } + # Check the checksum equals the second last element + logger.debug { "Error: checksum should be 0x#{checksum.to_s(16)}" } unless checksum == data[-2] + checksum == data[-2] + end + + # Types of messages sent to and from the LCD + enum MsgType : UInt8 + Command = 0x41 # 'A' + CommandReply = 0x42 # 'B' + GetParameter = 0x43 # 'C' + GetParameterReply = 0x44 # 'D' + SetParameter = 0x45 # 'E' + SetParameterReply = 0x46 # 'F' + + def build(command : Nec::NpSeries::Command, data : Int? = nil) + command = command.to_s + + message = String.build do |str| + str << "0*0" + str.write_byte self.value # Type + + message_length = command.size + 2 + message_length += 4 if d = data # If there is data, add 4 to the message length + str << message_length.to_s(16, true).rjust(2, '0') # Message length + str.write_byte 0x02 # Start of messsage + str << command # Message + str << d.to_s(16, true).rjust(4, '0') if d # Data if required + str.write_byte 0x03 # End of message + end + + String.build do |str| + str.write_byte 0x01 # SOH + str << message # Message + str.write_byte message.each_byte.reduce { |a, b| a ^ b } # Checksum + str.write_byte DELIMITER # Delimiter + end + end + end +end \ No newline at end of file diff --git a/drivers/nec/np_series_spec.cr b/drivers/nec/np_series_spec.cr new file mode 100644 index 00000000000..b62392ec466 --- /dev/null +++ b/drivers/nec/np_series_spec.cr @@ -0,0 +1,70 @@ +DriverSpecs.mock_driver "Nec::NpSeries" do + # do_poll + # power? + should_send("\x010*0A06\x0201D6\x03\x1F\x0D") + responds("\x0100*B12\x020200D60000040001\x03\x1F\x0D") + status[:power].should eq(true) + # mute_status + should_send("\x010*0C06\x02008D\x03\x12\x0D") + responds("\x0100*D12\x0200008D0000000002\x03\x12\x0D") + status[:audio_mute].should eq(false) + # volume_status + should_send("\x010*0C06\x020062\x03\x6A\x0D") + responds("\x0100*D12\x020000620000000032\x03\x69\x0D") + status[:volume].should eq(50) + # video_input + should_send("\x010*0C06\x020060\x03\x68\x0D") + responds("\x0100*D12\x020000600000000011\x03\x6A\x0D") + status[:input].should eq("Hdmi") + # audio_input + should_send("\x010*0C06\x02022E\x03\x1B\x0D") + responds("\x0100*D12\x0200022E0000000001\x03\x18\x0D") + status[:audio].should eq("Audio1") + + exec(:mute_audio) + should_send("\x010*0E0A\x02008D0001\x03\x62\x0D") + responds("\x0100*F12\x0200008D0000000001\x03\x13\x0D") + status[:audio_mute].should eq(true) + status[:volume].should eq(0) + + exec(:unmute_audio) + should_send("\x010*0E0A\x02008D0000\x03\x63\x0D") + responds("\x0100*F12\x0200008D0000000000\x03\x12\x0D") + status[:audio_mute].should eq(false) + + exec(:volume, 25) + should_send("\x010*0E0A\x0200620019\x03\x13\x0D") + responds("\x0100*F12\x020000620000640019\x03\x60\x0D") + should_send("\x010*0A04\x020C\x03\x1D\x0D") + responds("\x0100*B06\x0200C\x03\x2C\x0D") + status[:audio_mute].should eq(false) + status[:volume].should eq(25) + + exec(:brightness_status) + should_send("\x010*0C06\x020010\x03\x6F\x0D") + responds("\x0100*D12\x020000100000000000\x03\x6D\x0D") + status[:brightness].should eq(0) + + exec(:brightness, 100) + should_send("\x010*0E0A\x0200100064\x03\x1C\x0D") + responds("\x0100*F12\x020000100000640064\x03\x6F\x0D") + should_send("\x010*0A04\x020C\x03\x1D\x0D") + responds("\x0100*B06\x0200C\x03\x2C\x0D") + status[:brightness].should eq(100) + + exec(:switch_to, "tv") + should_send("\x010*0E0A\x020060000A\x03\x68\x0D") + responds("\x0100*F12\x02000060000000000A\x03\x19\x0D") + status[:input].should eq("Tv") + + exec(:switch_audio, "audio_2") + sleep 6 # since switch_to has 6 seconds of delay + should_send("\x010*0E0A\x02022E0002\x03\x68\x0D") + responds("\x0100*F12\x0200022E0000000002\x03\x19\x0D") + status[:audio].should eq("Audio2") + + exec(:power, false) + should_send("\x010*0A0C\x02C203D60004\x03\x1D\x0D") + responds("\x0100*B0E\x0200C203D60004\x03\x18\x0D") + status[:power].should eq(false) +end From 50efd17e3fc2732a977bc2f74ff35d67ef243539 Mon Sep 17 00:00:00 2001 From: Philip Kheav Date: Fri, 27 Nov 2020 11:57:16 +0000 Subject: [PATCH 02/27] feat(nec_proj): add command hash --- drivers/nec/np_series.cr | 52 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 52 insertions(+) diff --git a/drivers/nec/np_series.cr b/drivers/nec/np_series.cr index db900ed2192..e17d775e428 100644 --- a/drivers/nec/np_series.cr +++ b/drivers/nec/np_series.cr @@ -32,12 +32,31 @@ class Nec::NpSeries < PlaceOS::Driver descriptive_name "NEC Projector" generic_name :Display + default_settings({ + volume_min: 0, + volume_max: 63, + }) + + @power_target : Bool? = nil + @input_target : Input? = nil + @volume_min : Int32 = 0 + @volume_max : Int32 = 63 + DELIMITER = 0x0D_u8 def on_load # Communication settings queue.delay = 100.milliseconds transport.tokenizer = Tokenizer.new(Bytes[DELIMITER]) + self[:error] = [] of String + on_update + end + + def on_update + @power_target = nil + @input_target = nil + @volume_min = setting(Int32, :volume_min) + @volume_max = setting(Int32, :volume_max) end def connected @@ -48,8 +67,41 @@ class Nec::NpSeries < PlaceOS::Driver def disconnected schedule.clear + # Disconnect often occurs on power off + # We may have not received a status response before the disconnect occurs + self[:power] = false end + # Command Listing + # Second byte used to detect command type + COMMAND = { + # Mute controls + mute_picture: "$02,$10,$00,$00,$00,$12", + unmute_picture: "$02,$11,$00,$00,$00,$13", + mute_audio_cmd: "02H 12H 00H 00H 00H 14H", + unmute_audio: "02H 13H 00H 00H 00H 15H", + mute_onscreen: "02H 14H 00H 00H 00H 16H", + unmute_onscreen: "02H 15H 00H 00H 00H 17H", + + freeze_picture: "$01,$98,$00,$00,$01,$01,$9B", + unfreeze_picture: "$01,$98,$00,$00,$01,$02,$9C", + + status_lamp: "00H 81H 00H 00H 00H 81H", # Running sense (ret 81) + status_input: "$00,$85,$00,$00,$01,$02,$88", # Input status (ret 85) + status_mute: "00H 85H 00H 00H 01H 03H 89H", # MUTE STATUS REQUEST (Check 10H on byte 5) + status_error: "00H 88H 00H 00H 00H 88H", # ERROR STATUS REQUEST (ret 88) + status_model: "00H 85H 00H 00H 01H 04H 8A", # request model name (both of these are related) + + # lamp hours / remaining information + lamp_information: "03H 8AH 00H 00H 00H 8DH", # LAMP INFORMATION REQUEST + filter_information: "03H 8AH 00H 00H 00H 8DH", + projector_information: "03H 8AH 00H 00H 00H 8DH", + + background_black: "$03,$B1,$00,$00,$02,$0B,$01,$C2", # set mute to be a black screen + background_blue: "$03,$B1,$00,$00,$02,$0B,$00,$C1", # set mute to be a blue screen + background_logo: "$03,$B1,$00,$00,$02,$0B,$02,$C3" # set mute to be the company logo + } + def power(state : Bool) # Do nothing if already in desired state return if self[:power]? == state From aaf4fdba2afc7f9a83aa22697d2a744c6e5126b7 Mon Sep 17 00:00:00 2001 From: Philip Kheav Date: Fri, 27 Nov 2020 12:59:26 +0000 Subject: [PATCH 03/27] feat(nec_proj): add more functions --- drivers/nec/np_series.cr | 81 ++++++++++++++++------- drivers/nec/np_series_spec.cr | 120 +++++++++++++++++----------------- 2 files changed, 119 insertions(+), 82 deletions(-) diff --git a/drivers/nec/np_series.cr b/drivers/nec/np_series.cr index e17d775e428..aefcaacacac 100644 --- a/drivers/nec/np_series.cr +++ b/drivers/nec/np_series.cr @@ -4,7 +4,7 @@ require "placeos-driver/interface/switchable" class Nec::NpSeries < PlaceOS::Driver include Interface::Powerable - include Interface::AudioMuteable + include Interface::Muteable enum Input Vga = 1 @@ -76,32 +76,75 @@ class Nec::NpSeries < PlaceOS::Driver # Second byte used to detect command type COMMAND = { # Mute controls - mute_picture: "$02,$10,$00,$00,$00,$12", - unmute_picture: "$02,$11,$00,$00,$00,$13", - mute_audio_cmd: "02H 12H 00H 00H 00H 14H", - unmute_audio: "02H 13H 00H 00H 00H 15H", - mute_onscreen: "02H 14H 00H 00H 00H 16H", + mute_picture: "$02,$10,$00,$00,$00,$12", + unmute_picture: "$02,$11,$00,$00,$00,$13", + mute_audio_cmd: "02H 12H 00H 00H 00H 14H", + unmute_audio: "02H 13H 00H 00H 00H 15H", + mute_onscreen: "02H 14H 00H 00H 00H 16H", unmute_onscreen: "02H 15H 00H 00H 00H 17H", - freeze_picture: "$01,$98,$00,$00,$01,$01,$9B", + freeze_picture: "$01,$98,$00,$00,$01,$01,$9B", unfreeze_picture: "$01,$98,$00,$00,$01,$02,$9C", - status_lamp: "00H 81H 00H 00H 00H 81H", # Running sense (ret 81) + status_lamp: "00H 81H 00H 00H 00H 81H", # Running sense (ret 81) status_input: "$00,$85,$00,$00,$01,$02,$88", # Input status (ret 85) - status_mute: "00H 85H 00H 00H 01H 03H 89H", # MUTE STATUS REQUEST (Check 10H on byte 5) - status_error: "00H 88H 00H 00H 00H 88H", # ERROR STATUS REQUEST (ret 88) - status_model: "00H 85H 00H 00H 01H 04H 8A", # request model name (both of these are related) + status_mute: "00H 85H 00H 00H 01H 03H 89H", # MUTE STATUS REQUEST (Check 10H on byte 5) + status_error: "00H 88H 00H 00H 00H 88H", # ERROR STATUS REQUEST (ret 88) + status_model: "00H 85H 00H 00H 01H 04H 8A", # request model name (both of these are related) # lamp hours / remaining information - lamp_information: "03H 8AH 00H 00H 00H 8DH", # LAMP INFORMATION REQUEST - filter_information: "03H 8AH 00H 00H 00H 8DH", + lamp_information: "03H 8AH 00H 00H 00H 8DH", # LAMP INFORMATION REQUEST + filter_information: "03H 8AH 00H 00H 00H 8DH", projector_information: "03H 8AH 00H 00H 00H 8DH", background_black: "$03,$B1,$00,$00,$02,$0B,$01,$C2", # set mute to be a black screen - background_blue: "$03,$B1,$00,$00,$02,$0B,$00,$C1", # set mute to be a blue screen - background_logo: "$03,$B1,$00,$00,$02,$0B,$02,$C3" # set mute to be the company logo + background_blue: "$03,$B1,$00,$00,$02,$0B,$00,$C1", # set mute to be a blue screen + background_logo: "$03,$B1,$00,$00,$02,$0B,$02,$C3", # set mute to be the company logo } + {% for name, data in COMMAND %} + def {{name.id}}(**options) + send(COMMAND[{{name.id}}], **options, name: {{name.id.stringify}}) + end + {% end %} + + def volume(vol : Int32) + # volume base command D1 D2 D3 D4 D5 + CKS + command = [0x03, 0x10, 0x00, 0x00, 0x05, 0x05, 0x00, 0x00, 0x00, 0x00] + # D3 = 00 (absolute vol) or 01 (relative vol) + # D4 = value (lower bits 0 to 63) + # D5 = value (higher bits always 00h) + + vol = vol.clamp(@volume_min, @volume_max) + command[-2] = vol + + send_checksum(command) + self[:volume] = vol + end + + # Mutes both audio/video + def mute( + state : Bool = true, + index : Int32 | String = 0, + layer : MuteLayer = MuteLayer::AudioVideo + ) + mute_video(state) if layer.video? || layer.audio_video? + mute_audio(state) if layer.audio? || layer.audio_video? + end + + def mute_video(state : Bool) + if state + mute_picture + mute_onscreen + else + unmute_picture + end + end + + def mute_audio(state : Bool) + state ? mute_audio_cmd : unmute_audio + end + def power(state : Bool) # Do nothing if already in desired state return if self[:power]? == state @@ -169,12 +212,6 @@ class Nec::NpSeries < PlaceOS::Driver send(MsgType::Command.build(Command::Save), name: "save", priority: 0) end - def mute_audio(state : Bool = true, index : Int32 | String = 0) - logger.debug { "requested to update mute to #{state}" } - data = MsgType::SetParameter.build(Command::MuteStatus, state ? 1 : 0) - send(data, name: "mute_audio") - end - def do_poll current_power = power?(priority: 0) logger.debug { "Polling, power = #{current_power}" } @@ -330,4 +367,4 @@ class Nec::NpSeries < PlaceOS::Driver end end end -end \ No newline at end of file +end diff --git a/drivers/nec/np_series_spec.cr b/drivers/nec/np_series_spec.cr index b62392ec466..d99facad813 100644 --- a/drivers/nec/np_series_spec.cr +++ b/drivers/nec/np_series_spec.cr @@ -1,70 +1,70 @@ DriverSpecs.mock_driver "Nec::NpSeries" do - # do_poll - # power? - should_send("\x010*0A06\x0201D6\x03\x1F\x0D") - responds("\x0100*B12\x020200D60000040001\x03\x1F\x0D") - status[:power].should eq(true) - # mute_status - should_send("\x010*0C06\x02008D\x03\x12\x0D") - responds("\x0100*D12\x0200008D0000000002\x03\x12\x0D") - status[:audio_mute].should eq(false) - # volume_status - should_send("\x010*0C06\x020062\x03\x6A\x0D") - responds("\x0100*D12\x020000620000000032\x03\x69\x0D") - status[:volume].should eq(50) - # video_input - should_send("\x010*0C06\x020060\x03\x68\x0D") - responds("\x0100*D12\x020000600000000011\x03\x6A\x0D") - status[:input].should eq("Hdmi") - # audio_input - should_send("\x010*0C06\x02022E\x03\x1B\x0D") - responds("\x0100*D12\x0200022E0000000001\x03\x18\x0D") - status[:audio].should eq("Audio1") + # # do_poll + # # power? + # should_send("\x010*0A06\x0201D6\x03\x1F\x0D") + # responds("\x0100*B12\x020200D60000040001\x03\x1F\x0D") + # status[:power].should eq(true) + # # mute_status + # should_send("\x010*0C06\x02008D\x03\x12\x0D") + # responds("\x0100*D12\x0200008D0000000002\x03\x12\x0D") + # status[:audio_mute].should eq(false) + # # volume_status + # should_send("\x010*0C06\x020062\x03\x6A\x0D") + # responds("\x0100*D12\x020000620000000032\x03\x69\x0D") + # status[:volume].should eq(50) + # # video_input + # should_send("\x010*0C06\x020060\x03\x68\x0D") + # responds("\x0100*D12\x020000600000000011\x03\x6A\x0D") + # status[:input].should eq("Hdmi") + # # audio_input + # should_send("\x010*0C06\x02022E\x03\x1B\x0D") + # responds("\x0100*D12\x0200022E0000000001\x03\x18\x0D") + # status[:audio].should eq("Audio1") - exec(:mute_audio) - should_send("\x010*0E0A\x02008D0001\x03\x62\x0D") - responds("\x0100*F12\x0200008D0000000001\x03\x13\x0D") - status[:audio_mute].should eq(true) - status[:volume].should eq(0) + # exec(:mute_audio) + # should_send("\x010*0E0A\x02008D0001\x03\x62\x0D") + # responds("\x0100*F12\x0200008D0000000001\x03\x13\x0D") + # status[:audio_mute].should eq(true) + # status[:volume].should eq(0) - exec(:unmute_audio) - should_send("\x010*0E0A\x02008D0000\x03\x63\x0D") - responds("\x0100*F12\x0200008D0000000000\x03\x12\x0D") - status[:audio_mute].should eq(false) + # exec(:unmute_audio) + # should_send("\x010*0E0A\x02008D0000\x03\x63\x0D") + # responds("\x0100*F12\x0200008D0000000000\x03\x12\x0D") + # status[:audio_mute].should eq(false) - exec(:volume, 25) - should_send("\x010*0E0A\x0200620019\x03\x13\x0D") - responds("\x0100*F12\x020000620000640019\x03\x60\x0D") - should_send("\x010*0A04\x020C\x03\x1D\x0D") - responds("\x0100*B06\x0200C\x03\x2C\x0D") - status[:audio_mute].should eq(false) - status[:volume].should eq(25) + # exec(:volume, 25) + # should_send("\x010*0E0A\x0200620019\x03\x13\x0D") + # responds("\x0100*F12\x020000620000640019\x03\x60\x0D") + # should_send("\x010*0A04\x020C\x03\x1D\x0D") + # responds("\x0100*B06\x0200C\x03\x2C\x0D") + # status[:audio_mute].should eq(false) + # status[:volume].should eq(25) - exec(:brightness_status) - should_send("\x010*0C06\x020010\x03\x6F\x0D") - responds("\x0100*D12\x020000100000000000\x03\x6D\x0D") - status[:brightness].should eq(0) + # exec(:brightness_status) + # should_send("\x010*0C06\x020010\x03\x6F\x0D") + # responds("\x0100*D12\x020000100000000000\x03\x6D\x0D") + # status[:brightness].should eq(0) - exec(:brightness, 100) - should_send("\x010*0E0A\x0200100064\x03\x1C\x0D") - responds("\x0100*F12\x020000100000640064\x03\x6F\x0D") - should_send("\x010*0A04\x020C\x03\x1D\x0D") - responds("\x0100*B06\x0200C\x03\x2C\x0D") - status[:brightness].should eq(100) + # exec(:brightness, 100) + # should_send("\x010*0E0A\x0200100064\x03\x1C\x0D") + # responds("\x0100*F12\x020000100000640064\x03\x6F\x0D") + # should_send("\x010*0A04\x020C\x03\x1D\x0D") + # responds("\x0100*B06\x0200C\x03\x2C\x0D") + # status[:brightness].should eq(100) - exec(:switch_to, "tv") - should_send("\x010*0E0A\x020060000A\x03\x68\x0D") - responds("\x0100*F12\x02000060000000000A\x03\x19\x0D") - status[:input].should eq("Tv") + # exec(:switch_to, "tv") + # should_send("\x010*0E0A\x020060000A\x03\x68\x0D") + # responds("\x0100*F12\x02000060000000000A\x03\x19\x0D") + # status[:input].should eq("Tv") - exec(:switch_audio, "audio_2") - sleep 6 # since switch_to has 6 seconds of delay - should_send("\x010*0E0A\x02022E0002\x03\x68\x0D") - responds("\x0100*F12\x0200022E0000000002\x03\x19\x0D") - status[:audio].should eq("Audio2") + # exec(:switch_audio, "audio_2") + # sleep 6 # since switch_to has 6 seconds of delay + # should_send("\x010*0E0A\x02022E0002\x03\x68\x0D") + # responds("\x0100*F12\x0200022E0000000002\x03\x19\x0D") + # status[:audio].should eq("Audio2") - exec(:power, false) - should_send("\x010*0A0C\x02C203D60004\x03\x1D\x0D") - responds("\x0100*B0E\x0200C203D60004\x03\x18\x0D") - status[:power].should eq(false) + # exec(:power, false) + # should_send("\x010*0A0C\x02C203D60004\x03\x1D\x0D") + # responds("\x0100*B0E\x0200C203D60004\x03\x18\x0D") + # status[:power].should eq(false) end From 297da7c8ea5eb3b1f9bc1f30d69593ff318e9745 Mon Sep 17 00:00:00 2001 From: Philip Kheav Date: Fri, 27 Nov 2020 16:05:27 +0000 Subject: [PATCH 04/27] feat(nec_proj): add more methods --- drivers/nec/np_series.cr | 266 +++++++++------------------------------ 1 file changed, 57 insertions(+), 209 deletions(-) diff --git a/drivers/nec/np_series.cr b/drivers/nec/np_series.cr index aefcaacacac..02a0ceff826 100644 --- a/drivers/nec/np_series.cr +++ b/drivers/nec/np_series.cr @@ -7,23 +7,17 @@ class Nec::NpSeries < PlaceOS::Driver include Interface::Muteable enum Input - Vga = 1 - Rgbhv = 2 - Dvi = 3 - HdmiSet = 4 - Video1 = 5 - Video2 = 6 - Svideo = 7 - Tuner = 9 - Tv = 10 - Dvd1 = 12 - Option = 13 - Dvd2 = 14 - DisplayPort = 15 - Hdmi = 17 - Hdmi2 = 18 - Hdmi3 = 130 - Usb = 135 + VGA = 0x01 + RGBHV = 0x02 + Composite = 0x06 + SVideo = 0x0B + Component = 0x10 + Component2 = 0x11 + HDMI = 0x1A + HDMI2 = 0x1B + DisplayPort = 0xA6 + LAN = 0x20 + Viewer = 0x1F end include PlaceOS::Driver::Interface::InputSelection(Input) @@ -109,15 +103,13 @@ class Nec::NpSeries < PlaceOS::Driver {% end %} def volume(vol : Int32) - # volume base command D1 D2 D3 D4 D5 + CKS - command = [0x03, 0x10, 0x00, 0x00, 0x05, 0x05, 0x00, 0x00, 0x00, 0x00] + vol = vol.clamp(@volume_min, @volume_max) + # volume base command D1 D2 D3 D4 D5 + CKS + command = Bytes[0x03, 0x10, 0x00, 0x00, 0x05, 0x05, 0x00, 0x00, vol, 0x00] # D3 = 00 (absolute vol) or 01 (relative vol) # D4 = value (lower bits 0 to 63) # D5 = value (higher bits always 00h) - vol = vol.clamp(@volume_min, @volume_max) - command[-2] = vol - send_checksum(command) self[:volume] = vol end @@ -145,29 +137,6 @@ class Nec::NpSeries < PlaceOS::Driver state ? mute_audio_cmd : unmute_audio end - def power(state : Bool) - # Do nothing if already in desired state - return if self[:power]? == state - - if state - logger.debug { "-- NEC LCD, requested to power on" } - # 1 = Power On - data = MsgType::Command.build(Command::SetPower, 1) - send(data, name: "power", delay: 5.seconds) - else - logger.debug { "-- NEC LCD, requested to power off" } - # 4 = Power Off - data = MsgType::Command.build(Command::SetPower, 4) - send(data, name: "power", delay: 10.seconds, timeout: 10.seconds) - end - end - - def power?(**options) : Bool - data = MsgType::Command.build(Command::PowerQuery) - send(data, **options, name: "power?").get - self[:power].as_bool - end - def switch_to(input : Input) logger.debug { "-- NEC LCD, requested to switch to: #{input}" } data = MsgType::SetParameter.build(Command::VideoInput, input.value) @@ -175,157 +144,62 @@ class Nec::NpSeries < PlaceOS::Driver end enum Audio - Audio1 = 1 - Audio2 = 2 - Audio3 = 3 - Hdmi = 4 - Tv = 6 - DisplayPort = 7 + HDMI + VGA end def switch_audio(input : Audio) - logger.debug { "-- NEC LCD, requested to switch audio to: #{input}" } - data = MsgType::SetParameter.build(Command::AudioInput, input.value) - send(data, name: "audio") + # C0 == HDMI Audio + command = Bytes[0x03, 0xB1, 0x00, 0x00, 0x02, 0xC0, input.value] + send_checksum(command, name: :switch_audio) end - def auto_adjust - data = MsgType::SetParameter.build(Command::AutoSetup, 1) - send(data, name: "auto_adjust") - end + def power(state : Bool) + #:lamp_on => "$02,$00,$00,$00,$00,$02", + #:lamp_off => "$02,$01,$00,$00,$00,$03", + @power_target = state - def brightness(val : Int32) - data = MsgType::SetParameter.build(Command::BrightnessStatus, val.clamp(0, 100)) - send(data, name: "brightness") - send(MsgType::Command.build(Command::Save), name: "save", priority: 0) + if state + command = Bytes[0x02, 0x00, 0x00, 0x00, 0x00, 0x02] + send(command, name: "power", timeout: 15.seconds, delay: 1.second) + else + command = Bytes[0x02, 0x01, 0x00, 0x00, 0x00, 0x03] + # Jump ahead of any other queued commands as they are no longer important + send( + command, + name: "power", + timeout: 60.seconds, # don't want retries occuring very fast + delay: 30.seconds, + clear_queue: true, + priority: 100, + # delay_on_receive: 200 # give it a little bit of breathing room + ) + end end - def contrast(val : Int32) - data = MsgType::SetParameter.build(Command::ContrastStatus, val.clamp(0, 100)) - send(data, name: "contrast") - send(MsgType::Command.build(Command::Save), name: "save", priority: 0) + def power?(**options) : Bool + send_checksum(COMMAND[:status_lamp], **options) + !!self[:power]?.try(&.as_bool) end - def volume(val : Int32) - data = MsgType::SetParameter.build(Command::VolumeStatus, val.clamp(0, 100)) - send(data, name: "volume") - send(MsgType::Command.build(Command::Save), name: "save", priority: 0) + def switch_to(input : Input) + @input_target = input + command = Bytes[0x02, 0x03, 0x00, 0x00, 0x02, 0x01, input.value] + send_checksum(command, name: "input") end def do_poll - current_power = power?(priority: 0) - logger.debug { "Polling, power = #{current_power}" } - - if current_power - mute_status - volume_status - video_input - audio_input + if power?(priority: 0) + status_input(priority: 0) + status_mute(priority: 0) + background_black(priority: 0) + lamp_information(priority: 0) end end - # LCD Response code def received(data, task) - task_name = task.try &.name - logger.debug { "task is #{task_name}" } - - ascii_string = String.new(data) - # Check for valid response - if !check_checksum(data) - return task.try &.retry("-- NEC LCD, invalid response was: #{ascii_string}") - end - - logger.debug { "NEC LCD responded with ascii_string #{ascii_string}" } - - if ascii_string[8..9] == "00" - parse_response(ascii_string) - elsif ascii_string[10..13] == "00D6" # Annoyingly unique case to deal with power status query - self[:power] = ascii_string[23] == '1' - elsif ascii_string[8..9] == "BE" # Wait response - return task.try &.retry("-- NEC LCD, response was a wait command") - else - return task.try &.abort("-- NEC LCD, command failed: #{task_name}\n-- NEC LCD, response was: #{ascii_string}") - end - - task.try &.success end - private def parse_response(data : String) - logger.debug { "data is #{data}" } - - if data.size >= 15 - command = ( - # For most commands - Command.from_value?(data[10..13].to_i(16)) || \ - # For Command::SetPower - Command.from_value?(data[10..15].to_i(16)) - ).not_nil! - else # This is a short command and is most likely Command::Save - # This line will error if this is not Command::Save which is fine - command = Command.from_value(data[9..10].to_i(16)) - # Don't do any processing for Command::Save - return if command.save? - end - value = (command.set_power? ? data[16..19] : data[20..23]).to_i(16) - - case command - when .video_input? - self[:input] = Input.from_value(value) - when .audio_input? - self[:audio] = Audio.from_value(value) - when .volume_status? - self[:volume] = value - self[:audio_mute] = value == 0 - when .brightness_status? - self[:brightness] = value - when .contrast_status? - self[:contrast] = value - when .mute_status? - self[:audio_mute] = value == 1 - self[:volume] = 0 if value == 1 - when .auto_setup? - # auto_setup - # nothing needed to do here (we are delaying the next command by 4 seconds) - when .set_power? - self[:power] = value == 1 - else - logger.debug { "-- NEC LCD, unknown response received: #{data}" } - end - end - - enum Command - VideoInput = 0x0060 - AudioInput = 0x022E - VolumeStatus = 0x0062 - MuteStatus = 0x008D - PowerOnDelay = 0x02D8 - ContrastStatus = 0x0012 - BrightnessStatus = 0x0010 - AutoSetup = 0x001E - PowerQuery = 0x01D6 - Save = 0x0C - SetPower = 0xC203D6 - - def to_s : String - case self - when .save? - length = 2 - when .set_power? - length = 6 - else - length = 4 - end - value.to_s(16, true).rjust(length, '0') - end - end - - {% for name in Command.constants %} - @[Security(Level::Administrator)] - def {{name.id.underscore}}(priority : Int32 = 0) - send(MsgType::GetParameter.build(Command::{{name.id}}), priority: priority, name: {{name.id.underscore.stringify}}) - end - {% end %} - private def check_checksum(data : Bytes) # Loop through the second to the third last element checksum = data[1..-3].reduce { |a, b| a ^ b } @@ -334,37 +208,11 @@ class Nec::NpSeries < PlaceOS::Driver checksum == data[-2] end - # Types of messages sent to and from the LCD - enum MsgType : UInt8 - Command = 0x41 # 'A' - CommandReply = 0x42 # 'B' - GetParameter = 0x43 # 'C' - GetParameterReply = 0x44 # 'D' - SetParameter = 0x45 # 'E' - SetParameterReply = 0x46 # 'F' - - def build(command : Nec::NpSeries::Command, data : Int? = nil) - command = command.to_s - - message = String.build do |str| - str << "0*0" - str.write_byte self.value # Type - - message_length = command.size + 2 - message_length += 4 if d = data # If there is data, add 4 to the message length - str << message_length.to_s(16, true).rjust(2, '0') # Message length - str.write_byte 0x02 # Start of messsage - str << command # Message - str << d.to_s(16, true).rjust(4, '0') if d # Data if required - str.write_byte 0x03 # End of message - end - - String.build do |str| - str.write_byte 0x01 # SOH - str << message # Message - str.write_byte message.each_byte.reduce { |a, b| a ^ b } # Checksum - str.write_byte DELIMITER # Delimiter - end - end + private def send_checksum(command, **options) + command = command.delete(' ').hexbytes if command.is_a?(String) + data = Bytes.new(command.size + 1) + data.copy_from(command) + data[-1] = command.reduce(&.+) + send(data, **options) end end From 2064b0326e0f4cad2a871f8d01f6c53b21a4b3d5 Mon Sep 17 00:00:00 2001 From: Philip Kheav Date: Fri, 27 Nov 2020 17:05:32 +0000 Subject: [PATCH 05/27] feat(nec_proj): add part of received method --- drivers/nec/np_series.cr | 61 ++++++++++++++++++++++++++--------- drivers/nec/np_series_spec.cr | 2 +- 2 files changed, 47 insertions(+), 16 deletions(-) diff --git a/drivers/nec/np_series.cr b/drivers/nec/np_series.cr index 02a0ceff826..3b6fb5e073e 100644 --- a/drivers/nec/np_series.cr +++ b/drivers/nec/np_series.cr @@ -2,7 +2,7 @@ require "placeos-driver/interface/powerable" require "placeos-driver/interface/muteable" require "placeos-driver/interface/switchable" -class Nec::NpSeries < PlaceOS::Driver +class Nec::Projector < PlaceOS::Driver include Interface::Powerable include Interface::Muteable @@ -72,24 +72,24 @@ class Nec::NpSeries < PlaceOS::Driver # Mute controls mute_picture: "$02,$10,$00,$00,$00,$12", unmute_picture: "$02,$11,$00,$00,$00,$13", - mute_audio_cmd: "02H 12H 00H 00H 00H 14H", - unmute_audio: "02H 13H 00H 00H 00H 15H", - mute_onscreen: "02H 14H 00H 00H 00H 16H", - unmute_onscreen: "02H 15H 00H 00H 00H 17H", + mute_audio_cmd: "02 12 00 00 00 14", + unmute_audio: "02 13 00 00 00 15", + mute_onscreen: "02 14 00 00 00 16", + unmute_onscreen: "02 15 00 00 00 17", freeze_picture: "$01,$98,$00,$00,$01,$01,$9B", unfreeze_picture: "$01,$98,$00,$00,$01,$02,$9C", - status_lamp: "00H 81H 00H 00H 00H 81H", # Running sense (ret 81) + status_lamp: Bytes[0x00, 0x81, 0x00, 0x00, 0x00, 0x81], # Running sense (ret 81) status_input: "$00,$85,$00,$00,$01,$02,$88", # Input status (ret 85) - status_mute: "00H 85H 00H 00H 01H 03H 89H", # MUTE STATUS REQUEST (Check 10H on byte 5) - status_error: "00H 88H 00H 00H 00H 88H", # ERROR STATUS REQUEST (ret 88) - status_model: "00H 85H 00H 00H 01H 04H 8A", # request model name (both of these are related) + status_mute: "00 85 00 00 01 03 89", # MUTE STATUS REQUEST (Check 10H on byte 5) + status_error: "00 88 00 00 00 88", # ERROR STATUS REQUEST (ret 88) + status_model: "00 85 00 00 01 04 8A", # request model name (both of these are related) # lamp hours / remaining information - lamp_information: "03H 8AH 00H 00H 00H 8DH", # LAMP INFORMATION REQUEST - filter_information: "03H 8AH 00H 00H 00H 8DH", - projector_information: "03H 8AH 00H 00H 00H 8DH", + lamp_information: "03 8A 00 00 00 8D", # LAMP INFORMATION REQUEST + filter_information: "03 8A 00 00 00 8D", + projector_information: "03 8A 00 00 00 8D", background_black: "$03,$B1,$00,$00,$02,$0B,$01,$C2", # set mute to be a black screen background_blue: "$03,$B1,$00,$00,$02,$0B,$00,$C1", # set mute to be a blue screen @@ -151,12 +151,10 @@ class Nec::NpSeries < PlaceOS::Driver def switch_audio(input : Audio) # C0 == HDMI Audio command = Bytes[0x03, 0xB1, 0x00, 0x00, 0x02, 0xC0, input.value] - send_checksum(command, name: :switch_audio) + send_checksum(command, name: "switch_audio") end def power(state : Bool) - #:lamp_on => "$02,$00,$00,$00,$00,$02", - #:lamp_off => "$02,$01,$00,$00,$00,$03", @power_target = state if state @@ -197,7 +195,40 @@ class Nec::NpSeries < PlaceOS::Driver end end + # Values of first byte in response of successful commands + enum Success + Status = 0x20 + Freeze = 0x21 + Mute = 0x22 + Lamp = 0x23 + end + + enum Response + Power = 0x81 + Error = 0x88 + SetInput = 0x03 + Lamp = 0x00 + Lamp2 = 0x01 + Mute = 0x10 + Mute1 = 0x11 + Mute2 = 0x12 + Mute3 = 0x13 + Mute4 = 0x14 + Mute5 = 0x15 + end + def received(data, task) + logger.debug { "NEC projector sent: 0x#{data.hexstring}" } + + # Only process response if successful + # Otherwise return success to prevent retries on commands we were not expecting + return task.try(&.success) unless Success.from_value?(data[0]) && (resp = Response.from_value?(data[1])) + + case resp + when .power? + end + + task.try(&.success) end private def check_checksum(data : Bytes) diff --git a/drivers/nec/np_series_spec.cr b/drivers/nec/np_series_spec.cr index d99facad813..a4f0eeb6c15 100644 --- a/drivers/nec/np_series_spec.cr +++ b/drivers/nec/np_series_spec.cr @@ -1,4 +1,4 @@ -DriverSpecs.mock_driver "Nec::NpSeries" do +DriverSpecs.mock_driver "Nec::Projector" do # # do_poll # # power? # should_send("\x010*0A06\x0201D6\x03\x1F\x0D") From d7df7f494bb0afeb500b911048216a6c0955104f Mon Sep 17 00:00:00 2001 From: Philip Kheav Date: Mon, 30 Nov 2020 12:57:57 +0000 Subject: [PATCH 06/27] feat(nec_proj): add access for request in received --- drivers/nec/np_series.cr | 88 ++++++++++++++++++++++++++++++++-------- 1 file changed, 72 insertions(+), 16 deletions(-) diff --git a/drivers/nec/np_series.cr b/drivers/nec/np_series.cr index 3b6fb5e073e..7b7b6f4cd2a 100644 --- a/drivers/nec/np_series.cr +++ b/drivers/nec/np_series.cr @@ -195,6 +195,22 @@ class Nec::Projector < PlaceOS::Driver end end + private def check_checksum(data : Bytes) + # Loop through the second to the third last element + checksum = data[1..-3].reduce { |a, b| a ^ b } + # Check the checksum equals the second last element + logger.debug { "Error: checksum should be 0x#{checksum.to_s(16)}" } unless checksum == data[-2] + checksum == data[-2] + end + + private def send_checksum(command, **options) + command = command.delete(' ').hexbytes if command.is_a?(String) + req = Bytes.new(command.size + 1) + req.copy_from(command) + req[-1] = command.reduce(&.+) + send(req, **options) { |data, task| process_response(data, task, req) } + end + # Values of first byte in response of successful commands enum Success Status = 0x20 @@ -206,7 +222,7 @@ class Nec::Projector < PlaceOS::Driver enum Response Power = 0x81 Error = 0x88 - SetInput = 0x03 + Input = 0x03 Lamp = 0x00 Lamp2 = 0x01 Mute = 0x10 @@ -217,33 +233,73 @@ class Nec::Projector < PlaceOS::Driver Mute5 = 0x15 end - def received(data, task) + private def process_response(data, task, req = nil) logger.debug { "NEC projector sent: 0x#{data.hexstring}" } + if (data[0] & 0xA0) == 0xA0 + # We were changing power state at time of failure we should keep trying + if req && [0x00, 0x01].includes?(req[1]) + # command[:delay_on_receive] = 6000 + power? + return task.try(&.success) + end + # logger.warn "-- NEC projector, sent fail code for command: 0x#{byte_to_hex(req)}" if req + # logger.warn "-- NEC projector, response was: 0x#{byte_to_hex(response)}" + return task.try(&.abort) + end + + # Check checksum + unless check_checksum(data) + # logger.warn "-- NEC projector, checksum failed for command: 0x#{byte_to_hex(req)}" if req + return task.try(&.abort) + end + # Only process response if successful # Otherwise return success to prevent retries on commands we were not expecting return task.try(&.success) unless Success.from_value?(data[0]) && (resp = Response.from_value?(data[1])) case resp when .power? + when .error? + # when 0x85 + # # Return if we can't work out what was requested initially + # return true unless req + + # case req[-2] + # when 0x02 + # return process_input_state(data, command) + # when 0x03 + # process_mute_state(data, req) + # return true + # end + when .input? + when .lamp?, .lamp2? + when .mute?, .mute1?, .mute2?, .mute3?, .mute4?, .mute5? + when 0x23 + # case data[1] + # when 0x10 + # # + # # Picture, Volume, Keystone, Image adjust mode + # # how to play this? + # # + # # TODO:: process volume control + # # + # return true + # when 0x8A + # process_projector_information(data, req) + # return true + + # when 0xB1 + # # This is the audio switch command + # # TODO:: data[-2] == 0:Normal, 1:Error + # # If error do we retry? Or does it mean something else + # return true end task.try(&.success) end - private def check_checksum(data : Bytes) - # Loop through the second to the third last element - checksum = data[1..-3].reduce { |a, b| a ^ b } - # Check the checksum equals the second last element - logger.debug { "Error: checksum should be 0x#{checksum.to_s(16)}" } unless checksum == data[-2] - checksum == data[-2] - end - - private def send_checksum(command, **options) - command = command.delete(' ').hexbytes if command.is_a?(String) - data = Bytes.new(command.size + 1) - data.copy_from(command) - data[-1] = command.reduce(&.+) - send(data, **options) + def received(data, task) + process_response(data, task) end end From 0f2ed526b82b015797fc5016bf2dc2608a28e239 Mon Sep 17 00:00:00 2001 From: Philip Kheav Date: Mon, 30 Nov 2020 13:26:39 +0000 Subject: [PATCH 07/27] feat(nec_proj): add more response processing --- drivers/nec/np_series.cr | 102 +++++++++++++++++++++------------------ 1 file changed, 55 insertions(+), 47 deletions(-) diff --git a/drivers/nec/np_series.cr b/drivers/nec/np_series.cr index 7b7b6f4cd2a..6b7f24211b0 100644 --- a/drivers/nec/np_series.cr +++ b/drivers/nec/np_series.cr @@ -213,13 +213,13 @@ class Nec::Projector < PlaceOS::Driver # Values of first byte in response of successful commands enum Success - Status = 0x20 + Query = 0x20 Freeze = 0x21 Mute = 0x22 Lamp = 0x23 end - enum Response + enum Type Power = 0x81 Error = 0x88 Input = 0x03 @@ -236,6 +236,7 @@ class Nec::Projector < PlaceOS::Driver private def process_response(data, task, req = nil) logger.debug { "NEC projector sent: 0x#{data.hexstring}" } + # Command failed if (data[0] & 0xA0) == 0xA0 # We were changing power state at time of failure we should keep trying if req && [0x00, 0x01].includes?(req[1]) @@ -243,60 +244,67 @@ class Nec::Projector < PlaceOS::Driver power? return task.try(&.success) end - # logger.warn "-- NEC projector, sent fail code for command: 0x#{byte_to_hex(req)}" if req - # logger.warn "-- NEC projector, response was: 0x#{byte_to_hex(response)}" - return task.try(&.abort) + return task.try(&.abort("-- NEC projector, sent fail code for command: 0x#{req.try(&.hexstring) || "unknown"}")) end - # Check checksum + # Verify checksum unless check_checksum(data) - # logger.warn "-- NEC projector, checksum failed for command: 0x#{byte_to_hex(req)}" if req - return task.try(&.abort) + return task.try(&.abort("-- NEC projector, checksum failed for command: 0x#{req.try(&.hexstring) || "unknown"}")) end # Only process response if successful # Otherwise return success to prevent retries on commands we were not expecting - return task.try(&.success) unless Success.from_value?(data[0]) && (resp = Response.from_value?(data[1])) - - case resp - when .power? - when .error? - # when 0x85 - # # Return if we can't work out what was requested initially - # return true unless req - - # case req[-2] - # when 0x02 - # return process_input_state(data, command) - # when 0x03 - # process_mute_state(data, req) - # return true - # end - when .input? - when .lamp?, .lamp2? - when .mute?, .mute1?, .mute2?, .mute3?, .mute4?, .mute5? - when 0x23 - # case data[1] - # when 0x10 - # # - # # Picture, Volume, Keystone, Image adjust mode - # # how to play this? - # # - # # TODO:: process volume control - # # - # return true - # when 0x8A - # process_projector_information(data, req) - # return true - - # when 0xB1 - # # This is the audio switch command - # # TODO:: data[-2] == 0:Normal, 1:Error - # # If error do we retry? Or does it mean something else - # return true + return task.try(&.success) unless (s = Success.from_value?(data[0])) && (type = Type.from_value?(data[1])) + + case s + when .query? + case type + when .power? + when .error? + when 0x85 + # Return if we can't work out what was requested initially + return task.try(&.success) unless req + + case req[-2] + when 0x02 + # return process_input_state(data, command) + when 0x03 + # process_mute_state(data, req) + return true + end + end + when .freeze? + # TODO + when .mute? + case type + when .input? + # return process_input_switch(data, req) + when .lamp?, .lamp2? + # process_lamp_command(data, req) + return true + when .mute?, .mute1?, .mute2?, .mute3?, .mute4?, .mute5? + status_mute # update mute status's (dry) + return true + end + when .lamp? + case type.value + when 0x10 + # Picture, Volume, Keystone, Image adjust mode + # how to play this? + # TODO:: process volume control + return true + when 0x8A + # process_projector_information(data, req) + return true + when 0xB1 + # This is the audio switch command + # TODO:: data[-2] == 0:Normal, 1:Error + # If error do we retry? Or does it mean something else + return true + end end - task.try(&.success) + task.try(&.success("-- NEC projector, no status updates defined for response for command: 0x#{req.try(&.hexstring) || "unknown"}")) end def received(data, task) From 6ab68c3efb8e81b13257601b70c697166a3a185e Mon Sep 17 00:00:00 2001 From: Philip Kheav Date: Mon, 30 Nov 2020 13:27:58 +0000 Subject: [PATCH 08/27] chore(nec_proj): cleanup with crystal tool format --- drivers/nec/np_series.cr | 26 +++++++++++++------------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/drivers/nec/np_series.cr b/drivers/nec/np_series.cr index 6b7f24211b0..ef4497d83de 100644 --- a/drivers/nec/np_series.cr +++ b/drivers/nec/np_series.cr @@ -81,10 +81,10 @@ class Nec::Projector < PlaceOS::Driver unfreeze_picture: "$01,$98,$00,$00,$01,$02,$9C", status_lamp: Bytes[0x00, 0x81, 0x00, 0x00, 0x00, 0x81], # Running sense (ret 81) - status_input: "$00,$85,$00,$00,$01,$02,$88", # Input status (ret 85) - status_mute: "00 85 00 00 01 03 89", # MUTE STATUS REQUEST (Check 10H on byte 5) - status_error: "00 88 00 00 00 88", # ERROR STATUS REQUEST (ret 88) - status_model: "00 85 00 00 01 04 8A", # request model name (both of these are related) + status_input: "$00,$85,$00,$00,$01,$02,$88", # Input status (ret 85) + status_mute: "00 85 00 00 01 03 89", # MUTE STATUS REQUEST (Check 10H on byte 5) + status_error: "00 88 00 00 00 88", # ERROR STATUS REQUEST (ret 88) + status_model: "00 85 00 00 01 04 8A", # request model name (both of these are related) # lamp hours / remaining information lamp_information: "03 8A 00 00 00 8D", # LAMP INFORMATION REQUEST @@ -213,7 +213,7 @@ class Nec::Projector < PlaceOS::Driver # Values of first byte in response of successful commands enum Success - Query = 0x20 + Query = 0x20 Freeze = 0x21 Mute = 0x22 Lamp = 0x23 @@ -223,9 +223,9 @@ class Nec::Projector < PlaceOS::Driver Power = 0x81 Error = 0x88 Input = 0x03 - Lamp = 0x00 + Lamp = 0x00 Lamp2 = 0x01 - Mute = 0x10 + Mute = 0x10 Mute1 = 0x11 Mute2 = 0x12 Mute3 = 0x13 @@ -289,13 +289,13 @@ class Nec::Projector < PlaceOS::Driver when .lamp? case type.value when 0x10 - # Picture, Volume, Keystone, Image adjust mode - # how to play this? - # TODO:: process volume control - return true + # Picture, Volume, Keystone, Image adjust mode + # how to play this? + # TODO:: process volume control + return true when 0x8A - # process_projector_information(data, req) - return true + # process_projector_information(data, req) + return true when 0xB1 # This is the audio switch command # TODO:: data[-2] == 0:Normal, 1:Error From 09ba96b3e45215dc41bde659d8349bb9e5f11b00 Mon Sep 17 00:00:00 2001 From: Philip Kheav Date: Mon, 30 Nov 2020 13:47:01 +0000 Subject: [PATCH 09/27] chore(nec_proj): remove not needed commas --- drivers/nec/np_series.cr | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/drivers/nec/np_series.cr b/drivers/nec/np_series.cr index ef4497d83de..3397c0e5944 100644 --- a/drivers/nec/np_series.cr +++ b/drivers/nec/np_series.cr @@ -28,7 +28,7 @@ class Nec::Projector < PlaceOS::Driver default_settings({ volume_min: 0, - volume_max: 63, + volume_max: 63 }) @power_target : Bool? = nil @@ -93,7 +93,7 @@ class Nec::Projector < PlaceOS::Driver background_black: "$03,$B1,$00,$00,$02,$0B,$01,$C2", # set mute to be a black screen background_blue: "$03,$B1,$00,$00,$02,$0B,$00,$C1", # set mute to be a blue screen - background_logo: "$03,$B1,$00,$00,$02,$0B,$02,$C3", # set mute to be the company logo + background_logo: "$03,$B1,$00,$00,$02,$0B,$02,$C3" # set mute to be the company logo } {% for name, data in COMMAND %} From 671b6708bf2ccae9d81e02b27ff142f9f9fa5d46 Mon Sep 17 00:00:00 2001 From: Philip Kheav Date: Mon, 30 Nov 2020 17:40:23 +0000 Subject: [PATCH 10/27] feat(nec_proj): add input mapping for responses --- drivers/nec/np_series.cr | 57 ++++++++++++++++++++++++++++++++++++++-- 1 file changed, 55 insertions(+), 2 deletions(-) diff --git a/drivers/nec/np_series.cr b/drivers/nec/np_series.cr index 3397c0e5944..6bcc7982a84 100644 --- a/drivers/nec/np_series.cr +++ b/drivers/nec/np_series.cr @@ -239,7 +239,7 @@ class Nec::Projector < PlaceOS::Driver # Command failed if (data[0] & 0xA0) == 0xA0 # We were changing power state at time of failure we should keep trying - if req && [0x00, 0x01].includes?(req[1]) + if req && (0..1).includes?(req[1]) # command[:delay_on_receive] = 6000 power? return task.try(&.success) @@ -267,7 +267,7 @@ class Nec::Projector < PlaceOS::Driver case req[-2] when 0x02 - # return process_input_state(data, command) + return process_input_state(data, task, req) when 0x03 # process_mute_state(data, req) return true @@ -310,4 +310,57 @@ class Nec::Projector < PlaceOS::Driver def received(data, task) process_response(data, task) end + + # NEC has different values for the input status when compared to input selection + INPUT_MAP = { + 0x01 => { + 0x01 => Input::VGA, + 0x02 => Input::Composite, + 0x03 => Input::SVideo, + 0x06 => Input::HDMI, + 0x07 => Input::Viewer, + 0x21 => Input::HDMI, + 0x22 => Input::DisplayPort + }, + 0x02 => { + 0x01 => Input::RGBHV, + 0x04 => Input::Component2, + 0x06 => Input::HDMI2, + 0x07 => Input::LAN, + 0x21 => Input::HDMI2 + }, + 0x03 => { + 0x04 => Input::Component + } + } + private def process_input_state(data, task, req) + return task.try(&.success) unless self[:power]?.try(&.as_bool) && (first = INPUT_MAP[data[-15]]) + + logger.debug { "-- NEC projector sent a response to an input state command" } + + self[:input] = current_input = first[data[-14]] || "unknown" + if data[-17] == 0x01 + # TODO + # command[:delay_on_receive] = 3000 # still processing signal + status_input + else + status_mute # get mute status one signal has settled + end + + logger.debug { "The input selected was: #{current_input}" } + + # Notify of bad input selection for debugging + # We ensure at the very least power state and input are always correct + if (input_target = @input_target) + # If we have reached the input_target, clear @input_target so input can be set again + if current_input == input_target + @input_target = nil + else + logger.debug { "-- NEC input state may not be correct, desired: #{input_target} current: #{current_input}" } + switch_to(input_target) + end + end + + task.try(&.success) + end end From c97465034d27806e511ca205a8221be30f5a598d Mon Sep 17 00:00:00 2001 From: Philip Kheav Date: Mon, 30 Nov 2020 18:01:41 +0000 Subject: [PATCH 11/27] feat(nec_proj): add more response handling --- drivers/nec/np_series.cr | 58 ++++++++++++++++++++++++++++------------ 1 file changed, 41 insertions(+), 17 deletions(-) diff --git a/drivers/nec/np_series.cr b/drivers/nec/np_series.cr index 6bcc7982a84..b7508a769a9 100644 --- a/drivers/nec/np_series.cr +++ b/drivers/nec/np_series.cr @@ -176,7 +176,7 @@ class Nec::Projector < PlaceOS::Driver end def power?(**options) : Bool - send_checksum(COMMAND[:status_lamp], **options) + send_checksum(COMMAND[:status_lamp], **options).get !!self[:power]?.try(&.as_bool) end @@ -267,40 +267,37 @@ class Nec::Projector < PlaceOS::Driver case req[-2] when 0x02 - return process_input_state(data, task, req) + return process_input_state(data, task) when 0x03 - # process_mute_state(data, req) - return true + return process_mute_state(data, task) end end - when .freeze? - # TODO + when .freeze? # TODO when .mute? case type when .input? - # return process_input_switch(data, req) + return process_input_switch(data, task, req) when .lamp?, .lamp2? - # process_lamp_command(data, req) - return true + return process_lamp_command(data, task, req) when .mute?, .mute1?, .mute2?, .mute3?, .mute4?, .mute5? status_mute # update mute status's (dry) - return true + return task.try(&.success) end when .lamp? - case type.value + case data[1] # TODO: add these cases to Type when 0x10 # Picture, Volume, Keystone, Image adjust mode - # how to play this? - # TODO:: process volume control - return true + # how to play this? + # TODO:: process volume control + return task.try(&.success) when 0x8A # process_projector_information(data, req) - return true + return task.try(&.success) when 0xB1 # This is the audio switch command # TODO:: data[-2] == 0:Normal, 1:Error # If error do we retry? Or does it mean something else - return true + return task.try(&.success) end end @@ -333,7 +330,7 @@ class Nec::Projector < PlaceOS::Driver 0x04 => Input::Component } } - private def process_input_state(data, task, req) + private def process_input_state(data, task) return task.try(&.success) unless self[:power]?.try(&.as_bool) && (first = INPUT_MAP[data[-15]]) logger.debug { "-- NEC projector sent a response to an input state command" } @@ -363,4 +360,31 @@ class Nec::Projector < PlaceOS::Driver task.try(&.success) end + + private def process_mute_state(data, task) + logger.debug { "-- NEC projector responded to mute state command" } + self[:picture_mute] = data[-17] == 0x01 + self[:audio_mute] = data[-16] == 0x01 + self[:onscreen_mute] = data[-15] == 0x01 + self[:mute] = data[-17] == 0x01 # Same as picture mute + task.try(&.success) + end + + private def process_input_switch(data, task, req) + logger.debug { "-- NEC projector responded to switch input command" } + if data[-2] != 0xFF + status_input # Double check with a status update + return task.try(&.success) + end + task.try(&.retry("-- NEC projector failed to switch input with command: #{req.try(&.hexstring) || "unknown"}")) + end + + private def process_lamp_command(data, task, req) + logger.debug { "-- NEC projector sent a response to a power command" } + # Ensure a change of power state was the last command sent + if req && (0..1).includes?(req[1]) + power? # Queues the status power command + end + task.try(&.success) + end end From 24e62e43ae1338719a4b762add6c9c3127b9e933 Mon Sep 17 00:00:00 2001 From: Philip Kheav Date: Mon, 30 Nov 2020 18:24:33 +0000 Subject: [PATCH 12/27] feat(nec_proj): add power status response handling --- drivers/nec/np_series.cr | 92 ++++++++++++++++++++++++++++++---------- 1 file changed, 69 insertions(+), 23 deletions(-) diff --git a/drivers/nec/np_series.cr b/drivers/nec/np_series.cr index b7508a769a9..a9a2ee82019 100644 --- a/drivers/nec/np_series.cr +++ b/drivers/nec/np_series.cr @@ -219,19 +219,19 @@ class Nec::Projector < PlaceOS::Driver Lamp = 0x23 end - enum Type - Power = 0x81 - Error = 0x88 - Input = 0x03 - Lamp = 0x00 - Lamp2 = 0x01 - Mute = 0x10 - Mute1 = 0x11 - Mute2 = 0x12 - Mute3 = 0x13 - Mute4 = 0x14 - Mute5 = 0x15 - end + # enum Type + # Power = 0x81 + # Error = 0x88 + # Input = 0x03 + # Lamp = 0x00 + # Lamp2 = 0x01 + # Mute = 0x10 + # Mute1 = 0x11 + # Mute2 = 0x12 + # Mute3 = 0x13 + # Mute4 = 0x14 + # Mute5 = 0x15 + # end private def process_response(data, task, req = nil) logger.debug { "NEC projector sent: 0x#{data.hexstring}" } @@ -254,17 +254,18 @@ class Nec::Projector < PlaceOS::Driver # Only process response if successful # Otherwise return success to prevent retries on commands we were not expecting - return task.try(&.success) unless (s = Success.from_value?(data[0])) && (type = Type.from_value?(data[1])) + return task.try(&.success) unless (s = Success.from_value?(data[0]))# && (type = Type.from_value?(data[1])) case s when .query? - case type - when .power? - when .error? + case data[1] + when 0x81 + return process_power_status(data, task) + when 0x88 # TODO + # return process_error_status(data, task) when 0x85 # Return if we can't work out what was requested initially return task.try(&.success) unless req - case req[-2] when 0x02 return process_input_state(data, task) @@ -274,12 +275,12 @@ class Nec::Projector < PlaceOS::Driver end when .freeze? # TODO when .mute? - case type - when .input? + case data[1] + when 0x03 return process_input_switch(data, task, req) - when .lamp?, .lamp2? + when (0..1) return process_lamp_command(data, task, req) - when .mute?, .mute1?, .mute2?, .mute3?, .mute4?, .mute5? + when (0x10..0x15) status_mute # update mute status's (dry) return task.try(&.success) end @@ -290,7 +291,7 @@ class Nec::Projector < PlaceOS::Driver # how to play this? # TODO:: process volume control return task.try(&.success) - when 0x8A + when 0x8A # TODO # process_projector_information(data, req) return task.try(&.success) when 0xB1 @@ -308,6 +309,51 @@ class Nec::Projector < PlaceOS::Driver process_response(data, task) end + # Process the lamp status response + # Intimately entwined with the power power command + # (as we need to control ensure we are in the correct target state) + private def process_power_status(data, task) + logger.debug { "-- NEC projector sent a response to a power status command" } + + self[:power] = (data[-2] & 0b10) > 0x0 + + # Projector cooling || power on off processing + if (data[-2] & 0b100000) > 0 || (data[-2] & 0b10000000) > 0 + if @power_target + self[:cooling] = false + self[:warming] = true + logger.debug { "power warming..." } + else + self[:warming] = false + self[:cooling] = true + logger.debug { "power cooling..." } + end + + schedule.in(3.seconds) { power? } + # Signal processing + elsif (data[-2] & 0b1000000) > 0 + schedule.in(3.seconds) { power? } + else # We are in a stable state! + if power_target = @power_target + if self[:power] == power_target + @power_target = nil + else # We are in an undesirable state and will try to correct it + logger.debug { "NEC projector in an undesirable power state... (Correcting)" } + power(power_target) + end + else + logger.debug { "NEC projector is in a good power state..." } + self[:warming] = false + self[:cooling] = false + # Ensure the input is in the correct state unless the lamp is off + status_input if self[:power]?.try(&.as_bool) # calls status mute + end + end + + logger.debug { "Current state {power: #{self[:power]}, warming: #{self[:warming]}, cooling: #{self[:cooling]}}" } + task.try(&.success) + end + # NEC has different values for the input status when compared to input selection INPUT_MAP = { 0x01 => { From c0869ac84a6b9d850a5691c6cfb0d926bf9c766c Mon Sep 17 00:00:00 2001 From: Philip Kheav Date: Tue, 1 Dec 2020 18:17:56 +0000 Subject: [PATCH 13/27] fix(nec_proj): correct checksum calculation --- drivers/nec/np_series.cr | 97 ++++++++++++++++++++++++++++++----- drivers/nec/np_series_spec.cr | 7 +-- 2 files changed, 87 insertions(+), 17 deletions(-) diff --git a/drivers/nec/np_series.cr b/drivers/nec/np_series.cr index a9a2ee82019..ded7ac93f72 100644 --- a/drivers/nec/np_series.cr +++ b/drivers/nec/np_series.cr @@ -54,9 +54,7 @@ class Nec::Projector < PlaceOS::Driver end def connected - schedule.every(50.seconds, true) do - do_poll - end + schedule.every(50.seconds, true) { do_poll } end def disconnected @@ -196,18 +194,17 @@ class Nec::Projector < PlaceOS::Driver end private def check_checksum(data : Bytes) - # Loop through the second to the third last element - checksum = data[1..-3].reduce { |a, b| a ^ b } - # Check the checksum equals the second last element - logger.debug { "Error: checksum should be 0x#{checksum.to_s(16)}" } unless checksum == data[-2] - checksum == data[-2] + checksum = data.sum(0) & 0xFF + logger.debug { "Error: checksum should be 0x#{checksum.to_s(16)}" } unless result = checksum == data[-2] + result end private def send_checksum(command, **options) command = command.delete(' ').hexbytes if command.is_a?(String) req = Bytes.new(command.size + 1) req.copy_from(command) - req[-1] = command.reduce(&.+) + req[-1] = (command.sum(0) & 0xFF).to_u8 + logger.debug { "Nec proj sending #{req.hexstring}"} send(req, **options) { |data, task| process_response(data, task, req) } end @@ -261,8 +258,8 @@ class Nec::Projector < PlaceOS::Driver case data[1] when 0x81 return process_power_status(data, task) - when 0x88 # TODO - # return process_error_status(data, task) + when 0x88 + return process_error_status(data, task) when 0x85 # Return if we can't work out what was requested initially return task.try(&.success) unless req @@ -291,9 +288,8 @@ class Nec::Projector < PlaceOS::Driver # how to play this? # TODO:: process volume control return task.try(&.success) - when 0x8A # TODO - # process_projector_information(data, req) - return task.try(&.success) + when 0x8A + return process_projector_information(data, task) when 0xB1 # This is the audio switch command # TODO:: data[-2] == 0:Normal, 1:Error @@ -433,4 +429,77 @@ class Nec::Projector < PlaceOS::Driver end task.try(&.success) end + + # Provide all the error information required + ERROR_CODES = [{ + 0b1 => "Lamp cover error", + 0b10 => "Temperature error (Bimetal)", + #0b100 == not used + 0b1000 => "Fan Error", + 0b10000 => "Fan Error", + 0b100000 => "Power Error", + 0b1000000 => "Lamp Error", + 0b10000000 => "Lamp has reached its end of life" + }, { + 0b1 => "Lamp has been used beyond its limit", + 0b10 => "Formatter error", + 0b100 => "Lamp no.2 Error" + }, { + #0b1 => "not used", + 0b10 => "FPGA error", + 0b100 => "Temperature error (Sensor)", + 0b1000 => "Lamp housing error", + 0b10000 => "Lamp data error", + 0b100000 => "Mirror cover error", + 0b1000000 => "Lamp no.2 has reached its end of life", + 0b10000000 => "Lamp no.2 has been used beyond its limit" + }, { + 0b1 => "Lamp no.2 housing error", + 0b10 => "Lamp no.2 data error", + 0b100 => "High temperature due to dust pile-up", + 0b1000 => "A foreign object sensor error" + }] + private def process_error_status(data, task) + logger.debug { "-- NEC projector sent a response to an error status command" } + errors = [] of String + error = data[5..8] + error.each_index do |byte_no| + if error[byte_no] > 0 # run throught each byte + ERROR_CODES[byte_no].each_key do |key| # if error indicated run though each key + if (key & error[byte_no]) > 0 # check individual bits + errors << ERROR_CODES[byte_no][key] # add errors to the error list + end + end + end + end + self[:error] = errors + task.try(&.success) + end + + # Process projector information response + # lamp1 hours + filter hours + private def process_projector_information(data, task) + logger.debug { "-- NEC projector sent a response to a projector information command" } + + lamp = 0 + filter = 0 + + # get lamp usage + shift = 0 + data[87..90].each do |byte| + lamp += byte << shift + shift += 8 + end + + # get filter usage + shift = 0 + data[91..94].each do |byte| + filter += byte << shift + shift += 8 + end + + self[:lamp_usage] = lamp / 3600 # Lamp usage in hours + self[:filter_usage] = filter / 3600 + task.try(&.success) + end end diff --git a/drivers/nec/np_series_spec.cr b/drivers/nec/np_series_spec.cr index a4f0eeb6c15..2534e03b773 100644 --- a/drivers/nec/np_series_spec.cr +++ b/drivers/nec/np_series_spec.cr @@ -1,7 +1,8 @@ DriverSpecs.mock_driver "Nec::Projector" do - # # do_poll - # # power? - # should_send("\x010*0A06\x0201D6\x03\x1F\x0D") + # do_poll + # power? + # exec(:power?) + should_send("\x00\x81\x00\x00\x00\x81\x02") # responds("\x0100*B12\x020200D60000040001\x03\x1F\x0D") # status[:power].should eq(true) # # mute_status From ad88cb1b62fdbb1e9b4e5e04cc357e22327af423 Mon Sep 17 00:00:00 2001 From: Philip Kheav Date: Tue, 1 Dec 2020 23:31:59 +0000 Subject: [PATCH 14/27] feat(nec_proj): add first passing spec --- drivers/nec/np_series.cr | 30 ++++++++++++++---------------- drivers/nec/np_series_spec.cr | 6 +++--- 2 files changed, 17 insertions(+), 19 deletions(-) diff --git a/drivers/nec/np_series.cr b/drivers/nec/np_series.cr index ded7ac93f72..90641b05c5c 100644 --- a/drivers/nec/np_series.cr +++ b/drivers/nec/np_series.cr @@ -36,12 +36,9 @@ class Nec::Projector < PlaceOS::Driver @volume_min : Int32 = 0 @volume_max : Int32 = 63 - DELIMITER = 0x0D_u8 - def on_load # Communication settings queue.delay = 100.milliseconds - transport.tokenizer = Tokenizer.new(Bytes[DELIMITER]) self[:error] = [] of String on_update end @@ -54,7 +51,7 @@ class Nec::Projector < PlaceOS::Driver end def connected - schedule.every(50.seconds, true) { do_poll } + # schedule.every(50.seconds, true) { do_poll } end def disconnected @@ -108,7 +105,7 @@ class Nec::Projector < PlaceOS::Driver # D4 = value (lower bits 0 to 63) # D5 = value (higher bits always 00h) - send_checksum(command) + do_send(command) self[:volume] = vol end @@ -149,7 +146,7 @@ class Nec::Projector < PlaceOS::Driver def switch_audio(input : Audio) # C0 == HDMI Audio command = Bytes[0x03, 0xB1, 0x00, 0x00, 0x02, 0xC0, input.value] - send_checksum(command, name: "switch_audio") + do_send(command, name: "switch_audio") end def power(state : Bool) @@ -174,14 +171,14 @@ class Nec::Projector < PlaceOS::Driver end def power?(**options) : Bool - send_checksum(COMMAND[:status_lamp], **options).get + do_send(COMMAND[:status_lamp], **options).get !!self[:power]?.try(&.as_bool) end def switch_to(input : Input) @input_target = input command = Bytes[0x02, 0x03, 0x00, 0x00, 0x02, 0x01, input.value] - send_checksum(command, name: "input") + do_send(command, name: "input") end def do_poll @@ -193,18 +190,18 @@ class Nec::Projector < PlaceOS::Driver end end - private def check_checksum(data : Bytes) - checksum = data.sum(0) & 0xFF - logger.debug { "Error: checksum should be 0x#{checksum.to_s(16)}" } unless result = checksum == data[-2] + private def checksum_valid?(data : Bytes) + checksum = data[0..-2].sum(0) & 0xFF + logger.debug { "Error: checksum should be 0x#{checksum.to_s(16)}" } unless result = checksum == data[-1] result end - private def send_checksum(command, **options) + private def do_send(command, **options) command = command.delete(' ').hexbytes if command.is_a?(String) req = Bytes.new(command.size + 1) req.copy_from(command) req[-1] = (command.sum(0) & 0xFF).to_u8 - logger.debug { "Nec proj sending #{req.hexstring}"} + logger.debug { "Nec proj sending 0x#{req.hexstring}"} send(req, **options) { |data, task| process_response(data, task, req) } end @@ -245,7 +242,7 @@ class Nec::Projector < PlaceOS::Driver end # Verify checksum - unless check_checksum(data) + unless checksum_valid?(data) return task.try(&.abort("-- NEC projector, checksum failed for command: 0x#{req.try(&.hexstring) || "unknown"}")) end @@ -311,7 +308,7 @@ class Nec::Projector < PlaceOS::Driver private def process_power_status(data, task) logger.debug { "-- NEC projector sent a response to a power status command" } - self[:power] = (data[-2] & 0b10) > 0x0 + self[:power] = (data[-2] & 0b10) > 0 # Projector cooling || power on off processing if (data[-2] & 0b100000) > 0 || (data[-2] & 0b10000000) > 0 @@ -341,8 +338,9 @@ class Nec::Projector < PlaceOS::Driver logger.debug { "NEC projector is in a good power state..." } self[:warming] = false self[:cooling] = false + # TODO # Ensure the input is in the correct state unless the lamp is off - status_input if self[:power]?.try(&.as_bool) # calls status mute + # status_input if self[:power]?.try(&.as_bool) # calls status mute end end diff --git a/drivers/nec/np_series_spec.cr b/drivers/nec/np_series_spec.cr index 2534e03b773..a5fe363de24 100644 --- a/drivers/nec/np_series_spec.cr +++ b/drivers/nec/np_series_spec.cr @@ -1,10 +1,10 @@ DriverSpecs.mock_driver "Nec::Projector" do # do_poll # power? - # exec(:power?) + exec(:power?) should_send("\x00\x81\x00\x00\x00\x81\x02") - # responds("\x0100*B12\x020200D60000040001\x03\x1F\x0D") - # status[:power].should eq(true) + responds(Bytes[0x20,0x81,0x01,0x00,0x10,0b_0000_0010,0xB4]) + status[:power].should eq(true) # # mute_status # should_send("\x010*0C06\x02008D\x03\x12\x0D") # responds("\x0100*D12\x0200008D0000000002\x03\x12\x0D") From 5e941a7bc698099019137cb55495d0a65f1b2183 Mon Sep 17 00:00:00 2001 From: Philip Kheav Date: Wed, 2 Dec 2020 12:47:40 +0000 Subject: [PATCH 15/27] feat(nec_proj): add spec for input? --- drivers/nec/np_series.cr | 55 +++++++++++++++++------------------ drivers/nec/np_series_spec.cr | 24 +++++++++++++-- 2 files changed, 49 insertions(+), 30 deletions(-) diff --git a/drivers/nec/np_series.cr b/drivers/nec/np_series.cr index 90641b05c5c..0a96090b428 100644 --- a/drivers/nec/np_series.cr +++ b/drivers/nec/np_series.cr @@ -68,23 +68,23 @@ class Nec::Projector < PlaceOS::Driver mute_picture: "$02,$10,$00,$00,$00,$12", unmute_picture: "$02,$11,$00,$00,$00,$13", mute_audio_cmd: "02 12 00 00 00 14", - unmute_audio: "02 13 00 00 00 15", + unmute_audio_cmd: "02 13 00 00 00 15", mute_onscreen: "02 14 00 00 00 16", unmute_onscreen: "02 15 00 00 00 17", freeze_picture: "$01,$98,$00,$00,$01,$01,$9B", unfreeze_picture: "$01,$98,$00,$00,$01,$02,$9C", - status_lamp: Bytes[0x00, 0x81, 0x00, 0x00, 0x00, 0x81], # Running sense (ret 81) - status_input: "$00,$85,$00,$00,$01,$02,$88", # Input status (ret 85) - status_mute: "00 85 00 00 01 03 89", # MUTE STATUS REQUEST (Check 10H on byte 5) - status_error: "00 88 00 00 00 88", # ERROR STATUS REQUEST (ret 88) - status_model: "00 85 00 00 01 04 8A", # request model name (both of these are related) + lamp?: Bytes[0x00,0x81,0x00,0x00,0x00,0x81], # Running sense (ret 81) + input?: Bytes[0x00,0x85,0x00,0x00,0x01,0x02], # Input status (ret 85) + mute?: Bytes[0x00,0x85,0x00,0x00,0x01,0x03], # MUTE STATUS REQUEST (Check 10H on byte 5) + error?: Bytes[0x00,0x88,0x00,0x00,0x00,0x88], # ERROR STATUS REQUEST (ret 88) + model?: Bytes[0x00,0x85,0x00,0x00,0x01,0x04], # Request model name (both of these are related) - # lamp hours / remaining information - lamp_information: "03 8A 00 00 00 8D", # LAMP INFORMATION REQUEST - filter_information: "03 8A 00 00 00 8D", - projector_information: "03 8A 00 00 00 8D", + # lamp hours / remaining info + lamp_info: "03 8A 00 00 00 8D", # LAMP INFORMATION REQUEST + filter_info: "03 8A 00 00 00 8D", + projector_info: "03 8A 00 00 00 8D", background_black: "$03,$B1,$00,$00,$02,$0B,$01,$C2", # set mute to be a black screen background_blue: "$03,$B1,$00,$00,$02,$0B,$00,$C1", # set mute to be a blue screen @@ -93,7 +93,7 @@ class Nec::Projector < PlaceOS::Driver {% for name, data in COMMAND %} def {{name.id}}(**options) - send(COMMAND[{{name.id}}], **options, name: {{name.id.stringify}}) + do_send(COMMAND[{{name.id.stringify}}], **options, name: {{name.id.stringify}}) end {% end %} @@ -129,7 +129,7 @@ class Nec::Projector < PlaceOS::Driver end def mute_audio(state : Bool) - state ? mute_audio_cmd : unmute_audio + state ? mute_audio_cmd : unmute_audio_cmd end def switch_to(input : Input) @@ -171,7 +171,7 @@ class Nec::Projector < PlaceOS::Driver end def power?(**options) : Bool - do_send(COMMAND[:status_lamp], **options).get + do_send(COMMAND[:lamp?], **options, name: "power?").get !!self[:power]?.try(&.as_bool) end @@ -183,16 +183,15 @@ class Nec::Projector < PlaceOS::Driver def do_poll if power?(priority: 0) - status_input(priority: 0) - status_mute(priority: 0) + mute?(priority: 0) background_black(priority: 0) - lamp_information(priority: 0) + lamp_info(priority: 0) end end private def checksum_valid?(data : Bytes) checksum = data[0..-2].sum(0) & 0xFF - logger.debug { "Error: checksum should be 0x#{checksum.to_s(16)}" } unless result = checksum == data[-1] + logger.debug { "Error: checksum should be 0x#{checksum.to_s(16,true)}" } unless result = checksum == data[-1] result end @@ -275,7 +274,7 @@ class Nec::Projector < PlaceOS::Driver when (0..1) return process_lamp_command(data, task, req) when (0x10..0x15) - status_mute # update mute status's (dry) + mute? # update mute status's (dry) return task.try(&.success) end when .lamp? @@ -286,7 +285,7 @@ class Nec::Projector < PlaceOS::Driver # TODO:: process volume control return task.try(&.success) when 0x8A - return process_projector_information(data, task) + return process_projector_info(data, task) when 0xB1 # This is the audio switch command # TODO:: data[-2] == 0:Normal, 1:Error @@ -340,7 +339,7 @@ class Nec::Projector < PlaceOS::Driver self[:cooling] = false # TODO # Ensure the input is in the correct state unless the lamp is off - # status_input if self[:power]?.try(&.as_bool) # calls status mute + input? if self[:power].as_bool # Calls status mute end end @@ -379,9 +378,9 @@ class Nec::Projector < PlaceOS::Driver if data[-17] == 0x01 # TODO # command[:delay_on_receive] = 3000 # still processing signal - status_input + input? else - status_mute # get mute status one signal has settled + mute? # get mute status one signal has settled end logger.debug { "The input selected was: #{current_input}" } @@ -413,8 +412,8 @@ class Nec::Projector < PlaceOS::Driver private def process_input_switch(data, task, req) logger.debug { "-- NEC projector responded to switch input command" } if data[-2] != 0xFF - status_input # Double check with a status update - return task.try(&.success) + input? # Double check with a status update + return task.try(&.success) end task.try(&.retry("-- NEC projector failed to switch input with command: #{req.try(&.hexstring) || "unknown"}")) end @@ -428,7 +427,7 @@ class Nec::Projector < PlaceOS::Driver task.try(&.success) end - # Provide all the error information required + # Provide all the error info required ERROR_CODES = [{ 0b1 => "Lamp cover error", 0b10 => "Temperature error (Bimetal)", @@ -474,10 +473,10 @@ class Nec::Projector < PlaceOS::Driver task.try(&.success) end - # Process projector information response + # Process projector info response # lamp1 hours + filter hours - private def process_projector_information(data, task) - logger.debug { "-- NEC projector sent a response to a projector information command" } + private def process_projector_info(data, task) + logger.debug { "-- NEC projector sent a response to a projector info command" } lamp = 0 filter = 0 diff --git a/drivers/nec/np_series_spec.cr b/drivers/nec/np_series_spec.cr index a5fe363de24..de7f2c62d61 100644 --- a/drivers/nec/np_series_spec.cr +++ b/drivers/nec/np_series_spec.cr @@ -1,10 +1,30 @@ +# NOTES +# (*1) Projector ID +# (*2) Model code: "xxH" inscription +# (*3) Checksum: "CKS" inscription +# (*4) Response error number +# (*5) Term “RGB” and “COMPUTER” +# (*6) Term “DVI” and “COMPUTER” + DriverSpecs.mock_driver "Nec::Projector" do + p_id = 0x00_u8 + mdlc = 0x10_u8 + # do_poll # power? exec(:power?) - should_send("\x00\x81\x00\x00\x00\x81\x02") - responds(Bytes[0x20,0x81,0x01,0x00,0x10,0b_0000_0010,0xB4]) + should_send(Bytes[0x00,0x81,0x00,0x00,0x00,0x81,0x02]) + responds(Bytes[0x20,0x81,p_id,mdlc,0x10,0b_0000_0010,0xC3]) status[:power].should eq(true) + # input? + should_send(Bytes[0x00,0x85,0x00,0x00,0x01,0x02,0x88]) + responds(Bytes[0x20,0x85,p_id,mdlc,0x10, + # Data, simplified for sanity + # We only care about the ones with 0x + #-17 -15 -14 + 0x00,2,0x01,0x06,5,6,7,8,9,10,11,12,13,14,15,16, + 0x4C]) # Checksum + status[:input].should eq("HDMI") # # mute_status # should_send("\x010*0C06\x02008D\x03\x12\x0D") # responds("\x0100*D12\x0200008D0000000002\x03\x12\x0D") From b5b42b9d52ad285e4874fba647bb3eaaac21a9d9 Mon Sep 17 00:00:00 2001 From: Philip Kheav Date: Wed, 2 Dec 2020 15:21:57 +0000 Subject: [PATCH 16/27] feat(nec_proj): add specs for do_poll --- drivers/nec/np_series.cr | 41 ++++++++------- drivers/nec/np_series_spec.cr | 96 +++++++++++------------------------ 2 files changed, 54 insertions(+), 83 deletions(-) diff --git a/drivers/nec/np_series.cr b/drivers/nec/np_series.cr index 0a96090b428..b0669eded63 100644 --- a/drivers/nec/np_series.cr +++ b/drivers/nec/np_series.cr @@ -51,7 +51,7 @@ class Nec::Projector < PlaceOS::Driver end def connected - # schedule.every(50.seconds, true) { do_poll } + schedule.every(50.seconds, true) { do_poll } end def disconnected @@ -65,15 +65,15 @@ class Nec::Projector < PlaceOS::Driver # Second byte used to detect command type COMMAND = { # Mute controls - mute_picture: "$02,$10,$00,$00,$00,$12", - unmute_picture: "$02,$11,$00,$00,$00,$13", - mute_audio_cmd: "02 12 00 00 00 14", - unmute_audio_cmd: "02 13 00 00 00 15", - mute_onscreen: "02 14 00 00 00 16", - unmute_onscreen: "02 15 00 00 00 17", + mute_picture: Bytes[0x02,0x10,0x00,0x00,0x00,0x12], + unmute_picture: Bytes[0x02,0x11,0x00,0x00,0x00,0x13], + mute_audio_cmd: Bytes[0x02,0x12,0x00,0x00,0x00,0x14], + unmute_audio_cmd: Bytes[0x02,0x13,0x00,0x00,0x00,0x15], + mute_onscreen: Bytes[0x02,0x14,0x00,0x00,0x00,0x16], + unmute_onscreen: Bytes[0x02,0x15,0x00,0x00,0x00,0x17], - freeze_picture: "$01,$98,$00,$00,$01,$01,$9B", - unfreeze_picture: "$01,$98,$00,$00,$01,$02,$9C", + freeze_picture: Bytes[0x01,0x98,0x00,0x00,0x01,0x01], + unfreeze_picture: Bytes[0x01,0x98,0x00,0x00,0x01,0x02], lamp?: Bytes[0x00,0x81,0x00,0x00,0x00,0x81], # Running sense (ret 81) input?: Bytes[0x00,0x85,0x00,0x00,0x01,0x02], # Input status (ret 85) @@ -82,17 +82,18 @@ class Nec::Projector < PlaceOS::Driver model?: Bytes[0x00,0x85,0x00,0x00,0x01,0x04], # Request model name (both of these are related) # lamp hours / remaining info - lamp_info: "03 8A 00 00 00 8D", # LAMP INFORMATION REQUEST - filter_info: "03 8A 00 00 00 8D", - projector_info: "03 8A 00 00 00 8D", + lamp_info: Bytes[0x03,0x8A,0x00,0x00,0x00,0x8D], # LAMP INFORMATION REQUEST + filter_info: Bytes[0x03,0x8A,0x00,0x00,0x00,0x8D], + projector_info: Bytes[0x03,0x8A,0x00,0x00,0x00,0x8D], - background_black: "$03,$B1,$00,$00,$02,$0B,$01,$C2", # set mute to be a black screen - background_blue: "$03,$B1,$00,$00,$02,$0B,$00,$C1", # set mute to be a blue screen - background_logo: "$03,$B1,$00,$00,$02,$0B,$02,$C3" # set mute to be the company logo + background_black: Bytes[0x03,0xB1,0x00,0x00,0x02,0x0B,0x01], # set mute to be a black screen + background_blue: Bytes[0x03,0xB1,0x00,0x00,0x02,0x0B,0x00], # set mute to be a blue screen + background_logo: Bytes[0x03,0xB1,0x00,0x00,0x02,0x0B,0x02] # set mute to be the company logo } {% for name, data in COMMAND %} def {{name.id}}(**options) + pp "sending " + {{name.id.stringify}} + " with" do_send(COMMAND[{{name.id.stringify}}], **options, name: {{name.id.stringify}}) end {% end %} @@ -200,6 +201,7 @@ class Nec::Projector < PlaceOS::Driver req = Bytes.new(command.size + 1) req.copy_from(command) req[-1] = (command.sum(0) & 0xFF).to_u8 + pp "Nec proj sending 0x#{req.hexstring}" logger.debug { "Nec proj sending 0x#{req.hexstring}"} send(req, **options) { |data, task| process_response(data, task, req) } end @@ -227,6 +229,7 @@ class Nec::Projector < PlaceOS::Driver # end private def process_response(data, task, req = nil) + pp "NEC projector sent: 0x#{data.hexstring}" logger.debug { "NEC projector sent: 0x#{data.hexstring}" } # Command failed @@ -379,10 +382,11 @@ class Nec::Projector < PlaceOS::Driver # TODO # command[:delay_on_receive] = 3000 # still processing signal input? - else - mute? # get mute status one signal has settled + else # TODO: figure out if this is needed from old ruby driver + # mute? # get mute status one signal has settled end + pp "The input selected was: #{current_input}" logger.debug { "The input selected was: #{current_input}" } # Notify of bad input selection for debugging @@ -481,6 +485,7 @@ class Nec::Projector < PlaceOS::Driver lamp = 0 filter = 0 + # TODO: cleanup here # get lamp usage shift = 0 data[87..90].each do |byte| @@ -495,6 +500,8 @@ class Nec::Projector < PlaceOS::Driver shift += 8 end + logger.debug { "lamp is #{lamp} filter is #{filter}" } + self[:lamp_usage] = lamp / 3600 # Lamp usage in hours self[:filter_usage] = filter / 3600 task.try(&.success) diff --git a/drivers/nec/np_series_spec.cr b/drivers/nec/np_series_spec.cr index de7f2c62d61..a94157bca94 100644 --- a/drivers/nec/np_series_spec.cr +++ b/drivers/nec/np_series_spec.cr @@ -7,12 +7,11 @@ # (*6) Term “DVI” and “COMPUTER” DriverSpecs.mock_driver "Nec::Projector" do - p_id = 0x00_u8 - mdlc = 0x10_u8 + p_id = 0x00_u8 # Projector ID + mdlc = 0x10_u8 # Model code # do_poll # power? - exec(:power?) should_send(Bytes[0x00,0x81,0x00,0x00,0x00,0x81,0x02]) responds(Bytes[0x20,0x81,p_id,mdlc,0x10,0b_0000_0010,0xC3]) status[:power].should eq(true) @@ -25,67 +24,32 @@ DriverSpecs.mock_driver "Nec::Projector" do 0x00,2,0x01,0x06,5,6,7,8,9,10,11,12,13,14,15,16, 0x4C]) # Checksum status[:input].should eq("HDMI") - # # mute_status - # should_send("\x010*0C06\x02008D\x03\x12\x0D") - # responds("\x0100*D12\x0200008D0000000002\x03\x12\x0D") - # status[:audio_mute].should eq(false) - # # volume_status - # should_send("\x010*0C06\x020062\x03\x6A\x0D") - # responds("\x0100*D12\x020000620000000032\x03\x69\x0D") - # status[:volume].should eq(50) - # # video_input - # should_send("\x010*0C06\x020060\x03\x68\x0D") - # responds("\x0100*D12\x020000600000000011\x03\x6A\x0D") - # status[:input].should eq("Hdmi") - # # audio_input - # should_send("\x010*0C06\x02022E\x03\x1B\x0D") - # responds("\x0100*D12\x0200022E0000000001\x03\x18\x0D") - # status[:audio].should eq("Audio1") - - # exec(:mute_audio) - # should_send("\x010*0E0A\x02008D0001\x03\x62\x0D") - # responds("\x0100*F12\x0200008D0000000001\x03\x13\x0D") - # status[:audio_mute].should eq(true) - # status[:volume].should eq(0) - - # exec(:unmute_audio) - # should_send("\x010*0E0A\x02008D0000\x03\x63\x0D") - # responds("\x0100*F12\x0200008D0000000000\x03\x12\x0D") - # status[:audio_mute].should eq(false) - - # exec(:volume, 25) - # should_send("\x010*0E0A\x0200620019\x03\x13\x0D") - # responds("\x0100*F12\x020000620000640019\x03\x60\x0D") - # should_send("\x010*0A04\x020C\x03\x1D\x0D") - # responds("\x0100*B06\x0200C\x03\x2C\x0D") - # status[:audio_mute].should eq(false) - # status[:volume].should eq(25) - - # exec(:brightness_status) - # should_send("\x010*0C06\x020010\x03\x6F\x0D") - # responds("\x0100*D12\x020000100000000000\x03\x6D\x0D") - # status[:brightness].should eq(0) - - # exec(:brightness, 100) - # should_send("\x010*0E0A\x0200100064\x03\x1C\x0D") - # responds("\x0100*F12\x020000100000640064\x03\x6F\x0D") - # should_send("\x010*0A04\x020C\x03\x1D\x0D") - # responds("\x0100*B06\x0200C\x03\x2C\x0D") - # status[:brightness].should eq(100) - - # exec(:switch_to, "tv") - # should_send("\x010*0E0A\x020060000A\x03\x68\x0D") - # responds("\x0100*F12\x02000060000000000A\x03\x19\x0D") - # status[:input].should eq("Tv") - - # exec(:switch_audio, "audio_2") - # sleep 6 # since switch_to has 6 seconds of delay - # should_send("\x010*0E0A\x02022E0002\x03\x68\x0D") - # responds("\x0100*F12\x0200022E0000000002\x03\x19\x0D") - # status[:audio].should eq("Audio2") - - # exec(:power, false) - # should_send("\x010*0A0C\x02C203D60004\x03\x1D\x0D") - # responds("\x0100*B0E\x0200C203D60004\x03\x18\x0D") - # status[:power].should eq(false) + # mute? + should_send(Bytes[0x00,0x85,0x00,0x00,0x01,0x03,0x89]) + responds(Bytes[0x20,0x85,p_id,mdlc,0x10, + #-17 -16 -15 + 0x00,0x00,0x00,4,5,6,7,8,9,10,11,12,13,14,15,16, + 0x47]) # Checksum + status[:picture_mute].should eq(false) + status[:audio_mute].should eq(false) + status[:onscreen_mute].should eq(false) + status[:mute].should eq(false) + # background_black + should_send(Bytes[0x03,0xB1,0x00,0x00,0x02,0x0B,0x01,0xC2]) + responds(Bytes[0x23,0xB1,p_id,mdlc,0x02,0x0B,0xF1]) + # lamp_info + should_send(Bytes[0x03,0x8A,0x00,0x00,0x00,0x8D,0x1A]) + # 5 for header, 1 for checksum and 98 for data + response = Bytes.new(104) + response.copy_from(Bytes[0x23,0x8A,p_id,mdlc,0x62,0x0B]) # header + # data + # lamp usage + response[87] = 0x24 + # filter usage + response[91] = 0x48 + # checksum + response[-1] = 0x96 + responds(response) + status[:lamp_usage].should eq(0.01) + status[:filter_usage].should eq(0.02) end From 10b67884e6a3e993697dabfac4a2cbc0072499ce Mon Sep 17 00:00:00 2001 From: Philip Kheav Date: Wed, 2 Dec 2020 19:05:28 +0000 Subject: [PATCH 17/27] feat(nec_proj): add proper specs for lamp/filter usage --- drivers/nec/np_series.cr | 20 ++++---------------- drivers/nec/np_series_spec.cr | 25 ++++++++++++++++++++----- 2 files changed, 24 insertions(+), 21 deletions(-) diff --git a/drivers/nec/np_series.cr b/drivers/nec/np_series.cr index b0669eded63..db5feedb687 100644 --- a/drivers/nec/np_series.cr +++ b/drivers/nec/np_series.cr @@ -482,25 +482,13 @@ class Nec::Projector < PlaceOS::Driver private def process_projector_info(data, task) logger.debug { "-- NEC projector sent a response to a projector info command" } - lamp = 0 - filter = 0 - - # TODO: cleanup here # get lamp usage - shift = 0 - data[87..90].each do |byte| - lamp += byte << shift - shift += 8 - end + lamp = data[87..90].each_with_index.sum { |byte, index| byte.to_i << (index * 8) } + logger.debug { "lamp is #{lamp}" } # get filter usage - shift = 0 - data[91..94].each do |byte| - filter += byte << shift - shift += 8 - end - - logger.debug { "lamp is #{lamp} filter is #{filter}" } + filter = data[91..94].each_with_index.sum { |byte, index| byte.to_i << (index * 8) } + logger.debug { "filter is #{filter}" } self[:lamp_usage] = lamp / 3600 # Lamp usage in hours self[:filter_usage] = filter / 3600 diff --git a/drivers/nec/np_series_spec.cr b/drivers/nec/np_series_spec.cr index a94157bca94..7c9f039b437 100644 --- a/drivers/nec/np_series_spec.cr +++ b/drivers/nec/np_series_spec.cr @@ -44,12 +44,27 @@ DriverSpecs.mock_driver "Nec::Projector" do response.copy_from(Bytes[0x23,0x8A,p_id,mdlc,0x62,0x0B]) # header # data # lamp usage - response[87] = 0x24 + response[87] = 0xC0 + response[88] = 0x65 + response[89] = 0x52 # filter usage - response[91] = 0x48 + response[92] = 0xE4 + response[93] = 0x57 # checksum - response[-1] = 0x96 + response[-1] = 0xDC responds(response) - status[:lamp_usage].should eq(0.01) - status[:filter_usage].should eq(0.02) + status[:lamp_usage].should eq(1500) + status[:filter_usage].should eq(1600) + + exec(:volume, 100) + should_send(Bytes[0x03,0x10,0x00,0x00,0x05,0x05,0x00,0x00,0x3F,0x00,0x5C]) + responds(Bytes[0x23,0x10,p_id,mdlc,0x02,0x00,0x45]) + status[:volume].should eq(63) + + exec(:mute) + # mute_picture + should_send(Bytes[0x02,0x10,0x00,0x00,0x00,0x12,0x24]) + responds(Bytes[0x22,0x10,p_id,mdlc,0x32,0x00,0x74]) + # TODO: handle this response + # status[:picture_mute] = true end From 7fba71abad8e4b2d54d48f42e8f4e8f9f5d36d71 Mon Sep 17 00:00:00 2001 From: Philip Kheav Date: Thu, 3 Dec 2020 14:07:43 +0000 Subject: [PATCH 18/27] feat(nec_proj): store responses in enum --- drivers/nec/np_series.cr | 168 +++++++++++++++------------------- drivers/nec/np_series_spec.cr | 25 ++++- 2 files changed, 97 insertions(+), 96 deletions(-) diff --git a/drivers/nec/np_series.cr b/drivers/nec/np_series.cr index db5feedb687..b3fff50ee71 100644 --- a/drivers/nec/np_series.cr +++ b/drivers/nec/np_series.cr @@ -100,14 +100,14 @@ class Nec::Projector < PlaceOS::Driver def volume(vol : Int32) vol = vol.clamp(@volume_min, @volume_max) - # volume base command D1 D2 D3 D4 D5 + CKS + # volume base command D1 D2 D3 D4 D5 command = Bytes[0x03, 0x10, 0x00, 0x00, 0x05, 0x05, 0x00, 0x00, vol, 0x00] # D3 = 00 (absolute vol) or 01 (relative vol) # D4 = value (lower bits 0 to 63) # D5 = value (higher bits always 00h) do_send(command) - self[:volume] = vol + self[:volume] = vol # TODO: remove end # Mutes both audio/video @@ -134,9 +134,10 @@ class Nec::Projector < PlaceOS::Driver end def switch_to(input : Input) - logger.debug { "-- NEC LCD, requested to switch to: #{input}" } - data = MsgType::SetParameter.build(Command::VideoInput, input.value) - send(data, name: "input", delay: 6.seconds) + logger.debug { "-- NEC projector, requested to switch to: #{input}" } + @input_target = input + command = Bytes[0x02, 0x03, 0x00, 0x00, 0x02, 0x01, input.value] + do_send(command, name: "input") end enum Audio @@ -154,12 +155,12 @@ class Nec::Projector < PlaceOS::Driver @power_target = state if state - command = Bytes[0x02, 0x00, 0x00, 0x00, 0x00, 0x02] - send(command, name: "power", timeout: 15.seconds, delay: 1.second) + command = Bytes[0x02, 0x00, 0x00, 0x00, 0x00] + do_send(command, name: "power", timeout: 15.seconds, delay: 1.second) else - command = Bytes[0x02, 0x01, 0x00, 0x00, 0x00, 0x03] + command = Bytes[0x02, 0x01, 0x00, 0x00, 0x00] # Jump ahead of any other queued commands as they are no longer important - send( + do_send( command, name: "power", timeout: 60.seconds, # don't want retries occuring very fast @@ -206,28 +207,29 @@ class Nec::Projector < PlaceOS::Driver send(req, **options) { |data, task| process_response(data, task, req) } end - # Values of first byte in response of successful commands - enum Success - Query = 0x20 - Freeze = 0x21 - Mute = 0x22 - Lamp = 0x23 + # TODO: add responses for freeze commands + enum Response : UInt16 + Power = 8321 # [0x20,0x81] + InputOrMuteQuery = 8325 # [0x20,0x85] + Error = 8328 # [0x20,0x88] + InputSwitch = 8707 # [0x22,0x03] + Lamp = 8704 # [0x22,0x00] + Lamp2 = 8705 # [0x22,0x01] + Mute = 8721 # [0x22,0x11] + Mute2 = 8722 # [0x22,0x12] + Mute3 = 8723 # [0x22,0x13] + Mute4 = 8724 # [0x22,0x14] + Mute5 = 8725 # [0x22,0x15] + VolumeOrImageAdjust = 8721 # [0x23,0x10] + Info = 9098 # [0x23,0x8A] + AudioSwitch = 9137 # [0x23,0xB1] + + def self.from_bytes(response) + value = IO::Memory.new(response[0..1]).read_bytes(UInt16, IO::ByteFormat::BigEndian) + Response.from_value?(value) + end end - # enum Type - # Power = 0x81 - # Error = 0x88 - # Input = 0x03 - # Lamp = 0x00 - # Lamp2 = 0x01 - # Mute = 0x10 - # Mute1 = 0x11 - # Mute2 = 0x12 - # Mute3 = 0x13 - # Mute4 = 0x14 - # Mute5 = 0x15 - # end - private def process_response(data, task, req = nil) pp "NEC projector sent: 0x#{data.hexstring}" logger.debug { "NEC projector sent: 0x#{data.hexstring}" } @@ -250,54 +252,38 @@ class Nec::Projector < PlaceOS::Driver # Only process response if successful # Otherwise return success to prevent retries on commands we were not expecting - return task.try(&.success) unless (s = Success.from_value?(data[0]))# && (type = Type.from_value?(data[1])) - - case s - when .query? - case data[1] - when 0x81 - return process_power_status(data, task) - when 0x88 - return process_error_status(data, task) - when 0x85 - # Return if we can't work out what was requested initially - return task.try(&.success) unless req - case req[-2] - when 0x02 - return process_input_state(data, task) - when 0x03 - return process_mute_state(data, task) - end - end - when .freeze? # TODO - when .mute? - case data[1] - when 0x03 - return process_input_switch(data, task, req) - when (0..1) - return process_lamp_command(data, task, req) - when (0x10..0x15) - mute? # update mute status's (dry) - return task.try(&.success) - end - when .lamp? - case data[1] # TODO: add these cases to Type - when 0x10 - # Picture, Volume, Keystone, Image adjust mode - # how to play this? - # TODO:: process volume control - return task.try(&.success) - when 0x8A - return process_projector_info(data, task) - when 0xB1 - # This is the audio switch command - # TODO:: data[-2] == 0:Normal, 1:Error - # If error do we retry? Or does it mean something else - return task.try(&.success) - end + unless resp = Response.from_bytes(data) + return task.try(&.success("-- NEC projector, no status updates defined for response for command: 0x#{req.try(&.hexstring) || "unknown"}")) end - task.try(&.success("-- NEC projector, no status updates defined for response for command: 0x#{req.try(&.hexstring) || "unknown"}")) + case resp + when .power? + process_power_status(data, task) + when .input_or_mute_query? + # Return if we can't work out what was requested initially + return task.try(&.success) unless req && (2..3).includes?(req[-2]) + process_input_state(data, task) if req[-2] == 2 + process_mute_state(data, task) if req[-2] == 3 + when .error? + process_error_status(data, task) + when .input_switch? + process_input_switch(data, task, req) + when .lamp?, .lamp2? + process_lamp_command(data, task, req) + when .mute?, .mute2?, .mute3?, .mute4?, .mute5? + mute? # update mute status + task.try(&.success) + when .volume_or_image_adjust? + # TODO:: process volume control + task.try(&.success) + when .info? + process_projector_info(data, task) + when .audio_switch? + # This is the audio switch command + # TODO:: data[-2] == 0:Normal, 1:Error + # If error do we retry? Or does it mean something else + task.try(&.success) + end end def received(data, task) @@ -406,10 +392,9 @@ class Nec::Projector < PlaceOS::Driver private def process_mute_state(data, task) logger.debug { "-- NEC projector responded to mute state command" } - self[:picture_mute] = data[-17] == 0x01 + self[:mute] = self[:picture_mute] = data[-17] == 0x01 self[:audio_mute] = data[-16] == 0x01 self[:onscreen_mute] = data[-15] == 0x01 - self[:mute] = data[-17] == 0x01 # Same as picture mute task.try(&.success) end @@ -463,13 +448,14 @@ class Nec::Projector < PlaceOS::Driver private def process_error_status(data, task) logger.debug { "-- NEC projector sent a response to an error status command" } errors = [] of String - error = data[5..8] - error.each_index do |byte_no| - if error[byte_no] > 0 # run throught each byte - ERROR_CODES[byte_no].each_key do |key| # if error indicated run though each key - if (key & error[byte_no]) > 0 # check individual bits - errors << ERROR_CODES[byte_no][key] # add errors to the error list - end + # Run through each byte + data[5..8].each_with_index do |byte, byte_no| + # If there is an error + if byte > 0 + # Go through each individual bit + ERROR_CODES[byte_no].each_key do |bit_check| + # Add the error if the bit corresponding to an error is set + errors.push(ERROR_CODES[byte_no][bit_check]) if (bit_check & byte) > 0 end end end @@ -477,21 +463,15 @@ class Nec::Projector < PlaceOS::Driver task.try(&.success) end - # Process projector info response - # lamp1 hours + filter hours private def process_projector_info(data, task) logger.debug { "-- NEC projector sent a response to a projector info command" } - - # get lamp usage + # Calculate lamp/filter usage in seconds lamp = data[87..90].each_with_index.sum { |byte, index| byte.to_i << (index * 8) } - logger.debug { "lamp is #{lamp}" } - - # get filter usage filter = data[91..94].each_with_index.sum { |byte, index| byte.to_i << (index * 8) } - logger.debug { "filter is #{filter}" } - - self[:lamp_usage] = lamp / 3600 # Lamp usage in hours + # Convert seconds to hours + self[:lamp_usage] = lamp / 3600 self[:filter_usage] = filter / 3600 + logger.debug { "lamp usage is #{self[:lamp_usage]} hours, filter usage is #{self[:filter_usage]} hours" } task.try(&.success) end end diff --git a/drivers/nec/np_series_spec.cr b/drivers/nec/np_series_spec.cr index 7c9f039b437..903b4dc6b93 100644 --- a/drivers/nec/np_series_spec.cr +++ b/drivers/nec/np_series_spec.cr @@ -30,10 +30,10 @@ DriverSpecs.mock_driver "Nec::Projector" do #-17 -16 -15 0x00,0x00,0x00,4,5,6,7,8,9,10,11,12,13,14,15,16, 0x47]) # Checksum + status[:mute].should eq(false) status[:picture_mute].should eq(false) status[:audio_mute].should eq(false) status[:onscreen_mute].should eq(false) - status[:mute].should eq(false) # background_black should_send(Bytes[0x03,0xB1,0x00,0x00,0x02,0x0B,0x01,0xC2]) responds(Bytes[0x23,0xB1,p_id,mdlc,0x02,0x0B,0xF1]) @@ -58,7 +58,7 @@ DriverSpecs.mock_driver "Nec::Projector" do exec(:volume, 100) should_send(Bytes[0x03,0x10,0x00,0x00,0x05,0x05,0x00,0x00,0x3F,0x00,0x5C]) - responds(Bytes[0x23,0x10,p_id,mdlc,0x02,0x00,0x45]) + responds(Bytes[0x23,0x10,p_id,mdlc,0x02,0x00,0x00,0x45]) status[:volume].should eq(63) exec(:mute) @@ -66,5 +66,26 @@ DriverSpecs.mock_driver "Nec::Projector" do should_send(Bytes[0x02,0x10,0x00,0x00,0x00,0x12,0x24]) responds(Bytes[0x22,0x10,p_id,mdlc,0x32,0x00,0x74]) # TODO: handle this response + # status[:mute] = true # status[:picture_mute] = true + # mute_onscreen + should_send(Bytes[0x02,0x14,0x00,0x00,0x00,0x16,0x2C]) + responds(Bytes[0x22,0x14,p_id,mdlc,0x00,0x46]) + # TODO: handle this response + # status[:onscreen_mute] = true + # mute_audio + should_send(Bytes[0x02,0x12,0x00,0x00,0x00,0x14,0x28]) + responds(Bytes[0x22,0x12,p_id,mdlc,0x00,0x44]) + # TODO: handle this response + # status[:audio_mute] = true + # mute? + should_send(Bytes[0x00,0x85,0x00,0x00,0x01,0x03,0x89]) + responds(Bytes[0x20,0x85,p_id,mdlc,0x10, + #-17 -16 -15 + 0x01,0x01,0x01,4,5,6,7,8,9,10,11,12,13,14,15,16, + 0x4A]) # Checksum + status[:mute].should eq(true) + status[:picture_mute].should eq(true) + status[:audio_mute].should eq(true) + status[:onscreen_mute].should eq(true) end From 25b4e788155e92e92f420c208a56904cde651b8f Mon Sep 17 00:00:00 2001 From: Philip Kheav Date: Thu, 3 Dec 2020 14:13:03 +0000 Subject: [PATCH 19/27] chore(nec_proj): run crystal tool format --- drivers/nec/np_series.cr | 134 ++++++++++++++++++++------------------- 1 file changed, 68 insertions(+), 66 deletions(-) diff --git a/drivers/nec/np_series.cr b/drivers/nec/np_series.cr index b3fff50ee71..19daed57d43 100644 --- a/drivers/nec/np_series.cr +++ b/drivers/nec/np_series.cr @@ -28,7 +28,7 @@ class Nec::Projector < PlaceOS::Driver default_settings({ volume_min: 0, - volume_max: 63 + volume_max: 63, }) @power_target : Bool? = nil @@ -65,30 +65,30 @@ class Nec::Projector < PlaceOS::Driver # Second byte used to detect command type COMMAND = { # Mute controls - mute_picture: Bytes[0x02,0x10,0x00,0x00,0x00,0x12], - unmute_picture: Bytes[0x02,0x11,0x00,0x00,0x00,0x13], - mute_audio_cmd: Bytes[0x02,0x12,0x00,0x00,0x00,0x14], - unmute_audio_cmd: Bytes[0x02,0x13,0x00,0x00,0x00,0x15], - mute_onscreen: Bytes[0x02,0x14,0x00,0x00,0x00,0x16], - unmute_onscreen: Bytes[0x02,0x15,0x00,0x00,0x00,0x17], - - freeze_picture: Bytes[0x01,0x98,0x00,0x00,0x01,0x01], - unfreeze_picture: Bytes[0x01,0x98,0x00,0x00,0x01,0x02], - - lamp?: Bytes[0x00,0x81,0x00,0x00,0x00,0x81], # Running sense (ret 81) - input?: Bytes[0x00,0x85,0x00,0x00,0x01,0x02], # Input status (ret 85) - mute?: Bytes[0x00,0x85,0x00,0x00,0x01,0x03], # MUTE STATUS REQUEST (Check 10H on byte 5) - error?: Bytes[0x00,0x88,0x00,0x00,0x00,0x88], # ERROR STATUS REQUEST (ret 88) - model?: Bytes[0x00,0x85,0x00,0x00,0x01,0x04], # Request model name (both of these are related) + mute_picture: Bytes[0x02, 0x10, 0x00, 0x00, 0x00, 0x12], + unmute_picture: Bytes[0x02, 0x11, 0x00, 0x00, 0x00, 0x13], + mute_audio_cmd: Bytes[0x02, 0x12, 0x00, 0x00, 0x00, 0x14], + unmute_audio_cmd: Bytes[0x02, 0x13, 0x00, 0x00, 0x00, 0x15], + mute_onscreen: Bytes[0x02, 0x14, 0x00, 0x00, 0x00, 0x16], + unmute_onscreen: Bytes[0x02, 0x15, 0x00, 0x00, 0x00, 0x17], + + freeze_picture: Bytes[0x01, 0x98, 0x00, 0x00, 0x01, 0x01], + unfreeze_picture: Bytes[0x01, 0x98, 0x00, 0x00, 0x01, 0x02], + + lamp?: Bytes[0x00, 0x81, 0x00, 0x00, 0x00, 0x81], # Running sense (ret 81) + input?: Bytes[0x00, 0x85, 0x00, 0x00, 0x01, 0x02], # Input status (ret 85) + mute?: Bytes[0x00, 0x85, 0x00, 0x00, 0x01, 0x03], # MUTE STATUS REQUEST (Check 10H on byte 5) + error?: Bytes[0x00, 0x88, 0x00, 0x00, 0x00, 0x88], # ERROR STATUS REQUEST (ret 88) + model?: Bytes[0x00, 0x85, 0x00, 0x00, 0x01, 0x04], # Request model name (both of these are related) # lamp hours / remaining info - lamp_info: Bytes[0x03,0x8A,0x00,0x00,0x00,0x8D], # LAMP INFORMATION REQUEST - filter_info: Bytes[0x03,0x8A,0x00,0x00,0x00,0x8D], - projector_info: Bytes[0x03,0x8A,0x00,0x00,0x00,0x8D], + lamp_info: Bytes[0x03, 0x8A, 0x00, 0x00, 0x00, 0x8D], # LAMP INFORMATION REQUEST + filter_info: Bytes[0x03, 0x8A, 0x00, 0x00, 0x00, 0x8D], + projector_info: Bytes[0x03, 0x8A, 0x00, 0x00, 0x00, 0x8D], - background_black: Bytes[0x03,0xB1,0x00,0x00,0x02,0x0B,0x01], # set mute to be a black screen - background_blue: Bytes[0x03,0xB1,0x00,0x00,0x02,0x0B,0x00], # set mute to be a blue screen - background_logo: Bytes[0x03,0xB1,0x00,0x00,0x02,0x0B,0x02] # set mute to be the company logo + background_black: Bytes[0x03, 0xB1, 0x00, 0x00, 0x02, 0x0B, 0x01], # set mute to be a black screen + background_blue: Bytes[0x03, 0xB1, 0x00, 0x00, 0x02, 0x0B, 0x00], # set mute to be a blue screen + background_logo: Bytes[0x03, 0xB1, 0x00, 0x00, 0x02, 0x0B, 0x02], # set mute to be the company logo } {% for name, data in COMMAND %} @@ -193,7 +193,7 @@ class Nec::Projector < PlaceOS::Driver private def checksum_valid?(data : Bytes) checksum = data[0..-2].sum(0) & 0xFF - logger.debug { "Error: checksum should be 0x#{checksum.to_s(16,true)}" } unless result = checksum == data[-1] + logger.debug { "Error: checksum should be 0x#{checksum.to_s(16, true)}" } unless result = checksum == data[-1] result end @@ -203,26 +203,26 @@ class Nec::Projector < PlaceOS::Driver req.copy_from(command) req[-1] = (command.sum(0) & 0xFF).to_u8 pp "Nec proj sending 0x#{req.hexstring}" - logger.debug { "Nec proj sending 0x#{req.hexstring}"} + logger.debug { "Nec proj sending 0x#{req.hexstring}" } send(req, **options) { |data, task| process_response(data, task, req) } end # TODO: add responses for freeze commands enum Response : UInt16 - Power = 8321 # [0x20,0x81] - InputOrMuteQuery = 8325 # [0x20,0x85] - Error = 8328 # [0x20,0x88] - InputSwitch = 8707 # [0x22,0x03] - Lamp = 8704 # [0x22,0x00] - Lamp2 = 8705 # [0x22,0x01] - Mute = 8721 # [0x22,0x11] - Mute2 = 8722 # [0x22,0x12] - Mute3 = 8723 # [0x22,0x13] - Mute4 = 8724 # [0x22,0x14] - Mute5 = 8725 # [0x22,0x15] + Power = 8321 # [0x20,0x81] + InputOrMuteQuery = 8325 # [0x20,0x85] + Error = 8328 # [0x20,0x88] + InputSwitch = 8707 # [0x22,0x03] + Lamp = 8704 # [0x22,0x00] + Lamp2 = 8705 # [0x22,0x01] + Mute = 8721 # [0x22,0x11] + Mute2 = 8722 # [0x22,0x12] + Mute3 = 8723 # [0x22,0x13] + Mute4 = 8724 # [0x22,0x14] + Mute5 = 8725 # [0x22,0x15] VolumeOrImageAdjust = 8721 # [0x23,0x10] - Info = 9098 # [0x23,0x8A] - AudioSwitch = 9137 # [0x23,0xB1] + Info = 9098 # [0x23,0x8A] + AudioSwitch = 9137 # [0x23,0xB1] def self.from_bytes(response) value = IO::Memory.new(response[0..1]).read_bytes(UInt16, IO::ByteFormat::BigEndian) @@ -307,7 +307,7 @@ class Nec::Projector < PlaceOS::Driver else self[:warming] = false self[:cooling] = true - logger.debug { "power cooling..." } + logger.debug { "power cooling..." } end schedule.in(3.seconds) { power? } @@ -336,7 +336,7 @@ class Nec::Projector < PlaceOS::Driver task.try(&.success) end - # NEC has different values for the input status when compared to input selection + # NEC has different values for the input status when compared to input selection INPUT_MAP = { 0x01 => { 0x01 => Input::VGA, @@ -345,19 +345,20 @@ class Nec::Projector < PlaceOS::Driver 0x06 => Input::HDMI, 0x07 => Input::Viewer, 0x21 => Input::HDMI, - 0x22 => Input::DisplayPort + 0x22 => Input::DisplayPort, }, 0x02 => { 0x01 => Input::RGBHV, 0x04 => Input::Component2, 0x06 => Input::HDMI2, 0x07 => Input::LAN, - 0x21 => Input::HDMI2 + 0x21 => Input::HDMI2, }, 0x03 => { - 0x04 => Input::Component - } + 0x04 => Input::Component, + }, } + private def process_input_state(data, task) return task.try(&.success) unless self[:power]?.try(&.as_bool) && (first = INPUT_MAP[data[-15]]) @@ -418,33 +419,34 @@ class Nec::Projector < PlaceOS::Driver # Provide all the error info required ERROR_CODES = [{ - 0b1 => "Lamp cover error", - 0b10 => "Temperature error (Bimetal)", - #0b100 == not used - 0b1000 => "Fan Error", - 0b10000 => "Fan Error", - 0b100000 => "Power Error", - 0b1000000 => "Lamp Error", - 0b10000000 => "Lamp has reached its end of life" + 0b1 => "Lamp cover error", + 0b10 => "Temperature error (Bimetal)", + # 0b100 => not used + 0b1000 => "Fan Error", + 0b10000 => "Fan Error", + 0b100000 => "Power Error", + 0b1000000 => "Lamp Error", + 0b10000000 => "Lamp has reached its end of life", }, { - 0b1 => "Lamp has been used beyond its limit", - 0b10 => "Formatter error", - 0b100 => "Lamp no.2 Error" + 0b1 => "Lamp has been used beyond its limit", + 0b10 => "Formatter error", + 0b100 => "Lamp no.2 Error", }, { - #0b1 => "not used", - 0b10 => "FPGA error", - 0b100 => "Temperature error (Sensor)", - 0b1000 => "Lamp housing error", - 0b10000 => "Lamp data error", - 0b100000 => "Mirror cover error", - 0b1000000 => "Lamp no.2 has reached its end of life", - 0b10000000 => "Lamp no.2 has been used beyond its limit" + # 0b1 => "not used" + 0b10 => "FPGA error", + 0b100 => "Temperature error (Sensor)", + 0b1000 => "Lamp housing error", + 0b10000 => "Lamp data error", + 0b100000 => "Mirror cover error", + 0b1000000 => "Lamp no.2 has reached its end of life", + 0b10000000 => "Lamp no.2 has been used beyond its limit", }, { - 0b1 => "Lamp no.2 housing error", - 0b10 => "Lamp no.2 data error", - 0b100 => "High temperature due to dust pile-up", - 0b1000 => "A foreign object sensor error" + 0b1 => "Lamp no.2 housing error", + 0b10 => "Lamp no.2 data error", + 0b100 => "High temperature due to dust pile-up", + 0b1000 => "A foreign object sensor error", }] + private def process_error_status(data, task) logger.debug { "-- NEC projector sent a response to an error status command" } errors = [] of String From 59aa07dd89d83ab31599a838f66df91ebcf24741 Mon Sep 17 00:00:00 2001 From: Philip Kheav Date: Thu, 3 Dec 2020 15:01:04 +0000 Subject: [PATCH 20/27] chore(nec_port): use from_bytes? instead --- drivers/nec/np_series.cr | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/drivers/nec/np_series.cr b/drivers/nec/np_series.cr index 19daed57d43..be4c60f3a7d 100644 --- a/drivers/nec/np_series.cr +++ b/drivers/nec/np_series.cr @@ -224,7 +224,7 @@ class Nec::Projector < PlaceOS::Driver Info = 9098 # [0x23,0x8A] AudioSwitch = 9137 # [0x23,0xB1] - def self.from_bytes(response) + def self.from_bytes?(response) value = IO::Memory.new(response[0..1]).read_bytes(UInt16, IO::ByteFormat::BigEndian) Response.from_value?(value) end @@ -252,7 +252,7 @@ class Nec::Projector < PlaceOS::Driver # Only process response if successful # Otherwise return success to prevent retries on commands we were not expecting - unless resp = Response.from_bytes(data) + unless resp = Response.from_bytes?(data) return task.try(&.success("-- NEC projector, no status updates defined for response for command: 0x#{req.try(&.hexstring) || "unknown"}")) end From 78b2f92106dee7da82e670e7d2dfb35cf4e022c6 Mon Sep 17 00:00:00 2001 From: Philip Kheav Date: Thu, 3 Dec 2020 17:04:41 +0000 Subject: [PATCH 21/27] feat(nec_proj): add volume response processing --- drivers/nec/np_series.cr | 6 +++--- drivers/nec/np_series_spec.cr | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/drivers/nec/np_series.cr b/drivers/nec/np_series.cr index be4c60f3a7d..df34ea0808f 100644 --- a/drivers/nec/np_series.cr +++ b/drivers/nec/np_series.cr @@ -107,7 +107,6 @@ class Nec::Projector < PlaceOS::Driver # D5 = value (higher bits always 00h) do_send(command) - self[:volume] = vol # TODO: remove end # Mutes both audio/video @@ -220,7 +219,7 @@ class Nec::Projector < PlaceOS::Driver Mute3 = 8723 # [0x22,0x13] Mute4 = 8724 # [0x22,0x14] Mute5 = 8725 # [0x22,0x15] - VolumeOrImageAdjust = 8721 # [0x23,0x10] + VolumeOrImageAdjust = 8976 # [0x23,0x10] Info = 9098 # [0x23,0x8A] AudioSwitch = 9137 # [0x23,0xB1] @@ -274,7 +273,8 @@ class Nec::Projector < PlaceOS::Driver mute? # update mute status task.try(&.success) when .volume_or_image_adjust? - # TODO:: process volume control + self[:volume] = req[-3] if req && data[-3] == 5 && data[-2] == 0 + # We don't care about image adjust task.try(&.success) when .info? process_projector_info(data, task) diff --git a/drivers/nec/np_series_spec.cr b/drivers/nec/np_series_spec.cr index 903b4dc6b93..a2c7bd990c0 100644 --- a/drivers/nec/np_series_spec.cr +++ b/drivers/nec/np_series_spec.cr @@ -58,7 +58,7 @@ DriverSpecs.mock_driver "Nec::Projector" do exec(:volume, 100) should_send(Bytes[0x03,0x10,0x00,0x00,0x05,0x05,0x00,0x00,0x3F,0x00,0x5C]) - responds(Bytes[0x23,0x10,p_id,mdlc,0x02,0x00,0x00,0x45]) + responds(Bytes[0x23,0x10,p_id,mdlc,0x05,0x00,0x48]) status[:volume].should eq(63) exec(:mute) From f13a4a545e77be1ac68580176bd7dfd3e6db9242 Mon Sep 17 00:00:00 2001 From: Philip Kheav Date: Thu, 3 Dec 2020 17:36:17 +0000 Subject: [PATCH 22/27] feat(nec_proj): add spec for switch_audio --- drivers/nec/np_series.cr | 27 +++++++++++++-------------- drivers/nec/np_series_spec.cr | 5 +++++ 2 files changed, 18 insertions(+), 14 deletions(-) diff --git a/drivers/nec/np_series.cr b/drivers/nec/np_series.cr index df34ea0808f..fe7e016cdd9 100644 --- a/drivers/nec/np_series.cr +++ b/drivers/nec/np_series.cr @@ -86,6 +86,7 @@ class Nec::Projector < PlaceOS::Driver filter_info: Bytes[0x03, 0x8A, 0x00, 0x00, 0x00, 0x8D], projector_info: Bytes[0x03, 0x8A, 0x00, 0x00, 0x00, 0x8D], + # TODO: figure out where these are in the docs as they conflict with audio_switch background_black: Bytes[0x03, 0xB1, 0x00, 0x00, 0x02, 0x0B, 0x01], # set mute to be a black screen background_blue: Bytes[0x03, 0xB1, 0x00, 0x00, 0x02, 0x0B, 0x00], # set mute to be a blue screen background_logo: Bytes[0x03, 0xB1, 0x00, 0x00, 0x02, 0x0B, 0x02], # set mute to be the company logo @@ -117,6 +118,7 @@ class Nec::Projector < PlaceOS::Driver ) mute_video(state) if layer.video? || layer.audio_video? mute_audio(state) if layer.audio? || layer.audio_video? + mute? # TODO: remove end def mute_video(state : Bool) @@ -141,7 +143,7 @@ class Nec::Projector < PlaceOS::Driver enum Audio HDMI - VGA + VGA # Computer in docs end def switch_audio(input : Audio) @@ -214,10 +216,11 @@ class Nec::Projector < PlaceOS::Driver InputSwitch = 8707 # [0x22,0x03] Lamp = 8704 # [0x22,0x00] Lamp2 = 8705 # [0x22,0x01] - Mute = 8721 # [0x22,0x11] - Mute2 = 8722 # [0x22,0x12] + PictureMute = 8720 # [0x22,0x10] + Mute1 = 8721 # [0x22,0x11] + AudioMute = 8722 # [0x22,0x12] Mute3 = 8723 # [0x22,0x13] - Mute4 = 8724 # [0x22,0x14] + OnscreenMute = 8724 # [0x22,0x14] Mute5 = 8725 # [0x22,0x15] VolumeOrImageAdjust = 8976 # [0x23,0x10] Info = 9098 # [0x23,0x8A] @@ -269,8 +272,8 @@ class Nec::Projector < PlaceOS::Driver process_input_switch(data, task, req) when .lamp?, .lamp2? process_lamp_command(data, task, req) - when .mute?, .mute2?, .mute3?, .mute4?, .mute5? - mute? # update mute status + when .picture_mute?, .mute1?, .audio_mute?, .mute3?, .onscreen_mute?, .mute5? + # TODO task.try(&.success) when .volume_or_image_adjust? self[:volume] = req[-3] if req && data[-3] == 5 && data[-2] == 0 @@ -278,10 +281,8 @@ class Nec::Projector < PlaceOS::Driver task.try(&.success) when .info? process_projector_info(data, task) - when .audio_switch? - # This is the audio switch command - # TODO:: data[-2] == 0:Normal, 1:Error - # If error do we retry? Or does it mean something else + when .audio_switch? # TODO: also seems to the seem as setting background response + self[:audio_input] = Audio.from_value(data[-2]) if data[-3] == 0xC0 task.try(&.success) end end @@ -324,10 +325,8 @@ class Nec::Projector < PlaceOS::Driver end else logger.debug { "NEC projector is in a good power state..." } - self[:warming] = false - self[:cooling] = false - # TODO - # Ensure the input is in the correct state unless the lamp is off + self[:warming] = self[:cooling] = false + # Ensure the input is in the correct state if power/lamp is on input? if self[:power].as_bool # Calls status mute end end diff --git a/drivers/nec/np_series_spec.cr b/drivers/nec/np_series_spec.cr index a2c7bd990c0..dafb32fafda 100644 --- a/drivers/nec/np_series_spec.cr +++ b/drivers/nec/np_series_spec.cr @@ -88,4 +88,9 @@ DriverSpecs.mock_driver "Nec::Projector" do status[:picture_mute].should eq(true) status[:audio_mute].should eq(true) status[:onscreen_mute].should eq(true) + + exec(:switch_audio, "VGA") + should_send(Bytes[0x03,0xB1,0x00,0x00,0x02,0xC0,0x01,0x77]) + responds(Bytes[0x23,0xB1,p_id,mdlc,0xC0,0x01,0xA5]) + status[:audio_input].should eq("VGA") end From b1e19c278b36c9a3a8fdd18692d4131facad6acc Mon Sep 17 00:00:00 2001 From: Philip Kheav Date: Tue, 8 Dec 2020 10:58:23 +0000 Subject: [PATCH 23/27] feat(nec_proj): add more mute response handling --- drivers/nec/np_series.cr | 61 ++++++++++++++++------------------- drivers/nec/np_series_spec.cr | 21 +++--------- 2 files changed, 32 insertions(+), 50 deletions(-) diff --git a/drivers/nec/np_series.cr b/drivers/nec/np_series.cr index fe7e016cdd9..3b0e32a6dd5 100644 --- a/drivers/nec/np_series.cr +++ b/drivers/nec/np_series.cr @@ -118,7 +118,6 @@ class Nec::Projector < PlaceOS::Driver ) mute_video(state) if layer.video? || layer.audio_video? mute_audio(state) if layer.audio? || layer.audio_video? - mute? # TODO: remove end def mute_video(state : Bool) @@ -216,12 +215,12 @@ class Nec::Projector < PlaceOS::Driver InputSwitch = 8707 # [0x22,0x03] Lamp = 8704 # [0x22,0x00] Lamp2 = 8705 # [0x22,0x01] - PictureMute = 8720 # [0x22,0x10] - Mute1 = 8721 # [0x22,0x11] - AudioMute = 8722 # [0x22,0x12] - Mute3 = 8723 # [0x22,0x13] - OnscreenMute = 8724 # [0x22,0x14] - Mute5 = 8725 # [0x22,0x15] + PictureMuteOn = 8720 # [0x22,0x10] + PictureMuteOff = 8721 # [0x22,0x11] + AudioMuteOn = 8722 # [0x22,0x12] + AudioMuteOff = 8723 # [0x22,0x13] + OnscreenMuteOn = 8724 # [0x22,0x14] + OnscreenMuteOff = 8725 # [0x22,0x15] VolumeOrImageAdjust = 8976 # [0x23,0x10] Info = 9098 # [0x23,0x8A] AudioSwitch = 9137 # [0x23,0xB1] @@ -260,31 +259,34 @@ class Nec::Projector < PlaceOS::Driver case resp when .power? - process_power_status(data, task) + process_power_status(data) when .input_or_mute_query? # Return if we can't work out what was requested initially return task.try(&.success) unless req && (2..3).includes?(req[-2]) - process_input_state(data, task) if req[-2] == 2 - process_mute_state(data, task) if req[-2] == 3 + process_input_state(data) if req[-2] == 2 + process_mute_state(data) if req[-2] == 3 when .error? - process_error_status(data, task) + process_error_status(data) when .input_switch? - process_input_switch(data, task, req) + return process_input_switch(data, task, req) when .lamp?, .lamp2? - process_lamp_command(data, task, req) - when .picture_mute?, .mute1?, .audio_mute?, .mute3?, .onscreen_mute?, .mute5? - # TODO - task.try(&.success) + process_lamp_command(data, req) + when .picture_mute_on?, .picture_mute_off? + self[:mute] = self[:picture_mute] = resp.picture_mute_on? + when .audio_mute_on?, .audio_mute_off? + self[:audio_mute] = resp.audio_mute_on? + when .onscreen_mute_on?, .onscreen_mute_off? + self[:onscreen_mute] = resp.onscreen_mute_on? when .volume_or_image_adjust? self[:volume] = req[-3] if req && data[-3] == 5 && data[-2] == 0 # We don't care about image adjust - task.try(&.success) when .info? - process_projector_info(data, task) + process_projector_info(data) when .audio_switch? # TODO: also seems to the seem as setting background response self[:audio_input] = Audio.from_value(data[-2]) if data[-3] == 0xC0 - task.try(&.success) end + + task.try(&.success) end def received(data, task) @@ -294,7 +296,7 @@ class Nec::Projector < PlaceOS::Driver # Process the lamp status response # Intimately entwined with the power power command # (as we need to control ensure we are in the correct target state) - private def process_power_status(data, task) + private def process_power_status(data) logger.debug { "-- NEC projector sent a response to a power status command" } self[:power] = (data[-2] & 0b10) > 0 @@ -332,7 +334,6 @@ class Nec::Projector < PlaceOS::Driver end logger.debug { "Current state {power: #{self[:power]}, warming: #{self[:warming]}, cooling: #{self[:cooling]}}" } - task.try(&.success) end # NEC has different values for the input status when compared to input selection @@ -358,8 +359,8 @@ class Nec::Projector < PlaceOS::Driver }, } - private def process_input_state(data, task) - return task.try(&.success) unless self[:power]?.try(&.as_bool) && (first = INPUT_MAP[data[-15]]) + private def process_input_state(data) + return unless self[:power]?.try(&.as_bool) && (first = INPUT_MAP[data[-15]]) logger.debug { "-- NEC projector sent a response to an input state command" } @@ -386,16 +387,13 @@ class Nec::Projector < PlaceOS::Driver switch_to(input_target) end end - - task.try(&.success) end - private def process_mute_state(data, task) + private def process_mute_state(data) logger.debug { "-- NEC projector responded to mute state command" } self[:mute] = self[:picture_mute] = data[-17] == 0x01 self[:audio_mute] = data[-16] == 0x01 self[:onscreen_mute] = data[-15] == 0x01 - task.try(&.success) end private def process_input_switch(data, task, req) @@ -407,13 +405,12 @@ class Nec::Projector < PlaceOS::Driver task.try(&.retry("-- NEC projector failed to switch input with command: #{req.try(&.hexstring) || "unknown"}")) end - private def process_lamp_command(data, task, req) + private def process_lamp_command(data, req) logger.debug { "-- NEC projector sent a response to a power command" } # Ensure a change of power state was the last command sent if req && (0..1).includes?(req[1]) power? # Queues the status power command end - task.try(&.success) end # Provide all the error info required @@ -446,7 +443,7 @@ class Nec::Projector < PlaceOS::Driver 0b1000 => "A foreign object sensor error", }] - private def process_error_status(data, task) + private def process_error_status(data) logger.debug { "-- NEC projector sent a response to an error status command" } errors = [] of String # Run through each byte @@ -461,10 +458,9 @@ class Nec::Projector < PlaceOS::Driver end end self[:error] = errors - task.try(&.success) end - private def process_projector_info(data, task) + private def process_projector_info(data) logger.debug { "-- NEC projector sent a response to a projector info command" } # Calculate lamp/filter usage in seconds lamp = data[87..90].each_with_index.sum { |byte, index| byte.to_i << (index * 8) } @@ -473,6 +469,5 @@ class Nec::Projector < PlaceOS::Driver self[:lamp_usage] = lamp / 3600 self[:filter_usage] = filter / 3600 logger.debug { "lamp usage is #{self[:lamp_usage]} hours, filter usage is #{self[:filter_usage]} hours" } - task.try(&.success) end end diff --git a/drivers/nec/np_series_spec.cr b/drivers/nec/np_series_spec.cr index dafb32fafda..309024530d7 100644 --- a/drivers/nec/np_series_spec.cr +++ b/drivers/nec/np_series_spec.cr @@ -65,29 +65,16 @@ DriverSpecs.mock_driver "Nec::Projector" do # mute_picture should_send(Bytes[0x02,0x10,0x00,0x00,0x00,0x12,0x24]) responds(Bytes[0x22,0x10,p_id,mdlc,0x32,0x00,0x74]) - # TODO: handle this response - # status[:mute] = true - # status[:picture_mute] = true + status[:mute] = true + status[:picture_mute] = true # mute_onscreen should_send(Bytes[0x02,0x14,0x00,0x00,0x00,0x16,0x2C]) responds(Bytes[0x22,0x14,p_id,mdlc,0x00,0x46]) - # TODO: handle this response - # status[:onscreen_mute] = true + status[:onscreen_mute] = true # mute_audio should_send(Bytes[0x02,0x12,0x00,0x00,0x00,0x14,0x28]) responds(Bytes[0x22,0x12,p_id,mdlc,0x00,0x44]) - # TODO: handle this response - # status[:audio_mute] = true - # mute? - should_send(Bytes[0x00,0x85,0x00,0x00,0x01,0x03,0x89]) - responds(Bytes[0x20,0x85,p_id,mdlc,0x10, - #-17 -16 -15 - 0x01,0x01,0x01,4,5,6,7,8,9,10,11,12,13,14,15,16, - 0x4A]) # Checksum - status[:mute].should eq(true) - status[:picture_mute].should eq(true) - status[:audio_mute].should eq(true) - status[:onscreen_mute].should eq(true) + status[:audio_mute] = true exec(:switch_audio, "VGA") should_send(Bytes[0x03,0xB1,0x00,0x00,0x02,0xC0,0x01,0x77]) From 857ddd961c1225e2800dc3da8ec544e2ae7d4c2f Mon Sep 17 00:00:00 2001 From: Philip Kheav Date: Tue, 8 Dec 2020 11:17:21 +0000 Subject: [PATCH 24/27] chore(nec_proj): remove debugging --- drivers/nec/np_series.cr | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/drivers/nec/np_series.cr b/drivers/nec/np_series.cr index 3b0e32a6dd5..7f3f955161d 100644 --- a/drivers/nec/np_series.cr +++ b/drivers/nec/np_series.cr @@ -94,7 +94,6 @@ class Nec::Projector < PlaceOS::Driver {% for name, data in COMMAND %} def {{name.id}}(**options) - pp "sending " + {{name.id.stringify}} + " with" do_send(COMMAND[{{name.id.stringify}}], **options, name: {{name.id.stringify}}) end {% end %} @@ -167,7 +166,7 @@ class Nec::Projector < PlaceOS::Driver delay: 30.seconds, clear_queue: true, priority: 100, - # delay_on_receive: 200 # give it a little bit of breathing room + # TODO delay_on_receive: 200 # give it a little bit of breathing room ) end end @@ -202,12 +201,11 @@ class Nec::Projector < PlaceOS::Driver req = Bytes.new(command.size + 1) req.copy_from(command) req[-1] = (command.sum(0) & 0xFF).to_u8 - pp "Nec proj sending 0x#{req.hexstring}" logger.debug { "Nec proj sending 0x#{req.hexstring}" } send(req, **options) { |data, task| process_response(data, task, req) } end - # TODO: add responses for freeze commands + # TODO: add responses for freeze commands if we need to process them enum Response : UInt16 Power = 8321 # [0x20,0x81] InputOrMuteQuery = 8325 # [0x20,0x85] @@ -232,7 +230,6 @@ class Nec::Projector < PlaceOS::Driver end private def process_response(data, task, req = nil) - pp "NEC projector sent: 0x#{data.hexstring}" logger.debug { "NEC projector sent: 0x#{data.hexstring}" } # Command failed @@ -366,14 +363,13 @@ class Nec::Projector < PlaceOS::Driver self[:input] = current_input = first[data[-14]] || "unknown" if data[-17] == 0x01 - # TODO + # TODO: figure out how to write in crystal and if needed # command[:delay_on_receive] = 3000 # still processing signal input? else # TODO: figure out if this is needed from old ruby driver # mute? # get mute status one signal has settled end - pp "The input selected was: #{current_input}" logger.debug { "The input selected was: #{current_input}" } # Notify of bad input selection for debugging From 7213b22ba4a891415287b261548e4d8320ee20db Mon Sep 17 00:00:00 2001 From: Philip Kheav Date: Tue, 8 Dec 2020 11:25:51 +0000 Subject: [PATCH 25/27] chore(nec_proj): add type for command --- drivers/nec/np_series.cr | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/drivers/nec/np_series.cr b/drivers/nec/np_series.cr index 7f3f955161d..2f47b5de9c4 100644 --- a/drivers/nec/np_series.cr +++ b/drivers/nec/np_series.cr @@ -196,8 +196,7 @@ class Nec::Projector < PlaceOS::Driver result end - private def do_send(command, **options) - command = command.delete(' ').hexbytes if command.is_a?(String) + private def do_send(command : Bytes, **options) req = Bytes.new(command.size + 1) req.copy_from(command) req[-1] = (command.sum(0) & 0xFF).to_u8 From 4ae5076a4e4a446129fa58279728781b54f6193e Mon Sep 17 00:00:00 2001 From: Philip Kheav Date: Tue, 15 Dec 2020 16:59:40 +0000 Subject: [PATCH 26/27] chore(nec_proj): make comment more clear --- drivers/nec/np_series.cr | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/drivers/nec/np_series.cr b/drivers/nec/np_series.cr index 2f47b5de9c4..c51063ef754 100644 --- a/drivers/nec/np_series.cr +++ b/drivers/nec/np_series.cr @@ -447,7 +447,7 @@ class Nec::Projector < PlaceOS::Driver if byte > 0 # Go through each individual bit ERROR_CODES[byte_no].each_key do |bit_check| - # Add the error if the bit corresponding to an error is set + # Add the error if the bit corresponding to it is set errors.push(ERROR_CODES[byte_no][bit_check]) if (bit_check & byte) > 0 end end From 70e9b072c83c184214c3e24620ec0bae74bf636c Mon Sep 17 00:00:00 2001 From: Stephen von Takach Date: Tue, 23 Feb 2021 13:37:30 +1100 Subject: [PATCH 27/27] feat(nec np series): simplify mute implementation --- drivers/nec/np_series.cr | 37 +++++++++++++-------------- drivers/nec/np_series_spec.cr | 48 +++++++++++++++++------------------ 2 files changed, 41 insertions(+), 44 deletions(-) diff --git a/drivers/nec/np_series.cr b/drivers/nec/np_series.cr index c51063ef754..a4cee3ed85c 100644 --- a/drivers/nec/np_series.cr +++ b/drivers/nec/np_series.cr @@ -19,7 +19,8 @@ class Nec::Projector < PlaceOS::Driver LAN = 0x20 Viewer = 0x1F end - include PlaceOS::Driver::Interface::InputSelection(Input) + + include Interface::InputSelection(Input) # Discovery Information tcp_port 7142 @@ -115,21 +116,18 @@ class Nec::Projector < PlaceOS::Driver index : Int32 | String = 0, layer : MuteLayer = MuteLayer::AudioVideo ) - mute_video(state) if layer.video? || layer.audio_video? - mute_audio(state) if layer.audio? || layer.audio_video? - end - - def mute_video(state : Bool) - if state - mute_picture - mute_onscreen - else - unmute_picture + if layer.video? || layer.audio_video? + if state + mute_picture + mute_onscreen + else + unmute_picture + end end - end - def mute_audio(state : Bool) - state ? mute_audio_cmd : unmute_audio_cmd + if layer.audio? || layer.audio_video? + state ? mute_audio_cmd : unmute_audio_cmd + end end def switch_to(input : Input) @@ -166,7 +164,6 @@ class Nec::Projector < PlaceOS::Driver delay: 30.seconds, clear_queue: true, priority: 100, - # TODO delay_on_receive: 200 # give it a little bit of breathing room ) end end @@ -310,7 +307,7 @@ class Nec::Projector < PlaceOS::Driver end schedule.in(3.seconds) { power? } - # Signal processing + # Signal processing elsif (data[-2] & 0b1000000) > 0 schedule.in(3.seconds) { power? } else # We are in a stable state! @@ -410,9 +407,9 @@ class Nec::Projector < PlaceOS::Driver # Provide all the error info required ERROR_CODES = [{ - 0b1 => "Lamp cover error", - 0b10 => "Temperature error (Bimetal)", - # 0b100 => not used + 0b1 => "Lamp cover error", + 0b10 => "Temperature error (Bimetal)", + # 0b100 => not used 0b1000 => "Fan Error", 0b10000 => "Fan Error", 0b100000 => "Power Error", @@ -423,7 +420,7 @@ class Nec::Projector < PlaceOS::Driver 0b10 => "Formatter error", 0b100 => "Lamp no.2 Error", }, { - # 0b1 => "not used" + # 0b1 => "not used" 0b10 => "FPGA error", 0b100 => "Temperature error (Sensor)", 0b1000 => "Lamp housing error", diff --git a/drivers/nec/np_series_spec.cr b/drivers/nec/np_series_spec.cr index 309024530d7..b06ff72d165 100644 --- a/drivers/nec/np_series_spec.cr +++ b/drivers/nec/np_series_spec.cr @@ -12,36 +12,36 @@ DriverSpecs.mock_driver "Nec::Projector" do # do_poll # power? - should_send(Bytes[0x00,0x81,0x00,0x00,0x00,0x81,0x02]) - responds(Bytes[0x20,0x81,p_id,mdlc,0x10,0b_0000_0010,0xC3]) + should_send(Bytes[0x00, 0x81, 0x00, 0x00, 0x00, 0x81, 0x02]) + responds(Bytes[0x20, 0x81, p_id, mdlc, 0x10, 0b_0000_0010, 0xC3]) status[:power].should eq(true) # input? - should_send(Bytes[0x00,0x85,0x00,0x00,0x01,0x02,0x88]) - responds(Bytes[0x20,0x85,p_id,mdlc,0x10, + should_send(Bytes[0x00, 0x85, 0x00, 0x00, 0x01, 0x02, 0x88]) + responds(Bytes[0x20, 0x85, p_id, mdlc, 0x10, # Data, simplified for sanity # We only care about the ones with 0x - #-17 -15 -14 - 0x00,2,0x01,0x06,5,6,7,8,9,10,11,12,13,14,15,16, + # -17 -15 -14 + 0x00, 2, 0x01, 0x06, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 0x4C]) # Checksum status[:input].should eq("HDMI") # mute? - should_send(Bytes[0x00,0x85,0x00,0x00,0x01,0x03,0x89]) - responds(Bytes[0x20,0x85,p_id,mdlc,0x10, - #-17 -16 -15 - 0x00,0x00,0x00,4,5,6,7,8,9,10,11,12,13,14,15,16, + should_send(Bytes[0x00, 0x85, 0x00, 0x00, 0x01, 0x03, 0x89]) + responds(Bytes[0x20, 0x85, p_id, mdlc, 0x10, + # -17 -16 -15 + 0x00, 0x00, 0x00, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 0x47]) # Checksum status[:mute].should eq(false) status[:picture_mute].should eq(false) status[:audio_mute].should eq(false) status[:onscreen_mute].should eq(false) # background_black - should_send(Bytes[0x03,0xB1,0x00,0x00,0x02,0x0B,0x01,0xC2]) - responds(Bytes[0x23,0xB1,p_id,mdlc,0x02,0x0B,0xF1]) + should_send(Bytes[0x03, 0xB1, 0x00, 0x00, 0x02, 0x0B, 0x01, 0xC2]) + responds(Bytes[0x23, 0xB1, p_id, mdlc, 0x02, 0x0B, 0xF1]) # lamp_info - should_send(Bytes[0x03,0x8A,0x00,0x00,0x00,0x8D,0x1A]) + should_send(Bytes[0x03, 0x8A, 0x00, 0x00, 0x00, 0x8D, 0x1A]) # 5 for header, 1 for checksum and 98 for data response = Bytes.new(104) - response.copy_from(Bytes[0x23,0x8A,p_id,mdlc,0x62,0x0B]) # header + response.copy_from(Bytes[0x23, 0x8A, p_id, mdlc, 0x62, 0x0B]) # header # data # lamp usage response[87] = 0xC0 @@ -57,27 +57,27 @@ DriverSpecs.mock_driver "Nec::Projector" do status[:filter_usage].should eq(1600) exec(:volume, 100) - should_send(Bytes[0x03,0x10,0x00,0x00,0x05,0x05,0x00,0x00,0x3F,0x00,0x5C]) - responds(Bytes[0x23,0x10,p_id,mdlc,0x05,0x00,0x48]) + should_send(Bytes[0x03, 0x10, 0x00, 0x00, 0x05, 0x05, 0x00, 0x00, 0x3F, 0x00, 0x5C]) + responds(Bytes[0x23, 0x10, p_id, mdlc, 0x05, 0x00, 0x48]) status[:volume].should eq(63) exec(:mute) # mute_picture - should_send(Bytes[0x02,0x10,0x00,0x00,0x00,0x12,0x24]) - responds(Bytes[0x22,0x10,p_id,mdlc,0x32,0x00,0x74]) + should_send(Bytes[0x02, 0x10, 0x00, 0x00, 0x00, 0x12, 0x24]) + responds(Bytes[0x22, 0x10, p_id, mdlc, 0x32, 0x00, 0x74]) status[:mute] = true status[:picture_mute] = true # mute_onscreen - should_send(Bytes[0x02,0x14,0x00,0x00,0x00,0x16,0x2C]) - responds(Bytes[0x22,0x14,p_id,mdlc,0x00,0x46]) + should_send(Bytes[0x02, 0x14, 0x00, 0x00, 0x00, 0x16, 0x2C]) + responds(Bytes[0x22, 0x14, p_id, mdlc, 0x00, 0x46]) status[:onscreen_mute] = true # mute_audio - should_send(Bytes[0x02,0x12,0x00,0x00,0x00,0x14,0x28]) - responds(Bytes[0x22,0x12,p_id,mdlc,0x00,0x44]) + should_send(Bytes[0x02, 0x12, 0x00, 0x00, 0x00, 0x14, 0x28]) + responds(Bytes[0x22, 0x12, p_id, mdlc, 0x00, 0x44]) status[:audio_mute] = true exec(:switch_audio, "VGA") - should_send(Bytes[0x03,0xB1,0x00,0x00,0x02,0xC0,0x01,0x77]) - responds(Bytes[0x23,0xB1,p_id,mdlc,0xC0,0x01,0xA5]) + should_send(Bytes[0x03, 0xB1, 0x00, 0x00, 0x02, 0xC0, 0x01, 0x77]) + responds(Bytes[0x23, 0xB1, p_id, mdlc, 0xC0, 0x01, 0xA5]) status[:audio_input].should eq("VGA") end