Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
23 changes: 23 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -260,6 +260,29 @@ archived by series under [docs/changelog/](docs/changelog/); see the
package's job. Two mesh controller tests were failing: they registered two
peers in a mesh with room for four, so the eviction they assert was never
weighed. They now fill the mesh, as their Kotlin twins have since #120.
- **Services on the LAN, and the first Python service wrappers.** A new
specification chapter, `docs/spec/dns-sd-mapping.md`, lays a
`ServiceDescriptor` out as a DNS-SD instance under the subtype
`_svc._sub._offlineprotocol._tcp`: a digest instance name, a TXT record
(`txtvers`, `sid`, `ver`, `addr`, one `c.<key>` per capability) and its
bounds (a service id over 200 bytes, a record over 1300 bytes or a
capability key DNS-SD cannot carry is refused, never truncated). An
imported LAN record is unsigned: it arrives as a `service_discovered`
event with `source: "lan"`, is kept in an application-level registry, and
is never registered with the engine, because a registration made from it
would go out in signed discovery responses under this node's identity.
A peer-stream browser now ignores a record carrying `sid`, so a published
service is not one more connector to the same host. In Python,
`services.Services` wraps the generated `MeshServices` with the copy of
this node's registrations the engine cannot enumerate, and refuses a
response status outside the engine's closed set with the reason;
`dnssd_bridge.DnsSdBridge` publishes those registrations and imports the
LAN's, over the existing optional `lan` extra, re-resolving an import at
half its time to live and dropping it at the whole. The service discovery
guide is corrected where it disagreed with the engine: discovery responses
go to the peer the query came from and are forwarded toward the
originator, the response status is one of exactly three values, the
version is opaque, and the peer-tracking hook is `on_neighbor_discovered`.

### Fixed

Expand Down
44 changes: 44 additions & 0 deletions bindings/python/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,36 @@ await pm.peer_stream.start()
A peer is announced only under the address its preamble proves, and
`pm.stop()` stops the stream layer with everything else.

### Services, and services on the LAN

`Services` wraps the generated `MeshServices` and keeps the one thing the
engine cannot give back: the list of what this node registered. `respond`
refuses a status the engine would refuse (`ok`, `not_found` and `error` are
the whole set) with the reason instead of an opaque core error.

```python
from offline_protocol_sdk.services import Services
from offline_protocol_sdk.dnssd_bridge import DnsSdBridge

services = Services(pm.protocol)
services.register("weather.v1", "2.0", {"format": "json"}) # before or after pm.start()

# Publish this node's registrations on the LAN and import the neighbours'.
# Needs: pip install 'offline-protocol-sdk[lan]'
bridge = DnsSdBridge(services, on_event=handle_event)
await bridge.start(address=pm.local_address, port=pm.peer_stream.listen_port)
...
await bridge.stop() # before pm.stop()
```

A LAN import arrives at `handle_event` as a `service_discovered` event with
`source: "lan"` and is kept in `bridge.lan_services()`; it is an unsigned
claim by whoever answered on the segment, never a discovery, and the bridge
never registers it with the engine. Registrations that do not fit the record
(a service id over 200 bytes, a capability key DNS-SD cannot carry, a record
over 1300 bytes) are kept on the mesh and not published, with a warning. The
mapping is [docs/spec/dns-sd-mapping.md](../../docs/spec/dns-sd-mapping.md).

## Architecture

```
Expand All @@ -106,6 +136,8 @@ offline_protocol_sdk/
├── protocol_manager.py # High-level wrapper (processing loop, lifecycle)
├── internet_manager.py # WebSocket transport (websockets library)
├── peer_stream_manager.py # TCP peer streams + DNS-SD (the wifi_direct slot)
├── services.py # Service registry wrappers with the copy the engine lacks
├── dnssd_bridge.py # Services on the LAN: publish own, import neighbours' (optional extra `lan`)
├── ble_manager.py # BLE transport (bleak library)
├── secure_storage.py # MLS key storage (keyring library)
├── state_storage.py # Restartable protocol state (application data)
Expand Down Expand Up @@ -429,3 +461,15 @@ Both license texts, along with `THIRD-PARTY-NOTICES.md`, are also installed with
package under `offline_protocol_sdk-<version>.dist-info/licenses/`. The links above are
absolute because this file is the PyPI long description, and PyPI does not resolve
repository-relative links.

### Optional dependencies

`THIRD-PARTY-NOTICES.md` covers the Rust crates compiled into the native
library. The `lan` extra (`pip install 'offline-protocol-sdk[lan]'`) adds two
runtime dependencies that pip installs from PyPI and that are never
redistributed in this wheel: [python-zeroconf](https://pypi.org/project/zeroconf/)
(LGPL-2.1-or-later), used by `peer_stream_manager.py` and `dnssd_bridge.py`
for DNS-SD, and [ifaddr](https://pypi.org/project/ifaddr/) (MIT), used to
list the interface addresses a record publishes. Both are imported only when a
manager or bridge is asked to advertise or discover; the base install imports
neither.
Loading
Loading