A collection of drivers and utilities for easily working with NM3 underwater acoustic modems. The modem has also colloquially been known as a 'Nanomodem'. Intended to get researchers/engineers up and running quickly so you can spend more effort on your application/science.
The NM3 underwater acoustic modem was developed by the SEA Lab team at Newcastle University, UK.
- Project Homepage: https://github.com/bensherlock/nm3-python-driver/
- USMART Homepage: https://research.ncl.ac.uk/usmart/
- Article "Ultra-Low-Cost and Ultra-Low-Power, Miniature Acoustic Modems Using Multipath Tolerant Spread-Spectrum Techniques": https://doi.org/10.3390/electronics11091446
The NM3 technology has now been incorporated into a number of products including the Succorfish DELPHIS modem: https://succorfish.com/products/delphis/
- NM3 Driver
- NM3 Examples
- NM3 Logger
- NM3 Logs Reader
- NM3 Standalone Virtual Modem
- NM3 Network Simulator with Virtual Modems
The nm3driver.py provides an interface to control the modem and to receive incoming messages without having to write your own parser.
The nm3example.py provides standalone functions showing how to use many of the modem's features.
The nm3logger.py lets you simply store all incoming message packets in a csv file, which can then be read back in with the nm3readlogfile.py for analysis of your data in Python.
The nm3networksimulator.py is a full virtual sea of virtual modems, each of which can be connected to exernal sensor node hardware via the serial port, or a human terminal interface, or else your own Python code for running network protocols. The Controller and virtual modems can be running on a single machine, or across the network for distributed development and testing of networks prior to deployment with physical modems.
MIT license, (C) 2019 Benjamin Sherlock benjamin.sherlock@ncl.ac.uk
This driver provides a Python interface to the NM3 Underwater Acoustic Modem via a serial port (pySerial).
Please Note: This is currently a work-in-progress, highly fluid, and the API/behaviour may change without warning. Although version tagging of the first useable alpha is expected quite soon.
The sourcefiles contain docstrings that can be viewed using pydocs for information about functions and classes. Further information about usage will be added to this README in due course.
Download the nm3driver.py and include as part of your project. Then import the modules as below:
from nm3driver import Nm3
from nm3driver import MessagePacket
If connecting to a physical NM3 modem via the serial port, using pyserial create a serial port instance based on 9600 baud, no parity, 8 databits, one stop bit, and read timeout of say 0.1s. Then use this to construct the NM3 instance.
from nm3driver import Nm3
from nm3driver import MessagePacket
import serial
serial_port = serial.Serial('/dev/ttyS4', 9600, 8, serial.PARITY_NONE, serial.STOPBITS_ONE, 0.1)
nm3_modem = Nm3(input_stream=serial_port, output_stream=serial_port)
# Receiving unicast and broadcast messages
while True:
# Periodically poll the serial port for bytes
nm3_modem.poll_receiver()
# Periodically process any bytes received
nm3_modem.process_incoming_buffer()
# Periodically check for received packets
while nm3_modem.has_received_packet():
message_packet = nm3_modem.get_received_packet()
payload_as_string = bytes(message_packet.packet_payload).decode('utf-8')
print('Received a message packet: ' +
MessagePacket.PACKETTYPE_NAMES[message_packet.packet_type] +
' src: ' + str(message_packet.source_address) + ' data: ' +
payload_as_string + ' timestamp_count: '
+ str(message_packet.packet_timestamp_count))
Example usage is shown in nm3example.py for modem commands for range pinging, getting and setting address, reading local battery level, and message transmission and receipt - broadcast, unicast, and unicast with ack. As well as the newer features such as Noise and Spectrum Measurements, and Channel Impulse Responses. The new System Timer module is also demonstrated for timestamping packet arrivals and timed transmission of packets.
The Nm3Logger will listen for incoming MessagePackets and write these to a csv file. The csv filename is based on the filename_root and the datetime it was created. If the logger is left running it will automatically create a fresh file at midnight.
python3 nm3logger.py /dev/ttyS1 Nm3Log
from nm3driver import Nm3
from nm3driver import MessagePacket
import serial
with serial.Serial(port, 9600, 8, serial.PARITY_NONE, serial.STOPBITS_ONE, 0.1) as serial_port:
nm3_modem = Nm3(input_stream=serial_port, output_stream=serial_port)
nm3_logger = Nm3Logger()
nm3_logger.start_logging(nm3_modem=nm3_modem, filename_root=filenameroot)
import nm3readlogfile
from nm3readlogfile import NM3LogFileEntry
entries = nm3readlogfile.read_nm3_logfile(filename)
for e in entries:
# Process a NM3LogFileEntry
print(e.PacketType)
Acts as a virtual modem without sending any acoustic packets.
Virtual NM3 modems (Nm3VirtualModem) in a virtual sea simulator (Nm3SimulatorController). The Controller and virtual modems can be run on a single machine or on multiple machines across the network/internet.
From the command line a number of use cases are already provided for:
The Controller
python3 nm3networksimulator.py --mode controller --network_address 127.0.0.1 --network_port 8080
The virtual modems can be connected to via the serial port for testing sensor hardware as if the hardware were connected to a real NM3 modem.
python3 nm3networksimulator.py --mode serial --network_address 127.0.0.1 --network_port 8080 \
--serial_port /dev/ttyS1 --position 0.0,0.0,10.0 --address 7
Virtual modems can be run as human usable terminal in the console window and respond to the typed commands as per the real NM3 hardware.
python3 nm3networksimulator.py --mode terminal --network_address 127.0.0.1 --network_port 8080 \
--position 0.0,0.0,10.0 --address 7
Virtual modems can be run as headless transponders.
python3 nm3networksimulator.py --mode headless --network_address 127.0.0.1 --network_port 8080 \
--position 0.0,0.0,10.0 --address 7
Virtual modems can act as Nm3Loggers.
python3 nm3networksimulator.py --mode logger --network_address 127.0.0.1 --network_port 8080 \
--filename_root Nm3Logs --position 0.0,0.0,10.0 --address 7
Virtual modems can be incorporated into user code with user defined network stacks interacting across the virtual sea. Virtual modems can have their position in the virtual sea modified at run time.
from nm3driver import Nm3
from nm3driver import MessagePacket
from nm3networksimulator import Nm3VirtualModem
from queue import Queue
import serial
import time
from threading import Thread
class BufferedIOQueueWrapper:
"""Wraps a Queue as IO with Read and Write binary functions."""
def __init__(self):
self._the_queue = Queue()
def readable(self):
return True
def read(self, n = -1):
"""Read up to n bytes"""
the_bytes = []
while not self._the_queue.empty() and (n == -1 or len(the_bytes) < n):
the_bytes.append(self._the_queue.get())
return bytes(the_bytes)
def writable(self):
return True
def write(self, the_bytes: bytes):
"""Returns the number of bytes written."""
for b in the_bytes:
self._the_queue.put(b)
return len(the_bytes)
def flush(self):
"""Nothing to flush to."""
pass
def main():
# Pipes
outgoing_stream = BufferedIOQueueWrapper()
incoming_stream = BufferedIOQueueWrapper()
nm3_driver = Nm3(input_stream=incoming_stream, output_stream=outgoing_stream)
# input_stream, output_stream, network_address=None, network_port=None, local_address=255, position_xy=(0.0,0.0), depth=10.0):
nm3_modem = Nm3VirtualModem(input_stream=outgoing_stream,
output_stream=incoming_stream,
network_address=network_address,
network_port=network_port,
local_address=address,
position_xy=position_xy,
depth=depth)
a_thread = Thread(target=nm3_modem.run)
a_thread.start() #nm3_modem.run()
# Now loop
beacon_time = time.time()
while True:
if beacon_time < time.time():
timestamp_str = "%d-%02d-%02d %02d:%02d:%02d" % time.localtime()[:6]
message_string = "Beacon Message: " + timestamp_str
message_bytes = message_string.encode('utf-8')
nm3_driver.send_broadcast_message(message_bytes)
beacon_time = beacon_time + 60.0 # Every 60 seconds
nm3_driver.poll_receiver()
nm3_driver.process_incoming_buffer()
while nm3_driver.has_received_packet():
packet = nm3_driver.get_received_packet()
print("Packet received:" + str(packet.json()))
At the moment the Controller handles AcousticPacket propagation in a very overly simple way.
The basic approach in Nm3PropagationModelBase
assumes no losses with isovelocity, no obstructions, no multipath, and no noise.
It simply calculates the expected arrival time based on a nominal speed of sound (1500 m/s) and
the straight line distance between the transmitting node and the receiving node.
A more detailed approach in Nm3PERPropagationModel
makes a better approximation based on simulated packet error
rates for a given received SNR. A received SNR is estimated based on range, bandwidth, and simple loss calculations.
This still makes assumption of an open sea and no obstructions. Note: This is a very simplistic model (overlooking
complexities of propagation, refraction and non-gaussian noise sources) and only provides indicative performance to
support network protocol development.
If you require more in-depth channel modelling then you are free to extend the Nm3PropagationModelBase
class and
override the calculate_propagation
function accordingly. Assign an instance of your model to the
Nm3SimulatorController.nm3_propagation_model
property.