From 54783b7a3beb597c141622d6fb2f2e7eee021a6b Mon Sep 17 00:00:00 2001 From: David Rebbe Date: Fri, 30 Jan 2026 12:25:30 -0500 Subject: [PATCH] added get_app_info --- freewili/framing.py | 2 +- freewili/fw.py | 41 ++++++++++++++--- freewili/fw_serial.py | 50 +++++++++------------ freewili/types.py | 17 ++++++- tests/test_fw_ver.py | 102 ++++++++++++++++++++++++++++++++++++++++++ 5 files changed, 176 insertions(+), 36 deletions(-) create mode 100644 tests/test_fw_ver.py diff --git a/freewili/framing.py b/freewili/framing.py index 9a234d4..28ebfa0 100644 --- a/freewili/framing.py +++ b/freewili/framing.py @@ -120,7 +120,7 @@ def validate_start_of_frame(data: bytes | str) -> tuple[ResponseFrameType, int]: # Parse an event frame first [*event_name ...] if re.match(rb"^\[\*\w+ ", sof_data): return (ResponseFrameType.Event, index) - elif re.match(rb"^\[[a-zA-Z](\\[a-zA-Z])* ", sof_data): + elif re.match(rb"^\[[a-zA-Z?](\\[a-zA-Z])* ", sof_data): return (ResponseFrameType.Standard, index) return (ResponseFrameType.Invalid, -1) diff --git a/freewili/fw.py b/freewili/fw.py index f7526e7..ad1850d 100644 --- a/freewili/fw.py +++ b/freewili/fw.py @@ -7,6 +7,8 @@ from dataclasses import dataclass from typing import Any, Callable, List +from freewili.fw_serial import FreeWiliSerial + if sys.version_info >= (3, 11): from typing import Self else: @@ -16,8 +18,14 @@ from result import Err, Ok, Result from freewili.framing import ResponseFrame -from freewili.fw_serial import FreeWiliSerial -from freewili.types import ButtonColor, EventType, FileSystemContents, FreeWiliProcessorType, IOMenuCommand +from freewili.types import ( + ButtonColor, + EventType, + FileSystemContents, + FreeWiliAppInfo, + FreeWiliProcessorType, + IOMenuCommand, +) # USB Locations: # first address = FTDI @@ -1946,11 +1954,9 @@ def wileye_set_resolution( return Err(msg) case _: raise RuntimeError("Missing case statement") - + def enable_nfc_read_events( - self, - enable: bool, - processor: FreeWiliProcessorType = FreeWiliProcessorType.Main + self, enable: bool, processor: FreeWiliProcessorType = FreeWiliProcessorType.Main ) -> Result[str, str]: """Enable or disable NFC read events. @@ -2273,6 +2279,29 @@ def can_write_registers( case _: raise RuntimeError("Missing case statement") + def get_app_info( + self, processor: FreeWiliProcessorType = FreeWiliProcessorType.Main + ) -> Result[FreeWiliAppInfo, str]: + """Detect the processor type and version of the FreeWili. + + Arguments: + ---------- + processor: FreeWiliProcessorType + Processor to use. + + Returns: + ------- + Result[FreeWiliProcessorType, str]: + Returns Ok(FreeWiliProcessorType) if the command was sent successfully, Err(str) if not. + """ + match self.get_serial_from(processor): + case Ok(serial): + return serial.get_app_info() + case Err(msg): + return Err(msg) + case _: + raise RuntimeError("Missing case statement") + @dataclass(frozen=True) class FileMap: diff --git a/freewili/fw_serial.py b/freewili/fw_serial.py index ab813e4..70ce6d2 100644 --- a/freewili/fw_serial.py +++ b/freewili/fw_serial.py @@ -3,7 +3,6 @@ This module provides functionality to find and control FreeWili boards. """ -import dataclasses import datetime import functools import pathlib @@ -28,22 +27,14 @@ import serial.tools.list_ports from result import Err, Ok, Result -from freewili.types import ButtonColor, EventType, FileSystemContents, FreeWiliProcessorType, IOMenuCommand - - -@dataclasses.dataclass -class FreeWiliAppInfo: - """Information of the FreeWili application.""" - - processor_type: FreeWiliProcessorType - version: int - - def __str__(self) -> str: - desc = f"{self.processor_type.name}" - if self.processor_type in (FreeWiliProcessorType.Main, FreeWiliProcessorType.Display): - desc += f" v{self.version}" - return desc - +from freewili.types import ( + ButtonColor, + EventType, + FileSystemContents, + FreeWiliAppInfo, + FreeWiliProcessorType, + IOMenuCommand, +) # Disable menu Ctrl+b CMD_DISABLE_MENU = b"\x02" @@ -1455,36 +1446,39 @@ def reset_to_uf2_bootloader(self) -> Result[None, str]: @needs_open() def get_app_info(self) -> Result[FreeWiliAppInfo, str]: - """Detect the processor type of the FreeWili. + """Detect the processor type and version of the FreeWili. Returns: ------- Result[FreeWiliProcessorType, str]: Returns Ok(FreeWiliProcessorType) if the command was sent successfully, Err(str) if not. """ + self._empty_all() self.serial_port.send("?") resp = self._wait_for_response_frame() if resp.is_err(): - return Err(resp.err()) - proc_type_regex = re.compile(r"(?:Main|Display|DEFCON25|Winky|DEFCON24)|(?:App version)|(?:\d+)") + return Err(str(resp.err())) + proc_type_regex = re.compile( + r"(?:MainCPU|DisplayCPU|Main|Display|DEFCON25|Winky|DEFCON24)|(?:App version)|(?:v?\d+(?:\.\d+)?)", + ) results = proc_type_regex.findall(resp.unwrap().response) if len(results) != 2: return Ok(FreeWiliAppInfo(FreeWiliProcessorType.Unknown, 0)) # New firmware >= 48 processor = results[0] - version = results[1] + version = results[1].lstrip("v") if "main" in processor.lower(): - return Ok(FreeWiliAppInfo(FreeWiliProcessorType.Main, int(version))) + return Ok(FreeWiliAppInfo(FreeWiliProcessorType.Main, float(version))) elif "display" in processor.lower(): - return Ok(FreeWiliAppInfo(FreeWiliProcessorType.Display, int(version))) + return Ok(FreeWiliAppInfo(FreeWiliProcessorType.Display, float(version))) elif "winky" in processor.lower(): - return Ok(FreeWiliAppInfo(FreeWiliProcessorType.Main, int(version))) + return Ok(FreeWiliAppInfo(FreeWiliProcessorType.Main, float(version))) elif "defcon24" in processor.lower(): - return Ok(FreeWiliAppInfo(FreeWiliProcessorType.Main, int(version))) + return Ok(FreeWiliAppInfo(FreeWiliProcessorType.Main, float(version))) elif "defcon25" in processor.lower(): - return Ok(FreeWiliAppInfo(FreeWiliProcessorType.Main, int(version))) + return Ok(FreeWiliAppInfo(FreeWiliProcessorType.Main, float(version))) else: - return Ok(FreeWiliAppInfo(FreeWiliProcessorType.Unknown, int(version))) + return Ok(FreeWiliAppInfo(FreeWiliProcessorType.Unknown, float(version))) @needs_open() def change_directory(self, directory: str) -> Result[str, str]: @@ -2389,7 +2383,7 @@ def can_write_registers( self.serial_port.send(cmd) return self._handle_final_response_frame() - + @needs_open() def enable_nfc_read_events(self, enable: bool) -> Result[str, str]: """Enable or disable NFC read events. diff --git a/freewili/types.py b/freewili/types.py index 469b215..7fcaf91 100644 --- a/freewili/types.py +++ b/freewili/types.py @@ -42,6 +42,20 @@ def __str__(self) -> str: return self.name +@dataclass +class FreeWiliAppInfo: + """Information of the FreeWili application.""" + + processor_type: FreeWiliProcessorType + version: float + + def __str__(self) -> str: + desc = f"{self.processor_type.name}" + if self.processor_type in (FreeWiliProcessorType.Main, FreeWiliProcessorType.Display): + desc += f" v{self.version}" + return desc + + class ButtonColor(enum.Enum): """Free-Wili Physical Button Color.""" @@ -364,7 +378,8 @@ def from_string(cls, data: str) -> Self: charging=bool(int(parts[4])), charge_complete=bool(int(parts[5])), ) - + + @dataclass(frozen=True) class NFCData(EventData): """NFC event data from Free-Wili Main.""" diff --git a/tests/test_fw_ver.py b/tests/test_fw_ver.py new file mode 100644 index 0000000..e65ce7e --- /dev/null +++ b/tests/test_fw_ver.py @@ -0,0 +1,102 @@ +"""Test code for fw_ver.py script functionality using real hardware.""" + +import pytest + +from freewili import FreeWili +from freewili.fw_serial import FreeWiliProcessorType +from freewili.types import FreeWiliAppInfo + + +@pytest.mark.skipif("len(FreeWili.find_all()) == 0") +def test_hw_find_first() -> None: + """Test finding first FreeWili device.""" + fw = FreeWili.find_first().expect("Failed to find FreeWili") + assert fw is not None + + +@pytest.mark.skipif("len(FreeWili.find_all()) == 0") +def test_hw_get_app_info_main_processor() -> None: + """Test getting app info for Main processor on real hardware.""" + with FreeWili.find_first().expect("Failed to find FreeWili") as fw: + result = fw.get_app_info(FreeWiliProcessorType.Main) + assert result.is_ok(), f"Failed to get Main processor info: {result.unwrap_err() if result.is_err() else ''}" + + app_info: FreeWiliAppInfo = result.unwrap() + assert app_info.processor_type == FreeWiliProcessorType.Main + assert app_info.version > 0, "Version should be greater than 0" + + print(f"Main Firmware version: {app_info}") + + +@pytest.mark.skipif("len(FreeWili.find_all()) == 0") +def test_hw_get_app_info_display_processor() -> None: + """Test getting app info for Display processor on real hardware.""" + with FreeWili.find_first().expect("Failed to find FreeWili") as fw: + result = fw.get_app_info(FreeWiliProcessorType.Display) + assert result.is_ok(), f"Failed to get Display processor info: {result.unwrap_err() if result.is_err() else ''}" + + app_info = result.unwrap() + assert app_info.processor_type == FreeWiliProcessorType.Display + assert app_info.version > 0, "Version should be greater than 0" + + print(f"Display Firmware version: {app_info}") + + +@pytest.mark.skipif("len(FreeWili.find_all()) == 0") +def test_hw_get_both_processor_versions() -> None: + """Test getting app info for both Main and Display processors.""" + with FreeWili.find_first().expect("Failed to find FreeWili") as fw: + main_result = fw.get_app_info(FreeWiliProcessorType.Main) + display_result = fw.get_app_info(FreeWiliProcessorType.Display) + + assert main_result.is_ok(), ( + f"Failed to get Main processor info: {main_result.unwrap_err() if main_result.is_err() else ''}" + ) + assert display_result.is_ok(), ( + f"Failed to get Display processor info: {display_result.unwrap_err() if display_result.is_err() else ''}" + ) + + main_info = main_result.unwrap() + display_info = display_result.unwrap() + + print(f"Main Firmware version: {main_info}") + print(f"Display Firmware version: {display_info}") + + # Verify both have valid versions + assert main_info.version > 0 + assert display_info.version > 0 + + +@pytest.mark.skipif("len(FreeWili.find_all()) == 0") +def test_hw_context_manager() -> None: + """Test FreeWili context manager usage with real hardware.""" + fw = FreeWili.find_first().expect("Failed to find FreeWili") + + with fw: + # Should be able to communicate while in context + result = fw.get_app_info(FreeWiliProcessorType.Main) + assert result.is_ok() + + # Context manager should handle cleanup + + +@pytest.mark.skipif("len(FreeWili.find_all()) == 0") +def test_hw_processor_type_values() -> None: + """Test that processor type enum values work with real hardware.""" + with FreeWili.find_first().expect("Failed to find FreeWili") as fw: + # Test each processor type + for proc_type in [FreeWiliProcessorType.Main, FreeWiliProcessorType.Display]: + result = fw.get_app_info(proc_type) + # Should get either valid info or an error, but not crash + assert result.is_ok() or result.is_err() + + +def test_processor_type_enum_attributes() -> None: + """Test FreeWiliProcessorType enum has expected values (no hardware needed).""" + assert hasattr(FreeWiliProcessorType, "Main") + assert hasattr(FreeWiliProcessorType, "Display") + assert hasattr(FreeWiliProcessorType, "Unknown") + + +if __name__ == "__main__": + pytest.main([__file__, "--verbose"])