From b9aa0970817018837c0a8e29d5908f945e8b7382 Mon Sep 17 00:00:00 2001 From: bdamokos <163609735+bdamokos@users.noreply.github.com> Date: Thu, 19 Dec 2024 22:08:45 +0100 Subject: [PATCH] webserial wifi setup --- docs/setup/index.html | 19 +++- docs/setup/js/wifi_setup.js | 184 ++++++++++++++++++++++++++++++++++++ webusb_server.py | 8 +- wifi_config.py | 165 ++++++++++++++++++++++++++++++++ 4 files changed, 370 insertions(+), 6 deletions(-) create mode 100644 docs/setup/js/wifi_setup.js create mode 100644 wifi_config.py diff --git a/docs/setup/index.html b/docs/setup/index.html index ff8bbe5..589418f 100644 --- a/docs/setup/index.html +++ b/docs/setup/index.html @@ -17,31 +17,40 @@

Connect Your Display

E-Paper Display Setup Wizard

-

Step 1: Basic Configuration

-

Let's start by setting up your basic display configuration.

+

Step 1: WiFi Configuration

+

Configure your WiFi networks.

+
+ +
+
+ +
+

Step 2: Basic Configuration

+

Set up your basic display configuration.

-

Step 2: Transit Configuration

+

Step 3: Transit Configuration

Configure your transit stops and preferences.

-

Step 3: Weather Configuration

+

Step 4: Weather Configuration

Set up your weather display preferences.

-

Step 4: Review & Finish

+

Step 5: Review & Finish

Review your configuration and start the display service.

+ \ No newline at end of file diff --git a/docs/setup/js/wifi_setup.js b/docs/setup/js/wifi_setup.js new file mode 100644 index 0000000..d5dc9ff --- /dev/null +++ b/docs/setup/js/wifi_setup.js @@ -0,0 +1,184 @@ +// WiFi configuration functionality +async function startWiFiSetup() { + try { + // Get available networks + const response = await displayDevice.sendCommand(0x10); + const networks = JSON.parse(new TextDecoder().decode(response.data)); + + // Get saved networks + const savedResponse = await displayDevice.sendCommand(0x11); + const savedNetworks = JSON.parse(new TextDecoder().decode(savedResponse.data)); + + // Show WiFi setup form + showWiFiSetupForm(networks, savedNetworks); + } catch (error) { + console.error('Failed to start WiFi setup:', error); + alert('Failed to get WiFi networks. Please try reconnecting the device.'); + } +} + +function showWiFiSetupForm(networks, savedNetworks) { + const setupDiv = document.getElementById('wifi-setup'); + if (!setupDiv) return; + + // Create HTML for available networks + let html = ` +

Available Networks

+
+ `; + + if (networks.error) { + html += `

${networks.error}

`; + } else if (networks.networks && networks.networks.length > 0) { + networks.networks.forEach(network => { + html += ` +
+ ${network.ssid} + ${network.signal}% + +
+ `; + }); + } else { + html += '

No networks found

'; + } + + html += '
'; + + // Add saved networks section + html += ` +

Saved Networks

+
+ `; + + if (savedNetworks.error) { + html += `

${savedNetworks.error}

`; + } else if (savedNetworks.saved_networks && savedNetworks.saved_networks.length > 0) { + savedNetworks.saved_networks.forEach(network => { + html += ` +
+ ${network.ssid} + +
+ `; + }); + } else { + html += '

No saved networks

'; + } + + html += '
'; + + // Add refresh button + html += ` +
+ +
+ `; + + setupDiv.innerHTML = html; +} + +async function connectToNetwork(ssid, requiresPassword) { + let password = ''; + if (requiresPassword) { + password = prompt(`Enter password for ${ssid}:`); + if (!password) return; // User cancelled + } + + try { + const response = await displayDevice.sendCommand(0x12, JSON.stringify({ + ssid: ssid, + password: password + })); + + const result = JSON.parse(new TextDecoder().decode(response.data)); + if (result.error) { + alert(`Failed to connect: ${result.error}`); + } else { + alert(`Successfully connected to ${ssid}`); + // Refresh the network list + startWiFiSetup(); + } + } catch (error) { + console.error('Failed to connect:', error); + alert('Failed to connect to network. Please try again.'); + } +} + +async function forgetNetwork(uuid) { + if (!confirm('Are you sure you want to remove this network?')) return; + + try { + const response = await displayDevice.sendCommand(0x13, JSON.stringify({ + uuid: uuid + })); + + const result = JSON.parse(new TextDecoder().decode(response.data)); + if (result.error) { + alert(`Failed to remove network: ${result.error}`); + } else { + // Refresh the network list + startWiFiSetup(); + } + } catch (error) { + console.error('Failed to forget network:', error); + alert('Failed to remove network. Please try again.'); + } +} + +// Add styles for WiFi setup +const style = document.createElement('style'); +style.textContent = ` + .network-list, .saved-network-list { + margin: 1em 0; + max-height: 300px; + overflow-y: auto; + } + + .network-item { + display: flex; + align-items: center; + padding: 0.5em; + border-bottom: 1px solid #eee; + } + + .network-name { + flex-grow: 1; + margin-right: 1em; + } + + .network-signal { + margin-right: 1em; + color: #666; + } + + .error { + color: red; + margin: 1em 0; + } + + .network-controls { + margin-top: 1em; + text-align: center; + } + + .refresh-button { + background-color: #4CAF50; + color: white; + padding: 10px 20px; + border: none; + border-radius: 4px; + cursor: pointer; + } + + .refresh-button:hover { + background-color: #45a049; + } +`; +document.head.appendChild(style); \ No newline at end of file diff --git a/webusb_server.py b/webusb_server.py index 485a6c4..c2a8fe7 100644 --- a/webusb_server.py +++ b/webusb_server.py @@ -4,6 +4,7 @@ import logging from pathlib import Path import dotenv +from wifi_config import handle_wifi_command # Configure logging logging.basicConfig(level=logging.INFO) @@ -61,7 +62,12 @@ def save_config(self, config): def handle_command(self, command, data=None): """Handle incoming USB commands""" try: - if command == 0x01: # GET_CONFIG + # WiFi configuration commands (0x10-0x1F) + if 0x10 <= command <= 0x1F: + return handle_wifi_command(command, data) + + # General configuration commands + elif command == 0x01: # GET_CONFIG return json.dumps(self.get_config()) elif command == 0x02: # SAVE_CONFIG diff --git a/wifi_config.py b/wifi_config.py new file mode 100644 index 0000000..6302d4c --- /dev/null +++ b/wifi_config.py @@ -0,0 +1,165 @@ +#!/usr/bin/env python3 +import subprocess +import logging +import json +import os + +logger = logging.getLogger(__name__) + +class WiFiConfig: + """Handles WiFi configuration commands for WebUSB interface""" + + def __init__(self): + self.nmcli_available = self._check_nmcli() + + def _check_nmcli(self): + """Check if nmcli is available""" + try: + subprocess.run(['which', 'nmcli'], check=True, capture_output=True) + return True + except subprocess.CalledProcessError: + logger.warning("nmcli not found. WiFi configuration will be limited.") + return False + + def get_available_networks(self): + """Get list of available WiFi networks""" + if not self.nmcli_available: + return {"error": "nmcli not available"} + + try: + # Force English language output + env = os.environ.copy() + env['LC_ALL'] = 'C' + + result = subprocess.run( + ['sudo', 'nmcli', '-t', '-f', 'SSID,SIGNAL,SECURITY', 'dev', 'wifi', 'list'], + capture_output=True, + text=True, + env=env + ) + + networks = [] + for line in result.stdout.splitlines(): + if line: + ssid, signal, security = line.split(':') + if ssid: # Skip empty SSIDs + networks.append({ + 'ssid': ssid, + 'signal': int(signal) if signal.isdigit() else 0, + 'security': security != '' + }) + + # Sort by signal strength + networks.sort(key=lambda x: x['signal'], reverse=True) + return {'networks': networks} + + except Exception as e: + logger.error(f"Error getting WiFi networks: {e}") + return {"error": str(e)} + + def connect_to_network(self, ssid, password=None): + """Connect to a WiFi network""" + if not self.nmcli_available: + return {"error": "nmcli not available"} + + try: + cmd = ['sudo', 'nmcli', 'dev', 'wifi', 'connect', ssid] + if password: + cmd.extend(['password', password]) + + result = subprocess.run(cmd, capture_output=True, text=True) + + if result.returncode == 0: + return {"status": "success", "message": f"Connected to {ssid}"} + else: + return {"error": f"Failed to connect: {result.stderr}"} + + except Exception as e: + logger.error(f"Error connecting to network: {e}") + return {"error": str(e)} + + def get_saved_networks(self): + """Get list of saved WiFi networks""" + if not self.nmcli_available: + return {"error": "nmcli not available"} + + try: + env = os.environ.copy() + env['LC_ALL'] = 'C' + + result = subprocess.run( + ['sudo', 'nmcli', '-t', '-f', 'NAME,UUID,TYPE', 'connection', 'show'], + capture_output=True, + text=True, + env=env + ) + + networks = [] + for line in result.stdout.splitlines(): + name, uuid, conn_type = line.split(':') + if conn_type == '802-11-wireless': + networks.append({ + 'ssid': name, + 'uuid': uuid + }) + + return {'saved_networks': networks} + + except Exception as e: + logger.error(f"Error getting saved networks: {e}") + return {"error": str(e)} + + def forget_network(self, uuid): + """Remove a saved WiFi network""" + if not self.nmcli_available: + return {"error": "nmcli not available"} + + try: + result = subprocess.run( + ['sudo', 'nmcli', 'connection', 'delete', uuid], + capture_output=True, + text=True + ) + + if result.returncode == 0: + return {"status": "success", "message": f"Removed network {uuid}"} + else: + return {"error": f"Failed to remove network: {result.stderr}"} + + except Exception as e: + logger.error(f"Error removing network: {e}") + return {"error": str(e)} + +# Command handlers for WebUSB server +def handle_wifi_command(command, data=None): + """Handle WiFi-related WebUSB commands""" + wifi = WiFiConfig() + + try: + if command == 0x10: # GET_AVAILABLE_NETWORKS + return json.dumps(wifi.get_available_networks()) + + elif command == 0x11: # GET_SAVED_NETWORKS + return json.dumps(wifi.get_saved_networks()) + + elif command == 0x12: # CONNECT_TO_NETWORK + if not data: + return json.dumps({"error": "No network data provided"}) + network_data = json.loads(data) + return json.dumps(wifi.connect_to_network( + network_data.get('ssid'), + network_data.get('password') + )) + + elif command == 0x13: # FORGET_NETWORK + if not data: + return json.dumps({"error": "No network UUID provided"}) + network_data = json.loads(data) + return json.dumps(wifi.forget_network(network_data.get('uuid'))) + + else: + return json.dumps({"error": f"Unknown WiFi command: {command}"}) + + except Exception as e: + logger.error(f"Error handling WiFi command {command}: {e}") + return json.dumps({"error": str(e)}) \ No newline at end of file