diff --git a/programs/umnp-rhtp.py b/programs/umnp-rhtp.py index ee04d853faf924642d02d829e16d6c4b9ee083d2..4d40c7144d69154573ead2bad1bffa862b697399 100644 --- a/programs/umnp-rhtp.py +++ b/programs/umnp-rhtp.py @@ -17,6 +17,26 @@ if sys.implementation.name == "micropython": else: from umnp.microcontroller.umock import machine +# SPI PARAMETERS +SPI_BAUD = 2_000_000 +SPI_MOSI = machine.Pin(19) +SPI_MISO = machine.Pin(16) +SPI_SCK = machine.Pin(18) # serial clock + +# I2C PARAMETERS +I2C_SCL = machine.Pin(4) # serial clock +I2C_SDA = machine.Pin(5) # serial data + +# ETHERNET PARAMETERS +ETH_CS = machine.Pin(17) # chip select +ETH_RST = machine.Pin(20) # reset + +ETH_IP = "192.168.0.33" # IP of microcontroller +ETH_SUBNET = "255.255.255.0" # ... +ETH_GATEWAY = "192.168.0.1" # ... +ETH_DNS = "192.168.0.2" # technically not necessary +ETH_USE_DHCP = False + async def aggregate_and_send( device: MeasurementDevice, sht45: SHT45, p_sensor: LPS28DFW @@ -25,6 +45,7 @@ async def aggregate_and_send( t, rh = await sht45.measure() p, p_t = await p_sensor.measure() data = f"{t},{rh},{p},{p_t}" + print(f"Sending: '{data}'") msg = DataMessage(data, device.identifier_raw, device.device_type) await comm.send_message(msg) @@ -32,25 +53,13 @@ async def aggregate_and_send( async def main(): # configure network device = MeasurementDevice(device_type=DEVICE_TYPE_RHTP) - spi = machine.SPI( - 0, 2_000_000, mosi=machine.Pin(19), miso=machine.Pin(16), sck=machine.Pin(18) - ) - - i2c = machine.I2C(id=1, scl=machine.Pin(5), sda=machine.Pin(4)) - ether = EthernetW5500( - spi, - machine.Pin(17), - machine.Pin(20), - mac=device.generated_mac_raw(), - dhcp=False, - ) - - ip = "192.168.0.33" # IP of computer with receiver software FIXME name - subnet_mask = "255.255.255.0" # ... - gateway = "192.168.0.1" # ... - dns = "192.168.0.2" # technically not necessary - - ether.set_network(ip, subnet_mask, gateway, dns) + spi = machine.SPI(0, SPI_BAUD, mosi=SPI_MOSI, miso=SPI_MISO, sck=SPI_SCK) + i2c = machine.I2C(id=1, scl=I2C_SCL, sda=I2C_SDA) + + dev_mac = device.generated_mac_raw() + ether = EthernetW5500(spi, ETH_CS, ETH_RST, dev_mac, ETH_USE_DHCP) + ether.set_network(ETH_IP, ETH_SUBNET, ETH_GATEWAY, ETH_DNS) + device.add_network_adapter(ether) comm = device.create_communicator() @@ -59,7 +68,7 @@ async def main(): task = PeriodicTask(aggregate_and_send, None, 1000, device, sht45, p_sensor) comm.add_task(task, "aggregate_and_send") - # start + asyncio.run(comm.start()) diff --git a/umnp-daq.py b/umnp-daq.py index 555240866320849683952a0a1d58dfc05d381ab6..56315de86bb61144348c2919aa2d6fc5e744b76d 100644 --- a/umnp-daq.py +++ b/umnp-daq.py @@ -10,6 +10,7 @@ sock.bind(("0.0.0.0", DEFAULT_UMNP_DATA_IN_PORT)) while True: data, addr = sock.recvfrom(2048) + msg = Message.from_bytes(data) if isinstance(msg, DataMessage): - print(msg.payload()) + print(msg.sender_id, msg.payload()) diff --git a/umnp/microcontroller/communication/serial_uart.py b/umnp/microcontroller/communication/serial_uart.py index d943879eee3f3914f8ef7eb7cd33adbfbd0aa161..ba87c620dc4b08c35abc43be7e1ab6f83277c09e 100644 --- a/umnp/microcontroller/communication/serial_uart.py +++ b/umnp/microcontroller/communication/serial_uart.py @@ -2,7 +2,7 @@ import sys from umnp.communication.abstract_serial_connection import AbstractSerialConnection -if sys.implementation == "micropython": +if sys.implementation.name == "micropython": import machine else: from umnp.microcontroller.umock import machine diff --git a/umnp/microcontroller/devices/network/ethernet_w5500.py b/umnp/microcontroller/devices/network/ethernet_w5500.py index 29de738914523c145384913ac677f95b2abeb079..23bddc21438dd06db97f207c4a212a5a91977e5e 100644 --- a/umnp/microcontroller/devices/network/ethernet_w5500.py +++ b/umnp/microcontroller/devices/network/ethernet_w5500.py @@ -16,15 +16,15 @@ class EthernetW5500(EthernetAdapter): def __init__( self, spi: machine.SPI, - pin1: machine.Pin, - pin2: machine.Pin, + pin_cs: machine.Pin, + pin_rst: machine.Pin, mac: bytes, dhcp=False, ): super().__init__() self._spi = spi - self._pin1 = pin1 - self._pin2 = pin2 + self._pin1 = pin_cs + self._pin2 = pin_rst self._nic = network.WIZNET5K(spi, self._pin1, self._pin2) self._nic.active(True) self._nic.config(mac=mac) diff --git a/umnp/microcontroller/measurementdevice.py b/umnp/microcontroller/measurementdevice.py index 45330d15ae4430ee2339cbad36f947c31cd02fe6..9a3aef14fca9fd88b3af06d8267aaacec571028d 100644 --- a/umnp/microcontroller/measurementdevice.py +++ b/umnp/microcontroller/measurementdevice.py @@ -23,6 +23,12 @@ class MeasurementDevice: def __init__(self, device_type: int = DEVICE_TYPE_UNKNOWN): self._boot_time = time.time() self._identifier_raw = machine.unique_id() + if len(self._identifier_raw) > 6: + self._identifier_raw = self._identifier_raw[:6] + elif len(self._identifier_raw) < 6: + self._identifier_raw = ( + b"0" * (6 - len(self._identifier_raw)) + self._identifier_raw + ) self._identifier = binascii.hexlify(self.identifier_raw).decode( MSG_STRING_ENCODING ) @@ -82,9 +88,7 @@ class MeasurementDevice: if not self._communicator: s = self.create_sender(port=data_port) r = self.create_receiver(port=cmd_port) - self._communicator = UDPCommunicator( - receiver=r, sender=s, device_id=self.identifier - ) + self._communicator = UDPCommunicator(receiver=r, sender=s, device=self) return self._communicator @property diff --git a/umnp/microcontroller/tasks/periodictask.py b/umnp/microcontroller/tasks/periodictask.py index bf37767637cb091aba9667a3fca5b6f82dafeeaf..ab0aa949ccbe07af47a0ef3e39907fa583073905 100644 --- a/umnp/microcontroller/tasks/periodictask.py +++ b/umnp/microcontroller/tasks/periodictask.py @@ -26,17 +26,15 @@ class PeriodicTask: delay_seconds = 25 / 1000 while True: - last = time.time() - print("A0", args) - print("A1", *args) + last = time.ticks_ms() result = await func(*args) - print(result) if call_back: await call_back(result) while True: now = time.ticks_ms() - diff = time.ticks_diff(last, now) + diff = time.ticks_diff(now, last) + # print(last, now, diff) if diff > self._every_ms: last = now break diff --git a/umnp/protocol/common/timestamp.py b/umnp/protocol/common/timestamp.py index f9f763546c480c1dec07851edc140a46abc8260e..0240240112bedf0cb0ee294a206d0e59bf3c361f 100644 --- a/umnp/protocol/common/timestamp.py +++ b/umnp/protocol/common/timestamp.py @@ -1,31 +1,19 @@ import sys -import time - -try: - import datetime -except ImportError: - from umnp.protocol.compat.datetime import datetime - - -if sys.implementation == "micropython": - # noinspection PyUnresolvedReferences - import machine +from umnp.protocol.umnp_time import seconds_since_epoch -""" - rtc = machine.RTC() - now = rtc.datetime() - year, month, day, hour, minute, second, second_fraction = now -""" +if sys.implementation.name == "micropython": + pass +else: + import datetime def ts_utc_now_second() -> int: - if sys.implementation == "micropython": - rtc = machine.RTC() + if sys.implementation.name == "micropython": + # rtc = machine.RTC() # https://docs.micropython.org/en/latest/library/time.html#module-time # Micropython's time.time() returns the number of seconds, as an integer, since the Epoch - return time.time() - + return seconds_since_epoch() else: ts = datetime.datetime.now(tz=datetime.timezone.utc).timestamp() return int(round(ts)) @@ -33,14 +21,20 @@ def ts_utc_now_second() -> int: class TimeStamp: def __init__(self, when=None): - if when: + if when is None: self._ts = ts_utc_now_second() - else: + elif isinstance(when, TimeStamp): + self._ts = when.value + elif isinstance(when, int): self._ts = when + else: + error = f"Could not convert time {when} (type {type(when)} to TimeStamp." + error += "Allowed values are integers > 0, TimeStamp objects or None." + raise ValueError(error) if not isinstance(self._ts, int): raise ValueError("TimeStamp not an integer") - if not self._ts >= 0: + if self._ts < 0: raise ValueError("TimeStamp < 0") @property @@ -53,6 +47,6 @@ def valid_timestamp(when: TimeStamp | None) -> TimeStamp: return TimeStamp() if not isinstance(when, TimeStamp): - raise ValueError("Expected None TimeStamp") + raise ValueError("Expected None or TimeStamp") return when diff --git a/umnp/protocol/compat/datetime.py b/umnp/protocol/compat/datetime.py deleted file mode 100644 index 62fafebd666a6af8c6cea63ab75a62b6fa484a0d..0000000000000000000000000000000000000000 --- a/umnp/protocol/compat/datetime.py +++ /dev/null @@ -1,44 +0,0 @@ -import time - -try: - # noinspection PyUnresolvedReferences - import machine -except ImportError: - import umnp.microcontroller.umock.machine as machine - - -class timezone: - def __init__(self): - pass - - @property - def utc(self): - return None - - -class datetime: - def __init__(self): - self.rtc = machine.RTC() - self._time = int(round(time.time())) - - date = self.rtc.datetime() - self.year, self.month, self.day, self.hour, self.minute, self.second, self.second_fraction = date - - def _update(self): - date = self.rtc.datetime() - self._time = time.time() - self.year, self.month, self.day, self.hour, self.minute, self.second, self.second_fraction = date - - @classmethod - def utcnow(cls): - return datetime() - - def timestamp(self): - return self._time - - @classmethod - def now(cls, *args): - return datetime() - - def timezone(self): - return timezone() diff --git a/umnp/protocol/compat/time.py b/umnp/protocol/compat/time.py deleted file mode 100644 index f6d2c32f3368ab0cf33335bbb148d892b4418201..0000000000000000000000000000000000000000 --- a/umnp/protocol/compat/time.py +++ /dev/null @@ -1,7 +0,0 @@ -try: - import typing -except ImportError: - pass - - - diff --git a/umnp/protocol/constants.py b/umnp/protocol/constants.py index 446b7ea017b67f35c423dbd25583a46f5beac793..633f37bc80a6557db135c4a231b0864eee5e0ebc 100644 --- a/umnp/protocol/constants.py +++ b/umnp/protocol/constants.py @@ -15,12 +15,12 @@ MSG_BYTE_ORDER: typing.Literal["little", "big"] = "big" MSG_LEN_PROTOCOL_VERSION: int = 2 # message type, see unmp.protocol.messagetype -MSG_LEN_TYPE: int = 2 +MSG_LEN_MESSAGE_TYPE: int = 2 # message timestamp (of sending), seconds since epoch for version 0 MSG_LEN_TIMESTAMP: int = 4 -# sender unique id (e.g. serial number) +# sender unique id (e.g. serial number or mac address) MSG_LEN_SENDER_ID: int = 6 # sender type (e.g. model of measurement or control device attached to microcontroller) diff --git a/umnp/protocol/message.py b/umnp/protocol/message.py index 17330f8b8130d0cfe7268be930affccba4f1805a..bd879870c6602d4281a114b0f5bf22032ddb0430 100644 --- a/umnp/protocol/message.py +++ b/umnp/protocol/message.py @@ -65,6 +65,10 @@ class Message: def version(self) -> int: return self._header.version + @property + def sender_id(self) -> str: + return ":".join(f"{byte:02x}" for byte in self._sender_id) + @property def data(self) -> bytes: return self._data @@ -76,9 +80,7 @@ class Message: sender_device_id=self._sender_id, ) payload = self.data - payload_length = len(payload).to_bytes( - length=MSG_LEN_PAYLOAD_SIZE, byteorder=MSG_BYTE_ORDER - ) + payload_length = len(payload).to_bytes(MSG_LEN_PAYLOAD_SIZE, MSG_BYTE_ORDER) return header.encode() + payload_length + payload @classmethod @@ -86,8 +88,17 @@ class Message: header = MessageHeader.decode(data) offset = header.encoded_size_bytes try: - payload_bytes = data[offset : offset + MSG_LEN_PAYLOAD_SIZE] - payload_length = int.from_bytes(payload_bytes, MSG_BYTE_ORDER) + # print("data:", data) + # print("len(data):", len(data)) + # print("offset:", offset) + + payload_len_bytes = data[offset : offset + MSG_LEN_PAYLOAD_SIZE] + payload_length = int.from_bytes(payload_len_bytes, MSG_BYTE_ORDER) + # print(f"{payload_len_bytes=}") + # print(f"{payload_length=}") + payload = data[offset + MSG_LEN_PAYLOAD_SIZE :] + # print(f"expected: {payload=}") + # raise OSError except (KeyError, ValueError): logging.error("Invalid message: could not extract payload length") return None @@ -96,7 +107,7 @@ class Message: payload = data[offset:] if len(payload) != payload_length: logging.error( - "Invalid message: mismatch between specified and actual payload length" + f"Invalid message: mismatch between specified {payload_length} and actual payload length {len(payload)}." ) return None diff --git a/umnp/protocol/message_header.py b/umnp/protocol/message_header.py index 2bed493c2e10f2f0c89214cbfb0c4d7f87f54806..e13a2f6b81879d1a2a96396924c9b2f26d2dab5b 100644 --- a/umnp/protocol/message_header.py +++ b/umnp/protocol/message_header.py @@ -3,7 +3,7 @@ from umnp.protocol.constants import ( MSG_PROTOCOL_VERSION, MSG_LEN_PROTOCOL_VERSION, MSG_BYTE_ORDER, - MSG_LEN_TYPE, + MSG_LEN_MESSAGE_TYPE, MSG_LEN_TIMESTAMP, MSG_LEN_SENDER_ID, MSG_LEN_SENDER_TYPE, @@ -51,7 +51,7 @@ class MessageHeader: @property def message_type_encoded(self) -> bytes: - return self.message_type.to_bytes(MSG_LEN_TYPE, MSG_BYTE_ORDER) + return self.message_type.to_bytes(MSG_LEN_MESSAGE_TYPE, MSG_BYTE_ORDER) @property def version(self) -> int: @@ -77,9 +77,15 @@ class MessageHeader: def timestamp(self) -> TimeStamp: return self._timestamp + @property + def sender_device_id_encoded(self) -> bytes: + if len(self._sender_device_id) != 6: + raise ValueError("Device ID length != 6 bytes") + return self._sender_device_id + def encode(self) -> bytes: version = self.version_encoded - sender_id = self._sender_device_id + sender_id = self.sender_device_id_encoded sender_type = self.sender_dev_type_encoded message_type = self.message_type_encoded timestamp = self.timestamp_encoded @@ -91,22 +97,24 @@ class MessageHeader: MSG_LEN_PROTOCOL_VERSION + MSG_LEN_SENDER_ID + MSG_LEN_SENDER_TYPE - + MSG_LEN_TYPE + + MSG_LEN_MESSAGE_TYPE + MSG_LEN_TIMESTAMP ) @classmethod def decode(cls, data: bytes): offset = 0 - + protocol_bytes = -1 if not (isinstance(data, bytes)): logging.error("Invalid message header: not bytes") return None try: protocol_bytes = data[:MSG_LEN_PROTOCOL_VERSION] protocol_version = int.from_bytes(protocol_bytes, MSG_BYTE_ORDER) + cls._version = protocol_version except (KeyError, ValueError): + logging.error(f"Protocol bytes: {protocol_bytes}") logging.error("Invalid message header: could not extract version") return None offset += MSG_LEN_PROTOCOL_VERSION @@ -122,25 +130,28 @@ class MessageHeader: return None try: - message_type_bytes = data[offset : offset + MSG_LEN_TYPE] + message_type_bytes = data[offset : offset + MSG_LEN_MESSAGE_TYPE] message_type = int.from_bytes(message_type_bytes, MSG_BYTE_ORDER) except (KeyError, ValueError): logging.error("Invalid message payload: could not extract version") return None - offset += MSG_LEN_TYPE + offset += MSG_LEN_MESSAGE_TYPE if protocol_version < 0 or protocol_version > MSG_PROTOCOL_VERSION: err = f"Invalid protocol version {protocol_version}, outside of range [0, {MSG_PROTOCOL_VERSION}]" logging.error(err) return None - + message_ts = None try: message_ts = data[offset : offset + MSG_LEN_TIMESTAMP] - timestamp = TimeStamp(int.from_bytes(message_ts, byteorder=MSG_BYTE_ORDER)) - except (KeyError, ValueError): - logging.error("Invalid message timestamp: could not extract version") + message_ts_int = int.from_bytes(message_ts, byteorder=MSG_BYTE_ORDER) + timestamp = TimeStamp(message_ts_int) + except (KeyError, ValueError) as e: + logging.error( + f"Invalid message timestamp: could not extract timestamp: {e}" + ) return None return cls( diff --git a/umnp/protocol/umnp_time.py b/umnp/protocol/umnp_time.py new file mode 100644 index 0000000000000000000000000000000000000000..47150b668e98eb60727884be7ca6bf85950b2869 --- /dev/null +++ b/umnp/protocol/umnp_time.py @@ -0,0 +1,5 @@ +import time + + +def seconds_since_epoch() -> int: + return int(time.time())