Examples

All code examples can be found in the examples directory of the repository.

Find all devices

Find and display all Free-Wili devices.

 1"""Example to find all Free-Wilis connected over USB."""
 2
 3import result
 4
 5from freewili import FreeWili
 6
 7# Find the first device and raise an exception otherwise
 8try:
 9    device = FreeWili.find_first().expect("Failed to find a FreeWili")
10except result.UnwrapError as ex:
11    print(ex)
12
13# Find all and display information about it.
14devices = FreeWili.find_all()
15print(f"Found {len(devices)} FreeWili(s)")
16for i, free_wili in enumerate(devices, start=1):
17    print(f"{i}. {free_wili}")
18    print(f"\t{free_wili.main}")
19    print(f"\t{free_wili.display}")
20    print(f"\t{free_wili.ftdi}")

Upload files

Upload files to the first Free-Wili device.

 1"""Upload a wasm file to a Free-Wili."""
 2
 3import pathlib
 4
 5from freewili import FreeWili
 6
 7# The fwi file to upload
 8my_fwi_file = pathlib.Path(r"~/path/to/MyFile.fwi")
 9# The wasm file to upload
10my_wasm_file = pathlib.Path(r"~/path/to/MyFile.wasm")
11
12# Find connected Free-Wilis
13devices = FreeWili.find_all()
14if not devices:
15    print("No Free-Wili devices found!")
16    exit(1)
17
18# Pick the first Free-Wili
19device = devices[0]
20
21# We can leave target_name and processor None and it will automatically
22# figure out where to send the file.
23device.send_file(my_fwi_file, None, None).expect("Failed to upload file")
24device.send_file(my_wasm_file, None, None).expect("Failed to upload file")
25device.run_script(my_wasm_file.name).expect("Failed to run script")

Device Settings

Configure FreeWili device settings including system sounds, default configurations, and real-time clock.

 1"""Example script to set FreeWili settings. Currently unstable and subject to API changes."""
 2
 3from datetime import datetime
 4
 5from freewili import FreeWili
 6from freewili.types import FreeWiliProcessorType
 7
 8with FreeWili.find_first().expect("Failed to find FreeWili") as fw:
 9    print(f"Connected to {fw}")
10    # Set default settings for both processors
11    for processor in (FreeWiliProcessorType.Main, FreeWiliProcessorType.Display):
12        print("Setting defaulting settings for", processor.name)
13        fw.set_settings_to_default(processor).expect(
14            f"Failed to set {processor.name.lower()} processor settings to default"
15        )
16    # Set system sounds to off
17    print("Disabling system sounds...")
18    fw.set_system_sounds(False).expect("Failed to disable system sounds")
19    # Save these settings for both processors on startup
20    for processor in (FreeWiliProcessorType.Main, FreeWiliProcessorType.Display):
21        print("Setting", processor.name, "processor settings on startup...")
22        fw.set_settings_as_startup(processor).expect(
23            f"Failed to set {processor.name.lower()} processor settings on startup"
24        )
25    # Set the current RTC time
26    print("Setting RTC to current time...")
27    fw.set_rtc(datetime.now()).expect("Failed to set RTC")
28    print("RTC set successfully.")
29print("Goodbye!")

FPGA Configuration

Load FPGA configuration files on the FreeWili device.

1"""Example script to load a FPGA file using FreeWili."""
2
3from freewili import FreeWili
4
5with FreeWili.find_first().expect("Failed to find FreeWili") as fw:
6    print(f"Connected to {fw}")
7    fw.load_fpga_from_file("i2c").expect("Failed to load i2c FPGA from file")
8    print("FPGA loaded successfully.")
9print("Goodbye!")

Toggle IO

Toggles IO on the first Free-Wili device.

 1"""Toggle IO on a Free-Wili."""
 2
 3from freewili import FreeWili
 4from freewili.types import IOMenuCommand
 5
 6if __name__ == "__main__":
 7    try:
 8        with FreeWili.find_first().expect("Failed to find FreeWili") as fw:
 9            print(f"Connected to {fw}")
10            # Set IO 25 high
11            fw.set_io(25, IOMenuCommand.High).expect("Failed to set IO high")
12            # Set IO 25 Low
13            fw.set_io(25, IOMenuCommand.Low).expect("Failed to set IO low")
14            # Toggle IO 25 Low
15            fw.set_io(25, IOMenuCommand.Toggle).expect("Failed to toggle IO")
16            # PWM IO 25
17            fw.set_io(25, IOMenuCommand.Pwm, 10, 50).expect("Failed to toggle IO")
18            # Toggle high-speed IO
19            fw.toggle_high_speed_io(True).expect("Failed to toggle high-speed IO")
20            fw.toggle_high_speed_io(False).expect("Failed to toggle high-speed IO off")
21    except Exception as ex:
22        print(f"Error: {ex}")
23
24    print("Done.")

Read Buttons

Read buttons on the first Free-Wili device.

 1"""Read buttons on a Free-Wili."""
 2
 3from freewili import FreeWili
 4
 5# find a FreeWili device and open it
 6device = FreeWili.find_first().expect("Failed to find a FreeWili")
 7device.open().expect("Failed to open")
 8
 9# Read the buttons and print on change
10print("Reading buttons...")
11last_button_read = device.read_all_buttons().expect("Failed to read buttons")
12
13keyboard_interrupt_requested = False
14while not keyboard_interrupt_requested:
15    try:
16        # Read the buttons
17        buttons = device.read_all_buttons().expect("Failed to read buttons")
18        for button_color, button_state in buttons.items():
19            # Check if the button state has changed
20            last_button_state = last_button_read[button_color]
21            if last_button_state == button_state:
22                continue
23            # Print the button change
24            msg = "Pressed \N{WHITE HEAVY CHECK MARK}"
25            if button_state == 0:
26                msg = "Released \N{CROSS MARK}"
27            print(f"{button_color.name} {msg}")
28        # Save the button state for the next loop
29        last_button_read = buttons
30    except KeyboardInterrupt:
31        keyboard_interrupt_requested = True
32
33device.close()

Set Board LEDs

Set Board LEDs on the first Free-Wili device.

 1"""Set Board RGB on a Free-Wili."""
 2
 3import time
 4
 5from freewili import FreeWili
 6
 7# find a FreeWili device and open it
 8device = FreeWili.find_first().expect("Failed to find a FreeWili")
 9device.open().expect("Failed to open")
10
11while True:
12    # Turn the LEDs on
13    for led_num in range(7):
14        print(device.set_board_leds(led_num, 10, 10, led_num * 2).expect("Failed to set LED"))
15    # Wait so we can see them
16    time.sleep(0.3)
17    # Turn the LEDS off
18    for led_num in reversed(range(7)):
19        print(device.set_board_leds(led_num, 0, 0, 0).expect("Failed to set LED"))
20
21device.close()

SparkFun 9DoF IMU Breakout - ISM330DHCX, MMC5983MA (Qwiic)

SparkFun 9DoF IMU Breakout example over I2C.

  1"""Example I2C on a FreeWili with SparkFun 9DoF IMU Breakout - ISM330DHCX, MMC5983MA (Qwiic) attached.
  2
  3https://www.sparkfun.com/sparkfun-9dof-imu-breakout-ism330dhcx-mmc5983ma-qwiic.html
  4ISM330DHCX I2C Address: 0x6B (Default)
  5MMC5983MA Magnetometer I2C Address: 0x30
  6"""
  7
  8import enum
  9from time import time
 10
 11from freewili import FreeWili
 12
 13MMC5983MA_ADDR = 0x30
 14ISM330DHCX_ADDR = 0x6B
 15
 16
 17class MMC5983MA(enum.Enum):
 18    """Register list for MMC5983MA Magnetometer."""
 19
 20    Xout0 = 0x00  # Xout [17:10]
 21    Xout1 = 0x01  # Xout [9:2]
 22    Yout0 = 0x02  # Yout [17:10]
 23    Yout1 = 0x03  # Yout [9:2]
 24    Zout0 = 0x04  # Zout [17:10]
 25    Zout1 = 0x05  # Zout [9:2]
 26    XYZout2 = 0x06  # Xout[1:0], Yout[1:0], Zout[1:0]
 27    Tout = 0x07  # Temperature output
 28    Status = 0x08  # Device status
 29    Control0 = 0x09  # Control register 0
 30    Control1 = 0x0A  # Control register 1
 31    Control2 = 0x0B  # Control register 2
 32    Control3 = 0x0C  # Control register 3
 33    ProductID = 0x2F  # Product ID
 34
 35
 36class ISM330DHCX(enum.Enum):
 37    """Register list for ISM330DHCX."""
 38
 39    FUNC_CFG_ACCESS = 0x01
 40    PIN_CTRL = 0x02
 41    FIFO_CTRL1 = 0x07
 42    FIFO_CTRL2 = 0x08
 43    FIFO_CTRL3 = 0x09
 44    FIFO_CTRL4 = 0x0A
 45    COUNTER_BDR_REG1 = 0x0B
 46    COUNTER_BDR_REG2 = 0x0C
 47    INT1_CTRL = 0x0D
 48    INT2_CTRL = 0x0E
 49    WHO_AM_I = 0x0F
 50    CTRL1_XL = 0x10
 51    CTRL2_G = 0x11
 52    CTRL3_C = 0x12
 53    CTRL4_C = 0x13
 54    CTRL5_C = 0x14
 55    CTRL6_C = 0x15
 56    CTRL7_G = 0x16
 57    CTRL8_XL = 0x17
 58    CTRL9_XL = 0x18
 59    CTRL10_C = 0x19
 60    ALL_INT_SRC = 0x1
 61    WAKE_UP_SRC = 0x1B
 62    TAP_SRC = 0x1C
 63    DRD_SRC = 0x1D
 64    STATUS_REG = 0x1E
 65    STATUS_SPIAux = 0x1E
 66    OUT_TEMP_L = 0x20
 67    OUT_TEMP_H = 0x21
 68    OUTX_L_G = 0x22
 69    OUTX_H_G = 0x23
 70    OUTY_L_G = 0x24
 71    OUTY_H_G = 0x25
 72    OUTZ_L_G = 0x26
 73    OUTZ_H_G = 0x27
 74    OUTX_L_A = 0x28
 75    OUTX_H_A = 0x29
 76    OUTY_L_A = 0x2A
 77    OUTY_H_A = 0x2B
 78    OUTZ_L_A = 0x2C
 79    OUTZ_H_A = 0x2D
 80    EMB_FUNC_STATUS_MAINPAGE = 0x35
 81    FSM_STATUS_A_MAINPAGE = 0x36
 82    FSM_STATUS_B_MAINPAGE = 0x37
 83    MLC_STATUS_MAINPAGE = 0x38
 84    STATUS_MASTER_MAINPAGE = 0x39
 85    FIFO_STATUS1 = 0x3A
 86    FIFO_STATUS2 = 0x3B
 87    TIMESTAMP0 = 0x40
 88    TIMESTAMP1 = 0x41
 89    TIMESTAMP2 = 0x42
 90    TIMESTAMP3 = 0x43
 91    TAP_CFG0 = 0x56
 92    TAP_CFG1 = 0x57
 93    TAP_CFG2 = 0x58
 94    TAP_THS_6D = 0x59
 95    INT_DUR2 = 0x5A
 96    WAKE_UP_THS = 0x5B
 97    WAKE_UP_DUR = 0x5C
 98    FREE_FALL = 0x5D
 99    MD1_CFG = 0x5E
100    MD2_CFG = 0x5F
101    INTERNAL_FREQ_FINE = 0x63
102    INT_OIS = 0x6F
103    CTRL1_OIS = 0x70
104    CTRL2_OIS = 0x71
105    CTRL3_OIS = 0x72
106    X_OFS_USR = 0x73
107    Y_OFS_USR = 0x74
108    Z_OFS_USR = 0x75
109    FIFO_DATA_OUT_TAG = 0x78
110    FIFO_DATA_OUT_X_L = 0x79
111    FIFO_DATA_OUT_X_H = 0x7A
112    FIFO_DATA_OUT_Y_L = 0x7B
113    FIFO_DATA_OUT_Y_H = 0x7C
114    FIFO_DATA_OUT_Z_L = 0x7D
115    FIFO_DATA_OUT_Z_H = 0x7E
116
117
118def get_mmc5983ma_temperature(device: FreeWili) -> float:
119    """Get temperature on MMC5983MA.
120
121    Arguments:
122    ----------
123        device: FreeWili
124            FreeWili device to use.
125
126    Returns:
127    ---------
128        tuple[float, int]:
129            (Temperature in degrees Celcius, timestamp in ns).
130    """
131    _ = device.write_i2c(MMC5983MA_ADDR, MMC5983MA.Control0.value, bytes([0x2A])).expect("Enable temp measure")
132    while True:
133        response = device.read_i2c(MMC5983MA_ADDR, MMC5983MA.Status.value, 1).expect("Reading Status failed")
134        status = response[0]
135        if (status & 0x2) == 0x2:
136            # print("Temperature measurement done!")
137            break
138    response = device.read_i2c(MMC5983MA_ADDR, MMC5983MA.Tout.value, 1).expect("Reading Tout failed")
139    # Temperature output, unsigned format. The range is -75~125°C, about 0.8°C/LSB, 00000000 stands for -75°C
140    temp = response
141    temp_int = int.from_bytes(temp, byteorder="little", signed=False)
142    converted = temp_int * 0.8 - 75
143    # print(resp._raw.strip())
144    return converted
145
146
147def get_mmc5983ma_magnetic_sensor(device: FreeWili) -> tuple[int, int, int]:
148    """Get magnetic field data on MMC5983MA.
149
150    Arguments:
151    ----------
152        device: FreeWili
153            FreeWili device to use.
154
155    Returns:
156    ---------
157        tuple[int, int, int]:
158            (x, y, z).
159    """
160    _ = device.write_i2c(MMC5983MA_ADDR, MMC5983MA.Control0.value, bytes([0x29])).expect("Enable magnetic field")
161    while True:
162        response = device.read_i2c(MMC5983MA_ADDR, MMC5983MA.Status.value, 1).expect("Reading Status failed")
163        status = response[0]
164        if (status & 0x1) == 0x1:
165            # print("Temperature measurement done!")
166            break
167    data = device.read_i2c(MMC5983MA_ADDR, MMC5983MA.Xout0.value, 6).expect("Reading Tout failed")
168    x = int.from_bytes(data[0:2], byteorder="little", signed=False) << 2
169    y = int.from_bytes(data[2:4], byteorder="little", signed=False) << 2
170    z = int.from_bytes(data[4:6], byteorder="little", signed=False) << 2
171    return (x, y, z)
172
173
174# find a FreeWili device
175device = FreeWili.find_first().expect("Failed to find a FreeWili")
176device.open().expect("Failed to open FreeWili")
177
178try:
179    # Poll the I2C to make sure we can read the breakout board
180    print("Polling I2C...")
181    addresses = device.poll_i2c().expect("Failed to poll I2C")
182    if MMC5983MA_ADDR not in addresses or ISM330DHCX_ADDR not in addresses:
183        print(f"Expected I2C addresses {MMC5983MA_ADDR} and {ISM330DHCX_ADDR} not found. Got {addresses}!")
184        exit(1)
185
186    start = time()
187    while True:
188        try:
189            temp_c = get_mmc5983ma_temperature(device)
190            temp_f = temp_c * 1.8 + 32
191            print(f"[{time() - start:.3f}] Temperature: {temp_c:.1f}C ({temp_f:.1f}F)")
192            magnetic_data = get_mmc5983ma_magnetic_sensor(device)
193            print(f"[{time() - start:.3f}] Magnetic Field: {magnetic_data}")
194        except KeyboardInterrupt:
195            break
196finally:
197    device.close()
198
199print("Done.")

Event Handling (Console)

Console-based example for handling real-time events from FreeWili devices including accelerometer, button, IR, and battery events.

 1"""Example script to handle events from FreeWili."""
 2
 3from freewili import FreeWili
 4from freewili.framing import ResponseFrame
 5from freewili.types import AccelData, EventDataType, EventType, GPIOData
 6
 7
 8def event_handler(event_type: EventType, frame: ResponseFrame, data: EventDataType) -> None:
 9    """Handle events from FreeWili."""
10    match event_type:
11        case EventType.Accel:
12            data: AccelData = data  # type:ignore
13            print(f"Accel Event: {data}")
14        case EventType.GPIO:
15            data: GPIOData = data  # type: ignore
16            print(f"GPIO Event: {data.raw}")  # type: ignore
17        case _:
18            # Handle other event types as needed
19            if data:
20                print(f"{event_type}: Event Data: {data}")
21            else:
22                print(f"No data for this {event_type}.")
23
24
25with FreeWili.find_first().expect("Failed to find FreeWili") as fw:
26    print(f"Connected to FreeWili {fw}")
27    fw.set_event_callback(event_handler)
28
29    print("Enabling events...")
30    fw.enable_gpio_events(True).expect("Failed to enable GPIO events")
31    fw.enable_accel_events(True, 33).expect("Failed to enable accel events")
32    fw.enable_button_events(True, 33).expect("Failed to enable button events")
33    fw.enable_ir_events(True).expect("Failed to enable IR events")
34    fw.enable_battery_events(True).expect("Failed to enable battery events")
35    # fw.enable_audio_events(True).expect("Failed to enable audio events")
36    print("Listening for events...")
37    while True:
38        try:
39            fw.process_events()
40        except KeyboardInterrupt:
41            break
42    # Disable events before exiting
43    print("Disabling events...")
44    fw.enable_gpio_events(False).expect("Failed to disable GPIO events")
45    fw.enable_accel_events(False).expect("Failed to disable accel events")
46    fw.enable_button_events(False).expect("Failed to disable button events")
47    fw.enable_ir_events(False).expect("Failed to disable IR events")
48    fw.enable_battery_events(False).expect("Failed to disable battery events")
49    # fw.enable_audio_events(False).expect("Failed to disable audio events")
50    print("Exiting event loop")

Event Handling (GUI)

PySide6 GUI application for monitoring real-time FreeWili events with visual displays and controls.

  1# type: ignore
  2"""PySide6 GUI example to display FreeWili events in real-time."""
  3
  4import sys
  5from typing import Any
  6
  7from PySide6.QtCore import QObject, QTimer, Signal
  8from PySide6.QtWidgets import (
  9    QApplication,
 10    QCheckBox,
 11    QGridLayout,
 12    QGroupBox,
 13    QHBoxLayout,
 14    QLabel,
 15    QMainWindow,
 16    QPushButton,
 17    QTextEdit,
 18    QVBoxLayout,
 19    QWidget,
 20)
 21
 22from freewili import FreeWili
 23from freewili.framing import ResponseFrame
 24from freewili.types import AccelData, BatteryData, ButtonData, EventType, GPIOData, IRData
 25
 26
 27class EventSignals(QObject):
 28    """Signals for thread-safe GUI updates."""
 29
 30    accel_updated = Signal(AccelData)
 31    button_updated = Signal(ButtonData)
 32    ir_updated = Signal(IRData)
 33    battery_updated = Signal(BatteryData)
 34    gpio_updated = Signal(GPIOData)
 35    raw_event = Signal(str)
 36
 37
 38class FreeWiliEventsGUI(QMainWindow):
 39    """Main window for FreeWili events display."""
 40
 41    def __init__(self):
 42        super().__init__()
 43        self.freewili = None
 44        self.event_timer = QTimer()
 45        self.signals = EventSignals()
 46
 47        # Connect signals
 48        self.signals.accel_updated.connect(self.update_accel_display)
 49        self.signals.button_updated.connect(self.update_button_display)
 50        self.signals.ir_updated.connect(self.update_ir_display)
 51        self.signals.battery_updated.connect(self.update_battery_display)
 52        self.signals.gpio_updated.connect(self.update_gpio_display)
 53        self.signals.raw_event.connect(self.update_raw_events)
 54
 55        self.setup_ui()
 56        self.setup_freewili()
 57
 58    def setup_ui(self):
 59        """Set up the user interface."""
 60        self.setWindowTitle("FreeWili Events Monitor")
 61        self.setGeometry(100, 100, 800, 600)
 62
 63        central_widget = QWidget()
 64        self.setCentralWidget(central_widget)
 65
 66        main_layout = QVBoxLayout(central_widget)
 67
 68        # Control buttons
 69        control_layout = QHBoxLayout()
 70
 71        self.connect_btn = QPushButton("Connect")
 72        self.connect_btn.clicked.connect(self.toggle_connection)
 73        control_layout.addWidget(self.connect_btn)
 74
 75        # Event enable checkboxes
 76        self.accel_cb = QCheckBox("Accelerometer")
 77        self.button_cb = QCheckBox("Buttons")
 78        self.ir_cb = QCheckBox("IR")
 79        self.battery_cb = QCheckBox("Battery")
 80        self.gpio_cb = QCheckBox("GPIO")
 81
 82        for cb in [self.accel_cb, self.button_cb, self.ir_cb, self.battery_cb, self.gpio_cb]:
 83            cb.stateChanged.connect(self.update_event_subscriptions)
 84            control_layout.addWidget(cb)
 85
 86        control_layout.addStretch()
 87        main_layout.addLayout(control_layout)
 88
 89        # Data display areas
 90        data_layout = QHBoxLayout()
 91
 92        # Left column - Sensor data
 93        left_layout = QVBoxLayout()
 94
 95        # Accelerometer group
 96        self.accel_group = self.create_accel_group()
 97        left_layout.addWidget(self.accel_group)
 98
 99        # Battery group
100        self.battery_group = self.create_battery_group()
101        left_layout.addWidget(self.battery_group)
102
103        data_layout.addLayout(left_layout)
104
105        # Right column - Digital inputs
106        right_layout = QVBoxLayout()
107
108        # Button group
109        self.button_group = self.create_button_group()
110        right_layout.addWidget(self.button_group)
111
112        # IR group
113        self.ir_group = self.create_ir_group()
114        right_layout.addWidget(self.ir_group)
115
116        # GPIO group
117        self.gpio_group = self.create_gpio_group()
118        right_layout.addWidget(self.gpio_group)
119
120        # Raw events log
121        self.raw_events_group = self.create_raw_events_group()
122        right_layout.addWidget(self.raw_events_group)
123
124        data_layout.addLayout(right_layout)
125        main_layout.addLayout(data_layout)
126
127    def create_accel_group(self):
128        """Create accelerometer data display group."""
129        group = QGroupBox("Accelerometer")
130        layout = QGridLayout(group)
131
132        # Labels and value displays
133        self.accel_labels = {}
134        fields = [
135            ("G-Force:", "g"),
136            ("X:", "x"),
137            ("Y:", "y"),
138            ("Z:", "z"),
139            ("Temp °C:", "temp_c"),
140            ("Temp °F:", "temp_f"),
141        ]
142
143        for i, (label_text, key) in enumerate(fields):
144            label = QLabel(label_text)
145            value_label = QLabel("--")
146            value_label.setStyleSheet("font-weight: bold; color: blue;")
147
148            layout.addWidget(label, i, 0)
149            layout.addWidget(value_label, i, 1)
150
151            self.accel_labels[key] = value_label
152
153        return group
154
155    def create_battery_group(self):
156        """Create battery data display group."""
157        group = QGroupBox("Battery")
158        layout = QGridLayout(group)
159
160        self.battery_labels = {}
161        fields = [
162            ("VBUS:", "vbus"),
163            ("VSYS:", "vsys"),
164            ("VBATT:", "vbatt"),
165            ("ICHG:", "ichg"),
166            ("Charging:", "charging"),
167            ("Complete:", "charge_complete"),
168        ]
169
170        for i, (label_text, key) in enumerate(fields):
171            label = QLabel(label_text)
172            value_label = QLabel("--")
173            value_label.setStyleSheet("font-weight: bold; color: green;")
174
175            layout.addWidget(label, i, 0)
176            layout.addWidget(value_label, i, 1)
177
178            self.battery_labels[key] = value_label
179
180        return group
181
182    def create_button_group(self):
183        """Create button state display group."""
184        group = QGroupBox("Buttons")
185        layout = QGridLayout(group)
186
187        self.button_labels = {}
188        buttons = [("Gray:", "gray"), ("Yellow:", "yellow"), ("Green:", "green"), ("Blue:", "blue"), ("Red:", "red")]
189
190        for i, (label_text, key) in enumerate(buttons):
191            label = QLabel(label_text)
192            value_label = QLabel("Released")
193            value_label.setStyleSheet("font-weight: bold;")
194
195            layout.addWidget(label, i, 0)
196            layout.addWidget(value_label, i, 1)
197
198            self.button_labels[key] = value_label
199
200        return group
201
202    def create_ir_group(self):
203        """Create IR data display group."""
204        group = QGroupBox("IR Data")
205        layout = QVBoxLayout(group)
206
207        self.ir_value_label = QLabel("No data")
208        self.ir_value_label.setStyleSheet("font-weight: bold; color: purple; font-family: monospace;")
209        layout.addWidget(self.ir_value_label)
210
211        return group
212
213    def create_gpio_group(self):
214        """Create GPIO state display group."""
215        group = QGroupBox("GPIO States")
216        layout = QGridLayout(group)
217
218        self.gpio_labels = {}
219
220        # Get GPIO mappings from types.py
221        from freewili.types import GPIO_MAP
222
223        # Create labels for each GPIO pin
224        row = 0
225        for _gpio_num, gpio_name in GPIO_MAP.items():
226            # Show just the GPIO number and main function
227            display_name = gpio_name.replace("/", " / ")
228
229            label = QLabel(f"{display_name}:")
230            value_label = QLabel("--")
231            value_label.setStyleSheet("font-weight: bold; font-family: monospace;")
232
233            layout.addWidget(label, row, 0)
234            layout.addWidget(value_label, row, 1)
235
236            self.gpio_labels[gpio_name] = value_label
237            row += 1
238
239        return group
240
241    def create_raw_events_group(self):
242        """Create raw events log group."""
243        group = QGroupBox("Raw Events Log")
244        layout = QVBoxLayout(group)
245
246        self.raw_events_text = QTextEdit()
247        # self.raw_events_text.setMaximumHeight(150)
248        self.raw_events_text.setStyleSheet("font-family: monospace; font-size: 10px;")
249        layout.addWidget(self.raw_events_text)
250
251        clear_btn = QPushButton("Clear Log")
252        clear_btn.clicked.connect(self.raw_events_text.clear)
253        layout.addWidget(clear_btn)
254
255        return group
256
257    def setup_freewili(self):
258        """Set up FreeWili connection and event processing."""
259        self.event_timer.timeout.connect(self.process_events)
260        self.event_timer.setInterval(50)  # Process events every 50ms
261
262    def event_handler(self, event_type: EventType, frame: ResponseFrame, data: Any) -> None:
263        """Handle events from FreeWili (runs in background thread)."""
264        # Convert the data to the appropriate type and emit signals for thread-safe GUI updates
265        try:
266            if event_type == EventType.Accel and isinstance(data, AccelData):
267                self.signals.accel_updated.emit(data)
268            elif event_type == EventType.Button and isinstance(data, ButtonData):
269                self.signals.button_updated.emit(data)
270            elif event_type == EventType.IR and isinstance(data, IRData):
271                self.signals.ir_updated.emit(data)
272            elif event_type == EventType.Battery and isinstance(data, BatteryData):
273                self.signals.battery_updated.emit(data)
274            elif event_type == EventType.GPIO and isinstance(data, GPIOData):
275                self.signals.gpio_updated.emit(data)
276
277            # Also log to raw events
278            self.signals.raw_event.emit(f"{event_type.name}: {data}")
279
280        except Exception as e:
281            self.signals.raw_event.emit(f"Error processing {event_type.name}: {e}")
282
283    def update_accel_display(self, data: AccelData):
284        """Update accelerometer display."""
285        self.accel_labels["g"].setText(f"{data.g:.2f}g")
286        self.accel_labels["x"].setText(f"{data.x:.1f}")
287        self.accel_labels["y"].setText(f"{data.y:.1f}")
288        self.accel_labels["z"].setText(f"{data.z:.1f}")
289        self.accel_labels["temp_c"].setText(f"{data.temp_c:.1f}°C")
290        self.accel_labels["temp_f"].setText(f"{data.temp_f:.1f}°F")
291
292    def update_button_display(self, data: ButtonData):
293        """Update button display."""
294        buttons = {"gray": data.gray, "yellow": data.yellow, "green": data.green, "blue": data.blue, "red": data.red}
295
296        for key, pressed in buttons.items():
297            label = self.button_labels[key]
298            if pressed:
299                label.setText("PRESSED")
300                label.setStyleSheet("font-weight: bold; color: green;")
301            else:
302                label.setText("Released")
303                label.setStyleSheet("font-weight: bold; color: red;")
304
305    def update_ir_display(self, data: IRData):
306        """Update IR display."""
307        hex_value = data.value.hex().upper()
308        self.ir_value_label.setText(f"0x{hex_value}" if hex_value else "No data")
309
310    def update_battery_display(self, data: BatteryData):
311        """Update battery display."""
312        self.battery_labels["vbus"].setText(f"{data.vbus:.1f}mV")
313        self.battery_labels["vsys"].setText(f"{data.vsys:.1f}mV")
314        self.battery_labels["vbatt"].setText(f"{data.vbatt:.1f}mV")
315        self.battery_labels["ichg"].setText(f"{data.ichg}mA")
316        self.battery_labels["charging"].setText("YES" if data.charging else "NO")
317        self.battery_labels["charge_complete"].setText("YES" if data.charge_complete else "NO")
318
319    def update_gpio_display(self, data: GPIOData):
320        """Update GPIO display."""
321        for gpio_name, value in data.pin.items():
322            if gpio_name in self.gpio_labels:
323                label = self.gpio_labels[gpio_name]
324                if value == 1:
325                    label.setText("HIGH")
326                    label.setStyleSheet("font-weight: bold; color: red; font-family: monospace;")
327                else:
328                    label.setText("LOW")
329                    label.setStyleSheet("font-weight: bold; color: blue; font-family: monospace;")
330
331    def update_raw_events(self, event_text: str):
332        """Update raw events log."""
333        self.raw_events_text.append(event_text)
334
335        # Keep log size manageable
336        if self.raw_events_text.document().blockCount() > 100:
337            cursor = self.raw_events_text.textCursor()
338            cursor.movePosition(cursor.MoveOperation.Start)
339            cursor.select(cursor.SelectionType.BlockUnderCursor)
340            cursor.removeSelectedText()
341            cursor.deleteChar()  # Remove the newline
342
343    def toggle_connection(self):
344        """Connect or disconnect from FreeWili."""
345        if self.freewili is None:
346            try:
347                result = FreeWili.find_first()
348                if result.is_err():
349                    self.signals.raw_event.emit(f"Error: {result.unwrap_err()}")
350                    return
351
352                self.freewili = result.unwrap()
353                self.freewili.set_event_callback(self.event_handler)
354
355                # Open connection
356                open_result = self.freewili.open()
357                if open_result.is_err():
358                    self.signals.raw_event.emit(f"Error opening connection: {open_result.unwrap_err()}")
359                    self.freewili = None
360                    return
361
362                self.connect_btn.setText("Disconnect")
363                self.event_timer.start()
364                self.signals.raw_event.emit("Connected to FreeWili")
365
366                # Enable default events
367                self.accel_cb.setChecked(True)
368                self.button_cb.setChecked(True)
369                self.ir_cb.setChecked(True)
370                self.battery_cb.setChecked(True)
371                self.gpio_cb.setChecked(True)
372
373            except Exception as e:
374                self.signals.raw_event.emit(f"Connection error: {e}")
375
376        else:
377            # Disconnect
378            self.event_timer.stop()
379
380            # Disable all events
381            for cb in [self.accel_cb, self.button_cb, self.ir_cb, self.battery_cb, self.gpio_cb]:
382                cb.setChecked(False)
383
384            if hasattr(self.freewili, "close"):
385                self.freewili.close()
386
387            self.freewili = None
388            self.connect_btn.setText("Connect")
389            self.signals.raw_event.emit("Disconnected from FreeWili")
390
391    def update_event_subscriptions(self):
392        """Update event subscriptions based on checkboxes."""
393        if self.freewili is None:
394            return
395
396        try:
397            # Update accelerometer events
398            if self.accel_cb.isChecked():
399                result = self.freewili.enable_accel_events(True, 100)
400                if result.is_err():
401                    self.signals.raw_event.emit(f"Error enabling accel: {result.unwrap_err()}")
402            else:
403                result = self.freewili.enable_accel_events(False)
404                if result.is_err():
405                    self.signals.raw_event.emit(f"Error disabling accel: {result.unwrap_err()}")
406
407            # Update button events (assuming these methods exist)
408            if hasattr(self.freewili, "enable_button_events"):
409                if self.button_cb.isChecked():
410                    result = self.freewili.enable_button_events(True, 100)
411                    if result.is_err():
412                        self.signals.raw_event.emit(f"Error enabling buttons: {result.unwrap_err()}")
413                else:
414                    result = self.freewili.enable_button_events(False)
415                    if result.is_err():
416                        self.signals.raw_event.emit(f"Error disabling buttons: {result.unwrap_err()}")
417
418            # Update IR events
419            if hasattr(self.freewili, "enable_ir_events"):
420                if self.ir_cb.isChecked():
421                    result = self.freewili.enable_ir_events(True)
422                    if result.is_err():
423                        self.signals.raw_event.emit(f"Error enabling IR: {result.unwrap_err()}")
424                else:
425                    result = self.freewili.enable_ir_events(False)
426                    if result.is_err():
427                        self.signals.raw_event.emit(f"Error disabling IR: {result.unwrap_err()}")
428
429            # Update battery events
430            if hasattr(self.freewili, "enable_battery_events"):
431                if self.battery_cb.isChecked():
432                    result = self.freewili.enable_battery_events(True)
433                    if result.is_err():
434                        self.signals.raw_event.emit(f"Error enabling battery: {result.unwrap_err()}")
435                else:
436                    result = self.freewili.enable_battery_events(False)
437                    if result.is_err():
438                        self.signals.raw_event.emit(f"Error disabling battery: {result.unwrap_err()}")
439
440            # Update GPIO events
441            if hasattr(self.freewili, "enable_gpio_events"):
442                if self.gpio_cb.isChecked():
443                    result = self.freewili.enable_gpio_events(True)
444                    if result.is_err():
445                        self.signals.raw_event.emit(f"Error enabling GPIO: {result.unwrap_err()}")
446                else:
447                    result = self.freewili.enable_gpio_events(False)
448                    if result.is_err():
449                        self.signals.raw_event.emit(f"Error disabling GPIO: {result.unwrap_err()}")
450
451        except Exception as e:
452            self.signals.raw_event.emit(f"Error updating subscriptions: {e}")
453
454    def process_events(self):
455        """Process FreeWili events (called by timer)."""
456        if self.freewili:
457            try:
458                self.freewili.process_events()
459            except Exception as e:
460                self.signals.raw_event.emit(f"Event processing error: {e}")
461
462    def closeEvent(self, event):  # noqa: N802
463        """Handle window close event."""
464        if self.freewili:
465            self.toggle_connection()  # Disconnect cleanly
466        event.accept()
467
468
469def main():
470    """Main function to run the GUI application."""
471    app = QApplication(sys.argv)
472
473    # Set application properties
474    app.setApplicationName("FreeWili Events Monitor")
475    app.setApplicationVersion("1.0")
476
477    # Create and show the main window
478    window = FreeWiliEventsGUI()
479    window.show()
480
481    # Run the application
482    sys.exit(app.exec())
483
484
485if __name__ == "__main__":
486    main()

Audio Recording

Record audio from FreeWili and save it to a WAV file.

 1"""Example script to record audio from FreeWili and save it to a WAV file."""
 2
 3import struct
 4import time
 5import wave
 6
 7from freewili import FreeWili
 8from freewili.framing import ResponseFrame
 9from freewili.types import AudioData, EventType
10
11audio_data = []
12
13
14def event_handler(event_type: EventType, frame: ResponseFrame, data: AudioData) -> None:
15    """Handle events from FreeWili."""
16    if event_type != EventType.Audio:
17        return
18    # print(f"Audio Event: {data.data}")
19    audio_data.append(data.data)
20
21
22with FreeWili.find_first().expect("Failed to find FreeWili") as fw, wave.open("test.wav", "wb") as wav_file:
23    print(f"Connected to {fw}")
24    wav_file.setnchannels(1)  # Mono
25    wav_file.setsampwidth(2)  # 2 bytes per sample (16-bit)
26    wav_file.setframerate(8000)  # 4kHz sample rate
27
28    fw.set_event_callback(event_handler)
29    fw.enable_audio_events(True).expect("Failed to enable audio events")
30    start_time = time.time()
31    print("Listening for audio events... Press Ctrl+C to stop recording...")
32    while True:
33        try:
34            fw.process_events()
35            # Clear the first 2 seconds of audio data to avoid initial noise
36            if time.time() - start_time < 2.0:
37                audio_data.clear()
38            if audio_data:
39                # Write the audio data to the WAV file
40                for data in audio_data:
41                    print(f"\tWriting audio data: {data!r}" + " " * 30, end="\r")
42                    # Convert data to bytes (little-endian 16-bit signed)
43                    audio_bytes = b"".join(struct.pack("<h", sample) for sample in data)
44                    wav_file.writeframes(audio_bytes)
45                audio_data.clear()  # Clear the list after writing
46        except KeyboardInterrupt:
47            print("\nStopping audio recording...")
48            break
49    # Disable audio events before exiting
50    print("Disabling audio events...")
51    fw.enable_audio_events(False).expect("Failed to disable audio events")
52    print(f"Audio saved to {wav_file._file.name}")  # type:ignore
53print("Done.")

Audio Playback - Tone

Play audio tones on the FreeWili device.

 1"""Example script to demonstrate playing audio tones with Free-WiLi."""
 2
 3import time
 4
 5from freewili import FreeWili
 6
 7with FreeWili.find_first().expect("Failed to find FreeWili") as fw:
 8    print(f"Connected to FreeWili {fw}")
 9    print("type 'list' to see audio assets or 'exit' to quit.")
10    while True:
11        try:
12            user_input = input("Enter to play audio tone [hz duration_sec amplitude]: ")
13            frequency_hz, duration_sec, amplitude = map(float, user_input.split())
14            frequency_hz = int(frequency_hz)  # Convert to int for frequency
15            # v54 firmware: Response frame always returns failure
16            fw.play_audio_tone(frequency_hz, duration_sec, amplitude)  # .expect("Failed to play audio tone")
17            time.sleep(duration_sec)  # Give some time for the audio to play, playing is async
18        except (ValueError, KeyboardInterrupt):
19            print("Goodbye!")
20            break
21    print("Done.")

Audio Playback - Number

Play audio number announcements on the FreeWili device.

 1"""Example script to demonstrate playing audio numbers with Free-WiLi."""
 2
 3import time
 4
 5from freewili import FreeWili
 6
 7with FreeWili.find_first().expect("Failed to find FreeWili") as fw:
 8    print(f"Connected to FreeWili {fw}")
 9    while True:
10        try:
11            user_input = int(input("Enter number: "))
12            fw.play_audio_number_as_speech(user_input).expect("Failed to play audio number as speech")
13            time.sleep(len(str(user_input)) * 0.8)  # Give some time for the audio to play, playing is async
14        except (ValueError, KeyboardInterrupt):
15            print("Goodbye!")
16            break
17    print("Done.")

Audio Playback - Assets

Play audio assets/files on the FreeWili device.

  1"""Example script to play audio on the FreeWili."""
  2
  3import time
  4
  5from freewili import FreeWili
  6
  7# https://docs.freewili.com/downloads/FwROMAudioAssets.pdf
  8AUDIO_NAMES = (
  9    "1",
 10    "10",
 11    "2",
 12    "3",
 13    "4",
 14    "42",
 15    "5",
 16    "6",
 17    "7",
 18    "8",
 19    "9",
 20    "About",
 21    "AllYourBases",
 22    "beephigh",
 23    "ChooseYourDestiny",
 24    "click",
 25    "daisy",
 26    "DaveICantDoThat",
 27    "DefCon",
 28    "Dominating",
 29    "DoOrDoNot",
 30    "DoubleKill",
 31    "EndOfLine",
 32    "Engage",
 33    "Excellent",
 34    "ExcuseMe",
 35    "FearIsTheMindKiller",
 36    "FreeWilli",
 37    "GodLike",
 38    "GoodJob",
 39    "GPIO",
 40    "GUI",
 41    "GUISettings",
 42    "GUITerminal",
 43    "HackMe",
 44    "HangTough",
 45    "HaveACupcake",
 46    "HolyShit",
 47    "I2C",
 48    "IllBeBack",
 49    "ImThinking",
 50    "Infinity",
 51    "Invalid",
 52    "IR",
 53    "ItsTime",
 54    "KillingSpree",
 55    "LudacrisKill",
 56    "MegaKill",
 57    "Minus",
 58    "MonsterKill",
 59    "MultiKill",
 60    "No",
 61    "NoWay",
 62    "NoWayWithLaugh",
 63    "Orca",
 64    "palebluedot",
 65    "PinOut",
 66    "PlansWithinPlans",
 67    "Plus",
 68    "Point",
 69    "PorqueNo",
 70    "Radios",
 71    "Rampage",
 72    "Revenge",
 73    "ScanMeHarder",
 74    "Scripts",
 75    "Settings",
 76    "shame",
 77    "ShutUpWesely",
 78    "SmileAndBeHappy",
 79    "Sorry",
 80    "SoSayWeAll",
 81    "SPI",
 82    "Sweet",
 83    "Tasty",
 84    "Terminal",
 85    "ThanksForAllTheFish",
 86    "tom",
 87    "UART",
 88    "UltraKill",
 89    "UnstopableWind",
 90    "unstoppable",
 91    "UserError",
 92    "VivaLosVegas",
 93    "WarWarNeverChanges",
 94    "Welcome",
 95    "WhaleEver",
 96    "WhaleThankU",
 97    "What",
 98    "WickedSick",
 99    "Yes",
100)
101
102with FreeWili.find_first().expect("Failed to find FreeWili") as fw:
103    print(f"Connected to FreeWili {fw}")
104    print("type 'list' to see audio assets or 'exit' to quit.")
105    while True:
106        try:
107            user_input = input("Enter to play audio asset (1-{}): ".format(len(AUDIO_NAMES)))
108            value = int(user_input) - 1  # Convert to zero-based index
109            print(f"\tPlaying: {AUDIO_NAMES[value]}")  # Fixed index here
110            fw.play_audio_asset(value).expect("Failed to play audio asset")
111            time.sleep(1)  # Give some time for the audio to play, playing is async
112        except ValueError:
113            if user_input.lower() == "list":
114                for i, name in enumerate(AUDIO_NAMES, start=1):
115                    print(f"\t{i}: {name}")
116                continue
117            print("Goodbye!")
118            break
119    print("Done.")

IR Communication - Send

Send infrared signals using the FreeWili device.

1"""Example script to demonstrate IR NEC communication with Free-WiLi."""
2
3from freewili import FreeWili
4
5with FreeWili.find_first().expect("Failed to find FreeWili") as fw:
6    roku_keyhome = bytes([0xBE, 0xEF, 00, 0xFF])
7    print("Sending Roku Key Home IR command:", roku_keyhome)
8    fw.send_ir(roku_keyhome).expect("Failed to send IR command")
9print("Done.")

IR Communication - Read

Read and decode infrared signals using the FreeWili device.

 1"""Example script to demonstrate IR NEC communication with Free-WiLi."""
 2
 3from freewili import FreeWili
 4from freewili.framing import ResponseFrame
 5from freewili.types import EventDataType, EventType, IRData
 6
 7
 8def event_callback(event_type: EventType, response_frame: ResponseFrame, event_data: EventDataType) -> None:
 9    """Callback function to handle events from FreeWili."""
10    if isinstance(event_data, IRData):
11        print(f"IR RX {len(event_data.value)}: {event_data.value!r}")
12
13
14with FreeWili.find_first().expect("Failed to find FreeWili") as fw:
15    fw.set_event_callback(event_callback)
16    print("Enabling IR events")
17    fw.enable_ir_events(True).expect("Failed to enable IR events")
18    print("Waiting for IR events... Press Ctrl+C to exit.")
19    while True:
20        try:
21            fw.process_events()
22        except KeyboardInterrupt:
23            print("Exiting IR event loop")
24            break
25    fw.enable_ir_events(False).expect("Failed to disable IR events")
26print("Done.")

UART Communication

UART serial communication example for the FreeWili device.

 1"""Example script to demonstrate UART communication with Free-WiLi."""
 2
 3# Note: v54 firmware has a limitation of 22 bytes per UART message, so we use 16 to be safe.
 4# Note: v54 firmware enable_uart_events is a toggle only, so we enable it once and disable it at the end.
 5
 6import time
 7
 8from freewili import FreeWili
 9from freewili.framing import ResponseFrame
10from freewili.types import EventDataType, EventType, UART1Data
11
12
13def event_callback(event_type: EventType, response_frame: ResponseFrame, event_data: EventDataType) -> None:
14    """Callback function to handle events from FreeWili."""
15    if isinstance(event_data, UART1Data):
16        print(f"UART1 RX {len(event_data.data)}: {event_data.data!r}")
17
18
19with FreeWili.find_first().expect("Failed to find FreeWili") as fw:
20    fw.set_event_callback(event_callback)
21    fw.enable_uart_events(True).expect("Failed to enable UART events")
22    data = b"Hello Free-WiLi from UART1!"
23    chunk_size = 16  # v54 firmware has a limitation of 22 bytes per UART message, so we use 16 to be safe.
24    for i in range(0, len(data), chunk_size):
25        chunk = data[i : i + chunk_size]
26        print(f"Sending UART Data: {chunk!r}")
27        fw.write_uart(chunk).expect("Failed to send UART message")
28    # Wait for the device to process and send back any events. If we close too soon, we won't see the events.
29    time.sleep(0.5)
30    try:
31        fw.process_events()
32    except KeyboardInterrupt:
33        print("Exiting UART event loop")
34    fw.enable_uart_events(False).expect("Failed to disable UART events")
35print("Done.")

SPI Communication

SPI read/write communication example for the FreeWili device.

 1"""Example script to read and write SPI data using FreeWili."""
 2
 3from freewili import FreeWili
 4
 5with FreeWili.find_first().expect("Failed to find FreeWili") as fw:
 6    print(f"Connected to {fw}")
 7    while True:
 8        try:
 9            user_input = input("Enter data bytes seperated by spaces: ").strip().split(" ")
10            data = bytes(int(x, 16) for x in user_input)
11            print(f"Sending {len(data)} bytes: {data!r}")
12            rx_data = fw.read_write_spi_data(data).expect("Failed to send SPI data")
13            print(f"Received {len(rx_data)} bytes: {rx_data!r}")
14        except (ValueError, KeyboardInterrupt):
15            break
16print("Goodbye!")

Filesystem Explorer

Recursively explore and list the filesystem contents on both Display and Main processors of the FreeWili device.

 1"""This script explores the FreeWili filesystem, listing directories and files recursively."""
 2
 3from freewili import FreeWili
 4from freewili.fw import FreeWiliProcessorType as FwProcessor
 5from freewili.types import FileType
 6
 7
 8def explore_directory(
 9    fw: FreeWili,
10    processor: FwProcessor,
11    path: str = "/",
12    indent: int = 0,
13    max_depth: int = 3,
14    visited: None | set[str] = None,
15) -> None:
16    """Recursively explore directories on the FreeWili filesystem."""
17    if visited is None:
18        visited = set()
19
20    # Normalize the path to avoid double slashes
21    path = "/" if path in ["/", "//"] else path
22
23    # Prevent infinite recursion
24    if indent > max_depth:
25        print("  " * indent + f"⚠️  Max depth reached for {path}")
26        return
27
28    # Prevent visiting the same path twice
29    if path in visited:
30        print("  " * indent + f"🔄 Already visited {path}")
31        return
32
33    visited.add(path)
34
35    # Change to the directory
36    if path != "/":
37        change_result = fw.change_directory(path, processor)
38        if change_result.is_err():
39            print("  " * indent + f"❌ Failed to change to {path}: {change_result.unwrap_err()}")
40            return
41
42    # List current directory contents
43    result = fw.list_current_directory(processor)
44    if result.is_err():
45        print("  " * indent + f"❌ Failed to list directory {path}: {result.unwrap_err()}")
46        return
47
48    fs_contents = result.unwrap()
49
50    # Calculate actual item count (excluding parent/current directory references)
51    actual_items = [item for item in fs_contents.contents if item.name not in ["..", "."]]
52
53    # Print current directory info
54    prefix = "  " * indent
55    # Normalize the path display to avoid double slashes
56    if fs_contents.cwd == "/":
57        display_path = "/"
58    else:
59        display_path = f"{fs_contents.cwd}/"
60    print(f"{prefix}📁 {display_path} ({len(actual_items)} items)")
61
62    # Print files only (we'll show directories when we explore them)
63    for item in fs_contents.contents:
64        if item.name == ".." or item.name == ".":
65            continue  # Skip parent/current directory references
66
67        if item.file_type == FileType.File:
68            print(f"{prefix}  📄 {item.name} ({item.size} bytes)")
69
70    # Recursively explore subdirectories (excluding parent directory references)
71    for item in fs_contents.contents:
72        if item.file_type == FileType.Directory and item.name not in ["..", "."]:
73            # Build the new path, handling root directory properly
74            current_dir = fs_contents.cwd if fs_contents.cwd not in ["/", "//"] else ""
75            if current_dir:
76                new_path = f"{current_dir}/{item.name}"
77            else:
78                new_path = f"/{item.name}"
79
80            explore_directory(fw, processor, new_path, indent + 1, max_depth, visited)
81
82            # Change back to the original directory after exploring subdirectory
83            # Normalize the path to avoid double slashes
84            return_path = fs_contents.cwd if fs_contents.cwd not in ["//"] else "/"
85            fw.change_directory(return_path, processor)
86
87
88with FreeWili.find_first().expect("Failed to find FreeWili") as fw:
89    print(f"Connected to {fw}")
90    print("\n=== Exploring Display Processor Filesystem ===")
91    fw.change_directory("/", FwProcessor.Display).expect("Failed to change to root directory on Display processor")
92    explore_directory(fw, FwProcessor.Display)
93
94    print("\n=== Exploring Main Processor Filesystem ===")
95    fw.change_directory("/", FwProcessor.Main).expect("Failed to change to root directory on Main processor")
96    explore_directory(fw, FwProcessor.Main)