Initial release

This commit is contained in:
Frans Veldman 2025-09-17 07:27:26 +00:00
parent 241581c794
commit 78158a5f9c
2 changed files with 395 additions and 1 deletions

103
README.md
View File

@ -1,3 +1,104 @@
# NFLpositionUpdater
Automatic Position Updater for NFL, using the NMEA stream of the boat.
Automatic Position Updater for NFL (https://NoForeignLand.com), using the NMEA stream of your boat.
This script takes the NMEA stream from the UDP/TCP network of the boat, or directly from OpenCPN. It periodically updates the boat position on NoForeignLand.com by sending them a position update e-mail.
The script distuingishes between stationary updates ("at anchor") and while sailing.
When sailing, optionally the speed and course over ground are added to the report. Position updates will be send at a configurable distance interval (default 2nm). The position update is deliberately one unit "behind", so that when the boat arrives at its destination, regardless of the distance of the most recent position update, the distance is sufficently large to be accepted by NFL for an accurate final update. This way the anchor spot is always spot on.
Furthermore, when arrived, optionally the depth will be added to the message.
An "arrival" is determined by the speed of the boat being less than 0.4 kts for at least 10 minutes, and a distance of at least 0.1nm from the most recent position update.
I wrote this script because none of the suggested tracking options on the NFL website were suitable to me.
I had hoped to be able to use a more efficient way of updating the position than by sending e-mails, but despite multiple attemps, I never got any replies from the NFL support e-mail address.
This script is open source, and a master copy is maintained at https://git.TheFloatingLab.world/NFLpositionUpdater. This is part of the website https://www.thefloatinglab.world where you can find my contact details and other information.
I'm in no way affiliated with NFL. Use this software "as is".
# Installation
This is a Perl script. It should run "out of the box" on any Linux system, and most likely also on Windows and Mac systems.
There is no installation required, just copy the script to a suitable location. On linux, you need to set the "executable" flag, either via a file manager GUI or on the command line with "sudo chmod a+x nfl-apu.pl".
You can then test run the program with "perl nfl-apu.pl -?". It should respond with a help screen.
# Usage
The program is started from the command line. A few command line options are necessary. The available options are:
### Control options
- -h --help Display help
- -T --test Debug output
- -d --daemon Run as daemon
### NMEA connection options
- -t --tcp Use TCP source instead of UDP
- -g --gpsd Use GPSD source
*The IP address and port of the NMEA stream are given as the final option on the command line. See example below*
### Update options
- -x --extended Extended updates (with SOG, COG, DPT)
- -i --interval=<NM> Nautical miles between position updates (default $interval). A value 0 means "arrival updates only"
### Email options
- -u --user=<USERNAME> Email account inlog user
- -w --password=<PASSWORD> The password associated with the user account
- -s --server=<ADDRESS> SMTP server
- -p --port=<PORT> SMTP port (default $port)
- -e --email=<ADDRESS> Source email address
- -o --override=<ADDRESS> Override destination email address (default '$destination')
### Example:
perl nfl-apu.pl -t -g -i=2 -x -u=Frans -s=mail.fransveldman.nl -e=noreply@thefloatinglab.world 127.0.0.1:2947
## NMEA source
The NMEA stream must be somehow accessible on the computer. There are a few possible sources:
- You have an NMEA multiplexer with a WiFi and/or Ethernet output.
- You have a "GPS-mouse" connected to the computer.
- You run OpenCPN on the computer.
You need to consult the documentation of these sources to see how they deliver the NMEA stream on the network.
- If you have an NMEA multiplexer, the NMEA stream is often available on port 10110. You can then use "192.168.1.255:10110" as a source. The first three octets are from your network, the 255 is the broadcast address, the 10110 part is the port.
- If you have a GPS-mouse, they can often be made available on the network via the program GPSD.
- If you run OpenCPN, you can in the connections screen configure an *output* for its NMEA data.
If you configure one of the above options, select UDP instead of TCP if possible. UDP is more suitable for this kind of services. If you need to select a port, use 10110 because this port is dedicated to NMEA streaming.
By default, nfl-apu assumes an UDP port. If you use TCP, use the -t option. If you use GPSD, use the -g option.
## Email
For sending position update emails you are going to use your own e-mail account. You need to provide:
- Your e-mail account user name.
- Your e-mail account password.
- The smtp server (like "smtp.gmail.com")
- The port number of the smtp server (default 587, leave it as is, unless you are sure that your server is one of the rare cases with a non standard e-mail submission port).
- Your e-mail address.
- The destination e-mail address is already set by default to deliver the e-mails to NFL, but for testing you can use the -o (--override) option to set a different destination.
The e-mail account user name and your e-mail address are often the same. Consult the information of your e-mail provider.
If you are not comfortable with writing the password on the command line, you can leave it away. At startup, nfl-apu will then ask you to type the password.
*Note that nfl-apu is completely open source. Anyone with some knowledge of programming can confirm that the command line options are only used for their intended purpose. Nothing is stored or send anywhere else. Your inlog credentials are only send to your e-mail provider, who has to verify that you have the right to use that e-mail account to submit a message.*
## Additional options
By default, nfl-apu will update your position at every 2 nm. For slow boats, you might want to set it to 1, for fast boats, you could set a higher number. For a boat sailing with 6 kts, the default will send 3 e-mails per hour.
If you don't want position updates "on the move", you can set the value to 0. In that case, only an update will be send when you have arrived at your destination. You are assumed to have arrived somewhere if the boat speed is less than 0.4 kts for at least 10 minutes.
The -x (--extended) option will add your SOG and COG to the position update, or the DEPTH when you are at anchor. The public can find this information if they read the "story" of your travel.
By default, nfl-apu will start as a normal program, so it "occupies" the terminal as long as it runs, and if you close the terminal it will stop. This is nice for testing, but once this is finished, it is better to use the -d (--daemon) option. The program will start "as a service process" and stay active in the background.
# Troubleshooting
First you need to establish that you have an NMEA input. When started, nfl-apu will almost immediately put a message on the screen saying "NMEA stream detected". It will then quickly follow with a one time location and depth report on the screen.
If the problem is with e-mail, use the -T (--test) option. It will output the conversation between the e-mail provider and nfl-apu. An "authentication error" means that eihter the user name or the password is incorrect.
For gmail, the server is smtp.gmail.com and the port is 587. For those who only use gmail via the app or web-interface, you might need to configure a password so that thirdt party e-mail clients can use the service.

293
nfl-apu.pl Normal file
View File

@ -0,0 +1,293 @@
#!perl -w
use strict;
use Socket;
use Math::Trig qw(great_circle_distance deg2rad);
use Net::SMTP;
use Authen::SASL qw(Perl);
# auto-flush on socket
$| = 1;
my $VERSION="0.1";
print "NoForeignLand-AutomaticPositionUpdater $VERSION, by Frans Veldman s/v ZwerfCat (https://www.thefloatinglab.world)\n";
my $tcp=0;
my $gpsd=0;
my $daemon=0;
my $test=0;
my $interval=2;
my $extended=0;
my $user='';
my $pass='';
my $server='';
my $from='';
my $destination='tracking@noforeignland.com';
my $port=587;
my @sockets;
# Get command line options
foreach my $a(@ARGV) {
$daemon=1 if($a eq "-d" || $a eq "--daemon");
$test=1 if($a eq "-T" || $a eq "--test");
$tcp=1 if($a eq "-t" || $a eq "--tcp");
$tcp=1, $gpsd=1 if($a eq "-g" || $a eq "--gpsd");
$extended=1 if($a eq "-x" || $a eq "--extended");
$interval=$2 if($a=~/-(-interval|i)=(\d+)/);
$port=$2 if($a=~/-(-port|p)=(\d+)/);
$destination=$2 if($a=~/-(-override|o)=(\S+)/);
$server=$2 if($a=~/-(-server|s)=(\S+)/);
$user=$2 if($a=~/-(-user|u)=(\S+)/);
$pass=$2 if($a=~/-(-password|w)=(\S+)/);
$from=$2 if($a=~/-(-email|e)=(\S+)/);
if($a eq "-?" || $a eq "-h" || $a eq "--help") {
print "\nUsage:\n";
print "\tperl nfl-apu.pl [OPTIONS] <SOURCE IP:PORT>\n";
print "Control options:\n";
print "\t-h --help Display help\n";
print "\t-T --test Debug output\n";
print "\t-d --daemon Run as daemon\n";
print "NMEA connection options:\n";
print "\t-t --tcp Use TCP source instead of UDP\n";
print "\t-g --gpsd Use GPSD source\n";
print "Update options:\n";
print "\t-x --extended Extended updates (with SOG, COG, DPT)\n";
print "\t-i --interval=<NM> Nautical miles between position updates (default $interval). A value 0 means \"arrival updates only\"\n";
print "Email options:\n";
print "\t-u --user=<USERNAME> Email account inlog user\n";
print "\t-w --password=<PASSWORD> The password associated with the user account\n";
print "\t-s --server=<ADDRESS> SMTP server\n";
print "\t-p --port=<PORT> SMTP port (default $port)\n";
print "\t-e --email=<ADDRESS> Source email address\n";
print "\t-o --override=<ADDRESS> Override destination email address (default '$destination')\n";
print "Example:\n";
print "\tperl nfl-apu.pl -t -g -i=2 -x -u=Frans -s=mail.fransveldman.nl -e=noreply\@thefloatinglab.world 127.0.0.1:2947\n";
exit;
}
next if($a=~/^-/);
if($a=~/^((([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\.){3}([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])):([0-9]+)$/) {
push @sockets,pack_sockaddr_in($5, inet_aton($1));
} else {
die "Error: $a is not a valid IP:PORT address!\n";
}
}
use Fcntl qw(LOCK_EX LOCK_NB);
open our $file, '<', $0 or die $!;
die "Another instance is already running!\n" unless (flock $file, LOCK_EX|LOCK_NB);
# Check that at least a source address has been specified
die "Error: No NMEA source specified!\n" unless(@sockets);
die "Error: No username specified!\n" if($user eq '');
die "Error: No smtp server specified!\n" if($server eq '');
die "Error: No email source specified!\n" if($from eq '');
while($pass eq '') {
print "Enter password: ";
$pass=<STDIN>;
chomp $pass;
print "\n";
}
$daemon=0 if($test);
daemonize() if($daemon);
my $sock;
sourceconnect();
my $datadetected=0;
my $positiondetected=0;
my $depthdetected=0;
my $starttime=time;
my $timer=0;
my $prevlat=0;
my $prevlon=0;
my $depth=0;
# just loop forever listening for packets
while (1) {
my $data=<$sock>;
if(length($data)==0) {
# We lost the connection, so re-establish it
close($sock) if($tcp);
sourceconnect();
next;
}
if(!$datadetected && $data=~/\$[A-Z]+,.*\*../) {
$datadetected++;
print "NMEA stream detected.\n";
}
if($data=~/\$\w\wDPT,(\d+\.?\d*),([+-]?\d*\.?\d*).*\*../) {
$depth=$1;
$depth+=$2 if(defined $2);
if(!$depthdetected) {
$depthdetected++;
print "Depth: $depth meters\n";
}
}
next if(time-$starttime<8 && !$depthdetected); # Just wait a little for NMEA data acquisition
# Get the position update
# $<TalkerID>RMC,<Timestamp>,<Status>,<Lat>,<N/S>,<Long>,<E/W>,<SOG>,<COG>,<Date>,<MagVar>,<MagVarDir>,<mode>,<NavStatus>*<checksum><CR><LF>
if($data=~/\$G[A-Z]RMC,\d*\.?\d*,A,(\d\d)(\d\d\.\d+),([NS]),(\d\d\d)(\d\d\.\d+),([EW]),([0-9]*\.?[0-9]*),(\d*)\.?\d*,.*\*../) {
my $latdeg=$1; my $latmin=$2; my $latdir=$3; my $londeg=$4; my $lonmin=$5; my $londir=$6; my $sog=$7; my $cog=$8;
my $lat=$latdeg+($latmin/60);
$lat=-$lat if($latdir eq 'S');
my $lon=$londeg+($lonmin/60);
$lon=-$lon if($londir eq 'W');
if(!$positiondetected) {
$positiondetected++;
print "Lat: $latdeg degrees $latmin minutes $latdir\nLon: $londeg degrees $lonmin minutes $londir\nSOG: $sog COG: $cog\n";
}
my $distance=gps_distance($lat,$lon,$prevlat,$prevlon);
# We want to be one unit behind, so that when arriving at a destination we are guaranteed to have a substantial distance from our previous waypoint.
# So, when the criteria for an update are met, we update a previous position, unless after we have arrived.
# If the speed is almost zero, and we have a distance between the previous reported position, it looks like we arrived somewhere.
my $arrived=0;
if($sog<0.4 && $distance>0.1) {
$timer=time if(!$timer);
$arrived=1 if(time-$timer>600);
} else {
$timer=0;
}
my $update=0;
$update=1 if($interval && $distance>$interval);
# At the start of the program, we broadcast the current position, so we update prevlat and prevlon right away. We also do this if we have arrived somewhere.
if((!$prevlat && !$prevlon) || $arrived) {
$prevlat=$lat;
$prevlon=$lon;
$update=1; # Force a position update
$arrived=1 if($sog<0.4); # Assume that we have arrived somewhere
}
if($update) {
# Submit position report
$latdir='N';
if($prevlat<0) {
$prevlat=-$prevlat;
$latdir='S';
}
$prevlat=~/(\d+)(\.\d+)/;
$latdeg=$1;
$latmin='0'.$2;
$latmin=sprintf("%.4f", $latmin*60);
$londir='E';
if($prevlon<0) {
$prevlon=-$prevlon;
$londir='W';
}
$prevlon=~/(\d+)(\.\d+)/;
$londeg=$1;
$lonmin='0'.$2;
$lonmin=sprintf("%.4f", $lonmin*60);
my $msgbody=<<END;
From: $from
To: $destination
Subject: Position update
LAT|$latdeg|$latmin|$latdir
LON|$londeg|$lonmin|$londir
END
if($extended) {
if($arrived) {
$msgbody.="Depth: $depth meters.\n";
} else {
$msgbody.="SOG: $sog kts, COG: $cog degrees.\n";
}
}
print "---\n$msgbody---\n" if(!$daemon);
print "Submitting position report... " if(!$daemon);
my $smtp = Net::SMTP->new($server, Port => $port, Timeout => 60, Debug => $test);
if($smtp) {
$smtp->starttls();
$smtp->auth($user,$pass) or die "Error: could not authenticate: $smtp->status, $smtp->message\n";
$smtp->mail($from);
$smtp->to($destination);
$smtp->data();
$smtp->datasend($msgbody);
$smtp->dataend;
$smtp->quit;
print "Done\n" if(!$daemon);
} else {
print "Failed\n" if(!$daemon);
}
$prevlat=$lat;
$prevlon=$lon;
}
}
}
sub gps_distance {
my ($lat0,$lon0,$lat1,$lon1) = @_;
return great_circle_distance(deg2rad($lon0), deg2rad(90-$lat0), deg2rad($lon1), deg2rad(90-$lat1), 3443.931); # nautical miles
}
sub sourceconnect {
if($tcp) {
# Connect for TCP source
print "Connecting... " if(!$daemon);
socket($sock, PF_INET, SOCK_STREAM, getprotobyname('tcp')) || die "socket: $!";
setsockopt($sock, SOL_SOCKET, SO_KEEPALIVE, 1);
connect($sock,$sockets[0]) || die "Could not connect to TCP port!\n";
print "Connected!\n" if(!$daemon);
if($gpsd) {
# Configure GPSD output, and skip config messages
send($sock,'?WATCH={"enable":true,"json":false,"nmea":true,"raw":0,"scaled":false,"timing":false,"split24":false,"pps":false}',0);
while(my $line= <$sock>) {
last unless($line=~/\{/);
print $line if(!$daemon);
}
}
} else {
# Connect to UDP source
socket($sock, PF_INET, SOCK_DGRAM, getprotobyname('udp')) || die "socket: $!";
setsockopt($sock, SOL_SOCKET, SO_REUSEADDR, pack("l", 1)) || die "setsockopt: $!";
setsockopt($sock,SOL_SOCKET,SO_RCVBUF,100000);
bind($sock, $sockets[0]) || die "bind: $!";
}
}
sub daemonize {
use POSIX;
POSIX::setsid or die "setsid: $!";
my $pid = fork() // die $!; #//
if($pid) {
print "Started daemon (PID $pid)\n";
exit(0);
}
chdir "/";
umask 0;
open (STDIN, "</dev/null");
open (STDOUT, ">/dev/null");
open (STDERR, ">&STDOUT");
}