Anonymous Monk has asked for the wisdom of the Perl Monks concerning the following question:

Dear Monks,

This little perl program uses GPS::NMEA to read data from a generic USB GPS dongle and continuously print out the results. It works great, when it works, which is most of the time, and I've never seen it stop. But sometimes when the program is first run:

  1. nothing happens, nothing. rerun program and it's fine.
  2. one line output (may or may not have altitude). then, more nothing.
The altitude is sometimes missing from the first or second line of output, but once (in many of test runs) I saw it spit this thing out as the altitude: x???x??x???f3Y??c??fF? which I think may be undecoded GPS data from my experiments with Serial Communication in Perl.

Anyway I need this thing to run no matter what and was wondering what's the best technique to achieve total fault tolerance. I tried to catch errors by checking the object and using eval on the hanging method (get_position has this until loop waiting to parse) to no avail. Would it be wise to fork the program and somehow monitor it for activity from the parent so a hanging child process could be restarted or something? How would that be done? Thank you!

#!/usr/bin/perl use strict; use warnings; use GPS::NMEA; use Time::Piece; my $gps = GPS::NMEA->new( Port => '/dev/tty.usbserial-110', # ls -l /dev | grep usb | grep tty Baud => 4800 # can't connect this old unit at 9600 ); STDOUT->autoflush(1); while () { # https://metacpan.org/dist/perl-GPS/source/NMEA.pm#L70 if (my ($ns, $lat, $ew, $lon) = $gps->get_position) { # hangs right +here my $ddmmyy = $gps->{NMEADATA}->{ddmmyy} || '???'; my $time_utc = $gps->{NMEADATA}->{time_utc} || '???'; my $alt_meters = $gps->{NMEADATA}->{alt_meters} || '???'; $time_utc =~ s/\.\d+$//; if (my $t = Time::Piece->strptime("$ddmmyy $time_utc","%d%m%y %H:% +M:%S")){ $time_utc = $t->strftime } print join ' ', $ns, $lat, $ew, $lon, 'A', $alt_meters, $time_utc, + "\n" } } __END__ Output, once per second: N 12.345678 W 123.456789 A 123.4 Mon, 29 Apr 2024 12:34:01 UTC N 12.345678 W 123.456789 A 123.4 Mon, 29 Apr 2024 12:34:02 UTC N 12.345678 W 123.456789 A 123.4 Mon, 29 Apr 2024 12:34:03 UTC

Replies are listed 'Best First'.
Re: Total fault tolerance for perl GPS program
by NERDVANA (Priest) on Apr 30, 2024 at 10:00 UTC
    For the DeLorean dashboard, I used gpsd, which takes care of talking to the device, and then you just stream JSON from a TCP port. Keep in mind that you need 4 satellites fixed in order to get altitude, so when the device first powers up, altitude usually won't be available for a bit.

    I notice that the GPS::NMEA module hasn't been updated since 2009. gpsd was updated in 2022, so there's a good chance that gpsd knows tricks to work around buggy hardware that GPS::NMEA doesn't know. GPS::NMEA seems to use SIGALRM for a timeout on reads, so it *ought* to wake up eventually.

    If you'd like some sample code for accessing gpsd, here are some excerpts from mine. (this is designed to be run from a graphics loop where ->update is called repeatedly every few milliseconds and needs to never hang)

    Disclaimer: there are simpler/cleaner ways to write this code, especially if you use Mojo::IOLoop, IO::Async, or AnyEvent. This is just what I have, from 2014, in a very "get it done" sort of C-programmer mentality.

    use Moo; ... my $json= JSON->new->utf8->allow_nonref; has source => is => 'ro', required => 1, default => sub +{ 'tcp:127.0.0.1:2947' }; sub has_daemon { return defined shift->_socket } sub has_device { return scalar @{ (shift->_devices_packet // + {})->{devices} // [] } } sub has_fix { return defined shift->longitude } has connect_retry_delay => is => 'rw', default => sub { 3 }; has read_fail_count => is => 'rwp', default => sub { 0 }; has last_connect_ts => is => 'rwp', default => sub { 0 }; has last_packet_ts => is => 'rwp', default => sub { 0 }; has last_location_ts => is => 'rwp', default => sub { 0 }; has _source_packed_addr => is => 'lazy'; has _socket => is => 'rw'; has _socket_rbuf => is => 'rw', default => sub { '' }; has _version_packet => is => 'rw'; has _devices_packet => is => 'rw'; has _sky_packet => is => 'rw'; has _tpv_packet => is => 'rw'; ... sub devices { return (shift->_devices_packet // {})->{dev +ices} } sub gps_time_str { return (shift->_tpv_packet // {})->{time} } sub gps_ts { my $self= shift; return undef unless $self->gps_time_str; my $d= $self->gps_time_str; $d =~ s/\.\d\d\dZ$//; return Time::Piece->strptime($d, '%Y-%m-%dT%H:%M:%S')->epoch; } sub latitude { return (shift->_tpv_packet // {})->{lat} } sub longitude { return (shift->_tpv_packet // {})->{lon} } sub satellites { return (shift->_sky_packet // {})->{satelli +tes} } sub lat_km_per_degree { return (shift->_tpv_avg // {})->{lat_deg_km +} } sub lon_km_per_degree { return (shift->_tpv_avg // {})->{lon_deg_km +} } ... sub _build__source_packed_addr { my $self= shift; my $spec= $self->source; if ($spec =~ /^tcp:(.*):(\d+)/) { my ($addr, $port)= ($1, $2); my $packed_addr= inet_aton($addr) // croak "Can't resolve $add +r"; return sockaddr_in($port, $packed_addr); } elsif ($spec =~ /^unix:(.*)/) { return sockaddr_un($1); } else { croak "Unknown address type '$spec'"; } } sub _connect { my $self= shift; $self->_set_last_connect_ts(time_mon); socket(my $sock, AF_INET, SOCK_STREAM, 0) or croak "socket: $!"; connect($sock, $self->_source_packed_addr) or croak "connect: $!"; $sock->blocking(0); $sock->autoflush(1); $self->_socket_rbuf(''); $self->_socket($sock); } sub try_connect { my $self= shift; if ($self->has_daemon) { $self->_reset; } $log->info("connecting to ", $self->source); try { $self->_connect; $self->send('?WATCH={"enable":true,"json":true};'); $log->info("connection succeeded"); 1; } catch { chomp( my $err= $_ ); $log->error("connection failed: $err"); 0; }; } sub update { my $self= shift; local $SIG{PIPE}= 'IGNORE'; if (!defined $self->_socket) { return 0 if defined $self->last_connect_ts and time_mon - $self->last_connect_ts < $self->connect_ret +ry_delay; return 0 unless $self->try_connect; } # Max 5 messages per loop my $i= 0; while (++$i <= 5) { my $msg= $self->_next_line; if (defined $msg) { chomp $msg; try { my $data= $json->decode($msg); $self->_set_last_packet_ts(time_mon); $self->process_message($data); } catch { $log->error("failed to process message \"$msg\": $_"); }; } elsif (!$self->_socket) { return; } else { last; } } # If it's been too long since the last location, reset it to undef $self->_tpv_packet(undef) if time_mon - $self->last_location_ts > 15; # Clear the other two if its been a while since we got anything if (time_mon - $self->last_packet_ts > 20) { if ($self->_devices_packet) { # re-request the device list, but only the first time we c +lear it $self->send('?DEVICE;') if $self->_socket; $self->_devices_packet(undef); } $self->_sky_packet(undef); } # If we have TPV, and it's different from the last one, then add i +t to the # smoothed coordinates. my $tpv= $self->_tpv_packet; my $avg= $self->_tpv_avg; my $t= $self->last_location_ts; if ($tpv && $t > $avg->{ts}) { my $dT= $t - $avg->{ts}; $self->_lat_curve->add_point($t, $tpv->{lat}); $self->_lon_curve->add_point($t, $tpv->{lon}); push @{ $avg->{hist} }, [ $t, $tpv->{lat}, $tpv->{lon} ]; shift @{ $avg->{hist} } if @{ $avg->{hist} } > 6; $avg->{lat}= $self->_lat_curve->evaluate($t); $log->debugf("(x(%.5f) - %.5f) * %.5f + %.5f = %.5f", $t, $self->_lat_curve->hist->[0][0], $self->_lat_curve->_avg_line->[1], $self->_lat_curve->_avg_line->[0], $avg->{lat}); $avg->{lon}= $self->_lon_curve->evaluate($t); $avg->{lat_velocity}= $avg->{lat} - $self->_lat_curve->evaluat +e($t-1); $avg->{lon_velocity}= $avg->{lon} - $self->_lon_curve->evaluat +e($t-1); $avg->{lat_deg_km}= deg2rad(1) * 6371.64; $avg->{lon_deg_km}= haversine_distance($avg->{lat},$avg->{lon} +, $avg->{lat},($avg->{lon}+1)) * 6371.64; $avg->{ts}= $t; } } sub send { my ($self, $msg)= @_; $self->_socket->print("$msg\n"); } sub _next_line { my $self= shift; my $msg= $self->_socket->getline; if (defined $msg) { $self->_set_read_fail_count(0); if ($msg =~ /\n$/) { $msg= $self->_socket_rbuf . $msg; $self->_socket_rbuf(''); return $msg; } # else $self->{_socket_rbuf} .= $msg; } elsif ($!{EWOULDBLOCK} || $!{EAGAIN}) { } elsif ($self->_socket->eof) { $log->info("connection closed"); $self->_reset; } else { $log->info("unexpected error from recv: $!"); if (++$self->{read_fail_count} > 3) { $log->info("too many read failures (".$self->read_fail_cou +nt."), will reconnect"); $self->_reset; } } return undef; } sub _reset { my $self= shift; $self->_socket_rbuf(''); $self->_socket->close if $self->_socket; $self->_socket(undef); $self->_set_read_fail_count(0); $self->_set_last_connect_ts(undef); # Reset data fields $self->_version_packet(undef); $self->_devices_packet(undef); $self->_sky_packet(undef); $self->_tpv_packet(undef); } sub process_message { my ($self, $msg)= @_; my $class= delete $msg->{class}; defined $class or die "Mesage missing 'class'"; if ($class eq 'DEVICES') { $self->_devices_packet($msg); } elsif ($class eq 'DEVICE') { $self->_devices_packet({ devices => [ $msg ]}); } elsif ($class eq 'VERSION') { $self->_version_packet($msg); } elsif ($class eq 'SKY') { $self->_sky_packet($msg); } elsif ($class eq 'TPV') { $self->_tpv_packet($msg); $self->_set_last_location_ts(time_mon); } } 1;
      Thank you for the reply! I was wondering about gpsd at the time of my question and appreciate your good advice. Here's what I've got from a very lazy sort of Perl-programmer mentality :^)
      perl -MJSON::XS -le '$_=`gpspipe -n 5 -w|grep TPV`;$_=decode_json$_;pr +int"$_->{lon} $_->{lat} $_->{alt} $_->{time}"'
      Result: -123.456789012 12.345678901 123.1234 2024-05-03T12:34:56.000Z

      My application requires the automated determination of timezone from GPS:

      perl -MJSON::XS -e '$_=`gpspipe -n 5 -w|grep TPV`;$_=decode_json$_;pri +nt`timezonefinder $_->{lon} $_->{lat}`'
      Result: Your/Timezone
      sudo apt install libopenblas-dev pip3 install timezonefinder export PATH=/home/user/.local/bin:$PATH

      Wait for GPS:
      perl -MJSON::XS -e 'RE:while(){$_=`gpspipe -n 5 -w|grep TPV`;last if$_ +;sleep 1}eval{$_=decode_json$_};if($@){sleep 1;goto RE};print`timezon +efinder $_->{lon} $_->{lat}`'
      Now my computer magically knows the time zone:

      sudo ln -s /usr/share/zoneinfo/Your/Timezone /etc/localtime

        That'll do the trick :-) but there isn't much in the way of detection and remediation in that code, which is what I thought you were aiming for. If you want a rock-solid solution you should probably use a non-blocking socket/pipe and then restarts gpsd if it goes too long without reporting anything. Maybe also add a software-controlled electrical disconnect on the USB port so that you can power-cycle the device :-)
Re: Total fault tolerance for perl GPS program
by hippo (Archbishop) on Apr 30, 2024 at 08:09 UTC
    Would it be wise to fork the program and somehow monitor it for activity from the parent so a hanging child process could be restarted or something?

    You could certainly do that and there are many ways to go about it. But that might not be necessary if a simple alarm would suffice. Have you tried that? Unfortunately without the requisite USB dongle I'm not in a position to test this.


    🦛

Re: Total fault tolerance for perl GPS program
by Anonymous Monk on May 04, 2024 at 22:34 UTC
    My original problem with connecting to GPS with GPS::NMEA has something to do with macOS Sonoma. When tried on linux, GPS::NMEA never fails to connect, but it still does fail to provide altitude sometimes on the first or second poll for reasons explained by NERDVANA. When using gpsd to manage the GPS unit and Net::GPSD3 to contact gpsd there is no problem connecting and it always provides an altitude! Thank you