diff --git a/.vscode/settings.json b/.vscode/settings.json
index 202c404d..7786812c 100644
--- a/.vscode/settings.json
+++ b/.vscode/settings.json
@@ -4,7 +4,8 @@
"GPIO",
"Jetson",
"mjpeg",
- "teleop"
+ "teleop",
+ "VESC"
],
"python.analysis.typeCheckingMode": "off",
"liveServer.settings.port": 5501,
diff --git a/BYODR_utils/common/__init__.py b/BYODR_utils/common/__init__.py
index b9d7d751..789bce1e 100644
--- a/BYODR_utils/common/__init__.py
+++ b/BYODR_utils/common/__init__.py
@@ -174,6 +174,8 @@ def run(self):
class ApplicationExit(object):
+ """Encapsulate the logic for checking if an application should exit and then performing the necessary action to shut down the application. It's designed as a callable object (using the __call__ method) so that it can be used like a function."""
+
def __init__(self, event, cb):
self._event = event
self._cb = cb
diff --git a/BYODR_utils/common/ssh.py b/BYODR_utils/common/ssh.py
index 521c7ef9..fb06c7a0 100644
--- a/BYODR_utils/common/ssh.py
+++ b/BYODR_utils/common/ssh.py
@@ -1,19 +1,22 @@
# TESTED AND WORKING ON
-# Firmware version :RUT9_R_00.07.06.1
-# Firmware build date: 2024-01-02 11:11:13
-# Internal modem firmware version: SLM750_4.0.6_EQ101
-# Kernel version: 5.4.259
+# Firmware version:RUT9_R_00.07.06.11
+# Firmware build date:2024-05-03 07:26:08
+# Internal modem firmware version:EC25EUGAR06A07M4G_01.001.01.001
+# Kernel version:5.4.259
+
+import json
import logging
+import re
import subprocess
import time
import traceback
+from ipaddress import ip_address
import paramiko
+from pythonping import ping
-# Declaring the logger
-logging.basicConfig(format="%(levelname)s: %(asctime)s %(filename)s %(funcName)s %(lineno)d %(message)s", datefmt="%Y-%m-%d %H:%M:%S %p")
logging.getLogger().setLevel(logging.INFO)
logger = logging.getLogger(__name__)
@@ -28,6 +31,10 @@ def __init__(self, ip=None, username="root", password="Modem001", port=22):
self.username = username
self.password = password
self.port = int(port) # Default value for SSH port
+ self.wifi_scanner = self.WifiNetworkScanner(self)
+ self.wifi_connect = self.ConnectToNetwork(self)
+ self.wifi_delete = self.WifiNetworkDeletion(self)
+ self.fetch_ip_from_mac = self.FetchIpFromMac(self)
self.client = None
self.__open_ssh_connection()
@@ -126,3 +133,657 @@ def fetch_ssid(self):
time.sleep(1)
return output
+ def fetch_router_mac(self):
+ output = None
+ while output is None:
+ output = self._execute_ssh_command("ifconfig wlan0 | grep -o -E '([[:xdigit:]]{2}:){5}[[:xdigit:]]{2}'", suppress_error_log=True)
+ if output is None:
+ time.sleep(1)
+ return output
+
+ def fetch_router_password(self):
+ output = None
+ while output is None:
+ output = self._execute_ssh_command("uci get wireless.@wifi-iface[0].key", suppress_error_log=True)
+ if output is None:
+ time.sleep(1)
+ return output
+
+ def fetch_ip_and_mac(self):
+ """Get list of all connected devices to the current segment
+
+ Example
+ >>>connected_devices= fetch_ip_and_mac() \n
+ data = json.loads(connected_devices)
+
+ Access the MAC addresses by specifying the index of each item \n
+ mac_address_type1 = data[0]['MAC'] \n
+ mac_address_type2 = data[1]['MAC']
+ """
+
+ output = self._execute_ssh_command("ip neigh")
+ devices = []
+ for line in output.splitlines():
+ # it looks for a pattern like number.number.number.number
+ # It looks for a pattern of six groups of two hexadecimal digits that are separated by either : or -
+ match = re.search(
+ r"(\d+\.\d+\.\d+\.\d+).+?([0-9A-Fa-f]{2}(?:[:-][0-9A-Fa-f]{2}){5})",
+ line,
+ )
+ if match:
+ ip, mac_address = match.groups()
+
+ # Determine the label based on IP address
+ label = ""
+ # MicroController
+ if ip.endswith(".32"):
+ label = "mc_pi"
+ elif ip.endswith(".64"):
+ label = "front camera"
+ elif ip.endswith(".65"):
+ label = "back camera"
+ elif ip.endswith(".100"):
+ label = "mc_nano"
+
+ device_info = {"label": label, "ip": ip, "mac": mac_address} if label else {"ip": ip, "mac": mac_address}
+
+ devices.append(device_info)
+
+ sorted_devices = sorted(devices, key=lambda x: ip_address(x["ip"]))
+ print("Devices found: ", sorted_devices)
+
+ def change_wifi_visibility(self, desired_state):
+ # should change it from the side of this network only, and not all the wifi interfaces
+ try:
+ # Get current state for the Wifi network
+ ssh_output = self._execute_ssh_command("uci get wireless.default_radio0.hidden").strip()
+
+ # Determine the current state: '0' for discoverable, '1' for hidden
+ is_currently_hidden = ssh_output == "1"
+
+ # Convert desired_state to boolean ('True' or 'False' string to a boolean value)
+ desired_state_bool = desired_state.lower() == "true"
+
+ # Check if the current state is different from the desired state
+ if (is_currently_hidden and desired_state_bool) or (not is_currently_hidden and not desired_state_bool):
+ # If the WiFi is hidden and should be discoverable, or vice versa
+ new_state = "0" if desired_state_bool else "1"
+ self._execute_ssh_command(f"uci set wireless.default_radio0.hidden={new_state}; uci commit wireless; wifi reload")
+
+ new_state_str = "discoverable" if desired_state_bool else "hidden"
+ logger.info(f"Wifi network visibility changed to {new_state_str}")
+ except Exception as e:
+ logger.error(f"Error in changing WiFi visibility: {e}")
+
+ def check_static_route(self):
+ network_config = self._execute_ssh_command(f"cat /etc/config/network")
+ lines = network_config.split("\n")
+ current_section = None
+ for line in lines:
+ if line.startswith(f"config route '1'"):
+ current_section = True
+ elif "option target" in line and current_section:
+ return line.split("'")[1] # Extracting gateway IP
+
+ @staticmethod
+ def check_network_connection(target_ip):
+ response = ping(target_ip, count=6, timeout=1, verbose=False)
+
+ if response.success():
+ # Calculate the total round-trip time of successful responses
+ total_time = sum(resp.time_elapsed for resp in response if resp.success)
+ # Calculate the average round-trip time (in milliseconds)
+ average_time_ms = (total_time / len([resp for resp in response if resp.success])) * 1000
+ logger.info(f"Success: Device at {target_ip} is reachable. Average speed: {average_time_ms:.2f}ms")
+ return True
+ else:
+ return False
+
+ class FetchIpFromMac:
+ def __init__(self, router):
+ self.router = router
+ self.networks = None
+
+ def driver(self, mac_address):
+ wireless_config_path = "/etc/config/wireless"
+ network_config_path = "/etc/config/network"
+
+ try:
+ # Fetch wireless configuration and find the network interface
+ wireless_config = self.router._execute_ssh_command(f"cat {wireless_config_path}")
+ network_interface = self._parse_wireless_config(wireless_config, mac_address)
+ if not network_interface:
+ return False
+
+ # Fetch network configuration and find the gateway IP
+ network_config = self.router._execute_ssh_command(f"cat {network_config_path}")
+ target_ip = self._parse_network_config(network_config, network_interface)
+ if target_ip:
+ # Split the IP and replace the fourth octet with '100'
+ ip_parts = target_ip.split(".")
+ target_nano_ip = ".".join(ip_parts[:3] + ["100"])
+
+ return [target_ip, target_nano_ip]
+
+ except Exception as e:
+ logging.error(f"An error occurred: {e}")
+ return None
+
+ def _parse_wireless_config(self, config, mac_address):
+ lines = config.split("\n")
+ current_section = None
+ for line in lines:
+ if line.startswith("config wifi-iface"):
+ current_section = {}
+ elif "option bssid" in line and mac_address in line:
+ current_section["mac_found"] = True
+ elif "option network" in line and current_section.get("mac_found"):
+ return line.split("'")[1] # option network 'ifWan2' will return (ifWan2)
+ return None
+
+ def _parse_network_config(self, config, network_interface):
+ lines = config.split("\n")
+ current_section = None
+ for line in lines:
+ if line.startswith(f"config interface '{network_interface}'"):
+ current_section = True
+ elif "option gateway" in line and current_section:
+ return line.split("'")[1] # Extracting gateway IP
+ return None
+
+ def get_ip_from_mac(self, mac_address):
+ return self.fetch_ip_from_mac.driver(mac_address)
+
+ class WifiNetworkScanner:
+ def __init__(self, router):
+ self.router = router
+ self.networks = None
+
+ def fetch_wifi_networks(self):
+ """
+ Connects to an SSH server and retrieves a list of available Wi-Fi networks.
+ Parses the output from the 'iwlist wlan0 scan' command to extract network details.
+
+ Returns:
+ list of dict: A list containing information about each network, including (ESSID and MAC).
+ """
+ output = self.router._execute_ssh_command("iwlist wlan0 scan")
+ self.parse_iwlist_output(output)
+ self.filter_teltonika_networks()
+
+ # DEBUGGING
+ # print(json.dumps(self.networks, indent=4)) # Pretty print the JSON
+ # for network in self.networks:
+ # print(network.get("ESSID"), network.get("MAC"), end="\n")
+ return self.networks
+
+ def parse_iwlist_output(self, output):
+ """Parses the output from the 'iwlist wlan0 scan' command.
+ Args:
+ output (str): The raw output string from the 'iwlist wlan0 scan' command.
+
+ Returns:
+ list of dict: A list of dictionaries, each representing a network with information such as (ESSID and MAC).
+ """
+ networks = []
+ current_network = {}
+
+ for line in output.splitlines():
+ if "Cell" in line and "Address" in line:
+ if current_network:
+ networks.append(current_network)
+ current_network = {}
+ current_network["MAC"] = line.split()[-1]
+ elif "ESSID:" in line:
+ current_network["ESSID"] = line.split('"')[1]
+
+ # Reorder the dictionary to show ESSID first
+ ordered_networks = []
+ for network in networks:
+ ordered_network = {k: network[k] for k in ["ESSID", "MAC"] if k in network}
+ ordered_networks.append(ordered_network)
+
+ self.networks = ordered_networks
+
+ def filter_teltonika_networks(self):
+ """
+ Filters the list of networks to include only those with MAC addresses belonging to Teltonika.
+
+ Args:
+ networks (list of dict): List of networks to be filtered.
+
+ Returns:
+ list of dict: Filtered list of networks.
+ """
+ teltonika_prefixes = ["20:97:27", "00:1E:42"]
+ filtered_networks = [network for network in self.networks if any(network["MAC"].startswith(prefix) for prefix in teltonika_prefixes)]
+ self.networks = filtered_networks
+
+ def get_wifi_networks(self):
+ return self.wifi_scanner.fetch_wifi_networks()
+
+ class ConnectToNetwork:
+ """Connect to a network using the SSID and MAC address for it"""
+
+ def __init__(self, router_instance):
+ self.router = router_instance
+ self.target_router_ip = None
+ self.current_router_client_address = None
+
+ def driver(self, network_name, network_mac):
+ """Main driver function that manages the connection process"""
+ start_time = time.time()
+ self.current_router_name = self.router.fetch_ssid()
+ self.target_network_name = network_name
+ self.target_network_mac = network_mac
+ try:
+ # Step 1: Connect current router to target network
+ self.__connect_to_target_network()
+ # Step 2: Update the firewall rules for the current router (before getting third octet)
+ self.__update_firewall_config()
+ # Step 3: Get the IP of the current router after connecting to the target network
+ self.__get_IP_new_network()
+ # Step 4: SSH to the target router and connect to the current router
+ self.__connect_target_router_to_current_router()
+ # Step 5: SSH to the target router and update its firewall
+ self.__update_target_router_firewall()
+
+ # Step 6: Restart services on both routers after making the connection
+ self.__restart_services_on_target_router()
+ self.__restart_services_on_current_router()
+
+ except Exception as e:
+ elapsed_time = time.time() - start_time
+ logger.info(f"Connecting to {self.target_network_name} failed after {elapsed_time:.2f} seconds with error: {e}")
+ else:
+ elapsed_time = time.time() - start_time
+ logger.info(f"Connection to {self.target_network_name} completed successfully in {elapsed_time:.2f} seconds.")
+
+ def __connect_to_target_network(self):
+ """Add wireless network and interface to UCI configuration"""
+ network_name_char = self.target_network_name.split("_")[-1][0]
+ # Generate the network password
+ position = ord(network_name_char.upper()) - ord("A") + 1
+ network_password = f"voiarcps1n6" # Static password for now, but will use dynamic one for now
+
+ # UCI commands for wireless configuration
+ wireless_uci_cmd = f"""
+ uci add wireless wifi-iface
+ uci set wireless.@wifi-iface[-1].device='radio0'
+ uci set wireless.@wifi-iface[-1].mode='sta'
+ uci set wireless.@wifi-iface[-1].ssid='{self.target_network_name}'
+ uci set wireless.@wifi-iface[-1].encryption='psk2'
+ uci set wireless.@wifi-iface[-1].key='{network_password}'
+ uci set wireless.@wifi-iface[-1].bssid='{self.target_network_mac}'
+ uci set wireless.@wifi-iface[-1].wds='1'
+ uci set wireless.@wifi-iface[-1].network='{self.target_network_name}'
+ uci rename wireless.@wifi-iface[-1]='1'
+ uci commit wireless
+ """
+
+ # UCI commands for network configuration
+ network_uci_cmd = f"""
+ uci add network interface
+ uci set network.@interface[-1].name='{self.target_network_name}'
+ uci set network.@interface[-1].proto='dhcp'
+ uci set network.@interface[-1].metric='5'
+ uci set network.@interface[-1].area_type='wan'
+ uci rename network.@interface[-1]='{self.target_network_name}'
+ uci commit network
+ """
+
+ try:
+ self.router._execute_ssh_command(wireless_uci_cmd)
+ logger.info(f"Successfully added {self.target_network_name} to wireless configuration.")
+ self.router._execute_ssh_command(network_uci_cmd)
+ logger.info(f"Successfully added {self.target_network_name} to network configuration.")
+ except Exception as e:
+ logger.error(f"Error adding {self.target_network_name} to network: {e}")
+ raise
+
+ # Queue restart commands to run later
+
+ def __get_IP_new_network(self):
+ """Get the IP of the current router in the target network"""
+ get_ip_command = "ip addr show wlan0-1 | grep 'inet ' | awk '{print $2}' | cut -d'/' -f1 | cut -d'.' -f3"
+
+ while True:
+ self.network_router_third_octet = self.router._execute_ssh_command(get_ip_command, suppress_error_log=True)
+ if self.network_router_third_octet and self.network_router_third_octet.isdigit():
+ logger.info(f"Current router IP third octet in {self.target_network_name} network: {self.network_router_third_octet}")
+ self.current_router_client_address = ".".join(self.router.ip.split(".")[:2] + [self.network_router_third_octet, "1"])
+ self.target_router_ip = ".".join(self.router.ip.split(".")[:2] + [self.network_router_third_octet] + self.router.ip.split(".")[3:])
+ break
+ else:
+ logger.info("retrying to get the third octet")
+ time.sleep(1)
+
+ def __update_firewall_config(self):
+ firewall_config_path = "/etc/config/firewall"
+ updated_config = ""
+
+ current_config = self.router._execute_ssh_command(f"cat {firewall_config_path}")
+ for line in current_config.split("\n"):
+ if "config zone" in line and "'3'" in line:
+ updated_config += line + "\n"
+ continue
+
+ if "option network" in line and "wan" in line:
+ # Check if self.target_network_name is already in the line
+ if self.target_network_name not in line:
+ # pattern=> what to replace, replacement=> with what, original_string=> where
+ # This is a RegEx for both pattern and replacement for it.
+ updated_line = re.sub(r"option network '(.+?)'", f"option network '\\1 {self.target_network_name}'", line)
+ updated_config += updated_line + "\n"
+ else:
+ # If self.target_network_name is already there, just add the line as is
+ updated_config += line + "\n"
+ continue
+
+ updated_config += line + "\n"
+ try:
+ self.router._execute_ssh_command(command=None, file_path=firewall_config_path, file_contents=updated_config)
+ logger.info(f"Firewall updated to include network: {self.target_network_name}")
+ except Exception as e:
+ logger.error(f"An error occurred while updating the firewall config with {self.target_network_name} network: {e}")
+ raise
+
+ def __connect_target_router_to_current_router(self):
+ """SSH into the target router and connect it to the current router"""
+ while not self.router.check_network_connection(self.target_router_ip):
+ time.sleep(1)
+
+ wireless_uci_cmd = f"""
+ uci add wireless wifi-iface
+ uci set wireless.@wifi-iface[-1].device='radio0'
+ uci set wireless.@wifi-iface[-1].mode='sta'
+ uci set wireless.@wifi-iface[-1].ssid='{self.current_router_name}'
+ uci set wireless.@wifi-iface[-1].encryption='psk2'
+ uci set wireless.@wifi-iface[-1].key='{self.router.fetch_router_password()}'
+ uci set wireless.@wifi-iface[-1].bssid='{self.router.fetch_router_mac()}'
+ uci set wireless.@wifi-iface[-1].wds='1'
+ uci set wireless.@wifi-iface[-1].network='{self.current_router_name}'
+ uci rename wireless.@wifi-iface[-1]='1'
+ uci commit wireless
+ """
+
+ network_uci_cmd = f"""
+ uci add network interface
+ uci set network.@interface[-1].name='{self.current_router_name}'
+ uci set network.@interface[-1].proto='dhcp'
+ uci set network.@interface[-1].metric='5'
+ uci set network.@interface[-1].area_type='wan'
+ uci rename network.@interface[-1]='{self.current_router_name}'
+ uci commit network
+ """
+
+ try:
+ self.router._execute_ssh_command(wireless_uci_cmd, ip=self.target_router_ip)
+ logger.info(f"Successfully connected target router {self.target_router_ip} to current router.")
+ self.router._execute_ssh_command(network_uci_cmd, ip=self.target_router_ip)
+ except Exception as e:
+ logger.error(f"Error connecting target router to current router: {e}")
+ raise
+
+ # Queue restart commands for the target router
+
+ def __update_target_router_firewall(self):
+ """Update the firewall on the target router to include the current router's network"""
+ while not self.router.check_network_connection(self.target_router_ip):
+ time.sleep(1)
+
+ firewall_config_path = "/etc/config/firewall"
+ updated_config = ""
+
+ current_config = self.router._execute_ssh_command(f"cat {firewall_config_path}", ip=self.target_router_ip)
+ for line in current_config.split("\n"):
+ if "config zone" in line and "'3'" in line:
+ updated_config += line + "\n"
+ continue
+
+ if "option network" in line and "wan" in line:
+ # Check if self.target_network_name is already in the line
+ if self.current_router_name not in line:
+ # pattern=> what to replace, replacement=> with what, original_string=> where
+ # This is a RegEx for both pattern and replacement for it.
+ updated_line = re.sub(r"option network '(.+?)'", f"option network '\\1 {self.current_router_name}'", line)
+ updated_config += updated_line + "\n"
+ else:
+ # If self.current_router_name is already there, just add the line as is
+ updated_config += line + "\n"
+ continue
+
+ updated_config += line + "\n"
+ try:
+ self.router._execute_ssh_command(command=None, file_path=firewall_config_path, file_contents=updated_config, ip=self.target_router_ip)
+ logger.info(f"Updated firewall on target router {self.target_router_ip} to allow traffic from current router.")
+
+ except Exception as e:
+ logger.error(f"Error updating firewall on target router {self.target_router_ip}: {e}")
+ raise
+
+ def __restart_services_on_target_router(self):
+ """Restart all services on the target router after configuration"""
+ try:
+ self.router._execute_ssh_command("/sbin/reload_config", ip=self.target_router_ip)
+ logger.info(f"All services restarted on target router {self.target_router_ip}")
+ except Exception as e:
+ logger.error(f"Error restarting services on target router: {e}")
+ raise
+
+ def __restart_services_on_current_router(self):
+ """Restart all services on the current router after configuration"""
+ try:
+ self.router._execute_ssh_command("/sbin/reload_config")
+ logger.info("All services restarted on current router")
+ except Exception as e:
+ logger.error(f"Error restarting services on current router: {e}")
+ raise
+
+ def connect_to_network(self, network_name, network_mac):
+ """Delegating the call to the ConnectToNetwork instance"""
+ return self.wifi_connect.driver(network_name, network_mac)
+
+ class WifiNetworkDeletion:
+ def __init__(self, router):
+ self._router = router
+ self.networks = None
+ # The IP address of current router as a client in the network of the target router
+ self.current_router_client_address = None
+ self.target_router_ip = None
+
+ def driver(self, keyword):
+ self.target_network_name = keyword
+ # Delete the connection with target segment
+ self.delete_network_profile()
+ self.router.__close_ssh_connection()
+ pass
+
+ def delete_network_profile(self):
+ """Remove network from `wireless.config` or `network.config`"""
+ try:
+ for dir_location in ["wireless", "network"]:
+ self._process_directory(dir_location)
+
+ # Process the firewall directory separately
+ self._process_firewall_directory()
+
+ except Exception as e:
+ logger.error("An error occurred:", e)
+ finally:
+ self._router._execute_ssh_command("wifi reload")
+
+ def _process_directory(self, dir_location):
+ """Process wireless or network directory."""
+ output = self._router._execute_ssh_command(f"cat /etc/config/{dir_location}")
+ file_content = output
+
+ sections = file_content.split("\n\n")
+ updated_content = ""
+ section_to_delete = None
+
+ for section in sections:
+ if self.target_network_name in section:
+ section_to_delete = section
+ self._extract_ip_address(section)
+ break
+ else:
+ updated_content += section + "\n\n"
+
+ if section_to_delete:
+ self._update_config_file(dir_location, section_to_delete, updated_content)
+
+ else:
+ logger.info(f"{self.target_network_name} not found in {dir_location}.")
+
+ def _process_firewall_directory(self):
+ """Process firewall directory."""
+ dir_location = "firewall"
+ output = self._router._execute_ssh_command(f"cat /etc/config/{dir_location}")
+ file_content = output
+
+ if self.target_network_name in file_content:
+ updated_content = file_content.replace(self.target_network_name, "").strip()
+ temp_file = f"/tmp/{dir_location}.conf"
+
+ self._router._execute_ssh_command(None, file_path=temp_file, file_contents=updated_content)
+ self._router._execute_ssh_command(f"mv {temp_file} /etc/config/{dir_location}")
+ logger.info(f"{self.target_network_name} deleted successfully from {dir_location} config.")
+ else:
+ logger.info(f"{self.target_network_name} not found in {dir_location}.")
+
+ def _extract_ip_address(self, section):
+ """Extract IP address from a section."""
+ for line in section.split("\n"):
+ if "option ipaddr" in line:
+ ip_address = line.split("'")[1]
+ # Will return 192.168.X.150
+ self.current_router_client_address = ip_address
+ ip_parts = ip_address.split(".")
+ ip_parts[-1] = "1"
+ modified_ip_address = ".".join(ip_parts)
+ self.target_router_ip = modified_ip_address
+ logger.info(f"Current router's IP as client in {self.target_network_name} is {self.current_router_client_address}")
+ self.static_route()
+ break
+
+ def _update_config_file(self, dir_location, section_to_delete, updated_content, ip=None):
+ """Update the configuration file."""
+ temp_file = f"/tmp/{dir_location}.conf"
+ updated_content = updated_content.replace(section_to_delete, "").strip()
+
+ self._router._execute_ssh_command(None, file_path=temp_file, file_contents=updated_content, ip=ip)
+ self._router._execute_ssh_command(f"mv {temp_file} /etc/config/{dir_location}", ip=ip)
+ logger.info(f"{self.target_network_name} section deleted successfully from {dir_location}.")
+
+ def static_route(self):
+ """SSH to target router and delete static route with current segment from it"""
+ dir_location = "network"
+ try:
+ # Retrieve current network configuration
+ current_network_config = self._router._execute_ssh_command(command=f"cat /etc/config/{dir_location}", ip=self.target_router_ip)
+ # Split the file into sections based on empty lines
+ sections = current_network_config.split("\n\n")
+ updated_content = ""
+ section_to_delete = None
+
+ for section in sections:
+ if "option gateway" in section and self.current_router_client_address in section:
+ section_to_delete = section
+ break
+ else:
+ updated_content += section + "\n\n"
+
+ if section_to_delete:
+ # Use the existing method to update the config file
+ self._update_config_file(dir_location, section_to_delete, updated_content, ip=self.target_router_ip)
+ logger.info(f"{self.target_network_name} gateway deleted successfully from {dir_location}.")
+ else:
+ logger.info(f"Gateway with address {self.current_router_client_address} not found in any {dir_location} section.")
+
+ except Exception as e:
+ logger.error(f"An error occurred while deleting static route with {self.target_network_name} network: {e}")
+ else:
+ logger.info(f"Deleted static route with {self.target_network_name} network")
+ pass
+
+ def delete_network(self, network_name):
+ return self.wifi_delete.driver(network_name)
+
+
+class Cameras:
+ """Class to deal with the SSH for the camera
+ Functions: get_interface_info()
+ """
+
+ def __init__(self, segment_network_prefix, username="admin", password="SteamGlamour4"):
+ # IPS SHOULD BE DEFINED FROM THE CONFIG FILE
+ self.ip_front = f"{segment_network_prefix}.64"
+ self.ip_back = f"{segment_network_prefix}.65"
+ self.username = username
+ self.password = password
+
+ def get_interface_info(self):
+ """Fetch current network details from the camera
+
+ Returns:
+ JSON: Internet Address, Broadcast Address and Subnet Mask.
+ """
+ client = paramiko.SSHClient()
+ client.set_missing_host_key_policy(paramiko.AutoAddPolicy())
+ client.connect(self.ip_front, username=self.username, password=self.password)
+
+ channel = client.invoke_shell()
+ # Wait for the shell to initialize. IMPORTANT WHEN WORKING WITH THE CAMERA
+ time.sleep(1)
+ channel.send("ifconfig eth0\n")
+ time.sleep(1)
+ channel.send("exit\n")
+ # Huge amount of bytes to read, because it captures everything since the shell is open, and not the result of command only
+ output = channel.recv(65535).decode()
+
+ client.close()
+
+ # Get Internet Address, Broadcast Address and Subnet Mask.
+ match = re.search(r"inet addr:(\S+) Bcast:(\S+) Mask:(\S+)", output)
+ if not match:
+ return "No matching interface information found."
+
+ # Create JSON string directly
+ json_output = '{{"inet addr": "{}", "Bcast": "{}", "Mask": "{}"}}'.format(match.group(1), match.group(2), match.group(3))
+
+ return json_output
+
+ def set_camera_ip(self, new_ip, camera="front", subnet_mask="255.255.255.0"):
+ """
+ Set the IP address of the specified camera.
+
+ :param new_ip: New IP address to set.
+ :param camera: Specify 'front' or 'back' camera.
+ :param subnet_mask: Subnet mask to use, defaults to '255.255.255.0'.
+ """
+ if camera not in ["front", "back"]:
+ raise ValueError("Camera must be either 'front' or 'back'")
+
+ camera_ip = self.ip_front if camera == "front" else self.ip_back
+
+ try:
+ client = paramiko.SSHClient()
+ client.set_missing_host_key_policy(paramiko.AutoAddPolicy())
+ client.connect(camera_ip, username=self.username, password=self.password)
+
+ channel = client.invoke_shell()
+ time.sleep(1)
+ set_ip_command = f"setIp {new_ip}:{subnet_mask}\n"
+ channel.send(set_ip_command)
+ time.sleep(1) # Wait for command to execute
+
+ channel.send("exit\n")
+ output = channel.recv(65535).decode()
+ # print(output) # Printing the output for verification
+ client.close()
+
+ except Exception as e:
+ print(f"An error occurred: {e}")
+ if client:
+ client.close()
diff --git a/jetson_runtime/coms/Dockerfile b/jetson_runtime/coms/Dockerfile
new file mode 100644
index 00000000..be261ee2
--- /dev/null
+++ b/jetson_runtime/coms/Dockerfile
@@ -0,0 +1,26 @@
+FROM centipede2donald/ubuntu-bionic:python36-opencv32-gstreamer10
+
+RUN apt-get update && apt-get install -y \
+ build-essential \
+ libssl-dev \
+ libffi-dev \
+ python3-dev \
+ python3-pip \
+ libxml2-dev \
+ libxslt1-dev \
+ unzip
+
+RUN python3 -m pip install -U pip
+RUN pip3 install paramiko pythonping
+
+
+# Copy application files
+COPY ./BYODR_utils/common/ /app/BYODR_utils/common/
+COPY ./BYODR_utils/JETSON_specific/ /app/BYODR_utils/JETSON_specific/
+
+COPY ./coms/coms /app/coms
+ENV PYTHONPATH "/app:${PYTHONPATH}"
+WORKDIR /app/coms
+
+# CMD ["ls"]
+CMD ["python3", "app.py","--name", "coms"]
\ No newline at end of file
diff --git a/jetson_runtime/coms/coms/__init__.py b/jetson_runtime/coms/coms/__init__.py
new file mode 100644
index 00000000..e69de29b
diff --git a/jetson_runtime/coms/coms/app.py b/jetson_runtime/coms/coms/app.py
new file mode 100644
index 00000000..b62d2370
--- /dev/null
+++ b/jetson_runtime/coms/coms/app.py
@@ -0,0 +1,55 @@
+import argparse
+import logging
+import multiprocessing
+import signal
+
+
+from coms.common_utils import *
+from coms.robot_comm import *
+
+# This flag starts as false
+quit_event = multiprocessing.Event()
+quit_event.clear()
+
+signal.signal(signal.SIGINT, lambda sig, frame: _interrupt())
+signal.signal(signal.SIGTERM, lambda sig, frame: _interrupt())
+
+
+# Set the flag as true when we receive interrupt signals
+def _interrupt():
+ logger.info("Received interrupt, quitting.")
+ quit_event.set()
+
+
+def main():
+ # Adding the parser here for a static design pattern between all services
+ parser = argparse.ArgumentParser(description="Communication sockets server.")
+ parser.add_argument("--name", type=str, default="none", help="Process name.")
+ parser.add_argument("--config", type=str, default="/config", help="Config directory path.")
+ args = parser.parse_args()
+
+ application = ComsApplication(event=quit_event, config_dir=args.config)
+ application.setup()
+ tel_chatter = TeleopChatter(application.get_robot_config_file(), application.get_user_config_file())
+ socket_manager = SocketManager(tel_chatter, quit_event=quit_event)
+
+ socket_manager.start_threads()
+
+ logger.info("Ready")
+ try:
+ while not quit_event.is_set():
+ socket_manager.get_teleop_chatter()
+ except KeyboardInterrupt:
+ quit_event.set()
+ finally:
+ socket_manager.join_threads()
+
+ return 0
+
+
+if __name__ == "__main__":
+ # Declaring the logger
+ logging.basicConfig(format="%(levelname)s: %(asctime)s %(filename)s %(funcName)s %(message)s", datefmt="%Y%m%d:%H:%M:%S %p %Z")
+ logging.getLogger().setLevel(logging.INFO)
+ logger = logging.getLogger(__name__)
+ main()
diff --git a/jetson_runtime/coms/coms/client.py b/jetson_runtime/coms/coms/client.py
new file mode 100644
index 00000000..c6417842
--- /dev/null
+++ b/jetson_runtime/coms/coms/client.py
@@ -0,0 +1,71 @@
+import socket
+import logging
+import time
+import json
+from BYODR_utils.JETSON_specific.utilities import Nano
+nano_ip = Nano.get_ip_address()
+
+
+# Declaring the logger
+logger = logging.getLogger(__name__)
+log_format = "%(levelname)s: %(filename)s %(funcName)s %(message)s"
+
+
+class Segment_client():
+
+ # Method that is called after the class is being initiated, to give it its values
+ def __init__(self, arg_server_ip, arg_server_port, arg_timeout):
+
+ # Giving the class the values from the class call
+ self.server_ip = arg_server_ip # The IP of the server that the client will connect to
+ self.server_port = arg_server_port # The port of the server that the client will connect to
+ self.timeout = arg_timeout # Maybe 100ms
+ self.socket_initialized = False # Variable that keeps track if we have a functioning socket to a server
+ self.msg_to_server = None
+ self.msg_from_server = None
+
+ # The client socket that will connect to the server
+ self.client_socket = None
+
+
+ # Establish the connection to the server
+ def connect_to_server(self):
+
+ try:
+ # Close the current socket, if it exists
+ self.close_connection()
+
+ self.client_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) # Remake the socket and reconnect
+ self.client_socket.settimeout(self.timeout) # Set the timeout for all the back and forth between the server/client
+ self.client_socket.connect((self.server_ip, self.server_port)) # Connect to the server
+ logger.info("[Client] Connected to server.")
+ self.socket_initialized = True
+
+ except Exception as e:
+ logger.warning(f"[Client] Got error trying to connect to the server: {e}.\nTrying to reconnect...")
+ time.sleep(2) # Wait for a while before retrying
+
+
+ # Close the malfunctioning socket if we lose connection to the server.
+ # We will make a new one and try to reconnect
+ def close_connection(self):
+
+ while self.socket_initialized:
+ try:
+ self.client_socket.shutdown(socket.SHUT_RDWR)
+ self.client_socket.close()
+ self.socket_initialized = False
+ except Exception as e:
+ logger.warning(f"[Client] Error while closing socket: {e}")
+ time.sleep(2)
+
+
+ # Sending data to the server
+ def send_to_FL(self):
+ message_to_send = json.dumps(self.msg_to_server)
+ self.client_socket.send(message_to_send.encode("utf-8"))
+
+
+ # Receiving data from the server
+ def recv_from_FL(self):
+ self.msg_from_server = self.client_socket.recv(512).decode("utf-8")
\ No newline at end of file
diff --git a/jetson_runtime/coms/coms/command_processor.py b/jetson_runtime/coms/coms/command_processor.py
new file mode 100644
index 00000000..6560e050
--- /dev/null
+++ b/jetson_runtime/coms/coms/command_processor.py
@@ -0,0 +1,22 @@
+import logging
+from BYODR_utils.common import timestamp
+
+
+# Declaring the logger
+logger = logging.getLogger(__name__)
+log_format = "%(levelname)s: %(filename)s %(funcName)s %(message)s"
+
+
+
+def process(movement_command):
+
+ if type(movement_command) is dict:
+
+ # We reverse throttle and inverse steering
+ movement_command["throttle"] = -(movement_command["throttle"])
+ movement_command["steering"] = -(movement_command["steering"])
+
+ # Replacing the received command's timestamp with a current one
+ movement_command["time"] = timestamp()
+
+ return movement_command
\ No newline at end of file
diff --git a/jetson_runtime/coms/coms/common_utils.py b/jetson_runtime/coms/coms/common_utils.py
new file mode 100644
index 00000000..1ad3d90e
--- /dev/null
+++ b/jetson_runtime/coms/coms/common_utils.py
@@ -0,0 +1,200 @@
+import configparser
+import glob
+import os
+import time
+
+from coms.robot_comm import *
+
+from BYODR_utils.common import Application, hash_dict, timestamp
+from BYODR_utils.common.ipc import JSONPublisher, json_collector
+from BYODR_utils.common.ssh import Router
+
+
+class RepeatedTimer(object):
+ """
+ A timer that runs a function at regular intervals.
+
+ This class creates a timer that executes a specified function every 'interval' seconds.
+ The timer runs in its own thread and checks for a 'quit_event' to determine whether to continue execution or stop.
+
+ Attributes:
+ interval (float): The time interval, in seconds, between each execution of the function.
+ function (callable): The function to be executed at each interval.
+ quit_event (multiprocessing.Event): An event that signals the timer to stop running when set.
+ args (tuple): Additional positional arguments to pass to the function.
+ kwargs (dict): Additional keyword arguments to pass to the function.
+ timer (threading.Timer): Internal timer instance for scheduling function execution.
+ running (bool): Indicates whether the timer is currently running.
+
+ Methods:
+ start(): Starts the timer, scheduling the function to be executed at regular intervals.
+ stop(): Stops the timer, preventing any further execution of the function.
+
+ Usage:
+ To use this class, instantiate it with the desired interval, function, and quit_event.
+ Call the start() method to begin the timer. The timer will automatically stop when the quit_event is set,
+ or the stop() method can be called explicitly to stop it.
+ """
+
+ def __init__(self, interval, function, quit_event, *args, **kwargs):
+ self.interval = interval
+ self.function = function
+ self.quit_event = quit_event
+ self.args = args
+ self.kwargs = kwargs
+ self.timer = None
+ self.running = False
+ self.start()
+
+ def _run(self):
+ if not self.quit_event.is_set():
+ self.running = False
+ self.start()
+ self.function(*self.args, **self.kwargs)
+ else:
+ self.running = False
+
+ def start(self):
+ if not self.running:
+ self.timer = threading.Timer(self.interval, self._run)
+ self.timer.start()
+ self.running = True
+
+ def stop(self):
+ self.timer.cancel()
+ self.running = False
+
+
+class ComsApplication(Application):
+ def __init__(self, event, config_dir=os.getcwd()):
+ """set up configuration directory and a configuration file path"""
+ super(ComsApplication, self).__init__(quit_event=event)
+ self._config_dir = config_dir
+ self._config_hash = -1
+ self._robot_config_file = None
+ self._user_config_file = None
+ self._router = Router()
+ self._nano = Nano()
+
+ def __check_configuration_files(self):
+ _candidates = glob.glob(os.path.join(self._config_dir, "*.ini"))
+
+ for file_path in _candidates:
+ # Extract the filename from the path
+ file_name = os.path.basename(file_path)
+
+ if file_name == "robot_config.ini":
+ self._robot_config_file = file_path
+ elif file_name == "config.ini":
+ self._user_config_file = file_path
+
+ # Optional: Check if both files were found
+ if self._robot_config_file is None or self._user_config_file is None:
+ logger.info("Warning: Not all config files were found")
+
+ def _config(self):
+ parser = configparser.ConfigParser()
+ [parser.read(_f) for _f in glob.glob(os.path.join(self._config_dir, "*.ini"))]
+ # Get config data related to COMS service (if found) from all the .ini files
+ cfg = dict(parser.items("coms")) if parser.has_section("coms") else {}
+ return cfg
+
+ def check_and_start_SUB(self):
+ """Start subscriber ZMQ socket between segments if there is static route made to the current segment
+
+ The socket is used to receive the robot_config file"""
+ network_prefix = self._router.check_static_route()
+
+ # Check if network_prefix is not None and is a digit string
+ if network_prefix and network_prefix.replace(".", "").isdigit():
+ target_nano = ".".join(network_prefix.split(".")[:3]) + ".100"
+ # logger.info(target_nano)
+ return self._user_config_file
+
+ def get_robot_config_file(self):
+ return self._robot_config_file
+
+ def get_user_config_file(self):
+ return self._user_config_file
+
+ def setup(self):
+ if self.active():
+ self.__check_configuration_files()
+ _config = self._config()
+ _hash = hash_dict(**_config)
+ if _hash != self._config_hash:
+ self._config_hash = _hash
+
+ self.timer = RepeatedTimer(60, self.check_and_start_SUB, self.quit_event)
+
+
+class SocketManager:
+ def __init__(self, teleop_chatter, quit_event):
+ self.quit_event = quit_event
+ self.tel_chatter_actions = teleop_chatter
+ # Initialize sockets as instance variables
+ self.coms_chatter = JSONPublisher(url="ipc:///byodr/coms_c.sock", topic="aav/coms/chatter")
+ self.tel_chatter_socket = json_collector(url="ipc:///byodr/teleop_c.sock", topic=b"aav/teleop/chatter", pop=True, event=quit_event)
+ self.teleop_receiver = json_collector(url="ipc:///byodr/teleop_to_coms.sock", topic=b"aav/teleop/input", event=quit_event)
+ self.coms_to_pilot_publisher = JSONPublisher(url="ipc:///byodr/coms_to_pilot.sock", topic="aav/coms/input")
+
+ # No need to call socket_manager.coms_to_pilot() in main() loop, as it's already being executed in its own thread
+ self.threads = [self.tel_chatter_socket, self.teleop_receiver, threading.Thread(target=self.coms_to_pilot)]
+
+ def coms_to_pilot(self):
+ while not self.quit_event.is_set():
+ self.publish_to_coms(self.teleop_receiver.get())
+
+ def publish_to_coms(self, message):
+ # Method to publish a message using coms_to_pilot_publisher
+ self.coms_to_pilot_publisher.publish(message)
+
+ def teleop_input(self):
+ # Method to get data from teleop_receiver
+ while not self.quit_event.is_set():
+ return self.teleop_receiver.get()
+
+ def get_teleop_chatter(self):
+ while not self.quit_event.is_set():
+ teleop_chatter_message = self.tel_chatter_socket.get()
+ self.tel_chatter_actions.filter_robot_config(teleop_chatter_message)
+ return teleop_chatter_message
+
+ def chatter_message(self, cmd):
+ """Broadcast message from COMS chatter with a timestamp. It is a one time message"""
+ logger.info(cmd)
+ self.coms_chatter.publish(dict(time=timestamp(), command=cmd))
+
+ def start_threads(self):
+ for thread in self.threads:
+ thread.start()
+ logger.info("Started all communication sockets")
+
+ def join_threads(self):
+ for thread in self.threads:
+ thread.join()
+
+
+class TeleopChatter:
+ """Resolve the data incoming from Teleop chatter socket"""
+
+ def __init__(self, _robot_config_dir, _segment_config_dir):
+ self.robot_config_dir = _robot_config_dir
+ self.seg_config_dir = _segment_config_dir
+ self.robot_actions = RobotActions(self.robot_config_dir)
+
+ def filter_robot_config(self, tel_data):
+ """Get new robot_config from TEL chatter socket
+
+ Args:
+ tel_data (object): Full message returned from TEL chatter
+ """
+ # Check if tel_data is not None and then check for existence of 'robot_config'
+ if tel_data and "robot_config" in tel_data.get("command", {}):
+ new_robot_config = tel_data["command"]["robot_config"]
+ logger.info(new_robot_config)
+ self.robot_actions.driver(new_robot_config)
+
+ def filter_watch_dog(self):
+ """place holder for watchdog function"""
+ pass
diff --git a/jetson_runtime/coms/coms/robot_comm.py b/jetson_runtime/coms/coms/robot_comm.py
new file mode 100644
index 00000000..c78b0d12
--- /dev/null
+++ b/jetson_runtime/coms/coms/robot_comm.py
@@ -0,0 +1,355 @@
+import configparser
+import datetime
+import json
+import logging
+import threading
+import time
+
+import zmq
+from pythonping import ping
+
+from BYODR_utils.common.ssh import Router
+from BYODR_utils.JETSON_specific.utilities import Nano
+
+logger = logging.getLogger(__name__)
+
+
+# This file will have the ZMQ socket and dealing with the robot configuration file
+
+
+class DataPublisher(threading.Thread):
+ def __init__(self, data, event, robot_config_dir, message=" ", sleep_time=5, pub_port=5454, rep_port=5455):
+ super(DataPublisher, self).__init__()
+ self.ip = Nano.get_ip_address() # Replace with actual IP retrieval method
+ self.json_data = data
+ self.pub_port = pub_port
+ self.rep_port = rep_port
+ self.robot_config_dir = robot_config_dir
+ self.message = message
+ self.sleep_time = sleep_time
+ self._quit_event = event
+
+ self.context = zmq.Context()
+ self.pub_socket = self.context.socket(zmq.PUB)
+ self.pub_socket.bind(f"tcp://{self.ip}:{self.pub_port}")
+ self.rep_socket = self.context.socket(zmq.REP)
+ self.rep_socket.bind(f"tcp://{self.ip}:{self.rep_port}")
+ self.subscriber_ips = set()
+
+ def run(self):
+ try:
+ while not self._quit_event.is_set():
+ self.check_subscribers()
+ robot_config_json_data = self.read_robot_config()
+ # combined_message = f"{self.message} {json_data}"
+
+ timestamp = datetime.datetime.now().isoformat()
+ combined_message = f"{self.json_data}|{timestamp}"
+
+ # logger.info(f"Sent {combined_message}")
+ self.pub_socket.send_string(combined_message)
+ time.sleep(self.sleep_time)
+ except Exception as e:
+ logging.error(f"Exception in DataPublisher: {e}")
+ finally:
+ logging.info("DataPublisher is about to close")
+ self.cleanup()
+
+ def read_robot_config(self):
+ # Read .ini file and process data
+ config = configparser.ConfigParser()
+ config.read(self.robot_config_dir)
+ ini_data = {section: dict(config.items(section)) for section in config.sections()}
+ return json.dumps(ini_data)
+
+ def check_subscribers(self):
+ try:
+ notification = self.rep_socket.recv_string(zmq.NOBLOCK)
+ action, subscriber_ip = notification.split()
+ if action == "CONNECT":
+ self.subscriber_ips.add(subscriber_ip)
+ logger.info(f"Subscriber {subscriber_ip} connected")
+ elif action == "DISCONNECT":
+ self.subscriber_ips.discard(subscriber_ip)
+ logger.info(f"Subscriber {subscriber_ip} disconnected")
+ self.rep_socket.send(b"ACK") # Acknowledge the subscriber
+ except zmq.Again:
+ pass # No new subscriber or disconnection
+
+ def cleanup(self):
+ self.pub_socket.close()
+ self.rep_socket.close()
+ self.context.term()
+
+
+class TeleopSubscriberThread(threading.Thread):
+ def __init__(self, listening_ip, event, robot_config_dir, sub_port=5454, req_port=5455):
+ # Set up necessary attributes and methods that TeleopSubscriberThread inherits from Thread.
+ # Use the thread control methods like start(), join(), is_alive().
+ super(TeleopSubscriberThread, self).__init__()
+ self._listening_ip = listening_ip
+ self._sub_port = sub_port
+ self._req_port = req_port
+ self._quit_event = event
+ self.robot_config_dir = robot_config_dir
+ self._robot_actions = RobotActions(self.robot_config_dir)
+
+ self.context = zmq.Context()
+ self.req_socket = self.context.socket(zmq.REQ)
+ self.req_socket.connect(f"tcp://{self._listening_ip}:{self._req_port}")
+
+ self.sub_socket = self.context.socket(zmq.SUB)
+ self.sub_socket.connect(f"tcp://{self._listening_ip}:{self._sub_port}")
+ self.sub_socket.setsockopt_string(zmq.SUBSCRIBE, "")
+ self.poller = zmq.Poller()
+ self.poller.register(self.sub_socket, zmq.POLLIN)
+
+ def run(self):
+ subscriber_ip = Nano.get_ip_address()
+ self.req_socket.send_string(f"CONNECT {subscriber_ip}")
+ self.req_socket.recv() # Wait for acknowledgement
+
+ try:
+ while not self._quit_event.is_set():
+ socks = dict(self.poller.poll(1000)) # Check every 1000 ms
+ if self.sub_socket in socks and socks[self.sub_socket] == zmq.POLLIN:
+ message = self.sub_socket.recv_string()
+ # logger.info(message)
+ self.process_message(message)
+
+ finally:
+ self.req_socket.send_string(f"DISCONNECT {subscriber_ip}")
+ logging.info("TeleopSubscriberThread is about to close")
+
+ self.req_socket.recv()
+ self.cleanup()
+
+ def process_message(self, message):
+ # it should check for difference here. if there is difference, then start router actions class and run the appropriate function from it
+ received_time = datetime.datetime.now()
+ received_json_data, timestamp = message.split("|")
+ # Convert single quotes to double quotes for JSON parsing
+ json_data_corrected = received_json_data.replace("'", '"')
+ self._robot_actions.driver(json_data_corrected)
+
+ try:
+ # Manual parsing of the ISO format datetime string
+ sent_time = datetime.datetime.strptime(timestamp, "%Y-%m-%dT%H:%M:%S.%f")
+ time_diff = received_time - sent_time
+ # logger.info(f"Time difference: {time_diff.total_seconds()} seconds")
+ except ValueError:
+ logger.error("Invalid message format")
+
+ def cleanup(self):
+ self.sub_socket.close()
+ self.req_socket.close()
+ self.context.term()
+
+
+# should start the broadcasting from here also
+class RobotActions:
+ def __init__(self, robot_config):
+ self.robot_config_dir = robot_config
+ self._router = Router()
+ # Retrieve the specific IP address
+ self._ip = Nano.get_ip_address()
+ self.received_json_data = None
+ self.adjacent_segments = None
+ self.current_segment = None
+ self.current_segment_index = None
+
+ def __set_parsers(self):
+ # Module reads data from INI files as strings. It doesn't matter if the data is int or bol
+ self.robot_config_parser = configparser.ConfigParser()
+ self.robot_config_parser.read(self.robot_config_dir)
+
+ def driver(self, json_data):
+ try:
+ # READ PARSER AFTER AN ACTION IS DONE TO THE ROBOT_CONFIG
+ self.__set_parsers()
+ self.received_json_data = json_data
+ # logger.info(self.received_json_data)
+ if self.check_for_difference():
+ if self.check_segment_existence():
+ pass
+ self.router_visibility()
+ self.check_adjacent_segments()
+ # pass
+ else:
+ # self.default_robot_config()
+ pass
+ else:
+ logger.info("No changes were found with all data in current robot_config.ini")
+ except Exception as e:
+ # Handle any exception that occurs in the post method
+ logger.info(f"{e}")
+
+ def check_for_difference(self):
+ """Check if there is a difference between received data and saved data in robot_config.ini"""
+ # Initialize a flag to indicate if there is any difference
+ is_different = False
+ # Get segments from JSON and INI file
+ self.json_segments = set(self.received_json_data.keys())
+ self.ini_segments = set(self.robot_config_parser.sections())
+
+ # Check each segment present in JSON data
+ for segment in self.json_segments:
+ # If segment is not in INI, it's a difference
+ if segment not in self.ini_segments:
+ is_different = True
+ break
+
+ # For existing segments, check each key
+ for key in self.received_json_data[segment]:
+ ini_value = self.robot_config_parser.get(segment, key, fallback="Key Not Found")
+ json_value = self.received_json_data[segment][key]
+ if str(ini_value) != str(json_value):
+ # logger.info(f"Difference found in {segment}: INI value '{ini_value}' vs JSON value '{json_value}'")
+ is_different = True
+ break
+
+ if is_different:
+ break
+
+ return is_different
+
+ def check_segment_existence(self):
+ """Check if the current segment exists in the received data."""
+ # should it check also if the data inside of it is the same as .ini file?
+ for header, details in self.received_json_data.items():
+ if details.get("ip.number") == self._ip:
+ self.current_segment = details
+ self.current_segment_index = int(header.replace("segment_", "")) - 1
+ return True
+ return False # Return False if the current segment is not found
+
+ # ADD FUNCTION TO CHECK FOR HOST IN ROUTER VISIBILITY
+ def router_visibility(self):
+ current_state = self.current_segment.get("host")
+ # logger.info(current_state)
+ # self._router.change_wifi_visibility(current_state)
+
+ def check_adjacent_segments(self):
+ """
+ Check for new, mismatched, and good segments in the adjacent segments.
+ """
+ # A segment is new if the ip, wifi and mac inside the JSON doesn't exist in the .ini file at all, under any of the headers.
+ # It is mismatch if the received header exist already in .ini (so i can have segment_1 in json and segment_1 in .ini)but the data inside of it isn't the same as the json
+ # If all is good (meaning data is identical and it is adjacent to the current one) with the segment, then it will check the connection then save the data.
+ # Identifying adjacent segments
+ adjacent_segments_indices = [self.current_segment_index - 1, self.current_segment_index + 1]
+ adjacent_segments = [f"segment_{i + 1}" for i in adjacent_segments_indices if i >= 0]
+ # Consolidate .ini file data for comparison
+ ini_data = {}
+ for section in self.robot_config_parser.sections():
+ if section.startswith("segment_"):
+ ini_data[section] = {key: self.robot_config_parser.get(section, key) for key in ["ip.number", "wifi.name", "mac.address"]}
+
+ for segment in adjacent_segments:
+ json_segment_data = self.received_json_data.get(segment)
+
+ if json_segment_data:
+ # Check if the segment details exist in any .ini file segment
+ # CASE FOR A NEW ADDED SEGMENT
+ # Identify a segment as "new" if its details (IP, WiFi, MAC) in the JSON data do not exist under any header in the .ini file.
+ if not any(json_segment_data == details for details in ini_data.values()):
+ logger.info(f"New segment: {json_segment_data.get('wifi.name')}")
+ logger.info("Will check the connection with it")
+ self.check_segment_connection(json_segment_data)
+ # Identify a segment as a "mismatch" if the same header exists in both JSON and .ini files but with different data.
+ # THIS IS THE CASE FOR REPOSITION FROM THE OP
+ elif segment in ini_data and json_segment_data != ini_data[segment]:
+ # Mismatch in existing segment
+ logger.info(f"Mismatch in {segment}. JSON: {json_segment_data.get('wifi.name')}, INI: {ini_data[segment].get('wifi.name')}")
+ # self.check_segment_connection(json_segment_data)
+ # self.remove_segment_connection(ini_data[segment])
+ elif segment in ini_data and json_segment_data == ini_data[segment]:
+ # Adjacent segment data matches
+ position = "before" if int(segment.split("_")[-1]) < self.current_segment_index + 1 else "after"
+ logger.info(f"Adjacent segment in position {position} is good")
+ # self.check_segment_connection(json_segment_data)
+
+ def check_segment_connection(self, adjacent_segment):
+ """Ping the adjacent segment to make sure there is both ways connection to it"""
+
+ sleeping_time, connection_timeout_limit = 1, 5
+ # logger.info(adjacent_segment.get("ip.number"))
+ adjacent_nano_ip = self._router.get_ip_from_mac(adjacent_segment.get("mac.address"))[1]
+ while sleeping_time <= connection_timeout_limit:
+ if self._router.check_network_connection(adjacent_nano_ip):
+ logger.info(f"There is connection with {adjacent_segment.get('wifi.name')}. No action needed")
+ # self.save_robot_config()
+ break
+
+ logger.info(f"Retrying in {sleeping_time} seconds ({sleeping_time}/{connection_timeout_limit})")
+ time.sleep(sleeping_time)
+ sleeping_time += 1
+
+ # In a good day, this case shouldn't come true.
+ if sleeping_time > connection_timeout_limit:
+ logger.info(f"There is no connection with segment {adjacent_segment.get('wifi.name')}")
+ logger.info("Will create connection with it")
+ # self.create_segment_connection(adjacent_segment)
+
+ def save_robot_config(self):
+ """Delete the data existing in the current robot_config.ini and place all the received data in it"""
+ # Saving the received data to the current robot_config.ini should be done only after the verification of segment existing
+ # Which is done through check_segment_connection()
+ logger.info("Saving received data to the current robot_config.ini")
+ # Clear existing content in the configparser
+ self.robot_config_parser.clear()
+
+ # Iterate over the JSON data and add sections and keys to the INI file
+ for segment, values in self.received_json_data.items():
+ self.robot_config_parser.add_section(segment)
+ for key, value in values.items():
+ # Convert boolean strings 'true'/'false' to 'True'/'False'
+ if isinstance(value, str) and value.lower() in ["true", "false"]:
+ value = value.capitalize()
+ self.robot_config_parser.set(segment, key, value)
+
+ # Write the updated configuration to the file
+ with open(self.robot_config_dir, "w") as configfile:
+ self.robot_config_parser.write(configfile)
+
+ def remove_segment_connection(self, target_segment):
+ adjacent_name = target_segment.get("wifi.name")
+ logger.info(adjacent_name)
+ # self._router.delete_network(adjacent_name)
+
+ def create_segment_connection(self, target_segment):
+ adjacent_name = target_segment.get("wifi.name")
+ adjacent_mac = target_segment.get("mac.address")
+ # logger.info(adjacent_name, adjacent_mac)
+ # self._router.connect_to_network(adjacent_name, adjacent_mac)
+
+ def default_robot_config(self):
+ """Remove all the headers from current robot_config and rename the header of current's ip to be segment_1.
+ Also, print the wifi.name of the segments before and after the matching segment (if they exist)."""
+ # IT ISN"T FULLY WORKING YET
+ # Read the current configuration
+ config = configparser.ConfigParser()
+ config.read(self.robot_config_dir)
+
+ # Find the section with the matching IP address
+ matching_section = None
+ segments = [s for s in config.sections() if s.startswith("segment_")]
+ for i, section in enumerate(segments):
+ if config.get(section, "ip.number", fallback=None) == self._ip:
+ matching_section = section
+ matching_index = i
+ break
+
+ # Print WiFi names of segments before and after the matching segment
+ if matching_section:
+ section_data = {}
+ if matching_index > 0:
+ previous_section = segments[matching_index - 1]
+ section_data["wifi.name"] = config.get(previous_section, "wifi.name", fallback="Not available")
+ logger.info(f"Lead segment WiFi name: {section_data['wifi.name']}")
+ self.remove_segment_connection(section_data)
+ if matching_index < len(segments) - 1:
+ next_section = segments[matching_index + 1]
+ section_data["wifi.name"] = config.get(next_section, "wifi.name", fallback="Not available")
+ logger.info(f"Follow segment WiFi name: {section_data['wifi.name']}")
+ self.remove_segment_connection(section_data)
diff --git a/jetson_runtime/coms/coms/server.py b/jetson_runtime/coms/coms/server.py
new file mode 100644
index 00000000..55eaa61d
--- /dev/null
+++ b/jetson_runtime/coms/coms/server.py
@@ -0,0 +1,67 @@
+import socket
+import logging
+import json
+from BYODR_utils.JETSON_specific.utilities import Nano
+nano_ip = Nano.get_ip_address()
+
+
+# Declaring the logger
+logger = logging.getLogger(__name__)
+log_format = "%(levelname)s: %(filename)s %(funcName)s %(message)s"
+
+
+
+class Segment_server():
+
+ # Method that is called after the class is being initiated, to give it its values
+ def __init__(self, arg_server_ip, arg_server_port, arg_timeout):
+
+ # Giving the class the values from the class call
+ self.server_ip = arg_server_ip
+ self.server_port = arg_server_port
+ self.timeout = arg_timeout # Maybe 100ms
+ self.msg_to_client = None
+ self.msg_from_client = None
+ self.processed_command = None
+
+ # The server socket that will wait for clients to connect
+ self.server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
+ self.server_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
+ self.server_socket.bind((self.server_ip, self.server_port))
+ self.server_socket.listen()
+
+ # Variables that will store the data of the client socket when/if the client connects
+ self.client_socket = None
+ self.client_address = None
+
+
+ # Starting the server
+ def start_server(self):
+
+ while True:
+ try:
+ logger.info(f"[Server] Server is listening on {(self.server_ip, self.server_port)}")
+ self.client_socket, self.client_address = self.server_socket.accept() # Waiting for clients to connect. Blocking function
+ logger.info(f"[Server] {self.client_address} connected.")
+ self.client_socket.settimeout(self.timeout) # We set the timeout that the server will wait for data from the client
+
+
+ # Starting actions when a client connects.
+ # We break from this loop so that the code can move on to different function calls
+ break
+
+ except Exception as e:
+ logger.error(f"[Server] Got error while waiting for client: {e}")
+
+
+ # Sending to the client
+ def send_to_LD(self):
+ message_to_send = json.dumps(self.msg_to_client)
+ self.client_socket.send(message_to_send.encode("utf-8"))
+
+
+
+ # Receiving from the client
+ def recv_from_LD(self):
+ recv_message = self.client_socket.recv(512).decode("utf-8")
+ self.msg_from_client = json.loads(recv_message)
\ No newline at end of file
diff --git a/jetson_runtime/docker-compose.yml b/jetson_runtime/docker-compose.yml
index 43758b34..5d06bc2e 100644
--- a/jetson_runtime/docker-compose.yml
+++ b/jetson_runtime/docker-compose.yml
@@ -146,3 +146,15 @@ services:
volumes:
- volume_byodr_sockets:/byodr:rw
- volume_byodr_config:/config:rw
+ coms:
+ cpuset: '3'
+ build:
+ context: .
+ dockerfile: coms/Dockerfile
+ restart: always
+ environment:
+ LD_PRELOAD: libgomp.so.1
+ volumes:
+ - volume_byodr_sockets:/byodr:rw
+ - volume_byodr_config:/config:rw
+ network_mode: 'host'
diff --git a/jetson_runtime/pilot/pilot/app.py b/jetson_runtime/pilot/pilot/app.py
index 9d5169ac..120d3315 100644
--- a/jetson_runtime/pilot/pilot/app.py
+++ b/jetson_runtime/pilot/pilot/app.py
@@ -17,7 +17,7 @@
from tornado.platform.asyncio import AnyThreadEventLoopPolicy
from BYODR_utils.common import Application, ApplicationExit
-from BYODR_utils.common.ipc import JSONPublisher, LocalIPCServer, json_collector
+from BYODR_utils.common.ipc import JSONPublisher, LocalIPCServer, json_collector, JSONServerThread
from BYODR_utils.common.navigate import FileSystemRouteDataSource, ReloadableDataSource
from BYODR_utils.common.option import parse_option
from BYODR_utils.common.usbrelay import SearchUsbRelayFactory, StaticRelayHolder, TransientMemoryRelay
@@ -51,7 +51,8 @@ def __init__(self, event, processor, config_dir=os.getcwd(), hz=100):
self.ipc_server = None
self.ipc_chatter = None
self.teleop = None
- self.ros = None
+ self.coms = None
+ self.movement_commands = None
self.vehicle = None
self.inference = None
self._check_relay_type()
@@ -132,10 +133,11 @@ def finish(self):
def step(self):
try:
- teleop = self.teleop()
- commands = (teleop, self.ros(), self.vehicle(), self.inference())
+ coms = self.coms()
+ commands = (self.coms(), self.vehicle(), self.inference())
pilot = self._processor.next_action(*commands)
- self._monitor.step(pilot, teleop)
+ # print(f"Sending command to relay.py: {pilot}, {coms}.")
+ self._monitor.step(pilot, coms)
if pilot is not None:
self.publisher.publish(pilot)
chat = self.ipc_chatter()
@@ -143,7 +145,7 @@ def step(self):
if chat.get("command") == "restart":
self.setup()
except Exception as e:
- logger.error("Error during step: %s", e)
+ logger.error(f"Error during step: {e}")
self.quit()
@@ -156,21 +158,20 @@ def main():
route_store = ReloadableDataSource(FileSystemRouteDataSource(directory=args.routes, load_instructions=True))
application = PilotApplication(quit_event, processor=CommandProcessor(route_store), config_dir=args.config)
-
- teleop = json_collector(url="ipc:///byodr/teleop.sock", topic=b"aav/teleop/input", event=quit_event)
- ros = json_collector(url="ipc:///byodr/ros.sock", topic=b"aav/ros/input", hwm=10, pop=True, event=quit_event)
+
+ coms = json_collector(url='ipc:///byodr/coms_to_pilot.sock', topic=b'aav/coms/input', event=quit_event)
vehicle = json_collector(url="ipc:///byodr/vehicle.sock", topic=b"aav/vehicle/state", event=quit_event)
inference = json_collector(url="ipc:///byodr/inference.sock", topic=b"aav/inference/state", event=quit_event)
ipc_chatter = json_collector(url="ipc:///byodr/teleop_c.sock", topic=b"aav/teleop/chatter", pop=True, event=quit_event)
- application.teleop = lambda: teleop.get()
- application.ros = lambda: ros.get()
application.vehicle = lambda: vehicle.get()
application.inference = lambda: inference.get()
application.ipc_chatter = lambda: ipc_chatter.get()
+ application.coms = lambda: coms.get()
+
application.publisher = JSONPublisher(url="ipc:///byodr/pilot.sock", topic="aav/pilot/output")
application.ipc_server = LocalIPCServer(url="ipc:///byodr/pilot_c.sock", name="pilot", event=quit_event)
- threads = [teleop, ros, vehicle, inference, ipc_chatter, application.ipc_server, threading.Thread(target=application.run)]
+ threads = [coms, vehicle, inference, ipc_chatter, application.ipc_server, threading.Thread(target=application.run)]
if quit_event.is_set():
return 0
diff --git a/jetson_runtime/pilot/pilot/core.py b/jetson_runtime/pilot/pilot/core.py
index 49d2016b..e311eeea 100644
--- a/jetson_runtime/pilot/pilot/core.py
+++ b/jetson_runtime/pilot/pilot/core.py
@@ -847,24 +847,15 @@ def _cache_safe(self, key, func, *arguments):
finally:
self._cache[key] = 1
- def _process_ros(self, c_ros):
- # It could be a collection of ros commands.
- ros_messages = [] if c_ros is None else c_ros if (isinstance(c_ros, tuple) or isinstance(c_ros, list)) else [c_ros]
- for _msg in ros_messages:
- if "pilot.driver.set" in _msg:
- self._cache_safe("ros switch driver", lambda: self._driver.switch_ctl(_msg.get("pilot.driver.set")))
- if "pilot.maximum.speed" in _msg:
- self._cache_safe("ros set cruise speed", lambda: self._driver.set_cruise_speed(_msg.get("pilot.maximum.speed")))
-
- def _process(self, c_teleop, c_ros, c_inference):
+
+ def _process(self, c_teleop, c_inference):
# r_action = None if c_external is None else c_external.get('action', None)
# if r_action == 'resume':
# self._cache_safe('external api call', lambda: self._driver.switch_ctl('driver_mode.inference.dnn'))
self._driver.process_navigation(c_teleop, c_inference)
- # Continue with ros instructions which take precedence over a route.
- self._process_ros(c_ros)
+
# Zero out max speed on any intervention as a safety rule for ap and to detect faulty controllers.
# With the left button this behavior can be overridden to allow for steer corrections.
@@ -894,7 +885,7 @@ def _process(self, c_teleop, c_ros, c_inference):
elif c_teleop.get("arrow_down", 0) == 1:
self._cache_safe("decrease cruise speed", lambda: self._driver.decrease_cruise_speed())
- def _unpack_commands(self, teleop, ros, vehicle, inference):
+ def _unpack_commands(self, teleop, vehicle, inference):
_ts = timestamp()
# Check if the incoming data is within the patience time limit.
# print("Received teleop data:", teleop)
@@ -903,12 +894,12 @@ def _unpack_commands(self, teleop, ros, vehicle, inference):
vehicle = vehicle if vehicle is not None and (_ts - vehicle.get("time", 0) < self._patience_micro) else None
inference = inference if inference is not None and (_ts - inference.get("time", 0) < self._patience_micro) else None
# ROS commands are processed each time as specified in your existing logic.
- return teleop, ros, vehicle, inference
+ return teleop, vehicle, inference
def next_action(self, *args):
- teleop, ros, vehicle, inference = self._unpack_commands(*args)
+ teleop, vehicle, inference = self._unpack_commands(*args)
# Handle instructions first.
- self._process(teleop, ros, inference)
+ self._process(teleop, inference)
# What to do on message timeout depends on which driver is active.
_ctl = self._driver.get_driver_ctl()
# Switch off autopilot on internal errors.
diff --git a/jetson_runtime/teleop/Dockerfile b/jetson_runtime/teleop/Dockerfile
index 08280760..82cc0b6c 100644
--- a/jetson_runtime/teleop/Dockerfile
+++ b/jetson_runtime/teleop/Dockerfile
@@ -12,7 +12,7 @@ RUN apt-get update && apt-get install -y \
# /\ unzip utility
RUN python3 -m pip install -U pip
-RUN pip3 install pymongo tornado folium Flask flask_socketio paramiko user-agents pysnmp pyasn1 pyasn1-modules
+RUN pip3 install pymongo tornado folium Flask flask_socketio paramiko user-agents pysnmp pyasn1 pyasn1-modules pythonping
# Ignore deprecation problem from cryptography
#/usr/local/lib/python3.6/dist-packages/pymongo/pyopenssl_context.py:26: CryptographyDeprecationWarning: Python 3.6 is no longer supported by the Python core team. Therefore, support for it is deprecated in cryptography. The next release of cryptography will remove support for Python 3.6.
diff --git a/jetson_runtime/teleop/htm/jmuxer/z_index_video_mux.js b/jetson_runtime/teleop/htm/jmuxer/z_index_video_mux.js
index b4057390..9b8276ca 100644
--- a/jetson_runtime/teleop/htm/jmuxer/z_index_video_mux.js
+++ b/jetson_runtime/teleop/htm/jmuxer/z_index_video_mux.js
@@ -32,6 +32,7 @@ if (page_utils.get_stream_type() == 'h264') {
const ws_protocol = (document.location.protocol === "https:") ? "wss://" : "ws://";
const uri = ws_protocol + document.location.hostname + ':' + port;
this.socket = new WebSocket(uri);
+
this.socket.binaryType = 'arraybuffer';
this.socket.attempt_reconnect = true;
this.socket.addEventListener('message', function(event) {
diff --git a/jetson_runtime/teleop/htm/static/CSS/menu_logbox.css b/jetson_runtime/teleop/htm/static/CSS/menu_logbox.css
index 2b1041d7..24180fff 100644
--- a/jetson_runtime/teleop/htm/static/CSS/menu_logbox.css
+++ b/jetson_runtime/teleop/htm/static/CSS/menu_logbox.css
@@ -7,13 +7,13 @@ body {
width: 100%;
}
-table#display {
+#logbox_container table#display {
border-collapse: collapse;
overflow: auto;
width: 100%;
}
-table td {
+#logbox_container table td {
margin-right: 2vh;
margin-left: 2vh;
padding: 10px;
@@ -22,28 +22,28 @@ table td {
white-space: nowrap;
}
-table tbody>tr:nth-child(odd) {
+#logbox_container table tbody>tr:nth-child(odd) {
background-color: var(--secondary_background_light_mode);
}
-table tbody>tr:nth-child(even) {
+#logbox_container table tbody>tr:nth-child(even) {
background-color: var(--tertiary_background_light_mode);
}
-table th {
+#logbox_container table th {
padding: 15px;
font-size: 21px;
font-weight: bold;
}
-table tbody {
+#logbox_container table tbody {
border-collapse: collapse;
border-top: 1px solid #000000;
border-bottom: 1px solid #000000;
}
-button {
+#logbox_container button {
min-width: 45px;
min-height: 45px;
margin: 2px;
@@ -52,38 +52,38 @@ button {
font-weight: bold;
}
-button.currentBtn {
+#logbox_container button.currentBtn {
margin: 0px;
cursor: default;
}
-button#prevBtn,
-button#nextBtn {
+#logbox_container button#prevBtn,
+#logbox_container button#nextBtn {
font-size: 1rem;
font-weight: 400;
}
-button#prevBtn.limit,
-button#nextBtn.limit {
+#logbox_container button#prevBtn.limit,
+#logbox_container button#nextBtn.limit {
cursor: default;
}
-#under_table {
+#logbox_container #under_table {
display: flex;
flex-direction: row;
justify-content: space-between;
width: 100%;
}
-#total_rows_number p {
+#logbox_container #total_rows_number p {
white-space: nowrap;
}
-#pagination {
+#logbox_container #pagination {
white-space: nowrap;
}
-#mySelect {
+#logbox_container #mySelect {
-moz-appearance: none;
-webkit-appearance: none;
appearance: none;
@@ -103,7 +103,7 @@ button#nextBtn.limit {
}
@media (max-width: 768px) {
- #under_table {
+ #logbox_container #under_table {
flex-direction: column;
align-items: center;
align-content: center;
diff --git a/jetson_runtime/teleop/htm/static/CSS/menu_robot_train.css b/jetson_runtime/teleop/htm/static/CSS/menu_robot_train.css
new file mode 100644
index 00000000..b17a8f79
--- /dev/null
+++ b/jetson_runtime/teleop/htm/static/CSS/menu_robot_train.css
@@ -0,0 +1,164 @@
+#container_segment_table,
+#container_connectable_networks_table {
+ margin-bottom: 2em;
+}
+
+
+#connectable_networks_table {
+ margin-bottom: 20px;
+}
+
+#segments_header,
+#header_found_segments {
+ font-size: xx-large;
+}
+
+#segment_table {
+ border-collapse: collapse;
+ border-spacing: 0;
+ -webkit-user-select: none;
+ -ms-user-select: none;
+ user-select: none;
+}
+
+#segment_table th,
+#segment_table td {
+ padding: 1em 0.5em;
+ border: 1px solid rgb(160, 160, 160);
+ border-right: none;
+ border-left: none;
+}
+
+#segment_table th {
+ border-top: none;
+}
+
+/* The Modal (background) */
+.modal {
+ display: none;
+ position: fixed;
+ z-index: 1000;
+ left: 0;
+ top: 0;
+ width: 100%;
+ height: 100%;
+ backdrop-filter: blur(5px);
+ transition: opacity 0.3s ease, visibility 0.3s ease; /* Smooth transition */
+ opacity: 0;
+ visibility: hidden;
+}
+
+.modal.show {
+ opacity: 1;
+ visibility: visible;
+}
+.modal-content {
+ position: relative;
+ margin: auto;
+ padding: 20px;
+ border-radius: 10px;
+ width: 30%;
+ background-color: var(--secondary_background_light_mode);
+ box-shadow: 0px 4px 15px rgba(var(--background_color_dark_mode), 0.2);
+ animation: fadeIn 0.3s ease;
+ top: 50%;
+ transform: translateY(-50%);
+ transition: transform 0.3s ease;
+}
+
+/* Close Button (X mark) */
+.close {
+ position: absolute;
+ top: 10px;
+ right: 15px;
+ font-size: 24px;
+ font-weight: bold;
+ cursor: pointer;
+}
+
+.close:hover,
+.close:focus {
+ color: #000;
+ text-decoration: none;
+ cursor: pointer;
+}
+
+/* Button Group at the bottom */
+.modal-content .button-group {
+ display: flex;
+ justify-content: flex-end;
+ margin-top: 20px;
+}
+
+/* Individual Buttons */
+.modal-content button {
+ margin-left: 10px;
+ padding: 10px 20px;
+ background-color: #4CAF50; /* Green background */
+ color: white;
+ border: none;
+ border-radius: 5px;
+ cursor: pointer;
+}
+
+.modal-content button.cancel {
+ background-color: #f44336; /* Red background for Cancel */
+}
+
+.modal-content button:hover {
+ opacity: 0.9; /* Slight hover effect */
+}
+
+/* Input field */
+.modal-content input {
+ padding: 8px;
+ margin: 15px 0;
+ width: 100%;
+ border: 1px solid #ccc;
+ border-radius: 5px;
+}
+
+/* Animation for fade-in effect */
+@keyframes fadeIn {
+ from { opacity: 0; transform: scale(0.95); }
+ to { opacity: 1; transform: scale(1); }
+}
+
+/* #segment_table td:first-child {
+ cursor: grab;
+}
+
+#segment_table td:first-child::before {
+ content: '☰';
+ display: inline-block;
+ padding-right: 10px;
+ cursor: grab;
+} */
+
+#connectable_networks_table th,
+#connectable_networks_table td {
+ padding: 0.2em 1em;
+}
+
+.floating {
+ box-shadow: 0 10px 20px rgba(0, 0, 0, 0.2);
+ z-index: 1000;
+ transform: scale(1.03);
+ transition: transform 0.2s, box-shadow 0.2s;
+}
+
+.fade-in-left {
+ animation: fadeInLeft 0.5s ease-out;
+}
+
+@keyframes fadeInLeft {
+ from {
+ opacity: 0;
+ transform: translateX(-40px);
+ }
+
+ to {
+ opacity: 1;
+ transform: translateX(0);
+ }
+}
\ No newline at end of file
diff --git a/jetson_runtime/teleop/htm/static/CSS/style.css b/jetson_runtime/teleop/htm/static/CSS/style.css
index df74e0cc..2cdc312b 100644
--- a/jetson_runtime/teleop/htm/static/CSS/style.css
+++ b/jetson_runtime/teleop/htm/static/CSS/style.css
@@ -53,7 +53,8 @@ body {
#logbox_container,
#controls_settings_container,
-#advanced_settings_container {
+#advanced_settings_container,
+#robot_train_settings_container {
display: flex;
flex-direction: column;
justify-content: flex-start;
@@ -65,7 +66,8 @@ body {
#advanced_settings_container .title,
#controls_settings_container .title,
-#logbox_container .title {
+#logbox_container .title,
+#robot_train_settings_container .title {
width: 100%;
margin-bottom: 25px;
padding-top: 40px;
diff --git a/jetson_runtime/teleop/htm/static/CSS/theme_mode.css b/jetson_runtime/teleop/htm/static/CSS/theme_mode.css
index a89bd4b2..063222cc 100644
--- a/jetson_runtime/teleop/htm/static/CSS/theme_mode.css
+++ b/jetson_runtime/teleop/htm/static/CSS/theme_mode.css
@@ -62,7 +62,8 @@ body.dark-theme #video_stream_type {
body.dark-theme #advanced_settings_container .title,
body.dark-theme #controls_settings_container .title,
-body.dark-theme #logbox_container .title {
+body.dark-theme #logbox_container .title,
+body.dark-theme #robot_train_settings_container .title {
color: var(--text_color_dark_mode);
}
@@ -84,6 +85,12 @@ body.dark-theme table tbody>tr:nth-child(even) {
background-color: var(--tertiary_background_dark_mode);
}
+body.dark-theme #robot_train_settings_container .modal-content {
+ background-color: var(--secondary_background_dark_mode);
+ box-shadow: 0px 4px 15px rgba(var(--background_color_light_mode), 0.2);
+
+}
+
/* #endregion */
/* #region AI training feature */
diff --git a/jetson_runtime/teleop/htm/static/JS/router.js b/jetson_runtime/teleop/htm/static/JS/router.js
index 0e013ffb..ac38fb46 100644
--- a/jetson_runtime/teleop/htm/static/JS/router.js
+++ b/jetson_runtime/teleop/htm/static/JS/router.js
@@ -9,7 +9,7 @@ import CTRL_STAT from './mobileController/mobileController_z_state.js';
import { ControlSettings } from './userMenu/menu_controls.js';
import { LogBox } from './userMenu/menu_logbox.js';
import { UserSettingsManager } from './userMenu/menu_settings.js';
-
+import { RobotTrainSettings } from './userMenu/robotConfiguration/robotConfiguration_a_app.js';
export class Router {
constructor(helpMessageManager, messageContainerManager, advancedThemeManager, pipThemeManager, start_all_handlers) {
this.helpMessageManager = helpMessageManager;
@@ -63,7 +63,7 @@ export class Router {
$('#header_bar .left_section').show();
$('#header_bar .right_section').show();
$('.rover_speed_label').css('font-size', '8px');
- if (['settings_link', 'controls_link', 'events_link'].includes(selectedLinkId)) {
+ if (['settings_link', 'controls_link', 'events_link', 'robot_train_link'].includes(selectedLinkId)) {
$('#header_bar .left_section').hide();
$('#header_bar .right_section').hide();
}
@@ -138,6 +138,7 @@ export class Router {
},
],
controls_link: ['/menu_controls', () => new ControlSettings()],
+ robot_train_link: ['/menu_robot_train', () => new RobotTrainSettings()],
events_link: ['/menu_logbox', () => new LogBox()],
};
const [url, callback] = pageMap[selectedLinkId] || [];
diff --git a/jetson_runtime/teleop/htm/static/JS/userMenu/menu_admin.js b/jetson_runtime/teleop/htm/static/JS/userMenu/menu_admin.js
new file mode 100644
index 00000000..09b64e74
--- /dev/null
+++ b/jetson_runtime/teleop/htm/static/JS/userMenu/menu_admin.js
@@ -0,0 +1,197 @@
+// This file isn't used anymore
+
+class RobotTrainSettings {
+ constructor() {
+ // Automatically call the method when an instance is created
+ this.fetchSegmentDataAndDisplay();
+ this.enableDragAndDrop();
+ this.getNanoIP();
+ this.setupWifiNetworksButton();
+ }
+
+ setupWifiNetworksButton() {
+ const wifiButton = document.getElementById('scanWifiNetworks');
+ wifiButton.addEventListener('click', () => {
+ this.getWifiNetworks();
+ });
+ }
+
+ async callRouterApi(action, params = {}) {
+ try {
+ const options = {
+ method: Object.keys(params).length === 0 ? 'GET' : 'POST',
+ headers: {
+ 'Content-Type': 'application/json',
+ },
+ };
+
+ // Add body only for POST requests
+ if (options.method === 'POST') {
+ options.body = JSON.stringify(params);
+ }
+
+ const response = await fetch(`/ssh/router?action=${action}`, options);
+ const contentType = response.headers.get('content-type');
+
+ if (contentType && contentType.includes('application/json')) {
+ return await response.json(); // Handle JSON response
+ } else {
+ return await response.text(); // Handle plain text response
+ }
+ } catch (error) {
+ console.error('Error while calling router endpoint:', error);
+ return null;
+ }
+ }
+
+ // Method to fetch data from the API and display it
+ async fetchSegmentDataAndDisplay() {
+ try {
+ const response = await fetch('/teleop/robot/options');
+ const jsonData = await response.json();
+ console.log(jsonData);
+ // Extract only the segments data
+ const segmentsData = this.extractSegmentsData(jsonData);
+ // Call a function to update the table with segment in robot data
+ this.updateSegmentsTable(segmentsData);
+ } catch (error) {
+ console.error('There has been a problem with your fetch operation:', error);
+ }
+ }
+
+ // Function to extract only the segments data
+ extractSegmentsData(data) {
+ let segmentsData = {};
+ for (const key in data) {
+ if (data.hasOwnProperty(key) && key.startsWith('segment_')) {
+ segmentsData[key] = data[key];
+ }
+ }
+ return segmentsData;
+ }
+
+ updateSegmentsTable(data) {
+ const tbody = document.querySelector('#container_segment_table table tbody');
+ tbody.innerHTML = ''; // Clear existing rows
+ for (const segment in data) {
+ const row = data[segment];
+ const tr = document.createElement('tr');
+
+ tr.innerHTML = `
+
|
+ ${row['position']} |
+ ${row['wifi.name']} |
+ |
+ |
+ `;
+ tbody.appendChild(tr);
+ }
+ }
+
+ async getNanoIP() {
+ const data = await this.callRouterApi('get_nano_ip'); // Calls fetch_ssid function in Router class
+ const showSSID = document.getElementById('dummy_text');
+ // console.log(data);
+ showSSID.innerHTML = data.message;
+ }
+
+ async getWifiNetworks() {
+ try {
+ let data = await this.callRouterApi('get_wifi_networks');
+
+ if (typeof data === 'string') {
+ data = JSON.parse(data);
+ }
+
+ const tbody = document.querySelector('#connectable_networks_table tbody');
+ tbody.innerHTML = '';
+ // console.log(data);
+ data.forEach((network, index) => {
+ const ssid = network['ESSID'];
+ const mac = network['MAC'];
+
+ const tr = document.createElement('tr');
+ const button = document.createElement('button');
+ button.type = 'button';
+ button.textContent = 'Add';
+
+ button.addEventListener('click', () => {
+ this.callRouterApi('add_network', { ssid: ssid, mac: mac });
+ });
+
+ tr.innerHTML = `${ssid} | | `;
+ tr.children[1].appendChild(button);
+
+ // Add animation with a delay
+ tr.style.animationDelay = `${index * 0.1}s`;
+ tr.classList.add('fade-in-left');
+
+ tbody.appendChild(tr);
+ });
+ } catch (error) {
+ console.error('Error fetching WiFi networks:', error);
+ }
+ }
+
+ enableDragAndDrop() {
+ const tbody = document.querySelector('table tbody'); // Select only tbody
+ let draggedElement = null;
+ tbody.addEventListener('touchstart', (e) => handleDragStart(e.target.closest('tr')), false);
+ tbody.addEventListener('touchmove', (e) => handleDragMove(e.touches[0]), false);
+ tbody.addEventListener('touchend', () => handleDragEnd(), false);
+
+ tbody.addEventListener('mousedown', (e) => handleDragStart(e.target.closest('tr')), false);
+ tbody.addEventListener('mousemove', (e) => handleDragMove(e), false);
+ tbody.addEventListener('mouseup', () => handleDragEnd(), false);
+
+ function handleDragStart(row) {
+ if (row && row.parentNode === tbody) {
+ draggedElement = row;
+ // Add a class to the dragged element for the floating effect
+ draggedElement.classList.add('floating');
+ }
+ }
+
+ function handleDragMove(event) {
+ if (!draggedElement) return;
+
+ const targetElement = document.elementFromPoint(event.clientX, event.clientY);
+ const targetRow = targetElement?.closest('tr');
+
+ if (targetRow && targetRow.parentNode === tbody && targetRow !== draggedElement) {
+ swapRows(draggedElement, targetRow);
+ }
+ }
+
+ function handleDragEnd() {
+ if (draggedElement) {
+ // Trigger the reverse transition for landing
+ draggedElement.style.transition = 'transform 0.2s, box-shadow 0.2s';
+ draggedElement.classList.remove('floating');
+
+ // Delay the removal of inline styles to allow the transition to complete
+ setTimeout(() => {
+ draggedElement.style.transition = '';
+ updateRowNumbers();
+ }, 200); // Duration should match the CSS transition time
+
+ draggedElement = null;
+ }
+ }
+
+ function swapRows(row1, row2) {
+ const parentNode = row1.parentNode;
+ const nextSibling = row1.nextElementSibling === row2 ? row1 : row1.nextElementSibling;
+ parentNode.insertBefore(row2, nextSibling);
+ }
+
+ function updateRowNumbers() {
+ const rows = tbody.querySelectorAll('tr');
+ rows.forEach((row, index) => {
+ console.log('made something here');
+ // Assuming the position number is in the second cell
+ row.cells[1].textContent = index + 1;
+ });
+ }
+ }
+}
diff --git a/jetson_runtime/teleop/htm/static/JS/userMenu/robotConfiguration/robotConfiguration_a_app.js b/jetson_runtime/teleop/htm/static/JS/userMenu/robotConfiguration/robotConfiguration_a_app.js
new file mode 100644
index 00000000..7246f278
--- /dev/null
+++ b/jetson_runtime/teleop/htm/static/JS/userMenu/robotConfiguration/robotConfiguration_a_app.js
@@ -0,0 +1,34 @@
+// robotConfiguration_a_main.js
+import { ApiService, PasswordModal, SegmentManager, Utils, WifiNetworkManager } from './robotConfiguration_b_utils.js';
+
+import { fetchSegmentDataAndDisplay, setupRemoveSegmentButtonListener } from './robotConfiguration_c_table_robot.js';
+
+// This file now focuses on DOM elements and linking functions between components and utilities
+class RobotTrainSettings {
+ constructor() {
+ this.setupButtons();
+
+ // Initialize network and segment data
+ ApiService.getNanoIP();
+ fetchSegmentDataAndDisplay();
+ WifiNetworkManager.fetchWifiNetworksAndPopulateTable();
+
+ // Initialize the password modal functionality
+ PasswordModal.init();
+ }
+
+ // Setup the button event listeners
+ setupButtons() {
+ this.setupTestConfigButton();
+ setupRemoveSegmentButtonListener(); // Imported from utils
+ }
+
+ setupTestConfigButton() {
+ const testData = document.getElementById('test_config');
+ testData.addEventListener('click', () => {
+ sendRobotConfig(); // Need to import sendRobotConfig from the appropriate module
+ });
+ }
+}
+
+export { RobotTrainSettings };
diff --git a/jetson_runtime/teleop/htm/static/JS/userMenu/robotConfiguration/robotConfiguration_b_utils.js b/jetson_runtime/teleop/htm/static/JS/userMenu/robotConfiguration/robotConfiguration_b_utils.js
new file mode 100644
index 00000000..b459188b
--- /dev/null
+++ b/jetson_runtime/teleop/htm/static/JS/userMenu/robotConfiguration/robotConfiguration_b_utils.js
@@ -0,0 +1,388 @@
+// robotConfiguration_b_utils.js
+
+import RobotState from './robotConfiguration_z_state.js';
+
+const WifiNetworkManager = (() => {
+ async function fetchWifiNetworksAndPopulateTable() {
+ try {
+ let data = await ApiService.callRouterApi('get_wifi_networks');
+ if (typeof data === 'string') {
+ data = JSON.parse(data);
+ }
+ populateWifiNetworksTable(data);
+ } catch (error) {
+ console.error('Error fetching WiFi networks:', error);
+ }
+ }
+
+ function populateWifiNetworksTable(networks) {
+ const tbody = document.querySelector('#connectable_networks_table tbody');
+ tbody.innerHTML = '';
+
+ networks.forEach((network, index) => {
+ const ssid = network['ESSID'];
+ const mac = network['MAC'];
+ const tr = createNetworkTableRow(ssid, index);
+ const button = createAddNetworkButton();
+ tr.children[1].appendChild(button);
+ tbody.appendChild(tr);
+
+ button.addEventListener('click', () => {
+ PasswordModal.show(ssid, mac);
+ });
+ });
+ }
+
+ function createNetworkTableRow(ssid, index) {
+ const tr = document.createElement('tr');
+ tr.innerHTML = `${ssid} | | `;
+ tr.style.animationDelay = `${index * 0.1}s`;
+ tr.classList.add('fade-in-left');
+ return tr;
+ }
+
+ function createAddNetworkButton() {
+ const button = document.createElement('button');
+ button.type = 'button';
+ button.textContent = 'Add';
+ return button;
+ }
+
+ return {
+ fetchWifiNetworksAndPopulateTable,
+ };
+})();
+
+const SegmentManager = (() => {
+ function addNetworkToSegments(ssid, mac) {
+ let segments = RobotState.segmentsData || {};
+ let newIndex = getNextSegmentIndex(segments);
+ const newSegment = createSegment(ssid, mac, newIndex);
+ const updatedSegments = { ...segments, [`segment_${newIndex}`]: newSegment };
+ RobotState.segmentsData = updatedSegments;
+ }
+
+ function getNextSegmentIndex(segments) {
+ let newIndex = 1;
+ while (segments[`segment_${newIndex}`]) {
+ newIndex++;
+ }
+ return newIndex;
+ }
+
+ function createSegment(ssid, mac, newIndex) {
+ return {
+ 'ip.number': '',
+ 'wifi.name': ssid,
+ 'mac.address': mac,
+ 'vin.number': '',
+ position: newIndex,
+ host: 'False',
+ };
+ }
+
+ function removeSegment(segName) {
+ // Iterate over each segment to find the one with the matching name
+ for (const key in RobotState.segmentsData) {
+ if (RobotState.segmentsData.hasOwnProperty(key)) {
+ const segment = RobotState.segmentsData[key];
+ if (segment['wifi.name'] === segName) {
+ // Delete the segment from the data
+ delete RobotState.segmentsData[key];
+
+ // Reorganize the remaining segments if needed
+ reorganizeSegments();
+ return;
+ }
+ }
+ }
+
+ console.log(`Segment with name ${segName} not found.`);
+ }
+
+ // Function to reorganize segments after deletion
+ function reorganizeSegments() {
+ const newSegmentData = {};
+ let newIndex = 1;
+ for (const key in RobotState.segmentsData) {
+ if (RobotState.segmentsData.hasOwnProperty(key)) {
+ newSegmentData[`segment_${newIndex}`] = RobotState.segmentsData[key];
+ newIndex++;
+ }
+ }
+ RobotState.segmentsData = newSegmentData;
+ }
+
+ /**
+ * Main function that orchestrates the updating of segment positions in the table
+ * and synchronizes these updates with the RobotState.segmentsData.
+ */
+ function updatePositionIndices() {
+ const rows = document.querySelectorAll('#segment_table tbody tr');
+
+ updatePositionsInData(rows);
+ const sortedSegments = collectAndSortSegments();
+ const renamedSegments = renameSegmentKeys(sortedSegments);
+ removeAllSegments();
+ reAddSegments(renamedSegments);
+ }
+
+ /**
+ * Updates the positions of segments in the RobotState.segmentsData based on the
+ * current order of rows in the table.
+ * @param {NodeListOf} rows - The rows of the table.
+ */
+ function updatePositionsInData(rows) {
+ rows.forEach((row, index) => {
+ const wifiNameCell = row.cells[2];
+ const wifiName = wifiNameCell.textContent;
+ const positionCell = row.cells[1];
+ if (positionCell) {
+ positionCell.textContent = index + 1;
+ }
+
+ for (let segment in RobotState.segmentsData) {
+ if (RobotState.segmentsData[segment]['wifi.name'] === wifiName) {
+ RobotState.segmentsData[segment].position = index + 1;
+ break;
+ }
+ }
+ });
+ }
+
+ /**
+ * Collects all segments from RobotState.segmentsData, sorts them based on their
+ * updated position, and returns the sorted array.
+ * @returns {Array} An array of sorted segments.
+ */
+ function collectAndSortSegments() {
+ let updatedSegments = [];
+ for (let segment in RobotState.segmentsData) {
+ if (segment.startsWith('segment_')) {
+ updatedSegments.push({ key: segment, data: RobotState.segmentsData[segment] });
+ }
+ }
+ return updatedSegments.sort((a, b) => a.data.position - b.data.position);
+ }
+
+ /**
+ * Renames the keys of the segment objects to match their position in the sorted array.
+ * @param {Array} segments - The array of segments to rename.
+ * @returns {Array} An array of segments with updated keys.
+ */
+ function renameSegmentKeys(segments) {
+ return segments.map((segment, index) => ({
+ key: `segment_${index + 1}`,
+ data: segment.data,
+ }));
+ }
+
+ /**
+ * Removes all segments from RobotState.segmentsData that start with "segment_".
+ */
+ function removeAllSegments() {
+ Object.keys(RobotState.segmentsData)
+ .filter((key) => key.startsWith('segment_'))
+ .forEach((segKey) => removeSegment(RobotState.segmentsData[segKey]['wifi.name']));
+ }
+
+ /**
+ * Adds the given segments back into RobotState.segmentsData.
+ * @param {Array} segments - The array of segments to be re-added.
+ */
+ function reAddSegments(segments) {
+ segments.forEach((segment) => {
+ RobotState.segmentsData[segment.key] = segment.data;
+ });
+ }
+
+ return {
+ addNetworkToSegments,
+ removeSegment,
+ reorganizeSegments,
+ updatePositionIndices,
+ };
+})();
+
+const ApiService = (() => {
+ async function callRouterApi(action, params = {}) {
+ try {
+ const options = {
+ method: Object.keys(params).length === 0 ? 'GET' : 'POST',
+ headers: {
+ 'Content-Type': 'application/json',
+ },
+ };
+
+ // Add body only for POST requests
+ if (options.method === 'POST') {
+ options.body = JSON.stringify(params);
+ }
+
+ const response = await fetch(`/ssh/router?action=${action}`, options);
+ const contentType = response.headers.get('content-type');
+
+ if (contentType && contentType.includes('application/json')) {
+ return await response.json(); // Handle JSON response
+ } else {
+ return await response.text(); // Handle plain text response
+ }
+ } catch (error) {
+ console.error('Error while calling router endpoint:', error);
+ return null;
+ }
+ }
+
+ async function postConfigData(data) {
+ return fetch('/teleop/send_config', {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/json',
+ },
+ body: JSON.stringify(data),
+ });
+ }
+
+ async function getNanoIP() {
+ const data = await callRouterApi('get_nano_ip');
+ const showSSID = document.getElementById('dummy_text');
+ showSSID.innerHTML = data.message;
+ }
+
+ return {
+ callRouterApi,
+ postConfigData,
+ getNanoIP,
+ };
+})();
+
+const Utils = (() => {
+ function showToast(message) {
+ const toast = document.createElement('div');
+ toast.textContent = message;
+ toast.style.position = 'fixed';
+ toast.style.bottom = '10px';
+ toast.style.left = '50%';
+ toast.style.transform = 'translateX(-50%)';
+ toast.style.backgroundColor = 'black';
+ toast.style.color = 'white';
+ toast.style.padding = '10px';
+ toast.style.borderRadius = '7px';
+ toast.style.zIndex = '1000';
+
+ document.body.appendChild(toast);
+
+ setTimeout(() => {
+ toast.remove();
+ }, 3000);
+ }
+
+ function generatePassword(ssid) {
+ const networkParts = ssid.split('_');
+ const suffix = networkParts[1]; // part after '_'
+ // Find the first alphabetic character in the suffix
+ const firstChar = suffix.match(/[A-Za-z]/) ? suffix.match(/[A-Za-z]/)[0] : null;
+ // Extract the digit (if any) in the suffix
+ const digitInName = parseInt(suffix.match(/\d+/), 10); // extract digits
+
+ if (firstChar) {
+ const position = firstChar.toUpperCase().charCodeAt(0) - 'A'.charCodeAt(0) + 1;
+
+ // Only return the pre-generated password if the digit matches the letter position
+ if (digitInName === position) {
+ return `voiarcps1n${position}`; // Valid password
+ }
+ }
+
+ return null; // Show empty if there's no valid match
+ }
+
+ return {
+ showToast,
+ generatePassword,
+ };
+})();
+
+const PasswordModal = (() => {
+ let modal;
+ let elPasswordInput;
+ let elNetworkNameSpan;
+ let elConfirmButton;
+ let closeBtn;
+ let cancelButton;
+
+ function init() {
+ // Access DOM elements inside the init function
+ modal = document.getElementById('passwordModal');
+ elPasswordInput = modal.querySelector('#password-input');
+ elNetworkNameSpan = modal.querySelector('#network-name');
+ elConfirmButton = modal.querySelector('#confirm-password-button');
+ closeBtn = modal.querySelector('#password-modal-close');
+ cancelButton = modal.querySelector('#cancel-button');
+
+ // Close the modal when the X button is clicked
+ closeBtn.onclick = hidePasswordPrompt;
+
+ // Close the modal if the user clicks outside of it
+ window.onclick = (event) => {
+ if (event.target === modal) {
+ hidePasswordPrompt();
+ }
+ };
+
+ // Handle the confirmation of the password
+ elConfirmButton.addEventListener('click', () => {
+ const password = elPasswordInput.value;
+ if (password) {
+ const ssid = elNetworkNameSpan.textContent;
+ const mac = elConfirmButton.dataset.mac; // store mac in dataset
+ handleNetworkAddition(ssid, mac, password);
+ hidePasswordPrompt();
+ }
+ });
+
+ // Close modal on cancel
+ cancelButton.addEventListener('click', hidePasswordPrompt);
+
+ // Disable the confirm button initially if password is empty
+ elPasswordInput.addEventListener('input', () => {
+ elConfirmButton.disabled = !elPasswordInput.value;
+ });
+ }
+
+ function show(ssid, mac) {
+ const generatedPassword = Utils.generatePassword(ssid);
+ elPasswordInput.value = generatedPassword || '';
+ elNetworkNameSpan.textContent = ssid;
+ elConfirmButton.dataset.mac = mac;
+
+ // Show the modal
+ showPasswordPrompt();
+
+ // Disable the confirm button if password is empty
+ elConfirmButton.disabled = !elPasswordInput.value;
+ }
+
+ function showPasswordPrompt() {
+ modal.classList.add('show');
+ modal.style.display = 'block'; // Ensure it becomes visible
+ }
+
+ function hidePasswordPrompt() {
+ modal.classList.remove('show');
+ modal.style.display = 'none'; // Ensure it hides
+ }
+
+ function handleNetworkAddition(ssid, mac, password) {
+ SegmentManager.addNetworkToSegments(ssid, mac);
+ ApiService.callRouterApi('add_network', { ssid, mac, password });
+ updateSegmentsTable();
+ }
+
+ return {
+ init,
+ show,
+ };
+})();
+
+export { ApiService, PasswordModal, SegmentManager, Utils, WifiNetworkManager };
diff --git a/jetson_runtime/teleop/htm/static/JS/userMenu/robotConfiguration/robotConfiguration_c_table_robot.js b/jetson_runtime/teleop/htm/static/JS/userMenu/robotConfiguration/robotConfiguration_c_table_robot.js
new file mode 100644
index 00000000..41e23e82
--- /dev/null
+++ b/jetson_runtime/teleop/htm/static/JS/userMenu/robotConfiguration/robotConfiguration_c_table_robot.js
@@ -0,0 +1,133 @@
+import { SegmentManager } from './robotConfiguration_b_utils.js';
+import RobotState from './robotConfiguration_z_state.js';
+
+let tbody;
+let draggedElement = null;
+
+async function initialize() {
+ tbody = await waitForTable();
+}
+
+// Method to fetch data from the API and display it
+async function fetchSegmentDataAndDisplay() {
+ try {
+ const response = await fetch('/teleop/robot/options');
+ const jsonData = await response.json();
+ RobotState.robotConfigData = jsonData;
+ RobotState.segmentsData = jsonData;
+ // console.log(RobotState.segmentsData)
+ updateSegmentsTable();
+ } catch (error) {
+ console.error('There has been a problem with your fetch operation:', error);
+ }
+}
+
+function waitForTable() {
+ return new Promise((resolve) => {
+ const checkExist = setInterval(() => {
+ const tbody = document.querySelector('#container_segment_table table tbody');
+ if (tbody) {
+ clearInterval(checkExist);
+ resolve(tbody);
+ }
+ }, 100); // Check every 100ms
+ });
+}
+
+function updateSegmentsTable() {
+ tbody.innerHTML = '';
+ for (const segment in RobotState.segmentsData) {
+ if (RobotState.segmentsData.hasOwnProperty(segment) && segment.startsWith('segment_')) {
+ const row = RobotState.segmentsData[segment];
+ const tr = document.createElement('tr');
+ const isMainSegment = row['host'] === 'True';
+
+ tr.innerHTML = `
+ |
+ |
+ ${row['wifi.name']} |
+ |
+ ${isMainSegment ? `` : ` | `}`;
+ // User cannot remove the host segment as this is where they are connected to
+ tbody.appendChild(tr);
+ }
+ }
+ SegmentManager.updatePositionIndices();
+}
+
+function setupRemoveSegmentButtonListener() {
+ document.addEventListener('click', (e) => {
+ if (e.target.matches('#segment_table tbody button[data-wifiname]')) {
+ const wifiName = e.target.getAttribute('data-wifiname');
+ removeSegmentAndUpdate(wifiName);
+ }
+ });
+}
+
+// Handle segment removal and table update
+function removeSegmentAndUpdate(wifiName) {
+ SegmentManager.removeSegment(wifiName);
+ //TODO: this function should also call the backend to append the changes
+ updateSegmentsTable();
+}
+
+function enableDragAndDrop() {
+ // Touch and mouse events
+ $('#application-content-container').on('touchstart', '#segment_table tbody', (e) => handleDragStart(e));
+ $('#application-content-container').on('touchmove', '#segment_table tbody', (e) => handleDragMove(e.touches[0]));
+ $('#application-content-container').on('touchend', '#segment_table tbody', () => handleDragEnd());
+ $('#application-content-container').on('mousedown', '#segment_table tbody', (e) => handleDragStart(e));
+ $('#application-content-container').on('mousemove', '#segment_table tbody', (e) => handleDragMove(e));
+ $('#application-content-container').on('mouseup', '#segment_table tbody', () => handleDragEnd());
+}
+
+function handleDragStart(event) {
+ if (event.target === event.target.closest('tr').firstElementChild) {
+ const row = event.target.closest('tr');
+ if (row && row.parentNode === tbody) {
+ draggedElement = row;
+ draggedElement.classList.add('floating');
+ }
+ }
+}
+
+function handleDragMove(event) {
+ if (!draggedElement) return;
+
+ const targetElement = document.elementFromPoint(event.clientX, event.clientY);
+ const targetRow = targetElement?.closest('tr');
+
+ if (targetRow && targetRow.parentNode === tbody && targetRow !== draggedElement) {
+ swapRows(draggedElement, targetRow);
+ }
+}
+
+function handleDragEnd() {
+ if (draggedElement) {
+ // Ensure that draggedElement is a valid DOM element
+ if (draggedElement instanceof HTMLElement) {
+ draggedElement.style.transition = 'transform 0.2s, box-shadow 0.2s';
+ draggedElement.classList.remove('floating');
+ setTimeout(() => {
+ if (draggedElement instanceof HTMLElement) {
+ draggedElement.style.transition = '';
+ }
+ }, 200);
+ }
+
+ draggedElement = null;
+
+ SegmentManager.updatePositionIndices();
+ }
+}
+
+function swapRows(row1, row2) {
+ const parentNode = row1.parentNode;
+ const nextSibling = row1.nextElementSibling === row2 ? row1 : row1.nextElementSibling;
+ parentNode.insertBefore(row2, nextSibling);
+}
+
+document.addEventListener('DOMContentLoaded', initialize);
+
+export { enableDragAndDrop, fetchSegmentDataAndDisplay, setupRemoveSegmentButtonListener, updateSegmentsTable };
+
diff --git a/jetson_runtime/teleop/htm/static/JS/userMenu/robotConfiguration/robotConfiguration_z_state.js b/jetson_runtime/teleop/htm/static/JS/userMenu/robotConfiguration/robotConfiguration_z_state.js
new file mode 100644
index 00000000..8160c92b
--- /dev/null
+++ b/jetson_runtime/teleop/htm/static/JS/userMenu/robotConfiguration/robotConfiguration_z_state.js
@@ -0,0 +1,59 @@
+class RobotState {
+ // Data in segments table
+ #segmentsData = []
+ //Save the raw data from the robot_config.ini
+ #robotConfigData = []
+
+
+ set segmentsData(newSegments) {
+ // Calculate the length of old and new data
+ const oldLength = Object.keys(this.#segmentsData).length;
+ const newLength = Object.keys(newSegments).length;
+ // If new data has additional segments
+ if (newLength > oldLength) {
+ // Identify the new segments only
+ const newSegmentKeys = Object.keys(newSegments).slice(oldLength);
+ // Check each new segment for duplicates in the existing data
+ for (const key of newSegmentKeys) {
+ const newSegment = newSegments[key];
+ let isDuplicate = false;
+ for (const existingKey in this.#segmentsData) {
+ if (this.#segmentsData.hasOwnProperty(existingKey)) {
+ const existingSegment = this.#segmentsData[existingKey];
+ if (existingSegment['wifi.name'] === newSegment['wifi.name'] &&
+ existingSegment['mac.address'] === newSegment['mac.address']) {
+ console.log(`${newSegment['wifi.name']} network is already added.`);
+ isDuplicate = true;
+ break;
+ }
+ }
+ }
+
+ // If any new segment is a duplicate, retain the old data and return
+ if (isDuplicate) {
+ return;
+ }
+ }
+
+ // If there are no duplicates, update #segmentData with the new data
+ this.#segmentsData = newSegments;
+ }
+ }
+
+ get segmentsData() {
+ return this.#segmentsData;
+ }
+
+ set robotConfigData(value) {
+ this.#robotConfigData = value
+ }
+ get robotConfigData() {
+ return this.#robotConfigData
+ }
+
+}
+
+const sharedState = new RobotState();
+
+// Shared instance will make sure that all the imports can access the same value without change in them
+export default sharedState;
diff --git a/jetson_runtime/teleop/htm/templates/index.html b/jetson_runtime/teleop/htm/templates/index.html
index 8cab77e3..e687670d 100644
--- a/jetson_runtime/teleop/htm/templates/index.html
+++ b/jetson_runtime/teleop/htm/templates/index.html
@@ -11,6 +11,7 @@
+
@@ -90,6 +91,21 @@
Controls
+
+ Robot Train