class EmuPower::Api
Constants
- LINE_TERMINATOR
Attributes
Public Class Methods
Initialize the serial connection and set up internal structures.
# File lib/emu_power/api.rb, line 15 def initialize(tty, debug: false) @port = SerialPort.new(tty, 115200, 8, 1, SerialPort::NONE) # Get rid of any existing buffered data - we only want to operate on # fresh notifications. @port.flush_input @port.flush_output @debug_mode = debug reset_callbacks! end
Public Instance Methods
Register the callback for specific notification events. Expects either an EmuPower::Notifications::Notification
subclass, or :global, or :fallback. If :global is passed, the callback will be fired on every notification. If :fallback is passed, the callback will be fired for every notification that does not have a specific callback registered already.
# File lib/emu_power/api.rb, line 35 def callback(klass, &block) if klass == :global || klass == 'global' @global_callback = block elsif klass == :fallback || klass == 'fallback' @fallback_callback = block elsif EmuPower::Notifications::Notification.subclasses.include?(klass) @callbacks[klass] = block else klass_list = EmuPower::Notifications::Notification.subclasses.map(&:name).join(', ') raise ArgumentError.new("Class must be :global, :fallback, or one of #{klass_list}") end return true end
Send a command to the device. Expects an instance of one of the command classes defined in commands.rb. The serial connection must be started before this can be used.
# File lib/emu_power/api.rb, line 62 def issue_command(obj) return false if @thread.nil? || !obj.respond_to?(:to_command) xml = obj.to_command @port.write(xml) return true end
Reset all callbacks to the default no-op state.
# File lib/emu_power/api.rb, line 53 def reset_callbacks! @global_callback = nil @fallback_callback = nil @callbacks = {} end
Begin polling for serial data. We spawn a new thread to handle this so we don't block input. This method blocks until the reader thread terminates, which in most cases is never. This should usually be called at the end of a program after all callbacks are registered. If blocking is set to false, returns immediately and lets the caller handle the spawned thread. Non-blocking mode should mostly be used for development purposes; most production scripts should use blocking mode and callbacks.
# File lib/emu_power/api.rb, line 80 def start_serial(blocking = true) return false unless @thread.nil? @thread = Thread.new do # Define boundary tags root_elements = EmuPower::Notifications.notify_roots start_tags = root_elements.map { |v| "<#{v}>" } stop_tags = root_elements.map { |v| "</#{v}>" } current_notify = '' # Build up complete XML fragments line-by-line and dispatch callbacks loop do line = @port.readline(LINE_TERMINATOR).strip if start_tags.include?(line) current_notify = line elsif stop_tags.include?(line) xml = current_notify + line current_notify = '' begin obj = EmuPower::Notifications.construct(xml) rescue StandardError puts "Failed to construct object for XML fragment: #{xml}" if @debug_mode next end if obj puts obj if @debug_mode perform_callbacks(obj) else puts "Incomplete XML stream: #{xml}" if @debug_mode end else current_notify += line end end end if blocking # Block until thread is terminated, and ensure we clean up after ourselves. begin @thread.join ensure stop_serial if @thread end else return @thread end end
Terminate the reader thread. The start_serial
method will return once this is called. This will usually be called from a signal trap or similar, since the main program will usually be blocked by start_serial.
# File lib/emu_power/api.rb, line 146 def stop_serial return false if @thread.nil? @thread.terminate @thread = nil return true end
Private Instance Methods
Dispatch the appropriate callback
# File lib/emu_power/api.rb, line 160 def perform_callbacks(obj) klass = obj.class # Fire global callback @global_callback&.call(obj) klass_specific = @callbacks[klass] if klass_specific klass_specific.call(obj) else @fallback_callback&.call(obj) end end