Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
68 commits
Select commit Hold shift + click to select a range
692df45
feat: add NORT compile-time flag for non-real-time bus operation
nicolas-rabault Apr 10, 2026
1c67639
feat: add detect ACK mode mirroring for slave nodes
nicolas-rabault Apr 10, 2026
60773c9
feat: add Linux Luos HAL for Raspberry Pi
nicolas-rabault Apr 10, 2026
5256b84
feat: add non-real-time system support for Robus
nicolas-rabault Apr 10, 2026
ff842bb
feat: add Linux Robus HAL for Raspberry Pi RS485 bus participation
nicolas-rabault Apr 10, 2026
5147a87
perf(robus_hal/linux): replace tcdrain with precise wire-time sleep
nicolas-rabault Apr 22, 2026
8fba0f5
fix(robus_hal/linux): reset RX state between back-to-back frames
nicolas-rabault Apr 22, 2026
48bb1aa
feat: add conditional debug instrumentation for Robus protocol layers
nicolas-rabault Apr 10, 2026
cfebc51
feat: add RPI Gate example project using Robus network
nicolas-rabault Apr 23, 2026
1da4ab0
fix(robus_network): include robus_hal.h before port_manager.h
nicolas-rabault Apr 23, 2026
a29a7d9
style: apply clang-format to branch-modified files
nicolas-rabault Apr 23, 2026
d72265a
Merge pull request #502 from Luos-io/feat/RPI_robus
nicolas-rabault Apr 27, 2026
5a12116
feat(ws_network): add Ws_SetBroker for runtime broker override
nicolas-rabault Apr 24, 2026
b7b0cc6
Enable shared libs compilation by compiling directly the lib
nicolas-rabault Feb 1, 2024
e5ce007
add -fpic flag
nicolas-rabault Feb 2, 2024
a708105
feat(shared_lib): finalize native_lib env and expose cmult smoke test
nicolas-rabault Feb 12, 2024
c5ef5d2
fix(shared_lib): force-load archive members into shared library
nicolas-rabault Apr 23, 2026
7c35566
feat(python): scaffold luos_engine package skeleton
nicolas-rabault Apr 23, 2026
e03586e
feat(python): add bindings/python/.gitignore (force-add past root ign…
nicolas-rabault Apr 23, 2026
5286386
chore(python): drop redundant package-data stanza, ignore .venv dirs
nicolas-rabault Apr 23, 2026
6869983
feat(python): add dylib discovery with env var overrides
nicolas-rabault Apr 23, 2026
e9b6a27
test(python): ctypes smoke — dylib loads and cmult roundtrips
nicolas-rabault Apr 24, 2026
f60388e
feat(python): cffi API-mode extension with cmult smoke
nicolas-rabault Apr 24, 2026
b002ea4
fix(python): thread-safe dylib preload, tidy imports
nicolas-rabault Apr 24, 2026
cd4e1a2
feat(python): expose header_t/msg_t as opaque bytes + peek helpers
nicolas-rabault Apr 24, 2026
86e8aa1
refactor(python): use cffi opaque `...` bodies for header_t/msg_t
nicolas-rabault Apr 24, 2026
56aa774
docs(python): correct misleading comment on opaque struct access
nicolas-rabault Apr 24, 2026
bafcdeb
feat(python): manual header pack/unpack for 7-byte wire layout
nicolas-rabault Apr 24, 2026
6a06267
test(python): verify manual header pack matches C bitfield layout
nicolas-rabault Apr 24, 2026
1cd8e1e
test(python): assert zero on non-flipped fields in header layout test
nicolas-rabault Apr 24, 2026
973c7ea
feat(python): cdef for Luos_Init/Loop/CreateService/SendMsg and helpers
nicolas-rabault Apr 24, 2026
742a6af
docs(python): note why revision_t omits the unmap sibling
nicolas-rabault Apr 24, 2026
c4a99ed
feat(python): service registry with trampoline pinning
nicolas-rabault Apr 24, 2026
742a7d1
feat(python): Message view with lazy header unpack
nicolas-rabault Apr 24, 2026
03e1d89
feat(python): Service, create_service, engine lifecycle
nicolas-rabault Apr 24, 2026
87a117e
fix(python): drop dead NULL check, reset init state, lock lifecycle
nicolas-rabault Apr 24, 2026
11452aa
test(python): init/start/stop lifecycle
nicolas-rabault Apr 24, 2026
4d15eec
feat(python): cdef for routing table + accessor helpers
nicolas-rabault Apr 24, 2026
11be6c8
feat(python): routing module with RemoteService and find_peer
nicolas-rabault Apr 24, 2026
9464584
test(python): end-to-end two-service send/receive
nicolas-rabault Apr 24, 2026
6293449
feat(python): generated IntEnums for Cmd/Type/TargetMode
nicolas-rabault Apr 24, 2026
5b8fee7
feat(python): blinker example with two Python services
nicolas-rabault Apr 24, 2026
c68e1db
test(python): blinker end-to-end handler-level alternation
nicolas-rabault Apr 24, 2026
24ff246
docs(python): README with C↔Python porting guide
nicolas-rabault Apr 24, 2026
272b350
chore(python): post-acceptance cleanup (FIRST_USER_CMD const, dead co…
nicolas-rabault Apr 24, 2026
1520f30
build(native_lib): emit one dylib per network phy archive
nicolas-rabault Apr 24, 2026
bf9f3e2
feat(python): declare ws_network symbols in cffi cdef
nicolas-rabault Apr 24, 2026
43616a7
feat(python): add PHYS registry and LoadedPhy dataclass
nicolas-rabault Apr 24, 2026
14a6cfd
test(python): unit tests for get_phy_handle
nicolas-rabault Apr 24, 2026
2e7a166
feat(python): add luos_engine.phy descriptor module
nicolas-rabault Apr 24, 2026
05af399
feat(python): implement load_phy and tick phys in the engine loop
nicolas-rabault Apr 24, 2026
d5e25a8
test(python): add minimal ws relay broker for Phase 2 tests
nicolas-rabault Apr 24, 2026
5b09bff
test(python): add session-scoped broker fixture
nicolas-rabault Apr 24, 2026
e0b2669
test(python): add ws worker entrypoint for two-process tests
nicolas-rabault Apr 24, 2026
2a31cd5
test(python): Task 14 – two-process blinker test over ws_network
nicolas-rabault Apr 24, 2026
d45847a
docs(python): two-terminal ws_network example scripts
nicolas-rabault Apr 24, 2026
017defa
docs(python): document ws_network phy and its limits
nicolas-rabault Apr 24, 2026
2dec9ad
test(python): use canonical Pyluos broker, drop in-tree duplicate
nicolas-rabault Apr 27, 2026
4ea18e0
refactor(python): decouple cffi extension from phy symbols
nicolas-rabault Jun 2, 2026
e1e58da
feat(python): add platform-key helper for phy binary selection
nicolas-rabault Jun 2, 2026
7a9435b
feat(python): add engine version module (CI overwrites at build)
nicolas-rabault Jun 2, 2026
ca0162d
feat(python): add lazy phy loader (local -> cache -> download)
nicolas-rabault Jun 2, 2026
7d8a19b
feat(python): load phys lazily via get_phy_handle, drop import preload
nicolas-rabault Jun 2, 2026
800fd5f
ci(python): build platform wheels and publish phy assets on release
nicolas-rabault Jun 2, 2026
0a17b75
fix(python): address final review (CI matrix, lazy-load docs, guards)
nicolas-rabault Jun 2, 2026
3340c22
fix(robus): use engine mutex macros in LINUX HAL RxThread
nicolas-rabault Jun 12, 2026
18b6cdf
Revert "fix(robus): use engine mutex macros in LINUX HAL RxThread"
nicolas-rabault Jun 12, 2026
d411c01
fix(detection): retry node introduction (LOCAL_RTB) on timeout
nicolas-rabault Jun 15, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
91 changes: 91 additions & 0 deletions .github/workflows/pio_release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -184,3 +184,94 @@ jobs:
pio package pack
pio package publish --owner luos --non-interactive
cd ../..

build_wheels:
needs: check_valid_tag
runs-on: ${{ matrix.os }}
strategy:
matrix:
# Windows is not yet supported by the binding (no win32 linker
# branch in _build.py; luos.run() uses POSIX signal.pause()).
os: [ubuntu-22.04, macos-latest]
steps:
- uses: actions/checkout@v4
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: "3.11"
- name: Install build tooling
run: |
python -m pip install --upgrade pip
pip install platformio build
- name: Build native engine + phy dylibs
run: pio run -e native_lib
- name: Stage core engine into the package and pin version
shell: bash
run: |
cd bindings/python
cp ../../.pio/build/native_lib/libluos_engine.* luos_engine/ || true
printf 'ENGINE_VERSION = "%s"\n' "${GITHUB_REF_NAME}" > luos_engine/_version.py
- name: Build wheel
run: |
cd bindings/python
python -m build --wheel
- uses: actions/upload-artifact@v4
with:
name: wheel-${{ matrix.os }}
path: bindings/python/dist/*.whl

publish_phys:
needs: check_valid_tag
runs-on: ${{ matrix.os }}
strategy:
matrix:
# Windows is not yet supported by the binding (no win32 linker
# branch in _build.py; luos.run() uses POSIX signal.pause()).
os: [ubuntu-22.04, macos-latest]
steps:
- uses: actions/checkout@v4
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: "3.11"
- name: Install PlatformIO
run: |
python -m pip install --upgrade pip
pip install platformio
- name: Build native engine + phy dylibs
run: pio run -e native_lib
- name: Rename phys by platform key and build manifest
shell: bash
run: |
python bindings/python/tools/stage_phy_assets.py \
.pio/build/native_lib release_assets
- name: Upload phy assets to the release
uses: softprops/action-gh-release@v2
with:
tag_name: ${{ github.ref_name }}
files: release_assets/*

smoke_test:
needs: [build_wheels, publish_phys]
runs-on: ${{ matrix.os }}
strategy:
matrix:
# Windows is not yet supported by the binding (no win32 linker
# branch in _build.py; luos.run() uses POSIX signal.pause()).
os: [ubuntu-22.04, macos-latest]
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: "3.11"
- uses: actions/download-artifact@v4
with:
name: wheel-${{ matrix.os }}
path: dist
- name: Install the built wheel and run the network blinker
shell: bash
run: |
python -m pip install --upgrade pip
pip install dist/*.whl pyluos pytest
# Phys are NOT bundled; this proves they download from the release.
python -m pytest bindings/python/tests/test_network_blinker.py -v
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
.pio/
**/.pio/*
**/.gitignore
.venv*/
*.out
*.log
*.info
Expand Down
10 changes: 10 additions & 0 deletions bindings/python/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
__pycache__/
*.pyc
*.so
*.dylib
_luos_cffi.*
_enum_values.py
build/
dist/
*.egg-info/
.venv*/
135 changes: 135 additions & 0 deletions bindings/python/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
# luos_engine — Python binding

Python binding for the [Luos engine](https://github.com/Luos-io/luos_engine), designed to feel familiar to C-Luos users.

## Install (development)

```bash
# 1. Build the shared library
~/.platformio/penv/bin/pio run -e native_lib

# 2. Install the binding in a venv
python3 -m venv .venv && source .venv/bin/activate
pip install -e bindings/python
```

## Quick start

```python
import luos_engine as luos

led = luos.create_service(
type=luos.Type.STATE, alias="led",
on_message=lambda msg: print("LED state:", msg.data[0]),
)
blinker = luos.create_service(type=luos.Type.STATE, alias="blinker")

luos.start()
peer = blinker.find_peer(alias="led")
blinker.send(cmd=luos.Cmd.IO_STATE, target=peer.id, data=b"\x01")
luos.stop()
```

## Network phy: ws_network

`ws_network` connects nodes through a local WebSocket **broker**, not
peer-to-peer. A broker must be running before any node calls
`luos.load_phy(luos.phy.ws_network)`. The broker isn't a naive relay —
it must respond to each node's PING control frame with OK/NOK and track
which nodes have finished topology detection. Use the canonical
[Pyluos](https://pypi.org/project/pyluos/) broker (`pip install pyluos`)
or any compatible WebSocket broker that implements the Luos topology
protocol.

Two-process example — run each block in its own terminal:

### Terminal 1: broker

```bash
pip install pyluos
python -m pyluos.tools.ws_broker --ip 127.0.0.1 --port 8000
# prints: Luos_engine WebSocket network broker opened on 127.0.0.1:8000
```

### Terminal 2: LED service (`examples/led_ws.py`)

```python
import os, luos_engine as luos
BROKER = os.environ.get("LUOS_WS_BROKER", "ws://127.0.0.1:8000")

def on_msg(msg):
if msg.cmd == luos.Cmd.IO_STATE:
print("state:", msg.data[0])

luos.load_phy(luos.phy.ws_network, broker=BROKER)
luos.create_service(type=luos.Type.STATE, alias="led", on_message=on_msg)
luos.run()
```

### Terminal 3: blinker service (`examples/blinker_ws.py`)

```python
import os, time, luos_engine as luos
BROKER = os.environ.get("LUOS_WS_BROKER", "ws://127.0.0.1:8000")

luos.load_phy(luos.phy.ws_network, broker=BROKER)
svc = luos.create_service(type=luos.Type.STATE, alias="blinker")
luos.start()
peer = svc.find_peer(alias="led", timeout=30.0)
state = 0
while True:
state ^= 1
svc.send(cmd=luos.Cmd.IO_STATE, target=peer.id, data=bytes([state]))
time.sleep(1.0)
```

Pass `LUOS_WS_BROKER=ws://host:port` to point at a different broker.

## Porting from C

| C | Python |
|-----------------------------------------------------------|---------------------------------------------------------------------------------------|
| `Luos_CreateService(cb, STATE_TYPE, "led", rev)` | `luos.create_service(type=luos.Type.STATE, alias="led", on_message=cb)` |
| `msg->header.cmd == IO_STATE` | `msg.cmd == luos.Cmd.IO_STATE` |
| `msg->data[0]` | `msg.data[0]` (memoryview, valid only inside the handler) |
| `Luos_SendMsg(svc, &m)` | `svc.send(cmd=..., target=..., data=b"...")` |
| `Luos_Detect(svc)` + poll routing table | `svc.find_peer(alias="led", timeout=5.0)` |
| `while(1) { Luos_Loop(); }` | `luos.start()` / `luos.stop()` (background loop thread) |

## Environment variables

- `LUOS_ENGINE_LIB` — absolute path to `libluos_engine.{dylib,so,dll}`.
- `LUOS_ENGINE_LIB_DIR` — directory containing it.
- If neither is set, the package walks up from its install location looking for `.pio/build/native_lib/libluos_engine.*`. Covers the `pip install -e` dev flow.

## Running

```bash
pytest bindings/python/tests/ # 17 tests green
python bindings/python/examples/blinker.py # ASCII LED toggles every second
```

## Protocol notes

- **Reserved commands.** Luos reserves `cmd` values 0–42 for internal protocol messages (detection, routing, bootloader, assert, etc.). User-defined commands start at 43. Handlers that only care about application traffic should ignore messages with `cmd < 43`. See `engine/engine_config.h:LUOS_LAST_RESERVED_CMD`.
- **Message allocator pressure.** The engine's message ring holds `2 * MAX_LOCAL_SERVICE_NUMBER` slots (10 by default). Burst-sending more messages than the loop can drain raises a C assertion. In practice, a short sleep between back-to-back sends (> 1 ms) is enough. The blinker example sends once per second, which is well under the limit.
- **Handlers run on the loop thread.** A slow Python handler (file I/O, network) will block `Luos_Loop()` and starve detection/routing. Offload heavy work to your own thread from inside the handler.

## Limitations

- Only `ws_network` is Python-exposed. `serial_network` and `robus_network`
dylibs are built (as a side effect of the same PlatformIO run) but
not yet Python-wired.
- Phys are load-once-per-process. There is no `unload_phy` or re-load
path; Mongoose's poll thread has no documented teardown.
- The broker address is fixed at `load_phy` time via the `broker=`
kwarg and cannot be changed mid-process.
- No streaming, big-data, pub/sub, or timestamped-send wrappers yet. The raw C symbols are reachable via `luos.lib.*` for power users.
- Tested on macOS (Darwin/arm64). Linux is expected to work via `-Wl,--unresolved-symbols=ignore-in-object-files` but not yet validated in CI. Windows is out of scope (`signal.pause()` in `luos.run()` is POSIX-only).

## Design

- Phase 1 spec: [docs/superpowers/specs/2026-04-23-python-binding-design.md](../../docs/superpowers/specs/2026-04-23-python-binding-design.md)
- Phase 1 plan: [docs/superpowers/plans/2026-04-23-python-binding.md](../../docs/superpowers/plans/2026-04-23-python-binding.md)
- Phase 2 spec: [docs/superpowers/specs/2026-04-24-python-binding-phase2-design.md](../../docs/superpowers/specs/2026-04-24-python-binding-phase2-design.md)
- Phase 2 plan: [docs/superpowers/plans/2026-04-24-python-binding-phase2.md](../../docs/superpowers/plans/2026-04-24-python-binding-phase2.md)
82 changes: 82 additions & 0 deletions bindings/python/examples/blinker.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
"""Two Python Luos services in one process: `led` (renders ASCII art)
and `blinker` (sends IO_STATE every second)."""
import os
import signal
import threading
import time

import luos_engine as luos

LED_ON, LED_OFF = 1, 0

LED_ON_ART = """ \\ | /
___________________________.-.________________
/ - |***| - /;
/ ________________ - |***| - //
/ / /; [` - ') //
/ / MCU // `---' //
/ / // | | //
/ /______________ // //
/ '---------------' //
/ //
/_____________________________________________//
`---------------------------------------------'
"""

LED_OFF_ART = """
___________________________.-.________________
/ | | /;
/ ________________ | | //
/ / /; [` - ') //
/ / MCU // `---' //
/ / // | | //
/ /______________ // //
/ '---------------' //
/ //
/_____________________________________________//
`---------------------------------------------'
"""


def _clear():
os.system("clear" if os.name != "nt" else "cls")


def on_led_message(msg):
if msg.cmd < luos.FIRST_USER_CMD:
return # skip internal detection / routing messages
if msg.cmd != luos.Cmd.IO_STATE:
return
state = msg.data[0]
_clear()
print("LED service running.\n", flush=True)
print(LED_ON_ART if state == LED_ON else LED_OFF_ART, flush=True)


def main():
led = luos.create_service(
type=luos.Type.STATE, alias="led", on_message=on_led_message
)
blinker = luos.create_service(type=luos.Type.STATE, alias="blinker")
luos.start()

peer = blinker.find_peer(alias="led", timeout=5.0)
print(f"blinker: found led at id={peer.id}", flush=True)

stop_event = threading.Event()
def handler(*_): stop_event.set()
signal.signal(signal.SIGINT, handler)

state = LED_OFF
try:
while not stop_event.is_set():
state ^= 1
blinker.send(cmd=luos.Cmd.IO_STATE, target=peer.id,
data=bytes([state]))
time.sleep(1.0)
finally:
luos.stop()


if __name__ == "__main__":
main()
33 changes: 33 additions & 0 deletions bindings/python/examples/blinker_ws.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
"""Blinker service over ws_network. Run `led_ws.py` in another terminal
(and a broker before both), then `python blinker_ws.py`."""
import os
import time
import luos_engine as luos

BROKER = os.environ.get("LUOS_WS_BROKER", "ws://127.0.0.1:8000")


def main():
luos.load_phy(luos.phy.ws_network, broker=BROKER)
svc = luos.create_service(type=luos.Type.STATE, alias="blinker")
luos.start()
peer = svc.find_peer(alias="led", timeout=30.0)
print(f"[blinker] found led at id={peer.id}", flush=True)
try:
state = 0
while True:
state ^= 1
svc.send(
cmd=luos.Cmd.IO_STATE,
target=peer.id,
data=bytes([state]),
)
time.sleep(1.0)
except KeyboardInterrupt:
pass
finally:
luos.stop()


if __name__ == "__main__":
main()
26 changes: 26 additions & 0 deletions bindings/python/examples/led_ws.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
"""LED service over ws_network. Run a broker first (e.g.
`python bindings/python/tests/_broker.py`), then `python led_ws.py`."""
import os
import luos_engine as luos

BROKER = os.environ.get("LUOS_WS_BROKER", "ws://127.0.0.1:8000")


def on_msg(msg):
if msg.cmd < luos.FIRST_USER_CMD:
return
if msg.cmd == luos.Cmd.IO_STATE:
state = "ON " if msg.data[0] else "off"
print(f"[led] {state}", flush=True)


def main():
luos.load_phy(luos.phy.ws_network, broker=BROKER)
luos.create_service(
type=luos.Type.STATE, alias="led", on_message=on_msg,
)
luos.run()


if __name__ == "__main__":
main()
Loading
Loading