#!/usr/bin/perl # Ezkey.pm # Interface to the Alpha Sign Communications Protocol, EZ KEY II # See http://www.ams-i.com/Pages/97088061.htm # # The purpose of this interface is to objectify and simplify communication # with LED signs like the BetaBrite: http://betabrite.com/ # # @author Matt Sparks package Ezkey; use strict; use Data::Dumper; use IO::Handle; use Date::Format; use POSIX qw/floor/; require Exporter; our @ISA = qw/Exporter/; our @EXPORT = qw/%modes %graphics %positions/; # Display Modes (p89) our %modes = ( "rotate" => "a", "hold" => "b", "flash" => "c", "roll_up" => "e", "roll_down" => "f", "roll_left" => "g", "roll_right" => "h", "wipe_up" => "i", "wipe_down" => "j", "wipe_left" => "k", "wipe_right" => "l", "scroll" => "m", "automode" => "o", "roll_in" => "p", "roll_out" => "q", "wipe_in" => "r", "wipe_out" => "s", "compressed_rotate" => "t", # only available on certain sign models "explode" => "u", # alpha 3.0 protocol "clock" => "v", # alpha 3.0 protocol # Special Modes "twinkle" => "n0", "sparkle" => "n1", "snow" => "n2", "interlock" => "n3", "switch" => "n4", "slide" => "n5", # only Betabrite 1036 (same as CYCLE_COLORS?) "spray" => "n6", "starburst" => "n7", "welcome" => "n8", "slot_machine" => "n9", "news_flash" => "nA", # only Betabrite 1036 "trumpet_animation" => "nb", # only betabrite 1036 "cycle_colors" => "nC", # only AlphaEclipse 3600 # Special Graphics (these display before the message) "thank_you" => "nS", "no_smoking" => "nU", "dont_drive_drink" => "nV", "running_animal" => "nW", "fish_animation" => "nW", "fireworks" => "nX", "turbo_car" => "nY", "balloon_animation" => "nY", "cherry_bomb" => "nZ", ); # Display Positions our %positions = ( "middle_line" => "\x20", "top_line" => "\x22", "bottom_line" => "\x26", "fill" => "\x30", "left" => "\x31", "right" => "\x32", ); # Character Sets our %charsets = ( "five_high_std" => "1", "five_stroke" => "2", "seven_high_std" => "3", "seven_stroke" => "4", "seven_high_fancy" => "5", "ten_high_std" => "6", "seven_shadow" => "7", "full_height_fancy" => "8", "full_height_std" => "9", "seven_shadow_fancy"=> ":", "five_wide" => ";", "seven_wide" => "<", "seven_fancy_wide" => "=", "wide_stroke_five" => ">", # The following four only work on Alpha 2.0 and Alpha 3.0 protocols "five_high_cust" => "W", "seven_high_cust" => "X", "ten_high_cust" => "Y", "fifteen_high_cust" => "Z", ); # Extended characters our %extchars = ( "up_arrow" => "\x64", "down_arrow" => "\x65", "left_arrow" => "\x66", "right_arrow" => "\x67", "pacman" => "\x68", "sail_boat" => "\x69", "ball" => "\x6A", "telephone" => "\x6B", "heart" => "\x6C", "car" => "\x6D", "handicap" => "\x6E", "rhino" => "\x6F", "mug" => "\x70", "satellite_dish" => "\x71", "copyright_symbol" => "\x72", "male_symbol" => "\x73", "female_symbol" => "\x74", "bottle" => "\x75", "diskette" => "\x76", "printer" => "\x77", "musical_note" => "\x78", "infinity_symbol" => "\x79", ); # Counters # We have 5 of them. our %counters = ( 1 => "z", 2 => "{", 3 => "|", 4 => "}", 5 => "-", ); # Colors our %colors = ( "red" => "1", "green" => "2", "amber" => "3", "dim_red" => "4", "dim_green" => "5", "brown" => "6", "orange" => "7", "yellow" => "8", "rainbow_1" => "9", "rainbow_2" => "A", "color_mix" => "B", "autocolor" => "C", ); # Command Codes use constant { WRITE_TEXT => "A", # Write TEXT file (p18) READ_TEXT => "B", # Read TEXT file (p19) WRITE_SPECIAL => "E", # Write SPECIAL FUNCTION commands (p21) READ_SPECIAL => "F", # Read SPECIAL FUNCTION commands (p29) WRITE_STRING => "G", # Write STRING (p37) READ_STRING => "H", # Read STRING (p38) WRITE_SMALL_DOTS => "I", # Write SMALL DOTS PICTURE file (p39) READ_SMALL_DOTS => "J", # Read SMALL DOTS PICTURE file (p41) WRITE_RGB_DOTS => "K", # Write RGB DOTS PICTURE file (p44) READ_RGB_DOTS => "L", # Read RGB DOTS PICTURE file (p46) WRITE_LARGE_DOTS => "M", # Write LARGE DOTS PICTURE file (p42) READ_LARGE_DOTS => "N", # Read LARGE DOTS PICTURE file (p43) WRITE_ALPHAVISION => "O", # Write ALPHAVISION BULLETIN (p48) SET_TIMEOUT => "T", # Set Timeout Message (p118) (Alpha 2.0/3.0) }; # Constants used in transmission packets use constant { NUL => "\x00", # NULL SOH => "\x01", # Start of Header STX => "\x02", # Start of TeXt (precedes a command code) ETX => "\x03", # End of TeXt EOT => "\x04", # End Of Transmission # ENQ => "\x05", # Enquiry # ACK => "\x06", # Acknowledge BEL => "\x07", # Bell BS => "\x08", # Backspace HT => "\x09", # Horizontal tab LF => "\x0A", # Line Feed NL => "\x0A", # New Line VT => "\x0B", # Vertical Tab # FF => "\x0C", # Form Feed # NP => "\x0C", # New Page CR => "\x0D", # Carriage Return CAN => "\x18", # Cancel SUB => "\x1A", # Substitute (select charset) ESC => "\x1B", # Escape character }; # Constructor # - device: device of the LED sign sub new { my($class,$device)=@_; my $self={ "device" => $device, "type" => "Z", # Type Code, see protocol "address" => "00", # Sign Address, see protocol "mode" => "rotate", # Default display mode "position" => "middle_line", # Approrpriate for one-line signs "debug" => 0, # debugging }; return bless $self,$class; } # Connect to the sign (open the serial device) # If no device is known, the default /dev/ttyS0 is selected sub connect { my($this,$device)=@_; $device ||= $this->{device}; if (!$device) { warn "No device specified. Defaulting to /dev/ttyS0.\n"; $device = "/dev/ttyS0"; } # Open a connection to the sign open(OUT,">",$device) or die "Could not open $device for output: $!\n"; OUT->autoflush(1); open(IN,"<",$device) or die "Could not open $device for input: $!\n"; IN->autoflush(1); } # Disconnect from the sign (close the serial device) sub disconnect { my($this)=@_; close OUT; close IN; } sub _packet { my($this,$contents)=@_; return ((NUL x 5) . SOH . $this->{type} . $this->{address} . STX . $contents . EOT); } sub _write { my($this,$packet)=@_; die "Not connected to device. Use \$sign->connect().\n" if !OUT->opened; if ($this->{debug}) { # make human-readable packet for display my $hr = $packet; #for(0..27) { # my $hex = hex $_; # $hr =~ s/\x$hex/[$hex]/g; #} print "Writing packet: $hr\n"; } print OUT $packet; } # Read from the sign # This does not seem to work correctly yet. All read_* functions therefore # do not work. sub _read { my($this)=@_; my $data; sysread IN,$data,1024; return $data; } sub dec2hex { my($dec) = @_; return sprintf("%lx ", $dec ); } sub hex2dec { return hex($_[0]); } # Set display mode # for $mode, use one of the Standard Mode constants exported # if $mode is SPECIAL, set $special_mode to one of the defined special modes sub set_mode { my($this,$mode)=@_; if ($modes{$mode}) { $this->{mode} = $mode; } else { warn "Warning: '$mode' is an invalid mode\n"; } } # Get display mode sub get_mode { my($this)=@_; return $this->{mode}; } # Set display position # This is mostly unimportant for one line signs. sub set_position { my($this,$position)=@_; if ($positions{$position}) { $this->{position} = $position; } else { warn "Warning: '$position' is an invalid position\n"; } } # Get display position sub get_position { my($this)=@_; return $this->{position}; } # Write TEXT to the sign sub write_text { my($this,$msg,$label)=@_; $label ||= "A"; # [WRITE_TEXT][File Label][ESC][Display Position][Mode Code] # [Special Specifier][ASCII Message] my $packet = $this->_packet(WRITE_TEXT . $label . ESC . $positions{$this->{position}} . $modes{$this->{mode}} . $msg); $this->_write($packet); } # Read TEXT from the sign sub read_text { my($this,$label)=@_; $label ||= "A"; $this->_write($this->_packet(READ_TEXT . $label)); return $this->_read(); } # Create a STRING # This is necessary to allocate memory for the STRING on the sign # # $string_label: label of the STRING to create # $string_size: size of the STRING to create, in bytes. 125 max. # Default is 32. sub create_string { my($this,$string_label,$string_size)=@_; $string_label ||= 1; $string_size = 125 if $string_size > 125; $string_size ||= 32; my $size_hex = hex($string_size); $size_hex = "0"x(4-length($size_hex)).$size_hex if length($size_hex) < 4; my $packet = $this->_packet(WRITE_SPECIAL . "\$" . "A" # call label.. why does this matter? . "A" # text file type . "U" # this TEXT file is unlocked . "0100" # text file size in hex . "FF" # text file's start time (FF = always) . "00" # text file's stop time . $string_label . "B" # string file type . "L" # this string file is locked . $size_hex . "0000" # padding ); $this->_write($packet); } # Write a STRING sub write_string { my($this,$data,$label)=@_; $label ||= 1; my $packet = $this->_packet(WRITE_STRING . $label . $data); $this->_write($packet); } sub read_string { my($this,$label)=@_; $label ||= 1; my $packet = $this->_packet(READ_STRING . $label); $this->_write($packet); return $this->_read(); } # Call a STRING # Returns the control code of specified string label. This is for # inserting a STRING file into a TEXT file sub call_string { my($this,$string_label)=@_; $string_label ||= "1"; return "\x10" . $string_label; } # Call Date # Returns the control code for the date to be inserted in a TEXT # $format: integer from 0 to 9 # 0 - MM/DD/YY # 1 - DD/MM/YY # 2 - MM-DD-YY # 3 - DD-MM-YY # 4 - MM.DD.YY # 5 - DD.MM.YY # 6 - MM DD YY # 7 - DD MM YY # 8 - MMM.DD, YYYY # 9 - Day of week # Format defaults to 0 if invalid or not specified sub call_date { my($this,$format); $format ||= 0; $format = 0 if ($format < 0 || $format > 9); return "\x0B" . $format; } # Call Time # Returns control code for the time. sub call_time { my($this)=@_; return "\x13"; } # Clear sign's memory sub clear_memory { my($this)=@_; my $packet = $this->_packet(WRITE_SPECIAL . "\$"); $this->_write($packet); } # Generate a tone/beep # $frequency: frequency of tone to generate, in hex ("00" through "FE") # $duration: duration, in hex, of tone in 0.1s increments ("1" through "F") # $repeat: number of times, in hex, to repeat the tone ("0" through "F") sub beep { my($this,$frequency,$duration,$repeat)=@_; $frequency ||= "10"; $duration ||= "2"; $repeat ||= 0; my $packet = $this->_packet(WRITE_SPECIAL . "(2" . $frequency . $duration . $repeat); $this->_write($packet); } # Perform a soft reset on the sign (does not clear memory; non-destructive) sub soft_reset { my($this)=@_; my $packet = $this->_packet(WRITE_SPECIAL . ","); $this->_write($packet); } # Set the day of the week on the sign # $day must be an integer between 1 and 7. # 1 = Sunday, 2 = Monday, etc. # Omitting the $day parameter will cause today's day to be sent # Returns -1 if an invalid day is specified. sub set_day { my($this,$day)=@_; return -1 if ($day && ($day < 1 || $day > 7)); $day ||= time2str("%w",time)+1; my $packet = $this->_packet(WRITE_SPECIAL . "&" . $day); $this->_write($packet); } # Sets the date in the memory of the sign. This must be done each day to keep # the clock 'up to date', because the sign will not automatically advance the # day. # # NOTE: each of the parameters must be two characters long. # # If no date is specified, today's date will be used. sub set_date { my($this,$year,$month,$day)=@_; $year ||= time2str("%y",time); $month ||= time2str("%m",time); $day ||= time2str("%d",time); my $packet = $this->_packet(WRITE_SPECIAL . ";" . $month . $day . $year); $this->_write($packet); } # Sets the hour and minute of the internal clock on the sign # $h: hour in twenty-four hour format (18 instead of 6 for 6PM) # $m: minute # If no time (or an invalid time) is specified, the current system time will # be used sub set_time { my($this,$h,$m)=@_; $h = "" if ($h < 0 or $h > 23); $m = "" if ($m < 0 or $m > 59); $h ||= time2str("%H",time); $m ||= time2str("%M",time); my $packet = $this->_packet(WRITE_SPECIAL . "\x20" . $h . $m); $this->_write($packet); } # Sets the time format on the sign # $format: 1 - 24-hour (military) time # 0 - 12-hour (standard am/pm) format # 12-hour is the default sub set_time_format { my($this,$format)=@_; $format ||= 0; $format = 0 if ($format > 1 || $format < 0); my $byte = ($format == 0) ? "S" : "M"; my $packet = $this->_packet(WRITE_SPECIAL . "\x27" . $byte); $this->_write($packet); } # Returns color code for a specified color # If an invalid color is specified, autocolor will be used sub color { my($this,$color)=@_; $color = "autocolor" if !$colors{$color}; return "\x1C" . $colors{$color}; } # Returns control code for a specified character set # Defaults to 'five_high_std', Five High Standard sub charset { my($this,$charset)=@_; $charset = "five_high_std" if !$charsets{$charset}; return "\x1A" . $charsets{$charset}; } # Returns control code for a specified extended char # Defaults to 'left_arrow' sub extchar { my($this,$extchar)=@_; $extchar = "left_arrow" if !$extchars{$extchar}; return "\x08" . $extchars{$extchar}; } # Returns control code to set the character spacing. # $option: if 0, set proportional characters (default) # 1, fixed width left justified characters sub spacing { my($this,$option)=@_; my $byte = ($option == 0) ? 0 : 1; return "\x1E" . $byte; } # Set the speed # $speed: integer 1 (slowest) through 5 (fastest) inclusive. sub speed { my($this,$speed)=@_; $speed ||= 1; $speed = 1 if ($speed < 1 || $speed > 5); my $n = 20+$speed; return chr($n); } 1;