Building a Client for Mr. Pumpkin
This guide explains how to build a client application to control Mr. Pumpkin remotely. Whether you’re integrating it into a home automation system, building a Halloween display, or creating an interactive performance, this document will help you get started.
Overview
Mr. Pumpkin is a controllable animated jack-o’-lantern face that listens for commands over a network connection. You can:
- Set expressions (happy, sad, angry, scared, surprised, neutral, sleeping)
- Trigger animations (blink, wink, eye rolls)
- Control gaze direction (where the eyes look)
- Adjust eyebrows (raise, lower, individual control)
- Move the head (turn left/right, tilt up/down)
- Animate the nose (wiggle, twitch, scrunch)
- Record and playback sequences (timeline system for complex performances)
All communication happens through simple text commands sent over TCP sockets (port 5000) or WebSocket connections (port 5001).
Connection Basics
Mr. Pumpkin supports two connection methods:
TCP Socket Connection (Port 5000)
Protocol: TCP socket
Port: 5000
Host: localhost (or the IP address of the machine running Mr. Pumpkin)
Connection Style: One connection per command
Each command follows this pattern:
- Connect to the server
- Send your command as UTF-8 text
- Signal end of command (call
shutdown(SHUT_WR)or close write side) - Read response (if any)
- Close the connection
Important: Use one connection per command. Don’t try to reuse connections.
WebSocket Connection (Port 5001)
Protocol: WebSocket
Port: 5001
Host: ws://localhost:5001
Connection Style: Persistent connection—send multiple commands without reconnecting
Requirements: The websockets library must be installed (pip install websockets). If not available, the WebSocket server will be disabled.
WebSocket connections are persistent. Send multiple commands on the same connection, making them ideal for interactive control and streaming updates.
Which Should I Use?
- TCP (Port 5000): Best for simple scripts, one-off commands, and fire-and-forget operations
- WebSocket (Port 5001): Best for interactive applications, ongoing control sessions, real-time streaming, and reducing connection overhead when sending many commands
Quick Start
Here’s a minimal Python example to send an expression:
import socket
def send_command(command):
client = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
client.connect(('localhost', 5000))
client.send(command.encode('utf-8'))
client.shutdown(socket.SHUT_WR) # Signal end of command
response = client.recv(1024).decode('utf-8')
client.close()
return response
# Make the pumpkin happy
send_command('happy')
That’s it! Five lines and you’re controlling a pumpkin face.
WebSocket Connection
WebSocket provides a persistent connection that lets you send multiple commands without reconnecting. This is more efficient for interactive applications and real-time control.
Prerequisites
Install the websockets library on the server:
pip install websockets
Then restart Mr. Pumpkin to enable WebSocket support.
Python WebSocket Example
import asyncio
import websockets
async def pumpkin_session():
async with websockets.connect('ws://localhost:5001') as ws:
# Send multiple commands on one persistent connection
await ws.send('happy')
await ws.send('blink')
await ws.send('gaze 45 30')
# Commands that return responses
await ws.send('timeline_status')
response = await ws.recv()
print(response)
await ws.send('list')
recordings = await ws.recv()
print(recordings)
asyncio.run(pumpkin_session())
Key Differences from TCP:
- One connection handles many commands (no reconnecting)
- Responses only sent for commands that return data (fire-and-forget commands like
happyorblinkget no response) - Perfect for streaming control or interactive applications
Node.js WebSocket Example
const WebSocket = require('ws');
const ws = new WebSocket('ws://localhost:5001');
ws.on('open', () => {
// Send multiple commands
ws.send('happy');
ws.send('blink');
ws.send('timeline_status');
});
ws.on('message', (data) => {
console.log('Response:', data.toString());
});
ws.on('error', (error) => {
console.error('WebSocket error:', error);
});
Install the ws package first:
npm install ws
Interactive Control Pattern
WebSocket excels at continuous, interactive control:
import asyncio
import websockets
async def interactive_pumpkin():
async with websockets.connect('ws://localhost:5001') as ws:
# Stream of control commands
emotions = ['happy', 'surprised', 'scared', 'neutral']
for emotion in emotions:
await ws.send(emotion)
await asyncio.sleep(2) # 2 seconds between expressions
await ws.send('blink')
await asyncio.sleep(1)
# Check status at the end
await ws.send('timeline_status')
status = await ws.recv()
print(f"Final status: {status}")
asyncio.run(interactive_pumpkin())
Upload Timeline via WebSocket
WebSocket uses a simpler single-message format for timeline uploads (unlike TCP’s multi-step handshake):
import asyncio
import websockets
import json
async def upload_timeline_ws(filename, timeline_data):
async with websockets.connect('ws://localhost:5001') as ws:
# Single message: command + filename + JSON content
json_str = json.dumps(timeline_data)
message = f"upload_timeline {filename} {json_str}"
await ws.send(message)
# Wait for response
response = await ws.recv()
print(response) # "OK Saved to <filename>.json"
# Usage
timeline = {
"version": "1.0",
"duration_ms": 3000,
"commands": [
{"time_ms": 0, "command": "happy"},
{"time_ms": 1000, "command": "blink"},
{"time_ms": 2000, "command": "neutral"}
]
}
asyncio.run(upload_timeline_ws('my_show', timeline))
Note: WebSocket upload is simpler than TCP—just one message with the full command, filename, and JSON content inline.
Command Reference
Expression Commands
Set the overall facial expression. These commands return no response (fire-and-forget).
send_command('neutral') # Default neutral face
send_command('happy') # Smiling
send_command('sad') # Frowning
send_command('angry') # Angry/scowling
send_command('surprised') # Wide-eyed surprise
send_command('scared') # Frightened expression
send_command('sleeping') # Eyes closed, relaxed
Animation Commands
Trigger short animations. These also return no response.
send_command('blink') # Both eyes blink
send_command('wink_left') # Left eye winks
send_command('wink_right') # Right eye winks
send_command('roll_clockwise') # Eyes roll clockwise
send_command('roll_counterclockwise') # Eyes roll counter-clockwise
Gaze Control
Control where the eyes look. Angles range from -90° to +90°.
0, 0= straight ahead (center)- Positive X = right, Negative X = left
- Positive Y = up, Negative Y = down
# Both eyes look in the same direction
send_command('gaze 0 0') # Look straight ahead
send_command('gaze 45 30') # Look up and to the right
send_command('gaze -90 0') # Look far left
# Independent eye control (cross-eyed, wall-eyed, etc.)
send_command('gaze -90 0 90 0') # Left eye left, right eye right
send_command('gaze 0 45 0 -45') # Left eye up, right eye down
Eyebrow Control
Adjust eyebrow positions for added expression.
# Preset movements
send_command('eyebrow_raise') # Raise both eyebrows
send_command('eyebrow_lower') # Lower both eyebrows
send_command('eyebrow_raise_left') # Raise left eyebrow only
send_command('eyebrow_lower_left') # Lower left eyebrow only
send_command('eyebrow_raise_right') # Raise right eyebrow only
send_command('eyebrow_lower_right') # Lower right eyebrow only
send_command('eyebrow_reset') # Reset both to neutral
# Precise numeric control
send_command('eyebrow 50') # Set both eyebrows to offset +50
send_command('eyebrow_left -30') # Set left eyebrow to offset -30
send_command('eyebrow_right 20') # Set right eyebrow to offset +20
Head Movement
Move or tilt the entire head.
send_command('turn_left') # Turn left by default amount (50px)
send_command('turn_right 100') # Turn right by 100 pixels
send_command('turn_up 75') # Tilt up by 75 pixels
send_command('turn_down') # Tilt down by default amount
send_command('center_head') # Return to center position
Nose Animation
Animate the nose for extra character.
send_command('wiggle_nose') # Wiggle with default magnitude
send_command('twitch_nose 100') # Twitch with magnitude 100
send_command('scrunch_nose 50') # Scrunch with magnitude 50
send_command('reset_nose') # Return nose to neutral
Projection Offset
Fine-tune the projection alignment (useful for physical setup).
send_command('projection_reset') # Reset to default position
send_command('jog_offset 10 -5') # Nudge projection by dx=10, dy=-5
send_command('set_offset 100 50') # Set absolute offset x=100, y=50
Recording Commands
Capture a sequence of commands to replay later.
# Start recording
response = send_command('record start')
# Response: "OK Recording started"
# ... send commands to record ...
send_command('happy')
send_command('blink')
send_command('gaze 45 0')
# Stop and save
response = send_command('record stop my_sequence')
# Response: "OK Saved to my_sequence.json"
# Cancel recording without saving
response = send_command('record cancel')
# Response: "OK Recording cancelled"
# Check recording status
response = send_command('record status')
# Response: JSON like {"is_recording": true, "command_count": 3, "duration_ms": 1500}
Playback Commands
Play back recorded sequences.
# Play a recording
response = send_command('play my_sequence')
# Response: "OK Playing my_sequence.json (5000ms)"
# Control playback
response = send_command('pause')
# Response: "OK Paused at 1234ms"
response = send_command('resume')
# Response: "OK Resumed from 1234ms"
response = send_command('stop')
# Response: "OK Playback stopped"
# Seek to specific time
response = send_command('seek 2500')
# Response: "OK Seeked to 2500ms"
# Check playback status
response = send_command('timeline_status')
# Response: JSON with state, filename, position_ms, duration_ms, is_playing
File Management
Manage your recorded timeline files.
# List all recordings
response = send_command('list')
# Response: JSON array like [{"filename": "show.json", "command_count": 42, "duration_ms": 15000}, ...]
# Delete a recording
response = send_command('delete_recording old_sequence')
# Response: "OK Deleted old_sequence.json"
# Rename a recording
response = send_command('rename_recording old_name new_name')
# Response: "OK Renamed old_name.json to new_name.json"
# Download timeline as JSON
json_content = send_command('download_timeline my_sequence')
# Response: Raw JSON content of the timeline file
# Clear all state
response = send_command('reset')
# Response: "OK Reset complete"
Upload Timeline
Upload a pre-made timeline file. The protocol differs between TCP and WebSocket.
TCP Upload (Multi-Step Protocol)
import socket
import json
def upload_timeline(filename, json_content):
client = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
client.connect(('localhost', 5000))
# Step 1: Send upload command
client.send(f"upload_timeline {filename}\n".encode('utf-8'))
# Step 2: Wait for READY signal
response = client.recv(1024).decode('utf-8').strip()
if response != "READY":
raise Exception(f"Expected READY, got: {response}")
# Step 3: Send JSON content
client.send(json_content.encode('utf-8'))
client.send(b"\n")
# Step 4: Send end marker
client.send(b"END_UPLOAD\n")
# Step 5: Read final response
response = client.recv(1024).decode('utf-8').strip()
client.close()
return response
# Usage
with open('my_timeline.json', 'r') as f:
timeline_data = f.read()
response = upload_timeline('uploaded_show', timeline_data)
WebSocket Upload (Single Message)
WebSocket uses a simpler single-message format:
import asyncio
import websockets
import json
async def upload_timeline_ws(filename, json_content):
async with websockets.connect('ws://localhost:5001') as ws:
# Single message with inline JSON
message = f"upload_timeline {filename} {json_content}"
await ws.send(message)
response = await ws.recv()
return response
# Usage
with open('my_timeline.json', 'r') as f:
timeline_data = f.read()
response = asyncio.run(upload_timeline_ws('uploaded_show', timeline_data))
Reading Responses
Commands return three types of responses:
1. No Response (Fire-and-Forget)
Expression and animation commands typically return empty strings. Just send and forget.
2. Text Response
Recording and playback commands return text status messages:
OK ...— Command succeededERROR ...— Command failed with description
response = send_command('play missing_file')
if response.startswith('ERROR'):
print(f"Failed: {response}")
3. JSON Response
Status query commands return JSON data:
import json
response = send_command('timeline_status')
data = json.loads(response)
print(f"State: {data['state']}")
print(f"Position: {data['position_ms']}ms")
print(f"Playing: {data['is_playing']}")
Complete Python Example
The repository includes a complete working example: client_example.py
This interactive client demonstrates:
- All command types
- Response handling
- Timeline upload protocol
- User-friendly command-line interface
Run it:
python client_example.py
Key snippet from client_example.py:
def send_command(command: str):
"""Send command and handle response"""
try:
client = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
client.connect(('localhost', 5000))
client.send(command.encode('utf-8'))
client.shutdown(socket.SHUT_WR)
# Read response for status commands
if command in ["record status", "list", "timeline_status"]:
response = client.recv(4096).decode('utf-8').strip()
client.close()
return json.loads(response)
else:
response = client.recv(1024).decode('utf-8').strip()
client.close()
return response
except Exception as e:
print(f"Error: {e}")
Using Other Languages
Mr. Pumpkin speaks TCP, so any language with socket support works.
Node.js Example
const net = require('net');
function sendCommand(command) {
return new Promise((resolve, reject) => {
const client = net.createConnection({ port: 5000 }, () => {
client.write(command);
client.end(); // Signal end of command
});
let response = '';
client.on('data', (data) => {
response += data.toString();
});
client.on('end', () => {
resolve(response);
});
client.on('error', reject);
});
}
// Use it
sendCommand('happy').then(response => console.log(response));
Bash / netcat Example
# Simple expression change
echo "happy" | nc localhost 5000
# Get status
echo "timeline_status" | nc localhost 5000
# Save to variable
RESPONSE=$(echo "record status" | nc localhost 5000)
echo $RESPONSE
C# Example
using System.Net.Sockets;
using System.Text;
string SendCommand(string command)
{
using var client = new TcpClient("localhost", 5000);
using var stream = client.GetStream();
// Send command
byte[] data = Encoding.UTF8.GetBytes(command);
stream.Write(data, 0, data.Length);
client.Client.Shutdown(SocketShutdown.Send);
// Read response
data = new byte[1024];
int bytes = stream.Read(data, 0, data.Length);
return Encoding.UTF8.GetString(data, 0, bytes);
}
// Use it
SendCommand("happy");
Advanced: Timeline Files
Timeline files are JSON documents that store recorded command sequences with precise timing.
Timeline Format
{
"version": "1.0",
"created_at": "2024-01-15T10:30:00Z",
"duration_ms": 5000,
"commands": [
{
"time_ms": 0,
"command": "happy"
},
{
"time_ms": 1000,
"command": "blink"
},
{
"time_ms": 2500,
"command": "gaze 45 30"
}
]
}
Creating Timelines Programmatically
You can create timeline files without recording:
import json
from datetime import datetime
timeline = {
"version": "1.0",
"created_at": datetime.utcnow().isoformat() + "Z",
"duration_ms": 5000,
"commands": [
{"time_ms": 0, "command": "neutral"},
{"time_ms": 500, "command": "eyebrow_raise"},
{"time_ms": 1000, "command": "happy"},
{"time_ms": 2000, "command": "blink"},
{"time_ms": 3000, "command": "gaze 45 0"},
{"time_ms": 5000, "command": "neutral"}
]
}
# Save and upload
with open('custom_sequence.json', 'w') as f:
json.dump(timeline, f, indent=2)
# Upload to server
upload_timeline('custom_sequence', json.dumps(timeline))
Use Cases
- Home Automation: Trigger expressions based on doorbell, weather, or time
- Halloween Displays: Synchronized shows with music and lighting
- Interactive Performances: React to audience input or sensor data
- Testing: Automated test sequences for development
- Art Installations: Looping ambient behaviors
Troubleshooting
Connection Refused
Problem: Connection refused error when trying to connect.
Solutions:
- Verify Mr. Pumpkin is running:
python pumpkin_face.py - Check if port 5000 is open:
netstat -an | grep 5000(Linux/Mac) ornetstat -an | findstr 5000(Windows) - If connecting remotely, use the correct IP address instead of
localhost - Check firewall settings
Port Already in Use
Problem: Mr. Pumpkin won’t start because port 5000 is busy.
Solutions:
- Find what’s using port 5000:
lsof -i :5000(Linux/Mac) ornetstat -ano | findstr :5000(Windows) - Kill the conflicting process or change Mr. Pumpkin’s port in
pumpkin_face.py
No Response Received
Problem: Command sent but no response received.
Solutions:
- Expression/animation commands intentionally return no response—this is normal
- Make sure you called
shutdown(SHUT_WR)or closed the write side to signal end of command - Some commands are fire-and-forget; only status/playback commands return responses
- Check if the command is valid (typos will be silently ignored for some commands)
Invalid Command
Problem: Command doesn’t work as expected.
Solutions:
- Check spelling and case (commands are case-insensitive, but be careful with arguments)
- Verify command syntax matches the reference above
- For gaze/eyebrow/numeric commands, ensure values are in valid ranges
- Check Mr. Pumpkin’s console output for error messages
Timeline Playback Issues
Problem: Timeline won’t play or plays incorrectly.
Solutions:
- Verify the file exists:
listcommand should show it - Check JSON syntax if you created the file manually
- Ensure timeline version is “1.0”
- Verify
duration_msmatches the longesttime_msin commands - Stop any active recording before playback:
record cancelthenplay <file>
Remote Connection Issues
Problem: Can’t connect from another machine.
Solutions:
- Use the server’s IP address instead of
localhost - Ensure the server is binding to
0.0.0.0not just127.0.0.1(checkpumpkin_face.py) - Configure firewall to allow port 5000 (TCP) and/or port 5001 (WebSocket)
- Verify both machines are on the same network (or proper routing exists)
WebSocket Not Available
Problem: Can’t connect to port 5001, or WebSocket server not responding.
Symptoms:
- Connection refused on port 5001
- Mr. Pumpkin console shows “websockets library not available, WebSocket server disabled”
Solutions:
- Install the
websocketslibrary:pip install websockets - Restart Mr. Pumpkin:
python pumpkin_face.py - Verify port 5001 isn’t being used by another process:
netstat -an | findstr 5001(Windows) orlsof -i :5001(Linux/Mac) - Check for firewall blocking port 5001
- Use TCP on port 5000 as a fallback if WebSocket isn’t available
Next Steps
Now that you understand the protocol, you can:
- Experiment with
client_example.pyto see all commands in action - Build your integration using the language of your choice
- Create timeline sequences for complex performances
- Automate behaviors based on external triggers (sensors, APIs, schedules)
Have fun bringing your pumpkin to life! 🎃