#!/usr/bin/env perl use strict; use warnings; use 5.010; use AnyEvent; use AnyEvent::SerialPort; use AnyEvent::ReadLine::Gnu; use Path::Class qw(file dir); use File::HomeDir; use Getopt::Long; use Pod::Usage; our $VERSION = '0.01'; my $HISTSIZE_MAX = $ENV{HISTSIZE} // 1000; exit main() unless caller(); sub main { my ($help); my $serial_fn = '/dev/ttyUSB0'; my $baudrate = 19200; my $parity = 'none'; my $databits = 8; my $stopbits = 1; my $handshake = 'none'; GetOptions( 'baudrate|b=s' => \$baudrate, 'parity|p=s' => \$parity, 'databits|d=s' => \$databits, 'stopbits|s=s' => \$stopbits, 'handshake|s=s' => \$handshake, 'help|h' => \$help, ) or pod2usage(1); pod2usage(0) if $help; if (@ARGV) { pod2usage('too many arguments') if @ARGV > 1; ($serial_fn) = @ARGV; } pod2usage('invalid baudrate') if ($baudrate !~ m{^\d+$}); pod2usage('invalid parity') if ($parity !~ m{^(?:none|odd|even)$}); pod2usage('invalid databits') if ($databits !~ m{^[5-8]$}); pod2usage('invalid stopbits') if ($stopbits !~ m{^[1-2]$}); pod2usage('invalid handshake') if ($handshake !~ m{^(?:none|rts|xoff)$}); # read & truncate history lines my $cfg_dir = dir(File::HomeDir->my_home, '.config', 'sersh'); if (!-d $cfg_dir) { $cfg_dir->mkpath or die 'failed to create "' . $cfg_dir . '": $!'; } my $hist_file = $cfg_dir->file('.sersh_history'); my @histfile_lines = $hist_file->slurp(); if (@histfile_lines > $HISTSIZE_MAX) { splice(@histfile_lines, 0, (@histfile_lines - $HISTSIZE_MAX)); $hist_file->spew(\@histfile_lines); } @histfile_lines = map {chomp($_); $_;} @histfile_lines; my $run_cv = AnyEvent->condvar; my $prompt_prefix = $serial_fn . "> "; my $serial_ae = AnyEvent::SerialPort->new( serial_port => [ $serial_fn, [baudrate => $baudrate], [parity => $parity], [databits => $databits], [stopbits => $stopbits], [handshake => $handshake], ], on_error => sub { my ($hdl, $fatal, $msg) = @_; die $msg; }, on_eof => sub { $run_cv->send; }, ); # print data from serial $serial_ae->on_read( sub { my ($hdl) = @_; AnyEvent::ReadLine::Gnu->print($hdl->rbuf); $hdl->rbuf = ''; } ); my $term_rl = AnyEvent::ReadLine::Gnu->new( prompt => $prompt_prefix, on_line => sub { my ($line) = @_; # EOF if (!defined($line)) { AnyEvent::ReadLine::Gnu->print("\n"); $run_cv->send; return; } # send line to serial $serial_ae->push_write($line . "\n"); # append line to history file use AnyEvent::IO qw(aio_open aio_write :flags); aio_open( $hist_file, (O_WRONLY | O_CREAT | O_APPEND), 0600, sub { my ($fh) = @_ or die "$!"; aio_write($fh, $line . "\n", sub { }); } ); }, on_eof => sub { $run_cv->send; }, ); # add history $term_rl->AddHistory(@histfile_lines); $term_rl->history_set_pos(scalar($term_rl->GetHistory)); # terminate on sighup/term my $term_cb = sub { AnyEvent::ReadLine::Gnu->print("terminating, bye bye\n"); $run_cv->send(); }; my $wt = AE::signal TERM => $term_cb; my $wh = AE::signal HUP => $term_cb; # on Ctrl-C cancel current command my $wi = AE::signal INT => sub { my $old_text = $term_rl->Attribs->{line_buffer}; $term_rl->modifying; $term_rl->delete_text; $term_rl->Attribs->{point} = $term_rl->Attribs->{end} = 0; $term_rl->redisplay; AnyEvent::ReadLine::Gnu->print($prompt_prefix . $old_text . "^C\n"); }; AnyEvent::ReadLine::Gnu->print( 'connected to ', $serial_fn, ', baudrate: ', $baudrate, ', databits: ', $databits, ', parity: ', $parity, ', stobits: ', $stopbits, ', handshake: ', $handshake, "\n" ); $run_cv->recv; return 0; } __END__ =head1 NAME sersh - serial port shell =head1 SYNOPSIS sersh --baudrate 19200 /dev/ttyUSB1 --baudrate [Int] - dft.: 19200 --parity [none|odd|even] - dft.: none --databits [5,6,7,8] - dft: 8 --stopbits [1,2] - dft: 1 --handshake [none,rts,xoff] - dft: none =head1 DESCRIPTION Serial port shell with Gnu readline support for command editing and history. Launch with serial device as argument and then write commands that will be sent over serial port after you hit enter. All input from serial port is printed on the screen as it arrives. =head1 AUTHOR Jozef Kutej, C<< >> =head1 COPYRIGHT & LICENSE Copyright 2020 Jozef Kutej, all rights reserved. This program is free software; you can redistribute it and/or modify it under the same terms as Perl itself. =cut