├── vendor └── muni-0.0.6.gem ├── client ├── lib.rb ├── font │ ├── amends.simpleglyphs │ ├── specific.simpleglyphs │ ├── genfont.pl │ └── 7x7.simpleglyphs ├── lib │ ├── sign.rb │ ├── simplefont.rb │ └── enhanced_open3.rb ├── lowlevel.pl └── client.rb ├── contrib ├── light-sensor-read.c ├── lightsense.rb ├── lightsensor ├── munisign └── morning_room.rb └── README /vendor/muni-0.0.6.gem: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pshved/muni-led-sign/HEAD/vendor/muni-0.0.6.gem -------------------------------------------------------------------------------- /client/lib.rb: -------------------------------------------------------------------------------- 1 | # Library to include with the tools for muni sign. 2 | 3 | # Nonstandard gems 4 | require 'muni' 5 | 6 | # Local includes 7 | require_relative 'lib/sign' 8 | require_relative 'lib/simplefont' 9 | 10 | -------------------------------------------------------------------------------- /contrib/light-sensor-read.c: -------------------------------------------------------------------------------- 1 | // Load it up onto your arduino with the Arduino SDK. 2 | void setup() { 3 | Serial.begin(9600); // Default baud rate. 4 | } 5 | 6 | void loop() { 7 | Serial.println(analogRead(0)); // Pin number 8 | delay(1000); 9 | } 10 | 11 | -------------------------------------------------------------------------------- /client/font/amends.simpleglyphs: -------------------------------------------------------------------------------- 1 | 38 0 7 & 2 | 01100 3 | 10010 4 | 10100 5 | 01000 6 | 10101 7 | 10010 8 | 01101 9 | 10 | 104 0 7 h 11 | 1000 12 | 1000 13 | 1110 14 | 1001 15 | 1001 16 | 1001 17 | 1001 18 | 19 | 110 0 5 n 20 | 1110 21 | 1001 22 | 1001 23 | 1001 24 | 1001 25 | 26 | 114 0 5 r 27 | 1110 28 | 1001 29 | 1000 30 | 1000 31 | 1000 32 | 33 | -------------------------------------------------------------------------------- /client/font/specific.simpleglyphs: -------------------------------------------------------------------------------- 1 | 32 0 1 2 | 00 3 | 4 | 45 1 4 - 5 | 111 6 | 7 | 128 0 1 ... 8 | 10101 9 | 10 | 129 0 5 --> 11 | 00010 12 | 11111 13 | 00010 14 | 15 | 130 0 7 deg 16 | 010 17 | 101 18 | 010 19 | 20 | 140 0 1 ... 21 | 000 22 | 23 | 141 0 7 ... 24 | 111 25 | 26 | 142 0 7 ... 27 | 111 28 | 111 29 | 30 | 143 0 7 ... 31 | 111 32 | 111 33 | 111 34 | 35 | 144 0 7 ... 36 | 111 37 | 111 38 | 111 39 | 111 40 | 41 | 145 0 7 ... 42 | 111 43 | 111 44 | 111 45 | 111 46 | 111 47 | 48 | 146 0 7 ... 49 | 111 50 | 111 51 | 111 52 | 111 53 | 111 54 | 111 55 | 56 | 147 0 7 ... 57 | 111 58 | 111 59 | 111 60 | 111 61 | 111 62 | 111 63 | 111 64 | 65 | -------------------------------------------------------------------------------- /contrib/lightsense.rb: -------------------------------------------------------------------------------- 1 | #!/usr/bin/ruby 2 | # Read numbers from a stdndard input, and erase/create a file specified if the 3 | # numbers are over/under a certain threashold. 4 | # 5 | # Use with morning_room.rb to shut down a sign when it's dark. 6 | 7 | require 'optparse' 8 | 9 | options = { 10 | :threshold => 100, 11 | } 12 | OptionParser.new do |opts| 13 | opts.banner = "Usage: lightsense.rb --file /tmp/light --threshold 100" 14 | 15 | opts.on('--threshold NUMBER', "Threshold. If the reading is above it, remove the file") {|v| options[:threshold] = v.to_i} 16 | opts.on('--file file_name', "File name to maintain created/erased") {|v| options[:file] = v} 17 | end.parse! 18 | 19 | raise "Specify file!" unless options[:file] 20 | 21 | ARGF.each do |line| 22 | # Remove all except numbers (and get rid of non-utf garbage). 23 | number = line.encode(Encoding.find('ASCII')).gsub(/[^0-9]/,'') 24 | # Do not allow an empty line be confused with a zero reading. 25 | if number != '' 26 | value = number.to_i 27 | if value < options[:threshold] 28 | if not File.exists?(options[:file]) 29 | File.open(options[:file], "w") {|f| f.puts("dark!")} 30 | end 31 | else 32 | # Make sure the file is deleted 33 | File.delete(options[:file]) rescue nil 34 | end 35 | end 36 | end 37 | 38 | -------------------------------------------------------------------------------- /README: -------------------------------------------------------------------------------- 1 | Readme 2 | ====== 3 | 4 | Information, rationale, background, and screenshots are in my blog here: 5 | 6 | http://coldattic.info/shvedsky/pro/blogs/a-foo-walks-into-a-bar/posts/98 7 | 8 | Installation 9 | ============ 10 | 11 | Before you are able to run the sign software, you need to install Perl 12 | and Ruby 1.9 with its "gem" package manager. 13 | 14 | $ sudo apt-get install ruby1.9 gem1.9 15 | $ sudo cpan Device::MiniLED 16 | $ gem install --user-install ./vendor/muni-0.0.6.gem 17 | 18 | To execute the program at startup: 19 | $ sudo cp contrib/munisign /etc/init.d 20 | $ nano /etc/init.d/munisign # Edit file to adjust command line 21 | $ sudo update-rc.d munisign defaults 22 | 23 | 24 | Running 25 | ======= 26 | 27 | There are two executable files in the package: 28 | 29 | - client/client.rb - Muni sign as seen on Muni stops; 30 | - contrib/morning_room.rb - morning dashboard. 31 | 32 | Call each program with '--help' to see the complete list of options, e.g. 33 | "./client/client.rb --help". Sample command lines: 34 | 35 | ./client/client.rb --update-interval 30 --stopId 16995 36 | 37 | ./contrib/morning_room.rb --update-interval 30 \ 38 | --route 30 --direction outbound --stop 'Townsend & 4th' \ 39 | --weather-url 'http://forecast.weather.gov/MapClick.php?lat=37.776905&lon=-122.395012&FcstType=digitalDWML' \ 40 | --weather-hour 20 41 | 42 | -------------------------------------------------------------------------------- /client/font/genfont.pl: -------------------------------------------------------------------------------- 1 | #!/usr/bin/perl 2 | # In order to not introduce extra dependencies such as Freetype O_o, I just 3 | # convert the font into simpler and more editable format. Besides, I add some 4 | # of my own glyphs. 5 | # 6 | # ... 7 | # 101 8 | # 110 9 | # 111 10 | # <---- empty line 11 | # 12 | # 13 | # Shifts are coords of top left corner as viewed from baseline (not from top). 14 | 15 | use Font::FreeType; 16 | my $freetype = Font::FreeType->new; 17 | # I have no idea how this stuff works, but this font and size 10 give 7x7 18 | # bitmaps! 19 | my $face = $freetype->face($ARGV[0]); 20 | my $sz = $ARGV[1] || 10; 21 | $face->set_pixel_size($sz, $sz); 22 | 23 | # $face->set_char_size($sz, $sz, 100, 100); 24 | 25 | for my $c (1..127) { 26 | my $glyph = $face->glyph_from_char_code($c); 27 | # Do not print the unprintable. 28 | next unless $glyph; 29 | my ($bitmap, $left, $top) = $glyph->bitmap(FT_RENDER_MODE_MONO); 30 | $,=' '; 31 | $\="\n"; 32 | # Do not print characters that are invisible. 33 | my $packed = ($c >= 32) ? chr($c) : ''; 34 | print $c, $left, $top, $packed; 35 | for my $line (@$bitmap) { 36 | # This is silly, but I don't know a better way to convert a binary string to 37 | # 0s and 1s. 38 | my $byte_line = unpack('H*', $line); 39 | $byte_line =~ s/ff/1/g; 40 | $byte_line =~ s/00/0/g; 41 | print $byte_line; 42 | } 43 | print; 44 | } 45 | 46 | -------------------------------------------------------------------------------- /client/lib/sign.rb: -------------------------------------------------------------------------------- 1 | # Ruby interface to Muni sign. Relied on a perl wrapper over the official Perl 2 | # API. 3 | require_relative 'enhanced_open3' 4 | 5 | class LED_Sign 6 | SCRIPT = File.join(File.dirname(__FILE__), '..', '..', 'client', 'lowlevel.pl') 7 | def self.text(data) 8 | draw = ['/usr/bin/perl', SCRIPT, '--type=text'] 9 | print = proc {|line| $stderr.puts line} 10 | EnhancedOpen3.open3_input_linewise(data, print, print, *draw) 11 | end 12 | 13 | def self.pic(data) 14 | draw = ['/usr/bin/perl', SCRIPT, '--type=pic'] 15 | print = proc {|line| $stderr.puts line} 16 | EnhancedOpen3.open3_input_linewise(data, print, print, *draw) 17 | end 18 | 19 | # Sign dimensions (to aid in text formatting). 20 | # The sign I have has a peculiarity that if the picture width is about ~50px, 21 | # then it aligns the text a bit to the left. We'll need the width to render 22 | # the text up to it. 23 | SCREEN_WIDTH = 96 24 | SCREEN_HEIGHT = 16 25 | end 26 | 27 | # Supply Array with a conversion function that makes input to LED_Sign.pic out 28 | # of a two-dimensional array. 29 | class Array 30 | def zero_one 31 | map{|row| row.join('')}.join("\n") 32 | end 33 | end 34 | 35 | # Darken the sign if dark_file exists. 36 | # Return true if sign has been darkened. 37 | def darken_if_necessary(options) 38 | dark_file = options[:dark_file] 39 | if dark_file && File.exists?(dark_file) 40 | # We can't "turn off" the sign, but we can send it an empty picture. 41 | LED_Sign.pic("0\n") 42 | return true 43 | end 44 | return false 45 | end 46 | 47 | -------------------------------------------------------------------------------- /contrib/lightsensor: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # Daemon to watch for light sensor and update the file if it's too dark. 3 | # 4 | # If your user is 'pi', will write pidfile to ~pi/.munisign.lightsensor.pid 5 | set -e 6 | 7 | USER=pi 8 | USER_DIR="/home/$USER" 9 | # Replace with your data--or with a different script! 10 | SCRIPT="$USER_DIR/sign/src/contrib/lightsense.rb" 11 | USER=pi 12 | DARK_FILE=/tmp/muni-$USER-dark 13 | # File to read a stream of numbers from. 14 | DEVICE=/dev/ttyUSB1 15 | # If the value is less, touch the DARK_FILE 16 | THRESHOLD=100 17 | 18 | declare -a SCRIPT_OPTIONS=(\ 19 | --file "$DARK_FILE" \ 20 | --threshold "$THRESHOLD" \ 21 | "$DEVICE" \ 22 | ) 23 | 24 | PIDFILE=$USER_DIR/.munisign.darkfile.pid 25 | ERROR_FILE=/dev/null 26 | 27 | export PATH="${PATH:+$PATH:}/usr/sbin:/sbin" 28 | 29 | case "$1" in 30 | start) 31 | echo "Starting light sensor watch" 32 | start-stop-daemon --start --chdir "$USER_DIR" --quiet --chuid "$USER" --pidfile "$PIDFILE" --make-pidfile \ 33 | --exec "$SCRIPT" -- "${SCRIPT_OPTIONS[@]}" >/dev/null 2>$ERROR_FILE & 34 | ;; 35 | stop) 36 | echo "Stopping light sensor watch" 37 | start-stop-daemon --stop --chdir "$USER_DIR" --quiet --chuid "$USER" --oknodo --pidfile "$PIDFILE" 38 | ;; 39 | restart) 40 | echo "Restarting light sensor watch" 41 | start-stop-daemon --stop --chdir "$USER_DIR" --quiet --chuid "$USER" --oknodo --retry 30 --pidfile "$PIDFILE" 42 | start-stop-daemon --start --chdir "$USER_DIR" --quiet --chuid "$USER" --pidfile "$PIDFILE" --make-pidfile \ 43 | --exec "$SCRIPT" -- "${SIGN_OPTIONS[@]}" >/dev/null 2>$ERROR_FILE & 44 | ;; 45 | 46 | *) 47 | echo "Usage: "$0" {start|stop|restart}" 48 | exit 1 49 | esac 50 | 51 | exit 0 52 | 53 | -------------------------------------------------------------------------------- /contrib/munisign: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # Daemon to auto-start up muni sign on Raspberry Pi load. 3 | # 4 | # If your user is 'pi', will write pidfile to ~pi/.munisign.pid. 5 | set -e 6 | 7 | USER=pi 8 | USER_DIR="/home/$USER" 9 | PIDFILE=$USER_DIR/.munisign.pid 10 | ERROR_FILE=/tmp/null 11 | 12 | # Replace with your data--or with a different script! 13 | SCRIPT="$USER_DIR/sign/src/contrib/morning_room.rb" 14 | declare -a SIGN_OPTIONS=(\ 15 | --update-interval 30 \ 16 | --route 30 --direction outbound --stop 'Townsend & 4th' \ 17 | --backup-route 45 --backup-direction outbound --backup-stop 'Townsend & 4th' \ 18 | --weather-url 'http://forecast.weather.gov/MapClick.php?lat=37.776905&lon=-122.395012&FcstType=digitalDWML' \ 19 | --weather-hour 20 ) 20 | 21 | # Uncomment these if you need muni sign emulation. 22 | # SCRIPT="$USER_DIR/sign/src/client/client.rb" 23 | # declare -a SIGN_OPTIONS=( --update-interval 30 --stopId 16995 ) 24 | 25 | # Darken the sign based on whether a specific file exists. 26 | DARK_FILE=/tmp/muni-$USER-dark 27 | SIGN_OPTIONS+=(--dark-file "$DARK_FILE") 28 | # Require the file to be under an active update. 29 | rm -f "$DARK_FILE" 30 | 31 | export PATH="${PATH:+$PATH:}/usr/sbin:/sbin" 32 | 33 | case "$1" in 34 | start) 35 | echo "Starting muni sign" 36 | start-stop-daemon --start --chdir "$USER_DIR" --quiet --chuid "$USER" --pidfile "$PIDFILE" --make-pidfile \ 37 | --exec "$SCRIPT" -- "${SIGN_OPTIONS[@]}" >/dev/null 2>$ERROR_FILE & 38 | ;; 39 | stop) 40 | echo "Stopping muni sign" 41 | start-stop-daemon --stop --chdir "$USER_DIR" --quiet --chuid "$USER" --oknodo --pidfile "$PIDFILE" 42 | ;; 43 | restart) 44 | echo "Restarting muni sign" 45 | start-stop-daemon --stop --chdir "$USER_DIR" --quiet --chuid "$USER" --oknodo --retry 30 --pidfile "$PIDFILE" 46 | start-stop-daemon --start --chdir "$USER_DIR" --quiet --chuid "$USER" --pidfile "$PIDFILE" --make-pidfile \ 47 | --exec "$SCRIPT" -- "${SIGN_OPTIONS[@]}" >/dev/null 2>$ERROR_FILE & 48 | ;; 49 | 50 | *) 51 | echo "Usage: "$0" {start|stop|restart}" 52 | exit 1 53 | esac 54 | 55 | exit 0 56 | -------------------------------------------------------------------------------- /client/lowlevel.pl: -------------------------------------------------------------------------------- 1 | #!/usr/bin/perl -w 2 | # This script simply udpates the sign with more data. If you need to write a 3 | # more sophisticated client, just call this file from somewhere else. 4 | # 5 | # Usage: 6 | # ./lowlevel.pl --type=text 7 | # Then, supply messages as stdin, in form of pictures comprized out of 0s and 1s 8 | # for --type=pic, or as text for --type=text. Separate messaages by double 9 | # lines. 10 | 11 | use strict qw(subs vars); 12 | use warnings; 13 | use Getopt::Long; 14 | use Device::MiniLED; 15 | my $sign=Device::MiniLED->new(devicetype => "sign"); 16 | 17 | my $type = 'text'; 18 | my $speed = 1; 19 | my $effect = 'hold'; 20 | 21 | my $options_result = GetOptions( 22 | 'type=s' => \$type, 23 | 'speed=i' => \$speed, 24 | 'effect=s' => \$effect, 25 | ); 26 | 27 | my $height = 0; 28 | my $data = ''; 29 | my @messages = (); 30 | while () { 31 | chomp; 32 | # Don't let perl treat a string of a single 0 as false! 33 | if (length($_) > 0) { 34 | $height ++; 35 | $data .= $_; 36 | } 37 | if (length($_) == 0 or eof(STDIN)) { 38 | # Add message if we have some. 39 | push @messages, {data => $data, height => $height} if length($data); 40 | $height = 0; 41 | $data = ''; 42 | } 43 | } 44 | 45 | for my $message_data (@messages) { 46 | if ($type eq 'pic') { 47 | my $data = $message_data->{data}; 48 | my $height = $message_data->{height}; 49 | $data =~ s/[^01]//g; 50 | my $width = int(length($data)/$height); 51 | # This will verify if we have correct number of bits. 52 | my $pic = $sign->addPix( 53 | height => $height, 54 | width => $width, 55 | data => $data, 56 | ); 57 | $sign->addMsg( 58 | data => $pic, 59 | effect => $effect, 60 | # For multiple messages, the speed seems to control transition speed in 61 | # multi-message mode. 62 | speed => $speed, 63 | ); 64 | } else { 65 | $sign->addMsg( 66 | data => $message_data->{data}, 67 | effect => (length($message_data->{data}) > 13) ? 'scroll' : 'hold', 68 | speed => $speed, 69 | ); 70 | } 71 | } 72 | 73 | # # 74 | # # add a text only message 75 | # # 76 | # $sign->addMsg( 77 | # data => "test", 78 | # effect => "scroll", 79 | # speed => 4 80 | # ); 81 | # # 82 | # # create a picture and an icon from built-in clipart 83 | # # 84 | # my $pic=$sign->addPix(clipart => "zen16"); 85 | # my $icon=$sign->addIcon(clipart => "heart16"); 86 | # # 87 | # # add a message with the picture and animated icon we just created 88 | # # 89 | # $sign->addMsg( 90 | # data => "Message 2 with a picture: $pic and an icon: $icon", 91 | # effect => "scroll", 92 | # speed => 3 93 | # ); 94 | $sign->send(device => "/dev/ttyUSB0"); 95 | -------------------------------------------------------------------------------- /client/client.rb: -------------------------------------------------------------------------------- 1 | #!/usr/bin/ruby 2 | 3 | require 'optparse' 4 | 5 | require_relative 'lib' 6 | 7 | font = muni_sign_font(File.join(File.dirname(__FILE__), 'font')) 8 | 9 | options = { 10 | :update_interval => 30, 11 | } 12 | OptionParser.new do |opts| 13 | opts.banner = "Usage: client.rb --route F --direction inbound --stop 'Ferry Building'" 14 | 15 | opts.on('--stopId [STOP_NAME]', "Stop to watch") {|v| options[:stop] = v} 16 | opts.on('--update-interval SECONDS', Integer, "Update sign each number of seconds") {|v| options[:update_interval] = v} 17 | 18 | # Darkening 19 | opts.on('--dark-file [FILENAME]', "Turn off the sign instead of updating, if FILENAME exists") {|v| options[:dark_file] = v} 20 | end.parse! 21 | 22 | 23 | # Returns hash of predictions for this stop in UTC times for all routes. Keys 24 | # are route names, and values are arrays of predictions for that route at this 25 | # stop. 26 | def get_stop_arrivals(stopId) 27 | raise unless stopId 28 | stop = Muni::Stop.new({ :stopId => stopId }) 29 | return stop.predictions_for_all_routes 30 | end 31 | 32 | # Convert from Nextbus format to what it actually displayed on a minu sign. 33 | # Ordered list of regexp -> string pairs. The first regexp to match a 34 | # prediction's dirTag field replaces the route name with the string. 35 | ROUTE_FIXUP_MAP = [ 36 | [ /^KT.*OB/, 'K-Ingleside'], 37 | [ /^KT.*IBMTME/, 'T-Metro East Garage'], 38 | # Let's all all inbound KT-s like this. 39 | [ /^KT.*IB/, 'T-Third Street'], 40 | ] 41 | def fixup_route_name(route_name, prediction) 42 | # For now, just truncate, except for one thing. 43 | unstripped_result = route_name 44 | ROUTE_FIXUP_MAP.each do |regex, fixup| 45 | if regex =~ prediction.dirTag 46 | unstripped_result = fixup 47 | break 48 | end 49 | end 50 | # Strip result 51 | unstripped_result.slice(0, 18) 52 | end 53 | 54 | def update_sign(font, options) 55 | arrival_times = get_stop_arrivals(options[:stop]) 56 | # Only debugging: $stderr.puts arrival_times.inspect 57 | texts_for_sign = [] 58 | arrival_times_text = arrival_times.each do |route, predictions| 59 | # Show first two predictions 60 | prediction_text = predictions.slice(0,2).map(&:muni_time).join(' & ') 61 | unless prediction_text.empty? 62 | # Fixup route name. 63 | route_name = fixup_route_name(route, predictions[0]) 64 | texts_for_sign << font.render_multiline([route_name, prediction_text], 8, :ignore_shift_h => true, :distance => 0, :fixed_width => LED_Sign::SCREEN_WIDTH) 65 | end 66 | end 67 | if texts_for_sign && !texts_for_sign.empty? 68 | text_for_sign = texts_for_sign.map(&:zero_one).join("\n\n") 69 | else 70 | # Empty predictions array: this may be just nighttime. 71 | text_for_sign = font.render_multiline(["No routes", "until next morning."], 8, :ignore_shift_h => true, :distance => 0, :fixed_width => LED_Sign::SCREEN_WIDTH).zero_one 72 | end 73 | LED_Sign.pic(text_for_sign) 74 | end 75 | 76 | while true 77 | begin 78 | darken_if_necessary(options) or update_sign(font, options) 79 | rescue => e 80 | $stderr.puts "Well, we continue despite this error: #{e}\n#{e.backtrace.join("\n")}" 81 | end 82 | sleep(options[:update_interval]) 83 | end 84 | 85 | -------------------------------------------------------------------------------- /client/lib/simplefont.rb: -------------------------------------------------------------------------------- 1 | # Library to work with simple fonts for rendering. 2 | # It seemed easier to write a small font renderer than to tame fontconfig. 3 | 4 | # Reads simple font files generated by scripts/genfont.pl, and renders text. 5 | class SimpleFont 6 | def initialize(data) 7 | @glyphs = {} 8 | load_glyphs(data) 9 | end 10 | 11 | # Load more glyphs from data (as if generated by scripts/genfont.pl). 12 | # Supersedes previous glyphs on clash. 13 | def load_glyphs(data) 14 | lines = data.split("\n") 15 | # Whether we're anticipating a glyph header or a next line of the glyph. 16 | mode = :need_header 17 | # pointer to the record we currently read 18 | write_to = nil 19 | lines.each do |line| 20 | line.chomp! 21 | if (mode == :need_header) and (m = /(\d+) (\d+) (\d+)/.match(line)) 22 | write_to = {:shift_h => m[2].to_i, :shift_v => m[3].to_i} 23 | @glyphs[m[1].to_i] = write_to 24 | mode = :need_line 25 | elsif mode == :need_line 26 | if line.empty? 27 | mode = :need_header 28 | else 29 | # This will write into @glyphs array as write_to references one of its 30 | # elements. 31 | write_to[:bitmap] ||= [] 32 | write_to[:bitmap] << line.split('') 33 | end 34 | end 35 | end 36 | end 37 | 38 | # Render string given the max height above the baseline. Returns rectangular 39 | # array, starting from top-left corner. 40 | # Opts: 41 | # ignore_shift_h - whether to ignore shift_h read from the font. 42 | # fixed_width - make the width exactly this, cropping or pannin the text to 43 | # it. 44 | def render(string, height, opts = {}) 45 | # We'll store, temporarily, bits in buf hash, where hash[[i,j]] is a bit i 46 | # points up, and j points right from the start of the baseline. 47 | buf = {} 48 | width = 0 49 | # Technically, it should be String#split, but we don't support chars >127 50 | # anyway. 51 | string.each_byte do |c_code| 52 | glyph = @glyphs[c_code] 53 | add_shift_h = opts[:ignore_shift_h] ? 0 : glyph[:shift_h] 54 | glyph[:bitmap].each_with_index do |row, i| 55 | row.each_with_index do |bit, j| 56 | bit_row = (glyph[:shift_v] - 1) - i 57 | bit_col = width + j + add_shift_h 58 | buf[[bit_row, bit_col]] = bit 59 | #height = bit_row if height < bit_row 60 | raise "negative value for letter #{c_code}" if bit_row < 0 61 | end 62 | # Compute the new width. 63 | end 64 | width += (glyph[:bitmap][0] || []).length 65 | # Insert interval between letters. 66 | width += 1 + add_shift_h 67 | end 68 | # now render the final array 69 | result = [] 70 | buf.each do |xy, bit| 71 | row = (height - 1) - xy[0] 72 | col = xy[1] 73 | result[row] ||= [] 74 | result[row][col] = bit 75 | end 76 | # Update width from mere maximum width to preset width if any. 77 | text_width = width 78 | image_width = opts[:fixed_width] || width 79 | # Fill nil-s with zeroes. 80 | result.map! do |row| 81 | expanded_row = row || [] 82 | # First, the row may be incomplete, lacking space to the left. Pan it to 83 | # text_width, if necessary. 84 | if expanded_row.size < text_width 85 | expanded_row += [0] * (text_width - expanded_row.size) 86 | end 87 | # Expand (or crop) row up to width. Check how much we should *remove*. 88 | slice_total = expanded_row.size - image_width 89 | # How much to slice, slice more from the right. 90 | slice_l = (slice_total.to_f / 2).floor 91 | if slice_total < 0 92 | # We don't remove, we add! 93 | expanded_row = [0] * (-slice_l) + expanded_row + [0] * (-slice_total+slice_l) 94 | elsif slice_total > 0 95 | expanded_row = expanded_row.slice(slice_l, image_width) 96 | end 97 | # Replace nil-s in this row with zeroes. 98 | expanded_row.map{|bit| bit || 0} 99 | end 100 | return result 101 | end 102 | 103 | # Same as render, but renders several lines (it is an array), and places them 104 | # below each other. Accepts the same options as "render," and also these: 105 | # distance: distance between lines in pixels. 106 | def render_multiline(lines, line_height, opts = {}) 107 | line_pics = lines.map {|line| render(line, line_height, opts)} 108 | # Used for debugging. line_pics.each {|lp| $stderr.puts lp.zero_one} 109 | # Compose text out of lines. Center the lines. 110 | # Determine the width of the overall canvas. 111 | width = line_pics.map {|img| (img.first || []).length}.max 112 | # Create wide enough empty canvas. 113 | line_shift = line_height + (opts[:distance] || 1) 114 | canvas = (1..line_shift*lines.length).map do |_| 115 | (1..width).map{|_| 0} 116 | end 117 | # Put each line onto the canvas. 118 | line_pics.each_with_index do |line_pic, line_i| 119 | line_pic.each_with_index do |row, i| 120 | h_shift = (width - row.length) / 2 121 | row.each_with_index do |bit, j| 122 | canvas[line_i*line_shift + i][h_shift + j] = bit 123 | end 124 | end 125 | end 126 | canvas 127 | end 128 | end 129 | 130 | # Returns the default, most useful instance of the font used in signs. 131 | def muni_sign_font(font_path) 132 | # Load generated font. 133 | sf = SimpleFont.new(IO.read(File.join(font_path, '7x7.simpleglyphs'))) 134 | # Load amendments to the letters I don't like. 135 | sf.load_glyphs(IO.read(File.join(font_path, 'amends.simpleglyphs'))) 136 | # Load local, application-specific glyphs. 137 | sf.load_glyphs(IO.read(File.join(font_path, 'specific.simpleglyphs'))) 138 | 139 | return sf 140 | end 141 | 142 | 143 | -------------------------------------------------------------------------------- /contrib/morning_room.rb: -------------------------------------------------------------------------------- 1 | #!/usr/bin/ruby 2 | # Client to help you in the morning. 3 | # 4 | # Displays departure time from one or two stops and outside temperature. 5 | 6 | require 'optparse' 7 | require_relative '../client/lib' 8 | 9 | require 'xmlsimple' 10 | 11 | font = muni_sign_font(File.join(File.dirname(__FILE__), '..', 'client', 'font')) 12 | 13 | options = { 14 | :bad_timing => 13, 15 | :update_interval => 30, 16 | :weather_hour => 19, 17 | } 18 | OptionParser.new do |opts| 19 | opts.banner = "Usage: morning_room.rb --route F --direction inbound --stop 'Ferry Building'" 20 | 21 | opts.on('--route [ROUTE]', "Route to get predictions for") {|v| options[:route] = v} 22 | opts.on('--direction [inbound/outbound]', "Route direction") {|v| options[:direction] = v} 23 | opts.on('--stop [STOP_NAME]', "Stop to watch") {|v| options[:stop] = v} 24 | opts.on('--timing MINUTES', Integer, "Warn if distance is longer than this.") {|v| options[:bad_timing] = v} 25 | opts.on('--update-interval SECONDS', Integer, "Update sign each number of seconds") {|v| options[:update_interval] = v} 26 | 27 | # Backup route 28 | opts.on('--backup-route [ROUTE]', "Route to get predictions for on 2nd line (3 predictions only)") {|v| options[:route2] = v} 29 | opts.on('--backup-direction [inbound/outbound]', "2nd line route direction") {|v| options[:direction2] = v} 30 | opts.on('--backup-stop [STOP_NAME]', "2nd line stop to watch") {|v| options[:stop2] = v} 31 | 32 | #Weather 33 | opts.on('--weather-url [URL]', "The url from weather.gov to fetch a weather from. Click on Tabular Weather, and choose XML.") {|v| options[:weather_xml] = v} 34 | opts.on('--weather-hour [HOUR]', "Hour of the day (0..23) to get the weather of") {|v| options[:weather_hour] = v} 35 | 36 | # Darkening 37 | opts.on('--dark-file [FILENAME]', "Turn off the sign instead of updating, if FILENAME exists") {|v| options[:dark_file] = v} 38 | end.parse! 39 | 40 | # Returns array of predictions for this route, direction, and stop in UTC times. 41 | # in_out is 'inbound' for inbound routes, or 'outbound' 42 | def get_arrival_times(route, stop, in_out) 43 | raise unless route and stop and in_out 44 | route_handler = Muni::Route.find(route) 45 | stop_handler = route_handler.send(in_out.to_sym).stop_at(stop) 46 | raise "Couldn't find stop: found '#{stop_handler.title}' for '#{stop}'" if 47 | stop != stop_handler.title 48 | return stop_handler.predictions.map(&:time) 49 | end 50 | 51 | def update_sign(font, options) 52 | # Render these times 53 | def prediction_string(arrival_times, options) 54 | puts arrival_times.inspect 55 | predictions = arrival_times.map{|t| ((t - Time.now)/60).floor} 56 | 57 | predictions_str = '' 58 | prev = 0 59 | first = true 60 | 61 | for t in predictions do 62 | # Add ellipsis between predictions if distance's too long. 63 | # 31 is a specific charater defined in specific.simpleglyphs 64 | if not first 65 | predictions_str << "#{((t-prev) >= options[:bad_timing])? 128.chr : '-'}" 66 | end 67 | first = false 68 | predictions_str << "#{t}" 69 | prev = t 70 | end 71 | 72 | return predictions_str 73 | end 74 | 75 | arrival_times = get_arrival_times(options[:route], options[:stop], options[:direction]) 76 | line1 = "#{options[:route]}:#{prediction_string(arrival_times, options)}" 77 | 78 | if options[:route2] 79 | arrival_times = get_arrival_times(options[:route2], options[:stop2], options[:direction2]) 80 | arrival_times = arrival_times.slice(0, 3) 81 | line2 = "#{options[:route2]}:#{prediction_string(arrival_times, options)}" 82 | else 83 | line2 = "" 84 | end 85 | 86 | # Get weather. 87 | if options[:weather_xml]; begin 88 | # Load forecast. TODO: add throttling (it hardly changes every 30 seconds). 89 | url = options[:weather_xml] 90 | xml = Net::HTTP.get(URI.parse(url)) 91 | doc = XmlSimple.xml_in(xml) 92 | # Pad hour with zero. 93 | hour = sprintf("%2d", options[:weather_hour]) 94 | # Find table cell index that represents the hour we're interested in. 95 | time_index = doc['data'].first['time-layout'].first['start-valid-time'].find_index {|t| t =~ /T#{hour}/ } 96 | # Now find the actual temperature at that hour. 97 | weather_later = doc['data'].first['parameters'].first['temperature'].first['value'][time_index] 98 | # And the current temperature, too (it's in the first cell). 99 | weather_now = doc['data'].first['parameters'].first['temperature'].first['value'][0] 100 | 101 | # Get rain conditions 102 | begin 103 | conditions = doc['data'].first['parameters'].first['weather'].first['weather-conditions'][time_index] 104 | rain = (conditions['value'] || []).find {|c| c['weather-type'] == 'rain'} 105 | if rain 106 | coverage_map = { 107 | # Todo: uncover more rainfall phrases! 108 | 'slight chance' => 141.chr, 109 | 'chance' => 143.chr, 110 | 'likely' => 146.chr, 111 | } 112 | # Display "?" if the rainfall string is not recognized (otherwise I'd 113 | # not see that there's a chance of rain in such cases). 114 | rain_str = coverage_map[rain['coverage'] || "?"] 115 | end 116 | rescue => e 117 | $stderr.puts "Weather error received: #{e}\n#{e.backtrace.join("\n")}" 118 | rain_str = '' 119 | end 120 | 121 | weather_str = "#{130.chr}#{weather_now}#{129.chr}#{weather_later}#{rain_str}" 122 | rescue => e 123 | # We rescue on various key errors, and inavailability. Turn this on for 124 | # debugging. 125 | # $stderr.puts "Weather error received: #{e}\n#{e.backtrace.join("\n")}" 126 | weather_str = 'E' 127 | end; end 128 | 129 | line2 << " #{weather_str}" 130 | 131 | LED_Sign.pic(font.render_multiline([line1, line2], 8, :ignore_shift_h => true, :distance => 0, :fixed_width => LED_Sign::SCREEN_WIDTH).zero_one) 132 | end 133 | 134 | while true 135 | begin 136 | darken_if_necessary(options) or update_sign(font, options) 137 | rescue => e 138 | $stderr.puts "Well, we continue despite this error: #{e}\n#{e.backtrace.join("\n")}" 139 | end 140 | sleep(options[:update_interval]) 141 | end 142 | 143 | -------------------------------------------------------------------------------- /client/font/7x7.simpleglyphs: -------------------------------------------------------------------------------- 1 | 33 2 7 ! 2 | 1 3 | 1 4 | 1 5 | 1 6 | 1 7 | 0 8 | 1 9 | 10 | 34 1 7 " 11 | 101 12 | 101 13 | 14 | 35 0 7 # 15 | 01010 16 | 01010 17 | 11111 18 | 01010 19 | 11111 20 | 01010 21 | 01010 22 | 23 | 36 0 7 $ 24 | 00100 25 | 01110 26 | 10000 27 | 01110 28 | 00001 29 | 01110 30 | 00100 31 | 32 | 37 0 7 % 33 | 11001 34 | 11001 35 | 00011 36 | 00100 37 | 01000 38 | 10011 39 | 10011 40 | 41 | 38 0 7 & 42 | 00100 43 | 01111 44 | 10000 45 | 01111 46 | 10000 47 | 01111 48 | 00100 49 | 50 | 39 2 7 ' 51 | 1 52 | 1 53 | 54 | 40 1 7 ( 55 | 001 56 | 010 57 | 100 58 | 100 59 | 100 60 | 010 61 | 001 62 | 63 | 41 1 7 ) 64 | 100 65 | 010 66 | 001 67 | 001 68 | 001 69 | 010 70 | 100 71 | 72 | 42 1 7 * 73 | 010 74 | 111 75 | 010 76 | 77 | 43 0 5 + 78 | 00100 79 | 00100 80 | 11111 81 | 00100 82 | 00100 83 | 84 | 44 2 3 , 85 | 01 86 | 01 87 | 10 88 | 89 | 45 1 3 - 90 | 111 91 | 92 | 46 2 2 . 93 | 11 94 | 11 95 | 96 | 47 0 7 / 97 | 00001 98 | 00001 99 | 00010 100 | 00100 101 | 01000 102 | 10000 103 | 10000 104 | 105 | 48 0 7 0 106 | 01110 107 | 10001 108 | 10001 109 | 10001 110 | 10001 111 | 10001 112 | 01110 113 | 114 | 49 1 7 1 115 | 010 116 | 110 117 | 010 118 | 010 119 | 010 120 | 010 121 | 111 122 | 123 | 50 0 7 2 124 | 01110 125 | 10001 126 | 00001 127 | 00010 128 | 00100 129 | 01000 130 | 11111 131 | 132 | 51 0 7 3 133 | 01110 134 | 10001 135 | 00001 136 | 01110 137 | 00001 138 | 10001 139 | 01110 140 | 141 | 52 0 7 4 142 | 00010 143 | 00110 144 | 01010 145 | 10010 146 | 11111 147 | 00010 148 | 00010 149 | 150 | 53 0 7 5 151 | 11111 152 | 10000 153 | 11110 154 | 00001 155 | 00001 156 | 10001 157 | 01110 158 | 159 | 54 0 7 6 160 | 00110 161 | 01000 162 | 10000 163 | 11110 164 | 10001 165 | 10001 166 | 01110 167 | 168 | 55 0 7 7 169 | 11111 170 | 00001 171 | 00010 172 | 00100 173 | 01000 174 | 10000 175 | 10000 176 | 177 | 56 0 7 8 178 | 01110 179 | 10001 180 | 10001 181 | 01110 182 | 10001 183 | 10001 184 | 01110 185 | 186 | 57 0 7 9 187 | 01110 188 | 10001 189 | 10001 190 | 01111 191 | 00001 192 | 00010 193 | 01100 194 | 195 | 58 2 5 : 196 | 11 197 | 11 198 | 00 199 | 11 200 | 11 201 | 202 | 59 2 6 ; 203 | 01 204 | 01 205 | 00 206 | 01 207 | 01 208 | 10 209 | 210 | 60 1 7 < 211 | 0001 212 | 0010 213 | 0100 214 | 1000 215 | 0100 216 | 0010 217 | 0001 218 | 219 | 61 0 4 = 220 | 11111 221 | 00000 222 | 11111 223 | 224 | 62 1 7 > 225 | 1000 226 | 0100 227 | 0010 228 | 0001 229 | 0010 230 | 0100 231 | 1000 232 | 233 | 63 0 7 ? 234 | 01110 235 | 10001 236 | 00001 237 | 00010 238 | 00100 239 | 00000 240 | 00100 241 | 242 | 64 0 7 @ 243 | 01110 244 | 10001 245 | 10111 246 | 10101 247 | 10111 248 | 10000 249 | 01111 250 | 251 | 65 0 7 A 252 | 01110 253 | 10001 254 | 10001 255 | 11111 256 | 10001 257 | 10001 258 | 10001 259 | 260 | 66 0 7 B 261 | 11110 262 | 10001 263 | 10001 264 | 11110 265 | 10001 266 | 10001 267 | 11110 268 | 269 | 67 0 7 C 270 | 01110 271 | 10001 272 | 10000 273 | 10000 274 | 10000 275 | 10001 276 | 01110 277 | 278 | 68 0 7 D 279 | 11110 280 | 10001 281 | 10001 282 | 10001 283 | 10001 284 | 10001 285 | 11110 286 | 287 | 69 0 7 E 288 | 11111 289 | 10000 290 | 10000 291 | 11111 292 | 10000 293 | 10000 294 | 11111 295 | 296 | 70 0 7 F 297 | 11111 298 | 10000 299 | 10000 300 | 11111 301 | 10000 302 | 10000 303 | 10000 304 | 305 | 71 0 7 G 306 | 01111 307 | 10000 308 | 10000 309 | 10011 310 | 10001 311 | 10001 312 | 01111 313 | 314 | 72 0 7 H 315 | 10001 316 | 10001 317 | 10001 318 | 11111 319 | 10001 320 | 10001 321 | 10001 322 | 323 | 73 1 7 I 324 | 111 325 | 010 326 | 010 327 | 010 328 | 010 329 | 010 330 | 111 331 | 332 | 74 0 7 J 333 | 00001 334 | 00001 335 | 00001 336 | 00001 337 | 00001 338 | 10001 339 | 01110 340 | 341 | 75 0 7 K 342 | 10001 343 | 10010 344 | 10100 345 | 11000 346 | 10100 347 | 10010 348 | 10001 349 | 350 | 76 0 7 L 351 | 10000 352 | 10000 353 | 10000 354 | 10000 355 | 10000 356 | 10000 357 | 11111 358 | 359 | 77 0 7 M 360 | 10001 361 | 11011 362 | 10101 363 | 10101 364 | 10001 365 | 10001 366 | 10001 367 | 368 | 78 0 7 N 369 | 10001 370 | 11001 371 | 10101 372 | 10011 373 | 10001 374 | 10001 375 | 10001 376 | 377 | 79 0 7 O 378 | 01110 379 | 10001 380 | 10001 381 | 10001 382 | 10001 383 | 10001 384 | 01110 385 | 386 | 80 0 7 P 387 | 11110 388 | 10001 389 | 10001 390 | 11110 391 | 10000 392 | 10000 393 | 10000 394 | 395 | 81 0 7 Q 396 | 01110 397 | 10001 398 | 10001 399 | 10001 400 | 10101 401 | 10010 402 | 01101 403 | 404 | 82 0 7 R 405 | 11110 406 | 10001 407 | 10001 408 | 11110 409 | 10001 410 | 10001 411 | 10001 412 | 413 | 83 0 7 S 414 | 01110 415 | 10001 416 | 10000 417 | 01110 418 | 00001 419 | 10001 420 | 01110 421 | 422 | 84 0 7 T 423 | 11111 424 | 00100 425 | 00100 426 | 00100 427 | 00100 428 | 00100 429 | 00100 430 | 431 | 85 0 7 U 432 | 10001 433 | 10001 434 | 10001 435 | 10001 436 | 10001 437 | 10001 438 | 01110 439 | 440 | 86 0 7 V 441 | 10001 442 | 10001 443 | 10001 444 | 10001 445 | 10001 446 | 01010 447 | 00100 448 | 449 | 87 0 7 W 450 | 10001 451 | 10001 452 | 10001 453 | 10101 454 | 10101 455 | 11011 456 | 10001 457 | 458 | 88 0 7 X 459 | 10001 460 | 10001 461 | 01010 462 | 00100 463 | 01010 464 | 10001 465 | 10001 466 | 467 | 89 0 7 Y 468 | 10001 469 | 10001 470 | 10001 471 | 01010 472 | 00100 473 | 00100 474 | 00100 475 | 476 | 90 0 7 Z 477 | 11111 478 | 00001 479 | 00010 480 | 00100 481 | 01000 482 | 10000 483 | 11111 484 | 485 | 91 1 7 [ 486 | 111 487 | 100 488 | 100 489 | 100 490 | 100 491 | 100 492 | 111 493 | 494 | 92 0 7 \ 495 | 10000 496 | 10000 497 | 01000 498 | 00100 499 | 00010 500 | 00001 501 | 00001 502 | 503 | 93 1 7 ] 504 | 111 505 | 001 506 | 001 507 | 001 508 | 001 509 | 001 510 | 111 511 | 512 | 94 0 7 ^ 513 | 00100 514 | 01010 515 | 10001 516 | 517 | 95 0 1 _ 518 | 11111 519 | 520 | 96 2 7 ` 521 | 10 522 | 01 523 | 524 | 97 0 5 a 525 | 01110 526 | 00001 527 | 01111 528 | 10001 529 | 01111 530 | 531 | 98 0 7 b 532 | 10000 533 | 10000 534 | 11110 535 | 10001 536 | 10001 537 | 10001 538 | 11110 539 | 540 | 99 0 5 c 541 | 01110 542 | 10001 543 | 10000 544 | 10001 545 | 01110 546 | 547 | 100 0 7 d 548 | 00001 549 | 00001 550 | 01111 551 | 10001 552 | 10001 553 | 10001 554 | 01111 555 | 556 | 101 0 5 e 557 | 01110 558 | 10001 559 | 11111 560 | 10000 561 | 01110 562 | 563 | 102 0 7 f 564 | 00110 565 | 01001 566 | 01000 567 | 11110 568 | 01000 569 | 01000 570 | 01000 571 | 572 | 103 0 5 g 573 | 01110 574 | 10001 575 | 01111 576 | 00001 577 | 11110 578 | 579 | 104 0 7 h 580 | 10000 581 | 10000 582 | 11110 583 | 10001 584 | 10001 585 | 10001 586 | 10001 587 | 588 | 105 2 7 i 589 | 1 590 | 0 591 | 1 592 | 1 593 | 1 594 | 1 595 | 1 596 | 597 | 106 1 7 j 598 | 0001 599 | 0000 600 | 0001 601 | 0001 602 | 0001 603 | 1001 604 | 0110 605 | 606 | 107 0 7 k 607 | 1000 608 | 1000 609 | 1001 610 | 1010 611 | 1100 612 | 1010 613 | 1001 614 | 615 | 108 2 7 l 616 | 1 617 | 1 618 | 1 619 | 1 620 | 1 621 | 1 622 | 1 623 | 624 | 109 0 5 m 625 | 11110 626 | 10101 627 | 10101 628 | 10101 629 | 10101 630 | 631 | 110 0 5 n 632 | 11110 633 | 10001 634 | 10001 635 | 10001 636 | 10001 637 | 638 | 111 0 5 o 639 | 01110 640 | 10001 641 | 10001 642 | 10001 643 | 01110 644 | 645 | 112 0 5 p 646 | 11110 647 | 10001 648 | 11110 649 | 10000 650 | 10000 651 | 652 | 113 0 5 q 653 | 01111 654 | 10001 655 | 01111 656 | 00001 657 | 00001 658 | 659 | 114 0 5 r 660 | 11110 661 | 10001 662 | 10000 663 | 10000 664 | 10000 665 | 666 | 115 0 5 s 667 | 01111 668 | 10000 669 | 01110 670 | 00001 671 | 11110 672 | 673 | 116 0 6 t 674 | 01000 675 | 11110 676 | 01000 677 | 01000 678 | 01001 679 | 00110 680 | 681 | 117 0 5 u 682 | 10001 683 | 10001 684 | 10001 685 | 10001 686 | 01111 687 | 688 | 118 0 5 v 689 | 10001 690 | 10001 691 | 10001 692 | 01010 693 | 00100 694 | 695 | 119 0 5 w 696 | 10101 697 | 10101 698 | 10101 699 | 10101 700 | 11110 701 | 702 | 120 0 5 x 703 | 10001 704 | 01010 705 | 00100 706 | 01010 707 | 10001 708 | 709 | 121 0 5 y 710 | 10001 711 | 10001 712 | 01111 713 | 00001 714 | 11110 715 | 716 | 122 0 5 z 717 | 11111 718 | 00010 719 | 00100 720 | 01000 721 | 11111 722 | 723 | 123 0 7 { 724 | 0011 725 | 0100 726 | 0100 727 | 1000 728 | 0100 729 | 0100 730 | 0011 731 | 732 | 124 2 7 | 733 | 1 734 | 1 735 | 1 736 | 1 737 | 1 738 | 1 739 | 1 740 | 741 | 125 1 7 } 742 | 1100 743 | 0010 744 | 0010 745 | 0001 746 | 0010 747 | 0010 748 | 1100 749 | 750 | 126 0 7 ~ 751 | 01101 752 | 10110 753 | 754 | -------------------------------------------------------------------------------- /client/lib/enhanced_open3.rb: -------------------------------------------------------------------------------- 1 | # Backport of the ruby 1.9's open3 to 1.8 2 | 3 | module EnhancedOpen3 4 | 5 | # Aside from a usual stuff, you may specify a :fork_callback option 6 | def popen3(*cmd, &block) 7 | if Hash === cmd.last 8 | opts = cmd.pop.dup 9 | else 10 | opts = {} 11 | end 12 | 13 | in_r, in_w = IO.pipe 14 | opts[:in] = in_r 15 | in_w.sync = true 16 | 17 | out_r, out_w = IO.pipe 18 | opts[:out] = out_w 19 | 20 | err_r, err_w = IO.pipe 21 | opts[:err] = err_w 22 | 23 | popen_run(cmd, opts, [in_r, out_w, err_w], [in_w, out_r, err_r], &block) 24 | end 25 | module_function :popen3 26 | 27 | def popen_run(cmd, opts, child_io, parent_io) # :nodoc: 28 | # Backport: merge opts and cmd 29 | cmop = cmd.dup.push opts 30 | pid = fork{ 31 | # Since we inherited all filehandlers, we should close those that belong to the parent 32 | parent_io.each {|io| io.close} 33 | # child 34 | STDIN.reopen(opts[:in]) 35 | STDOUT.reopen(opts[:out]) 36 | STDERR.reopen(opts[:err]) 37 | 38 | opts[:fork_callback].call if opts[:fork_callback] 39 | 40 | exec(*cmd) 41 | } 42 | wait_thr = Process.detach(pid) 43 | # Save PID in thread to comply to Ruby1.9-like api. Crazy, huh? 44 | wait_thr[:pid]=pid 45 | child_io.each {|io| io.close } 46 | result = parent_io.dup.push wait_thr 47 | if defined? yield 48 | begin 49 | return yield(*result) 50 | ensure 51 | parent_io.each{|io| io.close unless io.closed?} 52 | wait_thr.join 53 | end 54 | end 55 | result 56 | end 57 | module_function :popen_run 58 | class << self 59 | private :popen_run 60 | end 61 | 62 | # Open a stream with open3, and invoke a callback when a stream is ready for reading (but may be in EOF mode). Waits till the process terminates, and returns its error code. Callbacks should not block for FDs with data available. 63 | def open3_callbacks(cin_callback, cout_callback, cerr_callback, *args) 64 | code = nil 65 | popen3(*args) do |cin,cout,cerr,thr| 66 | pid = thr[:pid] 67 | # Close input at once, if we don't use it 68 | if cin_callback 69 | in_ss = [cin] 70 | else 71 | in_ss = nil 72 | cin.close_write 73 | end 74 | # If the End-Of-File is reached on all of the streams, then the process might have already ended 75 | non_eof_streams = [cerr,cout] 76 | # Progressive timeout. We assume that probability of task to be shorter is greater than for it to be longer. So we increase timeout interval of select, as with time it's less likely that a task will die in the fixed interval. 77 | sleeps = [ [0.05]*20,[0.1]*5,[0.5]*3,1,2,4].flatten 78 | while non_eof_streams.length > 0 79 | # Get next timeout value from sleeps array until none left 80 | timeout = sleeps.shift || timeout 81 | r = select(non_eof_streams,in_ss,nil,timeout) 82 | # If nothing happened during a timeout, check if the process is alive. 83 | # Perhaps, it's dead, but the pipes are still open, This actually happened by sshfs process, which spawns a child and dies, but the child inherits the in-out-err streams, and does not close them. 84 | unless r 85 | if thr.alive? 86 | # The process is still running, no paniv 87 | next 88 | else 89 | # The process is dead. We consider that it won't print anymore, and thus the further polling the pipes will only lead to a hangup. Thus, breaking. 90 | break 91 | end 92 | end 93 | if r[1].include? cin 94 | begin 95 | # If cin_callback is nil, we wouldn't have get here: cin is instantly closed before polling; see above 96 | case cin_callback[pid,cin] 97 | when :close 98 | # Close the output 99 | cin.close_write 100 | in_ss = [] 101 | when :detach 102 | # Do not close, but do not poll as well 103 | in_ss = [] 104 | # Otherwise, we have written something to CIN, do nothnig 105 | end 106 | rescue EOFError 107 | in_ss = [] 108 | end 109 | end 110 | if r[0].include? cerr 111 | begin 112 | cerr_callback[pid,cerr] 113 | # TODO: in_ss should be filled after callback succeedes 114 | rescue EOFError 115 | non_eof_streams.delete_if {|s| s==cerr} 116 | end 117 | end 118 | if r[0].include? cout 119 | begin 120 | cout_callback[pid,cout] 121 | # TODO: in_ss should be filled after callback succeedes 122 | rescue EOFError 123 | non_eof_streams.delete_if {|s| s==cout} 124 | end 125 | end 126 | end 127 | cin.close_write if in_ss && !in_ss.empty? 128 | # Reap process status 129 | # NOTE: in the ruby 1.8.7 I used this line may block for up to a second (due to internal thread scheduling machanism of Ruby). In 1.9 this waitup is gone. Upgrade your software if you encounter differences. 130 | code = thr.value 131 | end 132 | # Return code, either nil if something bad happened, or the actual return code if we were successful 133 | code 134 | end 135 | module_function :open3_callbacks 136 | 137 | # Read linewise and supply lines to callbacks 138 | # Linewise read can not use "readline" because the following situation may (and did) happen. The process spawned writes some data to stderr, but does not terminate it with a newline. We run a callback for stderr, use readline and block. The process spawned then writes a lot of data to stdout, reaches pipe limit, and blocks as well in a write(stdout) call. Deadlock. So, we use more low-level read. 139 | # No returns are allowed in callbacks (ruby 1.9) 140 | def open3_linewise(cin_callback, cout_callback, cerr_callback, *args) 141 | # Read this number of bytes from stream per nonblocking read 142 | some = 4096 143 | 144 | # Standard output backend 145 | cout_buf = '' 146 | cout_backend = proc do |pid,cout| 147 | cout_buf += cout.readpartial some 148 | while md = /(.*)\n/.match(cout_buf) 149 | #$stderr.puts "feed: #{md[1]}" 150 | cout_callback[md[1]] 151 | cout_buf = md.post_match 152 | end 153 | end 154 | 155 | # standard error backend 156 | cerr_buf = '' 157 | cerr_backend = proc do |pid,cerr| 158 | cerr_buf += cerr.readpartial some 159 | while md = /(.*)\n/.match(cerr_buf) 160 | #$stderr.puts "feed: #{md[1]}" 161 | cerr_callback[md[1]] 162 | cerr_buf = md.post_match 163 | end 164 | end 165 | 166 | # standard input backend 167 | cin_buf = '' 168 | # cin_status may be :read (call the procedure and get string or a new status), or :close (close stdin and do not call the proc anymore), or :detach (do not close cin, and do not handle it anymore: something else will close it). 169 | cin_status = :read 170 | cin_backend = cin_callback.nil?? nil : proc do |pid,cin| 171 | # If the buffer is empty, call the procedure back 172 | # We intentionally supply the stream handler to the callback, as, most likely, the callback would like to access it directly 173 | if cin_buf.empty? && cin_status == :read 174 | cb = cin_callback[pid,cin] 175 | if cb.is_a? String 176 | cin_buf += cb 177 | else 178 | # Treat nil as :close 179 | cin_status = cb || :close 180 | end 181 | end 182 | # If the buffer is still empty, return nil, showing that cin is temporarly excluded from polling 183 | if cin_buf.empty? && cin_status != :read 184 | cin_status 185 | else 186 | # Something is in the buffer. Print a portion of it. 187 | to_print, cin_buf = cin_buf[0..some-1],cin_buf[some..cin_buf.length-1] 188 | cin_buf ||= '' # the previous line would null-ify the buffer if it's less than some 189 | #$stderr.puts "Length: to_print: #{to_print.length}, buf: #{cin_buf.length}" 190 | cin.write to_print 191 | :read 192 | end 193 | end 194 | 195 | retcode = open3_callbacks(cin_backend,cout_backend,cerr_backend,*args) 196 | 197 | # Read the rest of buffers 198 | cout_callback[cout_buf] if cout_buf.length > 0 199 | cerr_callback[cerr_buf] if cerr_buf.length > 0 200 | 201 | retcode 202 | end 203 | module_function :open3_linewise 204 | 205 | def open3_input_linewise(input_string,cout_callback,cerr_callback,*args) 206 | printed = false 207 | cin_callback = proc do 208 | #$stderr.puts "CB: #{printed}" 209 | if printed 210 | nil 211 | else 212 | printed = true 213 | input_string 214 | end 215 | end 216 | 217 | open3_linewise(cin_callback,cout_callback,cerr_callback,*args) 218 | end 219 | module_function :open3_input_linewise 220 | 221 | end 222 | 223 | --------------------------------------------------------------------------------