Supporting UART Boards - 1st Part #46

Closed
opened 2026-10-27 09:30:06 +01:00 by adam · 0 comments
Owner

I went to our friend Chatgpt and got the info below for building a UART board. I ran the script and it shows the RX flashing (which is much better than what I got before...)

Here is the Code and conversation, hope it helps. I am not a coder so I did it all thru AI.

1 - Patch to integrate the driver into pyMC_Repeater

For the project pyMC_Repeater the typical structure looks like:

pyMC_Repeater/
├─ pymc_core/
│ ├─ hardware/
│ │ ├─ sx1262_radio.py
│ │ └─ uart_sx1262_radio.py ← new file
│ ├─ radio_factory.py

You add the UART driver file and then modify the radio factory so the repeater can create the radio.

2️⃣ Add the new driver

Create the file:

pymc_core/hardware/uart_sx1262_radio.py

and place the UART driver code there.

UART Driver Code:

CODE START:

import serial
import threading
import time
import struct

class UARTSX1262Radio:

"""
UART driver for Waveshare SX1262 LoRa HAT (UART version)
Designed for Raspberry Pi / pyMC repeater environments
"""

def __init__(
    self,
    port="/dev/serial0",
    baudrate=9600,
    freq=868000000,
    channel=23,
    power=22,
    air_rate=2400
):

    self.port = port
    self.baudrate = baudrate
    self.freq = freq
    self.channel = channel
    self.power = power
    self.air_rate = air_rate

    self.serial = serial.Serial(
        port=self.port,
        baudrate=self.baudrate,
        timeout=0.1
    )

    self.running = False
    self.rx_callback = None
    self.rx_thread = None

    self._rx_buffer = bytearray()

    self.configure_radio()

# --------------------------------------------------
# LOW LEVEL SERIAL
# --------------------------------------------------

def _write(self, data):

    self.serial.write(data)
    self.serial.flush()

def _read_all(self):

    return self.serial.read(self.serial.in_waiting or 1)

def send_command(self, cmd):

    self._write(cmd)
    time.sleep(0.05)

    resp = self._read_all()

    return resp

# --------------------------------------------------
# RADIO CONFIG
# --------------------------------------------------

def configure_radio(self):

    print("Initializing UART SX1262 radio")

    self.set_channel(self.channel)
    self.set_power(self.power)

    print("Radio configured:")
    print(" channel:", self.channel)
    print(" power:", self.power)

def set_channel(self, channel):

    """
    Channel mapping used by Waveshare firmware
    """

    self.channel = channel

    cmd = bytes([
        0xC0,
        0x00,
        0x09,
        0x17,
        channel
    ])

    return self.send_command(cmd)

def set_power(self, power):

    """
    TX power (0-22 dBm typical)
    """

    self.power = power

    cmd = bytes([
        0xC0,
        0x00,
        0x09,
        0x18,
        power
    ])

    return self.send_command(cmd)

# --------------------------------------------------
# TX
# --------------------------------------------------

def send_frame(self, payload):

    """
    Send LoRa payload
    """

    if isinstance(payload, str):
        payload = payload.encode()

    length = len(payload)

    frame = bytes([0xC1, length]) + payload

    self._write(frame)

# --------------------------------------------------
# RX
# --------------------------------------------------

def _parse_frames(self, data):

    """
    Very simple frame parser
    """

    frames = []

    self._rx_buffer.extend(data)

    while True:

        if len(self._rx_buffer) < 2:
            break

        length = self._rx_buffer[1]

        if len(self._rx_buffer) < length + 2:
            break

        payload = self._rx_buffer[2:2 + length]

        frames.append(payload)

        del self._rx_buffer[:length + 2]

    return frames

def _read_radio(self):

    data = self.serial.read(self.serial.in_waiting or 1)

    if not data:
        return []

    return self._parse_frames(data)

# --------------------------------------------------
# RSSI (mock / firmware dependent)
# --------------------------------------------------

def get_rssi(self):

    """
    Request RSSI if supported by firmware
    """

    cmd = bytes([0xC1, 0x00, 0x02])

    resp = self.send_command(cmd)

    if len(resp) >= 1:
        return resp[-1]

    return None

# --------------------------------------------------
# RX THREAD
# --------------------------------------------------

def _rx_loop(self):

    while self.running:

        frames = self._read_radio()

        for frame in frames:

            if self.rx_callback:
                self.rx_callback(frame)

        time.sleep(0.01)

def start_rx(self, callback):

    self.rx_callback = callback
    self.running = True

    self.rx_thread = threading.Thread(target=self._rx_loop)
    self.rx_thread.daemon = True
    self.rx_thread.start()

    print("RX thread started")

def stop(self):

    self.running = False

    if self.rx_thread:
        self.rx_thread.join(timeout=1)

    self.serial.close()

# --------------------------------------------------
# STATUS
# --------------------------------------------------

def get_status(self):

    return {
        "port": self.port,
        "baudrate": self.baudrate,
        "channel": self.channel,
        "power": self.power
    }

CODE END:

3️⃣ Modify radio_factory.py

Edit:

pymc_core/radio_factory.py
Patch
@@
from pymc_core.hardware.sx1262_radio import SX1262Radio
+from pymc_core.hardware.uart_sx1262_radio import UARTSX1262Radio

def create_radio(config):

 radio_type = config.get("radio_type", "sx1262")

 if radio_type == "sx1262":

     return SX1262Radio(
         bus_id=config.get("spi_bus", 0),
         cs_pin=config.get("cs_pin", 21),
         reset_pin=config.get("reset_pin", 18),
         busy_pin=config.get("busy_pin", 20),
         irq_pin=config.get("irq_pin", 16)
     )
  • elif radio_type == "uart-sx1262":

  •    return UARTSX1262Radio(
    
  •        port=config.get("serial_port", "/dev/serial0"),
    
  •        baudrate=config.get("baudrate", 9600),
    
  •        channel=config.get("channel", 23),
    
  •        power=config.get("power", 22)
    
  •    )
    

    else:
    raise Exception("Unsupported radio type")

4️⃣ Example repeater configuration

Example config.json:

{
"radio_type": "uart-sx1262",
"serial_port": "/dev/serial0",
"baudrate": 9600,
"channel": 23,
"power": 22
}

5️⃣ Start the repeater
python3 repeater.py --config config.json

Expected output:

Initializing UART SX1262 radio
Radio configured
RX thread started
⚠️ 6️⃣ The hidden problem with Waveshare UART boards

This is the important part.

Those boards are not a raw LoRa radio.

Internally they contain:

SX1262 + microcontroller + serial firmware

So communication becomes:

pyMC → UART → firmware → SX1262

instead of the ideal:

pyMC → SX1262 directly

That firmware layer introduces several issues.

Problem 1 — Modified frames

The firmware sometimes sends packets formatted like:

[ADDR_H][ADDR_L][CHANNEL][PAYLOAD...]

Meaning the payload arrives modified.

But pyMC expects:

payload only

This can break:

packet parsing

signatures

mesh routing

Problem 2 — Packet size limits

Many UART LoRa modules limit payload size to roughly:

200 bytes
or
240 bytes

If pyMC sends something larger:

packet gets truncated
Problem 3 — Internal buffering

The firmware often does this:

LoRa RX
↓
internal buffer
↓
UART TX

This introduces extra latency:

10–80 ms

In a mesh network that can cause:

collisions

timeouts

unnecessary retransmissions

Problem 4 — Fake or missing RSSI

Some UART firmware implementations:

do not provide RSSI

provide averaged RSSI

return invalid values

But pyMC uses RSSI for:

routing
link quality estimation

So the mesh can make incorrect routing decisions.

Problem 5 — Transparent mode

Many modules start in:

transparent serial mode

Where:

UART → LoRa broadcast

But with:

no metadata

no RSSI

no ACK handling

For a mesh network this is very limited.

👍 Recommended configuration

If you use the UART module, configure it for:

fixed transmission mode
no address header
raw payload

Meaning:

LoRa payload = UART payload

This makes it much easier to integrate with pyMC.

💡 Raspberry Pi tip

On a Raspberry Pi 4, /dev/serial0 may change depending on Bluetooth usage.

It is usually safer to use:

/dev/ttyAMA0

and disable Bluetooth.

Edit:

/boot/config.txt

Add:

dtoverlay=disable-bt
enable_uart=1

Then reboot.

🚀 One more useful thing

If you want, I can also show you how to build a much better driver for these UART boards that:

removes the internal module headers

provides real RSSI

adds collision buffering

improves pyMC mesh stability

In small LoRa mesh networks this often improves reliability by 3× to 5×.

I went to our friend Chatgpt and got the info below for building a UART board. I ran the script and it shows the RX flashing (which is much better than what I got before...) Here is the Code and conversation, hope it helps. I am not a coder so I did it all thru AI. 1 - Patch to integrate the driver into pyMC_Repeater For the project pyMC_Repeater the typical structure looks like: pyMC_Repeater/ ├─ pymc_core/ │ ├─ hardware/ │ │ ├─ sx1262_radio.py │ │ └─ uart_sx1262_radio.py ← new file │ ├─ radio_factory.py You add the UART driver file and then modify the radio factory so the repeater can create the radio. 2️⃣ Add the new driver Create the file: pymc_core/hardware/uart_sx1262_radio.py and place the UART driver code there. UART Driver Code: **CODE START:** import serial import threading import time import struct class UARTSX1262Radio: """ UART driver for Waveshare SX1262 LoRa HAT (UART version) Designed for Raspberry Pi / pyMC repeater environments """ def __init__( self, port="/dev/serial0", baudrate=9600, freq=868000000, channel=23, power=22, air_rate=2400 ): self.port = port self.baudrate = baudrate self.freq = freq self.channel = channel self.power = power self.air_rate = air_rate self.serial = serial.Serial( port=self.port, baudrate=self.baudrate, timeout=0.1 ) self.running = False self.rx_callback = None self.rx_thread = None self._rx_buffer = bytearray() self.configure_radio() # -------------------------------------------------- # LOW LEVEL SERIAL # -------------------------------------------------- def _write(self, data): self.serial.write(data) self.serial.flush() def _read_all(self): return self.serial.read(self.serial.in_waiting or 1) def send_command(self, cmd): self._write(cmd) time.sleep(0.05) resp = self._read_all() return resp # -------------------------------------------------- # RADIO CONFIG # -------------------------------------------------- def configure_radio(self): print("Initializing UART SX1262 radio") self.set_channel(self.channel) self.set_power(self.power) print("Radio configured:") print(" channel:", self.channel) print(" power:", self.power) def set_channel(self, channel): """ Channel mapping used by Waveshare firmware """ self.channel = channel cmd = bytes([ 0xC0, 0x00, 0x09, 0x17, channel ]) return self.send_command(cmd) def set_power(self, power): """ TX power (0-22 dBm typical) """ self.power = power cmd = bytes([ 0xC0, 0x00, 0x09, 0x18, power ]) return self.send_command(cmd) # -------------------------------------------------- # TX # -------------------------------------------------- def send_frame(self, payload): """ Send LoRa payload """ if isinstance(payload, str): payload = payload.encode() length = len(payload) frame = bytes([0xC1, length]) + payload self._write(frame) # -------------------------------------------------- # RX # -------------------------------------------------- def _parse_frames(self, data): """ Very simple frame parser """ frames = [] self._rx_buffer.extend(data) while True: if len(self._rx_buffer) < 2: break length = self._rx_buffer[1] if len(self._rx_buffer) < length + 2: break payload = self._rx_buffer[2:2 + length] frames.append(payload) del self._rx_buffer[:length + 2] return frames def _read_radio(self): data = self.serial.read(self.serial.in_waiting or 1) if not data: return [] return self._parse_frames(data) # -------------------------------------------------- # RSSI (mock / firmware dependent) # -------------------------------------------------- def get_rssi(self): """ Request RSSI if supported by firmware """ cmd = bytes([0xC1, 0x00, 0x02]) resp = self.send_command(cmd) if len(resp) >= 1: return resp[-1] return None # -------------------------------------------------- # RX THREAD # -------------------------------------------------- def _rx_loop(self): while self.running: frames = self._read_radio() for frame in frames: if self.rx_callback: self.rx_callback(frame) time.sleep(0.01) def start_rx(self, callback): self.rx_callback = callback self.running = True self.rx_thread = threading.Thread(target=self._rx_loop) self.rx_thread.daemon = True self.rx_thread.start() print("RX thread started") def stop(self): self.running = False if self.rx_thread: self.rx_thread.join(timeout=1) self.serial.close() # -------------------------------------------------- # STATUS # -------------------------------------------------- def get_status(self): return { "port": self.port, "baudrate": self.baudrate, "channel": self.channel, "power": self.power } **CODE END:** 3️⃣ Modify radio_factory.py Edit: pymc_core/radio_factory.py Patch @@ from pymc_core.hardware.sx1262_radio import SX1262Radio +from pymc_core.hardware.uart_sx1262_radio import UARTSX1262Radio def create_radio(config): radio_type = config.get("radio_type", "sx1262") if radio_type == "sx1262": return SX1262Radio( bus_id=config.get("spi_bus", 0), cs_pin=config.get("cs_pin", 21), reset_pin=config.get("reset_pin", 18), busy_pin=config.get("busy_pin", 20), irq_pin=config.get("irq_pin", 16) ) + elif radio_type == "uart-sx1262": + + return UARTSX1262Radio( + port=config.get("serial_port", "/dev/serial0"), + baudrate=config.get("baudrate", 9600), + channel=config.get("channel", 23), + power=config.get("power", 22) + ) else: raise Exception("Unsupported radio type") 4️⃣ Example repeater configuration Example config.json: { "radio_type": "uart-sx1262", "serial_port": "/dev/serial0", "baudrate": 9600, "channel": 23, "power": 22 } 5️⃣ Start the repeater python3 repeater.py --config config.json Expected output: Initializing UART SX1262 radio Radio configured RX thread started ⚠️ 6️⃣ The hidden problem with Waveshare UART boards This is the important part. Those boards are not a raw LoRa radio. Internally they contain: SX1262 + microcontroller + serial firmware So communication becomes: pyMC → UART → firmware → SX1262 instead of the ideal: pyMC → SX1262 directly That firmware layer introduces several issues. Problem 1 — Modified frames The firmware sometimes sends packets formatted like: [ADDR_H][ADDR_L][CHANNEL][PAYLOAD...] Meaning the payload arrives modified. But pyMC expects: payload only This can break: packet parsing signatures mesh routing Problem 2 — Packet size limits Many UART LoRa modules limit payload size to roughly: 200 bytes or 240 bytes If pyMC sends something larger: packet gets truncated Problem 3 — Internal buffering The firmware often does this: LoRa RX ↓ internal buffer ↓ UART TX This introduces extra latency: 10–80 ms In a mesh network that can cause: collisions timeouts unnecessary retransmissions Problem 4 — Fake or missing RSSI Some UART firmware implementations: do not provide RSSI provide averaged RSSI return invalid values But pyMC uses RSSI for: routing link quality estimation So the mesh can make incorrect routing decisions. Problem 5 — Transparent mode Many modules start in: transparent serial mode Where: UART → LoRa broadcast But with: no metadata no RSSI no ACK handling For a mesh network this is very limited. 👍 Recommended configuration If you use the UART module, configure it for: fixed transmission mode no address header raw payload Meaning: LoRa payload = UART payload This makes it much easier to integrate with pyMC. 💡 Raspberry Pi tip On a Raspberry Pi 4, /dev/serial0 may change depending on Bluetooth usage. It is usually safer to use: /dev/ttyAMA0 and disable Bluetooth. Edit: /boot/config.txt Add: dtoverlay=disable-bt enable_uart=1 Then reboot. 🚀 One more useful thing If you want, I can also show you how to build a much better driver for these UART boards that: removes the internal module headers provides real RSSI adds collision buffering improves pyMC mesh stability In small LoRa mesh networks this often improves reliability by 3× to 5×. <!-- github-sync-id:4036485715 -->
adam closed this issue 2026-10-27 09:30:06 +01:00
Sign in to join this conversation.
No labels
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: adam/pyMC_Repeater#46