# -*- mode: text; tab-stop-list: (2 4 40 42); indent-tabs-mode: t -*-
# $Id$
# Simple TAP client with modem dialer.
# Typographical conventions.  As far as l2poe is concerned, hyphens
# are equivalent to underscores in symbol names.
# The general structure and format for his is shamelessly nicked from
# Libero, which lives at .
#------------------------------------------------------------------------------
# The  section defines simple macros.  Macros may be defined
# two ways: assigned and unassigned.
#
# Assigned macros simply assign some text to a macro name.  When the
# macro is invoked, it's replaced with the text that was assigned to
# it.  For example:
#
#   MacroName = "Some Text Here"
#
# Unassigned macros are assumed to be runstate variables.  The
# following assigned and unassigned macros are equivalent:
#
#   MacroName = $_[RUNSTATE]->{MacroName}
#   MacroName
#
# Unassigned macros exist as a convenience.
# 
Port
Pager_ID
Message
Device_Name
Sys  = $_[KERNEL]
Arg0 = $_[ARG0]
Arg1 = $_[ARG1]
Arg2 = $_[ARG2]
#------------------------------------------------------------------------------
# The  section defines code that will be used in event handlers.
# Subs which are only used once could be inlined into the state
# machine definition, but that would make the definition beholden to a
# single language.  It should be possible to rip the Perl bits out of
# l2poe and replace them with a schema like Libero does.
# 
# Multi-line subs look like regular Perl subs, but their names can
# contain hyphens.  Hyphens are translated to underscores when it's
# time to generate code.
sub open-port {
  Port =
    POE::Wheel::ReadWrite->new
      ( Handle       => Async->new( Device => Device_Name,
                                    Speed  => 38400,
                                    Size   => 7,
                                    Parity => 'e',
                                    Stop   => 1,
                                    Vmin   => 1,
                                  ),
        InputFilter  => POE::Filter::Line->new( Literal => "\x0D\x0A" ),
        OutputFilter => POE::Filter::Stream->new(),
        Driver       => POE::Driver::SysRW->new( BlockSize => 1 ),
        InputState   => 'input',
      );
}
# It turns out that a lot of subs are single lines.  The single-line
# sub syntax is relatively new.
sub close-port				delete Port;
sub set-close-settle-delay		Sys->delay( port_settled => 1 );
sub send-first-init-string		&put( Port, 'ATZ' . CR );
sub send-second-init-string		&put( Port, 'ATE0M0' . CR );
sub set-init-timeout			Sys->delay( init_timeout => 5 );
sub clear-init-timeout			Sys->delay( 'init_timeout' );
sub set-redial-timeout			Sys->delay( redial_timeout => 4 );
sub clear-redial-timeout		Sys->delay( 'redial_timeout' );
sub send-dial-command {
  &put( Port, 'ATDT ' . substr(Pager_ID, 3, 3) . '-6683' . CR );
}
sub set-dial-timeout			Sys->delay( dial_timeout => 90 );
sub clear-dial-timeout			Sys->delay( 'dial_timeout' );
sub set-id-timeout			Sys->delay( id_timeout => 2 );
sub clear-id-timeout			Sys->delay( 'id_timeout' );
sub set-state-timeout			Sys->delay( state_timeout => 15 );
sub clear-state-timeout			Sys->delay( 'state_timeout' );
sub send-cr				&put( Port, CR );
sub send-esc-pg1-cr {
  Port->set_input_filter( POE::Filter::Line->new( Literal => CR ) );
  &put( Port, ESC . 'PG1' . CR );
}
sub send-packet {
  &put( Port, &make_packet( Pager_ID, Message ) );
}
sub send-eot-cr				&put( Port, EOT . CR );
sub exit				# does nothing
sub trace-input {
  print STDERR "<<< ", &show($_[ARG0]), "\n";
}
sub store-page-info {
  Device_Name = Arg0;
  Pager_ID    = Arg1;
  Message     = Arg2;
}
#------------------------------------------------------------------------------
# Each  section defines a state machine.  The 
# directive includes the name of the machine being defined, so you can
# define more than one machine in the same file.  This example defines
# a state machine named "tap-machine".
#
# States follow the format:
#
# state-name:
#   event-name   -> optional-default-transition
#     action     -> optional-transition-if-action-is-true
#     action     -> optional-transition-if-action-is-true
#     action     -> optional-transition-if-action-is-true
#     ...
#
# The first outbound transition is followed; subsequent actions are
# ignored.
# 
start:
  enter					-> open-modem
    &store-page-info
open-modem:
  enter
    &open-port
    &set-close-settle-delay
  port-settled				-> init-1
close-modem:
  (template open-modem)
  enter
    &close-port
    &set-close-settle-delay
  port-settled				-> open-modem
init-1:
  enter
    &send-first-init-string
    &set-init-timeout
  init-timeout				-> close-modem
  input
    &trace-input
    /^OK$/				-> init-2
  leave
    &clear-init-timeout
init-2:
  enter
    &send-second-init-string
    &set-init-timeout
  init-timeout				-> close-modem
  input
    &trace-input
    /^OK$/				-> dial
  leave
    &clear-init-timeout
redial:
  enter
    &set-redial-timeout
  redial-timeout			-> dial
dial:
  enter
    &send-dial-command
    &set-dial-timeout
  input
    &trace-input
    /^CONNECT (\d+)/			-> tap-await-id
    /^NO CARRIER$/			-> redial
  dial-timeout				-> close-modem
  leave
    &clear-dial-timeout
tap-await-id:
  enter
    &set-id-timeout
    &set-state-timeout
  id-timeout
    &send-cr
    &set-id-timeout
  state-timeout				-> close-modem
  input
    &trace-input
    /^ID=$/				-> tap-send-page-type
    /^NO CARRIER$/			-> close-modem
  leave
    &clear-id-timeout
    &clear-state-timeout
tap-send-page-type:
  enter
    &set-state-timeout
    &send-esc-pg1-cr
  state-timeout				-> close-modem
  input
    &trace-input
    /^\e\[p$/				-> tap-send-page-packet
    /^NO CARRIER$/			-> close-modem
  leave
    &clear-state-timeout
tap-send-page-packet:
  enter
    &set-state-timeout
    &send-packet
  state-timeout				-> close-modem
  input
    &trace-input
    /^\006$/				-> tap-wait-disconnect
    /^NO CARRIER$/			-> close-modem
  leave
    &clear-state-timeout
tap-wait-disconnect:
  enter
    &set-state-timeout
    &send-eot-cr
  state-timeout				-> done
  input
    &trace-input
    /^\e\004$/				-> done
    /^NO CARRIER$/			-> done
  leave
    &clear-state-timeout
done:
  (inherits close-modem with exit)
  enter
    &close-port
    &set-close-settle-delay
  port-settled				-> exit
# Make a separate state for exiting to test inheritance with done.
exit:
  enter
    &exit
#------------------------------------------------------------------------------
# The  section is like the Force.  It surrounds and
# permeates the state machine(s), and gives them life.  Or something.
#
# The  directive is only valid within a template.  It
# generates the Perl code for a previously defined state machine.
# This comment would be near  in the template, but then
# it would be mysteriously included in the generated code.
# 
#!/usr/bin/perl -w
use strict;
use lib '/home/troc/perl/poe';
use lib '/home/troc/perl/Async';
# Import POE-y things we'll need.  Async is a helper to open serial
# ports.
use POE::Kernel;
use POE::Wheel::ReadWrite;
use POE::Filter::Line;
use POE::Filter::Stream;
use POE::Driver::SysRW;
use POE::NFA;
use Async;
# ASCII constants.
sub STX   () { "\002" }
sub ETX   () { "\003" }
sub EOT   () { "\004" }
sub ENQ   () { "\005" }
sub ACK   () { "\006" }
sub LF    () { "\012" }
sub CR    () { "\015" }
sub NAK   () { "\025" }
sub ESC   () { "\033" }
sub RS    () { "\036" }
sub READY () { ESC . '[p' }
sub DISC  () { ESC . EOT  }
# Generate a showable representation of some data which may not
# necessarily display properly.
sub show {
  my @stuff =
    ( map { ( ($_ lt ' ' or $_ gt '~')
              ? ('<' . unpack('H*', $_) . '>')
              : $_
            )
          }
      split //, shift
    );
  join '', @stuff;
}
# Send some data, but first show it.
sub put {
  my ($port, $stuff) = @_;
  print STDERR ">>> ", &show($stuff), "\n";
  $port->put( $stuff );
}
# Build a TAP packet.  This wraps the pager ID and short message in a
# VISA protocol packet, with a funky sort of checksum.
sub make_packet {
  my ($pager_id, $message) = @_;
  my $packet = STX . $pager_id . CR . $message . CR . ETX;
  my $checksum = substr(unpack('H*', pack('N', unpack('%32C*', $packet))), -3);
  $checksum =~ tr[a-f][:-?];
  $packet .= $checksum . CR;
  return $packet;
}
# Get the pager ID and short message from the command line.
my $pager_id = shift @ARGV;
my $message = "@ARGV";
die "usage: $0 pager-number message"
  unless ( defined $pager_id and length $pager_id
           and defined $message and length $message
         );
# The TAP machine itself, plus some inline code to spawn it.
# 
# 
# Spawn a TAP machine with a pager ID and a message to send it.  Run
# POE until the message is sent, and then exit.  Takes a device
# name... it's possible to spawn several of these at once, each on a
# different port, to page people in parallel.
&spawn_tap_machine( '/dev/com2', $pager_id, $message );
$poe_kernel->run();
exit 0;