diff --git a/LICENSE b/LICENSE
new file mode 100644
index 0000000000000000000000000000000000000000..984c347101f288f66c4549f4430b6964d3f5d706
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,9 @@
+This project is licensed under the terms of the GPLv3, unless otherwise stated, especially in the external/ directory.
+
+See the individual files for more information.
+
+
+
+# Apache License 2.0 http://www.apache.org/licenses/LICENSE-2.0
+ * external/rp_devices.py: Copyright (c) 2021 Jeremy P Bentham
+ * programs/adc_test.py  based on https://github.com/jbentham/pico/blob/main/rp_adc_test.py, which is Copyright (c) 2021 Jeremy P Bentham
diff --git a/TODO.md b/TODO.md
new file mode 100644
index 0000000000000000000000000000000000000000..a4b2b4eef67f2b50bff083e793d7fff30fd46577
--- /dev/null
+++ b/TODO.md
@@ -0,0 +1,6 @@
+# To do
+
+## Life-sign support
+
+## Network errors
+- Centralize network restart functionality.
\ No newline at end of file
diff --git a/umnp/protocol/compat/__init__.py b/external/__init__.py
similarity index 100%
rename from umnp/protocol/compat/__init__.py
rename to external/__init__.py
diff --git a/external/rp_devices.py b/external/rp_devices.py
new file mode 100644
index 0000000000000000000000000000000000000000..f292b6d728fe0df748464466953f4db19298e3f3
--- /dev/null
+++ b/external/rp_devices.py
@@ -0,0 +1,177 @@
+# RP2040 uctype definitions for MicroPython
+# See https://iosoft.blog/pico-adc-dma for description
+#
+# Copyright (c) 2021 Jeremy P Bentham
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+from uctypes import BF_POS, BF_LEN, UINT32, BFUINT32, struct
+
+GPIO_BASE = 0x40014000
+GPIO_CHAN_WIDTH = 0x08
+GPIO_PIN_COUNT = 30
+PAD_BASE = 0x4001C000
+PAD_PIN_WIDTH = 0x04
+ADC_BASE = 0x4004C000
+DMA_BASE = 0x50000000
+DMA_CHAN_WIDTH = 0x40
+DMA_CHAN_COUNT = 12
+
+# DMA: RP2040 datasheet 2.5.7
+DMA_CTRL_TRIG_FIELDS = {
+    "AHB_ERROR": 31 << BF_POS | 1 << BF_LEN | BFUINT32,
+    "READ_ERROR": 30 << BF_POS | 1 << BF_LEN | BFUINT32,
+    "WRITE_ERROR": 29 << BF_POS | 1 << BF_LEN | BFUINT32,
+    "BUSY": 24 << BF_POS | 1 << BF_LEN | BFUINT32,
+    "SNIFF_EN": 23 << BF_POS | 1 << BF_LEN | BFUINT32,
+    "BSWAP": 22 << BF_POS | 1 << BF_LEN | BFUINT32,
+    "IRQ_QUIET": 21 << BF_POS | 1 << BF_LEN | BFUINT32,
+    "TREQ_SEL": 15 << BF_POS | 6 << BF_LEN | BFUINT32,
+    "CHAIN_TO": 11 << BF_POS | 4 << BF_LEN | BFUINT32,
+    "RING_SEL": 10 << BF_POS | 1 << BF_LEN | BFUINT32,
+    "RING_SIZE": 6 << BF_POS | 4 << BF_LEN | BFUINT32,
+    "INCR_WRITE": 5 << BF_POS | 1 << BF_LEN | BFUINT32,
+    "INCR_READ": 4 << BF_POS | 1 << BF_LEN | BFUINT32,
+    "DATA_SIZE": 2 << BF_POS | 2 << BF_LEN | BFUINT32,
+    "HIGH_PRIORITY": 1 << BF_POS | 1 << BF_LEN | BFUINT32,
+    "EN": 0 << BF_POS | 1 << BF_LEN | BFUINT32,
+}
+# Channel-specific DMA registers
+DMA_CHAN_REGS = {
+    "READ_ADDR_REG": 0x00 | UINT32,
+    "WRITE_ADDR_REG": 0x04 | UINT32,
+    "TRANS_COUNT_REG": 0x08 | UINT32,
+    "CTRL_TRIG_REG": 0x0C | UINT32,
+    "CTRL_TRIG": (0x0C, DMA_CTRL_TRIG_FIELDS),
+}
+# General DMA registers
+DMA_REGS = {
+    "INTR": 0x400 | UINT32,
+    "INTE0": 0x404 | UINT32,
+    "INTF0": 0x408 | UINT32,
+    "INTS0": 0x40C | UINT32,
+    "INTE1": 0x414 | UINT32,
+    "INTF1": 0x418 | UINT32,
+    "INTS1": 0x41C | UINT32,
+    "TIMER0": 0x420 | UINT32,
+    "TIMER1": 0x424 | UINT32,
+    "TIMER2": 0x428 | UINT32,
+    "TIMER3": 0x42C | UINT32,
+    "MULTI_CHAN_TRIGGER": 0x430 | UINT32,
+    "SNIFF_CTRL": 0x434 | UINT32,
+    "SNIFF_DATA": 0x438 | UINT32,
+    "FIFO_LEVELS": 0x440 | UINT32,
+    "CHAN_ABORT": 0x444 | UINT32,
+}
+
+# GPIO status and control: RP2040 datasheet 2.19.6.1.10
+GPIO_STATUS_FIELDS = {
+    "IRQTOPROC": 26 << BF_POS | 1 << BF_LEN | BFUINT32,
+    "IRQFROMPAD": 24 << BF_POS | 1 << BF_LEN | BFUINT32,
+    "INTOPERI": 19 << BF_POS | 1 << BF_LEN | BFUINT32,
+    "INFROMPAD": 17 << BF_POS | 1 << BF_LEN | BFUINT32,
+    "OETOPAD": 13 << BF_POS | 1 << BF_LEN | BFUINT32,
+    "OEFROMPERI": 12 << BF_POS | 1 << BF_LEN | BFUINT32,
+    "OUTTOPAD": 9 << BF_POS | 1 << BF_LEN | BFUINT32,
+    "OUTFROMPERI": 8 << BF_POS | 1 << BF_LEN | BFUINT32,
+}
+GPIO_CTRL_FIELDS = {
+    "IRQOVER": 28 << BF_POS | 2 << BF_LEN | BFUINT32,
+    "INOVER": 16 << BF_POS | 2 << BF_LEN | BFUINT32,
+    "OEOVER": 12 << BF_POS | 2 << BF_LEN | BFUINT32,
+    "OUTOVER": 8 << BF_POS | 2 << BF_LEN | BFUINT32,
+    "FUNCSEL": 0 << BF_POS | 5 << BF_LEN | BFUINT32,
+}
+GPIO_REGS = {
+    "GPIO_STATUS_REG": 0x00 | UINT32,
+    "GPIO_STATUS": (0x00, GPIO_STATUS_FIELDS),
+    "GPIO_CTRL_REG": 0x04 | UINT32,
+    "GPIO_CTRL": (0x04, GPIO_CTRL_FIELDS),
+}
+
+# PAD control: RP2040 datasheet 2.19.6.3
+PAD_FIELDS = {
+    "OD": 7 << BF_POS | 1 << BF_LEN | BFUINT32,
+    "IE": 6 << BF_POS | 1 << BF_LEN | BFUINT32,
+    "DRIVE": 4 << BF_POS | 2 << BF_LEN | BFUINT32,
+    "PUE": 3 << BF_POS | 1 << BF_LEN | BFUINT32,
+    "PDE": 2 << BF_POS | 1 << BF_LEN | BFUINT32,
+    "SCHMITT": 1 << BF_POS | 1 << BF_LEN | BFUINT32,
+    "SLEWFAST": 0 << BF_POS | 1 << BF_LEN | BFUINT32,
+}
+PAD_REGS = {"PAD_REG": 0x00 | UINT32, "PAD": (0x00, PAD_FIELDS)}
+
+# ADC: RP2040 datasheet 4.9.6
+ADC_CS_FIELDS = {
+    "RROBIN": 16 << BF_POS | 5 << BF_LEN | BFUINT32,
+    "AINSEL": 12 << BF_POS | 3 << BF_LEN | BFUINT32,
+    "ERR_STICKY": 10 << BF_POS | 1 << BF_LEN | BFUINT32,
+    "ERR": 9 << BF_POS | 1 << BF_LEN | BFUINT32,
+    "READY": 8 << BF_POS | 1 << BF_LEN | BFUINT32,
+    "START_MANY": 3 << BF_POS | 1 << BF_LEN | BFUINT32,
+    "START_ONCE": 2 << BF_POS | 1 << BF_LEN | BFUINT32,
+    "TS_EN": 1 << BF_POS | 1 << BF_LEN | BFUINT32,
+    "EN": 0 << BF_POS | 1 << BF_LEN | BFUINT32,
+}
+ADC_FCS_FIELDS = {
+    "THRESH": 24 << BF_POS | 4 << BF_LEN | BFUINT32,
+    "LEVEL": 16 << BF_POS | 4 << BF_LEN | BFUINT32,
+    "OVER": 11 << BF_POS | 1 << BF_LEN | BFUINT32,
+    "UNDER": 10 << BF_POS | 1 << BF_LEN | BFUINT32,
+    "FULL": 9 << BF_POS | 1 << BF_LEN | BFUINT32,
+    "EMPTY": 8 << BF_POS | 1 << BF_LEN | BFUINT32,
+    "DREQ_EN": 3 << BF_POS | 1 << BF_LEN | BFUINT32,
+    "ERR": 2 << BF_POS | 1 << BF_LEN | BFUINT32,
+    "SHIFT": 1 << BF_POS | 1 << BF_LEN | BFUINT32,
+    "EN": 0 << BF_POS | 1 << BF_LEN | BFUINT32,
+}
+ADC_REGS = {
+    "CS_REG": 0x00 | UINT32,
+    "CS": (0x00, ADC_CS_FIELDS),
+    "RESULT_REG": 0x04 | UINT32,
+    "FCS_REG": 0x08 | UINT32,
+    "FCS": (0x08, ADC_FCS_FIELDS),
+    "FIFO_REG": 0x0C | UINT32,
+    "DIV_REG": 0x10 | UINT32,
+    "INTR_REG": 0x14 | UINT32,
+    "INTE_REG": 0x18 | UINT32,
+    "INTF_REG": 0x1C | UINT32,
+    "INTS_REG": 0x20 | UINT32,
+}
+DREQ_PIO0_TX0, DREQ_PIO0_RX0, DREQ_PIO1_TX0 = 0, 4, 8
+DREQ_PIO1_RX0, DREQ_SPI0_TX, DREQ_SPI0_RX = 12, 16, 17
+DREQ_SPI1_TX, DREQ_SPI1_RX, DREQ_UART0_TX = 18, 19, 20
+DREQ_UART0_RX, DREQ_UART1_TX, DREQ_UART1_RX = 21, 22, 23
+DREQ_I2C0_TX, DREQ_I2C0_RX, DREQ_I2C1_TX = 32, 33, 34
+DREQ_I2C1_RX, DREQ_ADC = 35, 36
+
+DMA_CHANS = [
+    struct(DMA_BASE + n * DMA_CHAN_WIDTH, DMA_CHAN_REGS)
+    for n in range(0, DMA_CHAN_COUNT)
+]
+DMA_DEVICE = struct(DMA_BASE, DMA_REGS)
+GPIO_PINS = [
+    struct(GPIO_BASE + n * GPIO_CHAN_WIDTH, GPIO_REGS) for n in range(0, GPIO_PIN_COUNT)
+]
+PAD_PINS = [
+    struct(PAD_BASE + n * PAD_PIN_WIDTH, PAD_REGS) for n in range(0, GPIO_PIN_COUNT)
+]
+ADC_DEVICE = struct(ADC_BASE, ADC_REGS)
+ADC_FIFO_ADDR = ADC_BASE + 0x0C
+
+GPIO_FUNC_SPI, GPIO_FUNC_UART, GPIO_FUNC_I2C = 1, 2, 3
+GPIO_FUNC_PWM, GPIO_FUNC_SIO, GPIO_FUNC_PIO0 = 4, 5, 6
+GPIO_FUNC_NULL = 0x1F
+
+# EOF
diff --git a/programs/adc_test.py b/programs/adc_test.py
new file mode 100644
index 0000000000000000000000000000000000000000..b4f6348c192139bbc786820106644e5f7db283ca
--- /dev/null
+++ b/programs/adc_test.py
@@ -0,0 +1,67 @@
+# based on https://github.com/jbentham/pico/blob/main/rp_adc_test.py
+# which is  Copyright (c) 2021 Jeremy P Bentham and licensed under the Apache License, Version 2.0
+
+import array
+import asyncio
+
+import uctypes
+
+import external.rp_devices as devs
+
+
+class DmaAdc:
+    def __init__(self, pin: int, sample_count: int = 1000, sampling_rate: int = 100000):
+        self._pin_number = pin
+        self._channel_number = pin - 26
+        self._adc = devs.ADC_DEVICE
+        self._pin = devs.GPIO_PINS[pin]
+        self._pad = devs.PAD_PINS[pin]
+        self._pin.GPIO_CTRL_REG = devs.GPIO_FUNC_NULL
+        self._pad.PAD_REG = 0
+        self._buffer = None
+        self._n_samples = sample_count
+        self.set_sample_count(sample_count)
+        self._sampling_rate = sampling_rate
+        self._dma_channel = devs.DMA_CHANS[self._channel_number]
+        self._dma = devs.DMA_DEVICE
+        self._ready = False
+        self._lock = asyncio.Lock()
+
+    def set_sample_count(self, count: int):
+        self._n_samples = count
+        self._buffer = array.array("H", (0 for _ in range(self._n_samples)))
+
+    async def sample(self):
+        with self._lock:
+            self._ready = False
+            self._adc.CS_REG = self._adc.FCS_REG = 0
+            self._adc.CS.EN = 1
+            self._adc.CS.AINSEL = self._channel_number
+            self._adc.FCS.EN = self._adc.FCS.DREQ_EN = 1
+            self._adc.DIV_REG = (48000000 // self._sampling_rate - 1) << 8
+            self._adc.FCS.THRESH = self._adc.FCS.OVER = self._adc.FCS.UNDER = 1
+
+            self._dma_channel.READ_ADDR_REG = devs.ADC_FIFO_ADDR
+            self._dma_channel.WRITE_ADDR_REG = uctypes.addressof(self._buffer)
+            self._dma_channel.TRANS_COUNT_REG = self._n_samples
+
+            self._dma_channel.CTRL_TRIG_REG = 0
+            self._dma_channel.CTRL_TRIG.CHAIN_TO = self._channel_number
+            self._dma_channel.CTRL_TRIG.INCR_WRITE = (
+                self._dma_channel.CTRL_TRIG.IRQ_QUIET
+            ) = 1
+            self._dma_channel.CTRL_TRIG.TREQ_SEL = devs.DREQ_ADC
+            self._dma_channel.CTRL_TRIG.DATA_SIZE = 1
+            self._dma_channel.CTRL_TRIG.EN = 1
+
+            while self._adc.FCS.LEVEL:
+                _ = self._adc.FIFO_REG
+
+            self._adc.CS.START_MANY = 1
+            while self._dma_channel.CTRL_TRIG.BUSY:
+                await asyncio.sleep(10 / 1000)
+
+            self._adc.CS.START_MANY = 0
+            self._dma_channel.CTRL_TRIG.EN = 0
+            self._ready = True
+            return [("%1.3f" % (val * 3.3 / 4096)) for val in self._buffer]
diff --git a/programs/ae33.py b/programs/ae33.py
index 1477b522596451ae4016674696884abc6ac4c83b..936931148410ee1006075e8169673a8407e544f6 100644
--- a/programs/ae33.py
+++ b/programs/ae33.py
@@ -5,7 +5,7 @@ from umnp.microcontroller.devices.network.ethernet_w5500 import EthernetW5500
 from umnp.microcontroller.devices.network.udp import UDPSender, UDPReceiver
 from umnp.microcontroller.measurementdevice import MeasurementDevice
 from umnp.microcontroller.tasks.periodictask import PeriodicTask
-from umnp.protocol.common import UDP_DATA_PORT, UDP_CMD_PORT
+from umnp.proto.common import UDP_DATA_PORT, UDP_CMD_PORT
 
 if sys.implementation.name == "micropython":
     # noinspection PyUnresolvedReferences
diff --git a/programs/base_program.py b/programs/base_program.py
index 47002bcc650d128620a7592a3f7e6709914f7913..96db56c841c1f4c4777c4fe1999791d0a9ae76a5 100644
--- a/programs/base_program.py
+++ b/programs/base_program.py
@@ -5,7 +5,7 @@ from umnp.microcontroller.devices.network.ethernet_w5500 import EthernetW5500
 from umnp.microcontroller.devices.network.udp import UDPSender, UDPReceiver
 from umnp.microcontroller.measurementdevice import MeasurementDevice
 from umnp.microcontroller.tasks.periodictask import PeriodicTask
-from umnp.protocol.common import UDP_DATA_PORT, UDP_CMD_PORT
+from umnp.proto.common import UDP_DATA_PORT, UDP_CMD_PORT
 
 if sys.implementation.name == "micropython":
     # noinspection PyUnresolvedReferences
diff --git a/programs/umnp-rhtp-local.py b/programs/umnp-rhtp-local.py
index 742e8af276eda1dbff7daf777e7cc35195b15a39..b7bd1263ef0c0b28a49f08927c3c4c80862fdee9 100644
--- a/programs/umnp-rhtp-local.py
+++ b/programs/umnp-rhtp-local.py
@@ -6,7 +6,7 @@ from umnp.microcontroller.measurementdevice import MeasurementDevice
 from umnp.microcontroller.sensors.lps28dfw import LPS28DFW
 from umnp.microcontroller.sensors.sht45 import SHT45
 from umnp.microcontroller.tasks.periodictask import PeriodicTask
-from umnp.protocol.data_message import DataMessage
+from umnp.proto.data_message import DataMessage
 
 if sys.implementation.name == "micropython":
     # noinspection PyUnresolvedReferences
@@ -47,54 +47,39 @@ async def aggregate_and_send(
     p, p_t = await p_sensor.measure()
     data = "{:2.2f},{:2.2f},{:2.2f},{:2.2f}".format(t, rh, p, p_t)
     print("Sending: '{}'".format(data))
-    msg = DataMessage(data, device.identifier_raw, device.device_type)
+
     if comm.network_error:
-        print("Network error")
-        spi: machine.SPI = device.spi
-        if spi:
-            print("SPI::deinit()")
-            spi.deinit()
-            spi = machine.SPI(0, SPI_BAUD, mosi=SPI_MOSI, miso=SPI_MISO, sck=SPI_SCK)
-            print("SPI::new()")
-            device.add_spi(spi)
-            print("SPI::added()")
-            dev_mac = device.generated_mac_raw()
-            device.network.deactivate()
-            print("ETH:new()")
-            ether = EthernetW5500(spi, ETH_CS, ETH_RST, dev_mac, ETH_USE_DHCP)
-            print("ETH:set()")
-            ether.set_network(ETH_IP, ETH_SUBNET, ETH_GATEWAY, ETH_DNS)
-            print("ETH:added()")
-            device.add_network_adapter(ether)
-            comm.clear_network_error()
+        await device.reset_network()
+
     if not comm.network_error:
+        msg = DataMessage(data, device.identifier_raw, device.device_type)
         await comm.send_message(msg)
-    msg = None
+        msg = None
+
     gc.collect()
 
 
 async def main():
     # configure network
     device = MeasurementDevice(device_type=DEVICE_TYPE_RHTP)
-    spi = machine.SPI(0, SPI_BAUD, mosi=SPI_MOSI, miso=SPI_MISO, sck=SPI_SCK)
-    device.add_spi(spi)
-
-    i2c = machine.I2C(id=1, scl=I2C_SCL, sda=I2C_SDA, timeout=10000)
-    device.add_i2c(i2c)
+    spi = device.add_spi(0, SPI_BAUD, mosi=SPI_MOSI, miso=SPI_MISO, sck=SPI_SCK)
+    i2c = device.add_i2c(i2c_id=1, scl=I2C_SCL, sda=I2C_SDA, timeout=10000)
 
     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)
-    device.add_i2c(i2c)
+
     comm = device.create_communicator()
 
     sht45 = SHT45(i2c)
     p_sensor = LPS28DFW(i2c, i2c_address=0x5C)
 
-    task = PeriodicTask(aggregate_and_send, None, 1000, device, sht45, p_sensor)
-    comm.add_task(task, "aggregate_and_send")
+    comm.add_task(
+        PeriodicTask(aggregate_and_send, None, 1000, device, sht45, p_sensor),
+        "aggregate_and_send",
+    )
+    comm.add_task(PeriodicTask(device.send_life_sign, None, 5000), "life_sign")
 
     asyncio.run(comm.start())
 
diff --git a/programs/umnp-rhtp.py b/programs/umnp-rhtp.py
index e09c853b7e94d7ecea4a14ad5c6e41b2f8822546..89bf20d75733bfc2ea8462bbc396897a628cfa12 100644
--- a/programs/umnp-rhtp.py
+++ b/programs/umnp-rhtp.py
@@ -6,7 +6,7 @@ from umnp.microcontroller.measurementdevice import MeasurementDevice
 from umnp.microcontroller.sensors.lps28dfw import LPS28DFW
 from umnp.microcontroller.sensors.sht45 import SHT45
 from umnp.microcontroller.tasks.periodictask import PeriodicTask
-from umnp.protocol.data_message import DataMessage
+from umnp.proto.data_message import DataMessage
 
 if sys.implementation.name == "micropython":
     # noinspection PyUnresolvedReferences
@@ -47,45 +47,27 @@ async def aggregate_and_send(
     p, p_t = await p_sensor.measure()
     data = "{:2.2f},{:2.2f},{:2.2f},{:2.2f}".format(t, rh, p, p_t)
     print("Sending: '{}'".format(data))
-    msg = DataMessage(data, device.identifier_raw, device.device_type)
+
     if comm.network_error:
-        print("Network error")
-        spi: machine.SPI = device.spi
-        if spi:
-            print("SPI::deinit()")
-            spi.deinit()
-            spi = machine.SPI(0, SPI_BAUD, mosi=SPI_MOSI, miso=SPI_MISO, sck=SPI_SCK)
-            print("SPI::new()")
-            device.add_spi(spi)
-            print("SPI::added()")
-            dev_mac = device.generated_mac_raw()
-            device.network.deactivate()
-            print("ETH:new()")
-            ether = EthernetW5500(spi, ETH_CS, ETH_RST, dev_mac, ETH_USE_DHCP)
-            print("ETH:set()")
-            ether.set_network(ETH_IP, ETH_SUBNET, ETH_GATEWAY, ETH_DNS)
-            print("ETH:added()")
-            device.add_network_adapter(ether)
-            comm.clear_network_error()
+        await device.reset_network()
+
     if not comm.network_error:
+        msg = DataMessage(data, device.identifier_raw, device.device_type)
         await comm.send_message(msg)
-    msg = None
+        msg = None
+
     gc.collect()
 
 
 async def main():
     # configure network
     device = MeasurementDevice(device_type=DEVICE_TYPE_RHTP)
-    spi = machine.SPI(0, SPI_BAUD, mosi=SPI_MOSI, miso=SPI_MISO, sck=SPI_SCK)
-    device.add_spi(spi)
-
-    i2c = machine.I2C(id=1, scl=I2C_SCL, sda=I2C_SDA, timeout=10000)
-    device.add_i2c(i2c)
+    spi = device.add_spi(0, SPI_BAUD, mosi=SPI_MOSI, miso=SPI_MISO, sck=SPI_SCK)
+    i2c = device.add_i2c(i2c_id=1, scl=I2C_SCL, sda=I2C_SDA, timeout=10000)
 
     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)
     device.add_i2c(i2c)
     comm = device.create_communicator()
@@ -93,8 +75,11 @@ async def main():
     sht45 = SHT45(i2c)
     p_sensor = LPS28DFW(i2c)
 
-    task = PeriodicTask(aggregate_and_send, None, 1000, device, sht45, p_sensor)
-    comm.add_task(task, "aggregate_and_send")
+    comm.add_task(
+        PeriodicTask(aggregate_and_send, None, 1000, device, sht45, p_sensor),
+        "aggregate_and_send",
+    )
+    comm.add_task(PeriodicTask(device.send_life_sign, None, 5000), "life_sign")
 
     asyncio.run(comm.start())
 
diff --git a/syntheticdata/__init__.py b/syntheticdata/__init__.py
new file mode 100644
index 0000000000000000000000000000000000000000..3da0fcd322110bcefbe2a2630d9c543fc24d0d12
--- /dev/null
+++ b/syntheticdata/__init__.py
@@ -0,0 +1,70 @@
+import datetime
+import random
+
+
+class SyntheticDatum:
+    def __init__(self):
+        self.__fields = []
+
+    def get_datum(self, sep=", "):
+        return sep.join(self.__fields)
+
+    def add_value(self, value):
+        self.__fields.append(str(value))
+
+    def add_values(self, values: list | tuple):
+        for field in values:
+            self.__fields.append(str(field))
+
+    def add_time(self, ts: datetime.datetime | None = None):
+        if ts is None:
+            ts = datetime.datetime.now(tz=datetime.UTC)
+        ts = round_to_second(ts)
+        self.__fields.append(str(ts.strftime("%H:%M:%S")))
+
+    def add_date(self, ts: datetime.datetime | None = None, fmt="%Y-%m-%d"):
+        if ts is None:
+            ts = datetime.datetime.now(tz=datetime.UTC)
+        ts = round_to_second(ts)
+        self.__fields.append(str(ts.strftime(fmt)))
+
+    def add_integer(
+        self,
+        min_val: int = 0,
+        max_val: int = 999,
+        count=1,
+    ):
+
+        for i in range(count):
+            value = random.randint(min_val, max_val)
+            self.__fields.append(str(value))
+
+    def add_float(
+        self, min_val: float = 0.0, max_val: float = 1.0, fmt: str = "0.2f", count=1
+    ):
+
+        for i in range(count):
+            value = random.uniform(min_val, max_val)
+            self.__fields.append(f"{value:{fmt}}")
+
+    def add_room_temperature(self, fmt: str = "0.2f", count=1):
+        for i in range(count):
+            value = random.uniform(15, 20)
+            self.__fields.append(f"{value:{fmt}}")
+
+    def add_pressure_pa(self, fmt: str = ".0f", count=1):
+        for i in range(count):
+            value = random.uniform(100000 * 0.95, 100000 * 1.05)
+            self.__fields.append(f"{value:{fmt}}")
+
+
+def round_to_second(when: datetime.datetime | None = None) -> datetime.datetime:
+
+    if when is None:
+        when = datetime.datetime.now(tz=datetime.timezone.utc)
+    if not isinstance(when, datetime.datetime):
+        raise ValueError(f"Timestamp {when} is not a datetime.datetime")
+    if when.microsecond >= 500 * 1000:
+        when = when + datetime.timedelta(seconds=1)
+    now = when.replace(microsecond=0)
+    return now
diff --git a/umnp-daq.py b/umnp-daq.py
index a8d20b5858329e35640897f9ce6a83d3bfba0f9d..341035277809b5c8c9130e51cc852098a771a8c3 100644
--- a/umnp-daq.py
+++ b/umnp-daq.py
@@ -3,8 +3,8 @@ import socket
 
 from umnp.daq.file_daq import FileDAQ
 from umnp.microcontroller.devices.network.udp import DEFAULT_UMNP_DATA_IN_PORT
-from umnp.protocol import DataMessage
-from umnp.protocol.message import Message
+from umnp.proto import DataMessage, DeviceMessage
+from umnp.proto.message import Message
 
 sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
 sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
@@ -21,6 +21,10 @@ def main():
         now = datetime.datetime.now(tz=datetime.timezone.utc)
         if isinstance(msg, DataMessage):
             print(f"{now},{msg.sender_id},{msg.payload()}")
+        elif isinstance(msg, DeviceMessage):
+            print(
+                f"{now},{msg.sender_id},{msg.get_message_type_string},{msg.payload()}"
+            )
         files.save_message(msg)
 
 
diff --git a/umnp/daq/file_daq.py b/umnp/daq/file_daq.py
index ffaaf0d75238f902e844cd89f02fede99b509be8..c768f70c4ba75e81e9626c00eee5b25731a79eb4 100644
--- a/umnp/daq/file_daq.py
+++ b/umnp/daq/file_daq.py
@@ -2,8 +2,13 @@ import datetime
 import os
 
 from umnp.devices import get_device_type_name
-from umnp.protocol.headers import MESSAGE_HEADERS
-from umnp.protocol.message import Message
+from umnp.proto import DataMessage, DeviceMessage
+from umnp.proto.headers import DEVICE_HEADERS
+from umnp.proto.message import Message
+
+
+def clean(field: str) -> str:
+    return field.replace(",", ";")
 
 
 class FileDAQ:
@@ -27,9 +32,19 @@ class FileDAQ:
         self.__open_files[file_id] = open(filename, "w")
         f = self.__open_files[file_id]
         self.__open_fns[file_id] = filename
-        header = MESSAGE_HEADERS.get(msg.sender_type, {}).get(msg.type)
+        headers = ["receive-time", "sender-id"]
+        if isinstance(msg, DataMessage):
+            headers.extend(DEVICE_HEADERS.get(msg.sender_type, {}).get(msg.type))
+
+        elif isinstance(msg, DeviceMessage):
+            headers.extend(msg.header_fields)
+
+        else:
+            headers = ["# unknown"]
+        headers = [header.replace(",", ";") for header in headers]
+        header = ",".join(headers)
         if header:
-            f.write("receive-time,sender-id," + ",".join(header) + "\n")
+            f.write(header + "\n")
         return f
 
     def save_message(self, msg: Message):
@@ -61,5 +76,13 @@ class FileDAQ:
                 f.flush()
 
         if f:
-            f.write(f"{when},{msg.sender_id},{msg.payload()}\n")
+            if isinstance(msg, DataMessage):
+                payload = clean(msg.payload())
+                f.write(f"{when},{msg.sender_id},{payload}\n")
+            elif isinstance(msg, DeviceMessage):
+                payload = clean(msg.payload())
+                msg_type = clean(msg.get_message_type_string)
+                f.write(f"{when},{msg.sender_id},{msg_type},{payload}\n")
+            else:
+                f.write(msg.payload + "\n")
             f.flush()
diff --git a/umnp/microcontroller/communication/udp_communicator.py b/umnp/microcontroller/communication/udp_communicator.py
index c9346cc07df6f6529885cb52d3429012b0604193..a713209cfe11185d5d107d6259f07f41e421bb5b 100644
--- a/umnp/microcontroller/communication/udp_communicator.py
+++ b/umnp/microcontroller/communication/udp_communicator.py
@@ -3,7 +3,7 @@ import time
 
 from umnp.microcontroller.devices.network.udp import UDPSender, UDPReceiver
 from umnp.microcontroller.tasks.periodictask import PeriodicTask
-from umnp.protocol.message import Message
+from umnp.proto.message import Message
 
 if sys.implementation.name == "micropython":
     # noinspection PyUnresolvedReferences
diff --git a/umnp/microcontroller/devices/network/ethernet_w5500.py b/umnp/microcontroller/devices/network/ethernet_w5500.py
index 01e6b8c9d0df3e1198f2cd22fd589f038df574b0..711ed48339c5f021dabaaf916632a49bef43464f 100644
--- a/umnp/microcontroller/devices/network/ethernet_w5500.py
+++ b/umnp/microcontroller/devices/network/ethernet_w5500.py
@@ -9,7 +9,7 @@ if sys.implementation.name == "micropython":
 
     # noinspection PyUnresolvedReferences
     import network
-    import umnp.protocol.compat.logging as logging
+    import umnp.proto.compat.logging as logging
 
 else:
     from umnp.microcontroller.umock import machine, network
@@ -26,16 +26,43 @@ class EthernetW5500(EthernetAdapter):
         dhcp=False,
     ):
         super().__init__()
+        self._dhcp = dhcp
         self._spi = spi
+        self._mac = mac
         self._pin1 = pin_cs
         self._pin2 = pin_rst
+        self._nic = None
+        self._ip = None
+        self._subnet = None
+        self._gateway = None
+        self._dns = None
+        self._init(spi)
+
+        # self._nic = network.WIZNET5K(spi, self._pin1, self._pin2)
+        # self._nic.active(True)
+
+        # self._nic.config(mac=self._mac)
+        # if self._dhcp:
+        #    self.enable_dhcp()
+
+    def _init(self, spi: machine.SPI):
         self._nic = network.WIZNET5K(spi, self._pin1, self._pin2)
         self._nic.active(True)
-        self._nic.config(mac=mac)
-        if dhcp:
+        self._nic.config(mac=self._mac)
+        if self._dhcp:
             self.enable_dhcp()
 
+    def reinit(self, spi: machine.SPI):
+        self._init(spi)
+        self.set_network(
+            ip=self._ip, subnet_mask=self._subnet, gateway=self._gateway, dns=self._dns
+        )
+
     def set_network(self, ip: str, subnet_mask: str, gateway: str, dns: str):
+        self._ip = ip
+        self._subnet = subnet_mask
+        self._gateway = gateway
+        self._dns = dns
         self._nic.ifconfig((ip, subnet_mask, gateway, dns))
 
     def enable_dhcp(self):
diff --git a/umnp/microcontroller/devices/network/udp.py b/umnp/microcontroller/devices/network/udp.py
index ef956206a3006be3c551b5162263b328a6c288a7..53955be1abb353ae88705da2c4216ba9b9f49148 100644
--- a/umnp/microcontroller/devices/network/udp.py
+++ b/umnp/microcontroller/devices/network/udp.py
@@ -10,7 +10,7 @@ from umnp.microcontroller.devices.network import (
 if sys.implementation.name == "micropython":
     # noinspection PyUnresolvedReferences
     import uasyncio as asyncio
-    import umnp.protocol.compat.logging as logging
+    import umnp.proto.compat.logging as logging
 
 else:
     import logging
diff --git a/umnp/microcontroller/measurementdevice.py b/umnp/microcontroller/measurementdevice.py
index 9a1ab8f94c7e8cb00e77f3bcb5b93b79474f889a..72aba1739ac974904463b06a52952883443c7772 100644
--- a/umnp/microcontroller/measurementdevice.py
+++ b/umnp/microcontroller/measurementdevice.py
@@ -10,7 +10,9 @@ from umnp.microcontroller.devices.network.udp import (
     DEFAULT_UMNP_DATA_IN_PORT,
     DEFAULT_UMNP_COMMAND_IN_PORT,
 )
-from umnp.protocol.constants import MSG_STRING_ENCODING
+from umnp.proto import DeviceMessage
+from umnp.proto.constants import MSG_STRING_ENCODING
+from umnp.proto.device_message import MSG_TYPE_LIFE_SIGN, MSG_TYPE_INFO
 
 if sys.implementation.name == "micropython":
     # noinspection PyUnresolvedReferences
@@ -39,6 +41,7 @@ class MeasurementDevice:
         self._type = device_type
         self._i2c = None
         self._spi = None
+        self._spi_params = None
 
     @property
     def device_type(self):
@@ -48,15 +51,49 @@ class MeasurementDevice:
     def boot_time(self):
         return self._boot_time
 
-    def add_i2c(self, i2c: machine.I2C):
-        self._i2c = i2c
-
     @property
     def spi(self):
         return self._spi
 
-    def add_spi(self, spi: machine.SPI):
-        self._spi = spi
+    def spi_reinit(self) -> machine.SPI:
+        if len(self._spi_params) == 0:
+            return
+
+        if self._spi:
+            print("SPI::reinit()")
+            self._spi.deinit()
+        self._spi = machine.SPI(
+            self._spi_params["id"],
+            self._spi_params["baud"],
+            mosi=self._spi_params["mosi"],
+            miso=self._spi_params["miso"],
+            sck=self._spi_params["sck"],
+        )
+        return self._spi
+
+    def add_spi(
+        self,
+        spi_id: int,
+        baudrate: int,
+        mosi: machine.Pin,
+        miso: machine.Pin,
+        sck: machine.Pin,
+    ) -> machine.SPI:
+
+        self._spi_params = {
+            "id": spi_id,
+            "baud": baudrate,
+            "mosi": mosi,
+            "miso": miso,
+            "sck": sck,
+        }
+
+        self._spi = self.spi_reinit()
+        return self._spi
+
+    def add_i2c(self, i2c_id: int, scl: machine.Pin, sda: machine.Pin, timeout: int):
+        self._i2c = machine.I2C(id=i2c_id, scl=scl, sda=sda, timeout=timeout)
+        return self._i2c
 
     @property
     def i2c(self):
@@ -65,9 +102,23 @@ class MeasurementDevice:
     def add_network_adapter(self, adapter):
         self._network = adapter
 
-    def reset_network_adapter(self):
-        if self._network:
-            self._network.reset()
+    async def reset_network(self):
+        print("Network error")
+        if self.spi:
+            self.spi_reinit()
+            # dev_mac = device.generated_mac_raw()
+            self.network.deactivate()
+            self.network.reinit(self.spi)
+            self.communicator.clear_network_error()
+
+            info_msg = DeviceMessage(
+                "restarted network",
+                MSG_TYPE_INFO,
+                self.identifier_raw,
+                self.device_type,
+            )
+            await self.communicator.send_message(info_msg)
+            info_msg = None
 
     @property
     def network(self):
@@ -118,3 +169,21 @@ class MeasurementDevice:
     @property
     def communicator(self) -> UDPCommunicator | None:
         return self._communicator
+
+    async def send_life_sign(self):
+        if not self.communicator:
+            return
+        if self.communicator.network_error:
+            return
+
+        now = time.time()
+        seconds_since_boot = now - self.boot_time
+
+        msg = DeviceMessage(
+            f"{seconds_since_boot}",
+            MSG_TYPE_LIFE_SIGN,
+            self.identifier_raw,
+            self.device_type,
+        )
+
+        await self.communicator.send_message(msg)
diff --git a/umnp/microcontroller/sensors/lps28dfw/__init__.py b/umnp/microcontroller/sensors/lps28dfw/__init__.py
index 11e55adf1fa97a16bbe3413cdb6f0f0921e6093a..4e10bdc058008cdcb06b27bbac71c835b878afc0 100644
--- a/umnp/microcontroller/sensors/lps28dfw/__init__.py
+++ b/umnp/microcontroller/sensors/lps28dfw/__init__.py
@@ -9,7 +9,7 @@ except ImportError:
     from umnp.microcontroller.umock.machine import I2C
 
 if sys.implementation.name == "micropython":
-    import umnp.protocol.compat.logging as logging
+    import umnp.proto.compat.logging as logging
 else:
 
     def const(x):
diff --git a/umnp/microcontroller/sensors/sht45/__init__.py b/umnp/microcontroller/sensors/sht45/__init__.py
index 40da97fbe67a4927cf6ae38aba770d79a49b7102..845b519570f8f0e5a921c24fd983495105526ebd 100644
--- a/umnp/microcontroller/sensors/sht45/__init__.py
+++ b/umnp/microcontroller/sensors/sht45/__init__.py
@@ -7,7 +7,7 @@ except ImportError:
 
 
 if sys.implementation.name == "micropython":
-    import umnp.protocol.compat.logging as logging
+    import umnp.proto.compat.logging as logging
 else:
 
     def const(x):
diff --git a/umnp/microcontroller/umock/machine/__init__.py b/umnp/microcontroller/umock/machine/__init__.py
index 178916d411c8f464f1aa0de2c247413c83ec2cea..7203546222d120ad1eb374d9cbb5e6a793f37ef6 100644
--- a/umnp/microcontroller/umock/machine/__init__.py
+++ b/umnp/microcontroller/umock/machine/__init__.py
@@ -1,7 +1,7 @@
 import datetime
 import uuid
 
-from umnp.protocol.message_header import MSG_BYTE_ORDER
+from umnp.proto.message_header import MSG_BYTE_ORDER
 
 
 def unique_id() -> bytes:
diff --git a/umnp/proto/__init__.py b/umnp/proto/__init__.py
new file mode 100644
index 0000000000000000000000000000000000000000..bd3c4af2ccc6f22f38ae7f39f4987dde74534473
--- /dev/null
+++ b/umnp/proto/__init__.py
@@ -0,0 +1,7 @@
+from umnp.proto.data_message import DataMessage
+from umnp.proto.device_message import DeviceMessage
+from umnp.proto.messagetype import MessageType
+from umnp.proto.register_messages import register_messages
+
+register_messages(MessageType.MSG_DEVICE_DATA, DataMessage, "data")
+register_messages(MessageType.MSG_DEVICE_INFO, DeviceMessage, "info")
diff --git a/umnp/protocol/common/__init__.py b/umnp/proto/common/__init__.py
similarity index 100%
rename from umnp/protocol/common/__init__.py
rename to umnp/proto/common/__init__.py
diff --git a/umnp/protocol/common/timestamp.py b/umnp/proto/common/timestamp.py
similarity index 96%
rename from umnp/protocol/common/timestamp.py
rename to umnp/proto/common/timestamp.py
index 0240240112bedf0cb0ee294a206d0e59bf3c361f..aff9f031775c5f915267de0f007732a35a6c624e 100644
--- a/umnp/protocol/common/timestamp.py
+++ b/umnp/proto/common/timestamp.py
@@ -1,6 +1,6 @@
 import sys
 
-from umnp.protocol.umnp_time import seconds_since_epoch
+from umnp.proto.umnp_time import seconds_since_epoch
 
 if sys.implementation.name == "micropython":
     pass
diff --git a/programs/test.py b/umnp/proto/compat/__init__.py
similarity index 100%
rename from programs/test.py
rename to umnp/proto/compat/__init__.py
diff --git a/umnp/protocol/compat/logging.py b/umnp/proto/compat/logging.py
similarity index 100%
rename from umnp/protocol/compat/logging.py
rename to umnp/proto/compat/logging.py
diff --git a/umnp/protocol/constants.py b/umnp/proto/constants.py
similarity index 100%
rename from umnp/protocol/constants.py
rename to umnp/proto/constants.py
diff --git a/umnp/protocol/data_message.py b/umnp/proto/data_message.py
similarity index 79%
rename from umnp/protocol/data_message.py
rename to umnp/proto/data_message.py
index b59994a116af0c9acba8a09d5fd706d55b03eb89..8640733a26414b1707f969bba0a92fc02370aa52 100644
--- a/umnp/protocol/data_message.py
+++ b/umnp/proto/data_message.py
@@ -1,7 +1,7 @@
-from umnp.protocol.common.timestamp import TimeStamp
-from umnp.protocol.constants import MSG_STRING_ENCODING
-from umnp.protocol.message import Message, MessageHeader
-from umnp.protocol.messagetype import MessageType
+from umnp.proto.common.timestamp import TimeStamp
+from umnp.proto.constants import MSG_STRING_ENCODING
+from umnp.proto.message import Message, MessageHeader
+from umnp.proto.messagetype import MessageType
 
 
 class DataMessage(Message):
diff --git a/umnp/protocol/device.py b/umnp/proto/device.py
similarity index 100%
rename from umnp/protocol/device.py
rename to umnp/proto/device.py
diff --git a/umnp/proto/device_message.py b/umnp/proto/device_message.py
new file mode 100644
index 0000000000000000000000000000000000000000..5e6128580859b93e69958d8d7f4aa6906a1d2014
--- /dev/null
+++ b/umnp/proto/device_message.py
@@ -0,0 +1,68 @@
+from umnp.proto.common.timestamp import TimeStamp
+from umnp.proto.constants import MSG_STRING_ENCODING
+from umnp.proto.message import Message, MessageHeader, MSG_BYTE_ORDER
+from umnp.proto.messagetype import MessageType
+
+MSG_TYPE_UNKNOWN = 0
+MSG_TYPE_ERROR = 1
+MSG_TYPE_INFO = 2
+MSG_TYPE_LIFE_SIGN = 3
+
+
+_MSG_TYPE_DICT = {
+    MSG_TYPE_ERROR: "error",
+    MSG_TYPE_INFO: "info",
+    MSG_TYPE_LIFE_SIGN: "seconds since boot",
+}
+
+
+class DeviceMessage(Message):
+    def __init__(
+        self,
+        data: str,
+        msg_type: int,
+        sender_id: bytes,
+        sender_type: int,
+        send_time: TimeStamp = None,
+    ):
+        self._msg_type = msg_type
+        self._payload = data
+        _msg_type = msg_type.to_bytes(1, MSG_BYTE_ORDER)
+        self._encoded_data = _msg_type + data.encode(MSG_STRING_ENCODING)
+        super().__init__(
+            MessageType.MSG_DEVICE_INFO,
+            self._encoded_data,
+            sender_id,
+            sender_type,
+            send_time,
+        )
+
+    @staticmethod
+    def _decode_payload(transferred_data):
+        return transferred_data.decode(MSG_STRING_ENCODING)
+
+    @property
+    def get_message_type_string(self):
+        return _MSG_TYPE_DICT.get(self._msg_type, "unknown")
+
+    @property
+    def message_type(self):
+        return self._msg_type
+
+    def payload(self) -> str:
+        return self._payload
+
+    @classmethod
+    def decode(cls, payload: bytes, header: MessageHeader) -> "DeviceMessage":
+        decoded_payload = cls._decode_payload(payload[1:])
+        return cls(
+            decoded_payload,
+            payload[0],
+            header.sender_id,
+            header.sender_type,
+            header.timestamp,
+        )
+
+    @property
+    def header_fields(self):
+        return ["message-type", "message"]
diff --git a/umnp/protocol/device_types.py b/umnp/proto/device_types.py
similarity index 100%
rename from umnp/protocol/device_types.py
rename to umnp/proto/device_types.py
diff --git a/umnp/protocol/headers.py b/umnp/proto/headers.py
similarity index 63%
rename from umnp/protocol/headers.py
rename to umnp/proto/headers.py
index d868e09b4aa50d589defdecf6e8506f20e2d2f5f..3441dd1a10d917e952b0001f7c60897dea660261 100644
--- a/umnp/protocol/headers.py
+++ b/umnp/proto/headers.py
@@ -1,6 +1,6 @@
 from umnp.devices import DEVICE_TYPE_RHTP
-from umnp.protocol.messagetype import MessageType
+from umnp.proto.messagetype import MessageType
 
-MESSAGE_HEADERS = {
+DEVICE_HEADERS = {
     DEVICE_TYPE_RHTP: {MessageType.MSG_DEVICE_DATA: ["T", "rH", "p", "T(p)"]}
 }
diff --git a/umnp/protocol/message.py b/umnp/proto/message.py
similarity index 93%
rename from umnp/protocol/message.py
rename to umnp/proto/message.py
index f18431f9982b2b8f4135f42b6e2d67df98a1f225..afbc8da7a08cd9d7eaa4f5a9e03107aa48031743 100644
--- a/umnp/protocol/message.py
+++ b/umnp/proto/message.py
@@ -7,12 +7,12 @@ except ImportError:
 try:
     import logging
 except ImportError:
-    import umnp.protocol.compat.logging as logging
+    from umnp.proto.compat import logging
 
 
-from umnp.protocol.common.timestamp import TimeStamp
-from umnp.protocol.constants import MSG_BYTE_ORDER, MSG_LEN_PAYLOAD_SIZE
-from umnp.protocol.message_header import MessageHeader
+from umnp.proto.common.timestamp import TimeStamp
+from umnp.proto.constants import MSG_BYTE_ORDER, MSG_LEN_PAYLOAD_SIZE
+from umnp.proto.message_header import MessageHeader
 
 
 class Message:
diff --git a/umnp/protocol/message_header.py b/umnp/proto/message_header.py
similarity index 96%
rename from umnp/protocol/message_header.py
rename to umnp/proto/message_header.py
index e13a2f6b81879d1a2a96396924c9b2f26d2dab5b..bdda79c46fc3b6b6c956c691d08f9dc31e5b43e9 100644
--- a/umnp/protocol/message_header.py
+++ b/umnp/proto/message_header.py
@@ -1,5 +1,5 @@
-from umnp.protocol.common.timestamp import TimeStamp, valid_timestamp
-from umnp.protocol.constants import (
+from umnp.proto.common.timestamp import TimeStamp, valid_timestamp
+from umnp.proto.constants import (
     MSG_PROTOCOL_VERSION,
     MSG_LEN_PROTOCOL_VERSION,
     MSG_BYTE_ORDER,
@@ -12,7 +12,7 @@ from umnp.protocol.constants import (
 try:
     import logging
 except ImportError:
-    import umnp.protocol.compat.logging as logging
+    from umnp.proto.compat import logging
 
 try:
     # noinspection PyUnresolvedReferences
diff --git a/umnp/protocol/messagetype.py b/umnp/proto/messagetype.py
similarity index 72%
rename from umnp/protocol/messagetype.py
rename to umnp/proto/messagetype.py
index b180fd99ead62f6528c8ff060b28eeb787c39ed3..077f10283f09b5aa9e9f6e3063c8851f4996f153 100644
--- a/umnp/protocol/messagetype.py
+++ b/umnp/proto/messagetype.py
@@ -3,11 +3,11 @@
 
 class MessageType:
     MSG_DEVICE_DATA = 1
-    MSG_TYPE_INFO = 16
+    MSG_DEVICE_INFO = 16
     MSG_TYPE_ERROR = 32
     MSG_TYPE_UNKNOWN = 65535
 
-    _allowed_message_types = [MSG_DEVICE_DATA, MSG_TYPE_UNKNOWN]
+    _allowed_message_types = [MSG_DEVICE_DATA, MSG_TYPE_UNKNOWN, MSG_DEVICE_INFO]
 
     @staticmethod
     def valid_message_type(msg_type: int):
diff --git a/umnp/protocol/register_messages.py b/umnp/proto/register_messages.py
similarity index 85%
rename from umnp/protocol/register_messages.py
rename to umnp/proto/register_messages.py
index 3e8c8dce7b43ac741d37206012ed651f125ea1a6..b29898bac2149b7d39477f4239fcbcdde1b7ccd9 100644
--- a/umnp/protocol/register_messages.py
+++ b/umnp/proto/register_messages.py
@@ -3,7 +3,7 @@ try:
     import typing
 except ImportError:
     pass
-from umnp.protocol.message import Message
+from umnp.proto.message import Message
 
 MESSAGE_NAMES = {}
 
diff --git a/umnp/protocol/umnp_time.py b/umnp/proto/umnp_time.py
similarity index 100%
rename from umnp/protocol/umnp_time.py
rename to umnp/proto/umnp_time.py
diff --git a/umnp/protocol/__init__.py b/umnp/protocol/__init__.py
deleted file mode 100644
index a7f0d18c8257f43157f4dbb7517ee04769b7c9fe..0000000000000000000000000000000000000000
--- a/umnp/protocol/__init__.py
+++ /dev/null
@@ -1,9 +0,0 @@
-from umnp.protocol.data_message import DataMessage
-from umnp.protocol.error_message import ErrorMessage
-from umnp.protocol.info_message import InfoMessage
-from umnp.protocol.messagetype import MessageType
-from umnp.protocol.register_messages import register_messages
-
-register_messages(MessageType.MSG_DEVICE_DATA, DataMessage, "data")
-register_messages(MessageType.MSG_TYPE_INFO, InfoMessage, "info")
-register_messages(MessageType.MSG_TYPE_ERROR, ErrorMessage, "error")
diff --git a/umnp/protocol/error_message.py b/umnp/protocol/error_message.py
deleted file mode 100644
index 314332f45e76a8d7e67af3d50a5984c321cdd8d6..0000000000000000000000000000000000000000
--- a/umnp/protocol/error_message.py
+++ /dev/null
@@ -1,33 +0,0 @@
-from umnp.protocol.common.timestamp import TimeStamp
-from umnp.protocol.constants import MSG_STRING_ENCODING
-from umnp.protocol.message import Message, MessageHeader
-from umnp.protocol.messagetype import MessageType
-
-
-class ErrorMessage(Message):
-    def __init__(
-        self, data: str, sender_id: bytes, sender_type: int, send_time: TimeStamp = None
-    ):
-        self._payload = data
-        self._encoded_data = data.encode(MSG_STRING_ENCODING)
-        super().__init__(
-            MessageType.MSG_DEVICE_ERROR,
-            self._encoded_data,
-            sender_id,
-            sender_type,
-            send_time,
-        )
-
-    @staticmethod
-    def _decode_payload(transferred_data):
-        return transferred_data.decode(MSG_STRING_ENCODING)
-
-    def payload(self) -> str:
-        return self._payload
-
-    @classmethod
-    def decode(cls, payload: bytes, header: MessageHeader) -> "ErrorMessage":
-        decoded_payload = cls._decode_payload(payload)
-        return cls(
-            decoded_payload, header.sender_id, header.sender_type, header.timestamp
-        )
diff --git a/umnp/protocol/info_message.py b/umnp/protocol/info_message.py
deleted file mode 100644
index b8a45b2ef4f1c4bbd87c05b080f15eca58d9fdde..0000000000000000000000000000000000000000
--- a/umnp/protocol/info_message.py
+++ /dev/null
@@ -1,33 +0,0 @@
-from umnp.protocol.common.timestamp import TimeStamp
-from umnp.protocol.constants import MSG_STRING_ENCODING
-from umnp.protocol.message import Message, MessageHeader
-from umnp.protocol.messagetype import MessageType
-
-
-class InfoMessage(Message):
-    def __init__(
-        self, data: str, sender_id: bytes, sender_type: int, send_time: TimeStamp = None
-    ):
-        self._payload = data
-        self._encoded_data = data.encode(MSG_STRING_ENCODING)
-        super().__init__(
-            MessageType.MSG_DEVICE_INFO,
-            self._encoded_data,
-            sender_id,
-            sender_type,
-            send_time,
-        )
-
-    @staticmethod
-    def _decode_payload(transferred_data):
-        return transferred_data.decode(MSG_STRING_ENCODING)
-
-    def payload(self) -> str:
-        return self._payload
-
-    @classmethod
-    def decode(cls, payload: bytes, header: MessageHeader) -> "InfoMessage":
-        decoded_payload = cls._decode_payload(payload)
-        return cls(
-            decoded_payload, header.sender_id, header.sender_type, header.timestamp
-        )