_ii is a live visual and projection-control system. It runs a controller on
the laptop screen, sends visuals to a projector or second display, and exposes a
browser portal for mapping, live controls, media, and display management.
The usual live setup is:
- Debian machine connected to the projector by HDMI.
- Laptop display used for the controller.
- Phone, tablet, or laptop on the same network opened to the web portal.
- Optional SSH session for maintenance.
Use this when the machine is already installed and you just need to run a show.
-
Connect projector or LED processor to HDMI.
-
Connect the control device to the same network as the
_iimachine. -
SSH into the machine:
ssh dob@192.168.88.136
-
Start the full X11 show layout:
ii xstart
-
Open the web portal from any browser on the same network:
http://192.168.88.136:7777 -
In the portal, open
OUTPUTSand build the stage output plan:- choose the laptop panel as
CONTROLLER SCREEN - choose HDMI/projector as
PROJECTOR SCREEN - choose the mapping preset
- click
PREVIEW PLAN - click
APPLY TO STAGE
- choose the laptop panel as
-
Open
CTRLfor live performance controls. -
If the projector is black or windows land on the wrong screen:
ii restart x
Use this exact order on show day. It keeps routing predictable and avoids starting performance controls before outputs are assigned.
- Run
ii xstart. - Open portal
OUTPUTSfirst (beforeCTRL). - Set
CONTROLLER SCREENto laptop panel (eDP-1/LVDS-1). - Set
PROJECTOR SCREENto HDMI output. - Choose mapping preset.
- Click
PREVIEW PLANand verify controller/projector targets. - Click
APPLY TO STAGE. - Open
MAPand confirm geometry. - Open
ZONESand confirm each surface mode assignment. - Only then open
CTRLand start the performance.
Run the portal on the _ii machine:
python3 map_server.pyOpen it from the machine itself:
http://localhost:7777
Open it from another device on the network:
http://192.168.88.136:7777
Portal tabs:
| Tab | Use |
|---|---|
MAP |
Create and edit projection surfaces. Drag corners to warp zones. |
ZONES |
Quickly enable/disable zones and assign modes per surface. |
CTRL |
Live VJ controls: mode, palette, BPM, layer B, flash, blackout. |
MEDIA |
Upload video/image files and play video through the output stack. |
OUTPUTS |
Build and apply the stage output plan: controller screen, projector screen, mapping preset. |
HELP |
Shows portal URL, SSH command, IP addresses, display state, rescue commands, and one-click restart buttons (VIS, CTRL, WEB, X, ALL). |
The portal writes to control.json, mapping files in mappings/, and runtime
state files. These runtime files are intentionally ignored by git.
Recommended live display layout:
| Screen | Purpose | Typical connector |
|---|---|---|
| Laptop panel | _ii.py controller |
built-in LVDS-1 or eDP-1 |
| Projector | ii-VISUALS terminal visuals |
HDMI-1, HDMI-A-1, or similar |
The project now uses visuals_monitor: "auto-second" in config.json, so it
prefers the non-primary connected display instead of depending on a single HDMI
name.
Useful display checks:
DISPLAY=:0 xrandr --query
DISPLAY=:0 wmctrl -lG
ii statusThe OUTPUTS tab follows the same practical idea as professional mapper tools:
choose a screen, choose which content goes to it, choose a mapping preset, then
apply deliberately. It avoids one-click mirror/projector-only changes during a
show because those can hide the controller or strand the visuals on the wrong
display.
Projection mapping workflow (recommended):
- Open
OUTPUTS. - Pick controller screen, projector screen, and mapping preset.
- Click
PREVIEW PLAN. - Click
OPEN MAP EDITORto inspect or edit the selected mapping geometry. - In
MAP, drag corners for each surface and save. - In
ZONES, set mode per surface if needed. - Return to
OUTPUTSand clickAPPLY TO STAGE.
If the projector is connected but black:
-
Check
OUTPUTSin the web portal. -
Choose the laptop panel for
CONTROLLER SCREEN. -
Choose the HDMI output for
PROJECTOR SCREEN. -
Choose the correct mapping preset.
-
Click
PREVIEW PLAN, thenAPPLY TO STAGE. -
If still wrong, restart X mode:
ii restart x
The ii command at ~/bin/ii manages the live engine.
ii start # start tmux controller + TTY visuals
ii stop # stop visuals and controller
ii status # show running pieces
ii attach # attach to tmux controller
ii logs vis # follow terminal visual logs
ii logs web # follow portal logs
ii logs x # follow X11 startup logs
ii update # git pull latest code
ii watch 10 # auto-pull every 10 seconds
ii xstart # start laptop-controller/projector-visuals X11 mode
ii xstop # stop X11 mode and return to TTY visuals
ii restart x # restart the X11 show layout
ii restart vis # restart only visuals
ii restart ctrl # restart only controller
ii restart web # restart only web portalOne-command Debian update + smart restart:
scripts/update-and-restart.shOptional flags:
scripts/update-and-restart.sh --dry-run # show what would restart
scripts/update-and-restart.sh --force-x # always restart full X layoutAttach to the controller directly:
# on di.ii hotspot
ssh dob@10.42.0.1 -t "tmux attach -t ii"
# on venue network
ssh dob@192.168.88.136 -t "tmux attach -t ii"Before doors:
- Power the projector or processor first.
- Boot the
_iimachine. - Start X mode with
ii xstart. - Open
http://192.168.88.136:7777. - Use
OUTPUTSto apply the controller/projector/mapping stage plan. - Use
MAPorZONESto confirm the projection surfaces. - Use
CTRLto pick a starting mode and palette. - Keep
BLACKOUTavailable in the CTRL tab for emergencies.
During the show:
- Use
CTRLfor mode, palette, BPM, layer, flash, and blackout. - Use
ZONESif each mapped surface should show a different mode. - Use
MEDIAonly when you intentionally want video playback. - Use
HELPrestart buttons first (VIS,CTRL,WEB,X) before considering a full machine reboot.
After the show:
ii stopController plus autostarted terminal visuals:
python3 _ii.pyManual terminal visuals:
python3 window.py
python3 visuals.pyPygame fullscreen/windowed output:
python3 output.py --display 0
python3 output.py --windowed 1280 720
python3 output.py --map mappings/quad.json --vw 128 --vh 72Framebuffer mapper for direct /dev/fb0 output:
python3 fb_mapper.py --map mappings/split3.jsonAudio and OSC helpers:
python3 audio.py --list
python3 audio.py --sensitivity 1.5
python3 osc_server.py --port 7000Core terminal visuals use the Python standard library. Optional outputs and inputs need packages:
pip install pygame numpy sounddevice opencv-python python-oscUseful system tools:
sudo apt install wmctrl x11-xserver-utils tmux kitty openbox unclutter mpvInstall system services from the repo:
sudo ./install-services.shServices:
| Service | Role |
|---|---|
ii-visuals.service |
visuals.py on TTY1 |
ii-web.service |
map_server.py on port 7777 |
ii-ctrl.service |
_ii.py deck in tmux session ii |
ii-boot.service |
optional wrapper around ~/bin/ii start |
Service commands:
sudo systemctl restart ii-visuals
sudo systemctl restart ii-web
sudo systemctl restart ii-ctrl
sudo journalctl -fu ii-visuals| File | Role |
|---|---|
_ii.py |
curses controller, node graph evaluator, manual overrides |
visuals.py |
ANSI terminal visual renderer |
window.py |
launches and places the visuals terminal window |
map_server.py |
web portal for mapping, controls, media, outputs, help |
output.py |
pygame output with fullscreen/windowed modes |
fb_mapper.py |
direct framebuffer renderer |
audio.py |
microphone level, peak, and BPM writer |
osc_server.py |
OSC receiver writing to control.json |
nodes.py |
live automation graph, hot-reloaded by _ii.py |
node_lib.py |
signal nodes, camera/audio inputs, Art-Net outputs |
modes/ |
visual modes |
mappings/ |
projection surface layouts |
media/ |
uploaded media files, ignored by git |
live/ |
headless GLSL, ANSI, SuperCollider, ffmpeg, tmux helpers |
_ii.py evaluates nodes.py, merges node output, MIDI state, OSC/audio writes,
and manual overrides, then writes control.json.
Renderers read control.json:
visuals.pyrenders terminal visuals and writesstatus.json.output.pyrenders through pygame and writesstatus.json.fb_mapper.pyrenders directly to/dev/fb0.map_server.pyedits mapping files and updates live controls.
status.json feeds liveness and FPS back into the controller and portal.
| Key | Action |
|---|---|
0-9 |
jump to mode by number |
[ / ] |
previous / next mode |
Up / Down |
move selected control |
Left / Right |
adjust selected control |
Tab / V |
jump between main controls and node controls |
P |
next palette |
F |
next symbol set |
T |
tap BPM |
S |
toggle BPM sync |
X |
toggle layer B |
M |
lock / release current mode |
G |
cycle projection mapping |
A |
toggle auto-cycle |
B |
blackout |
Space |
toggle flash text |
Enter |
edit flash text |
C |
clear selected override |
W |
toggle visuals fullscreen |
, / . |
previous / next cue |
Z |
store current cue |
F1-F10 |
performance presets |
! |
panic reset + blackout |
Q |
quit |
| # | Mode |
|---|---|
| 0 | RAIN |
| 1 | WAVE |
| 2 | GLITCH |
| 3 | STROBE |
| 4 | LOGO |
| 5 | PULSE |
| 6 | TEXT |
| 7 | TUNNEL |
| 8 | PLASMA |
| 9 | VORTEX |
| 10 | GRID |
| 11 | PARTICLES |
| 12 | SCANNER |
| 13 | STORM |
| 14 | CUBE |
| 15 | SHOCKWAVE |
| 16 | NOISE |
| 17 | LIQUID |
| 18 | POSTER |
| 19 | MAPTST |
MAPTST is the safest mode for checking projection surfaces.
Palettes: STEEL, ACID, VOID, NEON, ULTRA, DEEP, BLOOD, EMBER.
Symbol sets: BLOCK, ASCII, DIGIT, GRID, SHAPES, MATH.
Primary, secondary, and accent colors can follow the active palette or be overridden manually from the controller.
Bundled mapping presets:
mappings/default.jsonfull screenmappings/split2.jsontwo vertical zonesmappings/quad.jsonfour zonesmappings/split3.jsonthree vertical zones
Use MAP in the portal for geometry. Use ZONES for fast mode assignment and
surface enable/disable during a show.
Edit nodes.py during a session. _ii.py hot-reloads it automatically.
Available node families include:
- clocks and generators:
Const,LFO,BeatLFO,Seq,Ramp,Noise - shaping:
Math,Clamp,Mix,Select,Hold,Scale,Gate - triggers:
BeatPulse,AudioTrigger - inputs:
AudioLevel,AudioPeak,CameraMotion,CameraBrightness,CameraPresence - outputs:
Out,IntOut,BoolOut,ArtNetOut,ArtNetRGB
Example:
from node_lib import *
MIC = AudioLevel(gain=9.0, smoothing=0.78)
CAM = CameraMotion(source='/dev/video4', fps=15, gain=7.0)
GRAPH = [
Out('bpm', Const(140)),
IntOut('mode', Seq([17, 18, 7, 8, 3, 9], beats=8)),
Out('glitch_intensity', Scale(CAM, out_min=0.08, out_max=1.0)),
BoolOut('flash_active', AudioTrigger(threshold=0.55, cooldown=8, gain=10.0)),
BoolOut('layer_b_enabled', MIC, threshold=0.28),
]osc_server.py listens on 0.0.0.0:7000 by default.
Native addresses:
/_ii/mode/_ii/mode_b/_ii/palette/_ii/bpm/_ii/blackout/_ii/master_dim/_ii/glitch/_ii/rain/_ii/wave/_ii/strobe/_ii/layer_b/_ii/layer_b_alpha/_ii/flash_text/_ii/flash/_ii/auto_cycle
Create osc_map.json in the project root to add or override address mappings.
Projector black, controller visible:
ii status
DISPLAY=:0 wmctrl -lG
ii restart xPortal does not open:
ii status
ii restart web
curl http://localhost:7777Wrong display names:
DISPLAY=:0 xrandr --queryVisuals are running but frozen:
ii restart xNeed a clean blank screen:
- Press
Bin the controller. - Or click
BLACKOUTin the portalCTRLtab.
The Debian machine runs a WiFi hotspot called di.ii. Its IP depends on the
network context:
| Context | Debian IP | Web portal |
|---|---|---|
Connected to di.ii hotspot |
10.42.0.1 |
http://10.42.0.1:7777 |
| Venue local network | 192.168.88.136 |
http://192.168.88.136:7777 |
The Debian machine usually has no internet when the hotspot is active. All code sync goes through the laptop, which bridges Debian and GitHub.
Before editing any code, sync with Debian to get its latest state:
cd /path/to/_ii
scripts/sync.shThis fetches Debian's commits and rebases any local work on top. Skipping this causes diverged branches and manual conflict resolution.
The full cycle for any code change:
# 1. Sync before touching anything
scripts/sync.sh
# 2. Edit files as needed (modes, nodes, cues, config, etc.)
# 3. Commit, push to GitHub, and deploy to Debian in one command
scripts/sync.sh push "describe the change"sync.sh push does all of this automatically:
- stages and commits all changes
- pushes to GitHub (skips gracefully if no internet)
- pushes directly to Debian over SSH
- resets Debian's worktree to the new commit
- restarts only the affected components (
vis,ctrl,web)
If Debian is not reachable when you push, push to it later:
scripts/sync.sh push-debianCheck sync state at any time:
scripts/sync.sh statusscripts/sync.sh replaces the old git-sync.sh + update-and-restart.sh
two-step. The old scripts still work but are not the recommended path.
Runtime files (control.json, status.json, display_assign.json,
cues.json, media uploads, Python caches) are git-ignored and never committed.
_ii/
├── _ii.py
├── visuals.py
├── output.py
├── window.py
├── map_server.py
├── fb_mapper.py
├── audio.py
├── osc_server.py
├── architecture.py
├── node_lib.py
├── nodes.py
├── modes/
├── mappings/
├── media/
├── live/
├── innux/
├── docs/
├── config.json
└── README.md