How-To Projects
Projects Intermediate 2-3 hours

Build a PLC-Controlled Robot Dog with ControlForge and Parallax P2

Complete walkthrough: build Megabite, a desk-sized robot dog with OLED eyes, servo ears, and head tilt — all controlled by AI-generated Structured Text running on ControlForge.

What is Megabite?

Megabite is a desk-sized robot dog avatar — laser-cut wood body, 3D printed linkages, animated OLED eyes, twitching ears, a wagging tail, and stereo sound. It sits on your desk and looks alive.

What makes it interesting isn’t the hardware. It’s that the entire PLC program controlling it was written by AI through natural language. Not a single line of Structured Text was typed by hand. ControlForge’s built-in MCP server gave Claude full access to the hardware function registry, and the code was generated, validated, and deployed — all from conversation.

Watch the video above to see Megabite in action, then read on if you want to build your own.


Bill of Materials

ComponentQtyPurpose
Parallax P2 Edge + P2 Edge Breadboard1Microcontroller — runs the cyclic I/O firmware
0.96” OLED display (SSD1306, I2C)2Animated eyes with pupils, blinking, and look-around
MS18 micro servo8Ears, legs, head tilt, tail
DFPlayer Mini clone1MP3 playback via UART
Audio amplifier1Drives the speakers
Small speaker2Stereo sound output
2x16 LCD display1Shows ControlForge runtime status
USB cable (USB-A to micro)1P2 Edge to host computer
Laser-cut wood panels1 setBody frame
3D printed linkages1 setServo-to-joint connections

You’ll also need a Linux machine running ControlForge and a Claude API key for AI code generation. The MCP tools are built into the ControlForge binary — no separate install needed.


Pin Mapping

These are the actual P2 Edge pin assignments used in the Megabite project. If you’re wiring your own build, follow this exactly.

Eyes — I2C OLED Displays

EyeSDASCLI2C AddressNotes
LeftPin 11Pin 100x3C (60)Mounted upside-down, flipped via SSD1306 commands
RightPin 25Pin 240x3C (60)Same address, separate I2C bus

Each eye gets its own I2C bus — no address conflicts, no multiplexer needed.

Servos — PWM at 50Hz

PinAssignmentNeutral Duty
0Ear (left)4915
2Servo 24915
4Servo 44915
6Servo 64915
41Servo 414915
43Servo 434915
45Servo 454915
47Ear (right)4915

Duty values are in microsecond-scale units. 4915 = center position. The usable range is roughly 600–8000. The firmware’s interpolation cog smooths all movement automatically — no jerky jumps even at the 100ms scan rate.

Sound — UART

TX PinRX PinBaudDevice
30299600DFPlayer Mini

The Firmware

The P2 runs custom cyclic I/O firmware written in Spin2. You don’t need to compile anything — ControlForge loads the pre-built binary onto the P2 automatically over USB when the PLC program starts.

Under the hood, the firmware runs a tight binary frame protocol at 3 Mbaud:

SYNC(2) | SEQ(1) | CMD(1) | LEN(2) | PAYLOAD(N) | CRC16(2)

The P2’s 8 cogs are allocated across dedicated subsystems:

  • Cog 0 — Main loop: frame processing and I/O exchange
  • Cog 1 — Smart pin manager: ADC, PWM generation
  • Cog 2 — Eye renderer: 30fps OLED framebuffer animation
  • Cog 3 — Servo interpolator: smooth exponential ramping

The firmware source is available as a download (Spin2, MIT licensed). It’s composed of modules — cyclic_io.spin2, hw_eye.spin2, hw_servo.spin2, hw_oled.spin2, hw_uart.spin2, and others. OBEX library dependencies (jm_i2c.spin2, jm_servo.spin2) by Jon “JonnyMac” McPhalen are included.

The source is provided so you can modify the firmware or use it with your own projects outside of ControlForge — Python, Node.js, or anything that can talk serial.


Connecting ControlForge to the P2

Plug the P2 Edge into USB. In your PLC program, two lines handle the entire connection:

port := SERIAL_FIND('Parallax');
p2_ok := P2_INIT('p2', port, '<install-dir>/go-p2/firmware/cyclic_io.binary');

SERIAL_FIND scans USB ports for a device with “Parallax” in its descriptor. P2_INIT loads the firmware binary onto the P2 and starts the cyclic exchange. From that point on, every hardware command goes through P2_CMD.

Hardware Initialization

Once connected, the startup code configures each subsystem:

(* Eyes — start two independent I2C OLED displays *)
P2_CMD('p2', 'eye_start', 'sda', 11, 'scl', 10, 'addr', 60);
P2_CMD('p2', 'eye_start', 'sda', 25, 'scl', 24, 'addr', 60);

(* Servos — configure all 8 pins for 50Hz PWM *)
P2_CMD('p2', 'pwm_setup', 'pin', 0, 'freq', 50);
P2_CMD('p2', 'pwm_setup', 'pin', 2, 'freq', 50);
P2_CMD('p2', 'pwm_setup', 'pin', 4, 'freq', 50);
P2_CMD('p2', 'pwm_setup', 'pin', 6, 'freq', 50);
P2_CMD('p2', 'pwm_setup', 'pin', 47, 'freq', 50);
P2_CMD('p2', 'pwm_setup', 'pin', 45, 'freq', 50);
P2_CMD('p2', 'pwm_setup', 'pin', 43, 'freq', 50);
P2_CMD('p2', 'pwm_setup', 'pin', 41, 'freq', 50);

(* Sound — DFPlayer Mini UART at 9600 baud *)
P2_CMD('p2', 'uart_setup', 'ch', 0, 'tx_pin', 30, 'rx_pin', 29, 'baud', 9600);

This all runs once in the INIT state (state 0) of a CASE state machine. After init succeeds, the program transitions to the main loop.


The AI-Generated Code

Here’s what makes this project different from every other robot dog build: every line of PLC code was generated by Claude through natural language.

The workflow:

  1. You describe what you want in plain English
  2. ControlForge’s built-in MCP server gives Claude the full function registry — every P2_CMD, every parameter, every valid value
  3. Claude generates valid IEC 61131-3 Structured Text
  4. The code is validated against the PLC compiler
  5. It’s deployed to the running runtime — hot-swapped, no restart

For example, to create the idle eye behavior:

“Make the eyes look around randomly every 3-5 seconds, with natural blinking every 5 seconds or so.”

Claude generated the complete look-around and blink state machines, including randomized timing, smooth interpolation targets, and proper variable scoping. No manual editing needed.


How the Idle Behaviors Work

The main loop runs at a 100ms scan cycle. Each behavior uses a simple scan counter pattern — increment a counter each scan, trigger the behavior when the counter hits a randomized target, then reset.

Eyes Looking Around

Every ~3 seconds, the pupils drift to a new random position:

look_cnt := look_cnt + 1;
IF look_cnt >= look_target THEN
    eye_x := RANDOM_RANGE(30, 98);
    eye_y := RANDOM_RANGE(15, 40);
    look_cnt := 0;
    look_target := RANDOM_RANGE(10, 25);
END_IF;

The eye renderer on the P2 smoothly interpolates toward the target position using exponential easing — so the pupils don’t snap, they drift naturally.

Blinking

Every ~5 seconds, both eyes blink:

IF NOT blinking THEN
    blink_cnt := blink_cnt + 1;
    IF blink_cnt >= blink_target THEN
        blinking := TRUE;
        lid := 100;
    END_IF;
ELSE
    blink_hold_cnt := blink_hold_cnt + 1;
    IF blink_hold_cnt >= 1 THEN
        lid := 0;
        blinking := FALSE;
        blink_target := RANDOM_RANGE(15, 35);
    END_IF;
END_IF;

The lid variable (0–100) controls how closed the eyes are. The firmware renders this as a descending black bar over the eye graphic.

Ear Twitches

Every ~6 seconds, the ears make small random movements and return to neutral:

IF NOT ear_twitching THEN
    ear_cnt := ear_cnt + 1;
    IF ear_cnt >= ear_target THEN
        ear_twitching := TRUE;
        servo_0 := ear_0_neutral + RANDOM_RANGE(-400, 400);
        servo_47 := ear_47_neutral + RANDOM_RANGE(-400, 400);
    END_IF;
ELSE
    ear_hold_cnt := ear_hold_cnt + 1;
    IF ear_hold_cnt >= 3 THEN
        servo_0 := ear_0_neutral;
        servo_47 := ear_47_neutral;
        ear_twitching := FALSE;
        ear_target := RANDOM_RANGE(25, 50);
    END_IF;
END_IF;

The combination of randomized timing on all three behaviors — eyes, blinks, ears — creates the illusion of a living thing. Nothing is synchronized. Nothing repeats on a fixed schedule. It just looks alive.


Sound

The DFPlayer Mini takes hex-encoded UART commands. The PLC program sends raw bytes through the P2’s UART channel:

(* Reset volume *)
P2_CMD('p2', 'uart_tx', 'ch', 0, 'data', '7EFF060600001EEF');

(* Play track 1 *)
P2_CMD('p2', 'uart_tx', 'ch', 0, 'data', '7EFF0603000001EF');

Sound is triggered by writing a track number to the play_track variable. The code detects when it changes and sends the appropriate command. Load MP3 files onto a micro SD card in the DFPlayer — tracks are numbered sequentially (001.mp3, 002.mp3, etc.).


The Hardware Output Loop

Every scan cycle, the PLC pushes the current state of all variables to the hardware. This is the core of the cyclic I/O pattern — the PLC program updates variables, and the output section writes them to the physical devices:

(* Eyes *)
P2_CMD('p2', 'eye_move', 'eye', 0, 'x', eye_x, 'y', eye_y);
P2_CMD('p2', 'eye_move', 'eye', 1, 'x', eye_x, 'y', eye_y);
P2_CMD('p2', 'eye_lid', 'eye', 0, 'position', lid);
P2_CMD('p2', 'eye_lid', 'eye', 1, 'position', lid);

(* Servos *)
P2_CMD('p2', 'servo_move', 'pin', 0, 'duty', servo_0, 'speed', -20);
P2_CMD('p2', 'servo_move', 'pin', 47, 'duty', servo_47, 'speed', -20);
(* ... all 8 servos ... *)

The speed parameter controls interpolation rate. A value of -20 means smooth, gradual movement. The firmware’s servo cog handles the ramping — the PLC just sets the target and moves on.


What This Demonstrates

Megabite isn’t a toy project. It’s a proof of concept for three things:

ControlForge runs anything. The same runtime architecture that manages industrial water treatment plants, data center BMS systems, and manufacturing lines also drives a robot dog. Scan cycles, I/O abstraction, and protocol drivers work at any scale.

AI code generation is real. Every line of Structured Text in this project was generated through conversation. The built-in MCP server gives the AI complete hardware context — function signatures, parameter types, valid ranges. The AI doesn’t guess. It generates verified code.

The P2 firmware is reusable. The Spin2 source is MIT licensed. Use it with ControlForge, or talk to the P2 from Python, Node.js, Rust — anything that can open a serial port and send binary frames. The protocol is documented in the source.


Get Started

  1. Download the project file above and import it into the ControlForge Web IDE
  2. Grab the firmware source if you want to see how the P2 side works
  3. Watch the video to see the final result
  4. Wire up a P2 Edge, connect some servos and OLEDs, and start talking to Claude