Add additional interfaces to AddInterfacePage

This commit is contained in:
rfnx
2025-02-01 01:09:06 -05:00
parent 59eba2ff64
commit adad97e917
6 changed files with 1731 additions and 334 deletions

View File

@@ -329,8 +329,30 @@ class ReticulumMeshChat:
if "interfaces" in self.reticulum.config:
interfaces = self.reticulum.config["interfaces"]
processed_interfaces = {}
for interface_name, interface in interfaces.items():
interface_data = interface.copy()
# handle sub-interfaces for RNodeMultiInterface
if interface_data.get("type") == "RNodeMultiInterface":
sub_interfaces = []
for sub_name, sub_config in interface_data.items():
if sub_name not in {"type", "port", "interface_enabled", "selected_interface_mode",
"configured_bitrate"}:
if isinstance(sub_config, dict):
sub_config["name"] = sub_name
sub_interfaces.append(sub_config)
# add sub-interfaces to the main interface data
interface_data["sub_interfaces"] = sub_interfaces
for sub in sub_interfaces:
del interface_data[sub["name"]]
processed_interfaces[interface_name] = interface_data
return web.json_response({
"interfaces": interfaces,
"interfaces": processed_interfaces,
})
# enable reticulum interface
@@ -443,11 +465,32 @@ class ReticulumMeshChat:
if "enabled" not in interface_details and "interface_enabled" not in interface_details:
interface_details["interface_enabled"] = "true"
# additional AutoInterface options
if interface_type == "AutoInterface":
if data.get("group_id"):
interface_details["group_id"] = data.get("group_id")
if data.get("multicast_address_type"):
interface_details["multicast_address_type"] = data.get("multicast_address_type")
if data.get("devices"):
interface_details["devices"] = data.get("devices")
if data.get("ignored_devices"):
interface_details["ignored_devices"] = data.get("ignored_devices")
if data.get("discovery_scope"):
interface_details["discovery_scope"] = data.get("discovery_scope")
if data.get("discovery_port"):
interface_details["discovery_port"] = data.get("discovery_port")
if data.get("data_port"):
interface_details["data_port"] = data.get("data_port")
# handle tcp client interface
if interface_type == "TCPClientInterface":
interface_target_host = data.get('target_host')
interface_target_port = data.get('target_port')
# optional parameters for kiss_framing and i2p tunnelling
interface_kiss_framing = data.get('kiss_framing')
interface_i2p_tunneled = data.get('i2p_tunneled')
# ensure target host provided
if interface_target_host is None or interface_target_host == "":
@@ -463,6 +506,13 @@ class ReticulumMeshChat:
interface_details["target_host"] = data.get('target_host')
interface_details["target_port"] = data.get('target_port')
interface_details["kiss_framing"] = interface_kiss_framing
interface_details["i2p_tunneled"] = interface_i2p_tunneled
# handle I2P interface
if interface_type == "I2PInterface":
interface_details['connectable'] = "True"
interface_details["peers"] = data.get('peers')
# handle tcp server interface
if interface_type == "TCPServerInterface":
@@ -470,6 +520,9 @@ class ReticulumMeshChat:
interface_listen_ip = data.get('listen_ip')
interface_listen_port = data.get('listen_port')
interface_network_device = data.get('device')
interface_prefer_ipv6 = data.get('prefer_ipv6')
# ensure listen ip provided
if interface_listen_ip is None or interface_listen_ip == "":
return web.json_response({
@@ -485,6 +538,11 @@ class ReticulumMeshChat:
interface_details["listen_ip"] = data.get('listen_ip')
interface_details["listen_port"] = data.get('listen_port')
if interface_network_device is not None and interface_network_device != "":
interface_details["device"] = interface_network_device
if interface_prefer_ipv6 is not None and interface_prefer_ipv6 != "" and interface_prefer_ipv6 != False:
interface_details["prefer_ipv6"] = True
# handle udp interface
if interface_type == "UDPInterface":
@@ -492,6 +550,7 @@ class ReticulumMeshChat:
interface_listen_port = data.get('listen_port')
interface_forward_ip = data.get('forward_ip')
interface_forward_port = data.get('forward_port')
interface_network_device = data.get('device')
# ensure listen ip provided
if interface_listen_ip is None or interface_listen_ip == "":
@@ -522,6 +581,9 @@ class ReticulumMeshChat:
interface_details["forward_ip"] = data.get('forward_ip')
interface_details["forward_port"] = data.get('forward_port')
if interface_network_device is not None and interface_network_device != "":
interface_details["network_device"] = interface_network_device
# handle rnode interface
if interface_type == "RNodeInterface":
@@ -575,6 +637,132 @@ class ReticulumMeshChat:
interface_details["spreadingfactor"] = interface_spreadingfactor
interface_details["codingrate"] = interface_codingrate
# Handle RNode Multi Interface
if interface_type == "RNodeMultiInterface":
interface_port = data.get("port")
sub_interfaces = data.get("sub_interfaces", [])
if not interface_port:
return web.json_response({"message": "Port is required"}, status=422)
if not isinstance(sub_interfaces, list) or not sub_interfaces:
return web.json_response({"message": "At least one sub-interface is required"}, status=422)
interface_details["type"] = interface_type
interface_details["interface_enabled"] = True
interface_details["port"] = interface_port
for idx, sub in enumerate(sub_interfaces):
# validate required fields for each sub-interface
required_subinterface_fields = ["name", "frequency", "bandwidth", "txpower", "spreadingfactor",
"codingrate",
"vport"]
missing_fields = [field for field in required_subinterface_fields if not sub.get(field)]
if missing_fields:
return web.json_response({
"message": f"Sub-interface {idx + 1} is missing required field(s): {', '.join(missing_fields)}"
}, status=422)
sub_interface_name = sub.get("name")
interface_details[sub_interface_name] = {
"interface_enabled": "true",
"frequency": int(sub["frequency"]),
"bandwidth": int(sub["bandwidth"]),
"txpower": int(sub["txpower"]),
"spreadingfactor": int(sub["spreadingfactor"]),
"codingrate": int(sub["codingrate"]),
"vport": int(sub["vport"]),
}
interfaces[interface_name] = interface_details
# Handle Serial, KISS, and AX25KISS
if interface_type == "SerialInterface" or interface_type == "KISSInterface" or interface_type == "AX25KISSInterface":
interface_port = data.get('port')
interface_speed = data.get('speed')
required_fields = {
interface_port: "Port is required",
interface_speed: "Serial speed is required",
}
for field, error_message in required_fields.items():
if field is None or field == "":
return web.json_response({
"message": error_message,
}, status=422)
interface_details["port"] = interface_port
interface_details['connectable'] = "True"
interface_details["type"] = interface_type
interface_details["interface_enabled"] = True
interface_details["speed"] = interface_speed
interface_details["databits"] = data.get('databits')
interface_details['parity'] = data.get('parity')
interface_details['stopbits'] = data.get('stopbits')
# Handle KISS and AX25KISS specific options
if (interface_type == "KISSInterface" or interface_type == "AX25KISSInterface"):
interface_details["preamble"] = data.get('preamble')
interface_details["txtail"] = data.get('txtail')
interface_details['persistence'] = data.get('persistence')
interface_details['slottime'] = data.get('slottime')
interface_details['callsign'] = data.get('callsign')
interface_details['ssid'] = data.get('ssid')
# RNode Airtime limits and station ID
callsign = data.get('callsign')
id_interval = data.get('id_interval')
airtime_limit_long = data.get('airtime_limit_long')
airtime_limit_short = data.get('airtime_limit_short')
if callsign is not None and callsign != "":
interface_details["callsign"] = callsign
if id_interval is not None and id_interval != "":
interface_details["id_interval"] = id_interval
if airtime_limit_long is not None and airtime_limit_long != "":
interface_details["airtime_limit_long"] = airtime_limit_long
if airtime_limit_short is not None and airtime_limit_short != "":
interface_details["airtime_limit_short"] = airtime_limit_short
# Handle Pipe Interface
if interface_type == "PipeInterface":
interface_command = data.get('command')
interface_respawn_delay = data.get('respawn_delay')
required_fields = {
interface_command: "Command is required",
interface_respawn_delay: "Respawn delay is required",
}
for field, error_message in required_fields.items():
if field is None or field == "":
return web.json_response({
"message": error_message,
}, status=422)
interface_details["command"] = interface_command
interface_details["respawn_delay"] = interface_respawn_delay
# Common interface options
inferred_bitrate = data.get('bitrate')
transport_mode = data.get('mode')
network_name = data.get('network_name')
ifac_passphrase = data.get('passphrase')
ifac_size = data.get('ifac_size')
if inferred_bitrate is not None and inferred_bitrate != "":
interface_details["bitrate"] = inferred_bitrate
if transport_mode is not None and transport_mode != "":
interface_details["mode"] = transport_mode
if network_name is not None and network_name != "":
interface_details["network_name"] = network_name
if ifac_passphrase is not None and ifac_passphrase != "":
interface_details["passphrase"] = ifac_passphrase
if ifac_size is not None and ifac_size != "":
interface_details["ifac_size"] = ifac_size
# merge new interface into existing interfaces
interfaces[interface_name] = interface_details
self.reticulum.config["interfaces"] = interfaces
@@ -619,7 +807,19 @@ class ReticulumMeshChat:
for key, value in interface.items():
output.append(f" {key} = {value}")
output.append("")
# Handle sub-interfaces for RNodeMultiInterface
if interface.get("type") == "RNodeMultiInterface":
for sub_name, sub_config in interface.items():
if sub_name in {"type", "port", "interface_enabled"}:
continue
if isinstance(sub_config, dict):
output.append(f" [[[ {sub_name} ]]]")
for sub_key, sub_value in sub_config.items():
output.append(f" {sub_key} = {sub_value}")
output.append("")
return web.Response(
text="\n".join(output),
content_type="text/plain",

View File

File diff suppressed because it is too large Load Diff

View File

@@ -36,6 +36,14 @@
<path d="M252.44,121.34l-48-32A8,8,0,0,0,192,96v24H72V72h33a32,32,0,1,0,0-16H72A16,16,0,0,0,56,72v48H8a8,8,0,0,0,0,16H56v48a16,16,0,0,0,16,16h32v8a16,16,0,0,0,16,16h32a16,16,0,0,0,16-16V176a16,16,0,0,0-16-16H120a16,16,0,0,0-16,16v8H72V136H192v24a8,8,0,0,0,12.44,6.66l48-32a8,8,0,0,0,0-13.32ZM136,48a16,16,0,1,1-16,16A16,16,0,0,1,136,48ZM120,176h32v32H120Zm88-30.95V111l25.58,17Z"></path>
</svg>
<svg v-else-if="iface.type === 'RNodeMultiInterface'" xmlns="http://www.w3.org/2000/svg" fill="currentColor" viewBox="0 0 256 256" class="size-6 dark:text-white"><path d="M128,88a40,40,0,1,0,40,40A40,40,0,0,0,128,88Zm0,64a24,24,0,1,1,24-24A24,24,0,0,1,128,152Zm73.71,7.14a80,80,0,0,1-14.08,22.2,8,8,0,0,1-11.92-10.67,63.95,63.95,0,0,0,0-85.33,8,8,0,1,1,11.92-10.67,80.08,80.08,0,0,1,14.08,84.47ZM69,103.09a64,64,0,0,0,11.26,67.58,8,8,0,0,1-11.92,10.67,79.93,79.93,0,0,1,0-106.67A8,8,0,1,1,80.29,85.34,63.77,63.77,0,0,0,69,103.09ZM248,128a119.58,119.58,0,0,1-34.29,84,8,8,0,1,1-11.42-11.2,103.9,103.9,0,0,0,0-145.56A8,8,0,1,1,213.71,44,119.58,119.58,0,0,1,248,128ZM53.71,200.78A8,8,0,1,1,42.29,212a119.87,119.87,0,0,1,0-168,8,8,0,1,1,11.42,11.2,103.9,103.9,0,0,0,0,145.56Z"></path></svg>
<svg v-else-if="iface.type === 'I2PInterface'" xmlns="http://www.w3.org/2000/svg" fill="currentColor" viewBox="0 0 256 256" class="size-6 dark:text-white"><path d="M72,92A12,12,0,1,1,60,80,12,12,0,0,1,72,92Zm56-12a12,12,0,1,0,12,12A12,12,0,0,0,128,80Zm68,24a12,12,0,1,0-12-12A12,12,0,0,0,196,104ZM60,152a12,12,0,1,0,12,12A12,12,0,0,0,60,152Zm68,0a12,12,0,1,0,12,12A12,12,0,0,0,128,152Zm68,0a12,12,0,1,0,12,12A12,12,0,0,0,196,152Z"></path></svg>
<svg v-else-if="iface.type === 'KISSInterface' || iface.type === 'AX25KISSInterface'" xmlns="http://www.w3.org/2000/svg" fill="currentColor" viewBox="0 0 256 256" class="size-6 dark:text-white"><path d="M104,168a8,8,0,0,1-8,8H64a8,8,0,0,1,0-16H96A8,8,0,0,1,104,168Zm-8-40H64a8,8,0,0,0,0,16H96a8,8,0,0,0,0-16Zm0-32H64a8,8,0,0,0,0,16H96a8,8,0,0,0,0-16ZM232,80V192a16,16,0,0,1-16,16H40a16,16,0,0,1-16-16V72a8,8,0,0,1,5.7-7.66l160-48a8,8,0,0,1,4.6,15.33L86.51,64H216A16,16,0,0,1,232,80ZM216,192V80H40V192H216Zm-16-56a40,40,0,1,1-40-40A40,40,0,0,1,200,136Zm-16,0a24,24,0,1,0-24,24A24,24,0,0,0,184,136Z"></path></svg>
<svg v-else-if="iface.type === 'PipeInterface'" xmlns="http://www.w3.org/2000/svg" fill="currentColor" viewBox="0 0 256 256" class="size-6 dark:text-white"><path d="M128,128a8,8,0,0,1-3,6.25l-40,32a8,8,0,1,1-10-12.5L107.19,128,75,102.25a8,8,0,1,1,10-12.5l40,32A8,8,0,0,1,128,128Zm48,24H136a8,8,0,0,0,0,16h40a8,8,0,0,0,0-16Zm56-96V200a16,16,0,0,1-16,16H40a16,16,0,0,1-16-16V56A16,16,0,0,1,40,40H216A16,16,0,0,1,232,56ZM216,200V56H40V200H216Z"></path></svg>
<svg v-else xmlns="http://www.w3.org/2000/svg" fill="currentColor" viewBox="0 0 256 256" class="size-6 dark:text-white">
<path d="M140,180a12,12,0,1,1-12-12A12,12,0,0,1,140,180ZM128,72c-22.06,0-40,16.15-40,36v4a8,8,0,0,0,16,0v-4c0-11,10.77-20,24-20s24,9,24,20-10.77,20-24,20a8,8,0,0,0-8,8v8a8,8,0,0,0,16,0v-.72c18.24-3.35,32-17.9,32-35.28C168,88.15,150.06,72,128,72Zm104,56A104,104,0,1,1,128,24,104.11,104.11,0,0,1,232,128Zm-16,0a88,88,0,1,0-88,88A88.1,88.1,0,0,0,216,128Z"></path>
</svg>
@@ -186,6 +194,10 @@
<div> Bitrate: {{ formatBitsPerSecond(iface._stats?.bitrate ?? 0) }}</div>
<div> TX: {{ formatBytes(iface._stats?.txb ?? 0) }}</div>
<div> RX: {{ formatBytes(iface._stats?.rxb ?? 0) }}</div>
<div v-if="iface.type === 'RNodeInterface'"> Noise Floor: {{
iface._stats?.noise_floor
}} dBm
</div>
<div v-if="iface._stats?.clients"> Clients: {{ iface._stats?.clients }}</div>
</div>

View File

@@ -1,6 +1,6 @@
<template>
<div class="flex flex-col flex-1 overflow-hidden min-w-full sm:min-w-[500px] dark:bg-zinc-950">
<div class="overflow-y-auto p-2 space-y-2">
<div class="h-full p-2 space-y-2">
<!-- warning - keeping orange-500 for warning visibility in both modes -->
<div class="flex bg-orange-500 p-2 text-sm font-semibold leading-6 text-white rounded shadow">

View File

@@ -69,7 +69,7 @@
</div>
<div class="my-auto">Loading {{ nodePageProgress }}%</div>
</div>
<pre v-else v-html="nodePageContent" class="h-full text-wrap"></pre>
<pre v-else v-html="nodePageContent" class="h-full break-words whitespace-pre-wrap"></pre>
</div>
<!-- file download bottom bar -->

View File

@@ -143,6 +143,10 @@ class Utils {
static isInterfaceEnabled(iface) {
const rawValue = iface.enabled ?? iface.interface_enabled;
if (rawValue === true) {
// when enabled or interface_enabled is True it needs to return true.
return true;
}
const value = rawValue?.toLowerCase();
return value === "on" || value === "yes" || value === "true";
}