in reply to Total fault tolerance for perl GPS program

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;

Replies are listed 'Best First'.
Re^2: Total fault tolerance for perl GPS program
by Anonymous Monk on May 03, 2024 at 11:50 UTC
    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 :-)
          If you want a rock-solid solution you should probably use a non-blocking socket/pipe

        I was just messing around. Net::GPSD3 uses IO::Socket::INET6 to handle it:

        #!/usr/bin/perl use Net::GPSD3; my $g = Net::GPSD3->new or die $!; $g->addHandler(\&tpv); $g->watch; sub tpv{ $t = shift; return unless $t->class eq 'TPV'; print join ' ', $t->timestamp, $t->lat, $t->lon, $t->alt; exit }
        perl -MNet::GPSD3 -le '$g=Net::GPSD3->new;$g->addHandler(\&tpv);sub tp +v{$t=shift;return unless$t->class eq"TPV";print join" ",$t->timestamp +,$t->lat,$t->lon,$t->alt;exit}$g->watch'
          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

        Those are cool ideas but I have to trust the GPS, and GPSD seems very robust, to prevent me from creating a race condition that bricks the device :-)