Compare commits

...

22 Commits

Author SHA1 Message Date
Mark Qvist a63dd67a07 Updated changelog 2025-11-19 15:40:03 +01:00
Mark Qvist b27f9836ae Updated docs and manual 2025-11-19 15:39:30 +01:00
Mark Qvist 9504c5b863 Updated docs 2025-11-19 15:37:24 +01:00
Mark Qvist 9767b3453e Updated docs 2025-11-19 15:23:35 +01:00
Mark Qvist 643fbbbc84 Fixed broken links. Thanks @symbioquine in #999. 2025-11-19 15:21:55 +01:00
Mark Qvist 2a5bcd5f52 Updated docs 2025-11-19 15:16:46 +01:00
Mark Qvist 237c3160eb Updated RNode TCP read timeouts 2025-11-19 14:37:47 +01:00
Mark Qvist fcdcf1a2a8 RNode TCP connection on Android 2025-11-18 12:27:36 +01:00
Mark Qvist 7c99aca1d0 Improved reconnect/hotplug reliability and responsiveness for RNodes connected over WiFi 2025-11-18 03:12:42 +01:00
Mark Qvist 309f1999e7 Cleanup 2025-11-17 21:13:21 +01:00
Mark Qvist fa6de7ff79 Updated docs 2025-11-17 19:00:13 +01:00
Mark Qvist 47dfcab170 Added ability to configure RNode IP settings to rnodeconf 2025-11-17 18:46:36 +01:00
Mark Qvist 8abd19800f Updated version 2025-11-17 17:16:29 +01:00
Mark Qvist b2d6ed733d Added support for configuring RNode WiFi settings to rnodeconf 2025-11-17 17:16:06 +01:00
Mark Qvist 1179757893 Added support for connecting RNode devices over TCP connections 2025-11-17 00:31:37 +01:00
Mark Qvist d328ef5ce0 Cleanup 2025-11-15 14:20:43 +01:00
Mark Qvist f577d3018f Updated manual 2025-11-15 13:43:36 +01:00
Mark Qvist e6db629915 Updated manual 2025-11-15 13:42:14 +01:00
Mark Qvist acaab30b91 Updated manual 2025-11-15 13:35:00 +01:00
Mark Qvist 76cedeed07 Updated BLE connection read timeouts on Android 2025-11-15 12:11:45 +01:00
Mark Qvist 5beea74eb3 Handle serial port never being opened due to failure on interface detach for RNodeInterface 2025-11-11 10:29:06 +01:00
Mark Qvist 1f91a8f6f2 Updated manual 2025-11-10 19:05:36 +01:00
34 changed files with 977 additions and 220 deletions
+57
View File
@@ -1,3 +1,60 @@
### 2025-11-19: RNS 1.0.3
This release includes updates to RNode BLE reliability, and adds support for connecting RNodes to a host over WiFi and Ethernet.
**Changes**
- Added support for connecting RNode devices over WiFi and Ethernet
- Added support for configuring RNode WiFi and IP settings to `rnodeconf`
- Updated BLE connection read timeouts on Android, fixes intermittent BLE connection resets in areas with high 2.4GHz spectrum utilization
- Added handling for edge case where RNode serial port was never opened due to failure on interface detach
- Fixed broken links in documentation
**Release Hashes**
```
6bafde4c838ad778bf6878967e84c798e34d6ca621b255f59a60f38cb04ac138 dist/rns-1.0.3-py3-none-any.whl
f277899f95c1189c6bf3beb40ac656c8b36dfd3d7e4cfb2bc3b4a1e6dc3484fa dist/rnspure-1.0.3-py3-none-any.whl
```
### 2025-11-10: RNS 1.0.2
This maintenance release adds support for high-power RNodes with a LoRa PA and/or LNA.
**Changes**
- Added support for RNodes with a PA and/or LNA
- Added support for monitoring RNode CPU temperature via `rnodeconf`
**Release Hashes**
```
723bcf0a839025060ff680c4202b09fa766b35093a4a08506bb85485b8a1f154 rns-1.0.2-py3-none-any.whl
b02de8aeb1381ed2610f27f78799bab031367ed7bf500951fb8d5c2542d4a409 rnspure-1.0.2-py3-none-any.whl
```
### 2025-11-02: RNS 1.0.1
This release brings a number of bugfixes, as well as stability and reliability improvements. It also adds support for using Weave devices as Reticulum interfaces, fixes long-standing Bluetooth Low Energy connection issues on Android, and includes several API and usability improvements.
**Changes**
- Added path response signalling to announce handler API
- Added interface module for Weave devices
- Added support for connecting to Weave devices over serial/USB on Android
- Added support for allow files to `rnx`
- Added detection and logging of multicast echoes never arriving on AutoInterface system devices.
- Added Heltec v4 support to `rnodeconf`
- Implemented handler for ensuring dynamic destination app data can be generated and sent even on first system-internal discovery announce
- Updated documentation and manual
- Improved `AutoInterface` peering timing
- Fixed RNodeInterface Bluetooth Low Energy connection hangs on Android
- Fixed RNodeInterface Bluetooth Low Energy MTU not being configured correctly on Android
- Fixed command byte collision in RNodeInterface and RNodeMultiInterface
- Fixed string formatting for Android log output
- Updated output formatting for `rnid`
**Release Hashes**
```
aa77b4c8e1b6899117666e1e55b05b3250416ab5fea2826254358ae320e8b3ed rns-1.0.1-py3-none-any.whl
b3ddfa0b533631d9f1213043a0282952ae6e9f72c3072bbca053ac48e0483f7e rnspure-1.0.1-py3-none-any.whl
```
### 2025-07-14: RNS 1.0.0
We're out of beta. Thanks to **everyone** who helped make it this far.
+230 -46
View File
@@ -32,6 +32,7 @@ from RNS.Interfaces.Interface import Interface
from time import sleep
import sys
import threading
import socket
import time
import math
import RNS
@@ -364,7 +365,9 @@ class RNodeInterface(Interface):
target_device_address = c["target_device_address"] if "target_device_address" in c else None
ble_name = c["ble_name"] if "ble_name" in c else None
ble_addr = c["ble_addr"] if "ble_addr" in c else None
tcp_host = c["tcp_host"] if "tcp_host" in c else None
force_ble = c["force_ble"] if "force_ble" in c else False
force_tcp = c["force_tcp"] if "force_tcp" in c else False
frequency = int(c["frequency"]) if "frequency" in c else 0
bandwidth = int(c["bandwidth"]) if "bandwidth" in c else 0
txpower = int(c["txpower"]) if "txpower" in c else 0
@@ -436,6 +439,14 @@ class RNodeInterface(Interface):
self.ble_rx_queue= b""
self.ble_tx_queue= b""
self.tcp = None
self.use_tcp = False
self.tcp_host = tcp_host
self.tcp_rx_queue= b""
self.tcp_tx_queue= b""
self.tcp_rx_lock = threading.Lock()
self.tcp_tx_lock = threading.Lock()
self.frequency = frequency
self.bandwidth = bandwidth
self.txpower = txpower
@@ -511,8 +522,8 @@ class RNodeInterface(Interface):
self.port_io_timeout = RNodeInterface.PORT_IO_TIMEOUT
self.last_imagedata = None
if force_ble or self.ble_addr != None or self.ble_name != None:
self.use_ble = True
if force_ble or self.ble_addr != None or self.ble_name != None: self.use_ble = True
if force_tcp or self.tcp_host != None: self.use_tcp = True
self.validcfg = True
if (self.frequency < RNodeInterface.FREQ_MIN or self.frequency > RNodeInterface.FREQ_MAX):
@@ -562,10 +573,8 @@ class RNodeInterface(Interface):
self.open_port()
if self.serial != None:
if self.serial.is_open:
self.configure_device()
else:
raise IOError("Could not open serial port")
if self.serial.is_open: self.configure_device()
else: raise IOError("Could not open serial port")
elif self.bt_manager != None:
if self.bt_manager.connected:
self.configure_device()
@@ -583,12 +592,9 @@ class RNodeInterface(Interface):
def read_mux(self, len=None):
if self.serial != None:
return self.serial.read()
elif self.bt_manager != None:
return self.bt_manager.read()
else:
raise IOError("No ports available for reading")
if self.serial != None: return self.serial.read()
elif self.bt_manager != None: return self.bt_manager.read()
else: raise IOError("No ports available for reading")
def write_mux(self, data):
if self.serial != None:
@@ -615,7 +621,7 @@ class RNodeInterface(Interface):
RNS.log(f"New connection instance: "+str(self.ble), RNS.LOG_DEBUG)
def open_port(self):
if not self.use_ble:
if not self.use_ble and not self.use_tcp:
if self.port != None:
RNS.log("Opening serial port "+self.port+"...")
# Get device parameters
@@ -683,7 +689,7 @@ class RNodeInterface(Interface):
if self.bt_manager != None:
self.bt_manager.connect_any_device()
else:
elif self.use_ble:
if self.ble == None:
self.ble = BLEConnection(owner=self, target_name=self.ble_name, target_bt_addr=self.ble_addr)
self.serial = self.ble
@@ -692,6 +698,24 @@ class RNodeInterface(Interface):
while not self.ble.connected and time.time() < open_time + self.ble.CONNECT_TIMEOUT:
time.sleep(1)
elif self.use_tcp:
RNS.log(f"Opening TCP connection for {self}...")
if self.tcp != None and self.tcp.running == False:
self.tcp.close()
self.tcp.cleanup()
self.tcp = None
if self.tcp == None:
self.tcp = TCPConnection(owner=self, target_host=self.tcp_host)
self.serial = self.tcp
open_time = time.time()
while not self.tcp.connected and time.time() < open_time + self.tcp.CONNECT_TIMEOUT:
time.sleep(1)
else:
raise TypeError("No valid device connection type defined for RNode interface")
def configure_device(self):
self.resetRadioState()
@@ -699,17 +723,19 @@ class RNodeInterface(Interface):
thread = threading.Thread(target=self.readLoop, daemon=True).start()
self.detect()
if not self.use_ble:
sleep(0.5)
else:
if self.use_tcp:
tcp_detect_timeout = 5.0
detect_time = time.time()
while not self.detected and time.time() < detect_time + tcp_detect_timeout: time.sleep(0.1)
if not self.detected: RNS.log(f"RNode detect timed out over TCP", RNS.LOG_ERROR)
elif self.use_ble:
ble_detect_timeout = 5
detect_time = time.time()
while not self.detected and time.time() < detect_time + ble_detect_timeout:
time.sleep(0.1)
if self.detected:
detect_time = RNS.prettytime(time.time()-detect_time)
else:
RNS.log(f"RNode detect timed out over {self.port}", RNS.LOG_ERROR)
while not self.detected and time.time() < detect_time + ble_detect_timeout: time.sleep(0.1)
if self.detected: detect_time = RNS.prettytime(time.time()-detect_time)
else: RNS.log(f"RNode detect timed out over {self.port}", RNS.LOG_ERROR)
else:
sleep(0.2)
if not self.detected:
raise IOError("Could not detect device")
@@ -722,11 +748,19 @@ class RNodeInterface(Interface):
if self.serial != None and self.port != None:
self.timeout = 200
RNS.log("Serial port "+self.port+" is now open")
RNS.log(f"Serial port {self.port} is now open")
if self.bt_manager != None and self.bt_manager.connected:
self.timeout = 1500
RNS.log("Bluetooth connection to RNode now open")
RNS.log(f"Bluetooth connection to RNode now open")
if self.ble != None and self.ble.connected:
self.timeout = 1500
RNS.log(f"BLE connection {self.port} to RNode now open")
if self.tcp != None and self.tcp.connected:
self.timeout = 1500
RNS.log(f"TCP connection tcp://{self.tcp_host} to RNode now open")
RNS.log("Configuring RNode interface...", RNS.LOG_VERBOSE)
self.initRadio()
@@ -978,10 +1012,8 @@ class RNodeInterface(Interface):
def validateRadioState(self):
RNS.log("Waiting for radio configuration validation for "+str(self)+"...", RNS.LOG_VERBOSE)
if not self.platform == KISS.PLATFORM_ESP32:
sleep(1.00);
else:
sleep(2.00);
if not self.platform == KISS.PLATFORM_ESP32: sleep(1.00);
else: sleep(2.00);
self.validcfg = True
if (self.r_frequency != None and abs(self.frequency - int(self.r_frequency)) > 100):
@@ -1447,7 +1479,7 @@ class RNodeInterface(Interface):
if got == 0:
time_since_last = int(time.time()*1000) - last_read_ms
if len(data_buffer) > 0 and time_since_last > self.timeout:
RNS.log(str(self)+" serial read timeout in command "+str(command), RNS.LOG_WARNING)
RNS.log(f"{self} device read timeout in command {command} after {RNS.prettytime(self.timeout/1000.0)}", RNS.LOG_WARNING)
data_buffer = b""
in_frame = False
command = KISS.CMD_UNKNOWN
@@ -1458,19 +1490,19 @@ class RNodeInterface(Interface):
if time.time() > self.first_tx + self.id_interval:
RNS.log("Interface "+str(self)+" is transmitting beacon data: "+str(self.id_callsign.decode("utf-8")), RNS.LOG_DEBUG)
self.process_outgoing(self.id_callsign)
if (time.time() - self.last_port_io > self.port_io_timeout):
self.detect()
if (time.time() - self.last_port_io > self.port_io_timeout*3):
raise IOError("Connected port for "+str(self)+" became unresponsive")
if self.bt_manager != None:
sleep(0.08)
if self.use_tcp:
if self.tcp and self.tcp.connected:
if time.time() > self.tcp.last_write + self.tcp.ACTIVITY_KEEPALIVE:
self.detect()
if (time.time() - self.last_port_io > self.port_io_timeout): self.detect()
if (time.time() - self.last_port_io > self.port_io_timeout*3): raise IOError("Connected port for "+str(self)+" became unresponsive")
if self.bt_manager != None or self.ble != None: sleep(0.08)
except Exception as e:
self.online = False
RNS.log("A serial port occurred, the contained exception was: "+str(e), RNS.LOG_ERROR)
RNS.log("A serial port error occurred, the contained exception was: "+str(e), RNS.LOG_ERROR)
RNS.log("The interface "+str(self)+" experienced an unrecoverable error and is now offline.", RNS.LOG_ERROR)
if RNS.Reticulum.panic_on_interface_error:
@@ -1528,12 +1560,18 @@ class RNodeInterface(Interface):
def detach(self):
self.detached = True
self.disable_external_framebuffer()
self.setRadioState(KISS.RADIO_STATE_OFF)
self.leave()
try:
self.disable_external_framebuffer()
self.setRadioState(KISS.RADIO_STATE_OFF)
self.leave()
if self.use_ble:
self.ble.close()
except Exception as e:
RNS.log(f"An error occurred while detaching {self}: {e}", RNS.LOG_ERROR)
if self.use_ble: self.ble.close()
if self.use_tcp:
time.sleep(0.5)
self.tcp.close()
def should_ingress_limit(self):
return False
@@ -1554,6 +1592,17 @@ class RNodeInterface(Interface):
def get_battery_percent(self):
return self.r_battery_percent
def tcp_receive(self, data):
with self.tcp_rx_lock: self.tcp_rx_queue += data
def tcp_waiting(self): return len(self.tcp_tx_queue) > 0
def get_tcp_waiting(self, n):
with self.tcp_tx_lock:
data = self.tcp_tx_queue[:n]
self.tcp_tx_queue = self.tcp_tx_queue[n:]
return data
def ble_receive(self, data):
with self.ble_rx_lock:
self.ble_rx_queue += data
@@ -1568,7 +1617,7 @@ class RNodeInterface(Interface):
return data
def __str__(self):
return "RNodeInterface["+str(self.name)+"]"
return f"RNodeInterface[{self.name}]"
class BLEConnection(BluetoothDispatcher):
UART_SERVICE_UUID = "6e400001-b5a3-f393-e0a9-e50e24dcca9e"
@@ -1811,4 +1860,139 @@ class BLEConnection(BluetoothDispatcher):
def on_characteristic_changed(self, characteristic):
if characteristic.getUuid().toString() == BLEConnection.UART_TX_CHAR_UUID:
recvd = bytes(characteristic.getValue())
self.owner.ble_receive(recvd)
self.owner.ble_receive(recvd)
class TCPConnection():
TARGET_PORT = 7633
CONNECT_TIMEOUT = 5.0
INITIAL_CONNECT_TIMEOUT = 5.0
RECONNECT_WAIT = 4.0
ACTIVITY_TIMEOUT = 6.0
ACTIVITY_KEEPALIVE = ACTIVITY_TIMEOUT-2.5
TCP_USER_TIMEOUT = 24
TCP_PROBE_AFTER = 5
TCP_PROBE_INTERVAL = 2
TCP_PROBES = 12
@property
def is_open(self):
return self.connected
@property
def in_waiting(self):
buflen = len(self.owner.tcp_rx_queue)
return buflen > 0
def write(self, data_bytes):
if self.connected and self.socket:
with self.owner.tcp_tx_lock:
if len(self.owner.tcp_tx_queue) > 0:
self.socket.sendall(self.owner.tcp_tx_queue)
self.owner.tcp_tx_queue = b""
self.socket.sendall(data_bytes)
self.last_write = time.time()
else:
with self.owner.tcp_tx_lock: self.owner.tcp_tx_queue += data_bytes
return len(data_bytes)
def read(self, n=4096):
with self.owner.tcp_rx_lock:
data = self.owner.tcp_rx_queue[:n]
self.owner.tcp_rx_queue = self.owner.tcp_rx_queue[n:]
return data
def close(self):
if self.connected:
RNS.log(f"Disconnecting TCP socket for {self.owner}", RNS.LOG_DEBUG)
self.must_disconnect = True
if self.socket: self.socket.close()
def __init__(self, owner=None, target_host=None):
self.owner = owner
self.target_host = target_host
self.connected = False
self.reconnecting = False
self.running = False
self.should_run = False
self.must_disconnect = False
self.connect_job_running = False
self.last_write = time.time()
self.should_run = True
self.connection_thread = threading.Thread(target=self.initial_connect, daemon=True).start()
def set_timeouts_linux(self):
self.socket.setsockopt(socket.IPPROTO_TCP, socket.TCP_USER_TIMEOUT, int(self.TCP_USER_TIMEOUT * 1000))
self.socket.setsockopt(socket.SOL_SOCKET, socket.SO_KEEPALIVE, 1)
self.socket.setsockopt(socket.IPPROTO_TCP, socket.TCP_KEEPIDLE, int(self.TCP_PROBE_AFTER))
self.socket.setsockopt(socket.IPPROTO_TCP, socket.TCP_KEEPINTVL, int(self.TCP_PROBE_INTERVAL))
self.socket.setsockopt(socket.IPPROTO_TCP, socket.TCP_KEEPCNT, int(self.TCP_PROBES))
def set_timeouts_osx(self):
if hasattr(socket, "TCP_KEEPALIVE"): TCP_KEEPIDLE = socket.TCP_KEEPALIVE
else: TCP_KEEPIDLE = 0x10
self.socket.setsockopt(socket.SOL_SOCKET, socket.SO_KEEPALIVE, 1)
self.socket.setsockopt(socket.IPPROTO_TCP, TCP_KEEPIDLE, int(self.TCP_PROBE_AFTER))
def cleanup(self):
try:
if self.socket: self.socket.close()
except Exception as e:
RNS.log(f"Error while disconnecting TCP socket on cleanup for {self.owner}", RNS.LOG_ERROR)
self.should_run = False
def initial_connect(self):
if self.connect(initial=True): threading.Thread(target=self.read_loop, daemon=True).start()
def connect(self, initial=False):
try:
if initial:
RNS.log(f"Establishing TCP connection to device for {self.owner}...", RNS.LOG_DEBUG)
address_info = socket.getaddrinfo(self.target_host, self.TARGET_PORT, proto=socket.IPPROTO_TCP)[0]
address_family = address_info[0]
target_address = address_info[4]
self.socket = socket.socket(address_family, socket.SOCK_STREAM)
self.socket.settimeout(self.INITIAL_CONNECT_TIMEOUT)
self.socket.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1)
self.socket.connect(target_address)
self.socket.settimeout(None)
self.connected = True
self.last_write = time.time()
RNS.log(f"TCP connection to device for {self.owner} established", RNS.LOG_DEBUG)
if RNS.vendor.platformutils.is_linux(): self.set_timeouts_linux()
elif RNS.vendor.platformutils.is_darwin(): self.set_timeouts_osx()
return True
except Exception as e:
if initial:
RNS.log(f"TCP connection to device for {self.owner} could not be established: {e}", RNS.LOG_ERROR)
return False
else: raise e
def read_loop(self):
try:
data_in = b""
while not self.must_disconnect:
if self.socket: data_in = self.socket.recv(4096)
else: data_in = b""
if len(data_in) > 0: self.owner.tcp_receive(data_in)
else:
self.connected = False
RNS.log(f"The TCP socket for {self} was closed", RNS.LOG_WARNING)
break
except Exception as e:
self.connected = False
RNS.log(f"A TCP read error occurred for {self}, the contained exception was: {e}", RNS.LOG_WARNING)
+254 -62
View File
@@ -32,6 +32,7 @@ from RNS.Interfaces.Interface import Interface
from time import sleep
import sys
import threading
import socket
import time
import math
import RNS
@@ -159,8 +160,11 @@ class RNodeInterface(Interface):
lt_alock = float(c["airtime_limit_long"]) if "airtime_limit_long" in c else None
force_ble = False
ble_name = None
ble_addr = None
ble_name = None
ble_addr = None
force_tcp = False
tcp_host = None
port = c["port"] if "port" in c else None
@@ -168,6 +172,7 @@ class RNodeInterface(Interface):
raise ValueError("No port specified for RNode interface")
if port != None:
tcp_uri_scheme = "tcp://"
ble_uri_scheme = "ble://"
if port.lower().startswith(ble_uri_scheme):
force_ble = True
@@ -180,6 +185,13 @@ class RNodeInterface(Interface):
else:
ble_name = ble_string
if port.lower().startswith(tcp_uri_scheme):
force_tcp = True
tcp_string = port[len(tcp_uri_scheme):]
port = None
if len(tcp_string) == 0: pass
else: tcp_host = tcp_string
self.HW_MTU = 508
self.pyserial = serial
@@ -196,6 +208,14 @@ class RNodeInterface(Interface):
self.reconnecting= False
self.hw_errors = []
self.use_tcp = False
self.tcp = None
self.tcp_host = tcp_host
self.tcp_rx_queue= b""
self.tcp_tx_queue= b""
self.tcp_rx_lock = threading.Lock()
self.tcp_tx_lock = threading.Lock()
self.use_ble = False
self.ble_name = ble_name
self.ble_addr = ble_addr
@@ -275,8 +295,8 @@ class RNodeInterface(Interface):
self.interface_ready = False
self.announce_rate_target = None
if force_ble or self.ble_addr != None or self.ble_name != None:
self.use_ble = True
if force_ble or self.ble_addr != None or self.ble_name != None: self.use_ble = True
if force_tcp or self.tcp_host != None: self.use_tcp = True
self.validcfg = True
if (self.frequency < RNodeInterface.FREQ_MIN or self.frequency > RNodeInterface.FREQ_MAX):
@@ -325,10 +345,8 @@ class RNodeInterface(Interface):
try:
self.open_port()
if self.serial.is_open:
self.configure_device()
else:
raise IOError("Could not open serial port")
if self.serial.is_open: self.configure_device()
else: raise IOError("Could not open serial port")
except Exception as e:
RNS.log("Could not open serial port for interface "+str(self), RNS.LOG_ERROR)
@@ -341,8 +359,8 @@ class RNodeInterface(Interface):
def open_port(self):
if not self.use_ble:
RNS.log("Opening serial port "+self.port+"...")
if not self.use_ble and not self.use_tcp:
RNS.log(f"Opening serial port {self.port}...")
self.serial = self.pyserial.Serial(
port = self.port,
baudrate = self.speed,
@@ -358,19 +376,37 @@ class RNodeInterface(Interface):
)
else:
RNS.log(f"Opening BLE connection for {self}...")
if self.ble != None and self.ble.running == False:
self.ble.close()
self.ble.cleanup()
self.ble = None
if self.use_ble:
RNS.log(f"Opening BLE connection for {self}...")
self.timeout = 1250
if self.ble != None and self.ble.running == False:
self.ble.close()
self.ble.cleanup()
self.ble = None
if self.ble == None:
self.ble = BLEConnection(owner=self, target_name=self.ble_name, target_bt_addr=self.ble_addr)
self.serial = self.ble
if self.ble == None:
self.ble = BLEConnection(owner=self, target_name=self.ble_name, target_bt_addr=self.ble_addr)
self.serial = self.ble
open_time = time.time()
while not self.ble.connected and time.time() < open_time + self.ble.CONNECT_TIMEOUT:
time.sleep(1)
open_time = time.time()
while not self.ble.connected and time.time() < open_time + self.ble.CONNECT_TIMEOUT:
time.sleep(1)
if self.use_tcp:
self.timeout = 1500
RNS.log(f"Opening TCP connection for {self}...")
if self.tcp != None and self.tcp.running == False:
self.tcp.close()
self.tcp.cleanup()
self.tcp = None
if self.tcp == None:
self.tcp = TCPConnection(owner=self, target_host=self.tcp_host)
self.serial = self.tcp
open_time = time.time()
while not self.tcp.connected and time.time() < open_time + self.tcp.CONNECT_TIMEOUT:
time.sleep(1)
def reset_radio_state(self):
self.r_frequency = None
@@ -391,38 +427,41 @@ class RNodeInterface(Interface):
thread.start()
self.detect()
if not self.use_ble:
sleep(0.2)
else:
ble_detect_timeout = 5
if self.use_tcp:
tcp_detect_timeout = 5.0
detect_time = time.time()
while not self.detected and time.time() < detect_time + ble_detect_timeout:
time.sleep(0.1)
if self.detected:
detect_time = RNS.prettytime(time.time()-detect_time)
else:
RNS.log(f"RNode detect timed out over {self.port}", RNS.LOG_ERROR)
while not self.detected and time.time() < detect_time + tcp_detect_timeout: time.sleep(0.1)
if not self.detected: RNS.log(f"RNode detect timed out over TCP", RNS.LOG_ERROR)
elif self.use_ble:
ble_detect_timeout = 5.0
detect_time = time.time()
while not self.detected and time.time() < detect_time + ble_detect_timeout: time.sleep(0.1)
if not self.detected: RNS.log(f"RNode detect timed out over BLE", RNS.LOG_ERROR)
else:
sleep(0.2)
if not self.detected:
RNS.log("Could not detect device for "+str(self), RNS.LOG_ERROR)
RNS.log(f"Could not detect device for {self}", RNS.LOG_ERROR)
self.serial.close()
else:
if self.platform == KISS.PLATFORM_ESP32 or self.platform == KISS.PLATFORM_NRF52:
self.display = True
if self.platform == KISS.PLATFORM_ESP32 or self.platform == KISS.PLATFORM_NRF52: self.display = True
RNS.log("Serial port "+self.port+" is now open")
RNS.log("Configuring RNode interface...", RNS.LOG_VERBOSE)
self.initRadio()
if (self.validateRadioState()):
self.interface_ready = True
RNS.log(str(self)+" is configured and powered up")
sleep(0.3)
self.online = True
else:
RNS.log("After configuring "+str(self)+", the reported radio parameters did not match your configuration.", RNS.LOG_ERROR)
RNS.log("Make sure that your hardware actually supports the parameters specified in the configuration", RNS.LOG_ERROR)
RNS.log("Aborting RNode startup", RNS.LOG_ERROR)
self.serial.close()
if self.use_tcp: RNS.log(f"TCP connection to {self.tcp_host} is now open", RNS.LOG_VERBOSE)
elif self.use_ble: RNS.log(f"BLE connection to {self} is now open", RNS.LOG_VERBOSE)
else: RNS.log(f"Serial port {self.port} is now open", RNS.LOG_VERBOSE)
RNS.log("Configuring RNode interface...", RNS.LOG_VERBOSE)
self.initRadio()
if (self.validateRadioState()):
self.interface_ready = True
RNS.log(str(self)+" is configured and powered up")
sleep(0.3)
self.online = True
else:
RNS.log("After configuring "+str(self)+", the reported radio parameters did not match your configuration.", RNS.LOG_ERROR)
RNS.log("Make sure that your hardware actually supports the parameters specified in the configuration", RNS.LOG_ERROR)
RNS.log("Aborting RNode startup", RNS.LOG_ERROR)
self.serial.close()
def initRadio(self):
@@ -617,10 +656,9 @@ class RNodeInterface(Interface):
def validateRadioState(self):
RNS.log("Waiting for radio configuration validation for "+str(self)+"...", RNS.LOG_VERBOSE)
if self.use_ble:
sleep(1.00)
else:
sleep(0.25)
if self.use_ble: sleep(1.00)
elif self.use_tcp: sleep(1.5)
else: sleep(0.25)
if self.use_ble and self.ble != None and self.ble.device_disappeared:
RNS.log(f"Device disappeared during radio state validation for {self}", RNS.LOG_ERROR)
@@ -1073,7 +1111,7 @@ class RNodeInterface(Interface):
else:
time_since_last = int(time.time()*1000) - last_read_ms
if len(data_buffer) > 0 and time_since_last > self.timeout:
RNS.log(str(self)+" serial read timeout in command "+str(command), RNS.LOG_WARNING)
RNS.log(f"{self} device read timeout in command {command} after {RNS.prettytime(self.timeout/1000.0)}", RNS.LOG_WARNING)
data_buffer = b""
in_frame = False
command = KISS.CMD_UNKNOWN
@@ -1085,6 +1123,11 @@ class RNodeInterface(Interface):
RNS.log("Interface "+str(self)+" is transmitting beacon data: "+str(self.id_callsign.decode("utf-8")), RNS.LOG_DEBUG)
self.process_outgoing(self.id_callsign)
if self.use_tcp:
if self.tcp and self.tcp.connected:
if time.time() > self.tcp.last_write + self.tcp.ACTIVITY_KEEPALIVE:
self.detect()
sleep(0.08)
except Exception as e:
@@ -1113,23 +1156,28 @@ class RNodeInterface(Interface):
time.sleep(5)
RNS.log("Attempting to reconnect serial port "+str(self.port)+" for "+str(self)+"...", RNS.LOG_VERBOSE)
self.open_port()
if self.serial.is_open:
self.configure_device()
if self.serial.is_open: self.configure_device()
except Exception as e:
RNS.log("Error while reconnecting port, the contained exception was: "+str(e), RNS.LOG_ERROR)
self.reconnecting = False
if self.online:
RNS.log("Reconnected serial port for "+str(self))
if self.online: RNS.log(f"Reconnected port for {self}")
def detach(self):
self.detached = True
self.disable_external_framebuffer()
self.setRadioState(KISS.RADIO_STATE_OFF)
self.leave()
try:
self.disable_external_framebuffer()
self.setRadioState(KISS.RADIO_STATE_OFF)
self.leave()
except Exception as e:
RNS.log(f"An error occurred while detaching {self}: {e}", RNS.LOG_ERROR)
if self.use_ble:
self.ble.close()
if self.use_ble: self.ble.close()
if self.use_tcp:
time.sleep(0.5)
self.tcp.close()
def should_ingress_limit(self):
return False
@@ -1150,6 +1198,17 @@ class RNodeInterface(Interface):
def get_battery_percent(self):
return self.r_battery_percent
def tcp_receive(self, data):
with self.tcp_rx_lock: self.tcp_rx_queue += data
def tcp_waiting(self): return len(self.tcp_tx_queue) > 0
def get_tcp_waiting(self, n):
with self.tcp_tx_lock:
data = self.tcp_tx_queue[:n]
self.tcp_tx_queue = self.tcp_tx_queue[n:]
return data
def ble_receive(self, data):
with self.ble_rx_lock:
self.ble_rx_queue += data
@@ -1164,7 +1223,7 @@ class RNodeInterface(Interface):
return data
def __str__(self):
return "RNodeInterface["+str(self.name)+"]"
return f"RNodeInterface[{self.name}]"
class BLEConnection():
UART_SERVICE_UUID = "6E400001-B5A3-F393-E0A9-E50E24DCCA9E"
@@ -1343,3 +1402,136 @@ class BLEConnection():
RNS.log(f"Error while determining device bond status for {device}, the contained exception was: {e}", RNS.LOG_ERROR)
return False
class TCPConnection():
TARGET_PORT = 7633
CONNECT_TIMEOUT = 5.0
INITIAL_CONNECT_TIMEOUT = 5.0
RECONNECT_WAIT = 4.0
ACTIVITY_TIMEOUT = 6.0
ACTIVITY_KEEPALIVE = ACTIVITY_TIMEOUT-2.5
TCP_USER_TIMEOUT = 24
TCP_PROBE_AFTER = 5
TCP_PROBE_INTERVAL = 2
TCP_PROBES = 12
@property
def is_open(self):
return self.connected
@property
def in_waiting(self):
buflen = len(self.owner.tcp_rx_queue)
return buflen > 0
def write(self, data_bytes):
if self.connected and self.socket:
with self.owner.tcp_tx_lock:
if len(self.owner.tcp_tx_queue) > 0:
self.socket.sendall(self.owner.tcp_tx_queue)
self.owner.tcp_tx_queue = b""
self.socket.sendall(data_bytes)
self.last_write = time.time()
else:
with self.owner.tcp_tx_lock: self.owner.tcp_tx_queue += data_bytes
return len(data_bytes)
def read(self, n):
with self.owner.tcp_rx_lock:
data = self.owner.tcp_rx_queue[:n]
self.owner.tcp_rx_queue = self.owner.tcp_rx_queue[n:]
return data
def close(self):
if self.connected:
RNS.log(f"Disconnecting TCP socket for {self.owner}", RNS.LOG_DEBUG)
self.must_disconnect = True
if self.socket: self.socket.close()
def __init__(self, owner=None, target_host=None):
self.owner = owner
self.target_host = target_host
self.connected = False
self.reconnecting = False
self.running = False
self.should_run = False
self.must_disconnect = False
self.connect_job_running = False
self.last_write = time.time()
self.should_run = True
self.connection_thread = threading.Thread(target=self.initial_connect, daemon=True).start()
def set_timeouts_linux(self):
self.socket.setsockopt(socket.IPPROTO_TCP, socket.TCP_USER_TIMEOUT, int(self.TCP_USER_TIMEOUT * 1000))
self.socket.setsockopt(socket.SOL_SOCKET, socket.SO_KEEPALIVE, 1)
self.socket.setsockopt(socket.IPPROTO_TCP, socket.TCP_KEEPIDLE, int(self.TCP_PROBE_AFTER))
self.socket.setsockopt(socket.IPPROTO_TCP, socket.TCP_KEEPINTVL, int(self.TCP_PROBE_INTERVAL))
self.socket.setsockopt(socket.IPPROTO_TCP, socket.TCP_KEEPCNT, int(self.TCP_PROBES))
def set_timeouts_osx(self):
if hasattr(socket, "TCP_KEEPALIVE"): TCP_KEEPIDLE = socket.TCP_KEEPALIVE
else: TCP_KEEPIDLE = 0x10
self.socket.setsockopt(socket.SOL_SOCKET, socket.SO_KEEPALIVE, 1)
self.socket.setsockopt(socket.IPPROTO_TCP, TCP_KEEPIDLE, int(self.TCP_PROBE_AFTER))
def cleanup(self):
try:
if self.socket: self.socket.close()
except Exception as e: RNS.log(f"Error while disconnecting TCP socket on cleanup for {self.owner}", RNS.LOG_ERROR)
self.should_run = False
def initial_connect(self):
if self.connect(initial=True): threading.Thread(target=self.read_loop, daemon=True).start()
def connect(self, initial=False):
try:
if initial:
RNS.log(f"Establishing TCP connection to device for {self.owner}...", RNS.LOG_DEBUG)
address_info = socket.getaddrinfo(self.target_host, self.TARGET_PORT, proto=socket.IPPROTO_TCP)[0]
address_family = address_info[0]
target_address = address_info[4]
self.socket = socket.socket(address_family, socket.SOCK_STREAM)
self.socket.settimeout(self.INITIAL_CONNECT_TIMEOUT)
self.socket.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1)
self.socket.connect(target_address)
self.socket.settimeout(None)
self.connected = True
self.last_write = time.time()
RNS.log(f"TCP connection to device for {self.owner} established", RNS.LOG_DEBUG)
if RNS.vendor.platformutils.is_linux(): self.set_timeouts_linux()
elif RNS.vendor.platformutils.is_darwin(): self.set_timeouts_osx()
return True
except Exception as e:
if initial:
RNS.log(f"TCP connection to device for {self.owner} could not be established: {e}", RNS.LOG_ERROR)
return False
else: raise e
def read_loop(self):
try:
data_in = b""
while not self.must_disconnect:
if self.socket: data_in = self.socket.recv(4096)
else: data_in = b""
if len(data_in) > 0: self.owner.tcp_receive(data_in)
else:
self.connected = False
RNS.log(f"The TCP socket for {self} was closed", RNS.LOG_WARNING)
break
except Exception as e:
self.connected = False
RNS.log(f"A TCP read error occurred for {self}, the contained exception was: {e}", RNS.LOG_WARNING)
+1 -1
View File
@@ -608,7 +608,7 @@ class Resource:
if sleep_time < 0:
if self.retries_left > 0:
ms = "" if self.outstanding_parts == 1 else "s"
RNS.log("Timed out waiting for "+str(self.outstanding_parts)+" part"+ms+", requesting retry", RNS.LOG_DEBUG)
RNS.log(f"Timed out waiting for {self.outstanding_parts} part{ms}, requesting retry on {self}", RNS.LOG_DEBUG)
if self.window > self.window_min:
self.window -= 1
if self.window_max > self.window_min:
+286 -36
View File
@@ -97,10 +97,17 @@ class KISS():
CMD_BT_CTRL = 0x46
CMD_BT_PIN = 0x62
CMD_DIS_IA = 0x69
CMD_WIFI_MODE = 0x6A
CMD_WIFI_SSID = 0x6B
CMD_WIFI_PSK = 0x6C
CMD_WIFI_CHN = 0x6E
CMD_WIFI_IP = 0x84
CMD_WIFI_NM = 0x85
CMD_BOARD = 0x47
CMD_PLATFORM = 0x48
CMD_MCU = 0x49
CMD_FW_VERSION = 0x50
CMD_CFG_READ = 0x6D
CMD_ROM_READ = 0x51
CMD_ROM_WRITE = 0x52
CMD_ROM_WIPE = 0x59
@@ -245,6 +252,12 @@ class ROM():
ADDR_CONF_PINT = 0xB6
ADDR_CONF_BSET = 0xB7
ADDR_CONF_DIA = 0xB9
ADDR_CONF_WIFI = 0xBA
ADDR_CONF_WCHN = 0xBB
ADDR_CONF_SSID = 0x00
ADDR_CONF_PSK = 0x21
ADDR_CONF_IP = 0x42
ADDR_CONF_NM = 0x46
INFO_LOCK_BYTE = 0x73
CONF_OK_BYTE = 0x73
@@ -402,6 +415,7 @@ class RNode():
self.platform = None
self.mcu = None
self.eeprom = None
self.cfg_sector = None
self.major_version = None
self.minor_version = None
self.version = None
@@ -461,12 +475,17 @@ class RNode():
in_frame = False
data_buffer = b""
command_buffer = b""
elif (in_frame and byte == KISS.FEND and command == KISS.CMD_CFG_READ):
self.cfg_sector = data_buffer
in_frame = False
data_buffer = b""
command_buffer = b""
elif (byte == KISS.FEND):
in_frame = True
command = KISS.CMD_UNKNOWN
data_buffer = b""
command_buffer = b""
elif (in_frame and len(data_buffer) < 512):
elif (in_frame and len(data_buffer) < 1024):
if (len(data_buffer) == 0 and command == KISS.CMD_UNKNOWN):
command = byte
elif (command == KISS.CMD_ROM_READ):
@@ -480,6 +499,17 @@ class RNode():
byte = KISS.FESC
escape = False
data_buffer = data_buffer+bytes([byte])
elif (command == KISS.CMD_CFG_READ):
if (byte == KISS.FESC):
escape = True
else:
if (escape):
if (byte == KISS.TFEND):
byte = KISS.FEND
if (byte == KISS.TFESC):
byte = KISS.FESC
escape = False
data_buffer = data_buffer+bytes([byte])
elif (command == KISS.CMD_DATA):
if (byte == KISS.FESC):
escape = True
@@ -788,6 +818,92 @@ class RNode():
if written != len(kiss_command):
raise IOError("An IO error occurred while sending firmware update command to device")
def set_wifi_mode(self, mode):
kiss_command = bytes([KISS.FEND])+bytes([KISS.CMD_WIFI_MODE, mode])+bytes([KISS.FEND])
written = self.serial.write(kiss_command)
if written != len(kiss_command):
raise IOError("An IO error occurred while sending wifi mode command to device")
def set_wifi_channel(self, channel):
try: ch = int(channel)
except: raise ValueError("Invalid WiFi channel")
if ch < 1 or ch > 14: raise ValueError("Invalid WiFi channel")
ch_data = bytes([ch])
data = KISS.escape(ch_data)
kiss_command = bytes([KISS.FEND])+bytes([KISS.CMD_WIFI_CHN])+data+bytes([KISS.FEND])
written = self.serial.write(kiss_command)
if written != len(kiss_command):
raise IOError("An IO error occurred while sending wifi channel to device")
def set_wifi_ip(self, ip):
if ip == None: ip_data = bytes([0x00, 0x00, 0x00, 0x00])
else:
ip_data = b""
if not type(ip) == str: raise TypeError("Invalid IP address")
octets = ip.split(".")
if not len(octets) == 4: raise ValueError("Invalid IP address length")
try:
for i in range(0, 4):
octet = int(octets[i])
if octet < 0 or octet > 255: raise ValueError("Invalid IP octet value")
else: ip_data += bytes([octet])
except Exception as e:
raise ValueError(f"Could not decode IP address octet: {e}")
data = KISS.escape(ip_data)
kiss_command = bytes([KISS.FEND])+bytes([KISS.CMD_WIFI_IP])+data+bytes([KISS.FEND])
written = self.serial.write(kiss_command)
if written != len(kiss_command): raise IOError("An IO error occurred while sending wifi IP address to device")
def set_wifi_nm(self, nm):
if nm == None: nm_data = bytes([0x00, 0x00, 0x00, 0x00])
else:
nm_data = b""
if not type(nm) == str: raise TypeError("Invalid IP address")
octets = nm.split(".")
if not len(octets) == 4: raise ValueError("Invalid IP address length")
try:
for i in range(0, 4):
octet = int(octets[i])
if octet < 0 or octet > 255: raise ValueError("Invalid IP octet value")
else: nm_data += bytes([octet])
except Exception as e:
raise ValueError(f"Could not decode IP address octet: {e}")
data = KISS.escape(nm_data)
kiss_command = bytes([KISS.FEND])+bytes([KISS.CMD_WIFI_NM])+data+bytes([KISS.FEND])
written = self.serial.write(kiss_command)
if written != len(kiss_command): raise IOError("An IO error occurred while sending wifi netmask to device")
def set_wifi_ssid(self, ssid):
if ssid == None: data = bytes([0x00])
else:
ssid_data = ssid.encode("utf-8")+bytes([0x00])
if len(ssid_data) < 0 or len(ssid_data) > 33: raise ValueError("Invalid SSID length")
data = KISS.escape(ssid_data)
kiss_command = bytes([KISS.FEND])+bytes([KISS.CMD_WIFI_SSID])+data+bytes([KISS.FEND])
written = self.serial.write(kiss_command)
if written != len(kiss_command):
raise IOError("An IO error occurred while sending wifi SSID to device")
def set_wifi_psk(self, psk):
if psk == None: data = bytes([0x00])
else:
psk_data = psk.encode("utf-8")+bytes([0x00])
if len(psk_data) < 8 or len(psk_data) > 33: raise ValueError("Invalid psk length")
data = KISS.escape(psk_data)
kiss_command = bytes([KISS.FEND])+bytes([KISS.CMD_WIFI_PSK])+data+bytes([KISS.FEND])
written = self.serial.write(kiss_command)
if written != len(kiss_command):
raise IOError("An IO error occurred while sending wifi SSID to device")
def initRadio(self):
self.setFrequency()
self.setBandwidth()
@@ -894,7 +1010,7 @@ class RNode():
kiss_command = bytes([KISS.FEND, KISS.CMD_ROM_READ, 0x00, KISS.FEND])
written = self.serial.write(kiss_command)
if written != len(kiss_command):
raise IOError("An IO error occurred while configuring radio state")
raise IOError("An IO error occurred while downloading EEPROM")
sleep(0.6)
if self.eeprom == None:
@@ -903,6 +1019,15 @@ class RNode():
else:
self.parse_eeprom()
def download_cfg_sector(self):
self.cfg_sector = None
kiss_command = bytes([KISS.FEND, KISS.CMD_CFG_READ, 0x00, KISS.FEND])
written = self.serial.write(kiss_command)
if written != len(kiss_command):
raise IOError("An IO error occurred while downloading config sector")
sleep(0.6)
def parse_eeprom(self):
global squashvw;
try:
@@ -1365,6 +1490,14 @@ def main():
parser.add_argument("-B", "--bluetooth-off", action="store_true", help="Turn device bluetooth off")
parser.add_argument("-p", "--bluetooth-pair", action="store_true", help="Put device into bluetooth pairing mode")
parser.add_argument("-w", "--wifi", action="store", metavar="mode", default=None, help="Set WiFi mode (OFF, AP or STATION)")
parser.add_argument("--channel", action="store", metavar="channel", default=None, help="Set WiFi channel")
parser.add_argument("--ssid", action="store", metavar="ssid", default=None, help="Set WiFi SSID (NONE to delete)")
parser.add_argument("--psk", action="store", metavar="psk", default=None, help="Set WiFi PSK (NONE to delete)")
parser.add_argument("--show-psk", action="store_true", default=False, help="Display stored WiFi PSK")
parser.add_argument("--ip", action="store", metavar="ip", default=None, help="Set static WiFi IP address (NONE for DHCP)")
parser.add_argument("--nm", action="store", metavar="nm", default=None, help="Set static WiFi network mask (NONE for DHCP)")
parser.add_argument("-D", "--display", action="store", metavar="i", type=int, default=None, help="Set display intensity (0-255)")
parser.add_argument("-t", "--timeout", action="store", metavar="s", type=int, default=None, help="Set display timeout in seconds, 0 to disable")
parser.add_argument("-R", "--rotation", action="store", metavar="rotation", type=int, default=None, help="Set display rotation, valid values are 0 through 3")
@@ -3558,17 +3691,14 @@ def main():
graceful_exit()
if args.config:
rnode.download_cfg_sector()
eeprom_reserved = 200
if rnode.platform == ROM.PLATFORM_ESP32:
eeprom_size = 296
elif rnode.platform == ROM.PLATFORM_NRF52:
eeprom_size = 296
else:
eeprom_size = 4096
if rnode.platform == ROM.PLATFORM_ESP32: eeprom_size = 296
elif rnode.platform == ROM.PLATFORM_NRF52: eeprom_size = 296
else: eeprom_size = 4096
eeprom_offset = eeprom_size-eeprom_reserved
def ea(a):
return a+eeprom_offset
def ea(a): return a+eeprom_offset
ec_bt = rnode.eeprom[ROM.ADDR_CONF_BT]
ec_dint = rnode.eeprom[ROM.ADDR_CONF_DINT]
ec_dadr = rnode.eeprom[ROM.ADDR_CONF_DADR]
@@ -3578,40 +3708,89 @@ def main():
ec_pint = rnode.eeprom[ROM.ADDR_CONF_PINT]
ec_bset = rnode.eeprom[ROM.ADDR_CONF_BSET]
ec_dia = rnode.eeprom[ROM.ADDR_CONF_DIA]
ec_wifi = rnode.eeprom[ROM.ADDR_CONF_WIFI]
ec_wchn = rnode.eeprom[ROM.ADDR_CONF_WCHN]
ec_ssid = None
ec_psk = None
ec_ip = None
ec_nm = None
if ec_wchn < 1 or ec_wchn > 14: ec_wchn = 1
if rnode.cfg_sector:
ssid_bytes = b""
for i in range(0, 32):
byte = rnode.cfg_sector[ROM.ADDR_CONF_SSID+i]
if byte == 0xFF: byte = 0x00
if byte == 0x00: break
else: ssid_bytes += bytes([byte])
try: ec_ssid = ssid_bytes.decode("utf-8")
except Exception as e: print(f"Error: Could not decode WiFi SSID read from device")
psk_bytes = b""
for i in range(0, 32):
byte = rnode.cfg_sector[ROM.ADDR_CONF_PSK+i]
if byte == 0xFF: byte = 0x00
if byte == 0x00: break
else: psk_bytes += bytes([byte])
ip_bytes = b""
for i in range(0, 4):
byte = rnode.cfg_sector[ROM.ADDR_CONF_IP+i]
ip_bytes += bytes([byte])
if len(ip_bytes) == 4: ec_ip = f"{int(ip_bytes[0])}.{int(ip_bytes[1])}.{int(ip_bytes[2])}.{int(ip_bytes[3])}"
if ec_ip == "255.255.255.255" or ec_ip == "0.0.0.0": ec_ip = None
nm_bytes = b""
for i in range(0, 4):
byte = rnode.cfg_sector[ROM.ADDR_CONF_NM+i]
nm_bytes += bytes([byte])
if len(nm_bytes) == 4: ec_nm = f"{int(nm_bytes[0])}.{int(nm_bytes[1])}.{int(nm_bytes[2])}.{int(nm_bytes[3])}"
if ec_nm == "255.255.255.255" or ec_nm == "0.0.0.0": ec_nm = None
if ec_wifi == 0x02:
ec_ip = "10.0.0.1"
ec_nm = "255.255.255.0"
try: ec_psk = psk_bytes.decode("utf-8")
except Exception as e: print(f"Error: Could not decode WiFi PSK read from device")
if not args.show_psk and ec_psk: ec_psk = "*"*len(ec_psk)
print("\nDevice configuration:")
if ec_bt == 0x73:
print(f" Bluetooth : Enabled")
else:
print(f" Bluetooth : Disabled")
if ec_dia == 0x00:
print(f" Interference avoidance : Enabled")
else:
print(f" Interference avoidance : Disabled")
if ec_bt == 0x73: print(f" Bluetooth : Enabled")
else: print(f" Bluetooth : Disabled")
if ec_wifi == 0x01: print(f" WiFi : Enabled (Station)")
if ec_wifi == 0x02: print(f" WiFi : Enabled (AP)")
else: print(f" WiFi : Disabled")
if ec_wifi == 0x01 or ec_wifi == 0x02:
if not ec_wchn: print(f" Channel : Unknown")
else: print(f" Channel : {ec_wchn}")
if not ec_ssid: print(f" SSID : Not set")
else: print(f" SSID : {ec_ssid}")
if not ec_psk: print(f" PSK : Not set")
else: print(f" PSK : {ec_psk}")
if not ec_ip: print(f" IP Address : DHCP")
else: print(f" IP Address : {ec_ip}")
if ec_ip and ec_nm: print(f" Network Mask : {ec_nm}")
if ec_dia == 0x00: print(f" Interference avoidance : Enabled")
else: print(f" Interference avoidance : Disabled")
print( f" Display brightness : {ec_dint}")
if ec_dadr == 0xFF:
print(f" Display address : Default")
else:
print(f" Display address : {RNS.hexrep(ec_dadr, delimit=False)}")
if ec_bset == 0x73 and ec_dblk != 0x00:
print(f" Display blanking : {ec_dblk}s")
else:
print(f" Display blanking : Disabled")
if ec_dadr == 0xFF: print(f" Display address : Default")
else: print(f" Display address : {RNS.hexrep(ec_dadr, delimit=False)}")
if ec_bset == 0x73 and ec_dblk != 0x00: print(f" Display blanking : {ec_dblk}s")
else: print(f" Display blanking : Disabled")
if ec_drot != 0xFF:
if ec_drot == 0x00:
rstr = "Landscape"
if ec_drot == 0x01:
rstr = "Portrait"
if ec_drot == 0x02:
rstr = "Landscape 180"
if ec_drot == 0x03:
rstr = "Portrait 180"
if ec_drot == 0x00: rstr = "Landscape"
if ec_drot == 0x01: rstr = "Portrait"
if ec_drot == 0x02: rstr = "Landscape 180"
if ec_drot == 0x03: rstr = "Portrait 180"
print(f" Display rotation : {rstr}")
else:
print(f" Display rotation : Default")
if ec_pset == 0x73:
print(f" Neopixel Intensity : {ec_pint}")
if ec_pset == 0x73: print(f" Neopixel Intensity : {ec_pint}")
print("")
rnode.leave()
graceful_exit()
if args.eeprom_dump:
@@ -3722,6 +3901,77 @@ def main():
input()
rnode.leave()
if args.channel:
try:
RNS.log(f"Setting WiFi channel to {args.channel}")
rnode.set_wifi_channel(args.channel)
except Exception as e:
print(f"Could not set WiFi channel: {e}")
graceful_exit()
if args.ssid:
try:
if args.ssid.lower() == "none":
ssid_str = None
RNS.log(f"Deleting WiFi SSID")
else:
ssid_str = str(args.ssid)
RNS.log(f"Setting WiFi SSID to: {ssid_str}")
rnode.set_wifi_ssid(ssid_str)
except Exception as e:
print(f"Could not set WiFi SSID: {e}")
graceful_exit()
if args.psk:
try:
if args.psk.lower() == "none":
psk_str = None
RNS.log(f"Deleting WiFi PSK")
else:
psk_str = str(args.psk)
RNS.log(f"Setting WiFi PSK")
rnode.set_wifi_psk(psk_str)
except Exception as e:
print(f"Could not set WiFi PSK: {e}")
graceful_exit()
if args.ip:
try:
if args.ip.lower() == "none":
RNS.log(f"Setting WiFi IP to DHCP...")
rnode.set_wifi_ip(None)
else:
RNS.log(f"Setting WiFi static IP to: {args.ip}")
rnode.set_wifi_ip(args.ip)
except Exception as e:
print(f"Could not set WiFi IP: {e}")
graceful_exit()
if args.nm:
try:
if args.nm.lower() == "none":
RNS.log(f"Deleting WiFi static netmask configuration...")
rnode.set_wifi_nm(None)
else:
RNS.log(f"Setting WiFi static netmask to: {args.nm}")
rnode.set_wifi_nm(args.nm)
except Exception as e:
print(f"Could not set WiFi netmask: {e}")
graceful_exit()
if args.wifi:
try:
mode = 0x00
if str(args.wifi).lower().startswith("sta"): mode = 0x01
elif str(args.wifi).lower().startswith("ap"): mode = 0x02
if mode == 0x00: RNS.log(f"Disabling WiFi...")
elif mode == 0x01: RNS.log(f"Setting WiFi to station mode")
elif mode == 0x02: RNS.log(f"Setting WiFi to AP mode")
rnode.set_wifi_mode(mode)
except Exception as e:
print(f"Could not set WiFi mode: {e}")
graceful_exit()
if args.info:
if rnode.provisioned:
timestamp = struct.unpack(">I", rnode.made)[0]
+1 -1
View File
@@ -1 +1 @@
__version__ = "1.0.2"
__version__ = "1.0.3"
Binary file not shown.
Binary file not shown.
+1 -1
View File
@@ -1,4 +1,4 @@
# Sphinx build info version 1
# This file records the configuration used when building these files. When it is not found, a full rebuild will be done.
config: 93ab8dc27b32f2bd5c1ef8e8719ce3a0
config: 42d644626d484631388dca045ac26048
tags: 645f666f9bcd5a90fca523b33c5a78b7
Binary file not shown.

After

Width:  |  Height:  |  Size: 152 KiB

@@ -25,7 +25,7 @@ and install them offline using ``pip``:
.. code:: shell
pip install ./rns-1.0.1-py3-none-any.whl
pip install ./rns-1.0.2-py3-none-any.whl
On platforms that limit user package installation via ``pip``, you may need to manually
allow this using the ``--break-system-packages`` command line flag when installing. This
@@ -149,8 +149,8 @@ MeshChat
^^^^^^^^
The `Reticulum MeshChat <https://github.com/liamcottle/reticulum-meshchat>`_ application
is a user-friendly LXMF client for macOS and Windows, that also includes voice call
functionality, and a range of other interesting functions.
is a user-friendly LXMF client for Linux, macOS and Windows, that also includes a Nomad Network
page browser and other interesting functionality.
.. only:: html
@@ -218,7 +218,7 @@ and :ref:`Interfaces<interfaces-main>` chapters of this manual.
Connecting Reticulum Instances Over the Internet
================================================
Reticulum currently offers two interfaces suitable for connecting instances over the Internet: :ref:`TCP<interfaces-tcps>`
Reticulum currently offers three interfaces suitable for connecting instances over the Internet: :ref:`Backbone<interfaces-backbone>`, :ref:`TCP<interfaces-tcps>`
and :ref:`I2P<interfaces-i2p>`. Each interface offers a different set of features, and Reticulum
users should carefully choose the interface which best suites their needs.
@@ -226,6 +226,10 @@ The ``TCPServerInterface`` allows users to host an instance accessible over TCP/
method is generally faster, lower latency, and more energy efficient than using ``I2PInterface``,
however it also leaks more data about the server host.
The ``BackboneInterface`` is a very fast and efficient interface type available on POSIX operating
systems, designed to handle many hundreds of connections simultaneously with low memory, processing
and I/O overhead. It is fully compatible with the TCP-based interface types.
TCP connections reveal the IP address of both your instance and the server to anyone who can
inspect the connection. Someone could use this information to determine your location or identity. Adversaries
inspecting your packets may be able to record packet metadata like time of transmission and packet size.
+13 -1
View File
@@ -152,7 +152,7 @@ OpenCom XL
""""""""""""""""""""
- **Transceiver ICs** Semtech SX1262 and SX1280 (dual transceiver)
- **Device Platform** nRF52
- **Manufacturer** `RAK Wireless <https://liberatedsystems.co.uk/>`_
- **Manufacturer** `Liberated Embedded Systems <https://liberatedsystems.co.uk/>`_
------------
@@ -240,6 +240,18 @@ Heltec T114
------------
.. image:: graphics/board_heltec32v4.png
:width: 58%
:align: center
Heltec LoRa32 v4.0
""""""""""""""""""
- **Transceiver IC** Semtech SX1262
- **Device Platform** ESP32
- **Manufacturer** `Heltec Automation <https://heltec.org>`_
------------
.. image:: graphics/board_heltec32v30.png
:width: 58%
:align: center
+9
View File
@@ -535,6 +535,15 @@ can be used, and offers full control over LoRa parameters.
# Serial port for the device
port = /dev/ttyUSB0
# You can connect wirelessly to the
# RNode device if it supports WiFi.
# Connect by IP address
# port = tcp://10.0.0.1
# Or, connect by hostname
# port = tcp://rnodef3b9.local
# It is also possible to use BLE devices
# instead of wired serial ports. The
# target RNode must be paired with the
+1 -1
View File
@@ -1,5 +1,5 @@
const DOCUMENTATION_OPTIONS = {
VERSION: '1.0.1',
VERSION: '1.0.3',
LANGUAGE: 'en',
COLLAPSE_INDEX: false,
BUILDER: 'html',
+4 -4
View File
@@ -7,7 +7,7 @@
<link rel="prefetch" href="_static/rns_logo_512.png" as="image">
<!-- Generated with Sphinx 8.2.3 and Furo 2025.09.25.dev1 -->
<title>Code Examples - Reticulum Network Stack 1.0.1 documentation</title>
<title>Code Examples - Reticulum Network Stack 1.0.3 documentation</title>
<link rel="stylesheet" type="text/css" href="_static/pygments.css?v=d111a655" />
<link rel="stylesheet" type="text/css" href="_static/styles/furo.css?v=580074bf" />
<link rel="stylesheet" type="text/css" href="_static/copybutton.css?v=76b2166b" />
@@ -180,7 +180,7 @@
</label>
</div>
<div class="header-center">
<a href="index.html"><div class="brand">Reticulum Network Stack 1.0.1 documentation</div></a>
<a href="index.html"><div class="brand">Reticulum Network Stack 1.0.3 documentation</div></a>
</div>
<div class="header-right">
<div class="theme-toggle-container theme-toggle-header">
@@ -204,7 +204,7 @@
<img class="sidebar-logo" src="_static/rns_logo_512.png" alt="Logo"/>
</div>
<span class="sidebar-brand-text">Reticulum Network Stack 1.0.1 documentation</span>
<span class="sidebar-brand-text">Reticulum Network Stack 1.0.3 documentation</span>
</a><form class="sidebar-search-container" method="get" action="search.html" role="search">
<input class="sidebar-search" placeholder="Search" name="q" aria-label="Search">
@@ -3660,7 +3660,7 @@ will be fully on-par with natively included interfaces, including all supported
</aside>
</div>
</div><script src="_static/documentation_options.js?v=292eb321"></script>
</div><script src="_static/documentation_options.js?v=baaebd52"></script>
<script src="_static/doctools.js?v=9bcbadda"></script>
<script src="_static/sphinx_highlight.js?v=dc90522c"></script>
<script src="_static/scripts/furo.js?v=46bd48cc"></script>
+4 -4
View File
@@ -7,7 +7,7 @@
<link rel="prefetch" href="_static/rns_logo_512.png" as="image">
<!-- Generated with Sphinx 8.2.3 and Furo 2025.09.25.dev1 -->
<title>An Explanation of Reticulum for Human Beings - Reticulum Network Stack 1.0.1 documentation</title>
<title>An Explanation of Reticulum for Human Beings - Reticulum Network Stack 1.0.3 documentation</title>
<link rel="stylesheet" type="text/css" href="_static/pygments.css?v=d111a655" />
<link rel="stylesheet" type="text/css" href="_static/styles/furo.css?v=580074bf" />
<link rel="stylesheet" type="text/css" href="_static/copybutton.css?v=76b2166b" />
@@ -180,7 +180,7 @@
</label>
</div>
<div class="header-center">
<a href="index.html"><div class="brand">Reticulum Network Stack 1.0.1 documentation</div></a>
<a href="index.html"><div class="brand">Reticulum Network Stack 1.0.3 documentation</div></a>
</div>
<div class="header-right">
<div class="theme-toggle-container theme-toggle-header">
@@ -204,7 +204,7 @@
<img class="sidebar-logo" src="_static/rns_logo_512.png" alt="Logo"/>
</div>
<span class="sidebar-brand-text">Reticulum Network Stack 1.0.1 documentation</span>
<span class="sidebar-brand-text">Reticulum Network Stack 1.0.3 documentation</span>
</a><form class="sidebar-search-container" method="get" action="search.html" role="search">
<input class="sidebar-search" placeholder="Search" name="q" aria-label="Search">
@@ -291,7 +291,7 @@
</aside>
</div>
</div><script src="_static/documentation_options.js?v=292eb321"></script>
</div><script src="_static/documentation_options.js?v=baaebd52"></script>
<script src="_static/doctools.js?v=9bcbadda"></script>
<script src="_static/sphinx_highlight.js?v=dc90522c"></script>
<script src="_static/scripts/furo.js?v=46bd48cc"></script>
+4 -4
View File
@@ -5,7 +5,7 @@
<meta name="color-scheme" content="light dark"><link rel="index" title="Index" href="#"><link rel="search" title="Search" href="search.html">
<link rel="prefetch" href="_static/rns_logo_512.png" as="image">
<!-- Generated with Sphinx 8.2.3 and Furo 2025.09.25.dev1 --><title>Index - Reticulum Network Stack 1.0.1 documentation</title>
<!-- Generated with Sphinx 8.2.3 and Furo 2025.09.25.dev1 --><title>Index - Reticulum Network Stack 1.0.3 documentation</title>
<link rel="stylesheet" type="text/css" href="_static/pygments.css?v=d111a655" />
<link rel="stylesheet" type="text/css" href="_static/styles/furo.css?v=580074bf" />
<link rel="stylesheet" type="text/css" href="_static/copybutton.css?v=76b2166b" />
@@ -178,7 +178,7 @@
</label>
</div>
<div class="header-center">
<a href="index.html"><div class="brand">Reticulum Network Stack 1.0.1 documentation</div></a>
<a href="index.html"><div class="brand">Reticulum Network Stack 1.0.3 documentation</div></a>
</div>
<div class="header-right">
<div class="theme-toggle-container theme-toggle-header">
@@ -202,7 +202,7 @@
<img class="sidebar-logo" src="_static/rns_logo_512.png" alt="Logo"/>
</div>
<span class="sidebar-brand-text">Reticulum Network Stack 1.0.1 documentation</span>
<span class="sidebar-brand-text">Reticulum Network Stack 1.0.3 documentation</span>
</a><form class="sidebar-search-container" method="get" action="search.html" role="search">
<input class="sidebar-search" placeholder="Search" name="q" aria-label="Search">
@@ -819,7 +819,7 @@
</aside>
</div>
</div><script src="_static/documentation_options.js?v=292eb321"></script>
</div><script src="_static/documentation_options.js?v=baaebd52"></script>
<script src="_static/doctools.js?v=9bcbadda"></script>
<script src="_static/sphinx_highlight.js?v=dc90522c"></script>
<script src="_static/scripts/furo.js?v=46bd48cc"></script>
+11 -8
View File
@@ -7,7 +7,7 @@
<link rel="prefetch" href="_static/rns_logo_512.png" as="image">
<!-- Generated with Sphinx 8.2.3 and Furo 2025.09.25.dev1 -->
<title>Getting Started Fast - Reticulum Network Stack 1.0.1 documentation</title>
<title>Getting Started Fast - Reticulum Network Stack 1.0.3 documentation</title>
<link rel="stylesheet" type="text/css" href="_static/pygments.css?v=d111a655" />
<link rel="stylesheet" type="text/css" href="_static/styles/furo.css?v=580074bf" />
<link rel="stylesheet" type="text/css" href="_static/copybutton.css?v=76b2166b" />
@@ -180,7 +180,7 @@
</label>
</div>
<div class="header-center">
<a href="index.html"><div class="brand">Reticulum Network Stack 1.0.1 documentation</div></a>
<a href="index.html"><div class="brand">Reticulum Network Stack 1.0.3 documentation</div></a>
</div>
<div class="header-right">
<div class="theme-toggle-container theme-toggle-header">
@@ -204,7 +204,7 @@
<img class="sidebar-logo" src="_static/rns_logo_512.png" alt="Logo"/>
</div>
<span class="sidebar-brand-text">Reticulum Network Stack 1.0.1 documentation</span>
<span class="sidebar-brand-text">Reticulum Network Stack 1.0.3 documentation</span>
</a><form class="sidebar-search-container" method="get" action="search.html" role="search">
<input class="sidebar-search" placeholder="Search" name="q" aria-label="Search">
@@ -274,7 +274,7 @@ of your system with a command like <code class="docutils literal notranslate"><s
<code class="docutils literal notranslate"><span class="pre">sudo</span> <span class="pre">pamac</span> <span class="pre">install</span> <span class="pre">python-pip</span></code> or similar.</p>
<p>You can also dowload the Reticulum release wheels from GitHub, or other release channels,
and install them offline using <code class="docutils literal notranslate"><span class="pre">pip</span></code>:</p>
<div class="highlight-shell notranslate"><div class="highlight"><pre><span></span>pip<span class="w"> </span>install<span class="w"> </span>./rns-1.0.1-py3-none-any.whl
<div class="highlight-shell notranslate"><div class="highlight"><pre><span></span>pip<span class="w"> </span>install<span class="w"> </span>./rns-1.0.2-py3-none-any.whl
</pre></div>
</div>
<p>On platforms that limit user package installation via <code class="docutils literal notranslate"><span class="pre">pip</span></code>, you may need to manually
@@ -372,8 +372,8 @@ the Nomad Network program.</p>
<section id="meshchat">
<h3>MeshChat<a class="headerlink" href="#meshchat" title="Link to this heading"></a></h3>
<p>The <a class="reference external" href="https://github.com/liamcottle/reticulum-meshchat">Reticulum MeshChat</a> application
is a user-friendly LXMF client for macOS and Windows, that also includes voice call
functionality, and a range of other interesting functions.</p>
is a user-friendly LXMF client for Linux, macOS and Windows, that also includes a Nomad Network
page browser and other interesting functionality.</p>
<a class="reference external image-reference" href="_images/meshchat_1.webp"><img alt="_images/meshchat_1.webp" class="align-center" src="_images/meshchat_1.webp" />
</a>
<p>Reticulum MeshChat is of course also compatible with Sideband and Nomad Network, or
@@ -422,12 +422,15 @@ and <a class="reference internal" href="interfaces.html#interfaces-main"><span c
</section>
<section id="connecting-reticulum-instances-over-the-internet">
<h2>Connecting Reticulum Instances Over the Internet<a class="headerlink" href="#connecting-reticulum-instances-over-the-internet" title="Link to this heading"></a></h2>
<p>Reticulum currently offers two interfaces suitable for connecting instances over the Internet: <a class="reference internal" href="interfaces.html#interfaces-tcps"><span class="std std-ref">TCP</span></a>
<p>Reticulum currently offers three interfaces suitable for connecting instances over the Internet: <a class="reference internal" href="interfaces.html#interfaces-backbone"><span class="std std-ref">Backbone</span></a>, <a class="reference internal" href="interfaces.html#interfaces-tcps"><span class="std std-ref">TCP</span></a>
and <a class="reference internal" href="interfaces.html#interfaces-i2p"><span class="std std-ref">I2P</span></a>. Each interface offers a different set of features, and Reticulum
users should carefully choose the interface which best suites their needs.</p>
<p>The <code class="docutils literal notranslate"><span class="pre">TCPServerInterface</span></code> allows users to host an instance accessible over TCP/IP. This
method is generally faster, lower latency, and more energy efficient than using <code class="docutils literal notranslate"><span class="pre">I2PInterface</span></code>,
however it also leaks more data about the server host.</p>
<p>The <code class="docutils literal notranslate"><span class="pre">BackboneInterface</span></code> is a very fast and efficient interface type available on POSIX operating
systems, designed to handle many hundreds of connections simultaneously with low memory, processing
and I/O overhead. It is fully compatible with the TCP-based interface types.</p>
<p>TCP connections reveal the IP address of both your instance and the server to anyone who can
inspect the connection. Someone could use this information to determine your location or identity. Adversaries
inspecting your packets may be able to record packet metadata like time of transmission and packet size.
@@ -1037,7 +1040,7 @@ All other available modules will still be loaded when needed.</p>
</aside>
</div>
</div><script src="_static/documentation_options.js?v=292eb321"></script>
</div><script src="_static/documentation_options.js?v=baaebd52"></script>
<script src="_static/doctools.js?v=9bcbadda"></script>
<script src="_static/sphinx_highlight.js?v=dc90522c"></script>
<script src="_static/scripts/furo.js?v=46bd48cc"></script>
+17 -5
View File
@@ -7,7 +7,7 @@
<link rel="prefetch" href="_static/rns_logo_512.png" as="image">
<!-- Generated with Sphinx 8.2.3 and Furo 2025.09.25.dev1 -->
<title>Communications Hardware - Reticulum Network Stack 1.0.1 documentation</title>
<title>Communications Hardware - Reticulum Network Stack 1.0.3 documentation</title>
<link rel="stylesheet" type="text/css" href="_static/pygments.css?v=d111a655" />
<link rel="stylesheet" type="text/css" href="_static/styles/furo.css?v=580074bf" />
<link rel="stylesheet" type="text/css" href="_static/copybutton.css?v=76b2166b" />
@@ -180,7 +180,7 @@
</label>
</div>
<div class="header-center">
<a href="index.html"><div class="brand">Reticulum Network Stack 1.0.1 documentation</div></a>
<a href="index.html"><div class="brand">Reticulum Network Stack 1.0.3 documentation</div></a>
</div>
<div class="header-right">
<div class="theme-toggle-container theme-toggle-header">
@@ -204,7 +204,7 @@
<img class="sidebar-logo" src="_static/rns_logo_512.png" alt="Logo"/>
</div>
<span class="sidebar-brand-text">Reticulum Network Stack 1.0.1 documentation</span>
<span class="sidebar-brand-text">Reticulum Network Stack 1.0.3 documentation</span>
</a><form class="sidebar-search-container" method="get" action="search.html" role="search">
<input class="sidebar-search" placeholder="Search" name="q" aria-label="Search">
@@ -382,7 +382,7 @@ by the auto-installer.</p>
<ul class="simple">
<li><p><strong>Transceiver ICs</strong> Semtech SX1262 and SX1280 (dual transceiver)</p></li>
<li><p><strong>Device Platform</strong> nRF52</p></li>
<li><p><strong>Manufacturer</strong> <a class="reference external" href="https://liberatedsystems.co.uk/">RAK Wireless</a></p></li>
<li><p><strong>Manufacturer</strong> <a class="reference external" href="https://liberatedsystems.co.uk/">Liberated Embedded Systems</a></p></li>
</ul>
<hr class="docutils" />
<a class="reference internal image-reference" href="_images/board_rnodev2.png"><img alt="_images/board_rnodev2.png" class="align-center" src="_images/board_rnodev2.png" style="width: 68%;" />
@@ -462,6 +462,17 @@ by the auto-installer.</p>
<li><p><strong>Manufacturer</strong> <a class="reference external" href="https://heltec.org">Heltec Automation</a></p></li>
</ul>
<hr class="docutils" />
<a class="reference internal image-reference" href="_images/board_heltec32v4.png"><img alt="_images/board_heltec32v4.png" class="align-center" src="_images/board_heltec32v4.png" style="width: 58%;" />
</a>
</section>
<section id="heltec-lora32-v4-0">
<h4>Heltec LoRa32 v4.0<a class="headerlink" href="#heltec-lora32-v4-0" title="Link to this heading"></a></h4>
<ul class="simple">
<li><p><strong>Transceiver IC</strong> Semtech SX1262</p></li>
<li><p><strong>Device Platform</strong> ESP32</p></li>
<li><p><strong>Manufacturer</strong> <a class="reference external" href="https://heltec.org">Heltec Automation</a></p></li>
</ul>
<hr class="docutils" />
<a class="reference internal image-reference" href="_images/board_heltec32v30.png"><img alt="_images/board_heltec32v30.png" class="align-center" src="_images/board_heltec32v30.png" style="width: 58%;" />
</a>
</section>
@@ -636,6 +647,7 @@ can be used with Reticulum. This includes virtual software modems such as
<li><a class="reference internal" href="#lilygo-t-deck">LilyGO T-Deck</a></li>
<li><a class="reference internal" href="#lilygo-t-echo">LilyGO T-Echo</a></li>
<li><a class="reference internal" href="#heltec-t114">Heltec T114</a></li>
<li><a class="reference internal" href="#heltec-lora32-v4-0">Heltec LoRa32 v4.0</a></li>
<li><a class="reference internal" href="#heltec-lora32-v3-0">Heltec LoRa32 v3.0</a></li>
<li><a class="reference internal" href="#heltec-lora32-v2-0">Heltec LoRa32 v2.0</a></li>
</ul>
@@ -659,7 +671,7 @@ can be used with Reticulum. This includes virtual software modems such as
</aside>
</div>
</div><script src="_static/documentation_options.js?v=292eb321"></script>
</div><script src="_static/documentation_options.js?v=baaebd52"></script>
<script src="_static/doctools.js?v=9bcbadda"></script>
<script src="_static/sphinx_highlight.js?v=dc90522c"></script>
<script src="_static/scripts/furo.js?v=46bd48cc"></script>
+4 -4
View File
@@ -7,7 +7,7 @@
<link rel="prefetch" href="_static/rns_logo_512.png" as="image">
<!-- Generated with Sphinx 8.2.3 and Furo 2025.09.25.dev1 -->
<title>Reticulum Network Stack 1.0.1 documentation</title>
<title>Reticulum Network Stack 1.0.3 documentation</title>
<link rel="stylesheet" type="text/css" href="_static/pygments.css?v=d111a655" />
<link rel="stylesheet" type="text/css" href="_static/styles/furo.css?v=580074bf" />
<link rel="stylesheet" type="text/css" href="_static/copybutton.css?v=76b2166b" />
@@ -180,7 +180,7 @@
</label>
</div>
<div class="header-center">
<a href="#"><div class="brand">Reticulum Network Stack 1.0.1 documentation</div></a>
<a href="#"><div class="brand">Reticulum Network Stack 1.0.3 documentation</div></a>
</div>
<div class="header-right">
<div class="theme-toggle-container theme-toggle-header">
@@ -204,7 +204,7 @@
<img class="sidebar-logo" src="_static/rns_logo_512.png" alt="Logo"/>
</div>
<span class="sidebar-brand-text">Reticulum Network Stack 1.0.1 documentation</span>
<span class="sidebar-brand-text">Reticulum Network Stack 1.0.3 documentation</span>
</a><form class="sidebar-search-container" method="get" action="search.html" role="search">
<input class="sidebar-search" placeholder="Search" name="q" aria-label="Search">
@@ -520,7 +520,7 @@ to participate in the development of Reticulum itself.</p>
</aside>
</div>
</div><script src="_static/documentation_options.js?v=292eb321"></script>
</div><script src="_static/documentation_options.js?v=baaebd52"></script>
<script src="_static/doctools.js?v=9bcbadda"></script>
<script src="_static/sphinx_highlight.js?v=dc90522c"></script>
<script src="_static/scripts/furo.js?v=46bd48cc"></script>
+13 -4
View File
@@ -7,7 +7,7 @@
<link rel="prefetch" href="_static/rns_logo_512.png" as="image">
<!-- Generated with Sphinx 8.2.3 and Furo 2025.09.25.dev1 -->
<title>Configuring Interfaces - Reticulum Network Stack 1.0.1 documentation</title>
<title>Configuring Interfaces - Reticulum Network Stack 1.0.3 documentation</title>
<link rel="stylesheet" type="text/css" href="_static/pygments.css?v=d111a655" />
<link rel="stylesheet" type="text/css" href="_static/styles/furo.css?v=580074bf" />
<link rel="stylesheet" type="text/css" href="_static/copybutton.css?v=76b2166b" />
@@ -180,7 +180,7 @@
</label>
</div>
<div class="header-center">
<a href="index.html"><div class="brand">Reticulum Network Stack 1.0.1 documentation</div></a>
<a href="index.html"><div class="brand">Reticulum Network Stack 1.0.3 documentation</div></a>
</div>
<div class="header-right">
<div class="theme-toggle-container theme-toggle-header">
@@ -204,7 +204,7 @@
<img class="sidebar-logo" src="_static/rns_logo_512.png" alt="Logo"/>
</div>
<span class="sidebar-brand-text">Reticulum Network Stack 1.0.1 documentation</span>
<span class="sidebar-brand-text">Reticulum Network Stack 1.0.3 documentation</span>
</a><form class="sidebar-search-container" method="get" action="search.html" role="search">
<input class="sidebar-search" placeholder="Search" name="q" aria-label="Search">
@@ -720,6 +720,15 @@ relevant regulation for your location, and to make decisions accordingly.</p>
<span class="w"> </span><span class="c1"># Serial port for the device</span>
<span class="w"> </span><span class="na">port</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s">/dev/ttyUSB0</span>
<span class="w"> </span><span class="c1"># You can connect wirelessly to the</span>
<span class="w"> </span><span class="c1"># RNode device if it supports WiFi.</span>
<span class="w"> </span><span class="c1"># Connect by IP address</span>
<span class="w"> </span><span class="c1"># port = tcp://10.0.0.1</span>
<span class="w"> </span><span class="c1"># Or, connect by hostname</span>
<span class="w"> </span><span class="c1"># port = tcp://rnodef3b9.local</span>
<span class="w"> </span><span class="c1"># It is also possible to use BLE devices</span>
<span class="w"> </span><span class="c1"># instead of wired serial ports. The</span>
<span class="w"> </span><span class="c1"># target RNode must be paired with the</span>
@@ -1474,7 +1483,7 @@ to <code class="docutils literal notranslate"><span class="pre">30</span></code>
</aside>
</div>
</div><script src="_static/documentation_options.js?v=292eb321"></script>
</div><script src="_static/documentation_options.js?v=baaebd52"></script>
<script src="_static/doctools.js?v=9bcbadda"></script>
<script src="_static/sphinx_highlight.js?v=dc90522c"></script>
<script src="_static/scripts/furo.js?v=46bd48cc"></script>
+4 -4
View File
@@ -7,7 +7,7 @@
<link rel="prefetch" href="_static/rns_logo_512.png" as="image">
<!-- Generated with Sphinx 8.2.3 and Furo 2025.09.25.dev1 -->
<title>Building Networks - Reticulum Network Stack 1.0.1 documentation</title>
<title>Building Networks - Reticulum Network Stack 1.0.3 documentation</title>
<link rel="stylesheet" type="text/css" href="_static/pygments.css?v=d111a655" />
<link rel="stylesheet" type="text/css" href="_static/styles/furo.css?v=580074bf" />
<link rel="stylesheet" type="text/css" href="_static/copybutton.css?v=76b2166b" />
@@ -180,7 +180,7 @@
</label>
</div>
<div class="header-center">
<a href="index.html"><div class="brand">Reticulum Network Stack 1.0.1 documentation</div></a>
<a href="index.html"><div class="brand">Reticulum Network Stack 1.0.3 documentation</div></a>
</div>
<div class="header-right">
<div class="theme-toggle-container theme-toggle-header">
@@ -204,7 +204,7 @@
<img class="sidebar-logo" src="_static/rns_logo_512.png" alt="Logo"/>
</div>
<span class="sidebar-brand-text">Reticulum Network Stack 1.0.1 documentation</span>
<span class="sidebar-brand-text">Reticulum Network Stack 1.0.3 documentation</span>
</a><form class="sidebar-search-container" method="get" action="search.html" role="search">
<input class="sidebar-search" placeholder="Search" name="q" aria-label="Search">
@@ -501,7 +501,7 @@ connected outliers are now an integral part of the network.</p>
</aside>
</div>
</div><script src="_static/documentation_options.js?v=292eb321"></script>
</div><script src="_static/documentation_options.js?v=baaebd52"></script>
<script src="_static/doctools.js?v=9bcbadda"></script>
<script src="_static/sphinx_highlight.js?v=dc90522c"></script>
<script src="_static/scripts/furo.js?v=46bd48cc"></script>
Binary file not shown.
+4 -4
View File
@@ -7,7 +7,7 @@
<link rel="prefetch" href="_static/rns_logo_512.png" as="image">
<!-- Generated with Sphinx 8.2.3 and Furo 2025.09.25.dev1 -->
<title>API Reference - Reticulum Network Stack 1.0.1 documentation</title>
<title>API Reference - Reticulum Network Stack 1.0.3 documentation</title>
<link rel="stylesheet" type="text/css" href="_static/pygments.css?v=d111a655" />
<link rel="stylesheet" type="text/css" href="_static/styles/furo.css?v=580074bf" />
<link rel="stylesheet" type="text/css" href="_static/copybutton.css?v=76b2166b" />
@@ -180,7 +180,7 @@
</label>
</div>
<div class="header-center">
<a href="index.html"><div class="brand">Reticulum Network Stack 1.0.1 documentation</div></a>
<a href="index.html"><div class="brand">Reticulum Network Stack 1.0.3 documentation</div></a>
</div>
<div class="header-right">
<div class="theme-toggle-container theme-toggle-header">
@@ -204,7 +204,7 @@
<img class="sidebar-logo" src="_static/rns_logo_512.png" alt="Logo"/>
</div>
<span class="sidebar-brand-text">Reticulum Network Stack 1.0.1 documentation</span>
<span class="sidebar-brand-text">Reticulum Network Stack 1.0.3 documentation</span>
</a><form class="sidebar-search-container" method="get" action="search.html" role="search">
<input class="sidebar-search" placeholder="Search" name="q" aria-label="Search">
@@ -2385,7 +2385,7 @@ will announce it.</p>
</aside>
</div>
</div><script src="_static/documentation_options.js?v=292eb321"></script>
</div><script src="_static/documentation_options.js?v=baaebd52"></script>
<script src="_static/doctools.js?v=9bcbadda"></script>
<script src="_static/sphinx_highlight.js?v=dc90522c"></script>
<script src="_static/scripts/furo.js?v=46bd48cc"></script>
+4 -4
View File
@@ -8,7 +8,7 @@
<!-- Generated with Sphinx 8.2.3 and Furo 2025.09.25.dev1 -->
<meta name="robots" content="noindex" />
<title>Search - Reticulum Network Stack 1.0.1 documentation</title><link rel="stylesheet" type="text/css" href="_static/pygments.css?v=d111a655" />
<title>Search - Reticulum Network Stack 1.0.3 documentation</title><link rel="stylesheet" type="text/css" href="_static/pygments.css?v=d111a655" />
<link rel="stylesheet" type="text/css" href="_static/styles/furo.css?v=580074bf" />
<link rel="stylesheet" type="text/css" href="_static/copybutton.css?v=76b2166b" />
<link rel="stylesheet" type="text/css" href="_static/styles/furo-extensions.css?v=8dab3a3b" />
@@ -180,7 +180,7 @@
</label>
</div>
<div class="header-center">
<a href="index.html"><div class="brand">Reticulum Network Stack 1.0.1 documentation</div></a>
<a href="index.html"><div class="brand">Reticulum Network Stack 1.0.3 documentation</div></a>
</div>
<div class="header-right">
<div class="theme-toggle-container theme-toggle-header">
@@ -204,7 +204,7 @@
<img class="sidebar-logo" src="_static/rns_logo_512.png" alt="Logo"/>
</div>
<span class="sidebar-brand-text">Reticulum Network Stack 1.0.1 documentation</span>
<span class="sidebar-brand-text">Reticulum Network Stack 1.0.3 documentation</span>
</a><form class="sidebar-search-container" method="get" action="#" role="search">
<input class="sidebar-search" placeholder="Search" name="q" aria-label="Search">
@@ -299,7 +299,7 @@
</aside>
</div>
</div><script src="_static/documentation_options.js?v=292eb321"></script>
</div><script src="_static/documentation_options.js?v=baaebd52"></script>
<script src="_static/doctools.js?v=9bcbadda"></script>
<script src="_static/sphinx_highlight.js?v=dc90522c"></script>
<script src="_static/scripts/furo.js?v=46bd48cc"></script>
File diff suppressed because one or more lines are too long
+4 -4
View File
@@ -7,7 +7,7 @@
<link rel="prefetch" href="_static/rns_logo_512.png" as="image">
<!-- Generated with Sphinx 8.2.3 and Furo 2025.09.25.dev1 -->
<title>Support Reticulum - Reticulum Network Stack 1.0.1 documentation</title>
<title>Support Reticulum - Reticulum Network Stack 1.0.3 documentation</title>
<link rel="stylesheet" type="text/css" href="_static/pygments.css?v=d111a655" />
<link rel="stylesheet" type="text/css" href="_static/styles/furo.css?v=580074bf" />
<link rel="stylesheet" type="text/css" href="_static/copybutton.css?v=76b2166b" />
@@ -180,7 +180,7 @@
</label>
</div>
<div class="header-center">
<a href="index.html"><div class="brand">Reticulum Network Stack 1.0.1 documentation</div></a>
<a href="index.html"><div class="brand">Reticulum Network Stack 1.0.3 documentation</div></a>
</div>
<div class="header-right">
<div class="theme-toggle-container theme-toggle-header">
@@ -204,7 +204,7 @@
<img class="sidebar-logo" src="_static/rns_logo_512.png" alt="Logo"/>
</div>
<span class="sidebar-brand-text">Reticulum Network Stack 1.0.1 documentation</span>
<span class="sidebar-brand-text">Reticulum Network Stack 1.0.3 documentation</span>
</a><form class="sidebar-search-container" method="get" action="search.html" role="search">
<input class="sidebar-search" placeholder="Search" name="q" aria-label="Search">
@@ -368,7 +368,7 @@ report issues, suggest functionality and contribute code to Reticulum.</p>
</aside>
</div>
</div><script src="_static/documentation_options.js?v=292eb321"></script>
</div><script src="_static/documentation_options.js?v=baaebd52"></script>
<script src="_static/doctools.js?v=9bcbadda"></script>
<script src="_static/sphinx_highlight.js?v=dc90522c"></script>
<script src="_static/scripts/furo.js?v=46bd48cc"></script>
+4 -4
View File
@@ -7,7 +7,7 @@
<link rel="prefetch" href="_static/rns_logo_512.png" as="image">
<!-- Generated with Sphinx 8.2.3 and Furo 2025.09.25.dev1 -->
<title>Understanding Reticulum - Reticulum Network Stack 1.0.1 documentation</title>
<title>Understanding Reticulum - Reticulum Network Stack 1.0.3 documentation</title>
<link rel="stylesheet" type="text/css" href="_static/pygments.css?v=d111a655" />
<link rel="stylesheet" type="text/css" href="_static/styles/furo.css?v=580074bf" />
<link rel="stylesheet" type="text/css" href="_static/copybutton.css?v=76b2166b" />
@@ -180,7 +180,7 @@
</label>
</div>
<div class="header-center">
<a href="index.html"><div class="brand">Reticulum Network Stack 1.0.1 documentation</div></a>
<a href="index.html"><div class="brand">Reticulum Network Stack 1.0.3 documentation</div></a>
</div>
<div class="header-right">
<div class="theme-toggle-container theme-toggle-header">
@@ -204,7 +204,7 @@
<img class="sidebar-logo" src="_static/rns_logo_512.png" alt="Logo"/>
</div>
<span class="sidebar-brand-text">Reticulum Network Stack 1.0.1 documentation</span>
<span class="sidebar-brand-text">Reticulum Network Stack 1.0.3 documentation</span>
</a><form class="sidebar-search-container" method="get" action="search.html" role="search">
<input class="sidebar-search" placeholder="Search" name="q" aria-label="Search">
@@ -1248,7 +1248,7 @@ those risks are acceptable to you.</p>
</aside>
</div>
</div><script src="_static/documentation_options.js?v=292eb321"></script>
</div><script src="_static/documentation_options.js?v=baaebd52"></script>
<script src="_static/doctools.js?v=9bcbadda"></script>
<script src="_static/sphinx_highlight.js?v=dc90522c"></script>
<script src="_static/scripts/furo.js?v=46bd48cc"></script>
+4 -4
View File
@@ -7,7 +7,7 @@
<link rel="prefetch" href="_static/rns_logo_512.png" as="image">
<!-- Generated with Sphinx 8.2.3 and Furo 2025.09.25.dev1 -->
<title>Using Reticulum on Your System - Reticulum Network Stack 1.0.1 documentation</title>
<title>Using Reticulum on Your System - Reticulum Network Stack 1.0.3 documentation</title>
<link rel="stylesheet" type="text/css" href="_static/pygments.css?v=d111a655" />
<link rel="stylesheet" type="text/css" href="_static/styles/furo.css?v=580074bf" />
<link rel="stylesheet" type="text/css" href="_static/copybutton.css?v=76b2166b" />
@@ -180,7 +180,7 @@
</label>
</div>
<div class="header-center">
<a href="index.html"><div class="brand">Reticulum Network Stack 1.0.1 documentation</div></a>
<a href="index.html"><div class="brand">Reticulum Network Stack 1.0.3 documentation</div></a>
</div>
<div class="header-right">
<div class="theme-toggle-container theme-toggle-header">
@@ -204,7 +204,7 @@
<img class="sidebar-logo" src="_static/rns_logo_512.png" alt="Logo"/>
</div>
<span class="sidebar-brand-text">Reticulum Network Stack 1.0.1 documentation</span>
<span class="sidebar-brand-text">Reticulum Network Stack 1.0.3 documentation</span>
</a><form class="sidebar-search-container" method="get" action="search.html" role="search">
<input class="sidebar-search" placeholder="Search" name="q" aria-label="Search">
@@ -1191,7 +1191,7 @@ systemctl --user enable rnsd.service
</aside>
</div>
</div><script src="_static/documentation_options.js?v=292eb321"></script>
</div><script src="_static/documentation_options.js?v=baaebd52"></script>
<script src="_static/doctools.js?v=9bcbadda"></script>
<script src="_static/sphinx_highlight.js?v=dc90522c"></script>
<script src="_static/scripts/furo.js?v=46bd48cc"></script>
+4 -4
View File
@@ -7,7 +7,7 @@
<link rel="prefetch" href="_static/rns_logo_512.png" as="image">
<!-- Generated with Sphinx 8.2.3 and Furo 2025.09.25.dev1 -->
<title>What is Reticulum? - Reticulum Network Stack 1.0.1 documentation</title>
<title>What is Reticulum? - Reticulum Network Stack 1.0.3 documentation</title>
<link rel="stylesheet" type="text/css" href="_static/pygments.css?v=d111a655" />
<link rel="stylesheet" type="text/css" href="_static/styles/furo.css?v=580074bf" />
<link rel="stylesheet" type="text/css" href="_static/copybutton.css?v=76b2166b" />
@@ -180,7 +180,7 @@
</label>
</div>
<div class="header-center">
<a href="index.html"><div class="brand">Reticulum Network Stack 1.0.1 documentation</div></a>
<a href="index.html"><div class="brand">Reticulum Network Stack 1.0.3 documentation</div></a>
</div>
<div class="header-right">
<div class="theme-toggle-container theme-toggle-header">
@@ -204,7 +204,7 @@
<img class="sidebar-logo" src="_static/rns_logo_512.png" alt="Logo"/>
</div>
<span class="sidebar-brand-text">Reticulum Network Stack 1.0.1 documentation</span>
<span class="sidebar-brand-text">Reticulum Network Stack 1.0.3 documentation</span>
</a><form class="sidebar-search-container" method="get" action="search.html" role="search">
<input class="sidebar-search" placeholder="Search" name="q" aria-label="Search">
@@ -494,7 +494,7 @@ want to help out with this, or can help sponsor an audit, please do get in touch
</aside>
</div>
</div><script src="_static/documentation_options.js?v=292eb321"></script>
</div><script src="_static/documentation_options.js?v=baaebd52"></script>
<script src="_static/doctools.js?v=9bcbadda"></script>
<script src="_static/sphinx_highlight.js?v=dc90522c"></script>
<script src="_static/scripts/furo.js?v=46bd48cc"></script>
+8 -4
View File
@@ -25,7 +25,7 @@ and install them offline using ``pip``:
.. code:: shell
pip install ./rns-1.0.1-py3-none-any.whl
pip install ./rns-1.0.2-py3-none-any.whl
On platforms that limit user package installation via ``pip``, you may need to manually
allow this using the ``--break-system-packages`` command line flag when installing. This
@@ -149,8 +149,8 @@ MeshChat
^^^^^^^^
The `Reticulum MeshChat <https://github.com/liamcottle/reticulum-meshchat>`_ application
is a user-friendly LXMF client for macOS and Windows, that also includes voice call
functionality, and a range of other interesting functions.
is a user-friendly LXMF client for Linux, macOS and Windows, that also includes a Nomad Network
page browser and other interesting functionality.
.. only:: html
@@ -218,7 +218,7 @@ and :ref:`Interfaces<interfaces-main>` chapters of this manual.
Connecting Reticulum Instances Over the Internet
================================================
Reticulum currently offers two interfaces suitable for connecting instances over the Internet: :ref:`TCP<interfaces-tcps>`
Reticulum currently offers three interfaces suitable for connecting instances over the Internet: :ref:`Backbone<interfaces-backbone>`, :ref:`TCP<interfaces-tcps>`
and :ref:`I2P<interfaces-i2p>`. Each interface offers a different set of features, and Reticulum
users should carefully choose the interface which best suites their needs.
@@ -226,6 +226,10 @@ The ``TCPServerInterface`` allows users to host an instance accessible over TCP/
method is generally faster, lower latency, and more energy efficient than using ``I2PInterface``,
however it also leaks more data about the server host.
The ``BackboneInterface`` is a very fast and efficient interface type available on POSIX operating
systems, designed to handle many hundreds of connections simultaneously with low memory, processing
and I/O overhead. It is fully compatible with the TCP-based interface types.
TCP connections reveal the IP address of both your instance and the server to anyone who can
inspect the connection. Someone could use this information to determine your location or identity. Adversaries
inspecting your packets may be able to record packet metadata like time of transmission and packet size.
Binary file not shown.

After

Width:  |  Height:  |  Size: 152 KiB

+13 -1
View File
@@ -152,7 +152,7 @@ OpenCom XL
""""""""""""""""""""
- **Transceiver ICs** Semtech SX1262 and SX1280 (dual transceiver)
- **Device Platform** nRF52
- **Manufacturer** `RAK Wireless <https://liberatedsystems.co.uk/>`_
- **Manufacturer** `Liberated Embedded Systems <https://liberatedsystems.co.uk/>`_
------------
@@ -240,6 +240,18 @@ Heltec T114
------------
.. image:: graphics/board_heltec32v4.png
:width: 58%
:align: center
Heltec LoRa32 v4.0
""""""""""""""""""
- **Transceiver IC** Semtech SX1262
- **Device Platform** ESP32
- **Manufacturer** `Heltec Automation <https://heltec.org>`_
------------
.. image:: graphics/board_heltec32v30.png
:width: 58%
:align: center
+9
View File
@@ -535,6 +535,15 @@ can be used, and offers full control over LoRa parameters.
# Serial port for the device
port = /dev/ttyUSB0
# You can connect wirelessly to the
# RNode device if it supports WiFi.
# Connect by IP address
# port = tcp://10.0.0.1
# Or, connect by hostname
# port = tcp://rnodef3b9.local
# It is also possible to use BLE devices
# instead of wired serial ports. The
# target RNode must be paired with the