-
Notifications
You must be signed in to change notification settings - Fork 233
Add MockHandler and allow ScienceLab(mock=True) #274
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,123 @@ | ||||||||||||||||||||||||||||||||||||||||||||||||||
| """Mock connection handler for PSLab. | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||
| This module provides a minimal in-memory `ConnectionHandler` implementation for | ||||||||||||||||||||||||||||||||||||||||||||||||||
| use in tests and development without physical PSLab hardware. | ||||||||||||||||||||||||||||||||||||||||||||||||||
| """ | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||
| from __future__ import annotations | ||||||||||||||||||||||||||||||||||||||||||||||||||
| from collections import deque | ||||||||||||||||||||||||||||||||||||||||||||||||||
| import pslab.protocol as CP | ||||||||||||||||||||||||||||||||||||||||||||||||||
| from pslab.connection.connection import ConnectionHandler | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||
| class MockHandler(ConnectionHandler): | ||||||||||||||||||||||||||||||||||||||||||||||||||
| """In-memory mock implementation of `ConnectionHandler`. | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||
| The handler queues deterministic responses based on bytes written via | ||||||||||||||||||||||||||||||||||||||||||||||||||
| `write()` so higher-level code can be exercised without an actual device. | ||||||||||||||||||||||||||||||||||||||||||||||||||
| """ | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||
| def __init__(self, version: str = "PSLab V6 ", fw=(3, 0, 0)) -> None: | ||||||||||||||||||||||||||||||||||||||||||||||||||
| """Initialize MockHandler. | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||
| Parameters | ||||||||||||||||||||||||||||||||||||||||||||||||||
| ---------- | ||||||||||||||||||||||||||||||||||||||||||||||||||
| version : str, optional | ||||||||||||||||||||||||||||||||||||||||||||||||||
| Version string to return when queried. Default is "PSLab V6 ". | ||||||||||||||||||||||||||||||||||||||||||||||||||
| fw : tuple of int, optional | ||||||||||||||||||||||||||||||||||||||||||||||||||
| Firmware version as a (major, minor, patch) tuple. Default is (3, 0, 0). | ||||||||||||||||||||||||||||||||||||||||||||||||||
| """ | ||||||||||||||||||||||||||||||||||||||||||||||||||
| self._rx = deque() # bytes to be read | ||||||||||||||||||||||||||||||||||||||||||||||||||
| self._tx = bytearray() # bytes written by client | ||||||||||||||||||||||||||||||||||||||||||||||||||
| self.version = version # convenient attribute for callers | ||||||||||||||||||||||||||||||||||||||||||||||||||
| self._fw = fw | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+28
to
+33
|
||||||||||||||||||||||||||||||||||||||||||||||||||
| Firmware version as a (major, minor, patch) tuple. Default is (3, 0, 0). | |
| """ | |
| self._rx = deque() # bytes to be read | |
| self._tx = bytearray() # bytes written by client | |
| self.version = version # convenient attribute for callers | |
| self._fw = fw | |
| Firmware version as a (major, minor, patch) tuple of integers in the | |
| range 0–255. Default is (3, 0, 0). | |
| """ | |
| self._rx = deque() # bytes to be read | |
| self._tx = bytearray() # bytes written by client | |
| self.version = version # convenient attribute for callers | |
| # Validate firmware version: must be three integers in byte range 0–255 | |
| if not isinstance(fw, (tuple, list)) or len(fw) != 3: | |
| raise ValueError( | |
| "fw must be a tuple or list of three integers in the range 0–255" | |
| ) | |
| for part in fw: | |
| if not isinstance(part, int) or not (0 <= part <= 255): | |
| raise ValueError( | |
| "fw must contain three integers in the range 0–255" | |
| ) | |
| self._fw = tuple(fw) |
Copilot
AI
Feb 13, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The __init__ method should have a Parameters section in its docstring following the NumPy docstring conventions used throughout the codebase. The version and fw parameters should be documented. For example:
Parameters
version : str, optional
Version string to return when queried. Default is "PSLab V6 ".
fw : tuple of int, optional
Firmware version tuple (major, minor, patch). Default is (3, 0, 0).
Copilot
AI
Feb 13, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
When insufficient bytes are available in the receive buffer, this method returns fewer bytes than requested instead of blocking or raising an exception. This differs from the behavior of real connection handlers (which would timeout). Consider documenting this behavior, or optionally raising an exception when insufficient data is available to help catch test setup issues early.
Copilot
AI
Feb 13, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
When an unknown command is encountered under CP.COMMON, the loop breaks instead of continuing to process remaining bytes. This means if multiple commands are written in sequence and one is unknown, subsequent commands won't be processed. Consider using continue after incrementing past the unknown command, or at minimum, consuming the unknown command bytes to prevent them from blocking future command processing.
| # Unknown command under CP.COMMON: drop and stop | |
| break | |
| # Unknown command under CP.COMMON: drop it and continue parsing | |
| self._tx = self._tx[2:] | |
| continue |
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -11,7 +11,7 @@ | |||||||||||||||||||||||||||||||||
| from typing import Iterable, List | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| import pslab.protocol as CP | ||||||||||||||||||||||||||||||||||
| from pslab.connection import ConnectionHandler, SerialHandler, autoconnect | ||||||||||||||||||||||||||||||||||
| from pslab.connection import ConnectionHandler, SerialHandler, MockHandler, autoconnect | ||||||||||||||||||||||||||||||||||
| from pslab.instrument.logic_analyzer import LogicAnalyzer | ||||||||||||||||||||||||||||||||||
| from pslab.instrument.multimeter import Multimeter | ||||||||||||||||||||||||||||||||||
| from pslab.instrument.oscilloscope import Oscilloscope | ||||||||||||||||||||||||||||||||||
|
|
@@ -22,27 +22,57 @@ | |||||||||||||||||||||||||||||||||
| class ScienceLab: | ||||||||||||||||||||||||||||||||||
| """Aggregate interface for the PSLab's instruments. | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| Parameters | ||||||||||||||||||||||||||||||||||
| ---------- | ||||||||||||||||||||||||||||||||||
| device : ConnectionHandler, optional | ||||||||||||||||||||||||||||||||||
| Connection handler for communicating with the PSLab device. If not | ||||||||||||||||||||||||||||||||||
| provided, a new one will be created via autoconnect. If both *device* | ||||||||||||||||||||||||||||||||||
| and *mock* are provided, *device* takes precedence and *mock* is | ||||||||||||||||||||||||||||||||||
| ignored. | ||||||||||||||||||||||||||||||||||
| mock : bool, optional | ||||||||||||||||||||||||||||||||||
| If True, use a MockHandler instead of connecting to physical hardware. | ||||||||||||||||||||||||||||||||||
| Instruments will not be instantiated in mock mode. The default is | ||||||||||||||||||||||||||||||||||
| False. | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| Attributes | ||||||||||||||||||||||||||||||||||
| ---------- | ||||||||||||||||||||||||||||||||||
| logic_analyzer : pslab.LogicAnalyzer | ||||||||||||||||||||||||||||||||||
| oscilloscope : pslab.Oscilloscope | ||||||||||||||||||||||||||||||||||
| waveform_generator : pslab.WaveformGenerator | ||||||||||||||||||||||||||||||||||
| pwm_generator : pslab.PWMGenerator | ||||||||||||||||||||||||||||||||||
| multimeter : pslab.Multimeter | ||||||||||||||||||||||||||||||||||
| power_supply : pslab.PowerSupply | ||||||||||||||||||||||||||||||||||
| logic_analyzer : pslab.LogicAnalyzer or None | ||||||||||||||||||||||||||||||||||
| oscilloscope : pslab.Oscilloscope or None | ||||||||||||||||||||||||||||||||||
| waveform_generator : pslab.WaveformGenerator or None | ||||||||||||||||||||||||||||||||||
| pwm_generator : pslab.PWMGenerator or None | ||||||||||||||||||||||||||||||||||
| multimeter : pslab.Multimeter or None | ||||||||||||||||||||||||||||||||||
| power_supply : pslab.PowerSupply or None | ||||||||||||||||||||||||||||||||||
| i2c : pslab.I2CMaster | ||||||||||||||||||||||||||||||||||
| nrf : pslab.peripherals.NRF24L01 | ||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||
| nrf : pslab.peripherals.NRF24L01 | |
| nrf : pslab.peripherals.NRF24L01 | |
| These attributes are set to None when initialized with mock=True. |
Copilot
AI
Feb 13, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The docstring for the ScienceLab class should document the new mock parameter. Following the pattern seen in other classes in the codebase (e.g., LogicAnalyzer), a Parameters section should be added documenting both the device and mock parameters. For example:
Parameters
device : ConnectionHandler, optional
Connection handler for communicating with the PSLab device. If not provided, a new one will be created via autoconnect.
mock : bool, optional
If True, use a mock handler instead of connecting to physical hardware. Instruments will not be instantiated in mock mode. The default is False.
| nrf : pslab.peripherals.NRF24L01 | |
| nrf : pslab.peripherals.NRF24L01 | |
| Parameters | |
| ---------- | |
| device : ConnectionHandler, optional | |
| Connection handler for communicating with the PSLab device. If not | |
| provided, a new one will be created via autoconnect. | |
| mock : bool, optional | |
| If True, use a mock handler instead of connecting to physical | |
| hardware. Instruments will not be instantiated in mock mode. The | |
| default is False. |
Copilot
AI
Feb 13, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
When both device and mock=True are provided, the device parameter silently takes precedence and the mock parameter is ignored. This behavior may be unexpected for users. Consider either raising a ValueError when both are provided, or documenting this precedence in the docstring if it's intentional.
Copilot
AI
Feb 13, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The conditional logic here only checks the mock parameter, which can lead to incorrect behavior when both device and mock are provided. For example:
ScienceLab(device=MockHandler(), mock=False)would attempt to initialize instruments with a MockHandlerScienceLab(device=real_device, mock=True)would skip instrument initialization despite having a real device
The logic should determine whether to initialize instruments based on the actual type of the device, not just the mock parameter. Consider checking isinstance(self.device, MockHandler) instead of relying solely on the mock parameter value.
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,25 @@ | ||
| from unittest.mock import patch | ||
|
|
||
| from pslab.sciencelab import ScienceLab | ||
|
|
||
|
|
||
| def test_sciencelab_mock_does_not_autoconnect(): | ||
| # If autoconnect is called, the test should fail immediately. | ||
| with patch( | ||
| "pslab.sciencelab.autoconnect", | ||
| side_effect=AssertionError("autoconnect should not be called"), | ||
| ): | ||
| psl = ScienceLab(mock=True) | ||
|
|
||
| # It should initialize and provide the expected mock firmware version object. | ||
| assert psl.firmware.major == 3 | ||
| assert psl.firmware.minor == 0 | ||
| assert psl.firmware.patch == 0 | ||
|
|
||
| # In mock mode, instruments should not be instantiated (no hardware required). | ||
| assert psl.logic_analyzer is None | ||
| assert psl.oscilloscope is None | ||
| assert psl.waveform_generator is None | ||
| assert psl.pwm_generator is None | ||
| assert psl.multimeter is None | ||
| assert psl.power_supply is None |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The
fwparameter is missing a type annotation. Consider addingtuple[int, int, int]to be consistent with other parameters and improve type safety.