Something is out there, over the horizon, in the clear. A planet. A star. A far galaxy. One of the old probes, still coasting outward. Entities unknown and unknowable. Choose, and the station wakes.
It reaches out. The antenna finds your target and holds. On your mark, your image leaves as open video, outbound across the dark at the speed of light; there is no faster way to send anything, and no way to call it back.
Your transmission is logged: target, range, and the date the light arrives - long after you've left the station, and in some cases long after all of us. It's sealed and keyed to the code on your screen; the station keeps nothing, and without that code no one opens it, not even us. It fades at the end of the one-week cycle. Take the code to keep it, or let the silence have it.
01The experience
- Choose a celestial body that's not obscured by the earth - at least 30 degrees above the horizon
- Pose for your photo
- The robot arm and antenna descend from the roof of the gazebo, it takes aim at your chosen target and transmits your image at 5.8GHz analogue video
- A QR code is shown on screen for you to collect your souvenir photo, which contains information about your target and how long the transmission will take to arrive
02What it's made of
The experience relies on a Waveshare RoArm M3 Pro to direct a 14dBi patch antenna toward the chosen target, then uses an FPV drone video transmitter to send the user's photo as video for 8 seconds.
03How to do this
How do you design a system that satisfies the following constraints?
- Must actually transmit the image toward the target for real. There's no faking it at EMF!
- Must be weatherproof (rain, wind and sun)
- Must be an engaging experience
- Must be a safe experience - robot arms are notoriously risky, since they can move quickly and be unpredictable
- Must work in an environment full of all kinds of electromagnetic interference
- Must not cause any more interference than is strictly necessary for the operation of the installation
3.1It transmits for real
You can't get away with smoke and mirrors at EMF. The core premise is that your image is transmitted at your target. Without this, there would be no point.
Within this requirement are a few levers we can pull though. Most notably, the transmission power. To transmit on the 5.8GHz band legally, you must keep your transmission below the 25mW EIRP power limit. Since this is a 14dBi directional antenna and the VTX has a minimum power output of 25mW, this means we'd be transmitting at ~600mW EIRP, which is way above the limit, so 15dB attenuator is used to counter the antenna gain.
The elephant in the gazebo is that this is really quite low power. I did actually gain a foundation level amateur radio license for this, which would increase the power limit to 25 Watts - a considerably higher power - but since we're not strictly communicating with another radio amateur and there's a commercial link to my company, it's not possible to apply this.
Ultimately, it's the user experience that matters, and transmitting at a higher power doesn't materially improve it.
3.2A digital twin decides whether the arm can make the shot
We draw from a few ephemeris sources and initially filter them down to just those above 30 degrees elevation from the horizon. Despite the low power, we'd like to avoid firing at terrestrial stuff - one of the core requirements is to not cause any more interference than is strictly necessary.
The arm and antenna are suspended from the main cross-beam on the gazebo roof structure, underneath the king post. I think this looks pretty awesome - reminiscent of GLaDOS from Portal - but it adds more challenges:
- There are a lot of things to hit
- There is a lot of metalwork in the way of our transmission
Both of these issues are solved in the same (potentially over-engineered) way: a full simulation of the gazebo structure, arm and antenna, with a 30 degree cone representing the RF emission itself. This is done using PyBullet, and the glTF model of the arm itself provided by Waveshare.
Using this simulation, we're able to test poses and work out exactly how much of the sky we're able to reach, within a tolerance of about 12 degrees to either side from the antenna boresight, due to the 30 degree beam angle of the antenna.
This simulation is run for every target that we've already filtered to be >30 degrees elevation. If a target is unreachable due to gazebo frame occlusion then we simply don't offer it to the user at this time.
The list of targets is updated every few minutes, so that we're always working with up-to-date vectors. There's always a good selection of targets available after the pipeline has completed, and due to the simulation, we store the exact joint angles for the arm along with the list of targets. When it comes to pointing the arm at a target, we already have the information we need.
3.3Putting yourself out there
After a photo is taken, the Pi 5 in the plinth composites it into a template containing information about the transmission and publishes the image as base64 on a private MQTT broker which the transmitter Pi 3 picks up and displays on its framebuffer.
The Pi 3 (as do all of the others except Pi 5) has a composite video output, which we hook up to the input of the video transmitter (VTX). The Pi 3 also has a PiFace 2 HAT that includes two relays - one of which is used to switch the VTX power on and off, to ensure that we're only transmitting when we want to be.
3.4You watch the actual signal come back
It's one thing to say that we're transmitting, but it's another thing to prove that we're transmitting. Beyond adding a second screen in the gazebo with an explicit always-on "THIS IS WHAT WE'RE SENDING", I wasn't quite sure how to prove this. In the end, I settled on adding a screen to the GUI which starts off with "NO SIGNAL" and during the transmission will display what we're actually sending.
The way this works is slightly convoluted.
An FPV drone video receiver is placed within the plinth, with its HDMI output connected to a GL.inet network KVM. The booth app maintains an authenticated connection to the KVM and holds two websockets open - one for control, one for media - which is what makes the KVM start encoding at all. The stream comes back as H.264, which we decode on a GStreamer thread and hand to the GUI a frame at a time. It only runs while the transmitting page is on screen, and there's a watchdog that drops it back to "NO SIGNAL" if frames stop arriving, because a frozen picture from a dead feed otherwise looks exactly like a live one.
3.5The lights track the antenna in real time
Since we've already gone to the trouble of building a digital twin simulation, we might as well use it for something fun. A large part of the theatre of the experience is the lighting system. It helps to draw people in, and enables us to direct peoples' attention toward what's happening next.
Once the user has chosen their target and had their photo taken, the 'show' begins. At which point, the moving-head spotlights stop performing their idle animation and start tracking the antenna on the end of the arm. We can do this by calculating the vector from where the moving-heads are mounted, to where the antenna currently is in our simulation and then transforming it into rotation and tilt angles to send out over DMX.
The combination of the spotlights and the LED strip lights on the arm and antenna, really help to create a focal point while the arm is moving, and the colours used help to signify when the transmission itself begins.
04One thing that went wrong
Prior to EMF I had the gazebo set up in the garden for a week. This allowed me to test various aspects of the experience, as well as update things that didn't work, buy longer cables for things and prepare myself for how the on-site build should work.
One thing that the test build can never prepare you for is quite how many other unpredictable things will be happening on the RF spectrum on site. Once I had brought the system online at EMF, I was working through some minor issues, when the arm suddenly moved at full speed to point straight down from its mount. This was alarming and dangerous. Once I'd removed the arm's power, I started to think about what could be happening. One of the tasks on my list that I had yet to complete was to disable the default WiFi access point that the arm itself broadcasts. It isn't outside the realm of possibility at a hacker festival that somebody had connected to the arm's access point and started to fiddle around.
Unfortunately, changing the SSID and password didn't fix the issue. The arm kept sporadically moving into various pre-defined poses, such as straight out forwards, straight out backwards, straight down, elbow bent at 90 degrees, wrist bent at 90 degrees and so on. If the antenna had been attached at this point, it would have been smashed to pieces.
One of the features of these robot arms is that you can link one 'leader' arm to a number of 'follower' arms. This allows you to send commands to, or manually move one arm, and the other arms will move along in sync. In order to achieve this, they use a protocol called ESP-NOW. You may already see where this is going when I say that the EMF festival badges also use ESP32 microcontrollers.
The arm was set up to run in 'follower' mode, which was picking up packets from ESP-NOW, independently of the WiFi setup. Another installation nearby was using ESP-NOW to control a music-making system with their event badge. Unbeknown to the users of that installation, they were also sending a nearby robot arm haywire.
Once this was switched off, and the arm carefully observed for a few hours, I was confident that we'd solved the problem and could attach the antenna to bring the installation online.
4.1One more small thing that went wrong
During the test build, once I set the moving-heads to run on a nice idle animation, the four small DMX wash lights I was using for ambient illumination began to periodically black out for a second or two at a time.
After a lot of debugging, it turned out that if any channel on the DMX universe was set to the value 204 (0xCC), the washes would immediately black out. This is due to a firmware bug in the lights themselves. The only real way to work around this is to post-process the DMX frames before they're sent out over ArtNet, and substitute any 204 with a 203. It's one step out of 255, so nothing looks different, but it does mean there's one value on the whole universe that the moving heads can never quite sit at. Only the frames going to the wired DMX node get this treatment - the WLED boxes on the arm don't have the bug, so they get the real numbers. This is ridiculous, but luckily meant we could continue with the installation.
05The unglamorous bits
5.1Arm safety
Safety is a real concern when moving a robot arm around, even if you're doing it slowly and the arm itself isn't particularly powerful. I've hit my head on the antenna twice so far.
Your first thought may have been (as was mine) to install an emergency stop button that visitors could press if the arm did anything unexpected or dangerous. The problem with this approach is that if the arm is held out sideways, cutting the power will cause an uncontrolled drop, and move faster than it ever would under power. This can injure someone as well as being likely to damage the servos.
Having taken this into consideration, I've designed the lobes surrounding the antenna to be somewhat sacrificial - they're only loosely attached, so should act as a sacrificial element if anything is collided with. As well as this, the daemon controlling the arm is set up so that if the servos report torque levels outside of the usual parameters, the arm will abort the move, back off a little and eventually return to its home position by reversing the movements it made to get there. If it then over-torques again, the arm will stop all movement and wait to be reset manually.
5.2Privacy
The photobooth system unavoidably captures photos of its users. Before these photos are stored to disk, they are encrypted with a freshly generated AES-256-GCM key. This key is never logged anywhere, and is only built into the URL from which the QR code is generated and shown at the end. Once that QR code is removed from the screen, there is no way for anyone to decrypt the photo.
On the collection website, the key is included as a URL fragment, that the client-side JavaScript code on the user's device uses to decrypt the downloaded binary blob and display the user's image to them.
This is built in to our photobooth product already, but of course the obvious caveat in this particular installation is that we're beaming the image to the user's celestial target in the clear. In theory, somebody with a sensitive enough receiver could pick up the image during the 8 seconds that it is transmitted, but it's inherent in the design that your image will be sent out, so I believe this is an acceptable departure from the norm.
5.3Ops
The system runs on an MQTT bus, and most of the components read and write to various topics, which are laid out in a centralised MQTT topic contract.
Home Assistant is really versatile, and can be easily be used as a monitoring and ops platform, especially when the system coordinates over MQTT.
The entire rig is provisioned and configured with Ansible, and uses tagging and a "justfile" to enable convenient deployment commands like "just lighting" which deploys the lighting fixtures and scenes configuration and restarts the relevant components when I make some on-site creative changes.
06Colophon
Hardware
| Booth | Raspberry Pi in a decorated 1.5 m wooden obelisk, touchscreen, Raspberry Pi HQ Camera (Sony IMX477, 12.3 MP, captured at full 4056 x 3040) |
|---|---|
| Hub | Intel NUC, running the show controller, digital twin simulation and the MQTT broker |
| Gazebo rig | 3 x 3 x 2 m frame; Waveshare RoArm-M3 Pro mounted inverted from the canopy, circularly-polarised 14dBi 5.8GHz antenna on the end |
| Arm executor | Raspberry Pi 3, USB to the arm (ArmLink JSON firmware protocol) |
| Transmitter | Raspberry Pi 3 (same one as the arm) composite video out into a TBS Unify Pro 5G8 HV, 5.8 GHz analogue video; PiFace Digital SPI relay gating the VTX battery line, second relay driving the amber beacon |
| Downlink | 5.8 GHz receiver into HDMI, captured by a GL.inet KVM on the booth LAN and fed live to the booth screen |
| Lighting | 2x Betopper LS30 moving heads (11 ch), 4x crappy Amazon RGBW washes (8 ch), 4x RGBW strips via DMX decoder, WLED-driven LED on the arm itself (8 segments), all on one 512-channel Art-Net universe |
| Network | Self-contained LAN. Sits behind a GL.inet x3000 Spitz AX router. This is plugged directly into the site ethernet, but for some reason the port has never been live. It's also set up as a client on the site WiFi and it's been so reliable that it was never worth chasing the ethernet connection. |
Software
Everything is Python except the collection site.
| Booth app | Python 3.13, PySide6 and QML, GPU-composited; skin-driven page flow, so each deployment is a self-contained theme directory |
|---|---|
| Camera daemon | picamd, with GStreamer preview over a shared-memory socket (NV12) and stills via picamera2 |
| Show controller | Ephemeris via Skyfield and JPL DE440, live JPL Horizons and Celestrak fetches; reachability via PyBullet; lighting via stupidArtnet at 30 Hz; audio via a GStreamer audiomixer |
| Arm executor | armd, pyserial only, with no kinematics on the Pi by design |
| Transmitter | sptx, using Pillow and numpy to write /dev/fb0 in RGB565, plus pifacedigitalio for the relays |
| Messaging | Mosquitto and paho-mqtt 2.x; every inter-service contract is a versioned MQTT topic |
| Crypto | AES-256-GCM (PyCA cryptography), fresh key per souvenir, held only in the QR code's URL fragment |
| Collection site | Vue 3 SPA, with decryption entirely client-side |
| Deployment | Ansible and Terraform, Tailscale, uv, publish-on-tag GitLab CI |