Metadata-Version: 2.5
Name: plesty-attodry2100xl-enspire-server
Version: 0.1.0
Summary: A Plesty AttoDRY2100XL eNSPIRE server device.
Author-email: C20H16N4 <c20h16n4@tutanota.de>
Maintainer-email: "Plesty Development Team, C20H16N4" <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<1,>=0.3.5
Description-Content-Type: text/markdown

# Plesty attoDRY2100XL eNSPIRE server

A Plesty device server for the attoDRY2100XL eNSPIRE controller. It translates a
small, explicitly typed Plesty API into the controller's JSON-RPC 2.0 protocol.
Laboratory clients can read cryostat telemetry and use guarded temperature and
magnet controls over ZeroMQ; arbitrary JSON-RPC forwarding is intentionally not
available.

## Architecture

The server runs on the computer that is directly connected to eNSPIRE:

```text
laboratory client
    |  Plesty over ZeroMQ, normally TCP 5555
    v
this device server on the control computer
    |  eNSPIRE JSON-RPC 2.0, normally TCP 9090
    v
eNSPIRE controller
```

The two TCP ports serve different protocols and must not be interchanged:

- `DEVICE_PORT` is the vendor JSON-RPC port on the eNSPIRE controller, normally
  `9090`. Only the directly connected control computer needs to reach it.
- `DEVICE_TCP_PORT` (or `--tcp-port`) is the ZeroMQ port that this server binds,
  normally `5555`. Approved laboratory clients connect to this port.

The server keeps one persistent controller connection and uses one dedicated
worker thread by default. This serializes controller calls and preserves the
connection's thread affinity.

## Installation

Python 3.12 or newer and [uv](https://docs.astral.sh/uv/) are required.

```bash
git clone https://gitlab.com/plesty/hub/devices/attocube/plesty-attodry2100xl-enspire-server.git
cd plesty-attodry2100xl-enspire-server
uv sync
```

Copy `.env.example` to `.env`, then edit `.env` on the control computer. The
real `.env` is ignored by Git and must never be committed or pasted into issue
reports. Process-environment values override `.env`; command-line connection
arguments override both.

```powershell
Copy-Item .env.example .env
```

At minimum, set:

```dotenv
DEVICE_HOST=<controller host>
DEVICE_PORT=9090
DEVICE_TCP_PORT=5555
```

`DEVICE_HOST` and `DEVICE_PORT` select the eNSPIRE endpoint. `DEVICE_TCP_PORT`
selects the Plesty listener. `--host`, `--port`, and `--tcp-port` are temporary
command-line overrides. `--id` changes the Plesty device instance ID.

The remaining settings are:

| Variable | Purpose |
| --- | --- |
| `ENSPIRE_RUN_HARDWARE_TESTS` | Explicit opt-in for the harmless real-controller pytest; leave `0` for normal test runs. |
| `ENSPIRE_MAGNET_{X,Y,Z}_CHANNEL` | Vendor channel number for an installed magnet axis. |
| `ENSPIRE_MAGNET_{X,Y,Z}_MIN_T` / `MAX_T` | Installation-approved axis bounds in tesla. |
| `ENSPIRE_MAGNET_COUPLED_VECTOR_MAX_T` | Maximum target-vector magnitude whenever X or Y is nonzero. |
| `ENSPIRE_MAGNET_TRANSVERSE_ZERO_TOLERANCE_T` | Measured X/Y tolerance for entering or starting Z-only operation. |
| `ENSPIRE_MAGNET_ZERO_TOLERANCE_T` | Measured absolute-field tolerance used by `magnet_off`. |
| `ENSPIRE_MAGNET_ZERO_TIMEOUT_S` | Maximum time `magnet_off` waits for measured zero. |
| `ENSPIRE_MAGNET_POLL_INTERVAL_S` | Interval between measured-field checks during `magnet_off`. |
| `DEVICE_ADDRESS` / `PLESTY_REPORT_ARCHIVE` | Optional Plesty field-test settings; not needed to run the server. |

Magnet writes are disabled unless at least one axis has a complete channel,
minimum, and maximum configuration and both global magnet settings are set.
Leave an unavailable axis completely blank. Partial configuration is a startup
error. Installation limits belong in the ignored `.env`, not in tracked files.
The Z-axis minimum and maximum define the available Z-only range.

## Running the server

On the directly connected control computer, run:

```bash
uv run python -m plesty.attodry2100xl_enspire_server --tcp-port 5555
```

The process binds ZeroMQ to all network interfaces. Stop it with `Ctrl+C`; the
shutdown path releases the ZeroMQ listener and closes the controller socket.
Run `--help` to see all command-line overrides. Keep the default dedicated-thread
mode. `--pool-threading` exists for diagnostics and is not recommended for normal
instrument operation.

For unattended operation, use the laboratory's approved Windows service wrapper
or task supervisor such as for example Windows Task Scheduler, WinSW, NSSM, or
another managed service system. Configure its working directory as this
repository, run the same `uv run python -m ...` command under a dedicated
account that can read the local `.env`, and send a normal termination before
forcing the process to stop. Do not configure two instances to use the same
controller or ZeroMQ port. Avoid an unbounded automatic restart loop: after an
unexpected exit, an operator must check the controller's current command,
targets, and measured state before the server is returned to service. After a
restart, perform read-only health queries before allowing state-changing calls.

Dedicated account explanation: Use a dedicated Windows user account created
specifically for this service—not a personal or administrator account. It should
have only the permissions it needs: reading the repository and .env, executing
the application, accessing the controller network, and listening on the
configured ZeroMQ port.

## Client examples

Install `plesty-lib` on a laboratory client and connect to the control computer's
ZeroMQ port. Parameter keys are grouped with a dot:

```python
from plesty.lib.service import build_client

with build_client("tcp://<control-computer>:5555", timeout=10_000) as client:
    sane = client.query("system.is_cryostat_sane")
    sample_k = client.query("temperature.sample")
    pressure_mbar = client.query("pressure.cryo_in")
    field_t = client.query("magnet.channel_0_field")
    api = client.describe()
```

`describe()` returns the discoverable parameter and operation schema. The public
parameter groups are `identity`, `system`, `temperature`, `pressure`, and
`magnet`. Important writable parameters are:

```python
# These calls change hardware state. Use only with operator approval.
client.write("temperature.sample_setpoint", 4.0)
client.write("temperature.sample_ramp_rate", 1.0)
client.set_temperature_ramp_control("sample", True)
client.set_temperature_control("sample", True)
```

Sample and VTI setpoints accept 0 through 310 K. Ramp rates accept `0` (limit
disabled) or 0.1 through 100 K/min. Writes change stored settings; the named
operations start or stop control. Calls return after controller acknowledgement,
not after thermal equilibrium. A ramp from below 10 K to 310 K can take at least
three hours, so monitor the measured value, setpoint, and control-status
parameters asynchronously.

The magnet API is also explicit:

```python
# Installation limits from the server's .env are enforced before any write.
client.set_magnetic_field(z_t=0.0, y_t=0.0, x_t=0.0)
client.set_field_control(True)

# This blocks until every configured measured field is within zero tolerance.
client.magnet_off(timeout=7_300)
```

`set_magnetic_field` validates every axis and the full vector before changing
targets. It does not start field control, although changing a target while field
control is already active can start motion immediately. Starting field control
checks for quench, driven mode, and a safe stored target. Full-scale ramps can
take tens of minutes. The `magnet_off` client timeout is in seconds and must be
longer than `ENSPIRE_MAGNET_ZERO_TIMEOUT_S`; the `build_client` default timeout is
in milliseconds and is too short for this blocking operation.

Fields above the coupled limit require exact-zero X/Y targets and measured X/Y values within
`ENSPIRE_MAGNET_TRANSVERSE_ZERO_TOLERANCE_T`. Envelope changes are deliberately
non-blocking and must be staged. To enter high-Z operation, first set X/Y to zero
while Z remains in the coupled envelope, wait for transverse telemetry to settle,
then request high Z. To leave it, lower Z with X/Y still zero, wait until measured
field is inside the coupled envelope, then request a transverse target. Premature
crossings fail before any setpoint write.

The other named operation, `cancel_current_command(expected_command)`, cancels
only when the controller's current command exactly matches the supplied value.
This guard prevents a stale client from cancelling a different command.

## Security assumptions

The current Plesty ZeroMQ transport does not authenticate clients or encrypt
traffic, and this server exposes hardware-changing operations. Bind it only on a
trusted, access-controlled laboratory network. Use the host firewall to allow
`DEVICE_TCP_PORT` only from approved client addresses; never expose it to the
public internet or an untrusted wireless network. Keep the eNSPIRE network itself
isolated so only the control computer can reach `DEVICE_PORT`.

Do not put credentials, controller addresses, serial numbers, installation field
limits, or measured laboratory values in commits or shared logs. The server does
not expose controller network configuration, firmware updates, calibration,
pumps, valves, shutdown, quench reset, service termination, or an unrestricted
raw JSON-RPC method.

## Validation

Normal validation uses a local fake TCP controller and an in-memory eNSPIRE
simulator, and does not touch hardware:

```bash
uv run pytest
uv run plesty check
```

The module is held to the `quantum` standard, so `plesty check` runs the full gate
set locally, including the eight-gate device mock pipeline.

`host="mock"` (or `--host mock`) replaces the TCP client with the simulator in
`mock_controller.py`, so the whole stack — read specs, decoders, device-error
validation, state changes — runs with no cryostat and nothing patched out. The
simulated controller is idealised: a control loop settles on its target at once.
It therefore says nothing about ramp behaviour, controller result layouts, or
transport pathologies on the real instrument.

The public parameter surface is declared as data in
`plesty/attodry2100xl_enspire_server/schema_param.json` and shipped in the wheel;
the vendor method behind each key stays in `device.READ_SPECS` / `WRITE_SPECS`,
and a test asserts the two never drift apart.

On Windows consoles that cannot encode the compliance command's Unicode status
symbols, run the equivalent check in UTF-8 mode:

```powershell
uv run python -X utf8 -c "from plesty.sdk.cli import app; app()" check
```

The real-controller pytest remains skipped unless
`ENSPIRE_RUN_HARDWARE_TESTS=1` is explicitly present. That opt-in test is
read-only; do not use live state-changing calls as a generic release check.

## Troubleshooting

- **Startup says `DEVICE_HOST` or `DEVICE_PORT` is missing:** create `.env` in
  the repository root or provide `--host` and `--port`. Check spelling and the
  process account's working directory.
- **Startup reports invalid magnet safety configuration:** define channel,
  minimum, and maximum together for every installed axis plus
  `ENSPIRE_MAGNET_COUPLED_VECTOR_MAX_T` and
  `ENSPIRE_MAGNET_TRANSVERSE_ZERO_TOLERANCE_T`, or blank all magnet settings to
  disable writes.
- **Controller connection fails:** verify the eNSPIRE Web GUI from the control
  computer, then verify that its JSON-RPC port is reachable. Do not substitute
  the ZeroMQ port for the vendor port.
- **A remote client cannot connect:** confirm the server is running, the client
  uses the control computer rather than the eNSPIRE address, the ZeroMQ port
  matches `DEVICE_TCP_PORT`, and the host firewall permits that client.
- **A request times out:** ordinary reads should finish quickly. For long ramps,
  setters return immediately and clients should poll telemetry. Only
  `magnet_off` waits for a physical target; give it a timeout longer than the
  configured zero timeout and continue monitoring if it reports a timeout.
- **Field control is rejected:** inspect quench state, driven mode, configured
  axes, stored targets, per-axis bounds, and vector magnitude. Do not weaken the
  local limits to bypass a controller condition.
- **A ramp is slower than expected:** read `read_ramp_rates(channel)`. The rate
  is not one number per magnet — it is a table of current zones, and the rate
  changes abruptly between them. The bounds are in amperes and the rates in
  A/s; converting to tesla needs the magnet's amps-per-tesla, which this API
  does not supply anywhere, so it has to come from the magnet's specification
  or from timing a known ramp. `set_ramp_rate` will not exceed a zone's factory
  rate and will not move a zone boundary.
- **The controller drops or returns malformed JSON-RPC:** the server closes the
  vendor socket and reports the failure, then reopens it on the next request —
  a stalled reply costs one call, not the session. Never blindly retry a
  state-changing call whose delivery is uncertain: the failed one may already
  have executed.
- **Replies stall while a ramp starts:** a control computer that does not answer
  within `ENSPIRE_REQUEST_TIMEOUT_S` (30 s by default) drops the socket, because
  a late reply would desync every later request. Raise that budget for an
  installation that stalls longer; it bounds one round trip, never a ramp.
- **The ZeroMQ port is already in use:** stop the previous server cleanly or
  choose one approved alternate port and update clients. Never run competing
  instances against the same controller.

## Scope

This first release supports enumerated telemetry, sample/VTI temperature
settings and controls, guarded current-command cancellation, installation-bounded
field targets and control, and verified magnet-off. Go-to-base and sample
exchange are intentionally absent. Refer to the generated Plesty parameter and
function pages for the complete machine-readable API.
