Beefy Boxes and Bandwidth Generously Provided by pair Networks
Don't ask to ask, just ask

perl daemon accessing serial over usb

by gordonendersby (Initiate)
on May 30, 2012 at 18:37 UTC ( #973357=perlquestion: print w/replies, xml ) Need Help??
gordonendersby has asked for the wisdom of the Perl Monks concerning the following question:

Hi all,

Ive been tasked with creating a little hardware device to control a zoneminder installation. Its just an arduino with a key switch and a couple of status led's connected by a serial usb cable,

This is due to my wifes scout group being broken into recently and they asked me to set up a cctv system that was easy to control for the average scout leader. Hence the key switch box rather than the zoneminder web front end.

Ive written a daemon in perl to run on the ubuntu box in "/usr/sbin" and set up the bash script in init.d to control the service. Its not complete, more error catching and capturing responses from commands over serial but it works as is at the moment apart from as a daemon
Bit out of practice with perl so please excuse any clangers. Im controlling the service cups while debugging but it will be changed to the zoneminder service on deployment

Everything works if I call the script from the command line but if I run the daemon thru init.d, "service zoneswitch start". Im getting nothing traveling over the serial link over the usb cable.

Is there something Im missing in setting up the script to run as a daemon under init.d? The run levels look ok to me
Has anyone successfully communicated over serial usb from a perl daemon?

Heres my daemon:

#! /usr/bin/perl use strict; #use warnings; # Had to turn off warnings as Device::SerialPort threw +warnings after closing and re-using see re_initialise_serial_port use Getopt::Std; use Sys::Syslog qw(:standard :macros); use Device::SerialPort; use Proc::Daemon; Proc::Daemon::Init; #TODO add response checking to write commands #TODO turn into service running on boot after zoneminder, maybe from z +oneminder init.d script # boolean type variables my $true = 1; my $false = 0; my $serviceRunning = $false; my $switchStateOn = $false; # Service to control - Change these to zoneminder specific strings my $serviceName = "cups"; my $serviceStatusCommand = "status"; my $serviceStopCommand = "stop"; my $serviceStartCommand = "start"; # service command response strings my $serviceStoppedResponse = "cupsd is not running"; my $serviceRunningResponse = "cupsd is running"; # commands to control switch box my $ledGreenOn = "ledgrnon"; my $ledGreenOff = "ledgrnoff"; my $ledRedOn = "ledredon"; my $ledRedOff = "ledredoff"; my $ledPending = "ledpending"; my $switchStatus = "swstatus"; # command response my $respGreenOn = "greenon"; my $respGreenOff = "greenoff"; my $respRedOn = "redon"; my $respRedOff = "redoff"; my $respSwitchOn = "on"; my $respSwitchOff = "off"; # Set up the serial port for arduino serial communication # 9600, 81N on the USB ftdi driver my $portPath = "/dev/ttyUSB0"; my $baudRate = 9600; my $dataBits = 8; my $parity = "none"; my $stopBits = 1; my $waitConnect = 5; # How long to wait to retry connecting to serial +port # check for option -d to determin level of logging my %options=(); getopts("d", \%options); # set up logging to syslog my $identity = "ZoneSwitchControl"; openlog ($identity, "ndelay,pid", LOG_DAEMON); if ($options{d}){ #if program called with -d for debugging, allow d +ebug info to go to log setlogmask( LOG_MASK(LOG_DEBUG) | LOG_MASK(LOG_ERR) | LOG_MASK(LOG +_INFO) ); }else{ setlogmask( LOG_MASK(LOG_ERR) | LOG_MASK(LOG_INFO) ); } syslog(LOG_INFO, "Starting ZoneSwitch to control $serviceName service" +); my $port = initialise_serial_port(); #TODO on first boot as part of service init, wait for zoneminder to st +art normally? my $continue = 1; $SIG{TERM} = sub { $continue = 0 }; while ($continue) { #Check to see if service running check_service (); #turn on led's to show state maintain_led_status ($port); #check switch position on or off check_switch ($port); #act on switch state mantain_service(); } sub mantain_service{ my $message = ""; if (($switchStateOn == $true) && ($serviceRunning == $false)){ #start service my $return = start_service(); $message = "Starting service - $serviceName service "; $message = $message . "returning: $return"; } if (($switchStateOn == $false) && ($serviceRunning == $true)){ #stop service my $return = stop_service(); $message = "Stopping service - $serviceName service "; $message = $message . "returning: $return"; } $message eq "" || syslog(LOG_INFO, $message); } sub led_pending { my $port = shift; send_command($port, $ledPending ); return (); } sub red_on { my $port = shift; send_command($port, $ledRedOn ); send_command($port, $ledGreenOff ); return (); } sub green_on { my $port = shift; send_command($port, $ledGreenOn ); send_command($port, $ledRedOff ); return (); } sub send_command{ my $port = shift; my $command = shift; clear_input($port); $port->write($command); $port->write("\n"); #not handling response yet #read and return response return(); } sub check_service { #check service state using back ticks to run command in shell my $return = `service $serviceName $serviceStatusCommand`; my $message = ""; if ( $return =~ m/$serviceRunningResponse/ ) { $serviceRunning = $true; $message = "Service $serviceName is running"; } elsif ( $return =~ m/$serviceStoppedResponse/ ) { $serviceRunning = $false; $message = "Service $serviceName is not running\n"; } ($message eq "") || syslog(LOG_DEBUG, $message); } sub maintain_led_status { my $port = shift; if ( $serviceRunning == $false ) { red_on($port); #clear any response for now } else { green_on($port); #clear any response for now } return (); } sub clear_input{ #by reading anything that may be waiting my $port = shift; my $STALL_DEFAULT = 1; # how many seconds to wait for new input my $timeout = $STALL_DEFAULT; $port->read_char_time(0); # don't wait for each character $port->read_const_time(1000); # 1 second per unfulfilled "read" + call eval { my $chars = 0; my $buffer = ""; while ( $timeout > 0 ) { my ( $count, $saw ) = $port->read(255) # will read _ +up to_ 255 chars or die "Cant connect to serial port"; if ( $count > 0 ) { $chars += $count; $buffer .= $saw; } else { $timeout--; } } }; if ($@) { syslog(LOG_ERR, "Cant get response from ZoneSwitch device "); re_initialise_serial_port(); # Then try reconnecting port to d +evice } } sub check_switch { my $port = shift; my $message = ""; my $STALL_DEFAULT = 1; # how many seconds to wait for new input my $timeout = $STALL_DEFAULT; $port->read_char_time(0); # don't wait for each character $port->read_const_time(1000); # 1 second per unfulfilled "read" + call send_command($port, $switchStatus ); # send command to request swi +tch status eval { my $chars = 0; my $buffer = ""; while ( $timeout > 0 ) { my ( $count, $saw ) = $port->read(255) # will read +_up to_ 255 chars or die "Cant connect to serial port"; # if the por +t has been disconnected or we cant read the port if ( $count > 0 ) { $chars += $count; $buffer .= $saw; # Check here to see if what we want is in the $buf +fer # say "last" if we find it if ( $buffer =~ m/\n/ ) { #print "Switch status returned \"$buffer\"\n"; $timeout = 1; } } else { $timeout--; } } if ( $buffer =~ m/on/ ) { $message = "Switch found to be ON"; $switchStateOn = $true; } else { $message = "Switch found to be OFF"; $switchStateOn = $false; } $message eq "" || syslog(LOG_DEBUG, $message); if ( $timeout == 0 ) { # havnt decided about this yet, it was in the example code #die "Waited $STALL_DEFAULT seconds and never saw what I w +anted\n"; } }; if ($@) { syslog(LOG_ERR, "Cant get response from ZoneSwitch device "); re_initialise_serial_port(); # Then try reconnecting port to d +evice } } sub re_initialise_serial_port{ my $return = start_service(); # Assume someone maybe fiddling with + switchbox and start service $switchStateOn = $true; syslog(LOG_ERR, "Started service $serviceName in case this is deli +berate damage"); # Getting errors from when disconnecting and connect +ing usb lead from switch box # close and undef then re-connecting serial port threw # Use of uninitialized value in subroutine entry at /usr/lib/perl5 +/Device/ line 1732. # # couldnt cleanly destoy original serial connection had to turn of +f warnings at head of script $port->close; undef $port; syslog(LOG_ERR, "Trying to re-initialise connection"); $port = initialise_serial_port(); } sub initialise_serial_port { my $port; while (){ $port = Device::SerialPort->new($portPath); last if ($port); syslog(LOG_ERR, "Serial port not ready waiting $waitConnect se +conds to try reconnect"); sleep($waitConnect); } syslog(LOG_DEBUG, "Serial port connected"); sleep(2); # allow microcontroller in switch box to initialise $port->databits($dataBits); $port->baudrate($baudRate); $port->parity($parity); $port->stopbits($stopBits); return $port; } sub start_service { my $return = `service $serviceName $serviceStartCommand`; return $return; } sub stop_service { my $return = `service $serviceName $serviceStopCommand`; return $return; }

and my init.d bash script:

#! /bin/sh ### BEGIN INIT INFO # Provides: skeleton # Required-Start: $remote_fs $syslog # Required-Stop: $remote_fs $syslog # Default-Start: 2 3 4 5 # Default-Stop: 0 1 6 # Short-Description: Daemon to monitor switch box connected on usb por +t and control zoneminder # Description: # ### END INIT INFO # Author: Gordon Endersby : # # Please remove the "Author" lines above and replace them # with your own name if you copy and modify this script. # Do NOT "set -e" # PATH should only include /usr/* if it runs after the scr +ipt PATH=/sbin:/usr/sbin:/bin:/usr/bin DESC="Daemon to monitor switch box connected on usb port and control z +oneminder" NAME=zoneswitch DAEMON=/usr/sbin/$NAME DAEMON_ARGS="--options args" PIDFILE=/var/run/$ SCRIPTNAME=/etc/init.d/$NAME # Exit if the package is not installed [ -x "$DAEMON" ] || exit 0 # Read configuration variable file if it is present [ -r /etc/default/$NAME ] && . /etc/default/$NAME # Load the VERBOSE setting and other rcS variables . /lib/init/ # Define LSB log_* functions. # Depend on lsb-base (>= 3.0-6) to ensure that this file is present. . /lib/lsb/init-functions # # Function that starts the daemon/service # do_start() { # Return # 0 if daemon has been started # 1 if daemon was already running # 2 if daemon could not be started start-stop-daemon --start --quiet --pidfile $PIDFILE --exec $DAEMO +N --test > /dev/null \ || return 1 start-stop-daemon --start --quiet --pidfile $PIDFILE --exec $DAEMO +N -- \ $DAEMON_ARGS \ || return 2 # Add code here, if necessary, that waits for the process to be re +ady # to handle requests from services started subsequently which depe +nd # on this one. As a last resort, sleep for some time. } # # Function that stops the daemon/service # do_stop() { # Return # 0 if daemon has been stopped # 1 if daemon was already stopped # 2 if daemon could not be stopped # other if a failure occurred start-stop-daemon --stop --quiet --retry=TERM/30/KILL/5 --pidfile +$PIDFILE --name $NAME RETVAL="$?" [ "$RETVAL" = 2 ] && return 2 # Wait for children to finish too if this is a daemon that forks # and if the daemon is only ever run from this initscript. # If the above conditions are not satisfied then add some other co +de # that waits for the process to drop all resources that could be # needed by services started subsequently. A last resort is to # sleep for some time. start-stop-daemon --stop --quiet --oknodo --retry=0/30/KILL/5 --ex +ec $DAEMON [ "$?" = 2 ] && return 2 # Many daemons don't delete their pidfiles when they exit. rm -f $PIDFILE return "$RETVAL" } # # Function that sends a SIGHUP to the daemon/service # do_reload() { # # If the daemon can reload its configuration without # restarting (for example, when it is sent a SIGHUP), # then implement that here. # start-stop-daemon --stop --signal 1 --quiet --pidfile $PIDFILE --n +ame $NAME return 0 } case "$1" in start) [ "$VERBOSE" != no ] && log_daemon_msg "Starting $DESC" "$NAME" do_start case "$?" in 0|1) [ "$VERBOSE" != no ] && log_end_msg 0 ;; 2) [ "$VERBOSE" != no ] && log_end_msg 1 ;; esac ;; stop) [ "$VERBOSE" != no ] && log_daemon_msg "Stopping $DESC" "$NAME" do_stop case "$?" in 0|1) [ "$VERBOSE" != no ] && log_end_msg 0 ;; 2) [ "$VERBOSE" != no ] && log_end_msg 1 ;; esac ;; status) status_of_proc "$DAEMON" "$NAME" && exit 0 || exit $? ;; #reload|force-reload) # # If do_reload() is not implemented then leave this commented out # and leave 'force-reload' as an alias for 'restart'. # #log_daemon_msg "Reloading $DESC" "$NAME" #do_reload #log_end_msg $? #;; restart|force-reload) # # If the "reload" option is implemented then remove the # 'force-reload' alias # log_daemon_msg "Restarting $DESC" "$NAME" do_stop case "$?" in 0|1) do_start case "$?" in 0) log_end_msg 0 ;; 1) log_end_msg 1 ;; # Old process is still running *) log_end_msg 1 ;; # Failed to start esac ;; *) # Failed to stop log_end_msg 1 ;; esac ;; *) #echo "Usage: $SCRIPTNAME {start|stop|restart|reload|force-reload} +" >&2 echo "Usage: $SCRIPTNAME {start|stop|status|restart|force-reload}" + >&2 exit 3 ;; esac :

and the arduino sketch if youre interested:

/* ZoneMinder switch control box */ #define LED_GRN_ON "ledgrnon" #define LED_GRN_OFF "ledgrnoff" #define LED_RED_ON "ledredon" #define LED_RED_OFF "ledredoff" #define LED_PENDING "ledpending" #define SWITCH_STATUS "swstatus" #define SWITCH_PIN 2 #define GREEN_LED_PIN 3 #define RED_LED_PIN 4 String inputString = ""; boolean stringComplete = false; boolean greenLedOn = false; boolean redLedOn = false; boolean ledPendingState = true; // true so led's flash on boot of devi +ce long previousMillis = 0; // for flashing pending state long interval = 1000; // interval at which to blink (milliseconds) boolean fledState = false; void setup() { // initialize serial: Serial.begin(9600); // reserve 20 bytes for the inputString: inputString.reserve(20); pinMode(GREEN_LED_PIN, OUTPUT); pinMode(RED_LED_PIN, OUTPUT); pinMode(SWITCH_PIN, INPUT); } void loop() { if (stringComplete) { ledControlEvent(); inputString = ""; stringComplete = false; } if(ledPendingState == true){ // maintain led pending flashing state unsigned long currentMillis = millis(); if(currentMillis - previousMillis > interval) { // save the last time you blinked the LED previousMillis = currentMillis; // if the LED is off turn it on and vice-versa: if (fledState == LOW) fledState = HIGH; else fledState = LOW; // set the LED with the ledState of the variable: digitalWrite(GREEN_LED_PIN, fledState); digitalWrite(RED_LED_PIN, !fledState); } } } /* SerialEvent occurs whenever a new data comes in the hardware serial RX. This routine is run between each time loop() runs, so using delay inside loop can delay response. Multiple bytes of data may be available. */ void serialEvent() { while (Serial.available()) { // get the new byte: char inChar = (char); // add it to the inputString: inputString += inChar; // if the incoming character is a newline, set a flag // so the main loop can do something about it: if (inChar == '\n') { stringComplete = true; } } } void ledControlEvent(){ //Serial.println(inputString); echo back string, dont want this to h +appen for perlscript if (inputString.indexOf(LED_PENDING)!=-1){ ledPendingState = true; Serial.println("ledpendingon"); } if (inputString.indexOf(LED_GRN_ON)!=-1){ digitalWrite(GREEN_LED_PIN, HIGH); if(ledPendingState == true){ ledPendingState = false; digitalWrite(RED_LED_PIN, LOW); } Serial.println("greenon"); } if (inputString.indexOf(LED_GRN_OFF)!=-1){ ledPendingState = false; digitalWrite(GREEN_LED_PIN, LOW); Serial.println("greenoff"); } if (inputString.indexOf(LED_RED_ON)!=-1){ digitalWrite(RED_LED_PIN, HIGH); if(ledPendingState == true){ ledPendingState = false; digitalWrite(GREEN_LED_PIN, LOW); } Serial.println("redon"); } if (inputString.indexOf(LED_RED_OFF)!=-1){ ledPendingState = false; digitalWrite(RED_LED_PIN, LOW); Serial.println("redoff"); } if (inputString.indexOf(SWITCH_STATUS)!=-1){ if(digitalRead(SWITCH_PIN) == HIGH){ Serial.println("on"); }else{ Serial.println("off"); } } }

Replies are listed 'Best First'.
Re: perl daemon accessing serial over usb
by Eliya (Vicar) on May 30, 2012 at 21:33 UTC

    Hardly anyone is going to wade through all the code and/or set up an environment where they can actually try to replicate the issue themselves.  So, to get more useful replies, you might want to add some more meta info, for example, what you've tried so far to debug the issue, and the results of those steps. Simple things like: does the daemon actually start and keep running when you invoke it via init.d, or does it die?  What (if anything) gets written to the syslog? Any other error messages?  Etc.

    The environment the deamon runs in could be different depending on the way you start it, which might lead to stuff not being found, or similar...   Those ideas might seem trivial to you, but as you haven't said much about them, it's hard to tell whether they really are...  Thus this reply :)

      Thanks for looking. I just put the code in for completeness. As usually you get moaned at for not including it. Ill rephrase my question.

      Its advice Im after from someone who might have done something similar with perl, daemons and serial/usb. What I suspect is that theres a gotcha with Device::SerialPort or something to do with the run levels that Im missing.

      When I run it from the command line directly calling the script with -d for debug. syslog shows all the debug I expect to see and it communicates with the arduino over serial/usb exactly as expected.

      When I run it through inet.d using "sudo service zoneswitch start" with debug set in the inet.d bash script syslog shows all the debug messages as expected but nothing goes over the serial/usb connection to the arduino. Obviously all errors from the script are lost as Im running it as a daemon using Proc::Daemon.

      In both cases there are no errors from the module Device::SerialPort when I pass it the serial port I want. If the device isnt plugged in my script waits and tries again until it is available. So syslog would show waiting debug messages. So the script seems to be connecting to the serial port ok.

      Is that enough info, or is there something Ive missed?

        Got there in the end. Several things caught me out.

        First, dirty test environment. I wasnt making sure that any processes started during testing were killed. Stupid mistake by me caused some confusion while trying to work out what was happening. Of course this shouldnt be able to happen if the pid files worked correctly. see Second point

        Second, pid files part 1 The skeleton bash script in init.d doesnt make it clear that pid files arnt created by start-stop-daemon when you give it the path to the pid file. You have to make sure that you set --make-pidfile to create the pid file or it assumes your service/daemon creates the pid file.

        Third, pid files part 2 When start-stop-daemon is called with --make-pidfile, it doesnt know anything about any forking within your service/script. It creates the pid file with the pid before forking. So any further calls of start-stop-daemon wont work as the pid file is not associated with the forked process.

        As I said got there in the end, just need to put the logic into my perl service/daemon to handle the pid file and locking rather than relying on the init.d script and start-stop-daemon as I assumed you could. Hopefully if someone else tries something like this, the above will help

Log In?

What's my password?
Create A New User
Node Status?
node history
Node Type: perlquestion [id://973357]
Approved by Corion
and all is quiet...

How do I use this? | Other CB clients
Other Users?
Others cooling their heels in the Monastery: (6)
As of 2017-03-30 09:13 GMT
Find Nodes?
    Voting Booth?
    Should Pluto Get Its Planethood Back?

    Results (355 votes). Check out past polls.