mirror of
https://github.com/do6uk/meshcore_multitcp.git
synced 2026-04-21 03:53:36 +00:00
Add files via upload
This commit is contained in:
parent
5dcd1da470
commit
6a277a9842
3 changed files with 489 additions and 0 deletions
17
meshcore-multitcp.service
Normal file
17
meshcore-multitcp.service
Normal file
|
|
@ -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
|
||||
361
meshcore_multitcp.py
Normal file
361
meshcore_multitcp.py
Normal file
|
|
@ -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()
|
||||
111
meshcore_multitcp_packets.py
Normal file
111
meshcore_multitcp_packets.py
Normal file
|
|
@ -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
|
||||
Loading…
Add table
Add a link
Reference in a new issue