Metadata-Version: 2.5
Name: plesty-thorlabs-elliptec
Version: 0.1.0
Summary: A plesty thorlabs elliptec device.
Author-email: Christopher Borchers <christopher.borchers@fkp.uni-hannover.de>
Maintainer-email: Plesty Development Team <plesty.dev@example.com>
License-Expression: LGPL-3.0-or-later
License-File: LICENSE
License-File: LICENSES/LGPL-3.0-or-later.txt
Requires-Python: >=3.12
Requires-Dist: plesty-lib>=0.3.5
Requires-Dist: pyserial>=3.5
Description-Content-Type: text/markdown

# Plesty Thorlabs Elliptec Device

A [Plesty](https://plesty.net) device for Thorlabs Elliptec resonant piezo
modules — an ELL14 rotation mount by default, and the rest of the family
through the same ASCII protocol. Modules share one serial bus and are told
apart by a hex address, so a port alone does not identify a device.

## Installation

A lab PC often has no system `git` at all. plesty-server ships its own, so put
that on `PATH` **before anything else** — the clone needs it and so does the
sync:

```powershell
$env:PATH = "$env:USERPROFILE\.plesty\server\tools\git\cmd;$env:PATH"
```

```bash
git clone https://gitlab.com/plesty/hub/devices/thorlab/plesty-thorlabs-elliptec.git
cd plesty-thorlabs-elliptec
uv sync
```

The sync needs `git` for a second reason, and fails differently without it.
The build backend is versioningit, which derives the version by shelling out
to `git describe` from the build subprocess — so an install driven through
plesty-server succeeds while the identical command in a bare shell fails
with:

```
NotSdistError: ... does not contain a PKG-INFO file
You may be installing from a shallow clone, in which case you need to
unshallow it first.
```

The message blames a shallow clone; the cause is the missing executable, and
the clone is fine.

A plain `git clone` checks out `main`. The ELL14 work — homing in particular —
is on `exp`.

## Configuration

Copy `.env.example` to `.env` and fill in `DEVICE_PORT` — the serial port the
bus is on, `COM3` on Windows or `/dev/ttyUSB0` on Linux. The bus interface
enumerates as an FTDI USB-serial port. `DEVICE_ADDRESS` defaults to `0`, which
is what modules ship at.

The same `.env` drives the standalone server and the real-device test.

### Finding the port instead of configuring it

A port is host-specific and can change across reboots, so it is the setting
people most often get wrong. Leave `DEVICE_PORT` unset and the server searches
for the module instead:

```bash
uv run python -m plesty.thorlabs_elliptec --tcp-port 5556
```

```python
from plesty.thorlabs_elliptec import discover_modules, Device

for found in discover_modules():
    print(found)  # ELL14 (SN …, 143360 pulses/unit) at address 0 on COM3

stage = Device.discover()  # first module found, ready to connect()
```

Only `in` is sent, so nothing moves. **Only ports with a USB vendor ID are
probed** — a motherboard's legacy `COM1` and a Bluetooth serial profile both
enumerate as serial ports, neither can be an Elliptec bus, and opening one
disturbs whatever is there for no possible gain. FTDI adapters are tried
first, since that is what the bus interface is, so the usual case opens one
port and stops. Pass `include_non_usb=True` (or `--include-non-usb`) to widen
that deliberately.

The default asks address `0` only. A bus whose modules have been re-addressed
needs `addresses=protocol.VALID_ADDRESSES` to sweep all sixteen.

## Run as TCP server

Make sure the module is connected and powered on.

```bash
uv run python -m plesty.thorlabs_elliptec --port COM3 --address 0 --tcp-port 5556
```

Give the port as the argument or as `DEVICE_PORT`; with neither, the server
searches for the module as described above. Run one server per module when
several share a bus, each on its own TCP port.

## Use as a library

```python
from plesty.thorlabs_elliptec import Device

with Device(port="COM3", address="0") as stage:
    stage.home()
    stage.move_absolute(degrees=45.0)
    stage.get_position()  # deg
```

A port whose name contains `mock` binds the simulator instead of a serial
line, which is what the test suite runs against.

Before commanding motion, check that the mount can actually turn — that
whatever it carries is clear of the bench and of neighbouring optics. A stage
fouling on something still reports status OK and still answers every move: it
simply arrives a few counts short, which reads as backlash rather than as an
obstruction. Positions read at rest alternate between two adjacent encoder
counts, so treat anything within a couple of counts as measurement noise.

## Testing

```bash
uv run pytest              # simulator only — never opens a port
uv run pytest --hardware   # adds the tests that talk to a real module
```

`--hardware` is deliberately a flag and not a config file. A bench sets
`DEVICE_PORT` in `.env` once and forgets it, so gating on that setting alone
made a bare `pytest` a hardware operation on every machine wired to an
instrument — harmless while the only such test connected and identified, and
not harmless the moment someone adds one that moves the stage. The flag says
*may*; `DEVICE_PORT` still says *where*, and both are required.

### Two ways a green suite lies

A green suite here does not mean the driver can talk to a module. There are
two distinct reasons, and they need different defences.

**A mock that agrees with the driver.** The simulator has three times
certified a command that did nothing on hardware:

- it accepted a `mode="query"` keyword the real serial transport rejects, so
  no command could be sent at all;
- it reported an ELL14 resolution of 262144, a figure no ELL14 has;
- it answered a home that carried no direction byte, which a rotary module
  ignores completely.

One cause underneath all three: **the simulator was written from the driver
rather than from the protocol manual**, so it encodes the driver's
misunderstanding and then certifies it. A mock built that way cannot fail in
the direction of the instrument, only in the direction of the code.

So when adding to `_simulator.py`, take the frame from the manual, and make it
refuse what the hardware refuses — a handler that ignores its payload will
accept a command the module will not.

**A path no development machine can reach.** Port discovery shipped with a
0.5 s probe window, which the serial transport truncates to `int(0.5) == 0`,
so it found nothing on any bench. Every test passed — not because anything was
mocked wrongly, but because a machine with no candidate serial port never
enters the probe at all, and an unexecuted path is indistinguishable from a
passing one in the test output.

This is the worse of the two. A wrong mock can be corrected by reading the
manual; a path that only a bench with the hardware attached can execute cannot
be reasoned about from a laptop at all. Anything about framing, timing or the
transport needs a real module in front of it before it is believed.

## Documentation

`uv run plesty docs serve` builds and serves the reference pages — parameters,
operations and standard methods are generated from the device itself.

## License

LGPL-3.0-or-later — see [LICENSE](LICENSE).
