diff --git a/examples/calibrate_cad.py b/examples/calibrate_cad.py index bcb09bc..ddefe1c 100644 --- a/examples/calibrate_cad.py +++ b/examples/calibrate_cad.py @@ -7,7 +7,7 @@ import logging from typing import Any, Dict, List, Optional, Tuple -from common import create_radio +from common import create_radio, RADIO_TYPES logging.basicConfig(level=logging.INFO, format="%(asctime)s - %(levelname)s - %(message)s") logger = logging.getLogger(__name__) @@ -304,7 +304,7 @@ def main(): parser = argparse.ArgumentParser(description="CAD Calibration Tool with Staged Workflow") parser.add_argument( "--radio", - choices=["waveshare", "uconsole", "meshadv-mini", "kiss-tnc", "kiss-modem"], + choices=RADIO_TYPES, default="waveshare", help="Radio type", ) diff --git a/examples/common.py b/examples/common.py index a444557..612c4c7 100644 --- a/examples/common.py +++ b/examples/common.py @@ -32,6 +32,9 @@ from pymc_core.node.node import MeshNode +RADIO_TYPES = ["waveshare", "uconsole", "meshadv-mini", "kiss-tnc", "kiss-modem", "ch341", "pinedio"] + + def create_radio( radio_type: str = "waveshare", serial_port: str = "/dev/ttyUSB0", @@ -46,6 +49,7 @@ def create_radio( "kiss-tnc" — KISS TNC over serial "kiss-modem" — MeshCore KISS modem over serial "ch341" — SX1262 via CH341 USB-to-SPI adapter + "pinedio" — Similar to CH341 but different pinout "pymc_usb" — pymc_usb firmware over USB-CDC "pymc_tcp" — pymc_usb firmware over Wi-Fi/TCP serial_port: Serial port path. Used by "kiss-tnc", "kiss-modem", and "pymc_usb". @@ -116,7 +120,7 @@ def create_radio( return modem_wrapper # Check if this is a CH341 configuration - if radio_type == "ch341": + if radio_type in ("ch341", "pinedio"): from pymc_core.hardware.ch341.ch341_gpio_manager import CH341GPIOManager from pymc_core.hardware.lora.LoRaRF.SX126x import set_gpio_manager, set_spi_transport from pymc_core.hardware.sx1262_wrapper import SX1262Radio @@ -134,27 +138,51 @@ def create_radio( set_spi_transport(ch341_spi) logger.debug("Set CH341 SPI transport globally") - # CH341 pin configuration (using actual CH341 GPIO pins 0-7) - ch341_config = { - "bus_id": 0, # Not used with CH341 but required parameter - "cs_id": 0, # Not used with CH341 but required parameter - "cs_pin": 0, # CH341 GPIO 0 for CS - "reset_pin": 2, # CH341 GPIO 2 for Reset - "busy_pin": 4, # CH341 GPIO 4 for Busy - "irq_pin": 6, # CH341 GPIO 6 for IRQ - "txen_pin": -1, # Not used - "rxen_pin": 1, # CH341 GPIO 1 for RX enable - "frequency": int(869.618 * 1000000), # EU: 869.618 MHz - "tx_power": 22, - "spreading_factor": 8, - "bandwidth": int(62.5 * 1000), - "coding_rate": 8, - "preamble_length": 17, - "use_dio2_rf": True, - "is_waveshare": False, # Waveshare SX1262 LoRa HAT pinout - "use_dio3_tcxo": True, # Enable TCXO on DIO3 - "dio3_tcxo_voltage": 1.8, # 1.8V TCXO - } + # NOTE: pin numbers are CH341 GPIO pin numbers, see CH341GPIOPin + # documentation to see how that translates to CH341 pin numbers. + if radio_type == "ch341": + ch341_config = { + "bus_id": 0, # Not used with CH341 but required parameter + "cs_id": 0, # Not used with CH341 but required parameter + "cs_pin": 0, # CH341 GPIO 0 for CS + "reset_pin": 2, # CH341 GPIO 2 for Reset + "busy_pin": 4, # CH341 GPIO 4 for Busy + "irq_pin": 6, # CH341 GPIO 6 for IRQ + "txen_pin": -1, # Not used + "rxen_pin": 1, # CH341 GPIO 1 for RX enable + "frequency": int(869.618 * 1000000), # EU: 869.618 MHz + "tx_power": 22, + "spreading_factor": 8, + "bandwidth": int(62.5 * 1000), + "coding_rate": 8, + "preamble_length": 17, + "use_dio2_rf": True, + "is_waveshare": False, # Waveshare SX1262 LoRa HAT pinout + "use_dio3_tcxo": True, # Enable TCXO on DIO3 + "dio3_tcxo_voltage": 1.8, # 1.8V TCXO + } + elif radio_type == "pinedio": + # NOTE pinedio doesn't expose DIO2, therefore no control + # over TXEN/RXEN. + ch341_config = { + "bus_id": 0, # Not used with CH341 but required parameter + "cs_id": 0, # Not used with CH341 but required parameter + "cs_pin": 0, # CH341 GPIO 0 for CS (handled by SPI engine) + "reset_pin": -1, # reset is done via CH341 reset + "busy_pin": 11, # CH341 GPIO 11 = pin 8 (BUSY/SLCT) + "irq_pin": 10, # CH341 GPIO 10 = pin 7 (INT#/DIO1) + "txen_pin": -1, # TXEN is done internally via MDIO2 + "rxen_pin": -1, # RXEN is inverse of TXEN + "frequency": int(869.618 * 1000000), # EU: 869.618 MHz + "tx_power": 22, + "spreading_factor": 8, + "bandwidth": int(62.5 * 1000), + "coding_rate": 8, + "preamble_length": 17, + "use_dio2_rf": True, + "is_waveshare": False, # Waveshare SX1262 LoRa HAT pinout + "use_dio3_tcxo": False, # Enable TCXO on DIO3 + } logger.debug(f"CH341 configuration: {ch341_config}") radio = SX1262Radio(**ch341_config) diff --git a/examples/discover_nodes.py b/examples/discover_nodes.py index 196b583..8a6a89a 100644 --- a/examples/discover_nodes.py +++ b/examples/discover_nodes.py @@ -19,7 +19,7 @@ import random import time -from common import create_mesh_node +from common import create_mesh_node, RADIO_TYPES from pymc_core.protocol.packet_builder import PacketBuilder @@ -149,7 +149,7 @@ def main(): parser = argparse.ArgumentParser(description="Discover nearby mesh nodes") parser.add_argument( "--radio-type", - choices=["waveshare", "uconsole", "meshadv-mini", "kiss-tnc", "kiss-modem"], + choices=RADIO_TYPES, default="waveshare", help="Radio hardware type (default: waveshare)", ) diff --git a/examples/login_server.py b/examples/login_server.py index 368f400..5df09a2 100644 --- a/examples/login_server.py +++ b/examples/login_server.py @@ -21,7 +21,7 @@ import time from typing import Dict, Optional -from common import create_mesh_node +from common import create_mesh_node, RADIO_TYPES from pymc_core.node.handlers.login_server import LoginServerHandler from pymc_core.protocol import Identity, LocalIdentity @@ -381,7 +381,7 @@ def main(): ) parser.add_argument( "--radio-type", - choices=["waveshare", "uconsole", "meshadv-mini", "kiss-tnc", "kiss-modem"], + choices=RADIO_TYPES, default="waveshare", help="Radio hardware type (default: waveshare)", ) diff --git a/examples/ping_repeater_trace.py b/examples/ping_repeater_trace.py index 5451bad..ff9afc5 100644 --- a/examples/ping_repeater_trace.py +++ b/examples/ping_repeater_trace.py @@ -17,7 +17,7 @@ import asyncio import random -from common import create_mesh_node, print_packet_info +from common import create_mesh_node, print_packet_info, RADIO_TYPES from pymc_core.protocol.constants import PAYLOAD_TYPE_TRACE from pymc_core.protocol.packet_builder import PacketBuilder @@ -86,7 +86,7 @@ def main(): parser = argparse.ArgumentParser(description="Ping a repeater using trace packets") parser.add_argument( "--radio-type", - choices=["waveshare", "uconsole", "meshadv-mini", "kiss-tnc", "kiss-modem"], + choices=RADIO_TYPES, default="waveshare", help="Radio hardware type (default: waveshare)", ) diff --git a/examples/respond_to_discovery.py b/examples/respond_to_discovery.py index f0fad58..08a3b03 100644 --- a/examples/respond_to_discovery.py +++ b/examples/respond_to_discovery.py @@ -10,7 +10,7 @@ import asyncio -from common import create_mesh_node +from common import create_mesh_node, RADIO_TYPES from pymc_core.protocol.packet_builder import PacketBuilder @@ -121,7 +121,7 @@ def main(): parser = argparse.ArgumentParser(description="Respond to mesh node discovery requests") parser.add_argument( "--radio-type", - choices=["waveshare", "uconsole", "meshadv-mini", "kiss-tnc", "kiss-modem"], + choices=RADIO_TYPES, default="waveshare", help="Radio hardware type (default: waveshare)", ) diff --git a/examples/send_channel_message.py b/examples/send_channel_message.py index 8d48e2c..12b5e13 100644 --- a/examples/send_channel_message.py +++ b/examples/send_channel_message.py @@ -8,7 +8,7 @@ import asyncio -from common import create_mesh_node, print_packet_info +from common import create_mesh_node, print_packet_info, RADIO_TYPES from pymc_core.protocol import Packet from pymc_core.protocol.packet_builder import PacketBuilder @@ -71,7 +71,7 @@ def main(): parser = argparse.ArgumentParser(description="Send a channel message to the Public channel") parser.add_argument( "--radio-type", - choices=["waveshare", "uconsole", "meshadv-mini", "kiss-tnc", "kiss-modem"], + choices=RADIO_TYPES, default="waveshare", help="Radio hardware type (default: waveshare)", ) diff --git a/examples/send_direct_advert.py b/examples/send_direct_advert.py index 9d9046e..0d78df0 100644 --- a/examples/send_direct_advert.py +++ b/examples/send_direct_advert.py @@ -10,7 +10,7 @@ import asyncio -from common import create_mesh_node, print_packet_info +from common import create_mesh_node, print_packet_info, RADIO_TYPES from pymc_core.protocol.constants import ADVERT_FLAG_IS_CHAT_NODE from pymc_core.protocol.packet_builder import PacketBuilder @@ -51,7 +51,7 @@ def main(): parser = argparse.ArgumentParser(description="Send a direct advertisement packet") parser.add_argument( "--radio-type", - choices=["waveshare", "uconsole", "meshadv-mini", "kiss-tnc", "kiss-modem"], + choices=RADIO_TYPES, default="waveshare", help="Radio hardware type (default: waveshare)", ) diff --git a/examples/send_flood_advert.py b/examples/send_flood_advert.py index ecf51e2..7e7e63c 100644 --- a/examples/send_flood_advert.py +++ b/examples/send_flood_advert.py @@ -17,7 +17,7 @@ import asyncio import sys -from common import create_mesh_node, print_packet_info +from common import create_mesh_node, print_packet_info, RADIO_TYPES from pymc_core.protocol.constants import ADVERT_FLAG_IS_CHAT_NODE from pymc_core.protocol.packet_builder import PacketBuilder @@ -58,7 +58,7 @@ def main(): parser = argparse.ArgumentParser(description="Send a flood advertisement packet") parser.add_argument( "--radio-type", - choices=["waveshare", "uconsole", "meshadv-mini", "kiss-tnc", "kiss-modem"], + choices=RADIO_TYPES, default="waveshare", help="Radio hardware type (default: waveshare)", ) diff --git a/examples/send_text_message.py b/examples/send_text_message.py index fa77f2a..c32f36c 100644 --- a/examples/send_text_message.py +++ b/examples/send_text_message.py @@ -8,7 +8,7 @@ import asyncio -from common import create_mesh_node, print_packet_info +from common import create_mesh_node, print_packet_info, RADIO_TYPES from pymc_core.protocol import Packet from pymc_core.protocol.packet_builder import PacketBuilder @@ -78,7 +78,7 @@ def main(): parser = argparse.ArgumentParser(description="Send a text message to the mesh network") parser.add_argument( "--radio-type", - choices=["waveshare", "uconsole", "meshadv-mini", "kiss-tnc", "kiss-modem"], + choices=RADIO_TYPES, default="waveshare", help="Radio hardware type (default: waveshare)", ) diff --git a/examples/send_tracked_advert.py b/examples/send_tracked_advert.py index c20fcc5..d26f3d1 100644 --- a/examples/send_tracked_advert.py +++ b/examples/send_tracked_advert.py @@ -12,7 +12,7 @@ import asyncio import time -from common import create_mesh_node, print_packet_info +from common import create_mesh_node, print_packet_info, RADIO_TYPES from pymc_core.protocol.constants import ( ADVERT_FLAG_HAS_LOCATION, @@ -93,7 +93,7 @@ def main(): parser = argparse.ArgumentParser(description="Send a location-tracked advertisement") parser.add_argument( "--radio-type", - choices=["waveshare", "uconsole", "meshadv-mini", "kiss-tnc", "kiss-modem", "ch341"], + choices=RADIO_TYPES, default="waveshare", help="Radio hardware type (default: waveshare)", ) diff --git a/src/pymc_core/hardware/ch341/ch341_async.py b/src/pymc_core/hardware/ch341/ch341_async.py index 12294b9..5766738 100644 --- a/src/pymc_core/hardware/ch341/ch341_async.py +++ b/src/pymc_core/hardware/ch341/ch341_async.py @@ -347,8 +347,8 @@ def gpio_set_direction(self, pin: int, is_output: bool): return self._gpio_set_direction_impl(pin, is_output) def _gpio_set_direction_impl(self, pin: int, is_output: bool): - if pin < 0 or pin > 7: - raise ValueError(f"GPIO pin must be 0-7, got {pin}") + if pin < 0 or pin > 15: + raise ValueError(f"GPIO pin must be 0-15, got {pin}") if pin > 5 and is_output: raise ValueError(f"GPIO pin {pin} only supports input mode (pins 6-7 are input-only)") @@ -372,8 +372,8 @@ def gpio_get(self, pin: int) -> bool: self._operation_lock.release() def _gpio_get_impl(self, pin: int) -> bool: - if pin < 0 or pin > 7: - raise ValueError(f"GPIO pin must be 0-7, got {pin}") + if pin < 0 or pin > 15: + raise ValueError(f"GPIO pin must be 0-15, got {pin}") GPIO_READ_BYTES = 6 GPIO_READ_TIMEOUT_MS = 200 @@ -388,7 +388,10 @@ def _gpio_get_impl(self, pin: int) -> bool: except Exception as e: raise CH341Error(f"GPIO read IN failed: {e}") from e - return bool(data[0] & (1 << pin)) + if pin < 8: + return bool(data[0] & (1 << pin)) + else: + return bool(data[1] & (1 << (pin - 8))) def __enter__(self): return self diff --git a/src/pymc_core/hardware/ch341/ch341_gpio_manager.py b/src/pymc_core/hardware/ch341/ch341_gpio_manager.py index 70e7127..21fa12d 100644 --- a/src/pymc_core/hardware/ch341/ch341_gpio_manager.py +++ b/src/pymc_core/hardware/ch341/ch341_gpio_manager.py @@ -15,7 +15,32 @@ class CH341GPIOPin: - """Mock GPIO pin that uses CH341 GPIO""" + """Mock GPIO pin that uses CH341 GPIO + + GPIO pin mapping for CH341B/A/F in synchronous serial communication + mode (device id = 0x5512) based on the information at + https://github.com/frank-zago/ch341-i2c-spi-gpio: + + CH341A/B/F GPIO Names Mode + pin bit + + 15 0 CS0 input/output + 16 1 CS1 input/output + 17 2 CS2 input/output + 18 3 SCK input/output + 19 4 DOUT2 input/output + 20 5 MOSI input/output + 21 6 DIN2 input + 22 7 MISO input + 5 8 General purpose input + 6 9 General purpose input + 7 10 INT# input + 8 11 General purpose input + ? 12 ? input + 27 13 WT (WAIT) input + 4 14 DS (Data Select?) input + 3 15 AS (Address Select?) input + """ def __init__(self, ch341_device, pin_number: int, is_output: bool = True, gpio_manager=None): self.ch341 = ch341_device @@ -215,7 +240,21 @@ def disable_interrupt(self): class CH341GPIOManager: - """GPIO manager that uses CH341's GPIO pins (0-7) directly""" + """GPIO manager that uses CH341's GPIO pins (0-15) directly. + + Only pins 0-5 are output-capable. Pins 6-15 are input-only. + Pin mapping for CH341F sync-serial mode (PID 0x5512): + GPIO 0 = pin 15 (CS0) + GPIO 1 = pin 16 (CS1) + GPIO 2 = pin 17 (CS2) + GPIO 3 = pin 18 (SCK) + GPIO 4 = pin 19 (DOUT2/CS3) + GPIO 5 = pin 20 (MOSI) + GPIO 6 = pin 21 (DIN2) + GPIO 7 = pin 22 (MISO) + GPIO 10 = pin 7 (INT#) + GPIO 11 = pin 8 (BUSY/SLCT) + """ def __init__(self, vid: int = 0x1A86, pid: int = 0x5512): """ @@ -236,22 +275,22 @@ def __init__(self, vid: int = 0x1A86, pid: int = 0x5512): self._led_threads = {} # pin_number -> Thread self._led_stop_events = {} # pin_number -> Event - logger.info("Using CH341 GPIO manager - CH341 pins 0-7 only") + logger.info("Using CH341 GPIO manager - CH341 pins 0-15 (0-5 output-capable)") def setup_output_pin(self, pin: int, initial_value: bool = True) -> bool: """ Setup output pin using CH341 GPIO Args: - pin: CH341 GPIO pin number (0-7) + pin: CH341 GPIO pin number (0-5, output-capable) initial_value: Initial pin state Returns: True if successful """ try: - if not (0 <= pin <= 7): - logger.error(f"CH341 pin {pin} out of range (must be 0-7)") + if not (0 <= pin <= 5): + logger.error(f"CH341 output pin {pin} out of range (must be 0-5)") return False if pin in self._pins: @@ -278,14 +317,14 @@ def setup_input_pin(self, pin: int) -> bool: Setup input pin using CH341 GPIO Args: - pin: CH341 GPIO pin number (0-7) + pin: CH341 GPIO pin number (0-15) Returns: True if successful """ try: - if not (0 <= pin <= 7): - logger.error(f"CH341 pin {pin} out of range (must be 0-7)") + if not (0 <= pin <= 15): + logger.error(f"CH341 input pin {pin} out of range (must be 0-15)") return False if pin in self._pins: @@ -310,15 +349,15 @@ def setup_interrupt_pin( to detect pin state changes and call the callback function. Args: - pin: CH341 GPIO pin number (0-7) + pin: CH341 GPIO pin number (0-15) pull_up: Pull-up resistor (ignored, CH341 has internal config) callback: Interrupt callback function Returns: Pin object with interrupt polling enabled, or None on failure """ - if not (0 <= pin <= 7): - logger.error(f"CH341 pin {pin} out of range (must be 0-7)") + if not (0 <= pin <= 15): + logger.error(f"CH341 interrupt pin {pin} out of range (must be 0-15)") return None # Create input pin