Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion freewili/framing.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand Down
41 changes: 35 additions & 6 deletions freewili/fw.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand All @@ -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
Expand Down Expand Up @@ -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.

Expand Down Expand Up @@ -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:
Expand Down
50 changes: 22 additions & 28 deletions freewili/fw_serial.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@
This module provides functionality to find and control FreeWili boards.
"""

import dataclasses
import datetime
import functools
import pathlib
Expand All @@ -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"
Expand Down Expand Up @@ -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]:
Expand Down Expand Up @@ -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.
Expand Down
17 changes: 16 additions & 1 deletion freewili/types.py
Original file line number Diff line number Diff line change
Expand Up @@ -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."""

Expand Down Expand Up @@ -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."""
Expand Down
102 changes: 102 additions & 0 deletions tests/test_fw_ver.py
Original file line number Diff line number Diff line change
@@ -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"])