Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
95 commits
Select commit Hold shift + click to select a range
ad96997
feat(samsung): initial commit
Jun 8, 2020
4acdd3c
fix(samsung): cleanup formatting and move to displays folder
Jun 8, 2020
fecf5b7
feat(samsung): port over more methods
Jun 8, 2020
d0bed9e
fix(samsung): update to pass driver test runner
Jun 12, 2020
5574e83
feat(samsung): port over power function
Jun 15, 2020
68fb7a0
feat(samsung): port over volume methods
Jun 15, 2020
27fd9a4
fix(samsung): force compiler to process type for previous_volume as I…
Jun 15, 2020
47c7f4f
feat(samsung): port over more methods
Jun 15, 2020
57908c1
feat(samsung): port over more methods
Jun 15, 2020
cbb88e1
fix(samsung): use enum to store constant values for commands
Jun 16, 2020
dfaaf81
fix(samsung): cleanup types
Jun 16, 2020
62cebc0
chore(samsung): cleanup todos
Jun 16, 2020
02334a0
fix(samsung): initialise @blank as nil
Jun 16, 2020
494ff4c
fix(samsung): add spec tests
Jun 16, 2020
dd178cc
fix(samsung): use safe getter to get self values
Jun 16, 2020
b0cda52
fix(samsung): use type safe getters when trying to access self values…
Jun 16, 2020
9dff2a5
fix(samsung): fix all warnings when running with spec
Jun 16, 2020
eb14925
fix(samsung): commit correct file
Jun 16, 2020
a342c93
fix(samsung): add check for sending of data from connected method in …
Jun 17, 2020
b91a62d
fix(samsung): fix abstract tokenizer and add responses in spec
Jun 17, 2020
fc951b0
feat(samsung): add skeleton and checksum verification for received me…
Jun 17, 2020
9afb025
feat(samsung): complete most of the received method
Jun 17, 2020
df40e66
fix(samsung): use task signals
Jun 17, 2020
4839b79
feat(samsung): port over split function
Jun 17, 2020
2d8cab7
feat(samsung): add status value checks to spec
Jun 17, 2020
aaab229
fix(samsung): save screen_split value
Jun 17, 2020
ef0f243
feat(samsung): add macro to define methods
Jun 19, 2020
71c7976
refactor(samsung): use Crystal's Bytes
Jun 22, 2020
754d41b
chore(samsung): cleanup do_send param types
Jun 22, 2020
effadb0
chore(samsung): remove documentation links
Jun 23, 2020
0f6ea0c
refactor(samsung): rename to MDCProtocol
Jun 23, 2020
1d8677d
refactor(samsung): use Enum's from_value instead of new
Jun 23, 2020
1066b95
refactor(samsung): use question methods for Enums
Jun 23, 2020
79b1bab
refactor(samsung): update do_send to only take in COMMAND enums
Jun 23, 2020
a44f7d8
refactor(samsung): use less intermediate arrays in do_send
Jun 23, 2020
9f08d9f
chore(samsung): fix indentation
Jun 23, 2020
e5e7d33
chore(samsung): remove custom command method
Jun 25, 2020
821ed9a
refactor/fix(samsung): move command assembly into enum class and use …
Jun 25, 2020
009e6aa
refactor(samsung): more Bytes
Jun 25, 2020
e074df8
chore(samsung): remove set timer method
Jun 25, 2020
ad1868b
chore(samsung): use camelcase convention for type names
Jun 25, 2020
7b57367
chore(samsung) remove wake method
Jun 25, 2020
3b1a9a2
chore(samsung): remove ScaleModes enum and split method
Jun 25, 2020
4a2abdd
fix(samsung): use panel mute command for mute
Jun 25, 2020
0f07bc4
fix(mdc_protocol): use Powerable interface
Jul 29, 2020
720f623
fix(mdc_protocol): use Muteable interface
Jul 29, 2020
140b609
fix(mdc_protocol): implement Switchable interface
Jul 29, 2020
fd77d80
fix(mdc_protocol): initiliase previous volume
Jul 29, 2020
45ae2c6
fix(mdc_protocol): use setting? instead of setting
Jul 29, 2020
b28d734
feat(samsung): add Security annotation to methods
Aug 6, 2020
dfbbee7
refactor(mdc_protocol): remove init_tokenizer method
Aug 6, 2020
06182fc
refactor(mdc_protocol): use instance variables
Aug 6, 2020
7880524
fix(mdc_protocol): use immediate flag for scheduler
Aug 6, 2020
a70f079
fix(mdc_protocol): let compiler infer type
Aug 6, 2020
1b3052d
fix(mdc_protocol): let compiler infer broadcast type
Aug 6, 2020
7ac3454
fix(mdc_protocol): define array for method names inline with macro
Aug 6, 2020
a5cecd2
feat(mdc_protocol): add logic for message indicator
Aug 7, 2020
feec321
chore(mdc): remove completed todo
Aug 7, 2020
3fd04f5
fix(mdc): define indicator as constant
Aug 7, 2020
a293bd0
fix(mdc): use indicator constant when building command
pkheav Aug 7, 2020
96e296e
chore(mdc): clean up do_device_config
Sep 4, 2020
bf41b5a
fix(mdc): stringify setting names
Sep 7, 2020
eaefef6
fix(mdc): update require lines
Sep 14, 2020
ad37378
fix(mdc): group device settings
Sep 14, 2020
eb65da9
chore(mdc): fix indentation
Sep 14, 2020
bc115c2
fix(mdc): return power after power query
Sep 16, 2020
630d121
chore(mdc): remove broadcast
Sep 22, 2020
0897720
fix(mdc): remove old ruby code
Sep 22, 2020
59fe9aa
fix(mdc): set input target as nil initially
Sep 22, 2020
3781786
fix(mdc): set power properly
Sep 22, 2020
95f64ba
fix(mdc): create variable for hard_off
Sep 22, 2020
a2f498f
fix(mdc): assign variables when checking self values
Sep 22, 2020
e5386f5
fix(mdc): assign more self values to variables
Sep 22, 2020
96923a9
chore(mdc): remove some logging
Sep 22, 2020
14468b4
fix(mdc): use Muteable interface properly
Sep 22, 2020
e746aa2
fix(mdc): add spec for changing volume
Sep 22, 2020
1480d35
fix(mdc): use correct hex value for spec
Sep 22, 2020
320ff1a
fix(mdc): use shorter way of checking self values
Sep 29, 2020
cd26260
fix(mdc): use as_bool instead of as_bool?
Sep 29, 2020
5bb7ceb
fix(mdc): use strict parse instead of parse?
pkheav Sep 29, 2020
0ccbe36
fix(mdc): correctly use commit github suggestion
Sep 29, 2020
7cb6ad5
fix(mdc): use if statement for checking MuteLayer
Sep 29, 2020
dc618a5
chore(mdc): use CamelCase
Sep 29, 2020
bdbc8ae
fix(samsung): address feedback
Oct 6, 2020
4edeb5e
chore(mdc): use Input instead of Inputs
Oct 6, 2020
6bbb6ff
chore(mdc): use state instead of power
Oct 6, 2020
0ee5b80
fix(mdc): change power_target to instance variable
Oct 6, 2020
92ef511
fix(mdc): use shorter method to check MuteLayer
Oct 6, 2020
326044a
fix(mdc): remove unmute audio
Oct 6, 2020
cda86f3
fix(mdc): set input_target correctly
Oct 12, 2020
9b464ea
fix(mdc): don't use linebreak in error logging
Oct 12, 2020
bf21a45
fix(mdc): make check_power_state method private
Oct 12, 2020
886d433
fix(mdc): don't map response
Oct 12, 2020
89f4747
fix(mdc): set type to UInt8 to reduce casting
Oct 12, 2020
4aec982
feat(mdc): add specs for switch_to and power
Oct 13, 2020
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
349 changes: 349 additions & 0 deletions drivers/samsung/displays/mdc_protocol.cr
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,
Comment thread
kimburgess marked this conversation as resolved.
layer : MuteLayer = MuteLayer::AudioVideo
Comment thread
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
Loading