Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
2 changes: 1 addition & 1 deletion drivers/amx/svsi/virtual_switcher.cr
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ class Amx::Svsi::VirtualSwitcher < PlaceOS::Driver
end
end

private def connect(inouts : Hash(Input, Array(Output)), &)
private def connect(inouts : FullSwitch, &)
inouts.each do |input, outputs|
if input == 0
stream = 0 # disconnected
Expand Down
241 changes: 241 additions & 0 deletions drivers/kramer/switches/protocol3000.cr
Original file line number Diff line number Diff line change
@@ -0,0 +1,241 @@
require "placeos-driver"
require "placeos-driver/interface/muteable"
require "placeos-driver/interface/switchable"

# Documentation: https://aca.im/driver_docs/Kramer/protocol_3000_2.10_user.pdf

class Kramer::Switcher::Protocol3000 < PlaceOS::Driver
include Interface::Muteable
include Interface::Switchable(Int32, Int32)
include Interface::InputSelection(Int32)

# Discovery Information
tcp_port 23
descriptive_name "Kramer Protocol 3000 Switcher"
generic_name :Switcher

@device_id : String? = nil
@destination : String? = nil
@login_level : Bool? = nil
@password : String? = nil

DELIMITER = "\x0D\x0A"

def on_load
transport.tokenizer = Tokenizer.new(DELIMITER)
on_update
end

def on_update
@device_id = setting?(String, :kramer_id)
@destination = "#{@device_id}@" if @device_id
@login_level = setting?(Bool, :kramer_login)
@password = setting?(String, :kramer_password) if @login_level

state
end

def connected
state

schedule.every(1.minute) do
logger.debug { "-- Kramer Maintaining Connection" }
do_send("MODEL?", priority: 0) # Low priority poll to maintain connection
end
end

def disconnected
schedule.clear
end

# Get current state of the switcher
private def state
protocol_handshake
login
get_machine_info
end

def switch_video(input : Int32, output : Array(Int32))
do_send(CMDS["switch_video"], build_switch_data({input => output}))
end

def switch_audio(input : Int32, output : Array(Int32))
do_send(CMDS["switch_audio"], build_switch_data({input => output}))
end

def switch(map : Hash(Int32, Array(Int32)), layer : SwitchLayer? = nil)
case layer
in Nil, .all?
switch_video(map.first_key, map.first_value)
switch_audio(map.first_key, map.first_value)
in .video?
switch_video(map.first_key, map.first_value)
in .audio?
switch_audio(map.first_key, map.first_value)
in .data?, .data2?
logger.debug { "layer #{layer} not available on extron matrix" }
return
end
end

def switch_to(input : Int32)
logger.debug { "switching input to #{input}" }
self[:input] = input.to_s
end

enum RouteType
Video = 1
Audio = 2
USB = 3
AudioVideo = 12
VideoUSB = 13
AudioVideoUSB = 123
end

def route(map : Hash(Int32, Array(Int32)), type : RouteType = RouteType::AudioVideo)
map.each do |input, outputs|
outputs.each do |output|
do_send(CMDS["route"], type.value, output, input)
end
end
end

def mute(
state : Bool = true,
index : Int32 | String = 0,
layer : MuteLayer = MuteLayer::AudioVideo
)
mute_video(index, state) if layer.video? || layer.audio_video?
mute_audio(index, state) if layer.audio? || layer.audio_video?
end

def mute_video(index : Int32 | String = 0, state : Bool = true)
do_send(CMDS["video_mute"], index, state ? 1 : 0)
end

def mute_audio(index : Int32 | String = 0, state : Bool = true)
do_send(CMDS["audio_mute"], index, state ? 1 : 0)
end

def help
do_send(CMDS["help"])
end

def model
do_send(CMDS["model"])
end

def received(data, task)
# Remove initialiser `~` and delimiter "\x0D\x0A"
data = String.new(data[1..-3])
logger.debug { "Kramer sent #{data}" }

# Extract and check the machine number if we've defined it
components = data.split('@')
return if components.size > 1 && @device_id && components[0] != @device_id

data = components[-1].strip
components = data.split(/\s+|,/)

cmd = components[0]
args = components[1..-1]
args.pop if args[-1]? == "OK"

logger.debug { "Kramer cmd: #{cmd}, args: #{args}" }

if cmd == "OK"
return task.try &.success
elsif cmd[0..2] == "ERR" || args[0][0..2] == "ERR"
if cmd[0..2] == "ERR"
error = cmd[3..-1]
errfor = nil
else
error = args[0][3..-1]
errfor = " on #{cmd}"
end
self[:last_error] = error
return task.try &.abort("Kramer command error #{error}#{errfor}")
end

case c = CMDS[cmd]
when "info"
self[:video_inputs] = args[1].to_i
self[:video_outputs] = args[3].to_i
when "route"
# response looks like ~01@ROUTE 12,1,4 OK
layer = args[0].to_i
dest = args[1].to_i
src = args[2].to_i
self["#{RouteType.from_value(layer).to_s.underscore}#{dest}"] = src
when "switch", "switch_audio", "switch_video"
# return string like "in>out,in>out,in>out OK"
case c
when "switch_audio" then type = "audio"
when "switch_video" then type = "video"
else type = "av"
end

args.each do |map|
inout = map.split('>')
logger.debug { "inout is #{inout}" }
self["#{type}#{inout[1]}"] = inout[0].to_i
end
when "audio_mute"
# Response looks like: ~01@VMUTE 1,0 OK
output = args[0]
mute = args[1]
self["audio#{output}_muted"] = mute[0] == '1'
when "video_mute"
output = args[0]
mute = args[1]
self["video#{output}_muted"] = mute[0] == '1'
end

task.try &.success
end

CMDS = {
"info" => "INFO-IO?",
"login" => "LOGIN",
"route" => "ROUTE",
"switch" => "AV",
"switch_audio" => "AUD",
"switch_video" => "VID",
"audio_mute" => "MUTE",
"video_mute" => "VMUTE",
"help" => "HELP",
"model" => "MODEL?",
}
CMDS.merge!(CMDS.invert)

private def build_switch_data(map : Hash(Int32, Array(Int32)))
data = String.build do |str|
map.each do |input, outputs|
str << outputs.join { |output| "#{input}>#{output}," }
end
end
data[0..-2] # Remove the comma at the end
end

private def protocol_handshake
do_send("", priority: 99)
end

private def login
if @login_level && (pass = @password)
do_send(CMDS["login"], pass, priority: 99)
end
end

private def get_machine_info
do_send(CMDS["info"], priority: 99)
end

private def do_send(command, *args, **options)
cmd = args.empty? ? "##{@destination}#{command}\r" : "##{@destination}#{command} #{args.join(',')}\r"

logger.debug { "Kramer sending: #{cmd}" }
pp "Kramer sending: #{cmd}"
send(cmd, **options)
end
end
60 changes: 60 additions & 0 deletions drivers/kramer/switches/protocol3000_spec.cr
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
DriverSpecs.mock_driver "Kramer::Switcher::Protocol3000" do
# on_load
# state
# protocol_handshake
should_send("#\r")
responds("~01@ OK\x0D\x0A")
# get_machine_info
should_send("#INFO-IO?\r")
responds("~01@ OK\x0D\x0A")

settings({
kramer_id: "01",
kramer_login: true,
kramer_password: "pass",
})
# on_update
# state
# protocol_handshake
should_send("#01@\r")
responds("~01@ OK\x0D\x0A")
# login
should_send("#01@LOGIN pass\r")
responds("~01@ OK\x0D\x0A")
# get_machine_info
should_send("#01@INFO-IO?\r")
responds("~01@ OK\x0D\x0A")

exec(:switch_video, 1, [1, 2, 3])
should_send("#01@VID 1>1,1>2,1>3\r")
responds("~01@VID 1>1,1>2,1>3 OK\x0D\x0A")
status[:video1].should eq(1)
status[:video2].should eq(1)
status[:video3].should eq(1)

exec(:switch_audio, 2, [1, 2, 3])
should_send("#01@AUD 2>1,2>2,2>3\r")
responds("~01@AUD 2>1,2>2,2>3 OK\x0D\x0A")
status[:audio1].should eq(2)
status[:audio2].should eq(2)
status[:audio3].should eq(2)

exec(:route, {3 => [1, 2, 3]})
should_send("#01@ROUTE 12,1,3\r")
responds("~01@ROUTE 12,1,3 OK\x0D\x0A")
should_send("#01@ROUTE 12,2,3\r")
responds("~01@ROUTE 12,2,3 OK\x0D\x0A")
should_send("#01@ROUTE 12,3,3\r")
responds("~01@ROUTE 12,3,3 OK\x0D\x0A")
status[:audio_video1].should eq(3)
status[:audio_video2].should eq(3)
status[:audio_video3].should eq(3)

exec(:mute, true, 6)
should_send("#01@VMUTE 6,1\r")
responds("#01@VMUTE 6,1 OK\x0D\x0A")
status[:video6_muted].should eq(true)
should_send("#01@MUTE 6,1\r")
responds("#01@MUTE 6,1 OK\x0D\x0A")
status[:audio6_muted].should eq(true)
end
Loading