Phage (RS-232) – Redux

A Phage is a réseau d’art, not an objet but a network, a “behaviourable and a futurible”1. As a speculative device, inspired by the membrane-penetrating abilities of bacteriophage viruses, they are collaborative instruments that allow the manipulation of virtual objects. Something between a game controller and a planchette from a Séance, each phage integrates microcontrollers and biometric and environmental sensors and operates as a networked device, allowing several phage to work together for collaborative control of virtual artefacts.
Like the planchette, the phage allows communication with entities from the beyond. Thomas Edison reflected building an early Phage, some kind of “valve”, later to become the ‘spirit phone’ or ‘necrophone’.”I have been thinking for some time of a machine or apparatus which could be operated by personalities which have passed on to another existence or sphere.”2
To date Phage applications have included: audience control of AI driven flocking “boids”; the incorporation of biometric sensors to access users’ bodily states whilst interacting with virtual objects; and the use of sentiment analysis to incorporate emotional qualities. As well as being an instrument for audience engagement beyond the gallery they have had a catalytic impact on Fulldome projection environments. Formerly known as the Planetarium, the Fulldome has been undergoing a disciplinary metamorphosis, it awoke one morning from uneasy astronomical dreams and found itself transformed from a planetarium into an ‘omniarium’- no longer just planets but a spherical perspective that provide immersive, performative, and polysensory experiences. The Phage is an instrument to breach the Fulldome membrane.

The Phage, 2023: Reworked montage of images captured from the E/M/D/L – European Mobile Dome Lab Murmuration performances from 2015 at the Society for Arts and Technology in Montreal, incorporating newly constructed AI-generated artefacts and audience members using phage – collaborative physical instruments that enable the manipulation of virtual objects. Published in: Phillips, M. Digital Ectoplasm and the Infinite Architecture of the Fulldome. Ghost Stories: Architecture and the Intangible. Edited by Baldwin, P. Volume94, Issue4, July/August 2024, Pages 110-117.
This Phage is fitted with an RS-232 port which remembers efforts whilst at the Slade School of Art, UCL in 1984, to facilitate a networking infrastructure for some of Roy Ascott’s telematic activities, such as Aspects of Gaia (1989). Built around access to UCL’s terminals our efforts to instal an RS-232 socket in the Slade were scuppered by the then Slade Professor, who simply could not understand why we couldn’t ‘just meet people and have a cup of coffee with them’.
Content on the video stream on this Phage is sourced from original 3D objects, constructed in Aldus Super 3D, and transmitted over these early networks. Many disappeared into the ether and were never recovered. This Phage imagines their existence beyond the networks membrane, somewhere still circulating as code. It wasn’t just the immateriality of code that dissolved the hardware of the Objet d’art: it was the ability for its transubstantiation through electronic networks that transformed the Objet into a behaviour and unleashed the futurible. Asynchronous liberation through the network shattered and redistributed the Objet still further, for where two or three were gathered on a modem link, there we were among them, but not necessarily all at the same time.
The random latency in the networks generated a new kind of space framed by the chirping machine sounds of modems and the soft screeching of dot matrix printers, accessing this networked terrain created a tangible feeling of a vast domain of potential.
1: Ascott, R. (1968). Behaviourables and Futurables. Control, London, 1970, Nº 5.
[No Face/Face…] [No Face left / Face right…]HERITÂGE:
Raspberry Pi OS (Bookworm) / Operating system – Debian Linux Various open source raspberrypi.com
Python 3 PSF Licence (open source) python.org
OpenCV 4 Computer vision – face detection Apache 2.0 opencv.org
Haar Cascade Classifier Face detection algorithm Intel open source Included with OpenCV
VLC Media Player (cvlc) Video and audio playback GPL v2 videolan.org
evdev USB input device reading MIT Licence python-evdev.readthedocs.io
hidapi HID device communication BSD Licence github.com/libusb/hidapi
devilspie2 VLC window decoration removal GPL v2 github.com/gusnan/devilspie2
xdotool Taskbar hiding MIT Licence semicomplete.com/projects/xdotool
alsamixer / ALSA Audio device control GPL v2 alsa-project.org
FFmpeg Video conversion on Mac LGPL/GPL ffmpeg.org
Raspberry Pi 4 Model B 2GB 2GB RAM
Waveshare 4″ HDMI LCD (C) 720×720 Micro HDMI + USB power
OV9732 USB Webcam Realtek UVC
Exqufood USB Single Key Button YYWL keyboard
MicroSD Card 16GB+ Class 10
5V 3A USB-C power supply 3A
USB Speaker UACDemoV1.0
Micro HDMI to HDMI cable
Dev: [images in process]
1: Install:
https://www.raspberrypi.com/software/
Raspberry Pi OS (64-bit) Bookworm Desktop version
1a: Waveshare HDMI Display
Update system
sudo apt update && sudo apt upgrade -y
Switch to X11
sudo raspi-config
Go to: Advanced Options, Wayland, W1 X11, OK, Finish, Reboot
Edit /boot/firmware/config.txt
sudo nano /boot/firmware/config.txt
config.txt
# For more options and information see
# http://rptl.io/configtxt
#dtparam=i2c_arm=on
#dtparam=i2s=on
#dtparam=spi=on
dtparam=audio=on
camera_auto_detect=1
display_auto_detect=1
auto_initramfs=1
# Enable DRM VC4 V3D driver
dtoverlay=vc4-kms-v3d
max_framebuffers=2
disable_fw_kms_setup=1
arm_64bit=1
disable_overscan=1
arm_boost=1
[cm4]
otg_mode=1
[cm5]
dtoverlay=dwc2,dr_mode=host
[all]
1b Screen Rotation (180 degrees)
Permanent rotation: xrandr.
DISPLAY=:0 xrandr –output HDMI-2 –rotate inverted
1c: Disable Screen Blanking
sudo nano /etc/xdg/lxsession/LXDE-pi/autostart
Add at the bottom:
@xset s off
@xset -dpms
@xset s noblank
@xrandr –output HDMI-2 –rotate inverted
2: Software Installation
# Install all required packages
sudo apt install -y python3-opencv opencv-data vlc xdotool devilspie2 wmctrl
# Install Python libraries
sudo pip3 install evdev –break-system-packages
sudo pip3 install hidapi –break-system-packages
Verify Camera
v4l2-ctl –list-devices
# Should show: USB Camera: USB Camera … /dev/video0
Camera Focus
DISPLAY=:0 python3 -c “
import cv2
cap = cv2.VideoCapture(0)
cap.set(cv2.CAP_PROP_FRAME_WIDTH, 640)
cap.set(cv2.CAP_PROP_FRAME_HEIGHT, 480)
print(‘Press Q to quit’)
while True:
ret, frame = cap.read()
if not ret: break
cv2.imshow(‘Camera Preview – Press Q to quit’, frame)
if cv2.waitKey(1) & 0xFF == ord(‘q’): break
cap.release()
cv2.destroyAllWindows()
“
2a: Verify Cascade File
find / -name ‘haarcascade_frontalface_default.xml’ 2>/dev/null
# Must show: /usr/share/opencv4/haarcascades/haarcascade_frontalface_default.xml
2b: Verify Audio Card Name
aplay -l
# USB speaker shows as: UACDemoV1.0
# Use name hw:UACDemoV10,0 in script — NOT hw:2,0 (number changes on reboot)
4c: Set Up devilspie2 — Removes VLC Title Bar
mkdir -p ~/.config/devilspie2
nano ~/.config/devilspie2/vlc.lua
Paste:
if (get_application_name() == “VLC media player”) then
undecorate_window()
end
Save Ctrl+O, Enter, Ctrl+X then add to autostart:
nano /home/pi/.config/autostart/devilspie2.desktop
Paste:
[Desktop Entry]
Type=Application
Name=Devilspie2
Exec=devilspie2
X-GNOME-Autostart-enabled=true
3: Video File Conversion
Convert on Mac — using FFmpeg (open source, ffmpeg.org)
brew install ffmpeg
# Convert to 720×720 H.264 MP4
ffmpeg -i ~/Desktop/yourvideo.mp4 \
-vf “scale=720:720:force_original_aspect_ratio=increase,crop=720:720” \
-c:v libx264 -preset fast -crf 20 -c:a aac -b:a 128k ~/Desktop/video1.mp4
Copy to Pi
mkdir -p /home/pi/videos
# Run on Mac — use full path and actual IP address
scp ~/Desktop/video1.mp4 pi@192.168.1.235:/home/pi/videos/
scp ~/Desktop/video2.mp4 pi@192.168.1.235:/home/pi/videos/
4: Face Detection Sensitivity
OpenCV’s Haar Cascade algorithm – Four settings control sensitivity:
Setting / Value / + / –
SCALE_FACTOR 1.05 Faster but misses more faces Slower but detects more faces
MIN_NEIGHBOURS 6 Fewer false positives More detections, more false positives
MIN_FACE_SIZE (80,80) Ignores distant/small faces Detects smaller/more distant faces
FACE_THRESHOLD 5 frames Slower to trigger video2 Triggers video2 faster
NO_FACE_THRESHOLD 5 frames Slower to return to video1 Returns to video1 faster
Common Adjustments
- False detections from background – increase MIN_NEIGHBOURS to 7 or 8
- Not detecting faces far away – decrease MIN_FACE_SIZE to (50,50)
- Switching too quickly – increase FACE_THRESHOLD and NO_FACE_THRESHOLD to 10
- Switching too slowly – decrease FACE_THRESHOLD and NO_FACE_THRESHOLD to 3
- Missing faces in low light – decrease SCALE_FACTOR to 1.03
Camera View
DISPLAY=:0 python3 -c “
import cv2
cap = cv2.VideoCapture(0)
cap.set(cv2.CAP_PROP_FRAME_WIDTH, 640)
cap.set(cv2.CAP_PROP_FRAME_HEIGHT, 480)
while True:
ret, frame = cap.read()
if not ret: break
cv2.imshow(‘Camera Preview – Press Q to quit’, frame)
if cv2.waitKey(1) & 0xFF == ord(‘q’): break
cap.release()
cv2.destroyAllWindows()
“
5: Python Script
Process:
Powers on – Taskbar hidden, video1.mp4 starts immediately, loops
5 second delay – Camera stabilises, 20 frames flushed to clear buffer
Face appears close – After 1 second (5 frames) switches to video2.mp4
Face stays visible – video2.mp4 loops continuously
Face disappears – After 1 second (5 frames) switches back to video1.mp4
Distant person – Ignored — MIN_FACE_SIZE (80,80) filters them out
Terminal visible – Shows script output during video switches
VLC window – 720×720 no decoration — fills screen without true fullscreen
Every event logged – Timestamped to /home/pi/face_log.txt
Code:
nano /home/pi/face_video.py
# Paste script, Save: Ctrl+O Enter Ctrl+X
chmod +x /home/pi/face_video.py
Complete Script
#!/usr/bin/env python3
import cv2
import subprocess
import time
import os
import signal
import sys
import logging
# ─── Logging setup ────────────────────────────────
logging.basicConfig(
filename=’/home/pi/face_log.txt’,
level=logging.INFO,
format=’%(asctime)s – %(message)s’,
datefmt=’%Y-%m-%d %H:%M:%S’
)
logging.info(“Script started”)
# ─── Configuration ────────────────────────────────
VIDEO_DIR = “/home/pi/videos”
VIDEO_FILES = [
os.path.join(VIDEO_DIR, “video1.mp4”), # idle video
os.path.join(VIDEO_DIR, “video2.mp4”), # triggered video
]
DETECT_WIDTH = 320
DETECT_HEIGHT = 240
# ─── Face detection sensitivity ───────────────────
# SCALE_FACTOR: lower = more sensitive but slower (1.01-1.3)
SCALE_FACTOR = 1.05
# MIN_NEIGHBOURS: lower = more sensitive, more false positives (1-10)
MIN_NEIGHBOURS = 6
# MIN_FACE_SIZE: smaller = detects more distant faces
MIN_FACE_SIZE = (80, 80)
# FACE_THRESHOLD: frames face must be seen before switching to video2
FACE_THRESHOLD = 5 # 5 x 0.2s = 1 second
# NO_FACE_THRESHOLD: frames face must be gone before switching back
NO_FACE_THRESHOLD = 5 # 5 x 0.2s = 1 second
DISPLAY = “:0”
# ─── State ────────────────────────────────────────
current_video = 0
vlc_process = None
playing = False
face_count = 0
no_face_count = 0
# ─── Load face detector ───────────────────────────
# Uses OpenCV Haar Cascade classifier (open source, Apache 2.0)
cascade_path = “/usr/share/opencv4/haarcascades/haarcascade_frontalface_default.xml”
face_cascade = cv2.CascadeClassifier(cascade_path)
if face_cascade.empty():
print(“ERROR: Could not load face cascade”)
sys.exit(1)
print(“Face detector loaded”)
# ─── Camera setup ─────────────────────────────────
cap = cv2.VideoCapture(0)
cap.set(cv2.CAP_PROP_FRAME_WIDTH, DETECT_WIDTH)
cap.set(cv2.CAP_PROP_FRAME_HEIGHT, DETECT_HEIGHT)
if not cap.isOpened():
print(“ERROR: Could not open camera”)
sys.exit(1)
print(“Camera started”)
# ─── Play video ───────────────────────────────────
def play_video(index):
global vlc_process, playing, current_video
stop_video()
path = VIDEO_FILES[index]
if not os.path.exists(path):
print(f”WARNING: Video not found: {path}”)
return
env = os.environ.copy()
env[“DISPLAY”] = DISPLAY
vlc_process = subprocess.Popen([
“cvlc”,
“–no-osd”,
“–no-video-title-show”,
“–no-embedded-video”,
“–loop”,
“–aout=alsa”,
“–alsa-audio-device=hw:UACDemoV10,0”,
“–gain=0.8”, # volume: 0.5=50% 0.8=80% 1.0=100% 1.5=150%
“–video-x=0”,
“–video-y=0”,
“–width=720”,
“–height=720”,
“–no-video-deco”,
“–no-qt-name-in-title”,
“–qt-minimal-view”,
“–no-qt-fs-controller”,
path
], env=env)
playing = True
current_video = index
print(f”Playing video {index + 1}: {path}”)
# ─── Stop video ───────────────────────────────────
def stop_video():
global vlc_process, playing
if vlc_process is not None:
vlc_process.terminate()
try:
vlc_process.wait(timeout=2)
except subprocess.TimeoutExpired:
vlc_process.kill()
vlc_process = None
playing = False
# ─── Graceful shutdown ────────────────────────────
def shutdown(sig, frame):
print(“Shutting down…”)
logging.info(“Script stopped”)
stop_video()
cap.release()
sys.exit(0)
signal.signal(signal.SIGTERM, shutdown)
signal.signal(signal.SIGINT, shutdown)
# ─── Hide taskbar ─────────────────────────────────
os.system(“DISPLAY=:0 xdotool search –class ‘lxpanel’ windowunmap”)
# ─── Start idle video immediately ─────────────────
print(“Starting idle video 1…”)
play_video(0)
# Wait for camera to stabilise
print(“Waiting for camera to stabilise…”)
time.sleep(5)
# Flush camera buffer — discard first 20 frames
print(“Flushing camera buffer…”)
for _ in range(20):
cap.read()
# ─── Main loop ────────────────────────────────────
print(“Starting face detection loop…”)
while True:
ret, frame = cap.read()
if not ret:
time.sleep(0.5)
continue
gray = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY)
faces = face_cascade.detectMultiScale(
gray,
scaleFactor=SCALE_FACTOR,
minNeighbors=MIN_NEIGHBOURS,
minSize=MIN_FACE_SIZE
)
face_detected = len(faces) > 0
if face_detected:
no_face_count = 0
face_count += 1
if face_count >= FACE_THRESHOLD and current_video == 0:
print(“Face detected – switching to video 2”)
logging.info(“Face detected – switched to video 2”)
play_video(1)
else:
face_count = 0
no_face_count += 1
if no_face_count >= NO_FACE_THRESHOLD and current_video == 1:
print(“Face lost – switching back to video 1”)
logging.info(“Face lost – switched back to video 1”)
play_video(0)
time.sleep(0.2)
11. USB Shutdown Button
Hardware Setup
- Program button to send letter ‘a’ using Windows config software
- Plug into any USB port on Pi
- Detected as /dev/input/by-id/usb-YYWL_YYWL-Keyboard-event-kbd
Install evdev library
sudo pip3 install evdev –break-system-packages
Create Shutdown Script
nano /home/pi/shutdown_button.py
#!/usr/bin/env python3
# Uses evdev library (open source, MIT licence)
import subprocess
from evdev import InputDevice, ecodes
# USB button device — check /dev/input/by-id/ if this path changes
device = InputDevice(‘/dev/input/by-id/usb-YYWL_YYWL-Keyboard-event-kbd’)
print(f’Listening on: {device.name}’)
print(‘Press button to shutdown…’)
for event in device.read_loop():
if event.type == ecodes.EV_KEY:
if event.value == 1: # key down
print(‘Button pressed – shutting down…’)
subprocess.run([‘sudo’, ‘shutdown’, ‘-h’, ‘now’])
break
Configure sudoers
sudo visudo
# Add at bottom:
pi ALL=(ALL) NOPASSWD: /sbin/shutdown
Add to Autostart
nano /home/pi/.config/autostart/shutdown-button.desktop
Paste:
[Desktop Entry]
Type=Application
Name=Shutdown Button
Exec=sudo /usr/bin/python3 /home/pi/shutdown_button.py
X-GNOME-Autostart-enabled=true
6: Autoboot on Power
Three autostart entries handle automatic startup — face detection script, shutdown button, and devilspie2 window decorator:
Face Detection Autostart
nano /home/pi/.config/autostart/face-video.desktop
Paste:
[Desktop Entry]
Type=Application
Name=Face Video
Exec=lxterminal –title=”Face Detection” –geometry=720×720 -e “python3 /home/pi/face_video.py”
X-GNOME-Autostart-enabled=true
Shutdown Button Autostart
nano /home/pi/.config/autostart/shutdown-button.desktop
Paste:
[Desktop Entry]
Type=Application
Name=Shutdown Button
Exec=sudo /usr/bin/python3 /home/pi/shutdown_button.py
X-GNOME-Autostart-enabled=true
devilspie2 Autostart
nano /home/pi/.config/autostart/devilspie2.desktop
Paste:
[Desktop Entry]
Type=Application
Name=Devilspie2
Exec=devilspie2
X-GNOME-Autostart-enabled=true
Reboot to test all three:
sudo reboot
7. Logs and Maintenance
View Face Detection Log
# View full log
cat /home/pi/face_log.txt
# Watch live
tail -f /home/pi/face_log.txt
# Count total face detections
grep ‘Face detected’ /home/pi/face_log.txt | wc -l
Log Rotation — Prevent Large Log Files
sudo nano /etc/logrotate.d/face-video
Paste:
/home/pi/face_log.txt {
weekly
rotate 4
compress
missingok
notifempty
}
System Updates
sudo apt update && sudo apt upgrade -y
sudo reboot
Safe Shutdown
# Press USB button — or via SSH:
sudo shutdown -h now
# Wait for green LED to stop before removing power














You must be logged in to post a comment.