xastir/scripts/ais.pl
Tom Russo db523a33ee Update all copyright dates
Make all copyright notices include this year.

Most say "2000-2026" but some files created later had later start
dates, and some created last year only had the start date.  They now
all have "-2026" regardless.

Closes #340
2026-01-24 20:04:37 -07:00

2479 lines
91 KiB
Perl
Executable file
Raw Permalink Blame History

#!/usr/bin/env perl
use warnings;
###########################################################################
#
#
# XASTIR, Amateur Station Tracking and Information Reporting
# Copyright (C) 2000-2026 The Xastir Group
#
# "ais.pl", a Perl script to connect aisdecoder and Xastir for receiving
# packets from ships. This script will create a UDP server at localhost:10110
# for aisdecoder to send packets into. It decodes AIS sentences, creates APRS
# packets out of them and sends them to Xastir's server port (2023) via UDP.
#
# AIS is the international tracking system for ships. They transmit on Marine
# VHF frequencies between 156.025 and 162.025, using a low power output of 1W,
# high power output of either 5W or 12.5W, at a bit rate of 9600 bits/s using
# NRZI data encoding and GMSK/FM at 0.5 modulation index.
#
# The two default AIS channels are:
# Ch 87B, AIS-1: 161.975 MHz
# Ch 88B, AIS-2: 162.025 MHz
#
# The two long-range channels are:
# Ch 75: 156.775 MHz
# Ch 76: 156.825 MHz
#
#
# You'll need "libusb", "libpthread", "librtlsdr" installed to be able to
# compile "rtl_ais".
#
# Get aisdecoder (rtl-ais).
# git clone https://github.com/dgiardini/rtl-ais/
# cd rtl_ais
# make
#
#
# Run "rtl_ais" like this (create a simple script!) for the two default channels:
#
# ./rtl_ais -h 127.0.0.1 -P 10110 -d 0 -l 161.975M -r 162.025M -n -p -2
#
# Change to marine channels 75 (156.775 MHz) and 76 (156.825 MHz) to pick
# up long-range AIS (Type 27 packets), perhaps with another dongle:
#
# ./rtl_ais -h 127.0.0.1 -P 10110 -d 1 -l 156.775M -r 156.825M -n -p -2
#
# Note that "-p -2" is the frequency error of the RTL dongle.
# Set that to the proper number determined from "Kal".
#
# "-d 0" or "-d 1" selects which dongle you're attaching to.
#
# Don't use "-g <number>" and instead use auto-gain, which appears to work
# better. This also works better than enabling AGC in the chipset itself.
#
#
# Run "ais.pl" like this (again, create a simple script!):
# ./ais.pl boats <passcode> [--pipe] [--logging]
#
# You may add the "--pipe" flag in order to have "ais.pl" accept data
# from a pipe instead of setting up a UDP server for "rtl_ais" to send
# packets to. This is very useful for debugging or for processing
# stored log files.
#
# If you add " --logging" to the end, this script will save the APRS portion of
# the output to a file called "~/.xastir/logs/ships.log". You can later suck
# this file back in to see the ships move around the map in hyperspeed. Useful
# for a quick demo.
#
#
# Antenna length should be:
# 1/2 wave dipole: 2.89' -or- 2' 10.7" -or- 2' 10 5/8"
# 1/4 wave: 1.44' -or- 1' 5.3" -or- 1' 5 5/16"
#
#
# Excellent AIS info:
# http://catb.org/gpsd/AIVDM.html # Eric Raymond/GPSD
# https://www.itu.int/dms_pubrec/itu-r/rec/m/R-REC-M.1371-5-201402-I!!PDF-E.pdf # ITU Spec
# http://www.navcen.uscg.gov/pdf/AIS/AIS_Special_Notice_and_AIS_Encoding_Guide_2012.pdf
#
# Example packets w/decoding of fields, plus large sample NMEA data file:
# http://fossies.org/linux/gpsd/test/sample.aivdm
#
# Possible live data feed (Too many connections when I try):
# ais.exploratorium.edu:80
#
###########################################################################
use IO::Socket;
use Storable;
#use File::HomeDir;
#use Data::Dumper; # Only used for debugging vessel_hash
# Enable sending APRS packets to Xastir
$enable_tx = 1;
# Enable the printing of a statistics line every 100 messages
# The format is: total type1 type2 ... type27
# Example: 3600 62 0 15 857 0 0 0 0 0 0 0 0 0 0 113 0 0 0 0 685 1868 0 0 0 0 0 0
$statistics_mode = 1;
# Turn on/off printing of various types of messages. Enables debugging of a few
# sentence types at a time w/o other messages cluttering up the output.
$print_123 = 1;
$print_4 = 1;
$print_5 = 1; # (includes vesselName and shipType)
$print_9 = 1;
$print_18 = 1;
$print_19 = 1; # (includes vesselName)
$print_21 = 1;
$print_24_A = 1; # A Variant (includes vesselName)
$print_24_B = 1; # B Variant (includes shipType
$print_27 = 1;
$print_others = 1; # Not decoded yet
# Keep statistics on each type (28 slots)
@message_type_count = (0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0);
$statistics_count = 0; # Number of messages before printing stats
# Hash to store tac-calls in, to minimize tac-call assignment packets
# This stores the complete tactical call, including whatever name
# the ship goes by currently + the country of registry.
my %tactical_hash;
# Create external copy of vessel_hash for persistent store.
# This contains only the vessel name, not the country of registry
# or the 9-digit MMSI number.
$home = `echo ~`;
chomp $home;
$persistentFileSpec = "$home/.xastir/config/vessel_hash";
#$persistentFileSpec = File::HomeDir->my_home . "/.xastir/config/vessel_hash";
#
#
# Hash to store vessel names in, for assigning tactical calls
# If it doesn't exist, create it with some initial values
# For final release this should simply create a dummy file
my %vessel_hash;
if ( !(-e $persistentFileSpec )) {
%vessel_hash = (
);
# Create the new hash file
store \%vessel_hash , $persistentFileSpec;
}
# Retrieve the cache
%vessel_hash = %{retrieve($persistentFileSpec)};
# Debug - print the hash values
#print Dumper(\%vessel_hash);
$udp_client = "xastir_udp_client";
$xastir_host = "localhost"; # Server where Xastir is running
$xastir_port = 2023; # 2023 is Xastir default UDP port
$log_file = "~/.xastir/logs/ships.log";
%countries = (
"201" => "Albania", #"Albania (Republic of)"
"202" => "Andorra", # "Andorra (Principality of)"
"203" => "Austria",
"204" => "Azores", # "Azores - Portugal"
"205" => "Belgium",
"206" => "Belarus", # "Belarus (Republic of)"
"207" => "Bulgaria", # "Bulgaria (Republic of)"
"208" => "Vatican", # "Vatican City State"
"209" => "Cyprus", # "Cyprus (Republic of)"
"210" => "Cyprus", # "Cyprus (Republic of)"
"211" => "Germany", # "Germany (Federal Republic of)"
"212" => "Cyprus", # "Cyprus (Republic of)"
"213" => "Georgia",
"214" => "Moldova", # "Moldova (Republic of)"
"215" => "Malta",
"216" => "Armenia", # "Armenia (Republic of)"
"218" => "Germany", # "Germany (Federal Republic of)"
"219" => "Denmark",
"220" => "Denmark",
"224" => "Spain",
"225" => "Spain",
"226" => "France",
"227" => "France",
"228" => "France",
"229" => "Malta",
"230" => "Finland",
"231" => "Faroe Is.", # "Faroe Islands - Denmark"
"232" => "U.K.", # "United Kingdom of Great Britain and Northern Ireland"
"233" => "U.K.", # "United Kingdom of Great Britain and Northern Ireland"
"234" => "U.K.", # "United Kingdom of Great Britain and Northern Ireland"
"235" => "U.K.", # "United Kingdom of Great Britain and Northern Ireland"
"236" => "Gibraltar", # "Gibraltar - United Kingdom of Great Britain and Northern Ireland"
"237" => "Greece",
"238" => "Croatia", # "Croatia (Republic of)"
"239" => "Greece",
"240" => "Greece",
"241" => "Greece",
"242" => "Morocco", # "Morocco (Kingdom of)"
"243" => "Hungary",
"244" => "Netherlands", # "Netherlands (Kingdom of the)"
"245" => "Netherlands", # "Netherlands (Kingdom of the)"
"246" => "Netherlands", # "Netherlands (Kingdom of the)"
"247" => "Italy",
"248" => "Malta",
"249" => "Malta",
"250" => "Ireland",
"251" => "Iceland",
"252" => "Liechtenstein", # "Liechtenstein (Principality of)"
"253" => "Luxembourg",
"254" => "Monaco", # "Monaco (Principality of)"
"255" => "Madeira", # "Madeira - Portugal"
"256" => "Malta",
"257" => "Norway",
"258" => "Norway",
"259" => "Norway",
"261" => "Poland", # "Poland (Republic of)"
"262" => "Montenegro",
"263" => "Portugal",
"264" => "Romania",
"265" => "Sweden",
"266" => "Sweden",
"267" => "Slovakia", # "Slovak Republic"
"268" => "San Marino", # "San Marino (Republic of)"
"269" => "Switzerland", # "Switzerland (Confederation of)"
"270" => "Czech Rep.", # "Czech Republic"
"271" => "Turkey",
"272" => "Ukraine",
"273" => "Russian Fed.", # "Russian Federation"
"274" => "Macedonia", # "The Former Yugoslav Republic of Macedonia"
"275" => "Latvia", # "Latvia (Republic of)"
"276" => "Estonia", # "Estonia (Republic of)"
"277" => "Lithuania", # "Lithuania (Republic of)"
"278" => "Slovenia", # "Slovenia (Republic of)"
"279" => "Serbia", # "Serbia (Republic of)"
"301" => "Anguilla", # "Anguilla - United Kingdom of Great Britain and Northern Ireland"
"303" => "Alaska,U.S.", # "Alaska (State of) - United States of America"
"304" => "Antigua & Barbuda", # "Antigua and Barbuda"
"305" => "Antigua & Barbuda", # "Antigua and Barbuda"
"306" => "Sint Maarten", # "Sint Maarten (Dutch part) - Netherlands (Kingdom of the)"
"306" => "Bonaire, Sint Eustatius & Saba", # "Bonaire, Sint Eustatius and Saba - Netherlands (Kingdom of the)"
"306" => "Cura<72>ao", # "Cura<72>ao - Netherlands (Kingdom of the)"
"307" => "Aruba", # "Aruba - Netherlands (Kingdom of the)"
"308" => "Bahamas", # "Bahamas (Commonwealth of the)"
"309" => "Bahamas", # "Bahamas (Commonwealth of the)"
"310" => "Bermuda", # "Bermuda - United Kingdom of Great Britain and Northern Ireland"
"311" => "Bahamas", # "Bahamas (Commonwealth of the)"
"312" => "Belize",
"314" => "Barbados",
"316" => "Canada",
"319" => "Cayman Is.", # "Cayman Islands - United Kingdom of Great Britain and Northern Ireland"
"321" => "Costa Rica",
"323" => "Cuba",
"325" => "Dominica", # "Dominica (Commonwealth of)"
"327" => "Dominican Rep.", # "Dominican Republic"
"329" => "Guadeloupe", # "Guadeloupe (French Department of) - France"
"330" => "Grenada",
"331" => "Greenland", # "Greenland - Denmark"
"332" => "Guatemala", # "Guatemala (Republic of)"
"334" => "Honduras", # "Honduras (Republic of)"
"336" => "Haiti", # "Haiti (Republic of)"
"338" => "U.S.", # "United States of America"
"339" => "Jamaica",
"341" => "St. Kitts & Nevis", # "Saint Kitts and Nevis (Federation of)"
"343" => "St. Lucia",
"345" => "Mexico",
"347" => "Martinique", # "Martinique (French Department of) - France"
"348" => "Montserrat", # "Montserrat - United Kingdom of Great Britain and Northern Ireland"
"350" => "Nicaragua",
"351" => "Panama", # "Panama (Republic of)"
"352" => "Panama", # "Panama (Republic of)"
"353" => "Panama", # "Panama (Republic of)"
"354" => "Panama", # "Panama (Republic of)"
"355" => "Panama", # "Panama (Republic of)"
"356" => "Panama", # "Panama (Republic of)"
"357" => "Panama", # "Panama (Republic of)"
"358" => "Puerto Rico,U.S.", # "Puerto Rico - United States of America"
"359" => "El Salvador", # "El Salvador (Republic of)"
"361" => "St. Pierre & Miquelon", # "Saint Pierre and Miquelon (Territorial Collectivity of) - France"
"362" => "Trinidad & Tobago", # "Trinidad and Tobago"
"364" => "Turks & Caicos Is.", # "Turks and Caicos Islands - United Kingdom of Great Britain and Northern Ireland"
"366" => "U.S.", # "United States of America"
"367" => "U.S.", # "United States of America"
"368" => "U.S.", # "United States of America"
"369" => "U.S.", # "United States of America"
"370" => "Panama", # "Panama (Republic of)"
"371" => "Panama", # "Panama (Republic of)"
"372" => "Panama", # "Panama (Republic of)"
"373" => "Panama", # "Panama (Republic of)"
"374" => "Panama", # "Panama (Republic of)"
"375" => "St. Vincent & Grenadines", # "Saint Vincent and the Grenadines"
"376" => "St. Vincent & Grenadines", # "Saint Vincent and the Grenadines"
"377" => "St. Vincent & Grenadines", # "Saint Vincent and the Grenadines"
"378" => "British Virgin Is.", # "British Virgin Islands - United Kingdom of Great Britain and Northern Ireland"
"379" => "U.S. Virgin Is.", # "United States Virgin Islands - United States of America"
"401" => "Afghanistan",
"403" => "Saudi Arabia", # "Saudi Arabia (Kingdom of)"
"405" => "Bangladesh", # "Bangladesh (People's Republic of)"
"408" => "Bahrain", # "Bahrain (Kingdom of)"
"410" => "Bhutan", # "Bhutan (Kingdom of)"
"412" => "China", # "China (People's Republic of)"
"413" => "China", # "China (People's Republic of)"
"414" => "China", # "China (People's Republic of)"
"416" => "Taiwan", # "Taiwan (Province of China) - China (People's Republic of)"
"417" => "Sri Lanka", # "Sri Lanka (Democratic Socialist Republic of)"
"419" => "India", # "India (Republic of)"
"422" => "Iran", # "Iran (Islamic Republic of)"
"423" => "Azerbaijan", # "Azerbaijan (Republic of)"
"425" => "Iraq", # "Iraq (Republic of)"
"428" => "Israel", # "Israel (State of)"
"431" => "Japan",
"432" => "Japan",
"434" => "Turkmenistan",
"436" => "Kazakhstan", # "Kazakhstan (Republic of)"
"437" => "Uzbekistan", # "Uzbekistan (Republic of)"
"438" => "Jordan", # "Jordan (Hashemite Kingdom of)"
"440" => "Korea", # "Korea (Republic of)"
"441" => "Korea", # "Korea (Republic of)"
"443" => "Palestine", # "State of Palestine (In accordance with Resolution 99 Rev. Guadalajara, 2010)"
"445" => "N. Korea", # "Democratic People's Republic of Korea"
"447" => "Kuwait", # "Kuwait (State of)"
"450" => "Lebanon",
"451" => "Kyrgyzstan", # "Kyrgyz Republic"
"453" => "Macao", # "Macao (Special Administrative Region of China) - China (People's Republic of)"
"455" => "Maldives", # "Maldives (Republic of)"
"457" => "Mongolia",
"459" => "Nepal", # "Nepal (Federal Democratic Republic of)"
"461" => "Oman", # "Oman (Sultanate of)"
"463" => "Pakistan", # "Pakistan (Islamic Republic of)"
"466" => "Qatar", # "Qatar (State of)"
"468" => "Syria", # "Syrian Arab Republic"
"470" => "United Arab Emirates",
"471" => "United Arab Emirates",
"472" => "Tajikistan", # "Tajikistan (Republic of)"
"473" => "Yemen", # "Yemen (Republic of)"
"475" => "Yemen", # "Yemen (Republic of)"
"477" => "Hong Kong", # "Hong Kong (Special Administrative Region of China) - China (People's Republic of)"
"478" => "Bosnia & Herzegovina", # "Bosnia and Herzegovina"
"501" => "Adelie Land", # "Adelie Land - France"
"503" => "Australia",
"506" => "Myanmar", # "Myanmar (Union of)"
"508" => "Brunei Darussalam",
"510" => "Micronesia", # "Micronesia (Federated States of)"
"511" => "Palau", # "Palau (Republic of)"
"512" => "New Zealand",
"514" => "Cambodia", # "Cambodia (Kingdom of)"
"515" => "Cambodia", # "Cambodia (Kingdom of)"
"516" => "Christmas Is.", # "Christmas Island (Indian Ocean) - Australia"
"518" => "Cook Is.", # "Cook Islands - New Zealand"
"520" => "Fiji", # "Fiji (Republic of)",
"523" => "Cocos (Keeling) Is.", # "Cocos (Keeling) Islands - Australia"
"525" => "Indonesia", # "Indonesia (Republic of)"
"529" => "Kiribati", # "Kiribati (Republic of)"
"531" => "Laos", # "Lao People's Democratic Republic"
"533" => "Malaysia",
"536" => "N. Mariana Is,U.S.", # "Northern Mariana Islands (Commonwealth of the) - United States of America"
"538" => "Marshall Is.", # "Marshall Islands (Republic of the)"
"540" => "New Caledonia", # "New Caledonia - France"
"542" => "Niue", # "Niue - New Zealand"
"544" => "Nauru", # "Nauru (Republic of)"
"546" => "French Polynesia", # "French Polynesia - France"
"548" => "Philippines", # "Philippines (Republic of the)"
"553" => "Papua New Guinea",
"555" => "Pitcairn Is.", # "Pitcairn Island - United Kingdom of Great Britain and Northern Ireland"
"557" => "Solomon Islands",
"559" => "American Samoa,U.S.", # "American Samoa - United States of America"
"561" => "Samoa", # "Samoa (Independent State of)"
"563" => "Singapore", # "Singapore (Republic of)"
"564" => "Singapore", # "Singapore (Republic of)"
"565" => "Singapore", # "Singapore (Republic of)"
"566" => "Singapore", # "Singapore (Republic of)"
"567" => "Thailand",
"570" => "Tonga", # "Tonga (Kingdom of)"
"572" => "Tuvalu",
"574" => "Viet Nam", # "Viet Nam (Socialist Republic of)"
"576" => "Vanuatu", # "Vanuatu (Republic of)"
"577" => "Vanuatu", # "Vanuatu (Republic of)"
"578" => "Wallis & Futuna Is.", # "Wallis and Futuna Islands - France"
"601" => "S. Africa", # "South Africa (Republic of)"
"603" => "Angola", # "Angola (Republic of)"
"605" => "Algeria", # "Algeria (People's Democratic Republic of)"
"607" => "St. Paul & Amsterdam Is.", # "Saint Paul and Amsterdam Islands - France"
"608" => "Ascension Is.", # "Ascension Island - United Kingdom of Great Britain and Northern Ireland"
"609" => "Burundi", # "Burundi (Republic of)"
"610" => "Benin", # "Benin (Republic of)"
"611" => "Botswana", # "Botswana (Republic of)"
"612" => "Central African Rep.", # "Central African Republic"
"613" => "Cameroon", # "Cameroon (Republic of)"
"615" => "Congo", # "Congo (Republic of the)"
"616" => "Comoros", # "Comoros (Union of the)"
"617" => "Cape Verde", # "Cabo Verde (Republic of)"
"618" => "Crozet Archipelago", # "Crozet Archipelago - France"
"619" => "C<>te d'Ivoire", # "C<>te d'Ivoire (Republic of)"
"620" => "Comoros", # "Comoros (Union of the)"
"621" => "Djibouti", # "Djibouti (Republic of)"
"622" => "Egypt", # "Egypt (Arab Republic of)"
"624" => "Ethiopia", # "Ethiopia (Federal Democratic Republic of)"
"625" => "Eritrea",
"626" => "Gabonese Rep.", # "Gabonese Republic"
"627" => "Ghana",
"629" => "Gambia", # "Gambia (Republic of the)"
"630" => "Guinea-Bissau", # "Guinea-Bissau (Republic of)"
"631" => "Equatorial Guinea", # "Equatorial Guinea (Republic of)"
"632" => "Guinea", # "Guinea (Republic of)"
"633" => "Burkina Faso",
"634" => "Kenya", # "Kenya (Republic of)"
"635" => "Kerguelen Is.", # "Kerguelen Islands - France"
"636" => "Liberia", # "Liberia (Republic of)"
"637" => "Liberia", # "Liberia (Republic of)"
"638" => "S. Sudan", # "South Sudan (Republic of)"
"642" => "Libya",
"644" => "Lesotho", # "Lesotho (Kingdom of)"
"645" => "Mauritius", # "Mauritius (Republic of)"
"647" => "Madagascar", # "Madagascar (Republic of)"
"649" => "Mali", # "Mali (Republic of)"
"650" => "Mozambique", # "Mozambique (Republic of)"
"654" => "Mauritania", # "Mauritania (Islamic Republic of)"
"655" => "Malawi",
"656" => "Niger", # "Niger (Republic of the)"
"657" => "Nigeria", # "Nigeria (Federal Republic of)"
"659" => "Namibia", # "Namibia (Republic of)"
"660" => "Reunion", # "Reunion (French Department of) - France"
"661" => "Rwanda", # "Rwanda (Republic of)"
"662" => "Sudan", # "Sudan (Republic of the)"
"663" => "Senegal", # "Senegal (Republic of)"
"664" => "Seychelles", # "Seychelles (Republic of)"
"665" => "St. Helena", # "Saint Helena - United Kingdom of Great Britain and Northern Ireland"
"666" => "Somalia", # "Somalia (Federal Republic of)"
"667" => "Sierra Leone",
"668" => "Sao Tome & Principe", # "Sao Tome and Principe (Democratic Republic of)"
"669" => "Swaziland", # "Swaziland (Kingdom of)"
"670" => "Chad", # "Chad (Republic of)"
"671" => "Togolese Rep.", # "Togolese Republic"
"672" => "Tunisia",
"674" => "Tanzania", # "Tanzania (United Republic of)"
"675" => "Uganda", # "Uganda (Republic of)",
"676" => "Dem. Rep. of Congo", # "Democratic Republic of the Congo"
"677" => "Tanzania", # "Tanzania (United Republic of)"
"678" => "Zambia", # "Zambia (Republic of)"
"679" => "Zimbabwe", # "Zimbabwe (Republic of)"
"701" => "Argentine Rep.", # "Argentine Republic"
"710" => "Brazil", # "Brazil (Federative Republic of)"
"720" => "Bolivia", # "Bolivia (Plurinational State of)"
"725" => "Chile",
"730" => "Columbia", # "Colombia (Republic of)"
"735" => "Ecuador",
"740" => "Falkland Is.", # "Falkland Islands (Malvinas) - United Kingdom of Great Britain and Northern Ireland"
"745" => "Guiana", # "Guiana (French Department of) - France"
"750" => "Guyana",
"755" => "Paraguay", # "Paraguay (Republic of)"
"760" => "Peru",
"765" => "Suriname", # "Suriname (Republic of)"
"770" => "Uruguay", # "Uruguay (Eastern Republic of)"
"775" => "Venezuela", # "Venezuela (Bolivarian Republic of)"
);
$xastir_user = shift;
if (defined($xastir_user)) {
chomp $xastir_user;
}
if ( (!defined($xastir_user)) || ($xastir_user eq "") ) {
print "Please enter a callsign for Xastir injection, but not Xastir's callsign/SSID!\n";
die;
}
$xastir_user =~ tr/a-z/A-Z/;
$xastir_pass = shift;
if (defined($xastir_pass)) {
chomp $xastir_pass;
}
if ( (!defined($xastir_pass)) || ($xastir_pass eq "") ) {
print "Please enter a passcode for Xastir injection\n";
die;
}
# Enable pipe mode by adding the "--pipe" flag, then can pipe
# packets into the program's STDIN for testing. Without this flag
# we'll get our packets from "rtl_ais" via a UDP server we set up.
# Enable logging mode by adding the "--logging" flag.
$pipe_mode = 0;
$logging_mode = 0;
$pipe_flag = shift;
if (defined($pipe_flag) && $pipe_flag ne "") {
chomp $pipe_flag;
if ($pipe_flag eq "--pipe") { $pipe_mode = 1; }
if ($pipe_flag eq "--logging") { $logging_mode = 1; }
}
# Try again in case there are two flags
$pipe_flag = shift;
if (defined($pipe_flag) && $pipe_flag ne "") {
chomp $pipe_flag;
if ($pipe_flag eq "--pipe") { $pipe_mode = 1; }
if ($pipe_flag eq "--logging") { $logging_mode = 1; }
}
if ($enable_tx) {
# Check Xastir's callsign/SSID to make sure we don't have a collision. This
# will prevent Xastir adopting the Items as its own and retransmitting them.
# "xastir_udp_client localhost 2023 <callsign> <passcode> -identify"
# "Received: WE7U-13"
#
$injection_call = $xastir_user;
$injection_call =~ s/-\d+//; # Get rid of dash and numbers
$injection_ssid = $xastir_user;
$injection_ssid =~ s/\w+//; # Get rid of letters
$injection_ssid =~ s/-//; # Get rid of dash
if ($injection_ssid eq "") { $injection_ssid = 0; }
# Find out Callsign/SSID of Xastir instance
$result = `$udp_client $xastir_host $xastir_port $xastir_user $xastir_pass -identify`;
if ($result =~ m/NACK/) {
die "Received NACK from Xastir: Callsign/Passcode don't match?\n";
}
($remote_call, $remote_ssid) = split('-', $result);
chomp($remote_call);
$remote_call =~ s/Received:\s+//;
if ( !defined($remote_ssid) ) { $remote_ssid = ""; }
if ($remote_ssid ne "") { chomp($remote_ssid); }
if ($remote_ssid eq "") { $remote_ssid = 0; }
#print "$remote_call $remote_ssid $injection_call $injection_ssid\n";
#if ($remote_call eq $injection_call) { print "Call matches\n"; }
#if ($remote_ssid == $injection_ssid) { print "SSID matches\n"; }
if ( ($remote_call eq $injection_call)
&& ($remote_ssid == $injection_ssid) ) {
$remote_ssid++;
$remote_ssid%= 16; # Increment by 1 mod 16
$xastir_user = "$remote_call-$remote_ssid";
print "Injection conflict. Corrected. New user = $xastir_user\n";
}
}
if ($pipe_mode) {
#
# Debug mode:
# Read from pipe in a loop
#
while (<STDIN>) {
&process_line($_);
}
}
else {
#
# Not debug mode:
# Create UDP server which connects to rtl_ais to get AIS data
# Listen on localhost port 10110 for UDP packets
#
my $host = "localhost";
my $port = 10110;
my $protocol = 'udp';
my $server = IO::Socket::INET->new(
#PeerAddr => $host,
LocalPort => $port,
Proto => $protocol,
Type => SOCK_DGRAM
) or die "Socket could not be created, failed with error: $!\n";
# Main processing loop. Fetch packets as they are received via
# UDP from port 10110. Create APRS packets out of the interesting
# ones and inject them into Xastir at port 2023 using UDP.
#
while ($server->recv($received_data, 1024)) {
#my $peer_address = $server->peerhost();
#my $peer_port = $server->peerport();
#print "Message was received from: $peer_address, $peer_port\n";
#print "$received_data";
&process_line($received_data);
} # End of main processing loop
}
sub process_line () {
$received_data = shift;
if ($pipe_mode) {
$received_data =~ s/\n//; # Remove LF if present
$received_data =~ s/\r//; # Remove CR if present
}
else { # Live mode, receiving from rtl-ais. CRLF line-ends.
chop($received_data);
chop($received_data);
}
# Check for empty line
if ($received_data eq "") { return; }
# Verify the checksum: Refuse to process a message if it's bad.
# Computed on entire sentence including the AIVDM tag but excluding
# the leading "!" character.
my $str = $received_data;
$str =~ s/^!//; # Chop off leading '!'
#$str =~ s/(,\d)\*../$1/; # Chop to '*' char with leading digit and two trailing chars
$str =~ s/(,\d)\*...*/$1/; # Chop to '*' char with leading digit and at least two trailing chars
#print "$str\n";
# Received checksum. Catches the correct bytes except
# for those cases where the NMEA sentences continues
# on past the checksum.
my $str2 = $received_data;
#$str2 =~ s/.*(..)/$1/; # Catches only a checksum at the end
$str2 =~ s/.*,\d\*(..).*/$1/; # Catches checksum at end or middle
# Compute the checksum on the string between the '!' and the '*' chars.
$j = length($str);
my $sum = 0;
my $i;
for ($i = 0; $i <= $j; $i++) {
my $c = ord( substr($str, $i, 1) );
$sum = $sum ^ $c;
}
my $sum2 = sprintf( "%02X", $sum);
if ($str2 ne $sum2) {
#print "$received_data\n";
#print "$str\t$str2\t$sum2\n";
printf("***** Bad checksum: Received:%s Computed:%s $received_data\n\n", $str2, $sum2);
return(); # Skip this sentence
}
else {
#print "$str2\t$sum2\n";
}
@pieces = split( ',', $received_data);
# $pieces[0] = NMEA message type
# $pieces[1] = Number of sentences
# $pieces[2] = Sequential Message ID
# $pieces[3] = Sentence number
# $pieces[4] = AIS Channel
# $pieces[5] = Encoded AIS data
# $pieces[6] = End of data & NMEA Checksum
$data = $pieces[5];
# Encoded AIS data: Each ASCII character represents 6 bits.
# Subtract 48. If still a decimal number > 40, subtract 8.
# Convert to binary.
$bin_string = "";
$len = length($data);
for ($i = 0; $i < $len; $i++) {
$c = substr($data, $i, 1);
#print("$c");
$d = ord($c) - 48;
if ($d > 40) { $d = $d - 8; }
#print "\t$d\n";
# Convert each character to 6 bits of binary.
# Add the binary strings together to make one
# long binary string. Pull out the bits we need
# to decode each piece of the message.
#
$b = &dec2bin($d);
#print "\t$b\n";
$bin_string = $bin_string . $b;
}
#print "$bin_string\n";
#printf("%d\n", length($data) );
#printf("%d\n", length($bin_string) );
# If first six bits are 000001 (message type 1):
# MMSI Number - bit 8 for 30 bits
# HDG - bit 128 for 9 bits
# COG - bit 116 for 12 bits (and divide by 10)
# SOG - bit 50 for 10 bits (and divide by 10)
# Lat - bit 89 for 27 bits (a signed binary number, divide by 600,000)
# Lon - bit 61 for 28 bits (a signed binary number, divide by 600,000)
#
#if (substr($bin_string,0,6) eq "000001") {
# print "Message Type: 1\n";
#}
#else {
# printf"Message Type: %s\n", substr($bin_string,0,6);
# die;
#}
$bmessage_type = substr($bin_string, 0, 6);
$message_type = &bin2dec($bmessage_type);
if ($statistics_mode) {
# Keep a count of the received message types
if ($message_type < 28) {
$message_type_count[$message_type] += 1;
}
# Since there's no message type 0 use it's array location for total
$message_type_count[0] += 1;
# Print the totals every 100 messages
if ( ++$statistics_count >= 100 ) {
printf("Stats: ");
for (my $jj = 0; $jj < 28; $jj++) {
if ($jj == 0) { printf("%d", $message_type_count[$jj]); }
else { printf(",%d", $message_type_count[$jj]); }
}
print "\n\n";
$statistics_count = 0;
}
}
# The escape clause for bad message types is here
# so that they get included in the total count above.
# Don't move this escape above that code.
if ( ($message_type > 27) || ($message_type == 0) ) {
# print "***** Undefined Msg Type $message_type: $received_data\n\n";
return();
}
#print "Message Type: $message_type\n";
# If not messages types 1, 2, 3, skip for now
if ( $message_type != 1
&& $message_type != 2
&& $message_type != 3
&& $message_type != 4
&& $message_type != 5
&& $message_type != 9
&& $message_type != 18
&& $message_type != 19
&& $message_type != 21
&& $message_type != 24
&& $message_type != 27
) {
# Not something we decode so don't process the sentence.
if ($print_others == 1) { print " Msg Type: $message_type\n\n"; }
return;
}
if ( $message_type == 1
|| $message_type == 2
|| $message_type == 3 ) {
if ($print_123) { print " Msg Type: $message_type\n"; }
&process_types_1_2_3($message_type);
}
elsif ( $message_type == 4 ) {
if ($print_4) { print " Msg Type: $message_type\n"; }
&process_type_4($message_type);
}
elsif ( $message_type == 5 ) {
if ($print_5) { print " Msg Type: $message_type\n"; }
&process_type_5($message_type);
}
elsif ( $message_type == 9 ) {
if ($print_9) { print " Msg Type: $message_type\n"; }
&process_type_9($message_type);
}
elsif ( $message_type == 18 ) {
if ($print_18) { print " Msg Type: $message_type\n"; }
&process_type_18($message_type);
}
elsif ( $message_type == 19 ) {
if ($print_19) { print " Msg Type: $message_type\n"; }
&process_type_19($message_type);
}
elsif ( $message_type == 21 ) {
if ($print_21) { print " Msg Type: $message_type\n"; }
&process_type_21($message_type);
}
elsif ( $message_type == 24 ) {
#if ($print_24) { print " Msg Type: $message_type\t"; } # This is printed from the subroutine instead
&process_type_24($message_type);
}
elsif ( $message_type == 27 ) {
if ($print_27) { print " Msg Type: $message_type\n"; }
if ($print_27) { print "$received_data\n"; }
&process_type_27($message_type);
}
} # End of "process_line"
# Message types 1, 2 and 3: Position Report Class A
# These messages should have 168 bits total
#
sub process_types_1_2_3() {
#if ( length($bin_string) < 168 ) {
if ( length($bin_string) < 128 ) {
#printf("123\t%s\t%d\n", $bin_string, length($bin_string));
#printf("123\t%d\n", length($bin_string));
if ($print_123) { print "Msg too short\n\n"; }
return;
}
# substr($bin_string, 0, 6); # Message Type
#my $brepeat_indicator = substr($bin_string, 6, 2);
my $bUserID = substr($bin_string, 8, 30); # MMSI
my $bnavStatus = substr($bin_string, 38, 4);
#my $brateOfTurn = substr($bin_string, 42, 8);
my $bSpeedOverGnd = substr($bin_string, 50, 10); #####
#my $bpositionAccuracy = substr($bin_string, 60, 1);
my $bLongitude = substr($bin_string, 61, 28); #####
my $bLatitude = substr($bin_string, 89, 27); #####
my $bCourseOverGnd = substr($bin_string, 116, 12); #####
#my $btrueHeading = substr($bin_string, 128, 9);
#my $btimestamp = substr($bin_string, 137, 6);
#my $bspecialManeuver = substr($bin_string, 143, 2);
#my $bspare = substr($bin_string, 145, 3);
#my $bRAIM = substr($bin_string, 148, 1);
#my $bCommState = substr($bin_string, 149, 19);
my $userID = &bin2dec($bUserID);
if ($print_123) { print " User ID: $userID\n"; }
my $country = &decode_MID($userID);
if ($print_123) { print " Country: $country\n"; }
my $navStatus = &bin2dec($bnavStatus);
my $navStatusTxt = "";
if ($navStatus == 0) { $navStatusTxt = "Under_way:engine"; }
elsif ($navStatus == 1) { $navStatusTxt = "At_anchor"; }
elsif ($navStatus == 2) { $navStatusTxt = "Not_under_command"; }
elsif ($navStatus == 3) { $navStatusTxt = "Restricted_maneuverability"; }
elsif ($navStatus == 4) { $navStatusTxt = "Constrained_by_draught"; }
elsif ($navStatus == 5) { $navStatusTxt = "Moored"; }
elsif ($navStatus == 6) { $navStatusTxt = "Aground"; }
elsif ($navStatus == 7) { $navStatusTxt = "Fishing"; }
elsif ($navStatus == 8) { $navStatusTxt = "Under_way:sailing"; }
#elsif ($navStatus == 9) { $navStatusTxt = "Reserved"; }
#elsif ($navStatus == 10) { $navStatusTxt = "Reserved"; }
#elsif ($navStatus == 11) { $navStatusTxt = "Reserved"; }
#elsif ($navStatus == 12) { $navStatusTxt = "Reserved"; }
#elsif ($navStatus == 13) { $navStatusTxt = "Reserved"; }
elsif ($navStatus == 14) { $navStatusTxt = "AIS-SART_is_active"; }
#elsif ($navStatus == 15) { $navStatusTxt = "Not defined"; }
# NOTE: 0-359 degrees
# NOTE: 360 = N/A
# APRS spec says can set to "000", "...", or " " if unknown
my $courseOverGnd = &bin2dec($bCourseOverGnd) / 10 ;
my $course = "";
if ($courseOverGnd == 360) {
$course = "...";
}
elsif ($courseOverGnd == 0) {
$course = "360";
}
else {
$course = sprintf("%03d", $courseOverGnd);
}
if ($print_123) { print " Course: $courseOverGnd\n"; }
# NOTE: 0 to 102 knots
# NOTE: 102.3 = N/A
# NOTE: 102.2 = 102.2 knots or higher
# APRS spec says can set to "000", "...", or " " if unknown
my $speedOverGnd = &bin2dec($bSpeedOverGnd) / 10;
my $speed = "";
if ($speedOverGnd == 102.3) {
$speed = "...";
}
else {
$speed = sprintf("%03d", $speedOverGnd);
}
if ($print_123) { print " Speed: $speedOverGnd\n"; }
# NOTE: -90 to +90
# NOTE: 91 = N/A
my $latitude = &signedBin2dec($bLatitude) / 600000.0;
if ($latitude == 91) {
if ($print_123) { print "\n"; }
return();
}
my $NS;
if ($print_123) { printf(" Latitude: %07.5f\n", $latitude); }
if ($latitude >= 0.0) {
$NS = 'N';
} else {
$NS = 'S';
$latitude = abs($latitude);
}
my $latdeg = int($latitude);
my $latmins = $latitude - $latdeg;
my $latmins2 = $latmins * 60.0;
my $lat = sprintf("%02d%05.2f%s", $latdeg, $latmins2, $NS);
# NOTE: -180 to +180
# NOTE: 181 = N/A
my $longitude = &signedBin2dec($bLongitude) / 600000.0;
if ($longitude == 181) {
if ($print_123) { print "\n"; }
return();
}
my $EW;
if ($print_123) { printf("Longitude: %08.5f\n", $longitude); }
if ($longitude >= 0.0) {
$EW = 'E';
} else {
$EW = 'W';
$longitude = abs($longitude);
}
my $londeg = int($longitude);
my $lonmins = $longitude - $londeg;
my $lonmins2 = $lonmins * 60.0;
my $lon = sprintf("%03d%05.2f%s", $londeg, $lonmins2, $EW);
my $symbol = "s";
# Set up to add country or vessel_name + country to comment field
my $vesselTag = " ($country)";
if ( defined($vessel_hash{$userID}) ) { $vesselTag = " " . $vessel_hash{$userID} . " (" . $country . ")"; }
my $aprs= &escape_from_shell("$xastir_user>APRS:)$userID!$lat/$lon$symbol$course/$speed $navStatusTxt$vesselTag");
if ($print_123) { print " APRS: $aprs\n"; }
&log_aprs($aprs);
if ($enable_tx) {
my $result = `$udp_client $xastir_host $xastir_port $xastir_user $xastir_pass \"$aprs\"`;
if ($result =~ m/NACK/) {
die "Received NACK from Xastir: Callsign/Passcode don't match?\n";
}
}
# Assign tactical call = "$userID + $country" or "$vesselName + $country"
# Max tactical call in Xastir is 57 chars (56 + terminator?)
#
my $temp;
if ( defined( $vessel_hash{$userID}) ) {
$temp = substr($vessel_hash{$userID} . " (" . $country . ")", 0, 56); # Chop at 56 chars
}
else {
# No vessel name
# N7IPB: Display country of registry
$temp = substr("($country)", 0, 56); # Chop at 56 chars
# WE7U: Display the 9-digit MMSI + country of registry
#$temp = substr($userID . " (" . $country . ")", 0, 56); # Chop at 56 chars
}
if ( !defined($tactical_hash{$userID}) ) {
$tactical_hash{$userID} = $temp;
$aprs = &escape_from_shell($xastir_user . '>' . "APRS::TACTICAL :" . $userID . "=" . $temp);
if ($print_123) { print " APRS: $aprs\n"; }
&log_aprs($aprs);
if ($enable_tx) {
$result = `$udp_client $xastir_host $xastir_port $xastir_user $xastir_pass \"$aprs\"`;
if ($result =~ m/NACK/) {
die "Received NACK from Xastir: Callsign/Passcode don't match?\n";
}
}
}
if ($print_123) { print "\n"; }
return();
}
# Message type 4: Base station report
# These messages should have 168 bits total
#
sub process_type_4() {
#if ( length($bin_string) < 168 ) {
if ( length($bin_string) < 128 ) {
#printf("123\t%s\t%d\n", $bin_string, length($bin_string));
#printf("123\t%d\n", length($bin_string));
if ($print_4) { print "Msg too short\n\n"; }
return;
}
my $bUserID = substr($bin_string, 8, 30); # MMSI
my $bLongitude = substr($bin_string, 79, 28); #####
my $bLatitude = substr($bin_string, 107, 27); #####
my $userID = &bin2dec($bUserID);
if ($print_4) { print " User ID: $userID\n"; }
my $country = &decode_MID($userID);
if ($print_4) { print " Country: $country\n"; }
# NOTE: -90 to +90
# NOTE: 91 = N/A
my $latitude = &signedBin2dec($bLatitude) / 600000.0;
if ($latitude == 91) { return(); }
my $NS;
if ($print_4) { printf(" Latitude: %07.5f\n", $latitude); }
if ($latitude >= 0.0) {
$NS = 'N';
} else {
$NS = 'S';
$latitude = abs($latitude);
}
my $latdeg = int($latitude);
my $latmins = $latitude - $latdeg;
my $latmins2 = $latmins * 60.0;
my $lat = sprintf("%02d%05.2f%s", $latdeg, $latmins2, $NS);
# NOTE: -180 to +180
# NOTE: 181 = N/A
my $longitude = &signedBin2dec($bLongitude) / 600000.0;
if ($longitude == 181) { return(); }
my $EW;
if ($print_4) { printf("Longitude: %08.5f\n", $longitude); }
if ($longitude >= 0.0) {
$EW = 'E';
} else {
$EW = 'W';
$longitude = abs($longitude);
}
my $londeg = int($longitude);
my $lonmins = $longitude - $londeg;
my $lonmins2 = $lonmins * 60.0;
my $lon = sprintf("%03d%05.2f%s", $londeg, $lonmins2, $EW);
my $symbol = "/";
# Set up to add country or vessel_name + country to comment field
my $vesselTag = " Fixed Base Station";
if ( defined($vessel_hash{$userID}) ) { $vesselTag = " " . $vessel_hash{$userID} . " (" . $country . ")"; }
my $aprs= &escape_from_shell("$xastir_user>APRS:)$userID!$lat/$lon$symbol $vesselTag");
if ($print_4) { print " APRS: $aprs\n"; }
&log_aprs($aprs);
if ($enable_tx) {
my $result = `$udp_client $xastir_host $xastir_port $xastir_user $xastir_pass \"$aprs\"`;
if ($result =~ m/NACK/) {
die "Received NACK from Xastir: Callsign/Passcode don't match?\n";
}
}
# Assign tactical call = "$userID + $country" or "$vesselName + $country"
# Max tactical call in Xastir is 57 chars (56 + terminator?)
#
my $temp;
# if ( defined( $vessel_hash{$userID}) ) {
# $temp = substr($vessel_hash{$userID} . " (" . $country . ")", 0, 56); # Chop at 56 chars
# }
# else {
# # No vessel name
# # N7IPB: Display country of registry
# $temp = substr("($country)", 0, 56); # Chop at 56 chars
#
# # WE7U: Display the 9-digit MMSI + country of registry
# #$temp = substr($userID . " (" . $country . ")", 0, 56); # Chop at 56 chars
# }
# Force the tactical call to be FS since there are no names for Base Stations
#
$temp = "FS";
if ( !defined($tactical_hash{$userID}) ) {
$tactical_hash{$userID} = $temp;
$aprs = &escape_from_shell($xastir_user . '>' . "APRS::TACTICAL :" . $userID . "=" . $temp);
if ($print_4) { print " APRS: $aprs\n"; }
&log_aprs($aprs);
if ($enable_tx) {
$result = `$udp_client $xastir_host $xastir_port $xastir_user $xastir_pass \"$aprs\"`;
if ($result =~ m/NACK/) {
die "Received NACK from Xastir: Callsign/Passcode don't match?\n";
}
}
}
if ($print_4) { print "\n"; }
return();
}
# Message type 5: Static and Voyage Related Data (Vessel name, callsign, ship type)
# This message should have 424 bits total
#
sub process_type_5() {
#if ( length($bin_string) < 424 ) {
if ( length($bin_string) < 240 ) {
#printf("5\t%s\t%d\n", $bin_string, length($bin_string));
#printf("5\t%d\n", length($bin_string));
if ($print_5) { print "Msg too short\n\n"; }
return;
}
# substr($bin_string, 0, 6); # Message Type
# my $brepeat_indicator = substr($bin_string, 6, 2); # Repeat Indicator
my $bUserID = substr($bin_string, 8, 30); # MMSI
# substr($bin_string, 38, 2); # AIS Version
# substr($bin_string, 40, 30); # IMO Number
#my $bCallsign = substr($bin_string, 70, 42); # Call Sign
my $bVesselName = substr($bin_string, 112, 120); # Vessel Name #####
my $bShipType = substr($bin_string, 232, 8); # Ship Type: Page 114 of ITU spec, Table 53
# substr($bin_string, 240, 9); # Dimension to Bow
# substr($bin_string, 249, 9); # Dimension to Stern
# substr($bin_string, 258, 6); # Dimension to Port
# substr($bin_string, 264, 6); # Dimension to Starboard
# substr($bin_string, 270, 4); # Position Fix Type
# substr($bin_string, 274, 4); # ETA month (UTC)
# substr($bin_string, 278, 5); # ETA day (UTC)
# substr($bin_string, 283, 5); # ETA hour (UC)
# substr($bin_string, 288, 6); # ETA minute (UTC)
# substr($bin_string, 294, 8); # Draught
# substr($bin_string, 302, 120); # Destination
# substr($bin_string, 422, 1); # DTE
# substr($bin_string, 423, 1); # Spare
my $userID = &bin2dec($bUserID);
if ($print_5) { print " User ID: $userID\n"; }
#my $callSign = &bin2text($bCallsign);
#print "Callsign: $callSign\n";
my $country = &decode_MID($userID);
if ($print_5) { print " Country: $country\n"; }
my $vesselName = &bin2text($bVesselName);
$vesselName =~ s/\s+$//; # Remove extra spaces at end
$vessel_hash{$userID} = $vesselName;
# Update the persistent store
store \%vessel_hash , $persistentFileSpec;
if ($print_5) { print " Vessel: $vesselName\n"; }
my $shipTypeTxt = &decodeShipType($bShipType);
if ($print_5) { print "Ship Type: $shipTypeTxt\n"; }
# Assign tactical call = $vesselName + $country
# Max tactical call in Xastir is 57 chars (56 + terminator?)
#
# my $temp = substr($vesselName . " (" . $country . ")", 0, 56); # Chop at 56 chars
my $temp = substr($vesselName . " ($shipTypeTxt:$country)", 0, 56); # Chop at 56 chars
if ( !defined($tactical_hash{$userID}) || $tactical_hash{$userID} ne $temp ) {
$tactical_hash{$userID} = $temp;
$aprs = &escape_from_shell($xastir_user . '>' . "APRS::TACTICAL :" . $userID . "=" . $temp);
if ($print_5) { print " APRS: $aprs\n"; }
&log_aprs($aprs);
if ($enable_tx) {
$result = `$udp_client $xastir_host $xastir_port $xastir_user $xastir_pass \"$aprs\"`;
if ($result =~ m/NACK/) {
die "Received NACK from Xastir: Callsign/Passcode don't match?\n";
}
}
}
if ($print_5) { print "\n"; }
return();
}
# Message type 9: Standard SAR Aircraft Position Report
# This message should have 168 bits total
#
sub process_type_9() {
#if ( length($bin_string) < 168 ) {
if ( length($bin_string) < 128 ) {
#printf("9\t%s\t%d\n", $bin_string, length($bin_string));
#printf("9\t%d\n", length($bin_string));
if ($print_9) { print "Msg too short\n\n"; }
return;
}
# substr($bin_string, 0, 6); # Message Type
# my $brepeat_indicator = substr($bin_string, 6, 2); # Repeat Indicator
my $bUserID = substr($bin_string, 8, 30); # MMSI
my $bAltitude = substr($bin_string, 38, 12); # Altitude
my $bSpeedOverGnd = substr($bin_string, 50, 10); # SOG
# substr($bin_string, 60, 1); # Position Accuracy
my $bLongitude = substr($bin_string, 61, 28); # Longitude
my $bLatitude = substr($bin_string, 89, 27); # Latitude
my $bCourseOverGnd = substr($bin_string, 116, 12); # Course Over Ground
# substr($bin_string, 128, 6); # Time Stamp
# substr($bin_string, 134, 1); # Altitude sensor
# substr($bin_string, 135, 7); # Spare
# substr($bin_string, 142, 1); # DTE
# substr($bin_string, 143, 3); # Spare
# substr($bin_string, 146, 1); # Assigned mode
# substr($bin_string, 147, 1); # RAIM flag
# substr($bin_string, 148, 1); # Comm state selector flag
# substr($bin_string, 149, 19); # Radio status
my $userID = &bin2dec($bUserID);
if ($print_9) { print " User ID: $userID\n"; }
my $country = &decode_MID($userID);
if ($print_9) { print " Country: $country\n"; }
# NOTE: 4095 = N/A
# NOTE: 4094 = 4094 meters or higher.
my $altitude_meters = &bin2dec($bAltitude);
my $altitude = "";
if ($altitude_meters != 4095) {
if ($altitude_meters == 4094) {
$altitude = sprintf( "%s%06d%s", " /A=", $altitude_meters * 3.28084, "(Higher than)" );
}
else {
$altitude = sprintf( "%s%06d", " /A=", $altitude_meters * 3.28084 );
}
if ($print_9) { print " Altitude: $altitude\n"; }
}
# NOTE: 0-359 degrees
# NOTE: 360 = N/A
# APRS spec says can set to "000", "...", or " " if unknown
my $courseOverGnd = &bin2dec($bCourseOverGnd) / 10 ;
my $course = "";
if ($courseOverGnd == 360) {
$course = "...";
}
elsif ($courseOverGnd == 0) {
$course = "360";
}
else {
$course = sprintf("%03d", $courseOverGnd);
}
if ($print_9) { print " Course: $courseOverGnd\n"; }
# NOTE: 0 to 102 knots
# NOTE: 102.3 = N/A
# NOTE: 102.2 = 102.2 knots or higher
# APRS spec says can set to "000", "...", or " " if unknown
my $speedOverGnd = &bin2dec($bSpeedOverGnd) / 10;
my $speed = "";
if ($speedOverGnd == 102.3) {
$speed = "...";
}
else {
$speed = sprintf("%03d", $speedOverGnd);
}
if ($print_9) { print " Speed: $speedOverGnd\n"; }
# NOTE: -90 to +90
# NOTE: 91 = N/A
my $latitude = &signedBin2dec($bLatitude) / 600000.0;
if ($latitude == 91) {
if ($print_9) { print "\n"; }
return();
}
my $NS;
if ($print_9) { printf(" Latitude: %07.5f\n", $latitude); }
if ($latitude >= 0.0) {
$NS = 'N';
} else {
$NS = 'S';
$latitude = abs($latitude);
}
my $latdeg = int($latitude);
my $latmins = $latitude - $latdeg;
my $latmins2 = $latmins * 60.0;
my $lat = sprintf("%02d%05.2f%s", $latdeg, $latmins2, $NS);
# NOTE: -180 to +180
# NOTE: 181 = N/A
my $longitude = &signedBin2dec($bLongitude) / 600000.0;
if ($longitude == 181) {
if ($print_9) { print "\n"; }
return();
}
my $EW;
if ($print_9) { printf("Longitude: %08.5f\n", $longitude); }
if ($longitude >= 0.0) {
$EW = 'E';
} else {
$EW = 'W';
$longitude = abs($longitude);
}
my $londeg = int($longitude);
my $lonmins = $longitude - $londeg;
my $lonmins2 = $lonmins * 60.0;
my $lon = sprintf("%03d%05.2f%s", $londeg, $lonmins2, $EW);
my $symbol = "^"; # "^" = Large Aircraft. Could be "'" for small aircraft, "X" for helicopter.
my $vesselTag = " ($country)";
if ( defined($vessel_hash{$userID}) ) { $vesselTag = " " . $vessel_hash{$userID} . " (" . $country . ")"; }
my $aprs = &escape_from_shell("$xastir_user>APRS:)$userID!$lat/$lon$symbol$course/$speed$altitude SAR Aircraft$vesselTag");
if ($print_9) { print " APRS: $aprs\n"; }
&log_aprs($aprs);
if ($enable_tx) {
my $result = `$udp_client $xastir_host $xastir_port $xastir_user $xastir_pass \"$aprs\"`;
if ($result =~ m/NACK/) {
die "Received NACK from Xastir: Callsign/Passcode don't match?\n";
}
}
# Assign tactical call = "$userID + $country" or "$vesselName + $country"
# Max tactical call in Xastir is 57 chars (56 + terminator?)
#
my $temp;
if ( defined( $vessel_hash{$userID}) ) {
$temp = substr($vessel_hash{$userID} . " (" . $country . ")", 0, 56); # Chop at 56 chars
}
else {
# No vessel name
# N7IPB: Display country of registry
$temp = substr("($country)", 0, 56); # Chop at 56 chars
# WE7U: Display the 9-digit MMSI + country of registry
#$temp = substr($userID . " (" . $country . ")", 0, 56); # Chop at 56 chars
}
if ( !defined($tactical_hash{$userID}) ) {
$tactical_hash{$userID} = $temp;
$aprs = &escape_from_shell($xastir_user . '>' . "APRS::TACTICAL :" . $userID . "=" . $temp);
if ($print_9) { print " APRS: $aprs\n"; }
&log_aprs($aprs);
if ($enable_tx) {
$result = `$udp_client $xastir_host $xastir_port $xastir_user $xastir_pass \"$aprs\"`;
if ($result =~ m/NACK/) {
die "Received NACK from Xastir: Callsign/Passcode don't match?\n";
}
}
}
if ($print_9) { print "\n"; }
return();
}
# Message type 18: Standard Class B CS Position Report
# This message should have 168 bits total
#
sub process_type_18() {
#if ( length($bin_string) < 168 ) {
if ( length($bin_string) < 124 ) {
#printf("18\t%s\t%d\n", $bin_string, length($bin_string));
#printf("18\t%d\n", length($bin_string));
if ($print_18) { print "Msg too short\n\n"; }
return;
}
# substr($bin_string, 0, 6); # Message Type
# my $brepeat_indicator = substr($bin_string, 6, 2); # Repeat Indicator
my $bUserID = substr($bin_string, 8, 30); # MMSI
# substr($bin_string, 38, 8); # Spare
my $bSpeedOverGnd = substr($bin_string, 46, 10); # Speed Over Ground
# substr($bin_string, 56, 1); # Position Accuracy
my $bLongitude = substr($bin_string, 57, 28); # Longitude
my $bLatitude = substr($bin_string, 85, 27); # Latitude
my $bCourseOverGnd = substr($bin_string, 112, 12); # Course Over Ground
# substr($bin_string, 124, 9); # True Heading
# substr($bin_string, 133, 6); # Time Stamp
# substr($bin_string, 139, 2); # Spare
# substr($bin_string, 141, 1); # CS Unit
# substr($bin_string, 142, 1); # Display flag
# substr($bin_string, 143, 1); # DSC Flag
# substr($bin_string, 144, 1); # Band flag
# substr($bin_string, 145, 1); # Message 22 flag
# substr($bin_string, 146, 1); # Assigned mode
# substr($bin_string, 147, 1); # RAIM flag
# substr($bin_string, 148, 1); # Comm state selector flag
# substr($bin_string, 149, 19); # Radio status
my $userID = &bin2dec($bUserID);
if ($print_18) { print " User ID: $userID\n"; }
my $country = &decode_MID($userID);
if ($print_18) { print " Country: $country\n"; }
# NOTE: 0-359 degrees
# NOTE: 360 = N/A
# APRS spec says can set to "000", "...", or " " if unknown
my $courseOverGnd = &bin2dec($bCourseOverGnd) / 10 ;
my $course = "";
if ($courseOverGnd == 360) {
$course = "...";
}
elsif ($courseOverGnd == 0) {
$course = "360";
}
else {
$course = sprintf("%03d", $courseOverGnd);
}
if ($print_18) { print " Course: $courseOverGnd\n"; }
# NOTE: 0 to 102 knots
# NOTE: 102.3 = N/A
# NOTE: 102.2 = 102.2 knots or higher
# APRS spec says can set to "000", "...", or " " if unknown
my $speedOverGnd = &bin2dec($bSpeedOverGnd) / 10;
my $speed = "";
if ($speedOverGnd == 102.3) {
$speed = "...";
}
else {
$speed = sprintf("%03d", $speedOverGnd);
}
if ($print_18) { print " Speed: $speedOverGnd\n"; }
# NOTE: -90 to +90
# NOTE: 91 = N/A
my $latitude = &signedBin2dec($bLatitude) / 600000.0;
if ($latitude == 91) {
if ($print_18) { print "\n"; }
return();
}
my $NS;
if ($print_18) { printf(" Latitude: %07.5f\n", $latitude); }
if ($latitude >= 0.0) {
$NS = 'N';
} else {
$NS = 'S';
$latitude = abs($latitude);
}
my $latdeg = int($latitude);
my $latmins = $latitude - $latdeg;
my $latmins2 = $latmins * 60.0;
my $lat = sprintf("%02d%05.2f%s", $latdeg, $latmins2, $NS);
# NOTE: -180 to +180
# NOTE: 181 = N/A
my $longitude = &signedBin2dec($bLongitude) / 600000.0;
if ($longitude == 181) {
if ($print_18) { print "\n"; }
return();
}
my $EW;
if ($print_18) { printf("Longitude: %08.5f\n", $longitude); }
if ($longitude >= 0.0) {
$EW = 'E';
} else {
$EW = 'W';
$longitude = abs($longitude);
}
my $londeg = int($longitude);
my $lonmins = $longitude - $londeg;
my $lonmins2 = $lonmins * 60.0;
my $lon = sprintf("%03d%05.2f%s", $londeg, $lonmins2, $EW);
my $symbol = "s";
my $vesselTag = " ($country)";
if ( defined($vessel_hash{$userID}) ) { $vesselTag = " " . $vessel_hash{$userID} . " (" . $country . ")"; }
my $aprs = &escape_from_shell("$xastir_user>APRS:)$userID!$lat/$lon$symbol$course/$speed$vesselTag");
if ($print_18) { print " APRS: $aprs\n"; }
&log_aprs($aprs);
if ($enable_tx) {
my $result = `$udp_client $xastir_host $xastir_port $xastir_user $xastir_pass \"$aprs\"`;
if ($result =~ m/NACK/) {
die "Received NACK from Xastir: Callsign/Passcode don't match?\n";
}
}
# Assign tactical call = "$userID + $country" or "$vesselName + $country"
# Max tactical call in Xastir is 57 chars (56 + terminator?)
#
my $temp;
if ( defined( $vessel_hash{$userID}) ) {
$temp = substr($vessel_hash{$userID} . " (" . $country . ")", 0, 56); # Chop at 56 chars
}
else {
# No vessel name
# N7IPB: Display country of registry
$temp = substr("($country)", 0, 56); # Chop at 56 chars
# WE7U: Display the 9-digit MMSI + country of registry
#$temp = substr($userID . " (" . $country . ")", 0, 56); # Chop at 56 chars
}
if ( !defined($tactical_hash{$userID}) ) {
$tactical_hash{$userID} = $temp;
$aprs = &escape_from_shell($xastir_user . '>' . "APRS::TACTICAL :" . $userID . "=" . $temp);
if ($print_18) { print " APRS: $aprs\n"; }
&log_aprs($aprs);
if ($enable_tx) {
$result = `$udp_client $xastir_host $xastir_port $xastir_user $xastir_pass \"$aprs\"`;
if ($result =~ m/NACK/) {
die "Received NACK from Xastir: Callsign/Passcode don't match?\n";
}
}
}
if ($print_18) { print "\n"; }
return();
}
# Message type 19: Extended Class B CS Position Report
# This message should have 312 bits total
#
sub process_type_19() {
#if ( length($bin_string) < 312 ) {
if ( length($bin_string) < 263 ) {
#printf("19\t%s\t%d\n", $bin_string, length($bin_string));
#printf("19\t%d\n", length($bin_string));
if ($print_19) { print "Msg too short\n\n"; }
return;
}
# substr($bin_string, 0, 6); # Message Type
# my $brepeat_indicator = substr($bin_string, 6, 2); # Repeat Indicator
my $bUserID = substr($bin_string, 8, 30); # MMSI
# substr($bin_string, 38, 8); # Spare
my $bSpeedOverGnd = substr($bin_string, 46, 10); # Speed Over Ground
# substr($bin_string, 56, 1); # Position Accuracy
my $bLongitude = substr($bin_string, 57, 28); # Longitude
my $bLatitude = substr($bin_string, 85, 27); # Latitude
my $bCourseOverGnd = substr($bin_string, 112, 12); # Course Over Ground
# substr($bin_string, 124, 9); # True Heading
# substr($bin_string, 133, 6); # Time Stamp
# substr($bin_string, 139, 4); # Spare
my $bVesselName = substr($bin_string, 143, 120); # Name
# substr($bin_string, 263, 8); # Type of ship and cargo
# substr($bin_string, 271, 9); # Dimension to Bow
# substr($bin_string, 280, 9); # Dimension to Stern
# substr($bin_string, 289, 6); # Dimension to Port
# substr($bin_string, 295, 6); # Dimension to Starboard
# substr($bin_string, 301, 4); # Position Fix Type
# substr($bin_string, 305, 1); # RAIM flag
# substr($bin_string, 306, 1); # DTE
# substr($bin_string, 307, 1); # Assigned mode flag
# substr($bin_string, 308, 4); # Spare
my $userID = &bin2dec($bUserID);
if ($print_19) { print " User ID: $userID\n"; }
my $country = &decode_MID($userID);
if ($print_19) { print " Country: $country\n"; }
# NOTE: 0-359 degrees
# NOTE: 360 = N/A
# APRS spec says can set to "000", "...", or " " if unknown
my $courseOverGnd = &bin2dec($bCourseOverGnd) / 10 ;
my $course = "";
if ($courseOverGnd == 360) {
$course = "...";
}
elsif ($courseOverGnd == 0) {
$course = "360";
}
else {
$course = sprintf("%03d", $courseOverGnd);
}
if ($print_19) { print " Course: $courseOverGnd\n"; }
# NOTE: 0 to 102 knots
# NOTE: 102.3 = N/A
# NOTE: 102.2 = 102.2 knots or higher
# APRS spec says can set to "000", "...", or " " if unknown
my $speedOverGnd = &bin2dec($bSpeedOverGnd) / 10;
my $speed = "";
if ($speedOverGnd == 102.3) {
$speed = "...";
}
else {
$speed = sprintf("%03d", $speedOverGnd);
}
if ($print_19) { print " Speed: $speedOverGnd\n"; }
# NOTE: -90 to +90
# NOTE: 91 = N/A
my $latitude = &signedBin2dec($bLatitude) / 600000.0;
if ($latitude == 91) {
if ($print_19) { print "\n"; }
return();
}
my $NS;
if ($print_19) { printf(" Latitude: %07.5f\n", $latitude); }
if ($latitude >= 0.0) {
$NS = 'N';
} else {
$NS = 'S';
$latitude = abs($latitude);
}
my $latdeg = int($latitude);
my $latmins = $latitude - $latdeg;
my $latmins2 = $latmins * 60.0;
my $lat = sprintf("%02d%05.2f%s", $latdeg, $latmins2, $NS);
# NOTE: -180 to +180
# NOTE: 181 = N/A
my $longitude = &signedBin2dec($bLongitude) / 600000.0;
if ($longitude == 181) {
if ($print_19) { print "\n"; }
return();
}
my $EW;
if ($print_19) { printf("Longitude: %08.5f\n", $longitude); }
if ($longitude >= 0.0) {
$EW = 'E';
} else {
$EW = 'W';
$longitude = abs($longitude);
}
my $londeg = int($longitude);
my $lonmins = $longitude - $londeg;
my $lonmins2 = $lonmins * 60.0;
my $lon = sprintf("%03d%05.2f%s", $londeg, $lonmins2, $EW);
my $vesselName = "";
if ($bVesselName ne "") {
$vesselName = &bin2text($bVesselName);
$vesselName =~ s/\s+$//; # Remove extra spaces at end
}
my $symbol = "s";
my $vesselTag = " ($country)";
if ( defined($vessel_hash{$userID}) ) { $vesselTag = " " . $vessel_hash{$userID} . " (" . $country . ")"; }
my $aprs = &escape_from_shell("$xastir_user>APRS:)$userID!$lat/$lon$symbol$course/$speed$vesselTag");
if ($print_19) { print " APRS: $aprs\n"; }
&log_aprs($aprs);
if ($enable_tx) {
my $result = `$udp_client $xastir_host $xastir_port $xastir_user $xastir_pass \"$aprs\"`;
if ($result =~ m/NACK/) {
die "Received NACK from Xastir: Callsign/Passcode don't match?\n";
}
}
if ($vesselName ne "") {
$vessel_hash{$userID} = $vesselName;
# Update the persistent store
store \%vessel_hash , $persistentFileSpec;
if ($print_19) { print " Vessel: $vesselName\n"; }
# Assign tactical call = $vesselName
# Max tactical call in Xastir is 57 chars (56 + terminator?)
#
my $temp = substr($vesselName . " (" . $country . ")", 0, 56); # Chop at 56 chars
if ( !defined($tactical_hash{$userID}) || $tactical_hash{$userID} ne $temp ) {
$tactical_hash{$userID} = $temp;
$aprs = &escape_from_shell($xastir_user . '>' . "APRS::TACTICAL :" . $userID . "=" . $temp);
if ($print_19) { print " APRS: $aprs\n"; }
&log_aprs($aprs);
if ($enable_tx) {
$result = `$udp_client $xastir_host $xastir_port $xastir_user $xastir_pass \"$aprs\"`;
if ($result =~ m/NACK/) {
die "Received NACK from Xastir: Callsign/Passcode don't match?\n";
}
}
}
}
if ($print_19) { print "\n"; }
return();
}
# Message type 21: Aid to Navigation report
# These messages should have between 272 and 360 bits total
# We don't use the full message and don't parse the name extension
#
sub process_type_21() {
#if ( length($bin_string) < 168 ) {
if ( length($bin_string) < 272 ) {
#printf("21\t%s\t%d\n", $bin_string, length($bin_string));
#printf("21\t%d\n", length($bin_string));
if ($print_21) { print "Msg too short\n\n"; }
return;
}
my $bUserID = substr($bin_string, 8, 30); # MMSI
my $bAidType = substr($bin_string, 38, 5);
my $bVesselName = substr($bin_string, 43, 120); # Vessel Name #####
my $bLongitude = substr($bin_string, 164, 28); #####
my $bLatitude = substr($bin_string, 192, 27); #####
my $userID = &bin2dec($bUserID);
if ($print_21) { print " User ID: $userID\n"; }
my $country = &decode_MID($userID);
if ($print_21) { print " Country: $country\n"; }
my $vesselName = &bin2text($bVesselName);
$vesselName =~ s/\s+$//; # Remove extra spaces at end
$vessel_hash{$userID} = $vesselName;
# Update the persistent store
store \%vessel_hash , $persistentFileSpec;
if ($print_21) { print " Vessel: $vesselName\n"; }
my $AidTypeTxt = &decodeAidType($bAidType);
if ($print_21) { print "Navaid Type: $AidTypeTxt\n"; }
# NOTE: -90 to +90
# NOTE: 91 = N/A
my $latitude = &signedBin2dec($bLatitude) / 600000.0;
if ($latitude == 91) { return(); }
my $NS;
if ($print_21) { printf(" Latitude: %07.5f\n", $latitude); }
if ($latitude >= 0.0) {
$NS = 'N';
} else {
$NS = 'S';
$latitude = abs($latitude);
}
my $latdeg = int($latitude);
my $latmins = $latitude - $latdeg;
my $latmins2 = $latmins * 60.0;
my $lat = sprintf("%02d%05.2f%s", $latdeg, $latmins2, $NS);
# NOTE: -180 to +180
# NOTE: 181 = N/A
my $longitude = &signedBin2dec($bLongitude) / 600000.0;
if ($longitude == 181) { return(); }
my $EW;
if ($print_21) { printf("Longitude: %08.5f\n", $longitude); }
if ($longitude >= 0.0) {
$EW = 'E';
} else {
$EW = 'W';
$longitude = abs($longitude);
}
my $londeg = int($longitude);
my $lonmins = $longitude - $londeg;
my $lonmins2 = $lonmins * 60.0;
my $lon = sprintf("%03d%05.2f%s", $londeg, $lonmins2, $EW);
my $symbol = "N";
# Set up to add country or vessel_name + country to comment field
my $vesselTag = " (nav-aid)";
if ( defined($vessel_hash{$userID}) ) { $vesselTag = " " . $vessel_hash{$userID} . " (" . $country . ")"; }
my $aprs= &escape_from_shell("$xastir_user>APRS:)$userID!$lat\\$lon$symbol $AidTypeTxt");
if ($print_21) { print " APRS: $aprs\n"; }
&log_aprs($aprs);
if ($enable_tx) {
my $result = `$udp_client $xastir_host $xastir_port $xastir_user $xastir_pass \"$aprs\"`;
if ($result =~ m/NACK/) {
die "Received NACK from Xastir: Callsign/Passcode don't match?\n";
}
}
# Assign tactical call = $vesselName + $country
# Max tactical call in Xastir is 57 chars (56 + terminator?)
#
my $temp = substr($vesselName, 0, 56); # Chop at 56 chars
if ( !defined($tactical_hash{$userID}) || $tactical_hash{$userID} ne $temp ) {
$tactical_hash{$userID} = $temp;
$aprs = &escape_from_shell($xastir_user . '>' . "APRS::TACTICAL :" . $userID . "=" . $temp);
if ($print_21) { print " APRS: $aprs\n"; }
&log_aprs($aprs);
if ($enable_tx) {
$result = `$udp_client $xastir_host $xastir_port $xastir_user $xastir_pass \"$aprs\"`;
if ($result =~ m/NACK/) {
die "Received NACK from Xastir: Callsign/Passcode don't match?\n";
}
}
}
if ($print_21) { print "\n"; }
return();
}
# Message type 24: Static Data Report (Vessel name, callsign, ship type)
# This one has three variations:
#
# If Part Number field is 0, it's a Type A message.
# If Part Number is 1, it's a Type B.
#
# Part B has two variations as well:
# If the MMSI is that of an auxiliary craft, then the Mothership MMSI
# is that of the mothership.
# If not, then those 30 bits represent vessel dimmensions.
#
# This message should have:
# Type A: 160 bits total
# Type B: 168 bits total
#
sub process_type_24() {
#if ( length($bin_string) < 160 ) {
if ( length($bin_string) < 160 ) {
#printf("24\t%s\t%d\n", $bin_string, length($bin_string));
#printf("24\t%d\n", length($bin_string));
if ($print_24_A || $print_24_B) { print "Msg too short\n\n"; }
return;
}
# substr($bin_string, 0, 6); # Message Type
# my $brepeat_indicator = substr($bin_string, 6, 2); # Repeat Indicator
my $bUserID = substr($bin_string, 8, 30); # MMSI
my $bPartNumber = substr($bin_string, 38, 2); # Part Number
my $userID = &bin2dec($bUserID);
# Check for Type A/B Variant (Type B also has two variants!)
$PartNumber = &bin2dec($bPartNumber);
if ($PartNumber == 0) {
# It is a Type A Variant
if ($print_24_A) { print " Msg Type: $message_type\tType A Variant\n"; }
if ($print_24_A) { print " User ID: $userID\n"; }
my $country = &decode_MID($userID);
if ($print_24_A) { print " Country: $country\n"; }
# Type A Variant format starts at bit 40:
my $bVesselName = substr($bin_string, 40, 120); # Vessel Name
# substr($bin_string, 160, 8); # Spare.
my $vesselName = "";
if ($bVesselName ne "") {
$vesselName = &bin2text($bVesselName);
$vesselName =~ s/\s+$//; # Remove extra spaces at end
}
if ($vesselName ne "") {
$vessel_hash{$userID} = $vesselName;
# Update the persistent store
store \%vessel_hash , $persistentFileSpec;
if ($print_24_A) { print " Vessel: $vesselName\n"; }
# Assign tactical call = $vesselName
# Max tactical call in Xastir is 57 chars (56 + terminator?)
#
my $temp = substr($vesselName . " (" . $country . ")", 0, 56); # Chop at 56 chars
if ( !defined($tactical_hash{$userID}) || $tactical_hash{$userID} ne $temp ) {
$tactical_hash{$userID} = $temp;
$aprs = &escape_from_shell($xastir_user . '>' . "APRS::TACTICAL :" . $userID . "=" . $temp);
if ($print_24_A) { print " APRS: $aprs\n"; }
&log_aprs($aprs);
if ($enable_tx) {
$result = `$udp_client $xastir_host $xastir_port $xastir_user $xastir_pass \"$aprs\"`;
if ($result =~ m/NACK/) {
die "Received NACK from Xastir: Callsign/Passcode don't match?\n";
}
}
}
}
if ($print_24_A) { print "\n"; }
}
else {
# It is a Type B Variant. There are two variants of this type!
if ($print_24_B) { print " Msg Type: $message_type\tType B Variant\n"; }
if ($print_24_B) { print " User ID: $userID\n"; }
my $country = &decode_MID($userID);
if ($print_24_B) { print " Country: $country\n"; }
# Check the UserID to see if it is an Auxiliary Craft,
# to determine which Type B Variant to decode.
# Auxiliary craft: MMSI of form 98XXXYYYY, the XXX digits are the country code.
if ( !($userID =~ m/^98/)) {
# It is NOT an auxiliary craft:
# Alternate format starting at bit 40:
$bShipType = substr($bin_string, 40, 8); # Ship Type: Page 114 of ITU spec, Table 53
# substr($bin_string, 48, 18); # Vendor ID
# substr($bin_string, 66, 4); # Unit Model Code
# substr($bin_string, 70, 20); # Serial Number)
# substr($bin_string, 90, 42); # Call Sign
# substr($bin_string, 132, 9); # Dimension to bow
# substr($bin_string, 141, 9); # Dimension to Stern
# substr($bin_string, 150, 6); # Dimension to Port
# substr($bin_string, 156, 6); # Dimension to Starboard
# substr($bin_string, 162, 4); # Type of fix
# substr($bin_string, 166, 2); # Spare
my $shipTypeTxt = &decodeShipType($bShipType);
if ($print_24_B) { print "Ship Type: $shipTypeTxt\n"; }
# Assign tactical call = $vesselName + $shipTypeTxt
# Max tactical call in Xastir is 57 chars (56 + terminator?)
#
#my $temp = substr($vesselName . " (" . $country . ")", 0, 56); # Chop at 56 chars
my $temp;
if (defined($vessel_hash{$userID}) ) {
$temp = substr($vessel_hash{$userID} . " ($shipTypeTxt:$country)", 0, 56); # Chop at 56 chars
}
else {
$temp = substr("($shipTypeTxt:$country)", 0, 56); # Chop at 56 chars
}
if ( !defined($tactical_hash{$userID}) || $tactical_hash{$userID} ne $temp ) {
$tactical_hash{$userID} = $temp;
$aprs = &escape_from_shell($xastir_user . '>' . "APRS::TACTICAL :" . $userID . "=" . $temp);
if ($print_24_A) { print " APRS: $aprs\n"; }
&log_aprs($aprs);
if ($enable_tx) {
$result = `$udp_client $xastir_host $xastir_port $xastir_user $xastir_pass \"$aprs\"`;
if ($result =~ m/NACK/) {
die "Received NACK from Xastir: Callsign/Passcode don't match?\n";
}
}
}
}
else {
# It IS an auxiliary craft:
# Alternate format starting at bit 132:
# substr($bin_string, 132, 30); # Mothership MMSI
# substr($bin_string, 162, 6); # Spare
}
if ($print_24_B) { print "\n"; }
}
return();
}
# Message type 27: Position Report For Long-Range Applications (rare)
# This message should have 96 bits total
#
sub process_type_27() {
#if ( length($bin_string) < 96 ) {
if ( length($bin_string) < 94 ) {
#printf("27\t%s\t%d\n", $bin_string, length($bin_string));
#printf("27\t%d\n", length($bin_string));
if ($print_27) { print "Msg too short\n\n"; }
return;
}
# substr($bin_string, 0, 6); # Message Type
# my $brepeat_indicator = substr($bin_string, 6, 2); # Repeat Indicator
my $bUserID = substr($bin_string, 8, 30); # MMSI
# substr($bin_string, 38, 1); # Position Accuracy
# substr($bin_string, 39, 1); # RAIM flag
my $bNavStatus = substr($bin_string, 40, 4); # Navigation Status
my $bLongitude = substr($bin_string, 44, 18); # Longitude
my $bLatitude = substr($bin_string, 62, 17); # Latitude
my $bSpeedOverGnd = substr($bin_string, 79, 6); # Speed Over Ground
my $bCourseOverGnd = substr($bin_string, 85, 9); # Course Over Ground
# substr($bin_string, 94, 1); # GNSS Position latency
# substr($bin_string, 95, 1); # Spare
my $userID = &bin2dec($bUserID);
if ($print_27) { print " User ID: $userID\n"; }
my $country = &decode_MID($userID);
if ($print_27) { print " Country: $country\n"; }
my $navStatus = &bin2dec($bNavStatus);
my $navStatusTxt = "";
if ($navStatus == 0) { $navStatusTxt = "Under_way:engine"; }
elsif ($navStatus == 1) { $navStatusTxt = "At_anchor"; }
elsif ($navStatus == 2) { $navStatusTxt = "Not_under_command"; }
elsif ($navStatus == 3) { $navStatusTxt = "Restricted_maneuverability"; }
elsif ($navStatus == 4) { $navStatusTxt = "Constrained_by_draught"; }
elsif ($navStatus == 5) { $navStatusTxt = "Moored"; }
elsif ($navStatus == 6) { $navStatusTxt = "Aground"; }
elsif ($navStatus == 7) { $navStatusTxt = "Fishing"; }
elsif ($navStatus == 8) { $navStatusTxt = "Under_way:sailing"; }
#elsif ($navStatus == 9) { $navStatusTxt = "Reserved"; }
#elsif ($navStatus == 10) { $navStatusTxt = "Reserved"; }
#elsif ($navStatus == 11) { $navStatusTxt = "Reserved"; }
#elsif ($navStatus == 12) { $navStatusTxt = "Reserved"; }
#elsif ($navStatus == 13) { $navStatusTxt = "Reserved"; }
elsif ($navStatus == 14) { $navStatusTxt = "AIS-SART_is_active"; }
#elsif ($navStatus == 15) { $navStatusTxt = "Not defined"; }
# NOTE: 0-359 degrees
# NOTE: 511 = N/A
# APRS spec says can set to "000", "...", or " " if unknown
my $courseOverGnd = &bin2dec($bCourseOverGnd);
my $course = "";
if ($courseOverGnd == 511) {
$course = "...";
}
elsif ($courseOverGnd == 0) {
$course = "360";
}
else {
$course = sprintf("%03d", $courseOverGnd);
}
if ($print_27) { print " Course: $courseOverGnd\n"; }
# NOTE: 0-62 knots
# NOTE: 63 = N/A
# APRS spec says can set to "000", "...", or " " if unknown
my $speedOverGnd = &bin2dec($bSpeedOverGnd);
my $speed = "";
if ($speedOverGnd == 63) {
$speed = "...";
}
else {
$speed = sprintf("%03d", $speedOverGnd);
}
if ($print_27) { print " Speed: $speedOverGnd\n"; }
# NOTE: -90 to +90
# NOTE: 91 = N/A = D548h or 54600 decimal. Divide by 91 to get 600 (our factor).
my $latitude = &signedBin2dec($bLatitude) / 600.0;
if ($latitude == 91) {
if ($print_27) { print "\n"; }
return();
}
if ($print_27) {
#printf(" bLatitude: %s\n", $bLatitude);
printf(" Latitude: %07.5f\n", $latitude);
}
my $NS;
if ($latitude >= 0.0) {
$NS = 'N';
} else {
$NS = 'S';
$latitude = abs($latitude);
}
my $latdeg = int($latitude);
my $latmins = $latitude - $latdeg;
my $latmins2 = $latmins * 60.0;
my $lat = sprintf("%02d%05.2f%s", $latdeg, $latmins2, $NS);
# NOTE: -180 to +180
# NOTE: 181 = N/A = 1A838h or 108600 decimal. Divide by 181 to get 600 (our factor).
my $longitude = &signedBin2dec($bLongitude) / 600.0;
if ($longitude == 181) {
if ($print_27) { print "\n"; }
return();
}
if ($print_27) {
#printf("bLongitude: %s\n", $bLongitude);
printf("Longitude: %08.5f\n", $longitude);
}
my $EW;
if ($longitude >= 0.0) {
$EW = 'E';
} else {
$EW = 'W';
$longitude = abs($longitude);
}
my $londeg = int($longitude);
my $lonmins = $longitude - $londeg;
my $lonmins2 = $lonmins * 60.0;
my $lon = sprintf("%03d%05.2f%s", $londeg, $lonmins2, $EW);
my $symbol = "s";
my $vesselTag = " ($country)";
if ( defined($vessel_hash{$userID}) ) { $vesselTag = " " . $vessel_hash{$userID} . " (" . $country . ")"; }
my $aprs = &escape_from_shell("$xastir_user>APRS:)$userID!$lat/$lon$symbol$course/$speed $navStatusTxt$vesselTag");
if ($print_27) { print " APRS: $aprs\n"; }
&log_aprs($aprs);
if ($enable_tx) {
my $result = `$udp_client $xastir_host $xastir_port $xastir_user $xastir_pass \"$aprs\"`;
if ($result =~ m/NACK/) {
die "Received NACK from Xastir: Callsign/Passcode don't match?\n";
}
}
# Assign tactical call = "$userID + $country" or "$vesselName + $country"
# Max tactical call in Xastir is 57 chars (56 + terminator?)
#
my $temp;
if ( defined( $vessel_hash{$userID}) ) {
$temp = substr($vessel_hash{$userID} . " (" . $country . ")", 0, 56); # Chop at 56 chars
}
else {
# No vessel name
# N7IPB: Display country of registry
$temp = substr("($country)", 0, 56); # Chop at 56 chars
# WE7U: Display the 9-digit MMSI + country of registry
#$temp = substr($userID . " (" . $country . ")", 0, 56); # Chop at 56 chars
}
if ( !defined($tactical_hash{$userID}) ) {
$tactical_hash{$userID} = $temp;
$aprs = &escape_from_shell($xastir_user . '>' . "APRS::TACTICAL :" . $userID . "=" . $temp);
if ($print_27) { print " APRS: $aprs\n"; }
&log_aprs($aprs);
if ($enable_tx) {
$result = `$udp_client $xastir_host $xastir_port $xastir_user $xastir_pass \"$aprs\"`;
if ($result =~ m/NACK/) {
die "Received NACK from Xastir: Callsign/Passcode don't match?\n";
}
}
}
if ($print_27) { print "\n"; }
return();
}
# Convert binary string $bShipType into text string $shipType
#
sub decodeAidType() {
my $bAidType = shift;
my $aidType = sprintf("%02d", &bin2dec($bAidType));
my $aidTypeTxt = "$aidType:";
# Special Craft
if ($aidType == 1) { $aidTypeTxt = $aidTypeTxt . "Reference point"; }
elsif ($aidType == 2) { $aidTypeTxt = $aidTypeTxt . "RACON"; }
elsif ($aidType == 3) { $aidTypeTxt = $aidTypeTxt . "Fixed structure off shore"; }
elsif ($aidType == 4) { $aidTypeTxt = $aidTypeTxt . "Reserved"; }
elsif ($aidType == 5) { $aidTypeTxt = $aidTypeTxt . "Light, without sectors"; }
elsif ($aidType == 6) { $aidTypeTxt = $aidTypeTxt . "Light, with sectors"; }
elsif ($aidType == 7) { $aidTypeTxt = $aidTypeTxt . "Leading Light Front"; }
elsif ($aidType == 8) { $aidTypeTxt = $aidTypeTxt . "Leading Light Rear"; }
elsif ($aidType == 9) { $aidTypeTxt = $aidTypeTxt . "Beacon, Cardinal N"; }
elsif ($aidType == 10) { $aidTypeTxt = $aidTypeTxt . "Beacon, Cardinal E"; }
elsif ($aidType == 11) { $aidTypeTxt = $aidTypeTxt . "Beacon, Cardinal S"; }
elsif ($aidType == 12) { $aidTypeTxt = $aidTypeTxt . "Beacon, Cardinal W"; }
elsif ($aidType == 13) { $aidTypeTxt = $aidTypeTxt . "Beacon, Port hand"; }
elsif ($aidType == 14) { $aidTypeTxt = $aidTypeTxt . "Beacon, Starboard hand"; }
elsif ($aidType == 15) { $aidTypeTxt = $aidTypeTxt . "Beacon, Preferred Channel port hand"; }
elsif ($aidType == 16) { $aidTypeTxt = $aidTypeTxt . "Beacon, Preferred Channel starboard hand"; }
elsif ($aidType == 17) { $aidTypeTxt = $aidTypeTxt . "Beacon, Isolated danger"; }
elsif ($aidType == 18) { $aidTypeTxt = $aidTypeTxt . "Beacon, Safe water"; }
elsif ($aidType == 19) { $aidTypeTxt = $aidTypeTxt . "Beacon, Special mark"; }
elsif ($aidType == 20) { $aidTypeTxt = $aidTypeTxt . "Cardinal Mark N"; }
elsif ($aidType == 21) { $aidTypeTxt = $aidTypeTxt . "Cardinal Mark E"; }
elsif ($aidType == 22) { $aidTypeTxt = $aidTypeTxt . "Cardinal Mark S"; }
elsif ($aidType == 23) { $aidTypeTxt = $aidTypeTxt . "Cardinal Mark W"; }
elsif ($aidType == 24) { $aidTypeTxt = $aidTypeTxt . "Port hand Mark"; }
elsif ($aidType == 25) { $aidTypeTxt = $aidTypeTxt . "Starboard hand Mark"; }
elsif ($aidType == 26) { $aidTypeTxt = $aidTypeTxt . "Preferred Channel Port hand"; }
elsif ($aidType == 27) { $aidTypeTxt = $aidTypeTxt . "Preferred Channel Starboard hand"; }
elsif ($aidType == 28) { $aidTypeTxt = $aidTypeTxt . "Isolated danger"; }
elsif ($aidType == 29) { $aidTypeTxt = $aidTypeTxt . "Safe Water"; }
elsif ($aidType == 30) { $aidTypeTxt = $aidTypeTxt . "Special Mark"; }
elsif ($aidType == 31) { $aidTypeTxt = $aidTypeTxt . "Light Vessel / LANBY / Rigs"; }
return($aidTypeTxt);
}
# Convert binary string $bShipType into text string $shipType
#
sub decodeShipType() {
my $bShipType = shift;
my $shipType = sprintf("%02d", &bin2dec($bShipType));
my $shipTypeTxt = "$shipType:";
# Special Craft
if ($shipType == 50) { $shipTypeTxt = $shipTypeTxt . "Pilot_Vessel"; }
elsif ($shipType == 51) { $shipTypeTxt = $shipTypeTxt . "Search_and_Rescue"; }
elsif ($shipType == 52) { $shipTypeTxt = $shipTypeTxt . "Harbor_Tug"; }
elsif ($shipType == 53) { $shipTypeTxt = $shipTypeTxt . "Fish,Offshore_or_Port_Tender"; }
elsif ($shipType == 54) { $shipTypeTxt = $shipTypeTxt . "Anti-pollution"; }
elsif ($shipType == 55) { $shipTypeTxt = $shipTypeTxt . "Law_Enforcement"; }
elsif ($shipType == 56) { $shipTypeTxt = $shipTypeTxt . "Local"; }
elsif ($shipType == 57) { $shipTypeTxt = $shipTypeTxt . "Local"; }
elsif ($shipType == 58) { $shipTypeTxt = $shipTypeTxt . "Medical_Transport_or_Public_Safety"; }
elsif ($shipType == 59) { $shipTypeTxt = $shipTypeTxt . "Neutral_State_Vessel"; }
# U.S. Specific Vessels
elsif ($shipType =~ m/^2/) {
if ($shipType =~ m/0$/) { $shipTypeTxt = $shipTypeTxt . "Wing_in_Ground"; }
if ($shipType =~ m/1$/) { $shipTypeTxt = $shipTypeTxt . "Tow-Push"; }
if ($shipType =~ m/2$/) { $shipTypeTxt = $shipTypeTxt . "Tow-Push"; }
if ($shipType =~ m/3$/) { $shipTypeTxt = $shipTypeTxt . "Light_Boat"; }
if ($shipType =~ m/4$/) { $shipTypeTxt = $shipTypeTxt . "Mobile_Offshore_Drilling"; }
if ($shipType =~ m/5$/) { $shipTypeTxt = $shipTypeTxt . "Offshore_Supply_Vessel"; }
if ($shipType =~ m/6$/) { $shipTypeTxt = $shipTypeTxt . "Processing_Vessel"; }
if ($shipType =~ m/7$/) { $shipTypeTxt = $shipTypeTxt . "School/Scientific/Research/Training_Vessel"; }
if ($shipType =~ m/8$/) { $shipTypeTxt = $shipTypeTxt . "U.S._Public_or_Govt"; }
if ($shipType =~ m/9$/) { $shipTypeTxt = $shipTypeTxt . "Autonomous_or_Remotely-Operated"; }
}
# Other Vessels
elsif ($shipType =~ m/^3/) {
if ($shipType =~ m/0$/) { $shipTypeTxt = $shipTypeTxt . "Fishing"; }
if ($shipType =~ m/1$/) { $shipTypeTxt = $shipTypeTxt . "Towing-Pull"; }
if ($shipType =~ m/2$/) { $shipTypeTxt = $shipTypeTxt . "Towing-Big"; }
if ($shipType =~ m/3$/) { $shipTypeTxt = $shipTypeTxt . "Dredging_or_Underwater_Ops"; }
if ($shipType =~ m/4$/) { $shipTypeTxt = $shipTypeTxt . "Diving_Ops"; }
if ($shipType =~ m/5$/) { $shipTypeTxt = $shipTypeTxt . "Military_Ops"; }
if ($shipType =~ m/6$/) { $shipTypeTxt = $shipTypeTxt . "Sailing"; }
if ($shipType =~ m/7$/) { $shipTypeTxt = $shipTypeTxt . "Pleasure_Craft"; }
if ($shipType =~ m/8$/) { $shipTypeTxt = $shipTypeTxt . "Other:Reserved"; }
if ($shipType =~ m/9$/) { $shipTypeTxt = $shipTypeTxt . "Other:Reserved"; }
}
elsif ($shipType =~ m/^0/) { $shipTypeTxt = $shipTypeTxt . "Not-Available"; }
elsif ($shipType =~ m/^1/) { $shipTypeTxt = $shipTypeTxt . "Reserved"; }
elsif ($shipType =~ m/^4/) { $shipTypeTxt = $shipTypeTxt . "Passenger_or_High_Speed"; }
elsif ($shipType =~ m/^6/) { $shipTypeTxt = $shipTypeTxt . "Passenger-Big"; }
elsif ($shipType =~ m/^7/) { $shipTypeTxt = $shipTypeTxt . "Cargo"; }
elsif ($shipType =~ m/^8/) { $shipTypeTxt = $shipTypeTxt . "Tanker"; }
elsif ($shipType =~ m/^9/) { $shipTypeTxt = $shipTypeTxt . "Other"; }
# Decode 2nd digit for 1/4/6/7/8/9 types:
if ( ($shipType =~ m/^1/)
|| ($shipType =~ m/^4/)
|| ($shipType =~ m/^6/)
|| ($shipType =~ m/^7/)
|| ($shipType =~ m/^8/)
|| ($shipType =~ m/^9/) ) {
if ($shipType =~ m/0$/) { $shipTypeTxt = $shipTypeTxt . ""; }
if ($shipType =~ m/1$/) { $shipTypeTxt = $shipTypeTxt . "-Haz_Cat:A/X"; }
if ($shipType =~ m/2$/) { $shipTypeTxt = $shipTypeTxt . "-Haz_Cat:B/Y"; }
if ($shipType =~ m/3$/) { $shipTypeTxt = $shipTypeTxt . "-Haz_Cat:C/Z"; }
if ($shipType =~ m/4$/) { $shipTypeTxt = $shipTypeTxt . "-Haz_Cat:D/O"; }
if ($shipType =~ m/5$/) { $shipTypeTxt = $shipTypeTxt . "-Reserved"; }
if ($shipType =~ m/6$/) { $shipTypeTxt = $shipTypeTxt . "-Reserved"; }
if ($shipType =~ m/7$/) { $shipTypeTxt = $shipTypeTxt . "-Reserved"; }
if ($shipType =~ m/8$/) { $shipTypeTxt = $shipTypeTxt . "-Reserved"; }
if ($shipType =~ m/9$/) { $shipTypeTxt = $shipTypeTxt . ""; }
}
return($shipTypeTxt);
}
# Convert a 6-digit binary string to ASCII text
# Encoded AIS data: Each 6 bits represents one ASCII character.
#
# Each six-bit nibble maps to an ASCII character.
# Decimal 0-31 map to "@" ( ASCII 64) through "\_" (ASCII 95)
# Decimal 32-63 map to " " (ASCII 32) though "?" (ASCII 63)
# Lowercase, backtick, right/left braces, pipe, tilde and DEL can't be encoded
sub bin2text() {
my $input = shift;
my $final_string = "";
my $len = length($input);
my $i;
for ($i = 0; $i < $len; $i+=6) {
my $binary_char = substr($input, $i, 6);
# Convert from binary to decimal (ord)
my $d = unpack("N", pack("B32", substr("00000000000000000000000000000000" . $binary_char, -32)));
if ($d > 0) { # Skip NULL characters
if ($d < 32) { $d += 64; }
$final_string = $final_string . chr($d);
}
}
$final_string =~ s/\s\s/ /g;
return($final_string);
}
## Convert a signed binary string of ASCII 0's and 1's to a decimal
#sub signedBin2dec {
# my $input = shift;
#
# if ( substr($input, 0, 1) eq "1") { # Negative number
# # Pad to 32 bits with 1's
## return unpack("i", pack("B32", substr("11111111111111111111111111111111" . $input, -32)));
# # Invert all the bits. Add 1. Convert to decimal. Negate.
# my $ii;
# my $input2 = "";
# my $len = length($input);
# for ($ii = 0; $ii < $len; $ii++) {
# my $j = substr($input, $ii, 1);
# if ($j eq "0") { $j = "1"; }
# else { $j = "0"; }
# $input2 = $input2 . $j;
# }
##print "$input\n";
##print "$input2\n";
# my $binary = pack("B32", substr("00000000000000000000000000000000" . $input2, -32, 32));
# my $decimal = unpack("N", $binary);
# $decimal++;
# $decimal = -$decimal;
# return $decimal;
# }
# else { # Positive number
# # Pad to 32 bits with 0's
# return unpack("N", pack("B32", substr("00000000000000000000000000000000" . $input, -32))); #####
#
# }
#}
# Convert a signed binary string of ASCII 0's and 1's to a decimal the hard way.
# This subroutine should be platform-agnostic as it does all the work internally.
sub signedBin2dec {
my $input = shift;
my $input2 = "";
my $decimal;
my $multiplier;
#print "\n$input\n";
if ( substr($input, 0, 1) eq "1") { # Negative number
# Invert all the bits. Add 1. Convert to decimal. Negate.
my $ii;
my $len = length($input);
for ($ii = 0; $ii < $len; $ii++) {
my $j = substr($input, $ii, 1);
if ($j eq "0") { $j = "1"; }
else { $j = "0"; }
$input2 = $input2 . $j;
}
$decimal = &toDecimal($input2);
$decimal++;
$decimal = -$decimal;
return $decimal;
}
else { # Positive number
return &toDecimal($input);
}
}
# Convert to decimal from positive binary string.
# Used by above signedBin2dec subroutine.
sub toDecimal {
my $input = shift;
my $multiplier = 1;
my $decimal = 0;
my $len = length($input);
# Start at LSB bit, work toward MSB bit
for (my $ii = $len-1; $ii >= 0; $ii--) {
if (substr($input, $ii, 1) == 1) {
$decimal = $decimal + $multiplier;
#print "1\tMult: $multiplier\n";
}
else {
#print "0\n";
}
$multiplier = $multiplier * 2 ;
}
return $decimal;
}
# Convert an unsigned binary string of ASCII 0's and 1's to a decimal
sub bin2dec {
my $temp = shift;
if (!defined($temp) || $temp eq "") { return; }
return unpack("N", pack("B32", substr("00000000000000000000000000000000" . $temp, -32))); #####
}
# Convert a decimal string (ASCII) to a binary string (ASCII)
sub dec2bin {
my $input = "00000000000000000000000000" . shift;
my $str = unpack("B32", pack("N", $input));
my $str2 = substr($str, -6);
return $str2;
}
# Decode MID out of MMSI ($userID) too:
# 8MIDXXXXX Diver's radio (not used in the U.S. in 2013)
# MIDXXXXXX Ship
# 0MIDXXXXX Group of ships; the U.S. Coast Guard, for example, is 03699999
# 00MIDXXXX Coastal stations
# 111MIDXXX SAR (Search and Rescue) aircraft
# 99MIDXXXX Aids to Navigation
# 98MIDXXXX Auxiliary craft associated with a parent ship
# 970MIDXXX AIS SART (Search and Rescue Transmitter)
# 972XXXXXX MOB (Man Overboard) device
# 974XXXXXX EPIRB (Emergency Position Indicating Radio Beacon) AIS
#
# NOTE: U.S. ships sometimes incorrectly send "669" for those first 3 digits.
# http://www.itu.int/online/mms/glad/cga_mids.sh?lng=E
#
sub decode_MID {
my $userID = shift;
my $MID = $userID;
if ($userID =~ m/^8.*/) { # Diver's radio
$MID =~ s/^8(...).*/$1/;
}
elsif ($userID =~ m/^00.*/) { # Coastal station
$MID =~ s/^00(...).*/$1/;
}
elsif ($userID =~ m/^0.*/) { # Group of ships
$MID =~ s/^0(...).*/$1/;
}
elsif ($userID =~ m/^111.*/) { # SAR aircraft
$MID =~ s/^111(...).*/$1/;
}
elsif ($userID =~ m/^99.*/) { # Navigation aid
$MID =~ s/^99(...).*/$1/;
}
elsif ($userID =~ m/^98.*/) { # Auxiliary craft
$MID =~ s/^98(...).*/$1/;
}
elsif ($userID =~ m/^970.*/) { # AIS SART
$MID =~ s/^970(...).*/$1/;
}
elsif ($userID =~ m/^972.*/) { # Man overboard device
$MID = "MOB";
}
elsif ($userID =~ m/^974.*/) { # EPIRB
$MID = "EPIRB";
}
else { # Ship
$MID =~ s/^(...).*/$1/;
}
my $country = "";
if (defined($countries{$MID})) {
$country = $countries{$MID};
}
else {
$country = "Unknown";
}
return $country;
}
# Escape characters from the shell
#
# Between double quotes the literal value of all characters is preserved except
# for dollar sign, backticks (backward single quotes, ``) and backslash.
#
# The backslash retains its meaning only when followed by dollar, backtick, double quote,
# backslash or newline. Within double quotes, the backslashes are removed from the input
# stream when followed by one of these characters. Backslashes preceding characters that
# don't have a special meaning are left unmodified for processing by the shell interpreter.
#
sub escape_from_shell {
$temp = shift;
$temp =~ s/\\/\\/g; # Look for \ and escape it. Do this one FIRST!
$temp =~ s/"/\\"/g; # Look for " and escape it
$temp =~ s/\$/\\\$/g; # Look for $ and escape it
$temp =~ s/\`/\\`/g; # Look for ` and escape it
return ($temp);
}
# Log messages to log file if logging enabled
#
sub log_aprs {
$sentence = shift;
if ($logging_mode) {
`echo \"$sentence\" >> $log_file`;
}
}