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;
|