Skip to content
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

feat: Planar Clarity Video Wall #65

Draft
wants to merge 13 commits into
base: master
Choose a base branch
from
91 changes: 91 additions & 0 deletions drivers/planar/clarity_matrix.cr
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
module Planar; end
GabFitzgerald marked this conversation as resolved.
Show resolved Hide resolved

require "placeos-driver/interface/powerable"

# Documentation: https://aca.im/driver_docs/Planar/020-1028-00%20RS232%20for%20Matrix.pdf
# also https://aca.im/driver_docs/Planar/020-0567-05_WallNet_guide.pdf

class Planar::ClarityMatrix < PlaceOS::Driver
include Interface::Powerable

# Discovery Information
generic_name :VideoWall
descriptive_name "Planar Clarity Matrix Video Wall"

def on_load
# Communication settings
queue.wait = false
transport.tokenizer = Tokenizer.new("\r")
end

def connected
schedule.every(60.seconds, true) { do_poll }
end

def disconnected
# Disconnected may be called without calling connected
# Hence the check if timer is nil here

GabFitzgerald marked this conversation as resolved.
Show resolved Hide resolved
schedule.clear
end

def power?
# options[:emit] = block if block_given?
# options[:wait] = true
# options[:name] = :pwr_query
send "op A1 display.power ? \r", name: :pwr_query #, wait: true
end

# def power(state, broadcast_ip = false, options = {}) # what does broadcast_ip do here??
GabFitzgerald marked this conversation as resolved.
Show resolved Hide resolved
def power(state : Bool = false)
self[:power] = state
GabFitzgerald marked this conversation as resolved.
Show resolved Hide resolved
# send("op A1 display.power = off \r")
if state == true
GabFitzgerald marked this conversation as resolved.
Show resolved Hide resolved
send("op A1 display.power = on \r")
schedule.in(20.seconds) { recall(0) }
else
send("op A1 display.power = off \r")
end
end

def switch_to
send "op A1 slot.recall(0) \r"

# this is called when we want the whole wall to show the one thing
# We'll just recall the one preset and have a different function for
# video wall specific functions
end

def recall(preset : Int32)
# options[:name] = :recall
send "op ** slot.recall #{preset} \r", name: :recall
end

def input_status
Copy link
Contributor

@pkheav pkheav Oct 15, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this should be renamed to input? to keep it consistent with power? but @kimburgess can probably better comment on this

# options[:wait] = true
send "op A1 slot.current ? \r", wait: true, priority: 0
end

def received(data, task)
data = String.new(data) # OPA1DISPLAY.POWER=ON || OPA1SLOT.CURRENT=0
logger.debug { "Vid Wall: #{data}" }
data = data.split(".")[1].split("=") # [POWER, ON] || [CURRENT, 0]

status = data[0].downcase # power || current
value = data[1] # ON || 0

case status
when "power"
self[:power] = value == "ON"
when "current"
self[:input] = value.to_i
end

task.try &.success(data)
end

protected def do_poll
power?
input_status if self["power"] == "ON"
Copy link
Contributor

@pkheav pkheav Oct 15, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

self[:power] will be of type JSON::Any so this should be changed to:
input_status if self[:power]?&.try as_bool

I've noticed this is necessary in order to use self values that are of type Bool properly in conditional statements.

Explanation of why this is needed is here #19 (comment) and is probably to do with how crystal deals with truthy/falsy values

Copy link
Contributor

@pkheav pkheav Oct 15, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

self[:key] will return an error if the key does not exist while self[:key]? will return nil so is safer to use generally.

.try is a safe way to deal with objects that may be nil https://crystal-lang.org/api/0.35.1/Object.html#try(&)-instance-method

as_bool https://crystal-lang.org/api/0.19.4/JSON/Any.html#as_bool%3ABool-instance-method is necessary in order to convert self[:power] which is of type JSON::Any to type Bool so it behaves as expected in conditional statements

end
end
19 changes: 19 additions & 0 deletions drivers/planar/clarity_matrix_spec.cr
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
DriverSpecs.mock_driver "Planar::ClarityMatrix" do
# on connect it should do_poll the device
should_send("op A1 display.power ? \r")
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When writing specs, you also want to mock mock the device responses so that you test full request -> response behaviour. For some info on this, see: https://github.com/PlaceOS/drivers/blob/master/docs/writing-a-spec.md#testing-streaming-io.

responds("A1 display.power ? ")
# status[:power].should eq(true)

exec(:power)
should_send("op A1 display.power = off \r")

exec(:switch_to)
should_send("op A1 slot.recall(0) \r")

transmit("OPA1DISPLAY.POWER=ON")
# .get.should eq("ON")
# responds("ON")
# response = exec(:received)

status["power"].should eq(false)
end