From 78158a5f9cd515501569a0c8b0c85f7d1cf41fe2 Mon Sep 17 00:00:00 2001 From: Frans Veldman Date: Wed, 17 Sep 2025 07:27:26 +0000 Subject: [PATCH] Initial release --- README.md | 103 ++++++++++++++++++- nfl-apu.pl | 293 +++++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 395 insertions(+), 1 deletion(-) create mode 100644 nfl-apu.pl diff --git a/README.md b/README.md index bee81ec..28a41be 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,104 @@ # NFLpositionUpdater -Automatic Position Updater for NFL, using the NMEA stream of the boat. \ No newline at end of file +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= Nautical miles between position updates (default $interval). A value 0 means "arrival updates only" + +### Email options +- -u --user= Email account inlog user +- -w --password= The password associated with the user account +- -s --server=
SMTP server +- -p --port= SMTP port (default $port) +- -e --email=
Source email address +- -o --override=
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. + diff --git a/nfl-apu.pl b/nfl-apu.pl new file mode 100644 index 0000000..4cb8b60 --- /dev/null +++ b/nfl-apu.pl @@ -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] \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= Nautical miles between position updates (default $interval). A value 0 means \"arrival updates only\"\n"; + print "Email options:\n"; + print "\t-u --user= Email account inlog user\n"; + print "\t-w --password= The password associated with the user account\n"; + print "\t-s --server=
SMTP server\n"; + print "\t-p --port= SMTP port (default $port)\n"; + print "\t-e --email=
Source email address\n"; + print "\t-o --override=
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=; + 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 + # $RMC,,,,,,,,,,,,,* + 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=<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 (STDERR, ">&STDOUT"); +} +