Skip to content
10 changes: 7 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,10 @@ Here's an example, a simplified version of the `aist` tool, which prints
one line per complete AIS message:


for sentence in sentences_from_sources(sys.argv[1:):
import sys
from simpleais import *

for sentence in sentences_from_source(sys.argv[1]):
result = []
if sentence.time:
result.append(sentence.time.strftime(TIME_FORMAT))
Expand All @@ -45,7 +48,8 @@ one line per complete AIS message:

print(" ".join(result))

The `sentence_from_sources()` function will pull from a wide variety of sources

The `sentence_from_source()` function will pull from a wide variety of sources
(local files, serial ports, HTTP URLs), yielding only complete sentences as they
arrive. Each sentence has a wide variety of readable information. Documented
fields can all be referred to by name. For example, `sentence['mmsi']` or
Expand Down Expand Up @@ -95,7 +99,7 @@ use aisgrep to get the relevant packets and aisinfo to plot the map:

## Sources

My main source for protocol information is here: http://catb.org/gpsd/AIVDM.html
My main source for protocol information is here: https://gpsd.gitlab.io/gpsd/AIVDM.html

More protocol info is here: http://www.itu.int/dms_pubrec/itu-r/rec/m/R-REC-M.1371-5-201402-I!!PDF-E.pdf

Expand Down
77 changes: 76 additions & 1 deletion simpleais/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,9 @@

aivdm_pattern = re.compile(r'([.0-9]+)?\s*(![A-Z]{5},\d,\d,.?,[AB12]?,[^,]+,[0-6]\*[0-9A-F]{2})')

# Timeout in seconds for serial, TCP, and UDP.
# Allows users to stop a Python script with CTRL-C.
source_timeout = 10

class Bits:
"""
Expand Down Expand Up @@ -882,6 +885,10 @@ def lines_from_source(source):
yield from _handle_serial_source(source)
elif re.match("https?://.*", source):
yield from _handle_url_source(source)
elif re.match("^:\\d{1,5}$", source):
yield from _handle_udp_source(source)
elif re.match(".*:\\d{1,5}$", source):
yield from _handle_tcp_client_source(source)
else:
# assume it's a file
yield from _handle_file_source(source)
Expand Down Expand Up @@ -920,7 +927,7 @@ def _handle_serial_source(source):
while True:
# noinspection PyBroadException
try:
with serial.Serial(source, 38400, timeout=10) as f:
with serial.Serial(source, 38400, timeout=source_timeout) as f:
while True:
raw_line = f.readline()
try:
Expand Down Expand Up @@ -955,3 +962,71 @@ def _handle_file_source(source):
with source_reader as f:
for line in f:
yield line


def _handle_udp_source(source):
import socket
import select

ip, port = source.split(':')
if ip.endswith('.255'):
# use default IP for receiving UDP broadcast messages
ip = ''
port = int(port)
line_buffer = ""

while True:
# noinspection PyBroadException
try:
with socket.socket(socket.AF_INET, socket.SOCK_DGRAM) as s:
s.settimeout(source_timeout)
s.bind((ip, port))
while True:
try:
line_buffer += s.recv(4096).decode('ascii')
lines = line_buffer.splitlines(True)
line_buffer = ""
for l in lines:
if l.find('\n') != -1:
yield l
else:
line_buffer += l
except socket.timeout:
# timeout gives the user a chance to CTRL-C even without AIS traffic
pass
except Exception:
logging.getLogger().error("unexpected failure in source {}".format(source), exc_info=True)
time.sleep(1)


def _handle_tcp_client_source(source):
import socket
import select

ip, port = source.split(':')
port = int(port)
line_buffer = ""

while True:
# noinspection PyBroadException
try:
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
s.settimeout(source_timeout)
s.connect((ip, port))
while True:
try:
line_buffer += s.recv(4096).decode('ascii')
lines = line_buffer.splitlines(True)
line_buffer = ""
for l in lines:
if l.find('\n') != -1:
yield l
else:
line_buffer += l
except socket.timeout:
# timeout gives the user a chance to CTRL-C even without AIS traffic
pass
except Exception:
print("error")
logging.getLogger().error("unexpected failure in source {}".format(source), exc_info=True)
time.sleep(1)
2 changes: 1 addition & 1 deletion tests/test_source_handling.py
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@ def test_io_source_by_sentence(self):
self.assertRaises(StopIteration, sentences.__next__)
logs.check(('root', 'WARNING', 'skipped: "garbage data"'))

# TODO: figure out how to test serial and url sources effectively
# TODO: figure out how to test serial, url, and udp sources effectively

def write_sample_data(self, file, compress=False):
if compress:
Expand Down