|
#!/usr/bin/env ruby |
|
|
|
require 'optparse' |
|
require 'pathname' |
|
require 'pp' |
|
|
|
$options = { |
|
debug: false, |
|
debug_path: '/home/pi/led_log.txt' |
|
} |
|
|
|
# Takes a string like "1,2,3,7,8,9" and determines which LEDs should be lit |
|
# up. Shells out to the `pacdrive` binary to do it. |
|
|
|
$usage = OptionParser.new do |opts| |
|
opts.banner = "Usage: led-start [OPTIONS] system game" |
|
opts.separator "" |
|
|
|
opts.on('-d', '--debug', "Writes debug output to the specified file (defaults to ~/led_log.txt).") do |path| |
|
$options[:debug] = true |
|
$options[:debug_path] = path unless (path.nil? || path.empty?) |
|
end |
|
|
|
opts.on_tail('-h', '--help', "Displays this help message.") |
|
end |
|
|
|
$usage.parse! |
|
|
|
# The path to the file that sets defaults for LED state. If no config is |
|
# found for a certain game, this file ends up determining all states. If a |
|
# game config omits a certain button, the default config can specify that |
|
# that button should nonetheless be on. (This is useful for, e.g., coin |
|
# buttons, aux buttons, and such. These should almost always be on, but it's |
|
# dumb to have to specify them in every game config file.) |
|
DEFAULT_CONFIG_PATH = Pathname.new('/home/pi/.ledrc') |
|
|
|
# The base directory where all configs are stored. Each system should be its |
|
# own folder; each game should be a text file in that folder whose name |
|
# matches the game's name and has a "cfg" extension. |
|
BASE_CONFIG_PATH = Pathname.new('/home/pi/leds') |
|
|
|
class LEDState |
|
|
|
def initialize(path) |
|
@contents = path.is_a?(String) ? path : File.read(path).chomp |
|
@button_map = {} |
|
debug "config file contents: #{@contents}" |
|
buttons = @contents.split(/,\s*/) |
|
(1..16).each do |digit| |
|
state = { value: 0, force: false } |
|
if buttons.include?(digit.to_s) |
|
state = { value: 1, force: false } |
|
elsif buttons.include?("+#{digit}") |
|
# "+x" syntax means we should force this button to be on. |
|
state = { value: 1, force: true } |
|
elsif buttons.include?("-#{digit}") |
|
# "-x" syntax means we should force this button to be off. |
|
state = { value: 0, force: true } |
|
end |
|
debug "State for button #{digit}:" |
|
debug state |
|
@button_map[digit] = state |
|
end |
|
self |
|
end |
|
|
|
def with_default(path) |
|
debug "Combining with default config" |
|
default = LEDState.new(path) |
|
merge!(default) |
|
self |
|
end |
|
|
|
def map |
|
@button_map |
|
end |
|
|
|
# Converts the state to the format expected by the `pacdrive` utility. |
|
def to_s |
|
str = (1..16).map do |digit| |
|
state = '0' |
|
data = @button_map[digit] |
|
if data && data.has_key?(:value) |
|
state = data[:value] == 1 ? '1' : '0' |
|
end |
|
end |
|
|
|
binary = str.join('').reverse |
|
"0x%04x" % binary.to_i(2) |
|
end |
|
|
|
# Calls the external `pacdrive` app to set the LEDs to the represented |
|
# state. |
|
# |
|
# Uses `exec`, so once this method is called, we implicitly exit. |
|
def apply! |
|
exec("/home/pi/bin/pacdrive", "-q", "-s", to_s) |
|
end |
|
|
|
protected |
|
|
|
# For combining this state with another. The argument to this function is |
|
# assumed to be a default, which means it will take precedence over this |
|
# state when conflicts happen. |
|
def merge!(default) |
|
output = {} |
|
default_map = default.map |
|
@button_map.keys.each do |digit| |
|
own = @button_map[digit] |
|
dflt = default_map[digit] |
|
|
|
result = nil |
|
|
|
if !dflt[:force] |
|
# Our version always wins. |
|
result = own[:value] |
|
else |
|
# Default wants to force a value... |
|
if !own[:force] |
|
# ...and we don't, so use the default. |
|
result = dflt[:value] |
|
else |
|
# ...and so do we, so we win. |
|
result = own[:value] |
|
end |
|
end |
|
output[digit] = { value: result } |
|
end # each |
|
|
|
# Replace the button map with the merged version. |
|
@button_map = output |
|
end # merge |
|
end # LEDState |
|
|
|
def debug(str) |
|
return unless $options[:debug] |
|
`echo "#{str}" >> #{$options[:debug_path]}` |
|
end |
|
|
|
if ARGV.size == 1 |
|
# We've been given a string like "1,2,3,7,8,9" (probably for debugging). |
|
# Ignore all configs and just create and apply an LED state. |
|
debug "Given input: #{ARGV[0]}" |
|
LEDState.new(ARGV[0]).apply! |
|
elsif ARGV.size == 2 |
|
# We've been given a system and a ROM name. Look up its config file and |
|
# turn it into a binary string. |
|
system, game = ARGV |
|
debug "Setting LEDs for game #{game} on system #{system}..." |
|
game_config_path = BASE_CONFIG_PATH.join(system, "#{game}.cfg") |
|
|
|
if game_config_path.exist? |
|
state = LEDState.new(game_config_path).with_default(DEFAULT_CONFIG_PATH) |
|
else |
|
# No specific LEDs for this game, so just apply the default. |
|
state = LEDState.new(DEFAULT_CONFIG_PATH) |
|
end |
|
|
|
state.apply! |
|
else |
|
puts $usage |
|
end |