Docs / docs (push) Successful in 37s
Playwright Tests / test-playwright (1, 2) (push) Failing after 1m35s
Playwright Tests / test-playwright (2, 2) (push) Failing after 17s
pre-commit / pre-commit (push) Failing after 2m8s
Test Backend / test-backend (push) Failing after 2m48s
Compose Smoke Test / test-compose (push) Failing after 13s
Playwright Tests / merge-reports (push) Failing after 2m25s
The site read as a design journal: rationale paragraphs, hedges
("deliberately", "on purpose", "genuinely"), meta-commentary about the docs
themselves, and one em-dash every ten lines carrying an aside.
Roughly twenty rationale blocks are gone or reduced to what a reader needs
in order to use the thing. Em-dashes go from 507 to 135, and what is left is
structural rather than prose: list and definition separators, table cells,
and four inside code blocks that quote what the CLI actually prints.
Also: api.example.com becomes api.fluksio.com (the emails stay, since
bootstrap.py really defaults to admin@example.com and RFC 2606 reserves it);
the mqtt table gains the two settings it had drifted behind on and inject's
wording matches the engine; llms.txt lists the two connector pages that were
in the nav but not in it; and the two device/device_policy notes now agree.
Builds clean under `zensical build --strict`.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015YrQnKV3bnQd4K342y8tKj
146 lines
5.0 KiB
Markdown
146 lines
5.0 KiB
Markdown
# Writing a connector
|
|
|
|
A walkthrough from an empty directory to a node on the canvas. The normative
|
|
surface is in [the connector contract](../reference/connector-contract.md);
|
|
this is how to use it.
|
|
|
|
## Start from the skeleton
|
|
|
|
A working connector, kept as small as a correct one can be, lives at
|
|
`skeleton/` in the [connectors repository](https://git.stroblme.de/Fluksio/connectors).
|
|
Copy it:
|
|
|
|
```bash
|
|
git clone https://git.stroblme.de/Fluksio/connectors.git
|
|
cp -r connectors/skeleton fluksio-connector-mydevice
|
|
cd fluksio-connector-mydevice
|
|
```
|
|
|
|
Rename the package (`src/fluksio_connector_skeleton/` and the `[project]` name
|
|
in `pyproject.toml`), then point the entry point at your class:
|
|
|
|
```toml
|
|
[project.entry-points."fluksio.node_types"]
|
|
my_device = "fluksio_connector_mydevice:MyDevice"
|
|
```
|
|
|
|
## Install it into the engine
|
|
|
|
Connectors are found through installed package metadata, so the engine has to
|
|
have yours installed. During development, install it as an editable path
|
|
dependency of the backend:
|
|
|
|
```bash
|
|
cd app/backend
|
|
uv add --editable ../../fluksio-connector-mydevice
|
|
```
|
|
|
|
Editing your connector's **code** then takes effect on the next engine restart,
|
|
with no reinstall. Changing its **metadata** (the entry point, the version,
|
|
the package name) needs `uv sync` to regenerate the installed metadata before
|
|
a restart picks it up.
|
|
|
|
Two things to know about this:
|
|
|
|
- `uv add` writes the path dependency into `backend/pyproject.toml` and
|
|
`uv.lock`. That path does not exist inside the backend's Docker image, so
|
|
**revert both files before committing**; the dependency belongs on your
|
|
machine, not in the repo. Distribution is a later milestone.
|
|
- For the same reason, develop against the **standalone backend** rather than
|
|
the Docker stack:
|
|
|
|
```bash
|
|
cd app && make dev-backend # FastAPI on :8000
|
|
make dev-frontend # Vite on :5173
|
|
```
|
|
|
|
Restart the backend and your node type appears in the add-node palette (⌘P),
|
|
labelled with the package it came from.
|
|
|
|
## Write the node
|
|
|
|
```python
|
|
from typing import Any
|
|
|
|
from pydantic import Field
|
|
from fluksio.flow.connector import CONTRACT_VERSION, ConnectorNode
|
|
|
|
|
|
class MyDevice(ConnectorNode):
|
|
contract = CONTRACT_VERSION
|
|
title = "My device"
|
|
description = "Reads temperature and humidity over the device's HTTP API."
|
|
|
|
class Params(ConnectorNode.Params):
|
|
host: str = "192.168.1.50"
|
|
api_key: str | None = Field(
|
|
default=None, json_schema_extra={"x-secret": True}
|
|
)
|
|
|
|
async def poll(self) -> dict[str, Any] | None:
|
|
reading = await self._read()
|
|
return {"temperature": reading.temp, "humidity": reading.humidity}
|
|
```
|
|
|
|
Then, on the canvas: place the node, set `poll_interval` to how often the
|
|
device should be read, pick the API key from the secrets store, and add output
|
|
ports named `temperature` and `humidity`. The port names are the wiring: any
|
|
node consuming those messages is downstream of this one.
|
|
|
|
## If the device is told rather than asked
|
|
|
|
A connector that commands something implements `write()` instead of, or
|
|
alongside, `poll()`. Input ports carry the values in:
|
|
|
|
```python
|
|
def write(self, **ports: Any) -> None:
|
|
self._device.set(ports["level"])
|
|
```
|
|
|
|
Place the node with *input* ports named `level`, and whatever publishes that
|
|
message drives the device. Set `idempotent = False` on the class: a write is a
|
|
command, and a redelivery after a crash should not undo a newer one.
|
|
|
|
Keep the first version off the wire. A boolean setting the code checks before
|
|
it sends (`artnet`'s `transmit` is the example) lets a flow be built and
|
|
watched in the logs before anything physically moves.
|
|
|
|
## If the reading is bytes
|
|
|
|
A camera or a microphone publishes a reference rather than the bytes:
|
|
|
|
```python
|
|
async def poll(self):
|
|
jpeg = await asyncio.to_thread(self._grab)
|
|
return {"frame": self.save_artifact(jpeg, "f.jpg", media_type="image/jpeg")}
|
|
```
|
|
|
|
Place the node with an `image`-typed output port and a Media widget draws each
|
|
frame as it lands. `fluksio-connector-test-media` publishes test frames and
|
|
tones this way, so the whole path can be wired up with no camera in the room.
|
|
Copy it if yours is a media device.
|
|
|
|
## Try it without a device
|
|
|
|
Give `poll` something predictable first and confirm the values reach the canvas
|
|
before pointing it at hardware:
|
|
|
|
```python
|
|
async def poll(self):
|
|
return {"temperature": 21.5}
|
|
```
|
|
|
|
Press Run, or wait for the poll interval, and the value appears on the edge.
|
|
Anything the connector prints shows up in the logs panel in the dock, and a
|
|
raised exception shows there with its traceback and marks the node down.
|
|
|
|
## Checklist before sharing it
|
|
|
|
- `contract = CONTRACT_VERSION` declared on the class itself.
|
|
- Credentials marked `x-secret`, never read from the environment.
|
|
- `stop()` is safe to call twice, and after a failed `start()`.
|
|
- Blocking I/O wrapped in `asyncio.to_thread`.
|
|
- `poll()` returns `None` rather than raising when there is simply nothing new.
|
|
- `write()` returns quickly; the thread it runs on is the scheduler's.
|
|
- Health reported when the connection comes and goes.
|