From 6a277a984225a0c4be46b7d5deb6ccd1515714ac Mon Sep 17 00:00:00 2001 From: do6uk <33249819+do6uk@users.noreply.github.com> Date: Mon, 17 Nov 2025 12:04:17 +0100 Subject: [PATCH] Add files via upload --- meshcore-multitcp.service | 17 ++ meshcore_multitcp.py | 361 +++++++++++++++++++++++++++++++++++ meshcore_multitcp_packets.py | 111 +++++++++++ 3 files changed, 489 insertions(+) create mode 100644 meshcore-multitcp.service create mode 100644 meshcore_multitcp.py create mode 100644 meshcore_multitcp_packets.py diff --git a/meshcore-multitcp.service b/meshcore-multitcp.service new file mode 100644 index 0000000..6df125a --- /dev/null +++ b/meshcore-multitcp.service @@ -0,0 +1,17 @@ +[Unit] +Description=meshcore TCP-Multiplexer +After=multi-user.target + +[Service] +Type=simple +User=*youruser* + +WorkingDirectory=/home/*youruser*/meshcore_multitcp/ +ExecStartPre=/bin/sleep 5 +ExecStart=/home/*youruser*/meshcore_multitcp/meshcore_multitcp.py -s *yourip*:5000 -d *deviceip*:5000 + +Restart=always +RestartSec=10 + +[Install] +WantedBy=multi-user.target diff --git a/meshcore_multitcp.py b/meshcore_multitcp.py new file mode 100644 index 0000000..e73985d --- /dev/null +++ b/meshcore_multitcp.py @@ -0,0 +1,361 @@ +#!/usr/bin/env python3 + +""" +(c) 2025 Rainer Fiedler - do6uk +https://github.com/do6uk/meshcore_multitcp + +""" + +import threading +import socket +import os +import sys +import signal + +import argparse +import traceback +import json +import time +from typing import Any, Dict +from meshcore_multitcp_packets import BinaryReqType, PacketType, CmdPacketType + +import logging +FORMAT = '%(asctime)-15s %(levelname)-10s %(message)s' +logging.basicConfig(format=FORMAT, level=logging.INFO) +logger = logging.getLogger(__name__) + +client_host = "0.0.0.0" +client_port = 5000 + +device_host = "192.168.5.62" +device_port = 5000 + +clients = [] +client_names = {} +threads = [] + +stop_device = False +device = None + +#ignore_packets_device = [PacketType.BATTERY.value, PacketType.ADVERTISEMENT.value] +ignore_packets_device = [PacketType.NO_MORE_MSGS.value, PacketType.OK.value, PacketType.BATTERY.value, PacketType.DEVICE_INFO.value, PacketType.LOG_DATA.value] + +#ignore_packets_app = [CmdPacketType.GET_BATT_AND_STORAGE.value] +ignore_packets_app = [] + +app_frame_started = False +app_frame_size = 0 +app_header = b"" +app_inframe = b"" + +device_frame_started = False +device_frame_size = 0 +device_header = b"" +device_inframe = b"" + +contact_nb = 0 +contacts = {} +channels = {} + +def get_ip(sock_handle): + host, port = sock_handle.getpeername() + return host + +def get_ip_port(sock_handle): + host, port = sock_handle.getpeername() + return host+':'+str(port) + +def get_client_name(sock_handle): + global client_names + host, port = sock_handle.getpeername() + + try: + client_name = host+' ('+client_names[host+':'+str(port)]+')' + except: + client_name = host+':'+str(port) + + return client_name + +def ip_to_tuple(ip): + ip, port = ip.split(':') + return (ip, int(port)) + +def handle_app_data(data: bytearray): + if len(data) >= 3: + frame_header = data[:3] + frame_size = int.from_bytes(frame_header[1:], byteorder="little") + frame_data = data[3:] + logger.debug(f"[handle_app_data] header: {frame_header} size: {frame_size} data: {frame_data}") + + packet_type_value = data[3] + if packet_type_value in ignore_packets_app: + logger.debug(f"[handle_app_data] packet {CmdPacketType(packet_type_value).name} ignored") + return False + + if CmdPacketType.exists(packet_type_value): + packet_type = CmdPacketType(packet_type_value).name + logger.debug(f"[handle_app_data] packet {packet_type} found ...") + return packet_type + + elif len(data) == 0: + logger.debug(f"[handle_app_data] connection lost") + return -1 + + else: + logger.error(f"[handle_app_data] header_len_error | data: {data}") + return False + +def handle_device_data(data: bytearray): + if len(data) >= 3: + frame_header = data[:3] + frame_size = int.from_bytes(frame_header[1:], byteorder="little") + frame_data = data[3:] + logger.debug(f"[handle_device_data] header: {frame_header} size: {frame_size} data: {frame_data}") + + packet_type_value = data[3] + logger.debug(f"[parse_device_data] raw-data: {data.hex()}") + + if packet_type_value in ignore_packets_device: + logger.debug(f"[parse_device_data] packet {PacketType(packet_type_value).name} ignored") + return False + + if PacketType.exists(packet_type_value): + packet_type = PacketType(packet_type_value).name + logger.debug(f"[parse_device_data] packet {packet_type} found ...") + return packet_type + + elif len(data) == 0: + logger.debug(f"[handle_device_data] connection lost") + return -1 + else: + logger.error(f"[handle_device_data] header_len_error | data: {data}") + return False + + + +def parse_status(data, pubkey_prefix=None, offset=0): + res = {} + + # Handle pubkey + if pubkey_prefix is None: + # Extract from data (format 1) + res["pubkey_pre"] = data[2:8].hex() + offset = 8 # Fields start at offset 8 + else: + # Use provided prefix (format 2) + res["pubkey_pre"] = pubkey_prefix + # offset stays as provided (typically 0) + + # Parse all fields with the given offset + res["bat"] = int.from_bytes(data[offset:offset+2], byteorder="little") + res["tx_queue_len"] = int.from_bytes(data[offset+2:offset+4], byteorder="little") + res["noise_floor"] = int.from_bytes(data[offset+4:offset+6], byteorder="little", signed=True) + res["last_rssi"] = int.from_bytes(data[offset+6:offset+8], byteorder="little", signed=True) + res["nb_recv"] = int.from_bytes(data[offset+8:offset+12], byteorder="little", signed=False) + res["nb_sent"] = int.from_bytes(data[offset+12:offset+16], byteorder="little", signed=False) + res["airtime"] = int.from_bytes(data[offset+16:offset+20], byteorder="little") + res["uptime"] = int.from_bytes(data[offset+20:offset+24], byteorder="little") + res["sent_flood"] = int.from_bytes(data[offset+24:offset+28], byteorder="little") + res["sent_direct"] = int.from_bytes(data[offset+28:offset+32], byteorder="little") + res["recv_flood"] = int.from_bytes(data[offset+32:offset+36], byteorder="little") + res["recv_direct"] = int.from_bytes(data[offset+36:offset+40], byteorder="little") + res["full_evts"] = int.from_bytes(data[offset+40:offset+42], byteorder="little") + res["last_snr"] = int.from_bytes(data[offset+42:offset+44], byteorder="little", signed=True) / 4 + res["direct_dups"] = int.from_bytes(data[offset+44:offset+46], byteorder="little") + res["flood_dups"] = int.from_bytes(data[offset+46:offset+48], byteorder="little") + res["rx_airtime"] = int.from_bytes(data[offset+48:offset+52], byteorder="little") + + return res + +def device_connect(): + global device + + try: + device = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + device.connect((device_host, device_port)) + return True + + except: + return False + +def device_handle(): + global stop_device, threads, device_host, device_port + + count_retry = 0 + + while not stop_device: + logger.debug(f"DEVICE: connect to {device_host}:{device_port} attemp {count_retry+1} ...") + + count_retry = count_retry + 1 + device_ready = device_connect() + if device_ready: + logger.info(f"DEVICE: ready ...") + count_retry = 0 + + while device_ready: + try: + message = device.recv(1024) + logger.debug(f"DEVICE: receive raw: {message}") + message_type = handle_device_data(message) + + if message_type == -1: + print('device lost') + raise socket.error + + if message_type and message_type != 'NONE': + logger.info(f"DEVICE {message_type}") + + client_forward(message, message_type) + + except socket.error: + logger.error(f"DEVICE: lost connection on read {get_ip(device)}") + device.close() + break + + if count_retry == 3: + stop_device = True + logger.error(f"DEVICE: re-connect failed {count_retry} times ... exit") + os.kill(os.getpid(), signal.SIGINT) + + logger.debug(f"DEVICE: re-connect failed {count_retry} times ... try again") + time.sleep(2*count_retry) + +def device_write(message): + global device, stop_device + + if not stop_device: + try: + logger.debug(f"DEVICE: send raw: {message}") + device.send(message) + return True + + except socket.error: + logger.error(f"DEVICE: lost connection on write {get_ip(device)}") + device.close() + return False + + else: + logger.error(f"DEVICE: closed ... exit") + sys.exit(1) + +def client_forward(message, message_type): + for client in clients: + if message_type == -1: + logger.debug(f"CLIENT {get_client_name(client)} lost connection on Forward") + raise socket.error + elif message_type and message_type != 'NONE': + logger.debug(f"CLIENT {get_client_name(client)} Forward {message_type}") + else: + logger.debug(f"CLIENT {get_client_name(client)} Forward raw-data: {message}") + + try: + client.send(message) + + except socket.error: + logger.error(f"CLIENT {get_client_name(client)} lost connection on write") + if client in clients: + index = clients.index(client) + clients.remove(client) + client.close() + continue + +def client_receive(client): + while True: + try: + message = client.recv(1024) + message_type = handle_app_data(message) + + if message_type == -1: + logger.debug(f"CLIENT {get_client_name(client)} lost connection on send") + raise socket.error + elif message_type: + logger.info(f"CLIENT {get_client_name(client)} Sent {message_type}") + else: + logger.debug(f"CLIENT {get_client_name(client)} Sent raw-data: {message}") + + if message_type == CmdPacketType.APP_START.name: + app_ver = message[4] + app_name = message[11:].decode('utf-8') + client_names[get_ip_port(client)] = app_name + logger.debug(f"CLIENT {get_ip(client)} app_name: {app_name}") + + device_write(message) + + except socket.error: + logger.error(f"CLIENT {get_client_name(device)} lost connection on read") + if client in clients: + index = clients.index(client) + clients.remove(client) + client.close() + break + +def client_connect(): + global clients, client_names, threads + + while True: + try: + client, address = server.accept() + logger.info(f"CLIENT: new connection from {get_ip(client)}") + + clients.append(client) + + thread = threading.Thread(target=client_receive, args=(client,), name="CLIENT "+get_ip(client)) + thread.start() + threads.append(thread) + + except KeyboardInterrupt: + logger.info(f"EXIT after KeyboardInterrupt ...") + stop_device = True + + for t in threads: + logger.debug(f"EXIT: waiting for thread {t.name} to finish ...") + t.join() + + sys.exit() + +def device_state(running_threads): + while True: + #print(running_threads) + + for thread in running_threads: + if thread.name == "DEVICE_HANDLE": + print('device_state',thread.is_alive()) + if not thread.is_alive(): + print('device failed') + os.kill(os.getpid(), signal.SIGINT) + + time.sleep(2) + +parser = argparse.ArgumentParser(description='TCP meshcore proxy') +parser.add_argument('-s', '--server', required=True, help='Server IP and port, i.e.: 127.0.0.1:5000') +parser.add_argument('-d', '--device', required=True, help='Device IP and port, i.e.: 127.0.0.2:5000') +output_group = parser.add_mutually_exclusive_group() +output_group.add_argument('-q', '--quiet', action='store_true', help='minimal logging to CLI') +output_group.add_argument('-v', '--verbose', action='store_true', help='maximum logging to CLI') +args = parser.parse_args() + +client_host, client_port = ip_to_tuple(args.server) +device_host, device_port = ip_to_tuple(args.device) + +if args.quiet: + logger.setLevel(logging.CRITICAL) +if args.verbose: + logger.setLevel(logging.DEBUG) + +logger.info('Welcome') + +if logger.getEffectiveLevel() == logging.DEBUG: + logger.info('LOGLEVEL is set to DEBUG') + +server = socket.socket(socket.AF_INET, socket.SOCK_STREAM) +server.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) +server.bind((client_host,client_port)) +logger.info(f"CLIENT: listening at {client_host}:{client_port} ...") +server.listen() + +device_handle_thread = threading.Thread(target=device_handle, name="DEVICE_HANDLE") +device_handle_thread.start() +threads.append(device_handle_thread) + +logger.debug(f"CLIENT: waiting for clients to connect ...") +client_connect() diff --git a/meshcore_multitcp_packets.py b/meshcore_multitcp_packets.py new file mode 100644 index 0000000..9071611 --- /dev/null +++ b/meshcore_multitcp_packets.py @@ -0,0 +1,111 @@ +""" +based on +https://github.com/meshcore-dev/meshcore_py + +modified by Rainer Fiedler - do6uk +https://github.com/do6uk/meshcore_multitcp + +""" + +from enum import Enum + +class BinaryReqType(Enum): + STATUS = 0x01 + KEEP_ALIVE = 0x02 + TELEMETRY = 0x03 + MMA = 0x04 + ACL = 0x05 + +# Packet prefixes for the protocol +class PacketType(Enum): + OK = 0 + ERROR = 1 + CONTACT_START = 2 + CONTACT = 3 + CONTACT_END = 4 + SELF_INFO = 5 + MSG_SENT = 6 + CONTACT_MSG_RECV = 7 + CHANNEL_MSG_RECV = 8 + CURRENT_TIME = 9 + NO_MORE_MSGS = 10 + CONTACT_URI = 11 + BATTERY = 12 + DEVICE_INFO = 13 + PRIVATE_KEY = 14 + DISABLED = 15 + CONTACT_MSG_RECV_V3 = 16 + CHANNEL_MSG_RECV_V3 = 17 + CHANNEL_INFO = 18 + SIGN_START = 19 + SIGNATURE = 20 + CUSTOM_VARS = 21 + BINARY_REQ = 50 + FACTORY_RESET = 51 + + # Push notifications + ADVERTISEMENT = 0x80 + PATH_UPDATE = 0x81 + ACK = 0x82 + MESSAGES_WAITING = 0x83 + RAW_DATA = 0x84 + LOGIN_SUCCESS = 0x85 + LOGIN_FAILED = 0x86 + STATUS_RESPONSE = 0x87 + LOG_DATA = 0x88 + TRACE_DATA = 0x89 + PUSH_CODE_NEW_ADVERT = 0x8A + TELEMETRY_RESPONSE = 0x8B + BINARY_RESPONSE = 0x8C + PATH_DISCOVERY_RESPONSE = 0x8D + + @classmethod + def exists(cls, val): + try: + return cls(val).name + except: + return False + +class CmdPacketType(Enum): + NONE = 0 + APP_START = 1 + SEND_TXT_MSG = 2 + SEND_CHANNEL_TXT_MSG = 3 + GET_CONTACTS = 4 + GET_DEVICE_TIME = 5 + SET_DEVICE_TIME = 6 + SELF_ADVERT = 7 + SET_ADVERT_NAME = 8 + ADD_UPDATE_CONTACT = 9 + SYNC_NEXT_MESSAGE = 10 + SET_RADIO_PARAMS = 11 + SET_RADIO_TX_POWER = 12 + RESET_PATH = 13 + SET_ADVERT_LATLON = 14 + REMOVE_CONTACT = 15 + SHARE_CONTACT = 16 + EXPORT_CONTACT = 17 + IMPORT_CONTACT = 18 + REBOOT = 19 + GET_BATT_AND_STORAGE = 20 + SET_TUNING_PARAMS = 21 + SEND_RAW_DATA = 25 + SEND_LOGIN = 26 + SEND_STATUS_REQ = 27 + GET_CHANNEL = 31 + SEND_TRACE_PATH = 36 + SET_OTHER_PARAMS = 38 + SEND_TELEMETRY_REQ = 39 + GET_CUSTOM_VARS = 40 + SET_CUSTOM_VAR = 41 + GET_ADVERT_PATH = 42 + GET_TUNING_PARAMS = 43 + SEND_BINARY_REQ = 50 + FACTORY_RESET = 51 + + @classmethod + def exists(cls, val): + try: + return cls(val).name + except: + return False