diff --git a/README.md b/README.md index ce185f5..7b0ecc0 100644 --- a/README.md +++ b/README.md @@ -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)) @@ -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 @@ -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 diff --git a/simpleais/__init__.py b/simpleais/__init__.py index d7b5d01..9ad4577 100644 --- a/simpleais/__init__.py +++ b/simpleais/__init__.py @@ -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: """ @@ -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) @@ -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: @@ -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) diff --git a/tests/test_source_handling.py b/tests/test_source_handling.py index 240e88d..bd254e0 100644 --- a/tests/test_source_handling.py +++ b/tests/test_source_handling.py @@ -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: