Metadata-Version: 2.2
Name: universal-robots-clients
Version: 0.4.0
Summary: Focused Python clients for Universal Robots controllers and program assets
Author: CraigBuilds
License: MIT License
        
        Copyright (c) 2026 CraigBuilds
        
        Permission is hereby granted, free of charge, to any person obtaining a copy
        of this software and associated documentation files (the "Software"), to deal
        in the Software without restriction, including without limitation the rights
        to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
        copies of the Software, and to permit persons to whom the Software is
        furnished to do so, subject to the following conditions:
        
        The above copyright notice and this permission notice shall be included in all
        copies or substantial portions of the Software.
        
        THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
        IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
        FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
        AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
        LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
        OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
        SOFTWARE.
        
Project-URL: Changelog, https://github.com/CraigBuilds/universal-robots-clients/blob/main/CHANGELOG.md
Project-URL: Documentation, https://github.com/CraigBuilds/universal-robots-clients#readme
Project-URL: Issues, https://github.com/CraigBuilds/universal-robots-clients/issues
Project-URL: Repository, https://github.com/CraigBuilds/universal-robots-clients
Keywords: dashboard-server,industrial,robotics,rtde,universal-robots
Classifier: Development Status :: 3 - Alpha
Classifier: Intended Audience :: Developers
Classifier: Intended Audience :: Manufacturing
Classifier: License :: OSI Approved :: MIT License
Classifier: Operating System :: OS Independent
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.8
Classifier: Programming Language :: Python :: 3.9
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Topic :: Scientific/Engineering
Classifier: Topic :: Software Development :: Libraries :: Python Modules
Classifier: Typing :: Typed
Requires-Python: >=3.8.3
Description-Content-Type: text/markdown
License-File: LICENSE
Provides-Extra: sftp
Requires-Dist: paramiko<4,>=3.5; extra == "sftp"
Provides-Extra: rtde
Requires-Dist: ur-rtde<2,>=1.6.3; extra == "rtde"
Provides-Extra: test
Requires-Dist: pytest<9,>=8; extra == "test"
Provides-Extra: system-test
Requires-Dist: docker<8,>=7; python_version >= "3.10" and extra == "system-test"
Requires-Dist: paramiko<4,>=3.5; python_version >= "3.10" and extra == "system-test"
Requires-Dist: pytest<9,>=8; python_version >= "3.10" and extra == "system-test"
Requires-Dist: testcontainers==4.14.2; python_version >= "3.10" and extra == "system-test"
Requires-Dist: ur-rtde<2,>=1.6.3; python_version >= "3.10" and extra == "system-test"
Provides-Extra: all
Requires-Dist: paramiko<4,>=3.5; extra == "all"
Requires-Dist: ur-rtde<2,>=1.6.3; extra == "all"
Provides-Extra: dev
Requires-Dist: black<25,>=24; extra == "dev"
Requires-Dist: build<2,>=1.2; extra == "dev"
Requires-Dist: mypy==1.14.1; extra == "dev"
Requires-Dist: paramiko<4,>=3.5; extra == "dev"
Requires-Dist: pytest<9,>=8; extra == "dev"
Requires-Dist: twine<7,>=5; extra == "dev"
Requires-Dist: types-paramiko==3.5.0.20240928; extra == "dev"
Requires-Dist: ur-rtde<2,>=1.6.3; extra == "dev"

# universal-robots-clients

`universal-robots-clients` provides small, functional Python clients for Universal Robots controller protocols and URP program catalogues. Each capability is an
explicit module, so call sites retain context and applications install only the optional protocol dependencies they use.

## Installation

```bash
python -m pip install --upgrade pip
python -m pip install universal-robots-clients
```

Install optional SFTP or RTDE support:

```bash
python -m pip install "universal-robots-clients[sftp]"
python -m pip install "universal-robots-clients[rtde]"
python -m pip install "universal-robots-clients[all]"
```

Python 3.8.3 and later are supported. Upgrade the old pip bundled with Python 3.8.3 before installing optional binary dependencies.

## Modules

| Module                       | Responsibility                                            | Optional dependency |
| ---------------------------- | --------------------------------------------------------- | ------------------- |
| `dashboard_client`           | Send Dashboard Server commands and program operations     | None                |
| `urp_discovery_client`       | Select local or SFTP discovery from runtime configuration | SFTP when selected  |
| `urp_discovery_local_client` | Discover URP files through a local filesystem             | None                |
| `urp_discovery_sftp_client`  | Discover URP files through caller-owned or managed SFTP   | Paramiko            |
| `rtde_client`                | Read telemetry, control tool I/O, and exchange registers  | ur-rtde             |

The package root deliberately re-exports no operations. Importing capability modules keeps ownership visible:

```python
import universal_robots_clients.dashboard_client as dashboard_client

response = dashboard_client.load_and_play_program("192.0.2.10", "Production/PickPart.urp")
```

### Dashboard client

Every Dashboard command uses one short-lived TCP connection. Responses are returned as stripped protocol text because Dashboard success and failure formats vary
by command.

```python
import universal_robots_clients.dashboard_client as dashboard_client

dashboard_client.load_program("192.0.2.10", "Main.urp")
dashboard_client.play_program("192.0.2.10")
state = dashboard_client.get_program_state("192.0.2.10")
dashboard_client.pause_program("192.0.2.10")
dashboard_client.stop_program("192.0.2.10")
```

`send_command()` remains available for Dashboard operations that do not yet have a named helper.

### URP discovery clients

Use the selector when configuration chooses the backend at runtime:

```python
import universal_robots_clients.urp_discovery_client as urp_discovery_client

programs = urp_discovery_client.discover_programs("local", "/programs")
```

Use a backend directly when it is already known:

```python
import universal_robots_clients.urp_discovery_local_client as urp_discovery_local_client
import universal_robots_clients.urp_discovery_sftp_client as urp_discovery_sftp_client

local_programs = urp_discovery_local_client.discover_programs("/programs")
sftp_programs = urp_discovery_sftp_client.connect_and_discover_programs(
    host="192.0.2.10",
    root="/programs",
    username="root",
    password="secret",
)
```

Both backends recursively find case-insensitive `.urp` files, return paths relative to the configured root, normalize separators to `/`, and sort the result.
Advanced callers can pass an existing connected SFTP client to `urp_discovery_sftp_client.discover_programs()`.

Use `catalog()` when the same configured source must both list programs and generate one start operation per program:

```python
programs = urp_discovery_client.catalog(
    "local",
    "/programs",
    start_program=lambda program: f"Started {program}",
)

paths = programs.discover()
start_methods = programs.methods()
```

The catalogue owns flat `StartProgram_...` naming and rejects collisions. Its explicit `start_program` action defines what invoking a generated method does.

### RTDE client

The RTDE module wraps the optional `ur-rtde` receive and I/O interfaces behind a functional API. A `Client` data class owns the persistent resources; module
functions own all behavior.

```python
import universal_robots_clients.rtde_client as rtde_client

client = rtde_client.connect("192.0.2.10")

try:
    pose = rtde_client.read_actual_tcp_pose(client)
    joints = rtde_client.read_actual_joint_positions(client)
    effective_speed = rtde_client.read_speed_scaling(client)

    # ur-rtde represents the speed slider as a fraction: 0.5 means 50%.
    rtde_client.write_speed_slider_fraction(0.5, client=client)

    # Tool inputs/outputs 0 and 1 can carry gripper-specific feedback and commands.
    gripper_feedback = rtde_client.read_tool_digital_input(0, client=client)
    rtde_client.write_tool_digital_output(0, True, client=client)

    rtde_client.write_input_int_register(42, 7, client=client)
    rtde_client.write_input_double_register(43, 1.25, client=client)
    counter = rtde_client.read_output_int_register(42, client=client)
    result = rtde_client.read_output_double_register(43, client=client)
finally:
    rtde_client.disconnect(client)
```

Small process-lifetime adapters can configure one default endpoint. `configure()` records the connection configuration without connecting; the first operation
opens the session and later operations reuse it:

```python
rtde_client.configure("192.0.2.10", frequency=20.0)
pose = rtde_client.read_actual_tcp_pose()
rtde_client.write_speed_slider_percent(50.0)
```

Lazy clients are closed at process exit. Embedded applications can call `disconnect()` or `close_lazy_clients()` earlier. `lazy()` remains available to
configure additional endpoints, which can be supplied explicitly with `client=`.

Upper registers 42 through 46 are selected by default for external RTDE clients. Pass `use_upper_range_registers=False` to select registers 18 through 22.
Register allocation, invocation handshakes, and robot-side program conventions remain application policy.

TCP pose is returned as `[x, y, z, rx, ry, rz]`, with translation in metres and the rotation vector in radians. Joint positions are radians, and all vector
results are ordinary `list[float]` values. Tool digital channels are deliberately generic: a gripper's open/closed, object-detected, or fault meaning depends on
how that gripper is wired and configured. The read functions expose actual I/O state; write functions request output state and raise if RTDE rejects the write.

## Security

- Dashboard and RTDE connections are unencrypted controller protocols and should be used on a controlled robot network.
- SFTP loads system host keys and rejects unknown hosts by default.
- `trust_unknown_host_keys=True` is an explicit opt-in intended only for controlled environments.
- Applications own credential storage, authorization, retry, and command-success policy.

## Development

From this package directory:

```bash
python -m pip install -e ".[dev]"
python -m pytest -m "not system"
python -m mypy
python -m build
python -m twine check dist/*
```

Unit tests do not require a robot or Docker. They cover protocol framing, validation, lifecycle failures, typed conversion, local filesystem traversal, SFTP
traversal, backend selection, and connection configuration with deterministic fakes.

### Real system tests

The package owns its protocol-level integration tests as well as its unit tests. On Linux `amd64`, with Docker running:

```bash
python -m pip install -e ".[system-test]"
python -m pytest -m system tests/system
```

This starts the pinned official `universalrobots/ursim_e-series:5.25.2` image and really loads, plays, pauses, and stops no-motion URP programs through the
Dashboard Server. A separate RTDE contract opens the native receive and I/O connections and exercises telemetry, speed-slider control, tool I/O, and typed
registers. The discovery contract compares the same catalogue through the local filesystem client and a disposable Debian OpenSSH/SFTP container. CI runs all
three contracts; the gateway repository keeps only the higher-level cross-package OPC UA workflow.

Release history is recorded in the [changelog](https://github.com/CraigBuilds/universal-robots-clients/blob/main/CHANGELOG.md). The gateway integration and real
URSim contract are validated independently in [`ur_dashboard_to_opcua_gateway`](https://github.com/CraigBuilds/ur_dashboard_to_opcua_gateway).

## License

This project is licensed under the MIT License. See [LICENSE](LICENSE).
