-
Notifications
You must be signed in to change notification settings - Fork 18
Port Samsung display driver #19
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
95 commits
Select commit
Hold shift + click to select a range
ad96997
feat(samsung): initial commit
4acdd3c
fix(samsung): cleanup formatting and move to displays folder
fecf5b7
feat(samsung): port over more methods
d0bed9e
fix(samsung): update to pass driver test runner
5574e83
feat(samsung): port over power function
68fb7a0
feat(samsung): port over volume methods
27fd9a4
fix(samsung): force compiler to process type for previous_volume as I…
47c7f4f
feat(samsung): port over more methods
57908c1
feat(samsung): port over more methods
cbb88e1
fix(samsung): use enum to store constant values for commands
dfaaf81
fix(samsung): cleanup types
62cebc0
chore(samsung): cleanup todos
02334a0
fix(samsung): initialise @blank as nil
494ff4c
fix(samsung): add spec tests
dd178cc
fix(samsung): use safe getter to get self values
b0cda52
fix(samsung): use type safe getters when trying to access self values…
9dff2a5
fix(samsung): fix all warnings when running with spec
eb14925
fix(samsung): commit correct file
a342c93
fix(samsung): add check for sending of data from connected method in …
b91a62d
fix(samsung): fix abstract tokenizer and add responses in spec
fc951b0
feat(samsung): add skeleton and checksum verification for received me…
9afb025
feat(samsung): complete most of the received method
df40e66
fix(samsung): use task signals
4839b79
feat(samsung): port over split function
2d8cab7
feat(samsung): add status value checks to spec
aaab229
fix(samsung): save screen_split value
ef0f243
feat(samsung): add macro to define methods
71c7976
refactor(samsung): use Crystal's Bytes
754d41b
chore(samsung): cleanup do_send param types
effadb0
chore(samsung): remove documentation links
0f6ea0c
refactor(samsung): rename to MDCProtocol
1d8677d
refactor(samsung): use Enum's from_value instead of new
1066b95
refactor(samsung): use question methods for Enums
79b1bab
refactor(samsung): update do_send to only take in COMMAND enums
a44f7d8
refactor(samsung): use less intermediate arrays in do_send
9f08d9f
chore(samsung): fix indentation
e5e7d33
chore(samsung): remove custom command method
821ed9a
refactor/fix(samsung): move command assembly into enum class and use …
009e6aa
refactor(samsung): more Bytes
e074df8
chore(samsung): remove set timer method
ad1868b
chore(samsung): use camelcase convention for type names
7b57367
chore(samsung) remove wake method
3b1a9a2
chore(samsung): remove ScaleModes enum and split method
4a2abdd
fix(samsung): use panel mute command for mute
0f07bc4
fix(mdc_protocol): use Powerable interface
720f623
fix(mdc_protocol): use Muteable interface
140b609
fix(mdc_protocol): implement Switchable interface
fd77d80
fix(mdc_protocol): initiliase previous volume
45ae2c6
fix(mdc_protocol): use setting? instead of setting
b28d734
feat(samsung): add Security annotation to methods
dfbbee7
refactor(mdc_protocol): remove init_tokenizer method
06182fc
refactor(mdc_protocol): use instance variables
7880524
fix(mdc_protocol): use immediate flag for scheduler
a70f079
fix(mdc_protocol): let compiler infer type
1b3052d
fix(mdc_protocol): let compiler infer broadcast type
7ac3454
fix(mdc_protocol): define array for method names inline with macro
a5cecd2
feat(mdc_protocol): add logic for message indicator
feec321
chore(mdc): remove completed todo
3fd04f5
fix(mdc): define indicator as constant
a293bd0
fix(mdc): use indicator constant when building command
pkheav 96e296e
chore(mdc): clean up do_device_config
bf41b5a
fix(mdc): stringify setting names
eaefef6
fix(mdc): update require lines
ad37378
fix(mdc): group device settings
eb65da9
chore(mdc): fix indentation
bc115c2
fix(mdc): return power after power query
630d121
chore(mdc): remove broadcast
0897720
fix(mdc): remove old ruby code
59fe9aa
fix(mdc): set input target as nil initially
3781786
fix(mdc): set power properly
95f64ba
fix(mdc): create variable for hard_off
a2f498f
fix(mdc): assign variables when checking self values
e5386f5
fix(mdc): assign more self values to variables
96923a9
chore(mdc): remove some logging
14468b4
fix(mdc): use Muteable interface properly
e746aa2
fix(mdc): add spec for changing volume
1480d35
fix(mdc): use correct hex value for spec
320ff1a
fix(mdc): use shorter way of checking self values
cd26260
fix(mdc): use as_bool instead of as_bool?
5bb7ceb
fix(mdc): use strict parse instead of parse?
pkheav 0ccbe36
fix(mdc): correctly use commit github suggestion
7cb6ad5
fix(mdc): use if statement for checking MuteLayer
dc618a5
chore(mdc): use CamelCase
bdbc8ae
fix(samsung): address feedback
4edeb5e
chore(mdc): use Input instead of Inputs
6bbb6ff
chore(mdc): use state instead of power
0ee5b80
fix(mdc): change power_target to instance variable
92ef511
fix(mdc): use shorter method to check MuteLayer
326044a
fix(mdc): remove unmute audio
cda86f3
fix(mdc): set input_target correctly
9b464ea
fix(mdc): don't use linebreak in error logging
bf21a45
fix(mdc): make check_power_state method private
886d433
fix(mdc): don't map response
89f4747
fix(mdc): set type to UInt8 to reduce casting
4aec982
feat(mdc): add specs for switch_to and power
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,349 @@ | ||
| require "placeos-driver/interface/powerable" | ||
| require "placeos-driver/interface/muteable" | ||
| require "placeos-driver/interface/switchable" | ||
|
|
||
| class Samsung::Displays::MDCProtocol < PlaceOS::Driver | ||
| include Interface::Powerable | ||
| include Interface::Muteable | ||
|
|
||
| INDICATOR = 0xAA_u8 | ||
|
|
||
| enum Input | ||
| Vga = 0x14 # pc in manual | ||
| Dvi = 0x18 | ||
| DviVideo = 0x1F | ||
| Hdmi = 0x21 | ||
| HdmiPc = 0x22 | ||
| Hdmi2 = 0x23 | ||
| Hdmi2Pc = 0x24 | ||
| Hdmi3 = 0x31 | ||
| Hdmi3Pc = 0x32 | ||
| Hdmi4 = 0x33 | ||
| Hdmi4Pc = 0x34 | ||
| DisplayPort = 0x25 | ||
| Dtv = 0x40 | ||
| Media = 0x60 | ||
| Widi = 0x61 | ||
| MagicInfo = 0x20 | ||
| Whiteboard = 0x64 | ||
| end | ||
| include PlaceOS::Driver::Interface::InputSelection(Input) | ||
|
|
||
| # Discovery Information | ||
| tcp_port 1515 | ||
| descriptive_name "Samsung MD, DM & QM Series LCD" | ||
| generic_name :Display | ||
|
|
||
| # Markdown description | ||
| description <<-DESC | ||
| For DM displays configure the following 1: | ||
|
|
||
| 1. Network Standby = ON | ||
| 2. Set Auto Standby = OFF | ||
| 3. Set Eco Solution, Auto Off = OFF | ||
|
|
||
| Hard Power off displays each night and hard power ON in the morning. | ||
| DESC | ||
|
|
||
| default_settings({ | ||
| display_id: 0, | ||
| rs232_control: false | ||
| }) | ||
|
|
||
| @id : UInt8 = 0 | ||
| @rs232 : Bool = false | ||
| @blank : Input? | ||
| @previous_volume : Int32 = 50 | ||
| @input_stable : Bool = true | ||
| @input_target : Input? = nil | ||
| @power_stable : Bool = true | ||
| @power_target : Bool = true | ||
|
|
||
| def on_load | ||
| transport.tokenizer = Tokenizer.new do |io| | ||
| bytes = io.peek | ||
| # Ensure message indicator is well-formed | ||
| disconnect unless bytes.first == INDICATOR | ||
| logger.debug { "Received: #{bytes}" } | ||
| # [header, command, id, data.size, [data], checksum] | ||
| # return 0 if the message is incomplete | ||
| bytes.size < 4 ? 0 : bytes[3].to_i + 5 | ||
| end | ||
|
|
||
| on_update | ||
| end | ||
|
|
||
| def on_update | ||
| @id = setting(UInt8, :display_id) | ||
| @rs232 = setting(Bool, :rs232_control) | ||
| @blank = setting?(String, :blanking_input).try &->Input.parse(String) | ||
| end | ||
|
|
||
| def connected | ||
| do_device_config unless self[:hard_off]?.try &.as_bool | ||
|
|
||
| schedule.every(30.seconds, true) do | ||
| do_poll | ||
| end | ||
| end | ||
|
|
||
| def disconnected | ||
| self[:power] = false unless @rs232 | ||
| schedule.clear | ||
| end | ||
|
|
||
| # As true power off disconnects the server we only want to power off the panel | ||
| def power(state : Bool) | ||
| @power_target = state | ||
| @power_stable = false | ||
|
|
||
| if state | ||
| # Power on | ||
| do_send(Command::HardOff, 1) | ||
| do_send(Command::PanelMute, 0) | ||
| else | ||
| # Blank the screen before turning off panel if required | ||
| # required by some video walls where screens are chained | ||
| if (blanking_input = @blank) && self[:power]? | ||
| switch_to(blanking_input) | ||
| end | ||
| do_send(Command::PanelMute, 1) | ||
| end | ||
| end | ||
|
|
||
| def hard_off | ||
| do_send(Command::PanelMute, 0) if self[:power]?.try &.as_bool | ||
| do_send(Command::HardOff, 0) | ||
| end | ||
|
|
||
| def power?(**options) | ||
| do_send(Command::PanelMute, Bytes.empty, **options).get | ||
| self[:power] | ||
| end | ||
|
|
||
| # Mutes both audio/video | ||
| def mute( | ||
| state : Bool = true, | ||
| index : Int32 | String = 0, | ||
| layer : MuteLayer = MuteLayer::AudioVideo | ||
|
kimburgess marked this conversation as resolved.
|
||
| ) | ||
| mute_video(state) if layer.video? || layer.audio_video? | ||
| mute_audio(state) if layer.audio? || layer.audio_video? | ||
| end | ||
|
|
||
| # Adds video mute state compatible with projectors | ||
| def mute_video(state : Bool = true) | ||
| state = state ? 1 : 0 | ||
| do_send(Command::PanelMute, state) | ||
| end | ||
|
|
||
| # Emulate audio mute | ||
| def mute_audio(state : Bool = true) | ||
| # Do nothing if already in desired state | ||
| return if self[:audio_mute]?.try &.as_bool == state | ||
| self[:audio_mute] = state | ||
| if state | ||
| @previous_volume = self[:volume].as_i | ||
| volume(0) | ||
| else | ||
| volume(@previous_volume) | ||
| end | ||
| end | ||
|
|
||
| # check software version | ||
| def software_version | ||
| do_send(Command::SoftwareVersion) | ||
| end | ||
|
|
||
| def serial_number | ||
| do_send(Command::SerialNumber) | ||
| end | ||
|
|
||
| def switch_to(input : Input, **options) | ||
| @input_stable = false | ||
| @input_target = input | ||
| do_send(Command::Input, input.value, **options) | ||
| end | ||
|
|
||
| enum SpeakerMode | ||
| Internal = 0 | ||
| External = 1 | ||
| end | ||
|
|
||
| def speaker_select(mode : SpeakerMode, **options) | ||
| do_send(Command::Speaker, mode.value, **options) | ||
| end | ||
|
|
||
| def do_poll | ||
| do_send(Command::Status, Bytes.empty, priority: 0) | ||
| power? unless self[:hard_off]?.try &.as_bool | ||
| end | ||
|
|
||
| DEVICE_SETTINGS = { | ||
| network_standby: Bool, | ||
| auto_off_timer: Bool, | ||
| auto_power: Bool, | ||
| volume: Int32, | ||
| contrast: Int32, | ||
| brightness: Int32, | ||
| sharpness: Int32, | ||
| colour: Int32, | ||
| tint: Int32, | ||
| red_gain: Int32, | ||
| green_gain: Int32, | ||
| blue_gain: Int32 | ||
| } | ||
| {% for name, kind in DEVICE_SETTINGS %} | ||
| @[Security(Level::Administrator)] | ||
| def {{name.id}}(value : {{kind}}, **options) | ||
| {% if kind.resolve == Bool %} | ||
| state = value ? 1 : 0 | ||
| data = {{name.id.stringify}} == "auto_off_timer" ? Bytes[0x81, state] : state | ||
| {% elsif kind.resolve == Int32 %} | ||
| data = value.clamp(0, 100) | ||
| {% end %} | ||
| do_send(Command.parse({{name.id.stringify}}), data, **options) | ||
| end | ||
| {% end %} | ||
|
|
||
| def do_device_config | ||
| {% for name, kind in DEVICE_SETTINGS %} | ||
| %value = setting?({{kind}}, {{name.id.stringify}}) | ||
| {{name.id}}(%value) unless %value.nil? | ||
| {% end %} | ||
| end | ||
|
|
||
| enum ResponseStatus | ||
| Ack = 0x41 # A | ||
| Nak = 0x4e # N | ||
| end | ||
|
|
||
| def received(data, task) | ||
| hex = data.hexstring | ||
| logger.debug { "Samsung sent: #{hex}" } | ||
|
|
||
| # Calculate checksum of response | ||
| checksum = data[1..-2].reduce(&.+) | ||
|
|
||
| if data[-1] != checksum | ||
| logger.error { "Invalid checksum, checksum should be: #{checksum.to_s(16)}" } | ||
| return task.try &.retry | ||
| end | ||
|
|
||
| status = ResponseStatus.from_value(data[4]) | ||
| command = Command.from_value(data[5]) | ||
| values = data[6..-2] | ||
| value = values.first | ||
|
|
||
| case status | ||
| when .ack? | ||
| case command | ||
| when .status? | ||
| self[:hard_off] = hard_off = values[0] == 0 | ||
| self[:power] = false if hard_off | ||
| self[:volume] = values[1] | ||
| self[:audio_mute] = values[2] == 1 | ||
| self[:input] = Input.from_value(values[3]) | ||
| check_power_state | ||
| when .panel_mute? | ||
| self[:power] = value == 0 | ||
| check_power_state | ||
| when .volume? | ||
| self[:volume] = value | ||
| self[:audio_mute] = false if value > 0 | ||
| when .brightness? | ||
| self[:brightness] = value | ||
| when .input? | ||
| current_input = Input.from_value(value) | ||
| self[:input] = current_input | ||
| # The input feedback behaviour seems to go a little odd when | ||
| # screen split is active. Ignore any input forcing when on. | ||
| unless self[:screen_split]?.try &.as_bool | ||
| @input_stable = current_input == (input_target = @input_target) | ||
| switch_to(input_target) if input_target && !@input_stable | ||
| end | ||
| when .speaker? | ||
| self[:speaker] = SpeakerMode.from_value(value) | ||
| when .hard_off? | ||
| unless self[:hard_off]?.try &.as_bool | ||
| self[:hard_off] = hard_off = value == 0 | ||
| self[:power] = false if hard_off | ||
| end | ||
| when .screen_split? | ||
| self[:screen_split] = value >= 0 | ||
| when .software_version? | ||
| self[:software_version] = values.join | ||
| when .serial_number? | ||
| self[:serial_number] = values.join | ||
| else | ||
| logger.debug { "Samsung responded with ACK: #{value}" } | ||
| end | ||
|
|
||
| task.try &.success | ||
| when .nak? | ||
| task.try &.abort("Samsung responded with NAK: #{hex}") | ||
| else | ||
| task.try &.retry | ||
| end | ||
| end | ||
|
|
||
| private def check_power_state | ||
| return if @power_stable | ||
| if self[:power]? == @power_target | ||
| @power_stable = true | ||
| else | ||
| power(@power_target) | ||
| end | ||
| end | ||
|
|
||
| enum Command : UInt8 | ||
| Status = 0x00 | ||
| HardOff = 0x11 # Completely powers off | ||
| PanelMute = 0xF9 # Screen blanking / visual mute | ||
| Volume = 0x12 | ||
| Contrast = 0x24 | ||
| Brightness = 0x25 | ||
| Sharpness = 0x26 | ||
| Colour = 0x27 | ||
| Tint = 0x28 | ||
| RedGain = 0x29 | ||
| GreenGain = 0x2A | ||
| BlueGain = 0x2B | ||
| Input = 0x14 | ||
| Mode = 0x18 | ||
| Size = 0x19 | ||
| Pip = 0x3C # picture in picture | ||
| AutoAdjust = 0x3D | ||
| WallMode = 0x5C # Video wall mode | ||
| Safety = 0x5D | ||
| WallOn = 0x84 # Video wall enabled | ||
| WallUser = 0x89 # Video wall user control | ||
| Speaker = 0x68 | ||
| NetworkStandby = 0xB5 # Keep NIC active in standby, enable power on (without WOL) | ||
| AutoOffTimer = 0xE6 # Eco options (auto power off) | ||
| AutoPower = 0x33 # Device auto power control (presumably signal based?) | ||
| ScreenSplit = 0xB2 # Tri / quad split (larger panels only) | ||
| SoftwareVersion = 0x0E | ||
| SerialNumber = 0x0B | ||
| Time = 0xA7 | ||
| Timer = 0xA4 | ||
|
|
||
| def build(id : UInt8, data : Bytes) : Bytes | ||
| Bytes.new(data.size + 5).tap do |bytes| | ||
| bytes[0] = INDICATOR # Header | ||
| bytes[1] = self.value # Command | ||
| bytes[2] = id # Display ID | ||
| bytes[3] = data.size.to_u8 # Data size | ||
| data.each_with_index(4) { |b, i| bytes[i] = b } # Data | ||
| bytes[-1] = bytes[1..-2].reduce(&.+) # Checksum | ||
| end | ||
| end | ||
| end | ||
|
|
||
| private def do_send(command : Command, data : Int | Bytes = Bytes.empty, **options) | ||
| data = Bytes[data] if data.is_a?(Int) | ||
| bytes = command.build(@id, data) | ||
| logger.debug { "Sending to Samsung: #{bytes.hexstring}" } | ||
| send(bytes, **options) | ||
| end | ||
| end | ||
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.