docs(sacn): Sender fuer die Abnahme, README und Bruno-Anfrage

This commit is contained in:
Michael Sauer
2026-09-20 00:57:16 +02:00
parent c547a9f2b2
commit f2cae9b7ce
3 changed files with 135 additions and 2 deletions
+28 -1
View File
@@ -65,12 +65,39 @@ Verfügbare Befehle in der Konsole: `help`.
gespeicherten Zugangsdaten). Im Fallback-AP (Router war > 5 min weg, Lampe hat aber bereits gespeicherten Zugangsdaten). Im Fallback-AP (Router war > 5 min weg, Lampe hat aber bereits
Zugangsdaten) ist der Token-Header wie bei jedem anderen Endpunkt erforderlich. Zugangsdaten) ist der Token-Header wie bei jedem anderen Endpunkt erforderlich.
Nach Erfolg schließt der AP nach 60 s. Ohne Verbindung für 5 min öffnet die Lampe den AP zusätzlich (Fallback). Nach Erfolg schließt der AP nach 60 s. Ohne Verbindung für 5 min öffnet die Lampe den AP zusätzlich (Fallback).
- REST auf Port 3333, Präfix `/api/v1`: `GET|POST device/state`, `GET system/info`, `GET|PUT system/settings` und `PUT system/command` (beide mit Header `Authorization: <token>`, Default-Token `aabbccddeeff0011223344`). - REST auf Port 3333, Präfix `/api/v1`: `GET|POST device/state`, `GET system/info`, `GET|PUT system/settings` (kennt u.a. den Typ `sacn`, siehe unten) und `PUT system/command` (beide mit Header `Authorization: <token>`, Default-Token `aabbccddeeff0011223344`).
- Beispiel: `curl -X POST http://ninlight-681f.local:3333/api/v1/device/state -d '{"on":true,"brightness":40,"color_temp":2700}'` - Beispiel: `curl -X POST http://ninlight-681f.local:3333/api/v1/device/state -d '{"on":true,"brightness":40,"color_temp":2700}'`
- Suchen: `dns-sd -B _ninlight._tcp` (macOS), Details `dns-sd -L ninlight-681f _ninlight._tcp`. - Suchen: `dns-sd -B _ninlight._tcp` (macOS), Details `dns-sd -L ninlight-681f _ninlight._tcp`.
- Bruno: `bruno/ninlight.yml` importieren, Environment „EVK (mDNS)" bzw. „Onboarding-AP" wählen. - Bruno: `bruno/ninlight.yml` importieren, Environment „EVK (mDNS)" bzw. „Onboarding-AP" wählen.
- Konsole: `net` zeigt Modus, SSID, IP, RSSI und den letzten Onboarding-Fehler. - Konsole: `net` zeigt Modus, SSID, IP, RSSI und den letzten Onboarding-Fehler.
### sACN / E1.31
Die Lampe empfängt sACN (E1.31) auf UDP-Port 5568 — per Multicast auf
`239.255.<universe_hi>.<universe_lo>` und per Unicast direkt an ihre IP. Nach einem
Update ist der Empfang aktiv: Universe 1, Startslot 1.
Slots ab `start_slot`: **Dimmer, R, G, B, CW, WW** (Boards ohne RGB: **Dimmer, CW, WW**).
Wieviele es sind, steht als `slots` in `GET system/settings`.
Während Pakete fließen, hat sACN Vorrang; die Werte gehen ohne Fade direkt an die
Kanäle, und der per API gesetzte Zustand bleibt unangetastet. 2,5 s nach dem letzten
Paket endet die Übernahme — je nach `on_timeout` fällt die Lampe auf den per API
gesetzten Zustand zurück (`release`, Vorgabe) oder behält die letzten Werte (`hold`).
Ein Befehl an `device/state` holt die Kontrolle in beiden Fällen sofort zurück.
Umstellen:
curl -X PUT http://<lampe>:3333/api/v1/system/settings \
-H 'Authorization: <token>' \
-d '{"type":"sacn","universe":12,"start_slot":7,"on_timeout":"hold"}'
Der Port ist offen und nicht authentifiziert — so sieht das Protokoll es vor. Wer im
WLAN sitzt, kann die Lampe damit leuchten lassen, aber nichts einstellen und nichts
speichern. Zum Abschalten: `{"type":"sacn","enabled":false}`.
Zum Ausprobieren liegt ein Sender bei: `tools/sacn_send.py --values 255,255,0,0,0,0 --seconds 5`.
## Firmware-Update ## Firmware-Update
Der Build erzeugt pro Gerät zwei Images: Der Build erzeugt pro Gerät zwei Images:
+23 -1
View File
@@ -328,10 +328,32 @@ items:
timeout: 0 timeout: 0
followRedirects: true followRedirects: true
maxRedirects: 5 maxRedirects: 5
- info:
name: Set sACN
type: http
seq: 7
http:
method: PUT
url: '{{device_address}}:{{device_port}}/api/v1/system/settings'
body:
type: json
data: |-
{
"type": "sacn",
"enabled": true,
"universe": 1,
"start_slot": 1,
"on_timeout": "release"
}
settings:
encodeUrl: true
timeout: 0
followRedirects: true
maxRedirects: 5
- info: - info:
name: Commands name: Commands
type: folder type: folder
seq: 7 seq: 8
items: items:
- info: - info:
name: Ping name: Ping
+84
View File
@@ -0,0 +1,84 @@
#!/usr/bin/env python3
"""Schickt sACN/E1.31-Pakete an eine ninlight-Lampe - fuer die Abnahme von Hand.
Aufruf:
tools/sacn_send.py --universe 1 --start-slot 1 --values 255,255,0,0,0,0 --seconds 5
tools/sacn_send.py --host 192.168.2.122 --values 255,0,255,0,0,0 --seconds 5
tools/sacn_send.py --values 255,255,255,255,255,255 --seconds 3 --terminate
Ohne --host geht es per Multicast an 239.255.<hi>.<lo>, mit --host per Unicast direkt an
die Lampe. Die Werte sind der Slot-Satz ab --start-slot: Dimmer, R, G, B, CW, WW (Boards
ohne RGB: Dimmer, CW, WW).
"""
import argparse
import socket
import struct
import time
E131_PORT = 5568
ACN_PID = b"ASC-E1.17\0\0\0"
def packet(universe, sequence, values, start_slot, priority, terminated):
slots = bytearray(512)
for i, v in enumerate(values):
slots[start_slot - 1 + i] = v
count = start_slot - 1 + len(values)
data = bytes(slots[:count])
total = 126 + count
p = bytearray(total)
struct.pack_into(">HH", p, 0, 0x0010, 0x0000)
p[4:16] = ACN_PID
struct.pack_into(">H", p, 16, 0x7000 | (total - 16))
struct.pack_into(">I", p, 18, 0x00000004)
p[22:38] = b"ninlight-abnahme" # CID, 16 Byte
struct.pack_into(">H", p, 38, 0x7000 | (total - 38))
struct.pack_into(">I", p, 40, 0x00000002)
p[44:44 + 13] = b"ninlight test"
p[108] = priority
p[111] = sequence & 0xFF
p[112] = 0x40 if terminated else 0x00
struct.pack_into(">H", p, 113, universe)
struct.pack_into(">H", p, 115, 0x7000 | (total - 115))
p[117] = 0x02
p[118] = 0xA1
struct.pack_into(">HH", p, 119, 0x0000, 0x0001)
struct.pack_into(">H", p, 123, count + 1)
p[125] = 0x00
p[126:] = data
return bytes(p)
def main():
ap = argparse.ArgumentParser()
ap.add_argument("--universe", type=int, default=1)
ap.add_argument("--start-slot", type=int, default=1)
ap.add_argument("--values", default="255,255,255,0,0,0")
ap.add_argument("--priority", type=int, default=100)
ap.add_argument("--seconds", type=float, default=5.0)
ap.add_argument("--fps", type=float, default=30.0)
ap.add_argument("--host", help="Unicast statt Multicast")
ap.add_argument("--terminate", action="store_true",
help="zum Schluss ein Paket mit gesetztem stream_terminated-Bit")
a = ap.parse_args()
values = [int(v) for v in a.values.split(",")]
dest = a.host or "239.255.%d.%d" % (a.universe >> 8, a.universe & 0xFF)
s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
s.setsockopt(socket.IPPROTO_IP, socket.IP_MULTICAST_TTL, 8)
seq, deadline, step = 0, time.time() + a.seconds, 1.0 / a.fps
while time.time() < deadline:
s.sendto(packet(a.universe, seq, values, a.start_slot, a.priority, False),
(dest, E131_PORT))
seq += 1
time.sleep(step)
if a.terminate:
s.sendto(packet(a.universe, seq, values, a.start_slot, a.priority, True),
(dest, E131_PORT))
print("%d Pakete an %s:%d, Universe %d" % (seq, dest, E131_PORT, a.universe))
if __name__ == "__main__":
main()