Metadata-Version: 2.4
Name: matterlab_serial_device
Version: 1.2.1
Summary: A wrapper for PySerial device interfaces.
Author: Han Hao, Martin Seifrid
License-Expression: MIT
Project-URL: homepage, https://gitlab.com/aspuru-guzik-group/self-driving-lab/devices/serial-device
Keywords: pyserial,device,interface
Classifier: Intended Audience :: Science/Research
Classifier: Programming Language :: Python :: 3
Classifier: Topic :: Scientific/Engineering
Classifier: Topic :: Scientific/Engineering :: Chemistry
Classifier: Topic :: System :: Hardware
Classifier: Topic :: System :: Hardware :: Hardware Drivers
Classifier: Topic :: System :: Hardware :: Universal Serial Bus (USB)
Requires-Python: >=3.9
Description-Content-Type: text/markdown
License-File: LICENSE
Requires-Dist: pyserial
Provides-Extra: dev
Requires-Dist: mypy>=0; extra == "dev"
Requires-Dist: pre-commit; extra == "dev"
Requires-Dist: pytest>=7; extra == "dev"
Requires-Dist: pytest-cov>=3; extra == "dev"
Requires-Dist: pytest-mock>=3; extra == "dev"
Requires-Dist: pytest-rerunfailures; extra == "dev"
Requires-Dist: pytest-timeout; extra == "dev"
Requires-Dist: ruff; extra == "dev"
Dynamic: license-file

# Serial Device Interface

`matterlab_serial_device` provides a small base class for serial lab instruments. Normal connections use pyserial directly. An optional transport factory lets higher-level device packages run their protocol code against a simulator.

## Install

```bash
pip install .
```

The runtime dependency is `pyserial`.

## Real serial devices

```python
from matterlab_serial_device import SerialDevice

device = SerialDevice(com_port="COM5", baudrate=9600, timeout=2)
try:
    device.open_device_comm()
    device.write(b"A")
    response = device.read_bytes(num_bytes=1)
finally:
    device.close_device_comm()
```

`read_bytes(num_bytes=1)` reads a one-byte reply. For line-oriented devices, `read()` decodes a response using `default_eol` (`b"\r\n"`), and `query()` writes then reads. Device subclasses can put `@open_close` on a complete command method to manage the port around that command.

`rts` and `dtr` are optional `SerialDevice` arguments. The current implementation applies them after pyserial's initial open, then closes the port; hardware sensitive to the initial line state may still see that first transition.

## Sharing one physical port

Set `share_port=True` on logical devices that use the same physical serial bus, such as addressed RS485 devices:

```python
first = SerialDevice(com_port="COM5", timeout=1, share_port=True)
second = SerialDevice(com_port="COM5", timeout=1, share_port=True)
try:
    assert first.device is second.device
    # Use query() or an @open_close-decorated method for a complete transaction.
finally:
    first.disconnect()
    second.disconnect()
```

Sharing is within one Python process. Instances must use matching serial settings and the same transport factory. A complete `query()` or `@open_close` method holds the shared lock; separate calls to `write()` and `read()` do not form one atomic transaction.

## Simulating a device

A simulator supplies a serial-like object through `transport_factory`. The factory receives a `SerialTransportConfig` and returns an object with `is_open`, `open()`, `close()`, `write(bytes)`, and `read_until(expected, size)`:

```python
from matterlab_serial_device import ScriptedSerialTransport, SerialDevice

def make_simulator(config):
    return ScriptedSerialTransport(
        config,
        response_map={b"PING\r\n": b"PONG\r\n"},
    )

device = SerialDevice(com_port="SIM::example", transport_factory=make_simulator)
try:
    device.open_device_comm()
    assert device.query("PING\r\n", read_delay=0) == "PONG\r\n"
finally:
    device.disconnect()
```

The default `SerialDevice` path does not construct a transport adapter: it calls `serial.Serial` for device names or `serial.serial_for_url` for supported URLs. The transport classes remain available for simulators and existing downstream code. See [transport design](docs/transport_layer_design.md) for the exact boundary and lifecycle.

Use `connect_hardware=False` to defer connection creation until `open_device_comm()` or an `@open_close` method is called. Code that previously accessed `device._connection` through the default adapter should use `device` directly, or explicitly select `transport_factory=PySerialTransport` while migrating.
