Skip to content

Commit bfbac98

Browse files
committed
feat: add callback functionality for SubscribeConnection
1 parent f32425a commit bfbac98

File tree

4 files changed

+661
-31
lines changed

4 files changed

+661
-31
lines changed

README.md

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

3-
This is also availabe as a [pip package on pypi](https://pypi.org/project/dsf-python/)
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.
46

5-
Find out more about [Duet Software Framework](https://github.com/Duet3D/DuetSoftwareFramework).
7+
This project is also published on [PyPI](https://pypi.org/project/dsf-python/).
68

7-
Examples of the [Duet Software Framework Python Bindings](https://github.com/Duet3D/dsf-python/tree/main/examples).
9+
Useful links:
810

9-
Get in touch with the community at [Duet Software Framework Forum](https://forum.duet3d.com/category/31/dsf-development) for bug reports, discussion and any kind of exchange.
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
1024

1125
## Installation
12-
This package contains a `setup.py` so it can be installed with `python3 setup.py install`.
1326

14-
## Usage
15-
See included `examples/` folder for various use cases.
27+
The package can be installed from source:
28+
29+
```bash
30+
python3 setup.py install
31+
```
32+
33+
Or with `pip`:
34+
35+
```bash
36+
python3 -m pip install dsf-python
37+
```
38+
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.
41+
42+
## Quick Start
43+
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+
49+
```python
50+
from dsf.connections import CommandConnection
51+
52+
connection = CommandConnection()
53+
connection.connect()
54+
55+
response = connection.perform_simple_code("M115")
56+
print(response.result)
57+
58+
connection.close()
59+
```
60+
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)
72+
73+
connection.close()
74+
```
75+
76+
### Subscribe To Object Model Updates
1677

17-
For patch subscriptions, `BaseConnection.has_data_available()` can be used to poll a
18-
subscription socket without blocking on `receive_json()` or `get_object_model_patch()`.
19-
This is useful when object model updates arrive less frequently than another data source.
78+
Use `SubscribeConnection` when you want DSF to push object model updates over a
79+
subscription socket.
2080

2181
```python
2282
from dsf.connections import SubscribeConnection, SubscriptionMode
2383

2484
subscription = SubscribeConnection(SubscriptionMode.PATCH)
2585
subscription.connect()
86+
2687
object_model = subscription.get_object_model()
2788

2889
while True:
29-
if subscription.has_data_available():
30-
object_model.update_from_json(subscription.get_object_model_patch())
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()`.
122+
123+
Key paths use dot notation and can include list indexes:
124+
125+
- `state.upTime`
126+
- `heat.heaters.0.current`
127+
- `move.axes.2.userPosition`
128+
129+
Use `^` in a list position to match any changed index:
130+
131+
- `heat.heaters.^.current`
132+
- `tools.^.state`
133+
134+
Callbacks always receive keyword arguments:
135+
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 `^`
139+
140+
```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+
)
156+
157+
while True:
158+
object_model = subscription.get_object_model()
159+
```
160+
161+
Wildcard example:
162+
163+
```python
164+
def handle_any_heater(*, key, data, indices):
165+
print(key, indices, data)
166+
167+
168+
subscription.subscribe_to_keys(
169+
["heat.heaters.^.current"],
170+
handle_any_heater,
171+
)
172+
```
173+
174+
### InterceptConnection
175+
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.
178+
179+
Typical use cases include:
180+
181+
- implementing custom M-codes
182+
- inspecting or rewriting commands
183+
- reacting to executed code notifications
184+
185+
See `examples/custom_m_codes.py` for a complete example.
186+
187+
## Object Model
188+
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+
```
200+
201+
Object model instances support JSON-driven updates:
202+
203+
```python
204+
object_model.update_from_json({"state": {"upTime": 1234}})
205+
print(object_model.state.up_time)
31206
```
207+
208+
This is the mechanism used internally when patch subscriptions are applied.
209+
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_simple_code.py`: send G-code over a command connection
240+
- `subscribe_object_model.py`: subscribe to object model updates
241+
- `custom_m_codes.py`: intercept and implement custom M-codes
242+
- `custom_http_endpoint.py`: serve a custom HTTP endpoint through DSF
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.

0 commit comments

Comments
 (0)