Phage

Phage (RS-232) – Redux

A Phage is a réseau d’art, not an objet but a network, a “behaviourable and a futurible1. 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.

2: Lescarboura, Austin (1920). Edison’s Views on Life and Death. An Interview with the Famous Inventor Regarding His Attempt to Communicate with the Next World. Scientific America, 30 October. Volume CXXIII, Number 18.

[No Face/Face…]
[No Face left / Face right…]
BEHAVIOUR:

FORM:

HERITÂGE:

SOFTWARE: [install & code below]

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

HARDWARE: [install & spec below]

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