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
48 changes: 44 additions & 4 deletions tools/traffic/PROTOCOL.md
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
# What BEYOND and an FB4 actually say to each other

Findings from two captures taken on the Grace Cathedral show machine
Findings from captures taken on the Grace Cathedral show machine
(`169.254.42.165`, hostname `WIN-R4FTH86FTV5`) with six FB4s on a link-local
network: `169.254.45.4`, `.53.5`, `.200.242`, `.210.242`, `.213.242`, `.214.242`.
One capture is idle-ish, one is a paint session.
One capture is idle-ish, one is a paint session, and a later set is sliced by
action (idle baseline, hand-sent OSC, `signals probe` walk, idle tail).

Everything below was read off the wire. Where the evidence stops, this document
says so — nothing here is inferred from how we imagine Pangolin works. Reproduce
Expand Down Expand Up @@ -80,7 +81,7 @@ Types observed:

| type | name used here | size | rate | direction |
| --- | --- | --- | --- | --- |
| `0x00030E02` | frame | 2392 B | 16–23/s per device | BEYOND → FB4 |
| `0x00030E02` | frame | 2392 B | 16–62/s per device | BEYOND → FB4 |
| `0x00010E02` | control | 80 B | 31–37/s per device | BEYOND → FB4 |
| `0x00008A0D` | telemetry | 2432 B | ~1/s | FB4 → BEYOND |
| `0x00008A0B` | unknown | 72 B | ~0.2/s | FB4 → BEYOND |
Expand All @@ -107,11 +108,50 @@ provide (key material, instrumented software, or vendor documentation) — and
per the standing constraint on this toolkit, nothing here transmits toward the
hardware regardless.

## Two things the sliced capture set settled

**One FB4 was getting no frames at all.** `169.254.45.4` (`FB4E`, id 566604)
announced itself on 9022 in every capture, but has zero TCP 3348 traffic in any
of them: BEYOND was streaming to five devices, not six, so that projector could
only ever have been dark. `decode` now says so per device — being on the network
and being driven are different things, and only the second one lights a laser.

**A closed RGBA panel is why OSC can land and do nothing.** Those captures show
90 OSC packets delivered to port 8000 with no ICMP port-unreachable in reply —
delivery proven — and *zero* packets on 16062, where the earlier paint capture
had 675. With the panel closed BEYOND does not broadcast, and the `livecontrol`
colour addresses are gated, so "the messages arrived" and "the colour changed"
were never the same claim. Silence on 16062 is the signal to check
`ShowRGBAPanel` before debugging anything else.

What they did *not* settle: none of them contains a TCP SYN, so BEYOND's
connection setup has still never been captured. If key material is negotiated
there, it remains unseen — capture with the FB4s power-cycled and BEYOND started
afterwards to catch it.

## Ports, precisely

Worth stating because it is easy to get backwards from packet counts: 9022 is
*only* discovery — 38 packets across a 7-second idle baseline. The frames are
TCP 3348, ~62 messages/s per device, continuous even through a blackout. A high
packet rate on the wire is the frame stream, never discovery.

## Ways in that are not this protocol

Since the frame stream can't be driven, the documented non-BEYOND routes matter.
Per Pangolin's wiki, an FB4 listens for OSC on port 8000 for its own settings
including `operation_mode`, and in ArtNet mode presents a 16- or 39-channel DMX
fixture profile over ethernet. Both are colour/brightness/playback control over
content stored on the FB4's SD card — not arbitrary geometry from us, which the
encrypted stream is the only path to. Untested here; noted so the option isn't
rediscovered from scratch.

## What this is good for

- Confirming an OSC message reached BEYOND, and what value it set, per zone
(`./bin/rgba`, or `./bin/decode --timeline` on a capture).
- Confirming every FB4 is present, with its id/MAC, without opening BEYOND.
- Confirming every FB4 is present, with its id/MAC, without opening BEYOND —
and which of them BEYOND is actually streaming to.
- Confirming BEYOND is streaming to each FB4, at what rate, and whether frames
are being dropped — i.e. telling "the show isn't reaching the lasers" apart
from "the lasers are being told to draw nothing".
15 changes: 15 additions & 0 deletions tools/traffic/lib/pangolin.py
Original file line number Diff line number Diff line change
Expand Up @@ -221,6 +221,15 @@ def read_packets(tshark: str, path: str) -> list[Packet]:
return packets


def streaming_ips(packets: list[Packet]) -> set[str]:
"""IPs BEYOND has a frame stream with, either direction."""
return {
ip
for p in packets if p.proto == 'tcp' and FB4_STREAM_PORT in (p.sport, p.dport)
for ip in (p.src, p.dst)
}


def report_devices(packets: list[Packet]) -> None:
devices: dict[str, Device] = {}
host_names: set[str] = set()
Expand All @@ -245,10 +254,16 @@ def report_devices(packets: list[Packet]) -> None:
print('== devices (UDP 9022) ==')
if host_names:
print(f' BEYOND host: {", ".join(sorted(host_names))}')
streaming = streaming_ips(packets)
for ip, d in sorted(devices.items()):
label = f'{d.tag} ' if d.tag else ''
print(f' {ip:<16} {d.mac:<18} {label}id={d.device_id or "?"} '
f'settings={len(d.settings)}')
if streaming and ip not in streaming:
# It is on the network and announcing, but BEYOND never opened a
# frame stream to it: a projector that will stay dark.
print(' no frame stream — BEYOND is not sending to '
'this device')
print()


Expand Down
30 changes: 30 additions & 0 deletions tools/traffic/lib/pangolin_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@
report_rgba,
report_stream,
split_messages,
streaming_ips,
)


Expand Down Expand Up @@ -155,6 +156,16 @@ def test_empty(self):
self.assertEqual(entropy(b''), 0.0)


class StreamingIpsTest(unittest.TestCase):
def test_both_ends_of_a_frame_stream_count(self):
self.assertEqual(streaming_ips([tcp(b'x')]),
{'169.254.42.165', '169.254.45.4'})

def test_discovery_traffic_is_not_a_frame_stream(self):
packet = udp(b'x', sport=FB4_DISCOVERY_PORT, dport=FB4_DISCOVERY_PORT)
self.assertEqual(streaming_ips([packet]), set())


class ReportTest(unittest.TestCase):
"""The reports must say 'nothing here' rather than crash on a quiet capture."""

Expand All @@ -177,6 +188,25 @@ def test_devices_are_listed_with_mac_and_id(self):
self.assertIn('00:16:42:fb:04:2c', text)
self.assertIn('id=566604', text)

def test_a_device_getting_no_frames_is_called_out(self):
# Seen on the show machine: 169.254.45.4 announced itself but BEYOND
# never streamed to it, so that projector stayed dark.
announce = bytes(0x20) + b'FB4E' + (566604).to_bytes(4, 'little')
packets = [
udp(announce, sport=FB4_DISCOVERY_PORT, dport=FB4_DISCOVERY_PORT,
src='169.254.53.5'),
tcp(message(STREAM_TYPE_FRAME, bytes(16), 1)),
]
self.assertIn('no frame stream', self.render(report_devices, packets))

def test_a_device_that_is_streaming_is_not_called_out(self):
announce = bytes(0x20) + b'FB4E' + (566604).to_bytes(4, 'little')
packets = [
udp(announce, sport=FB4_DISCOVERY_PORT, dport=FB4_DISCOVERY_PORT),
tcp(message(STREAM_TYPE_FRAME, bytes(16), 1)),
]
self.assertNotIn('no frame stream', self.render(report_devices, packets))

def test_zone_state_is_the_last_value_seen(self):
packets = [
udp(b'ControlZone 2\r\nRGBA 0, 10\r\n', sport=5000,
Expand Down
Loading