Skip to content

Commit aff9e38

Browse files
committed
docs: update readme
1 parent d678bd9 commit aff9e38

6 files changed

Lines changed: 369 additions & 357 deletions

File tree

‎README.md‎

Lines changed: 133 additions & 202 deletions
Original file line numberDiff line numberDiff line change
@@ -1,261 +1,192 @@
11
# Duet Software Framework Python Bindings
22

3-
`dsf-python` provides Python bindings for the Duet Software Framework control server.
4-
It exposes the DSF socket protocol as Python connection classes, typed object model
5-
classes, command builders, and helpers for custom HTTP endpoints.
3+
`dsf-python` is a Python client for the [Duet Software Framework](https://github.com/Duet3D/DuetSoftwareFramework)
4+
(DSF) control server. It talks to DSF over its UNIX socket and provides:
65

7-
This project is also published on [PyPI](https://pypi.org/project/dsf-python/).
6+
- `dsf.connections`: connections for sending commands, subscribing to the object model and intercepting codes
7+
- `dsf.object_model`: the DSF object model as typed Python classes
8+
- `dsf.http`: custom HTTP and WebSocket endpoints served through DSF
9+
- `dsf.commands`: low-level builders for every DSF command
810

9-
Useful links:
11+
It follows the C# `DuetAPIClient` closely, but uses Python naming. See
12+
[Differences from the DSF API](#differences-from-the-dsf-api) before porting C# code or reading the DSF docs.
1013

11-
- [Duet Software Framework](https://github.com/Duet3D/DuetSoftwareFramework)
12-
- [dsf-python examples](https://github.com/Duet3D/dsf-python/tree/main/examples)
13-
- [Duet Software Framework forum](https://forum.duet3d.com/category/31/dsf-development)
14-
15-
## What The Library Contains
16-
17-
The top-level package is organised around the main DSF workflows.
18-
19-
- `dsf.connections`: socket-based client connections for commands, subscriptions, and code interception
20-
- `dsf.commands`: request payload builders for low-level DSF commands
21-
- `dsf.object_model`: the typed DSF object model and related enums
22-
- `dsf.http`: helpers for custom HTTP and WebSocket endpoints exposed through DSF
23-
- `dsf.exceptions`: shared exception types raised by connection classes
14+
- [Examples](https://github.com/Duet3D/dsf-python/tree/HEAD/examples)
15+
- [PyPI package](https://pypi.org/project/dsf-python/)
16+
- [DSF forum](https://forum.duet3d.com/category/31/dsf-development)
2417

2518
## Installation
2619

27-
The package can be installed from source:
28-
29-
```bash
30-
python3 setup.py install
31-
```
32-
33-
Or with `pip`:
34-
3520
```bash
3621
python3 -m pip install dsf-python
3722
```
3823

39-
Most code using this library must run on a system with Duet Software Framework
40-
installed and with permission to access the DSF UNIX socket.
24+
Python 3.11 or newer is required. Your script must run on the machine that runs DSF, as a user
25+
that can access the DSF socket (usually the `dsf` user). The socket path is read from
26+
`/opt/dsf/conf/config.json` and falls back to `/run/dsf/dcs.sock`.
4127

4228
## Quick Start
4329

44-
### Run A Simple Command
45-
46-
Use `CommandConnection` to send general-purpose commands such as G-code or to
47-
request the full object model on demand.
48-
4930
```python
5031
from dsf.connections import CommandConnection
5132

5233
connection = CommandConnection()
5334
connection.connect()
54-
55-
response = connection.perform_simple_code("M115")
56-
print(response.result)
57-
58-
connection.close()
35+
try:
36+
print(connection.perform_simple_code("M115"))
37+
38+
object_model = connection.get_object_model()
39+
print(object_model.state.status.value)
40+
print(object_model.state.up_time) # "upTime" in DSF
41+
finally:
42+
connection.close()
5943
```
6044

61-
### Read The Object Model Once
62-
63-
```python
64-
from dsf.connections import CommandConnection
65-
66-
connection = CommandConnection()
67-
connection.connect()
68-
69-
object_model = connection.get_object_model()
70-
print(object_model.state.status)
71-
print(object_model.move.axes[0].letter)
45+
## Connections
7246

73-
connection.close()
74-
```
47+
| Connection | Use it to | Example |
48+
|---|---|---|
49+
| `CommandConnection` | Run codes, evaluate expressions, read the object model, manage plugins, files and HTTP endpoints | [send_commands.py](https://github.com/Duet3D/dsf-python/blob/HEAD/examples/send_commands.py) |
50+
| `SubscribeConnection` | Keep a local copy of the object model up to date and react to changes | [subscribe_object_model.py](https://github.com/Duet3D/dsf-python/blob/HEAD/examples/subscribe_object_model.py) |
51+
| `InterceptConnection` | Handle custom M-codes, or inspect codes before or after DSF processes them | [custom_m_codes.py](https://github.com/Duet3D/dsf-python/blob/HEAD/examples/custom_m_codes.py) |
7552

76-
### Subscribe To Object Model Updates
53+
Every connection is configured in its constructor, opened with `connect()` and closed with `close()`.
54+
Errors reported by DSF are raised as `InternalServerException` (with `error_type` and `error_message`),
55+
or as `TaskCanceledException` if DSF cancelled the request.
7756

78-
Use `SubscribeConnection` when you want DSF to push object model updates over a
79-
subscription socket.
57+
### SubscribeConnection
8058

8159
```python
8260
from dsf.connections import SubscribeConnection, SubscriptionMode
8361

84-
subscription = SubscribeConnection(SubscriptionMode.PATCH)
62+
subscription = SubscribeConnection(SubscriptionMode.PATCH, filter_list=["state/status", "heat/heaters[*]/current"])
8563
subscription.connect()
8664

87-
object_model = subscription.get_object_model()
88-
89-
while True:
90-
object_model = subscription.get_object_model()
91-
```
92-
93-
In `SubscriptionMode.PATCH`, the first `get_object_model()` call reads the full
94-
model. Later calls reuse an internal cached model and apply one queued patch if
95-
available.
96-
97-
## Connections
98-
99-
### CommandConnection
100-
101-
`CommandConnection` is the general-purpose connection type. It is used for:
102-
103-
- sending simple G-code
104-
- requesting the full object model
105-
- working with files, packages, plugins, and user sessions through DSF commands
106-
- issuing lower-level requests through the command helpers in `dsf.commands`
107-
108-
Choose this connection when you want request-response behaviour and do not need
109-
streamed updates.
110-
111-
### SubscribeConnection
112-
113-
`SubscribeConnection` receives streamed object model updates from DSF. It supports:
114-
115-
- `SubscriptionMode.FULL`: every update is a full object model
116-
- `SubscriptionMode.PATCH`: every update is a partial JSON fragment
117-
118-
#### Key-Based Callbacks
119-
120-
`SubscribeConnection.subscribe_to_keys()` can run callbacks synchronously when
121-
selected object model paths appear in a patch update processed by `get_object_model()`.
65+
object_model = subscription.get_object_model() # First call: blocks for the full object model
12266

123-
Key paths use dot notation and can include list indexes:
67+
def on_heater_changed(*, key, data, indices):
68+
print(f"Heater {indices[0]} is now at {data}C")
12469

125-
- `state.upTime`
126-
- `heat.heaters.0.current`
127-
- `move.axes.2.userPosition`
70+
unsubscribe = subscription.subscribe_to_keys(["heat.heaters.^.current"], on_heater_changed)
12871

129-
Use `^` in a list position to match any changed index:
130-
131-
- `heat.heaters.^.current`
132-
- `tools.^.state`
72+
while True:
73+
object_model = subscription.get_object_model() # Later calls: apply queued patches, run callbacks
74+
...
75+
```
13376

134-
Callbacks always receive keyword arguments:
77+
- In `SubscriptionMode.PATCH`, the first `get_object_model()` call blocks until the full model arrives.
78+
Later calls do not block: they apply every queued patch to the cached model and return it.
79+
- In `SubscriptionMode.FULL`, every call blocks until the next full object model arrives.
80+
- `subscribe_to_keys()` callbacks run synchronously inside `get_object_model()`, only in patch mode, and
81+
receive the keyword arguments `key`, `data` (the raw JSON value from the patch) and `indices` (the list
82+
indexes matched by `^`, or `None`). Register them after the first `get_object_model()` call.
83+
The return value removes the callback again.
13584

136-
- `key`: the subscribed key path that matched
137-
- `data`: the changed value found at that path
138-
- `indices`: a tuple of matched wildcard indexes, or `None` when the key does not use `^`
85+
### InterceptConnection
13986

14087
```python
141-
from dsf.connections import SubscribeConnection, SubscriptionMode
142-
143-
144-
def handle_change(*, key, data, indices):
145-
print(key, data, indices)
146-
147-
148-
subscription = SubscribeConnection(SubscriptionMode.PATCH)
149-
subscription.connect()
150-
subscription.get_object_model()
151-
152-
unsubscribe = subscription.subscribe_to_keys(
153-
["heat.heaters.0.current", "state.upTime"],
154-
handle_change,
155-
)
88+
from dsf.connections import InterceptConnection, InterceptionMode
89+
from dsf.object_model import MessageType
15690

91+
interceptor = InterceptConnection(InterceptionMode.PRE, filters=["M1234"])
92+
interceptor.connect()
15793
while True:
158-
object_model = subscription.get_object_model()
94+
code = interceptor.receive_code()
95+
name = code.parameter("S", "world").string_value
96+
interceptor.resolve_code(MessageType.Success, f"Hello {name}!")
15997
```
16098

161-
Wildcard example:
162-
163-
```python
164-
def handle_any_heater(*, key, data, indices):
165-
print(key, indices, data)
99+
Every intercepted code must be answered with `resolve_code()`, `ignore_code()` or `cancel_code()`.
100+
`InterceptConnection` also has all `CommandConnection` methods, so it can run other codes while a
101+
code is intercepted, e.g. `perform_simple_code("M117 hi", code.channel)`.
166102

103+
### Custom HTTP Endpoints
167104

168-
subscription.subscribe_to_keys(
169-
["heat.heaters.^.current"],
170-
handle_any_heater,
171-
)
105+
```python
106+
from dsf.http import HttpEndpointConnection, HttpResponseType
107+
from dsf.object_model import HttpEndpointType
108+
109+
async def hello(connection: HttpEndpointConnection) -> None:
110+
request = await connection.read_request()
111+
await connection.send_response(200, f"Hello {request.queries.get('name', 'world')}!", HttpResponseType.PlainText)
112+
113+
endpoint = command_connection.add_http_endpoint(HttpEndpointType.GET, "example", "hello")
114+
endpoint.set_endpoint_handler(hello) # Serves GET /machine/example/hello in a background thread
115+
...
116+
command_connection.remove_http_endpoint(HttpEndpointType.GET, "example", "hello")
117+
endpoint.close()
172118
```
173119

174-
### InterceptConnection
120+
See [custom_http_endpoint.py](https://github.com/Duet3D/dsf-python/blob/HEAD/examples/custom_http_endpoint.py).
175121

176-
`InterceptConnection` is used for custom code handling and code interception. Use it
177-
when a plugin needs to receive G/M/T-code events before or after the firmware handles them.
122+
## Differences From The DSF API
178123

179-
Typical use cases include:
124+
### Naming
180125

181-
- implementing custom M-codes
182-
- inspecting or rewriting commands
183-
- reacting to executed code notifications
126+
Python attributes and methods are `snake_case`. **Anything passed to DSF as a string stays in DSF's
127+
`camelCase` format**, because DSF interprets it and not this library.
184128

185-
See `examples/custom_m_codes.py` for a complete example.
129+
| | DSF / C# | dsf-python |
130+
|---|---|---|
131+
| Object model properties | `state.upTime`, `move.axes[0].userPosition` | `state.up_time`, `move.axes[0].user_position` |
132+
| Uppercase acronyms | `move.skew.tanXY` | `move.skew.tan_XY` (acronyms stay uppercase) |
133+
| Global variables | `global` | `globals` (`global` is a Python keyword) |
134+
| Names shadowing builtins (`type`, `id`, `min`, `max`, `format`, `license`) | `axis.max` | `axis.max` (unchanged) |
135+
| Methods | `PerformSimpleCodeAsync()` | `perform_simple_code()` |
136+
| Intercepted `Code` attributes | `MajorNumber`, `KeywordArgument` | `majorNumber`, `keywordArgument` (**camelCase, as in the JSON**) |
137+
| `CodeParameter` attributes | `StringValue`, `IsString` | `string_value`, `is_string` |
138+
| HTTP request fields | `SessionId`, `ContentType` | `session_id`, `content_type` |
139+
| Error responses | `errorType`, `errorMessage` | `error_type`, `error_message` |
140+
| Enum members | `MessageType.Success` | Not normalised: `MessageType.Success`, `MachineStatus.idle`, `SubscriptionMode.PATCH`, `HttpEndpointType.GET`. Values match the DSF JSON. |
186141

187-
## Object Model
142+
These strings keep DSF naming:
188143

189-
The `dsf.object_model` package mirrors the DSF object model in typed Python classes.
190-
It lets you work with structured properties instead of manually traversing raw JSON.
191-
192-
Examples:
193-
194-
```python
195-
print(object_model.boards[0].name)
196-
print(object_model.state.up_time)
197-
print(object_model.move.axes[0].letter)
198-
print(object_model.tools[0].state)
199-
```
144+
| Where | Format | Example |
145+
|---|---|---|
146+
| `SubscribeConnection(filter_list=...)` | `/`-separated, `[*]` for any list item, `**` for everything below | `"heat/heaters[*]/current"`, `"move/**"` |
147+
| `CommandConnection.get_object_model(filters)` | `.`-separated key paths | `["state", "move.axes"]` |
148+
| `SubscribeConnection.subscribe_to_keys(keys)` | `.`-separated, list indexes as numbers, `^` for any list item | `"heat.heaters.^.current"`, `"move.axes.2.userPosition"` |
149+
| `evaluate_expression()`, `query_object_model()`, G-code | RepRapFirmware expressions | `"state.upTime"`, `"move.axes[0].homed"` |
200150

201-
Object model instances support JSON-driven updates:
151+
`ObjectModel.update_from_json()` and `from_json()` take DSF JSON (`camelCase`), and `to_json()` writes it back:
202152

203153
```python
204154
object_model.update_from_json({"state": {"upTime": 1234}})
205-
print(object_model.state.up_time)
155+
object_model.state.up_time # 1234
156+
object_model.to_json() # '{..."state": {..."upTime": 1234...}...}'
206157
```
207158

208-
This is the mechanism used internally when patch subscriptions are applied.
159+
### Behaviour
160+
161+
- **Synchronous.** Connection methods block and have no `Async` suffix or `CancellationToken`.
162+
Only HTTP endpoint handlers are `async` coroutines.
163+
- **Configuration in the constructor.** Options passed to C# `ConnectAsync()` (mode, filters, channels,
164+
`verbose`, ...) are constructor arguments. `connect()` takes only an optional socket path.
165+
- **Patches are applied for you.** `SubscribeConnection.get_object_model()` keeps an internal model up
166+
to date in patch mode. In C#, you apply patches from `GetObjectModelPatchAsync()` yourself.
167+
`get_object_model_patch()` and `get_serialized_object_model()` still return raw JSON if you need it.
168+
- **Key callbacks** (`subscribe_to_keys()`) are specific to dsf-python.
169+
- **Return values.** `perform_simple_code()` returns the reply as a string. `evaluate_expression()`
170+
and the lower-level methods return a `Response` whose value is in `.result`.
171+
- **Interception.** `resolve_code()` takes a `MessageType` and an optional string, not a `Message`.
172+
There is no `rewrite_code()`. `flush()` takes a channel instead of flushing the intercepted code's channel.
173+
- **HTTP endpoints.** Handlers are registered with `set_endpoint_handler()` instead of an event.
174+
`add_http_endpoint()` returns an `HttpEndpointUnixSocket`, which must be closed after
175+
`remove_http_endpoint()`.
176+
177+
## Examples
178+
179+
| Example | Shows |
180+
|---|---|
181+
| [send_commands.py](https://github.com/Duet3D/dsf-python/blob/HEAD/examples/send_commands.py) | Run codes, evaluate expressions, read part of the object model, write messages, handle errors |
182+
| [subscribe_object_model.py](https://github.com/Duet3D/dsf-python/blob/HEAD/examples/subscribe_object_model.py) | Filtered patch subscription, key callbacks with and without `^` |
183+
| [custom_m_codes.py](https://github.com/Duet3D/dsf-python/blob/HEAD/examples/custom_m_codes.py) | Custom M-codes, reading parameters, running codes from an interceptor |
184+
| [custom_http_endpoint.py](https://github.com/Duet3D/dsf-python/blob/HEAD/examples/custom_http_endpoint.py) | GET and POST endpoints with plain text and JSON responses |
185+
186+
## Development
209187

210-
## Commands
211-
212-
The `dsf.commands` package contains low-level command builders for code execution,
213-
file access, plugins, packages, object model manipulation, and other DSF protocol
214-
requests.
215-
216-
Most users should start with the higher-level methods on `CommandConnection` or
217-
`BaseCommandConnection`. Reach for `dsf.commands` directly when you need explicit
218-
control over the payload sent to DSF.
219-
220-
## Custom HTTP Endpoints
221-
222-
The `dsf.http` module contains helpers for custom HTTP and WebSocket endpoints.
223-
This is useful when a DSF plugin needs to expose an HTTP route that is handled by
224-
Python code.
225-
226-
Key types include:
227-
228-
- `HttpEndpointUnixSocket`
229-
- `HttpEndpointConnection`
230-
- `ReceivedHttpRequest`
231-
- `HttpResponseType`
232-
233-
See `examples/custom_http_endpoint.py` for a practical example.
234-
235-
## Included Examples
236-
237-
The `examples/` directory demonstrates the main workflows supported by the library.
238-
239-
- `send_commands.py`: run G-codes, evaluate expressions, read the object model, write messages and handle errors with a `CommandConnection`
240-
- `subscribe_object_model.py`: keep a filtered object model up-to-date and react to changes with key callbacks
241-
- `custom_m_codes.py`: implement custom M-codes with an `InterceptConnection`, read code parameters and run codes from the interceptor
242-
- `custom_http_endpoint.py`: serve custom GET and POST endpoints with plain text and JSON responses
243-
244-
## API Reference
245-
246-
The Sphinx docs expose the package API for the major public modules:
247-
248-
- `dsf`
249-
- `dsf.connections`
250-
- `dsf.commands`
251-
- `dsf.object_model`
252-
- `dsf.http`
253-
254-
## Development Notes
255-
256-
The library talks to DSF using the configured UNIX socket path. By default this is
257-
resolved from the DSF config and falls back to `/run/dsf/dcs.sock`.
258-
259-
If you are extending the library itself, the test suite under `tests/` contains
260-
mock socket servers and object model fixtures that are useful for validating new
261-
connection behaviour.
188+
```bash
189+
python3 -m pip install -e ".[dev]"
190+
python3 -m pytest # Tests use mock DSF sockets, no DSF install needed
191+
sphinx-build -b html docs/source docs/build # API reference
192+
```

0 commit comments

Comments
 (0)