Add Hotspot configuration

This commit is contained in:
Thomas Farstrike
2026-03-17 15:17:38 +01:00
parent ff21743fbb
commit 5b50ce8528
10 changed files with 717 additions and 307 deletions
@@ -0,0 +1,23 @@
{
"name": "Hotspot",
"publisher": "MicroPythonOS",
"short_description": "Configure Wi-Fi hotspot settings.",
"long_description": "Configure and toggle the device Wi-Fi hotspot, including SSID, security, and network options.",
"icon_url": "https://apps.micropythonos.com/apps/com.micropythonos.hotspot/icons/com.micropythonos.hotspot_0.1.0_64x64.png",
"download_url": "https://apps.micropythonos.com/apps/com.micropythonos.hotspot/mpks/com.micropythonos.hotspot_0.1.0.mpk",
"fullname": "com.micropythonos.hotspot",
"version": "0.1.0",
"category": "networking",
"activities": [
{
"entrypoint": "assets/hotspot.py",
"classname": "Hotspot",
"intent_filters": [
{
"action": "main",
"category": "launcher"
}
]
}
]
}
@@ -0,0 +1,112 @@
import lvgl as lv
from mpos import Activity, Intent, SettingsActivity, SharedPreferences, WifiService
class Hotspot(SettingsActivity):
"""
Hotspot configuration app.
Uses SettingsActivity to render and edit hotspot preferences stored under
com.micropythonos.system.hotspot.
"""
DEFAULTS = {
"enabled": False,
"ssid": "MicroPythonOS",
"password": "",
"channel": 1,
"hidden": False,
"max_clients": 4,
"authmode": None,
"ip": "192.168.4.1",
"netmask": "255.255.255.0",
"gateway": "192.168.4.1",
"dns": "8.8.8.8",
}
def getIntent(self):
prefs = SharedPreferences("com.micropythonos.system.hotspot", defaults=self.DEFAULTS)
intent = Intent()
intent.putExtra("prefs", prefs)
intent.putExtra(
"settings",
[
{
"title": "Hotspot Enabled",
"key": "enabled",
"ui": "radiobuttons",
"ui_options": [("On", "True"), ("Off", "False")],
"changed_callback": self.toggle_hotspot,
},
{
"title": "Network Name (SSID)",
"key": "ssid",
"placeholder": "Hotspot SSID",
},
{
"title": "Password",
"key": "password",
"placeholder": "Leave empty for open network",
},
{
"title": "Channel",
"key": "channel",
"placeholder": "Wi-Fi channel, e.g. 1",
},
{
"title": "Hidden Network",
"key": "hidden",
"ui": "radiobuttons",
"ui_options": [("Visible", "False"), ("Hidden", "True")],
"changed_callback": self.toggle_hotspot,
},
{
"title": "Max Clients",
"key": "max_clients",
"placeholder": "Max connections, e.g. 4",
},
{
"title": "Auth Mode",
"key": "authmode",
"ui": "dropdown",
"ui_options": [
("Auto", None),
("Open", "open"),
("WPA", "wpa"),
("WPA2", "wpa2"),
("WPA/WPA2", "wpa_wpa2"),
],
"changed_callback": self.toggle_hotspot,
},
{
"title": "IP Address",
"key": "ip",
"placeholder": "Hotspot IP, e.g. 192.168.4.1",
},
{
"title": "Netmask",
"key": "netmask",
"placeholder": "Netmask, e.g. 255.255.255.0",
},
{
"title": "Gateway",
"key": "gateway",
"placeholder": "Gateway, e.g. 192.168.4.1",
},
{
"title": "DNS",
"key": "dns",
"placeholder": "DNS, e.g. 8.8.8.8",
},
],
)
return intent
def toggle_hotspot(self, new_value):
enabled_value = self.prefs.get_string("enabled", "False")
should_enable = str(enabled_value).lower() in ("true", "1", "yes", "on")
if should_enable:
WifiService.enable_hotspot()
else:
WifiService.disable_hotspot()
Binary file not shown.

After

Width:  |  Height:  |  Size: 6.1 KiB

@@ -12,6 +12,12 @@ class LaunchWiFi(Activity):
AppManager.start_app("com.micropythonos.wifi")
class LaunchHotspot(Activity):
def onCreate(self):
AppManager.start_app("com.micropythonos.hotspot")
class Settings(SettingsActivity):
"""Override getIntent to provide prefs and settings via Intent extras"""
@@ -46,6 +52,7 @@ class Settings(SettingsActivity):
intent.putExtra("prefs", SharedPreferences("com.micropythonos.settings"))
intent.putExtra("settings", [
{"title": "Wi-Fi", "key": "wifi_settings", "ui": "activity", "activity_class": LaunchWiFi},
{"title": "Hotspot", "key": "hotspot_settings", "ui": "activity", "activity_class": LaunchHotspot},
# Basic settings, alphabetically:
{"title": "Light/Dark Theme", "key": "theme_light_dark", "ui": "radiobuttons", "ui_options": [("Light", "light"), ("Dark", "dark")], "changed_callback": self.theme_changed},
{"title": "Theme Color", "key": "theme_primary_color", "placeholder": "HTML hex color, like: EC048C", "ui": "dropdown", "ui_options": theme_colors, "changed_callback": self.theme_changed, "default_value": AppearanceManager.DEFAULT_PRIMARY_COLOR},
@@ -46,6 +46,129 @@ class WifiService:
# Desktop mode: simulated connected SSID (None = not connected)
_desktop_connected_ssid = None
# Hotspot state tracking
hotspot_enabled = False
_temp_disable_state = None
_needs_hotspot_restore = False
@staticmethod
def _get_hotspot_config():
prefs = mpos.config.SharedPreferences("com.micropythonos.system.hotspot")
return {
"enabled": prefs.get_bool("enabled", False),
"ssid": prefs.get_string("ssid", "MicroPythonOS"),
"password": prefs.get_string("password", ""),
"channel": prefs.get_int("channel", 1),
"hidden": prefs.get_bool("hidden", False),
"max_clients": prefs.get_int("max_clients", 4),
"authmode": prefs.get_string("authmode", None),
"ip": prefs.get_string("ip", "192.168.4.1"),
"netmask": prefs.get_string("netmask", "255.255.255.0"),
"gateway": prefs.get_string("gateway", "192.168.4.1"),
"dns": prefs.get_string("dns", "8.8.8.8"),
}
@staticmethod
def _resolve_hotspot_authmode(net, password, authmode_value):
if authmode_value is None:
if password:
return net.AUTH_WPA_WPA2_PSK
return net.AUTH_OPEN
if isinstance(authmode_value, int):
return authmode_value
if isinstance(authmode_value, str):
authmode_key = authmode_value.lower().strip()
mapping = {
"open": net.AUTH_OPEN,
"wpa": net.AUTH_WPA_PSK,
"wpa2": net.AUTH_WPA2_PSK,
"wpa_wpa2": net.AUTH_WPA_WPA2_PSK,
"wpa-wpa2": net.AUTH_WPA_WPA2_PSK,
}
return mapping.get(authmode_key, net.AUTH_WPA_WPA2_PSK)
return net.AUTH_WPA_WPA2_PSK
@staticmethod
def enable_hotspot(network_module=None):
if WifiService.wifi_busy:
print("WifiService: Cannot enable hotspot, WiFi is busy")
return False
if not HAS_NETWORK_MODULE and network_module is None:
WifiService.hotspot_enabled = True
print("WifiService: Desktop mode, hotspot enabled (simulated)")
return True
net = network_module if network_module else network
config = WifiService._get_hotspot_config()
try:
sta = net.WLAN(net.STA_IF)
if sta.active() or sta.isconnected():
sta.disconnect()
sta.active(False)
ap = net.WLAN(net.AP_IF)
ap.active(True)
authmode = WifiService._resolve_hotspot_authmode(
net, config.get("password"), config.get("authmode")
)
ap_config = {
"essid": config.get("ssid"),
"channel": config.get("channel"),
"hidden": config.get("hidden"),
"max_clients": config.get("max_clients"),
"authmode": authmode,
}
if config.get("password"):
ap_config["password"] = config.get("password")
ap.config(**ap_config)
ap.ifconfig(
(
config.get("ip"),
config.get("netmask"),
config.get("gateway"),
config.get("dns"),
)
)
WifiService.hotspot_enabled = True
print("WifiService: Hotspot enabled")
return True
except Exception as e:
print(f"WifiService: Failed to enable hotspot: {e}")
return False
@staticmethod
def disable_hotspot(network_module=None):
if not HAS_NETWORK_MODULE and network_module is None:
WifiService.hotspot_enabled = False
print("WifiService: Desktop mode, hotspot disabled (simulated)")
return
try:
net = network_module if network_module else network
ap = net.WLAN(net.AP_IF)
ap.active(False)
WifiService.hotspot_enabled = False
print("WifiService: Hotspot disabled")
except Exception:
WifiService.hotspot_enabled = False
@staticmethod
def is_hotspot_enabled(network_module=None):
if not HAS_NETWORK_MODULE and network_module is None:
return WifiService.hotspot_enabled
try:
net = network_module if network_module else network
ap = net.WLAN(net.AP_IF)
return ap.active()
except Exception:
return WifiService.hotspot_enabled
@staticmethod
def connect(network_module=None, time_module=None):
"""
@@ -141,7 +264,16 @@ class WifiService:
net = network_module if network_module else network
def _restore_hotspot_if_needed():
if WifiService._needs_hotspot_restore:
WifiService._needs_hotspot_restore = False
WifiService.enable_hotspot(network_module=network_module)
try:
if WifiService.is_hotspot_enabled(network_module=network_module):
WifiService._needs_hotspot_restore = True
WifiService.disable_hotspot(network_module=network_module)
wlan = net.WLAN(net.STA_IF)
wlan.connect(ssid, password)
@@ -156,21 +288,25 @@ class WifiService:
except Exception as e:
print(f"WifiService: Could not sync time: {e}")
WifiService._needs_hotspot_restore = False
return True
elif not wlan.active():
# WiFi was disabled during connection attempt
print("WifiService: WiFi disabled during connection, aborting")
_restore_hotspot_if_needed()
return False
print(f"WifiService: Waiting for connection, attempt {i+1}/10")
time_mod.sleep(1)
print(f"WifiService: Connection timeout for '{ssid}'")
_restore_hotspot_if_needed()
return False
except Exception as e:
print(f"WifiService: Connection error: {e}")
_restore_hotspot_if_needed()
return False
@staticmethod
@@ -187,21 +323,37 @@ class WifiService:
"""
print("WifiService: Auto-connect thread starting")
hotspot_config = WifiService._get_hotspot_config()
if hotspot_config.get("enabled"):
print("WifiService: Hotspot enabled, skipping STA auto-connect")
WifiService.enable_hotspot(network_module=network_module)
return
if WifiService.is_hotspot_enabled(network_module=network_module):
WifiService._needs_hotspot_restore = True
WifiService.disable_hotspot(network_module=network_module)
# Load saved access points from config
WifiService.access_points = mpos.config.SharedPreferences(
"com.micropythonos.system.wifiservice"
).get_dict("access_points")
if not len(WifiService.access_points):
if WifiService._needs_hotspot_restore:
WifiService._needs_hotspot_restore = False
WifiService.enable_hotspot(network_module=network_module)
print("WifiService: No access points configured, exiting")
return
# Check if WiFi is busy (e.g., WiFi app is scanning)
if WifiService.wifi_busy:
if WifiService._needs_hotspot_restore:
WifiService._needs_hotspot_restore = False
WifiService.enable_hotspot(network_module=network_module)
print("WifiService: WiFi busy, auto-connect aborted")
return
WifiService.wifi_busy = True
connected = False
try:
if not HAS_NETWORK_MODULE and network_module is None:
@@ -209,6 +361,7 @@ class WifiService:
print("WifiService: Desktop mode, simulating connection...")
time_mod = time_module if time_module else time
time_mod.sleep(2)
connected = True
print("WifiService: Simulated connection complete")
else:
# Attempt to connect to saved networks
@@ -216,6 +369,7 @@ class WifiService:
network_module=network_module,
time_module=time_module,
):
connected = True
print("WifiService: Auto-connect successful")
else:
print("WifiService: Auto-connect failed")
@@ -231,6 +385,9 @@ class WifiService:
print("WifiService: WiFi disabled to conserve power")
finally:
if not connected and WifiService._needs_hotspot_restore:
WifiService._needs_hotspot_restore = False
WifiService.enable_hotspot(network_module=network_module)
WifiService.wifi_busy = False
print("WifiService: Auto-connect thread finished")
@@ -254,16 +411,23 @@ class WifiService:
if WifiService.wifi_busy:
raise RuntimeError("Cannot disable WiFi: WifiService is already busy")
# Check actual connection status BEFORE setting wifi_busy
was_connected = False
hotspot_was_enabled = False
if HAS_NETWORK_MODULE or network_module:
try:
net = network_module if network_module else network
wlan = net.WLAN(net.STA_IF)
was_connected = wlan.isconnected()
ap = net.WLAN(net.AP_IF)
hotspot_was_enabled = ap.active()
except Exception as e:
print(f"WifiService: Error checking connection: {e}")
WifiService._temp_disable_state = {
"was_connected": was_connected,
"hotspot_was_enabled": hotspot_was_enabled,
}
# Now set busy flag and disconnect
WifiService.wifi_busy = True
WifiService.disconnect(network_module=network_module)
@@ -283,6 +447,12 @@ class WifiService:
"""
WifiService.wifi_busy = False
state = WifiService._temp_disable_state or {}
WifiService._temp_disable_state = None
if state.get("hotspot_was_enabled"):
WifiService.enable_hotspot(network_module=network_module)
# Only reconnect if WiFi was connected before we disabled it
if was_connected:
try:
@@ -313,9 +483,11 @@ class WifiService:
if not HAS_NETWORK_MODULE and network_module is None:
return True
# Check actual connection status
try:
net = network_module if network_module else network
if WifiService.is_hotspot_enabled(network_module=network_module):
ap = net.WLAN(net.AP_IF)
return ap.active()
wlan = net.WLAN(net.STA_IF)
return wlan.isconnected()
except Exception as e:
@@ -333,9 +505,11 @@ class WifiService:
if not HAS_NETWORK_MODULE and network_module is None:
return "123.456.789.000"
# Check actual connection status
try:
net = network_module if network_module else network
if WifiService.is_hotspot_enabled(network_module=network_module):
ap = net.WLAN(net.AP_IF)
return ap.ifconfig()[0]
wlan = net.WLAN(net.STA_IF)
return wlan.ipconfig("addr4")
except Exception as e:
@@ -352,9 +526,11 @@ class WifiService:
if not HAS_NETWORK_MODULE and network_module is None:
return "000.123.456.789"
# Check actual connection status
try:
net = network_module if network_module else network
if WifiService.is_hotspot_enabled(network_module=network_module):
ap = net.WLAN(net.AP_IF)
return ap.ifconfig()[2]
wlan = net.WLAN(net.STA_IF)
return wlan.ipconfig("gw4")
except Exception as e:
@@ -378,6 +554,9 @@ class WifiService:
wlan = net.WLAN(net.STA_IF)
wlan.disconnect()
wlan.active(False)
ap = net.WLAN(net.AP_IF)
ap.active(False)
WifiService.hotspot_enabled = False
print("WifiService: Disconnected and WiFi disabled")
except Exception as e:
#print(f"WifiService: Error disconnecting: {e}") # probably "Wifi Not Started" so harmless
-19
View File
@@ -1,19 +0,0 @@
# Partition table for Fri3D Camp 2024 Badge with ESP-IDF OTA support using 16MB flash
#
# Also present in flash:
# 0x0 images/bootloader.bin
# 0x8000 images/partition-table.bin
#
# Notes:
# - app partitions should be aligned at 0x10000 (64k block)
# - otadata size should be 0x2000
#
# Name, Type, SubType, Offset, Size, Flags
otadata, data, ota, 0x9000, 0x2000,
nvs, data, nvs, 0xb000, 0x5000,
ota_0,app,ota_0,0x20000,0x400000
ota_1,app,ota_1,0x420000,0x400000
launcher, app, ota_2, 0x820000, 0x100000,
retro-core, app, ota_3, 0x930000, 0xd0000
prboom-go, app, ota_4, 0xa00000, 0xe0000,
vfs, data, fat, 0xae0000, 0x520000
1 # Partition table for Fri3D Camp 2024 Badge with ESP-IDF OTA support using 16MB flash
2 #
3 # Also present in flash:
4 # 0x0 images/bootloader.bin
5 # 0x8000 images/partition-table.bin
6 #
7 # Notes:
8 # - app partitions should be aligned at 0x10000 (64k block)
9 # - otadata size should be 0x2000
10 #
11 # Name Type SubType Offset Size Flags
12 otadata data ota 0x9000 0x2000
13 nvs data nvs 0xb000 0x5000
14 ota_0 app ota_0 0x20000 0x400000
15 ota_1 app ota_1 0x420000 0x400000
16 launcher app ota_2 0x820000 0x100000
17 retro-core app ota_3 0x930000 0xd0000
18 prboom-go app ota_4 0xa00000 0xe0000
19 vfs data fat 0xae0000 0x520000
+8 -77
View File
@@ -7,86 +7,17 @@ Tests ADC1/ADC2 detection, caching, WiFi coordination, and voltage calculations.
import unittest
import sys
# Allow importing shared test mocks
sys.path.insert(0, "../tests")
from mocks import MockADC, MockMachineADC, MockWifiService
# Add parent directory to path for imports
sys.path.insert(0, '../internal_filesystem')
# Mock modules before importing BatteryManager
class MockADC:
"""Mock ADC for testing."""
ATTN_11DB = 3
def __init__(self, pin):
self.pin = pin
self._atten = None
self._read_value = 2048 # Default mid-range value
def atten(self, value):
self._atten = value
def read(self):
return self._read_value
def set_read_value(self, value):
"""Test helper to set ADC reading."""
self._read_value = value
class MockPin:
"""Mock Pin for testing."""
def __init__(self, pin_num):
self.pin_num = pin_num
class MockMachine:
"""Mock machine module."""
ADC = MockADC
Pin = MockPin
class MockWifiService:
"""Mock WifiService for testing."""
wifi_busy = False
_connected = False
_temporarily_disabled = False
@classmethod
def is_connected(cls):
return cls._connected
@classmethod
def disconnect(cls):
cls._connected = False
@classmethod
def temporarily_disable(cls):
"""Temporarily disable WiFi and return whether it was connected."""
if cls.wifi_busy:
raise RuntimeError("Cannot disable WiFi: WifiService is already busy")
was_connected = cls._connected
cls.wifi_busy = True
cls._connected = False
cls._temporarily_disabled = True
return was_connected
@classmethod
def temporarily_enable(cls, was_connected):
"""Re-enable WiFi and reconnect if it was connected before."""
cls.wifi_busy = False
cls._temporarily_disabled = False
if was_connected:
cls._connected = True # Simulate reconnection
@classmethod
def reset(cls):
"""Test helper to reset state."""
cls.wifi_busy = False
cls._connected = False
cls._temporarily_disabled = False
sys.path.insert(0, "../internal_filesystem")
# Inject mocks
sys.modules['machine'] = MockMachine
sys.modules['mpos.net.wifi_service'] = type('module', (), {'WifiService': MockWifiService})()
sys.modules["machine"] = MockMachineADC
sys.modules["mpos.net.wifi_service"] = type("module", (), {"WifiService": MockWifiService})()
# Now import BatteryManager
from mpos.battery_manager import BatteryManager
+6 -19
View File
@@ -1,31 +1,18 @@
import unittest
import sys
# Add parent directory to path so we can import network_test_helper
# Add parent directory to path so we can import shared mocks/network_test_helper
# When running from unittest.sh, we're in internal_filesystem/, so tests/ is ../tests/
sys.path.insert(0, '../tests')
sys.path.insert(0, "../tests")
from mocks import make_machine_timer_module, make_usocket_module
# Import our network test helpers
from network_test_helper import MockNetwork, MockTimer, MockTime, MockRequests, MockSocket
# Mock machine module with Timer
class MockMachine:
"""Mock machine module."""
Timer = MockTimer
# Mock usocket module
class MockUsocket:
"""Mock usocket module."""
AF_INET = MockSocket.AF_INET
SOCK_STREAM = MockSocket.SOCK_STREAM
@staticmethod
def socket(af, sock_type):
return MockSocket(af, sock_type)
# Inject mocks into sys.modules BEFORE importing connectivity_manager
sys.modules['machine'] = MockMachine
sys.modules['usocket'] = MockUsocket
sys.modules["machine"] = make_machine_timer_module(MockTimer)
sys.modules["usocket"] = make_usocket_module(MockSocket)
# Mock requests module
mock_requests = MockRequests()
+20 -131
View File
@@ -2,91 +2,17 @@
import unittest
import sys
# Allow importing shared test mocks
sys.path.insert(0, "../tests")
# Mock hardware before importing SensorManager
class MockI2C:
"""Mock I2C bus for testing."""
def __init__(self, bus_id, sda=None, scl=None):
self.bus_id = bus_id
self.sda = sda
self.scl = scl
self.memory = {} # addr -> {reg -> value}
def readfrom_mem(self, addr, reg, nbytes):
"""Read from memory (simulates I2C read)."""
if addr not in self.memory:
raise OSError("I2C device not found")
if reg not in self.memory[addr]:
return bytes([0] * nbytes)
return bytes(self.memory[addr][reg])
def writeto_mem(self, addr, reg, data):
"""Write to memory (simulates I2C write)."""
if addr not in self.memory:
self.memory[addr] = {}
self.memory[addr][reg] = list(data)
class MockQMI8658:
"""Mock QMI8658 IMU sensor."""
def __init__(self, i2c_bus, address=0x6B, accel_scale=0b10, gyro_scale=0b100):
self.i2c = i2c_bus
self.address = address
self.accel_scale = accel_scale
self.gyro_scale = gyro_scale
@property
def temperature(self):
"""Return mock temperature."""
return 25.5 # Mock temperature in °C
@property
def acceleration(self):
"""Return mock acceleration (in G)."""
return (0.0, 0.0, 1.0) # At rest, Z-axis = 1G
@property
def gyro(self):
"""Return mock gyroscope (in deg/s)."""
return (0.0, 0.0, 0.0) # Stationary
class MockWsenIsds:
"""Mock WSEN_ISDS IMU sensor."""
def __init__(self, i2c, address=0x6B, acc_range="8g", acc_data_rate="104Hz",
gyro_range="500dps", gyro_data_rate="104Hz"):
self.i2c = i2c
self.address = address
self.acc_range = acc_range
self.gyro_range = gyro_range
self.acc_sensitivity = 0.244 # mg/digit for 8g
self.gyro_sensitivity = 17.5 # mdps/digit for 500dps
self.acc_offset_x = 0
self.acc_offset_y = 0
self.acc_offset_z = 0
self.gyro_offset_x = 0
self.gyro_offset_y = 0
self.gyro_offset_z = 0
def get_chip_id(self):
"""Return WHO_AM_I value."""
return 0x6A
def _read_raw_accelerations(self):
"""Return mock acceleration (in mg)."""
return (0.0, 0.0, 1000.0) # At rest, Z-axis = 1000 mg
def read_angular_velocities(self):
"""Return mock gyroscope (in mdps)."""
return (0.0, 0.0, 0.0)
def acc_calibrate(self, samples=None):
"""Mock calibration."""
pass
def gyro_calibrate(self, samples=None):
"""Mock calibration."""
pass
from mocks import (
MockI2C,
MockQMI8658,
MockSharedPreferences,
MockWsenIsds,
make_config_module,
make_machine_i2c_module,
)
# Mock constants from drivers
@@ -96,57 +22,20 @@ _ACCELSCALE_RANGE_8G = 0b10
_GYROSCALE_RANGE_256DPS = 0b100
# Mock SharedPreferences to prevent loading real calibration
class MockSharedPreferences:
"""Mock SharedPreferences for testing."""
def __init__(self, package, filename=None):
self.package = package
self.filename = filename
self.data = {}
def get_list(self, key):
"""Get list value."""
return self.data.get(key)
def edit(self):
"""Return editor."""
return MockEditor(self.data)
class MockEditor:
"""Mock SharedPreferences editor."""
def __init__(self, data):
self.data = data
def put_list(self, key, value):
"""Put list value."""
self.data[key] = value
return self
def commit(self):
"""Commit changes."""
pass
mock_config = type('module', (), {
'SharedPreferences': MockSharedPreferences
})()
mock_config = make_config_module(MockSharedPreferences)
# Create mock modules
mock_machine = type('module', (), {
'I2C': MockI2C,
'Pin': type('Pin', (), {})
mock_machine = make_machine_i2c_module(MockI2C)
mock_qmi8658 = type("module", (), {
"QMI8658": MockQMI8658,
"_QMI8685_PARTID": _QMI8685_PARTID,
"_REG_PARTID": _REG_PARTID,
"_ACCELSCALE_RANGE_8G": _ACCELSCALE_RANGE_8G,
"_GYROSCALE_RANGE_256DPS": _GYROSCALE_RANGE_256DPS,
})()
mock_qmi8658 = type('module', (), {
'QMI8658': MockQMI8658,
'_QMI8685_PARTID': _QMI8685_PARTID,
'_REG_PARTID': _REG_PARTID,
'_ACCELSCALE_RANGE_8G': _ACCELSCALE_RANGE_8G,
'_GYROSCALE_RANGE_256DPS': _GYROSCALE_RANGE_256DPS
})()
mock_wsen_isds = type('module', (), {
'Wsen_Isds': MockWsenIsds
})()
mock_wsen_isds = type("module", (), {"Wsen_Isds": MockWsenIsds})()
# Mock esp32 module
def _mock_mcu_temperature(*args, **kwargs):
+358 -57
View File
@@ -2,70 +2,20 @@ import unittest
import sys
# Add tests directory to path for network_test_helper
sys.path.insert(0, '../tests')
sys.path.insert(0, "../tests")
# Import network test helpers
from network_test_helper import MockNetwork, MockTime
# Mock config classes
class MockSharedPreferences:
"""Mock SharedPreferences for testing."""
_all_data = {} # Class-level storage
def __init__(self, app_id):
self.app_id = app_id
if app_id not in MockSharedPreferences._all_data:
MockSharedPreferences._all_data[app_id] = {}
def get_dict(self, key):
return MockSharedPreferences._all_data.get(self.app_id, {}).get(key, {})
def edit(self):
return MockEditor(self)
@classmethod
def reset_all(cls):
cls._all_data = {}
class MockEditor:
"""Mock editor for SharedPreferences."""
def __init__(self, prefs):
self.prefs = prefs
self.pending = {}
def put_dict(self, key, value):
self.pending[key] = value
def commit(self):
if self.prefs.app_id not in MockSharedPreferences._all_data:
MockSharedPreferences._all_data[self.prefs.app_id] = {}
MockSharedPreferences._all_data[self.prefs.app_id].update(self.pending)
# Create mock mpos module
class MockMpos:
"""Mock mpos module with config and time."""
class config:
@staticmethod
def SharedPreferences(app_id):
return MockSharedPreferences(app_id)
class time:
@staticmethod
def sync_time():
pass # No-op for testing
from mocks import HotspotMockNetwork, MockMpos, MockSharedPreferences
# Inject mocks before importing WifiService
sys.modules['mpos'] = MockMpos
sys.modules['mpos.config'] = MockMpos.config
sys.modules['mpos.time'] = MockMpos.time
sys.modules["mpos"] = MockMpos
sys.modules["mpos.config"] = MockMpos.config
sys.modules["mpos.time"] = MockMpos.time
# Add path to wifi_service.py
sys.path.append('lib/mpos/net')
sys.path.append("lib/mpos/net")
# Import WifiService
from wifi_service import WifiService
@@ -331,7 +281,9 @@ class TestWifiServiceIsConnected(unittest.TestCase):
def test_is_connected_when_disconnected(self):
"""Test is_connected returns False when WiFi is disconnected."""
mock_network = MockNetwork(connected=False)
mock_network = HotspotMockNetwork()
ap_wlan = mock_network.WLAN(mock_network.AP_IF)
ap_wlan.active(False)
result = WifiService.is_connected(network_module=mock_network)
@@ -347,6 +299,17 @@ class TestWifiServiceIsConnected(unittest.TestCase):
# Should return False even though connected
self.assertFalse(result)
def test_is_connected_when_hotspot_enabled(self):
"""Test is_connected checks AP state when hotspot is enabled."""
mock_network = HotspotMockNetwork()
ap_wlan = mock_network.WLAN(mock_network.AP_IF)
ap_wlan.active(True)
WifiService.hotspot_enabled = True
result = WifiService.is_connected(network_module=mock_network)
self.assertTrue(result)
def test_is_connected_desktop_mode(self):
"""Test is_connected in desktop mode."""
result = WifiService.is_connected(network_module=None)
@@ -424,6 +387,329 @@ class TestWifiServiceNetworkManagement(unittest.TestCase):
self.assertEqual(len(saved), 0)
class TestWifiServiceHotspot(unittest.TestCase):
"""Test hotspot configuration and mode switching."""
def setUp(self):
"""Set up test fixtures."""
MockSharedPreferences.reset_all()
WifiService.hotspot_enabled = False
WifiService.wifi_busy = False
WifiService._needs_hotspot_restore = False
def tearDown(self):
"""Clean up after test."""
WifiService.hotspot_enabled = False
WifiService.wifi_busy = False
WifiService._needs_hotspot_restore = False
MockSharedPreferences.reset_all()
def test_enable_hotspot_applies_config(self):
"""Test enable_hotspot reads config and configures AP."""
prefs = MockSharedPreferences("com.micropythonos.system.hotspot")
editor = prefs.edit()
editor.put_bool("enabled", True)
editor.put_string("ssid", "MyAP")
editor.put_string("password", "ap-pass")
editor.put_int("channel", 6)
editor.put_bool("hidden", True)
editor.put_int("max_clients", 3)
editor.put_string("authmode", "wpa2")
editor.put_string("ip", "192.168.4.2")
editor.put_string("netmask", "255.255.255.0")
editor.put_string("gateway", "192.168.4.1")
editor.put_string("dns", "1.1.1.1")
editor.commit()
mock_network = HotspotMockNetwork()
ap_wlan = mock_network.WLAN(mock_network.AP_IF)
sta_wlan = mock_network.WLAN(mock_network.STA_IF)
sta_wlan.active(True)
sta_wlan._connected = True
result = WifiService.enable_hotspot(network_module=mock_network)
self.assertTrue(result)
self.assertTrue(WifiService.hotspot_enabled)
self.assertTrue(ap_wlan.active())
self.assertFalse(sta_wlan.active())
self.assertEqual(ap_wlan._config.get("essid"), "MyAP")
self.assertEqual(ap_wlan._config.get("channel"), 6)
self.assertTrue(ap_wlan._config.get("hidden"))
self.assertEqual(ap_wlan._config.get("max_clients"), 3)
self.assertEqual(ap_wlan._config.get("authmode"), mock_network.AUTH_WPA2_PSK)
self.assertEqual(ap_wlan._config.get("password"), "ap-pass")
self.assertEqual(
ap_wlan.ifconfig(),
("192.168.4.2", "255.255.255.0", "192.168.4.1", "1.1.1.1"),
)
def test_enable_hotspot_respects_busy_flag(self):
"""Test enable_hotspot returns False when WiFi is busy."""
WifiService.wifi_busy = True
mock_network = HotspotMockNetwork()
result = WifiService.enable_hotspot(network_module=mock_network)
self.assertFalse(result)
self.assertFalse(WifiService.hotspot_enabled)
def test_disable_hotspot_deactivates_ap(self):
"""Test disable_hotspot turns off AP and updates flag."""
mock_network = HotspotMockNetwork()
ap_wlan = mock_network.WLAN(mock_network.AP_IF)
ap_wlan.active(True)
WifiService.hotspot_enabled = True
WifiService.disable_hotspot(network_module=mock_network)
self.assertFalse(ap_wlan.active())
self.assertFalse(WifiService.hotspot_enabled)
def test_enable_hotspot_desktop_mode(self):
"""Test enable_hotspot in desktop mode uses simulated flag."""
result = WifiService.enable_hotspot(network_module=None)
self.assertTrue(result)
self.assertTrue(WifiService.hotspot_enabled)
def test_disable_hotspot_desktop_mode(self):
"""Test disable_hotspot in desktop mode uses simulated flag."""
WifiService.hotspot_enabled = True
WifiService.disable_hotspot(network_module=None)
self.assertFalse(WifiService.hotspot_enabled)
def test_auto_connect_with_hotspot_enabled_prefers_ap_mode(self):
"""Test auto_connect uses hotspot mode when enabled in config."""
prefs = MockSharedPreferences("com.micropythonos.system.hotspot")
editor = prefs.edit()
editor.put_bool("enabled", True)
editor.commit()
mock_network = HotspotMockNetwork()
WifiService.auto_connect(network_module=mock_network, time_module=MockTime())
ap_wlan = mock_network.WLAN(mock_network.AP_IF)
self.assertTrue(ap_wlan.active())
self.assertTrue(WifiService.hotspot_enabled)
def test_attempt_connecting_temporarily_disables_hotspot(self):
"""Test STA connect disables hotspot and leaves it off on success."""
mock_network = HotspotMockNetwork()
ap_wlan = mock_network.WLAN(mock_network.AP_IF)
ap_wlan.active(True)
WifiService.hotspot_enabled = True
sta_wlan = mock_network.WLAN(mock_network.STA_IF)
call_count = [0]
def mock_isconnected():
call_count[0] += 1
return call_count[0] >= 1
sta_wlan.isconnected = mock_isconnected
result = WifiService.attempt_connecting(
"TestSSID",
"pass",
network_module=mock_network,
time_module=MockTime(),
)
self.assertTrue(result)
self.assertFalse(WifiService.hotspot_enabled)
self.assertFalse(ap_wlan.active())
def test_attempt_connecting_restores_hotspot_on_timeout(self):
"""Test STA connect restores hotspot when connection times out."""
mock_network = HotspotMockNetwork()
ap_wlan = mock_network.WLAN(mock_network.AP_IF)
ap_wlan.active(True)
WifiService.hotspot_enabled = True
sta_wlan = mock_network.WLAN(mock_network.STA_IF)
def mock_isconnected():
return False
sta_wlan.isconnected = mock_isconnected
result = WifiService.attempt_connecting(
"TestSSID",
"pass",
network_module=mock_network,
time_module=MockTime(),
)
self.assertFalse(result)
self.assertTrue(WifiService.hotspot_enabled)
self.assertTrue(ap_wlan.active())
def test_attempt_connecting_restores_hotspot_on_abort(self):
"""Test STA connect restores hotspot if WiFi is disabled mid-try."""
mock_network = HotspotMockNetwork()
ap_wlan = mock_network.WLAN(mock_network.AP_IF)
ap_wlan.active(True)
WifiService.hotspot_enabled = True
sta_wlan = mock_network.WLAN(mock_network.STA_IF)
def mock_isconnected():
return False
def mock_active(state=None):
if state is not None:
sta_wlan._active = state
return None
return False
sta_wlan.isconnected = mock_isconnected
sta_wlan.active = mock_active
result = WifiService.attempt_connecting(
"TestSSID",
"pass",
network_module=mock_network,
time_module=MockTime(),
)
self.assertFalse(result)
self.assertTrue(WifiService.hotspot_enabled)
self.assertTrue(ap_wlan.active())
class TestWifiServiceTemporaryDisable(unittest.TestCase):
"""Test temporarily_disable/temporarily_enable behavior."""
def setUp(self):
"""Set up test fixtures."""
WifiService.wifi_busy = False
WifiService._temp_disable_state = None
WifiService.hotspot_enabled = False
def tearDown(self):
"""Clean up after test."""
WifiService.wifi_busy = False
WifiService._temp_disable_state = None
WifiService.hotspot_enabled = False
def test_temporarily_disable_raises_when_busy(self):
"""Test temporarily_disable raises if wifi_busy is set."""
WifiService.wifi_busy = True
with self.assertRaises(RuntimeError):
WifiService.temporarily_disable(network_module=HotspotMockNetwork())
def test_temporarily_disable_disconnects_and_tracks_state(self):
"""Test temporarily_disable stores state and disconnects."""
mock_network = HotspotMockNetwork()
sta_wlan = mock_network.WLAN(mock_network.STA_IF)
ap_wlan = mock_network.WLAN(mock_network.AP_IF)
sta_wlan._connected = True
ap_wlan.active(True)
WifiService.hotspot_enabled = True
disconnect_called = [False]
def mock_disconnect(network_module=None):
disconnect_called[0] = True
original_disconnect = WifiService.disconnect
WifiService.disconnect = mock_disconnect
try:
was_connected = WifiService.temporarily_disable(network_module=mock_network)
finally:
WifiService.disconnect = original_disconnect
self.assertTrue(was_connected)
self.assertTrue(WifiService.wifi_busy)
self.assertEqual(
WifiService._temp_disable_state,
{"was_connected": True, "hotspot_was_enabled": True},
)
self.assertTrue(disconnect_called[0])
def test_temporarily_enable_restores_hotspot_and_reconnects(self):
"""Test temporarily_enable restores hotspot and triggers reconnect."""
mock_network = HotspotMockNetwork()
WifiService._temp_disable_state = {"was_connected": True, "hotspot_was_enabled": True}
WifiService.wifi_busy = True
thread_calls = []
class MockThreadModule:
@staticmethod
def start_new_thread(func, args):
thread_calls.append((func, args))
original_thread = sys.modules.get("_thread")
sys.modules["_thread"] = MockThreadModule
try:
WifiService.temporarily_enable(True, network_module=mock_network)
finally:
if original_thread is not None:
sys.modules["_thread"] = original_thread
else:
sys.modules.pop("_thread", None)
ap_wlan = mock_network.WLAN(mock_network.AP_IF)
self.assertFalse(WifiService.wifi_busy)
self.assertIsNone(WifiService._temp_disable_state)
self.assertTrue(ap_wlan.active())
self.assertTrue(WifiService.hotspot_enabled)
self.assertEqual(thread_calls[0][0], WifiService.auto_connect)
class TestWifiServiceIPv4Info(unittest.TestCase):
"""Test IPv4 info accessors for AP/STA modes."""
def setUp(self):
"""Set up test fixtures."""
WifiService.wifi_busy = False
WifiService.hotspot_enabled = False
def tearDown(self):
"""Clean up after test."""
WifiService.wifi_busy = False
WifiService.hotspot_enabled = False
def test_get_ipv4_info_from_ap_when_hotspot_enabled(self):
"""Test IPv4 getters use AP info when hotspot is enabled."""
mock_network = HotspotMockNetwork()
ap_wlan = mock_network.WLAN(mock_network.AP_IF)
ap_wlan.active(True)
ap_wlan.ifconfig(("192.168.4.1", "255.255.255.0", "192.168.4.1", "8.8.4.4"))
WifiService.hotspot_enabled = True
address = WifiService.get_ipv4_address(network_module=mock_network)
gateway = WifiService.get_ipv4_gateway(network_module=mock_network)
self.assertEqual(address, "192.168.4.1")
self.assertEqual(gateway, "192.168.4.1")
def test_get_ipv4_info_returns_none_when_busy(self):
"""Test IPv4 getters return None when wifi_busy is set."""
WifiService.wifi_busy = True
address = WifiService.get_ipv4_address(network_module=HotspotMockNetwork())
gateway = WifiService.get_ipv4_gateway(network_module=HotspotMockNetwork())
self.assertIsNone(address)
self.assertIsNone(gateway)
def test_get_ipv4_info_desktop_mode(self):
"""Test IPv4 getters return simulated values in desktop mode."""
address = WifiService.get_ipv4_address(network_module=None)
gateway = WifiService.get_ipv4_gateway(network_module=None)
self.assertEqual(address, "123.456.789.000")
self.assertEqual(gateway, "000.123.456.789")
class TestWifiServiceDisconnect(unittest.TestCase):
"""Test WifiService.disconnect() method."""
@@ -453,6 +739,21 @@ class TestWifiServiceDisconnect(unittest.TestCase):
self.assertTrue(disconnect_called[0])
self.assertTrue(active_false_called[0])
def test_disconnect_disables_ap(self):
"""Test disconnect also disables AP and clears hotspot flag."""
mock_network = HotspotMockNetwork()
ap_wlan = mock_network.WLAN(mock_network.AP_IF)
sta_wlan = mock_network.WLAN(mock_network.STA_IF)
ap_wlan.active(True)
sta_wlan._connected = True
WifiService.hotspot_enabled = True
WifiService.disconnect(network_module=mock_network)
self.assertFalse(ap_wlan.active())
self.assertFalse(WifiService.hotspot_enabled)
def test_disconnect_desktop_mode(self):
"""Test disconnect in desktop mode."""
# Should not raise an error