From 6147187a286bf49576cdbf0c16fd0e5af53ff3a7 Mon Sep 17 00:00:00 2001 From: Anubhav Harsh Sinha Date: Tue, 22 Sep 2026 20:48:43 +0530 Subject: [PATCH 1/5] fix: set the bootloader baudrate on the connection enter_bootloader() still used self.device.interface, which the SerialHandler refactor removed, so it raised AttributeError. Set the baudrate through SerialHandler.baudrate. --- pslab/sciencelab.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pslab/sciencelab.py b/pslab/sciencelab.py index 1cbc81e..cd52e51 100644 --- a/pslab/sciencelab.py +++ b/pslab/sciencelab.py @@ -123,7 +123,7 @@ def enter_bootloader(self): raise RuntimeError(msg) self.reset() - self.device.interface.baudrate = 460800 + self.device.baudrate = 460800 # The PSLab's RGB LED flashes some colors on boot. boot_lightshow_time = 0.6 # Wait before sending magic number to make sure UART is initialized. From 63b3fd7d093ad25e4073185e8a5036238c6ec70b Mon Sep 17 00:00:00 2001 From: Anubhav Harsh Sinha Date: Tue, 22 Sep 2026 20:48:51 +0530 Subject: [PATCH 2/5] fix: flash through the ScienceLab's serial connection flash() read psl.interface, which ScienceLab no longer has, and passed the ScienceLab to mcbootflash, which needs an object with read() and write(). Use psl.device, the SerialHandler, for both. --- pslab/cli.py | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/pslab/cli.py b/pslab/cli.py index 5ba833b..b9ea321 100644 --- a/pslab/cli.py +++ b/pslab/cli.py @@ -530,29 +530,31 @@ def flash(psl: pslab.ScienceLab, hexfile: str): PSLab must be in bootloader mode. """ - if psl.interface.baudrate == 1000000: - psl.interface.timeout = 5 + device = psl.device + + if device.baudrate == 1000000: + device.timeout = 5 psl.enter_bootloader() try: - bootattrs = mcbootflash.get_boot_attrs(psl) + bootattrs = mcbootflash.get_boot_attrs(device) except struct.error: print("Flashing failed: PSLab is not in bootloader mode.") return - mcbootflash.erase_flash(psl, bootattrs.memory_range, bootattrs.erase_size) + mcbootflash.erase_flash(device, bootattrs.memory_range, bootattrs.erase_size) total_bytes, chunks = mcbootflash.chunked(hexfile, bootattrs) written = 0 for chunk in chunks: - mcbootflash.write_flash(psl, chunk) - mcbootflash.checksum(psl, chunk) + mcbootflash.write_flash(device, chunk) + mcbootflash.checksum(device, chunk) written += len(chunk.data) print(f"{written}/{total_bytes} bytes flashed.", end="\r") print("", end="\n") - mcbootflash.self_verify(psl) - mcbootflash.reset(psl) + mcbootflash.self_verify(device) + mcbootflash.reset(device) def add_flash_args(subparser: argparse._SubParsersAction): From 2fca0844273533b7bcf4e0ee5ea0bca27a9b7450 Mon Sep 17 00:00:00 2001 From: Anubhav Harsh Sinha Date: Tue, 22 Sep 2026 20:48:51 +0530 Subject: [PATCH 3/5] fix: build the flashing ScienceLab from the open handler ScienceLab now takes a connection handler, but main() passed it the port string, which failed with AttributeError. Pass the SerialHandler that main() has already opened on that port. --- pslab/cli.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pslab/cli.py b/pslab/cli.py index b9ea321..241b0a6 100644 --- a/pslab/cli.py +++ b/pslab/cli.py @@ -237,7 +237,7 @@ def main(args: argparse.Namespace): handler = SerialHandler(port=args.port) if args.function == "flash": - flash(pslab.ScienceLab(args.port), args.hexfile) + flash(pslab.ScienceLab(handler), args.hexfile) return if args.function == "collect": From 3c8c1195d3cc456ff6dba19729a65837a1c7a724 Mon Sep 17 00:00:00 2001 From: Anubhav Harsh Sinha Date: Tue, 22 Sep 2026 20:49:21 +0530 Subject: [PATCH 4/5] test: cover flashing without hardware Run enter_bootloader() and flash() against a SerialHandler on pyserial's loop:// port with mcbootflash stubbed, and check that main() hands the opened handler to ScienceLab. --- tests/test_flash.py | 67 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 67 insertions(+) create mode 100644 tests/test_flash.py diff --git a/tests/test_flash.py b/tests/test_flash.py new file mode 100644 index 0000000..5e78249 --- /dev/null +++ b/tests/test_flash.py @@ -0,0 +1,67 @@ +"""Tests for flashing firmware from the CLI. + +These tests do not require a connected PSLab. +""" + +import argparse +from unittest.mock import MagicMock + +import pytest +import serial + +import pslab +from pslab import cli +from pslab.connection import SerialHandler + + +@pytest.fixture +def handler() -> SerialHandler: + sh = SerialHandler("loop://") + sh._ser = serial.serial_for_url("loop://", baudrate=1000000, timeout=0.1) + return sh + + +@pytest.fixture +def psl(handler: SerialHandler) -> pslab.ScienceLab: + psl = pslab.ScienceLab.__new__(pslab.ScienceLab) + psl.device = handler + return psl + + +def test_enter_bootloader_switches_baudrate(psl, monkeypatch): + monkeypatch.setattr("time.sleep", lambda _: None) + + psl.enter_bootloader() + + assert psl.device.baudrate == 460800 + + +def test_flash_uses_the_serial_connection(psl, monkeypatch): + bootflash = MagicMock() + bootflash.chunked.return_value = (0, []) + monkeypatch.setattr(cli, "mcbootflash", bootflash) + monkeypatch.setattr(psl, "enter_bootloader", MagicMock()) + + cli.flash(psl, "firmware.hex") + + psl.enter_bootloader.assert_called_once() + assert psl.device.timeout == 5 + bootflash.get_boot_attrs.assert_called_once_with(psl.device) + bootflash.reset.assert_called_once_with(psl.device) + + +def test_main_flashes_through_the_opened_handler(handler, monkeypatch): + monkeypatch.setattr(cli, "SerialHandler", lambda port: handler) + monkeypatch.setattr( + pslab.ScienceLab, + "__init__", + lambda self, device: setattr(self, "device", device), + ) + flash = MagicMock() + monkeypatch.setattr(cli, "flash", flash) + + cli.main(argparse.Namespace(function="flash", port=None, hexfile="firmware.hex")) + + psl, hexfile = flash.call_args.args + assert psl.device is handler + assert hexfile == "firmware.hex" From d3b8015173a867a6b47211f066e0b28242fe170e Mon Sep 17 00:00:00 2001 From: Anubhav Harsh Sinha Date: Tue, 22 Sep 2026 21:53:52 +0530 Subject: [PATCH 5/5] test: close the loop:// port after each flash test --- tests/test_flash.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tests/test_flash.py b/tests/test_flash.py index 5e78249..d9aebbb 100644 --- a/tests/test_flash.py +++ b/tests/test_flash.py @@ -18,7 +18,8 @@ def handler() -> SerialHandler: sh = SerialHandler("loop://") sh._ser = serial.serial_for_url("loop://", baudrate=1000000, timeout=0.1) - return sh + yield sh + sh._ser.close() @pytest.fixture