#! /usr/bin/env ruby # # playstand - "Put and Play" CD Player # # Copyright (C) 2002 Satoru Takabayashi # All rights reserved. # This is free software with ABSOLUTELY NO WARRANTY. # # You can redistribute it and/or modify it under the terms of # the GNU General Public License version 2. # require 'termios' require 'fcntl' require 'getoptlong' require 'net/http' class LightController def initialize (host) @host = host end def on Net::HTTP.start(@host, 80) {|http| http.get('modify.cgi?RB2=H') } end def off Net::HTTP.start(@host, 80) {|http| http.get('modify.cgi?RB2=L') } end end class Message def initialize (start, len, status, data, bcc) @start = start @len = len @status = status @data = data @id = to_hex(@data) @bcc = bcc end attr_reader :status attr_reader :data attr_reader :id private # # Generate a hexadecimal string from binary data. # e.g. "82 11 c6 00 00 00 00 00" # def to_hex (data) data.map {|x| sprintf("%02x", x) }.join(" ") end public def inspect msg = sprintf("message: %02x %02x %02x ", @start, @len, @status) msg << sprintf("%s", (if @data.empty? then "" else to_hex(data) + " " end)) msg << sprintf("%02x", @bcc) msg end def id_detected? @status == 0x0c end end class Player def initialize (device = "/dev/ttyS0", mp3dir = "#{ENV['HOME']}/mp3", mp3cmd = "mpg123", mp3opt = "-b4096") @line = open_serial(device) @oldtio = Termios::getattr(@line) init_line @now_playing_id = nil @jukebox = Hash.new @mp3dir = mp3dir @mp3cmd = mp3cmd @mp3opt = mp3opt @verbose = false @allowable_mishit = 3 @start_hook = lambda {} @stop_hook = lambda {} end attr_accessor :verbose private def open_serial (device) # # To avoid blocking while opening the serial device, open # the serial device with the NONBLOCK flag and cancel # the NONBLOCK flag after the serial device is opened. # The technique is borrowed from minicom-1.82.1.orig/src/main.c. # line = open(device, File::RDWR | File::NONBLOCK) mode = line.fcntl(Fcntl::F_GETFL, 0); line.fcntl(Fcntl::F_SETFL, mode & ~File::NONBLOCK); return line end def init_line newtio = Termios::new_termios() newtio.c_iflag = Termios::IGNPAR newtio.c_oflag = 0 newtio.c_cflag = (Termios::B9600 | Termios::CLOCAL | Termios::CS8 | Termios::CREAD | Termios::HUPCL) newtio.c_lflag = 0 newtio.c_cc[Termios::VMIN] = 1 newtio.c_cc[Termios::VTIME] = 0 Termios::flush(@line, Termios::TCIOFLUSH) Termios::setattr(@line, Termios::TCSANOW, newtio) end # Redundancy Check calculation (Xor'ed bytes) def valid_bcc? (len, status, data, bcc) x = len ^ status data.each {|y| x = x ^ y } x == bcc end def read_message start = @line.getc raise "bad start byte #{start}" unless start == 1 len = @line.getc data = [] len.times {|x| data.push(@line.getc) } status = data.shift bcc = @line.getc raise "bad bcc #{bcc}" unless valid_bcc?(len, status, data, bcc) return Message.new(start, len, status, data, bcc) end def start_playing (id) volume_max artist, album = @jukebox[id] printf "Artist: %s\n", artist printf "Album: %s\n", album path = sprintf("%s/%s/%s", @mp3dir, artist, album) system "#{@mp3cmd} #{@mp3opt} #{path}/*.mp3 >/dev/null 2>&1 &" @now_playing_id = id @start_hook.call end def play (id) if @jukebox[id].nil? puts "unknown disc." elsif @jukebox[id] and id != @now_playing_id stop_playing start_playing(id) end end public def add_stop_hook (proc) @stop_hook = proc end def add_start_hook (proc) @start_hook = proc end def get_message @line.syswrite("\x01\x02\x08\x32\x38") read_message end def volume_max system("aumix -v 100") end def volume_fadeout point = 40 100.step(point, -1) {|x| system("aumix -v #{x}") sleep(0.015) } point.step(0, -10) {|x| system("aumix -v #{x}") sleep(0.01) } end def stop_playing if @now_playing_id Thread.new { @stop_hook.call } volume_fadeout system "killall #{@mp3cmd}" @now_playing_id = nil end end def start mishit = 0 while true message = get_message p message if @verbose if message.id_detected? play(message.id) mishit = 0 else mishit += 1 stop_playing if mishit > @allowable_mishit end sleep(0.1) end end def add (id, artist, album) @jukebox[id] = [artist, album] end def finish stop_playing Termios::setattr(@line, Termios::TCSANOW, @oldtio) Termios::flush(@line, Termios::TCIOFLUSH) @line.close end end def show_usage puts "usage: playstand [OPTIONS] [DEVICE] [PICNIC]" puts " -v, --verbose work noisily (diagnostic output)" puts " -h, --help display this help and exit" end def parse_options parser = GetoptLong.new parser.set_options(['--verbose', '-v', GetoptLong::NO_ARGUMENT], ['--help', '-h', GetoptLong::NO_ARGUMENT]) options = Hash.new parser.each_option {|name, arg| options[name.sub /^--/, ""] = arg} (show_usage; exit(1)) if options['help'] return options end def main options = parse_options raise if ARGV.empty? device = (ARGV.shift or "/dev/ttyS0") picnic = (ARGV.shift or nil) player = Player.new(device) player.verbose = true if options['verbose'] player.add("82 11 c6 00 00 00 00 00", "chick_corea_and_return_to_forever", "light_as_a_feather") player.add("83 11 c6 00 00 00 00 00", "booker_little", "bainbridge") player.add("77 11 c6 00 00 00 00 00", "pat_metheny_group", "we_live_here") player.add("79 11 c6 00 00 00 00 00", "freddie_hubbard", "hub-tones") player.add("81 11 c6 00 00 00 00 00", "arista_all_stars", "blue_montreux") player.add("78 11 c6 00 00 00 00 00", "chet_baker", "chet_baker_sings") player.add("84 11 c6 00 00 00 00 00", "john_coltrane", "my_favorite_things") player.add("7a 11 c6 00 00 00 00 00", "squarepusher", "feed_me_weird_things") if picnic controller = LightController.new(picnic) player.add_start_hook(lambda { controller.on }) player.add_stop_hook( lambda { controller.off }) end trap(:TERM) { player.finish } trap(:INT) { player.finish } begin player.start ensure player.finish end end main