Roboktober

From joystick to wheel

From software to motion in a 150-gram robot

Rein Velt · PHP Meetup Groningen · 5 November 2026

Hackerspace Drenthe Hackerspace Drenthe

Who am I?

Rein Velt · software engineer & maker

Code by day, soldering iron by night.

  • Software Engineer at Dawn Technology
  • Founder of the Dutch PHP User Group (2001) → PHPBenelux
  • Founder of Hackerspace Drenthe (Coevorden)
  • From building a PHP community… to building a maker community
  • This spring (March–May): Mei MeshCore Maand (MeshCore Month in May) · Drenthenoodnetwerk.nl
  • This summer: Roboktober → website, PCBs, parts, chassis and organising the event

I'm more of a builder than a talker — so today I'll mostly show you what we built.

Soldering at the hackerspace
Hackerspace Drenthe, build night

Hands up

Who has ever soldered anything?

Who has programmed an Arduino or ESP32?

The thread through this talk

What happens when you push the joystick forward?

We'll follow it together: from smartphone to motor.

WebUI joystick on a smartphone
① Phone or remote

THE EVENT

Roboktober

How it started — and what exactly is an antweight?

How it started

Wallie (WallieOnline)

▶ Meet Wallie · 1:24 · in Dutch

Kickoff excerpt · 0:26–1:50 · no subtitles
  • Has been building antweight robots for many years
  • Team captain of Team Upstart — including Cookiemonster, TLC and Kecil
  • 🏆 1st at INDI Robot Games 2020 — Master Milo also competed; 2nd at Boeliebots 2026 (Groningen)
  • Often visits the hackerspace to make robot parts using the milling machine and CNC equipment
  • The idea: organise an event together → get people excited about technology and the hackerspace

Today · under the hood

Same robot, a new perspective

At the build nights

  • Build and experiment together
  • Children, families and makers
  • Get something moving quickly

Today, as developers

  • What happens between input and motion?
  • Interfaces, code and schematics
  • Which software skills transfer?

So today: code · PCBs · schematics

No electronics knowledge needed. Ask if I skip a step.

The event

Roboktober

A month of building, testing and robot action — Hackerspace Drenthe + WallieOnline. Solo or as a team: children, families, colleagues.

WhenWhat
7 OctKickoff: live demo, try driving, start building (livestream)
14 OctAntweight building workshop
21 OctFinish building and test
28 OctTechnical inspection + Battle Day

3D printers, laser cutters and guidance at the hackerspace.

Build night
Build night at the hackerspace

What is an antweight?

150 g

maximum weight, everything included

~€25per kit
15 cmmax. 15×15×15 cm
Antweight pusher
Small, affordable, relatively safe, quick results

Our class & safety limits

Our robot

  • Antweight — maximum 150 g
  • Battery and electronics count too
  • At Roboktober: push, don't destroy
  • Other events: check their own rules

Types

  • Pushers / wedges
  • Flippers
  • Spinners

Safety

  • Open arena → no active weapons at Roboktober 2026 (pushers/wedges only)
  • Active weapons require suitable containment, rules and inspection
  • Inspection · failsafe

Safety isn't just in the arena — it's in the code too. We'll see how.

THE ROBOT

Follow one command

From thumb on screen to wheel on floor

The robot opened up · what goes where?From parts to movement · follow one command

3D render of the empty robot chassis with space for electronics and motors
3D render · chassis without electronics

1 / 7 · THE BODY

Chassis

Holds all the parts in place.

Space for the battery, PCB and drivetrain. You can design and 3D-print it yourself.

Schematic render of a 1S LiPo battery with its connection cable
Schematic render · 1S LiPo

2 / 7 · THE ENERGY

Battery

Supplies the robot's energy.

1S LiPo: 3.7 V nominal, 4.2 V fully charged. Motor current comes from here, not from the ESP32.

Schematic view of a switch in the off position and a removed link plug or jumper cap: both interrupt the positive supply to the robot
Schematic · switch OR removable link plug/jumper cap · both off

3 / 7 · PHYSICAL POWER DISCONNECT

On/off

Switch or removable link plug/jumper cap.

Physically disconnects battery power. Accessible from outside and rated for the motor current.

Stopping motors in software is not the same as switching power off.

KiCad render of the bare robot PCB with copper traces and connection points
KiCad 3D render · bare PCB v4

4 / 7 · THE CONNECTIONS

Circuit board · PCB

Connects the parts with copper traces.

Connections for power, ESP32, motor driver and motors. The circuit board itself does not run code.

Real photo of an ESP32-C3 SuperMini with USB-C connector, two push buttons and antenna
Photo: Tobias Weltner · done.land
CC BY-ND 4.0 · unchanged

5 / 7 · THE BRAIN

ESP32-C3 SuperMini

Our firmware runs here.

Receives wireless commands, processes throttle and steering, and sends control signals to the motor driver.

Product photo of a DRV8833 motor driver module with connection holes and dimensions of 18.4 by 16 mm
Product photo: AliExpress · supplied source

6 / 7 · MOTOR CONTROL

Motor driver

DRV8833: between ESP32 and motors.

Switches battery voltage to two motors. The ESP32 decides what should happen; the driver switches the motor current.

Product photo of an N20 motor with metal gearbox and output shaftProduct photo of a wheel with rubber tyre and white hub for the motor shaft
Product photos: AliExpress · N20 motor (cropped) · wheel

7 / 7 · THE MOVEMENT

Motors + wheels

Two N20 motors drive the wheels.

Controlling left and right independently makes the robot drive and steer.

Now what happens when you push forward?

1S LiPo 3.7–4.2 V N20 N20 ESP32-C3 DRV8833 📶 throttle + steering GPIO6 GPIO10 PWM ≈ 78 % ON/OFF ⚡ 3.7 V on / off
PCB v4, underside · battery, motors and wheels shown schematically · chassis not shown
  1. 📶 Signal arrivesESP32-C3 receives throttle + steering
  2. 🧠 ESP controlsmotor command 200 → PWM ≈ 78 %
  3. 🔋 Battery supplies power1S LiPo → DRV8833
  4. ⚡ DRV8833 switchesswitches battery voltage to the motors
  5. 🛞 Wheels turnequal commands on both sides → (roughly) straight ahead

STEP 1 · REMOTE CONTROL · 5 MIN

The controller

How does a touch on your screen become a command?

Two controllers, one goal

📱 Phone (WebUI)

  • The robot creates its own WiFi network
  • Browser, no app
  • WebSocket

🎮 Dedicated remote

  • Second ESP32 + analogue stick → ESP-NOW
  • Or a regular RC transmitter + receiver → CPPM
  • Two separate routes, same result

Both deliver the same thing: throttle + steering

Start with the phone; for the code, we'll then use the public ESP-NOW version.

WebUI · the robot as a WiFi access point

WebUI joystick mode WebUI tank mode
  • WiFi worc-424242 · password worc1234
  • Open http://192.168.4.1 in your browser
  • No app, router or transmitter needed
  • WebSocket: one open connection, rather than reconnecting → low latency
  • Choose: joystick or tank (two sliders)
  • Mix throttle and steering, reverse direction: change settings, not code
  • Wallie's new firmware: release on 5 November 2026 — today · Wallie's GitHub

Just like your home router's settings page.

Dedicated remote · ESP32 → ESP-NOW

One shared format — the same structure in both programs:


typedef struct rc_data_t {
  uint16_t gAIL, gELE, gTHR, gRUD, gAUX1, gAUX2, gAUX3, gAUX4;  // 8 × 2 = 16 bytes
} rc_data_t;
rc_data_t gEspn;

① Transmitter fills and sends it (every 20 ms)


// Abridged: fill the channels, then send.
if (gStick[AIL]) gEspn.gAIL = gStick[AIL]; // steering
if (gStick[ELE]) gEspn.gELE = gStick[ELE]; // throttle
// … other channels …
esp_now_send(0, (uint8_t *) &gEspn, sizeof(gEspn));

worc-remote-esp32.ino#L221–235

② Receiver checks the length and reads the structure


void OnDataRecv(const uint8_t *mac,
                const uint8_t *incomingData, int len) {
  if (len == sizeof(gEspn)) {
    memcpy(&gEspn, incomingData, sizeof(gEspn));
    // … validate and scale channel values …
    lastCommandTime = millis();
  }
}

worc-esp32c3-drv8833-espnow.ino#L129–150

Like pack()/unpack(): 16 raw bytes. Transmitter and receiver must use exactly the same format.

STEPS 2–4 · FIRMWARE AND MOTOR CONTROL · 12 MIN

The firmware

From an incoming message to a turning motor

Flashing from the browser

  • Web flasher (Web Serial): plug in USB, press a button, done
  • No toolchain, no Arduino IDE
  • Chromium browsers only
  • Pitfalls: charge-only USB cable, missing driver

From browser to chip

Browser → USB → ESP32

Web Serial gives the browser access to the serial connection.

Inspiration: wallieonline.nl/blogs/worc-robot-controller-nl.html

A firmware update like your printer or router — but through the browser.

The software is now on the chip. What does it do with a command?

The complete firmware · ESP-NOW + DRV8833

1 / 9 · L19–30

Which hardware are we using?

What: load libraries and name connection pins.

Why: connect software to the right hardware.

For the robot: IN2A_OUT_PIN means GPIO 6. It is a connection number, not a speed.

2 / 9 · L32–50

A fixed message format

What: eight 16-bit channels form one 16-byte message.

Why: sender and receiver share the same format.

For the robot: gELE is throttle, gAIL steering. The limits and deadband determine how these values will be converted.

3 / 9 · L51–73

Memory between messages

What: store driving commands, settings and a timestamp.

Why: keep values available between messages.

For the robot: throttle and steering start at 0. lastCommandTime and the 500 ms limit enable the timeout later.

4 / 9 · L152–167

Prepare once at startup

What: load settings and configure PWM: 5 kHz, 8 bit.

Why: prepare motor outputs once at startup.

For the robot: the driver is held in standby during startup, then enabled after two seconds. Motor commands are still 0.

5 / 9 · L170–189

Listen to the remote

What: start ESP-NOW and register OnDataRecv.

Why: process incoming messages automatically.

For the robot: the remote must use the same radio channel. If ESP-NOW fails to start, the code logs the error and restarts the ESP32.

6 / 9 · L129–150

Bytes become driving commands

What: check message length, scale channels and apply deadband.

Why: convert joystick values without creeping near neutral.

Caution: an out-of-range channel keeps its previous value. Every correctly sized message refreshes the timeout here, even in that case.

7 / 9 · L93–105

Two wheels from one joystick

What: mixing: left = throttle + steering, right = throttle − steering.

Why: different wheel outputs turn the robot.

Example after scaling: throttle 200 + steering 100 → left 255, right 100. Inversion settings then correct motor direction.

8 / 9 · L106–127

Numbers become control signals

What: ledcWrite(channel, value) sets PWM through the Arduino-ESP32 core, not a custom function.

Why: turn motor commands into electrical control signals.

LEDC = LED Control, also usable for motors. Value 200 ≈ 78% on-time, not 78% speed. Motor current comes from the battery.

9 / 9 · L191–205

Keep driving — or stop

What: keep updating motors; after >500 ms silence, throttle/steering become 0.

Why: stop when the connection drops.

For the robot: every iteration calls updateMotors(), including for the stop command. This is a software failsafe, not a physical power disconnect.

→ next block · ← back · scroll for all code
Public ESP-NOW version, not the WebUI source.

© 2026 wallieonline · complete source · fb3afa886117 · GPL-3.0-or-later

Scaling + deadband · calm around the centre


if (gEspn.gELE > unThrottleMin && gEspn.gELE < unThrottleMax) {
  gThrottle = map(gEspn.gELE, unThrottleMin, unThrottleMax, -255, 255);
  gThrottle = constrain(gThrottle, -255, 255);
  if (abs(gThrottle) <= RC_DEADBAND) {
    gThrottle = 0;
  }
}
0−255−40+40+255

Deadband (RC_DEADBAND 40): a resting joystick is never exactly at 0 — there's always some noise and play. Everything from −40 to +40 becomes 0, otherwise the robot creeps away while you do nothing.

OnDataRecv() L129–150 (abridged)

Mixing


gMotorLeft = constrain(gThrottle + gSteering, -255, 255);
gMotorRight = constrain(gThrottle - gSteering, -255, 255);

if (gInvertLeft) gMotorLeft = -gMotorLeft;
if (gInvertRight) gMotorRight = -gMotorRight;
ThrottleSteeringLR
2000200200
200100255100
0150150−150

updateMotors() L93–105 · abridged, mixer enabled

Steer like a tank or excavator: different left/right speeds = turning.

PWM: a dimmer

25%
50%
90%

  if (gMotorLeft > 0) { //DIRECTION_FORWARD
    ledcWrite(0, LOW); //IN1A_OUT_PIN
    ledcWrite(1, abs(gMotorLeft)); //IN2A_OUT_PIN
  } else if (gMotorLeft < 0) { //DIRECTION_REVERSE
    ledcWrite(0, abs(gMotorLeft)); //IN1A_OUT_PIN
    ledcWrite(1, LOW); //IN2A_OUT_PIN
  } else { //DIRECTION_STOP
    ledcWrite(0, LOW); //IN1A_OUT_PIN
    ledcWrite(1, LOW); //IN2A_OUT_PIN
  }

5000 Hz, 8 bit: 0 = off · 200 ≈ 78% on · 255 = always on

ledcWrite() L106–125 · same for the right motor on channels 2/3

H-bridge: four switches

Forward: A + D closed
Reverse: B + C closed

DRV8833 = two H-bridges in one chip. IN1/IN2 determine which switches are closed.

The H-bridge selects current direction; PWM sets how much of the time the motor is driven.

Failsafe · no command is a state too


if (millis() - lastCommandTime > COMMAND_TIMEOUT) {
  gThrottle = 0;
  gSteering = 0;
  digitalWrite(FLED_OUT_PIN, (millis() / 250) % 2);
}

updateMotors();

500 ms no packet → stop · 🔴 LIVE: remote off

loop() L191–205 · simplified, LED/logging abridged · COMMAND_TIMEOUT = 500

Like a heartbeat timeout: without fresh commands, enter a safe state.

IMPLEMENTATION · PCB · 4 MIN

The PCB

The motor turns. How do we connect the parts reliably?

Components & power

  • ESP32-C3 SuperMini — WiFi + ESP-NOW
  • DRV8833 — dual H-bridge
  • 2× N20 6 V, 50:1
  • LiPo 1S → on/off → DRV8833; optional 2S via a 5V regulator → ESP_5V_IN
  • ⚠ Never bridge VIN → 3.3V

Pin contract · the interface between code and PCB

GPIOFunctionFirmware
5DRV8833 nSLEEPSTBY_OUT_PIN
6 / 7Left motor IN2/IN1IN2A / IN1A
9 / 10Right motor IN1/IN2IN1B / IN2B
8Failsafe-LEDFLED_OUT_PIN

You don't need to memorise pin numbers. Schematic and firmware must follow the same contract.

KiCad · one shot

My first ever PCB — and it had to work first time.

150bare PCBs produced
78×56mm
  • Revisions v2 → v3 → v4 before production
  • v2: prototype · v3: power issues fixed · v4: production version
  • 🔁 Pass around: bare + populated PCB
Bare PCB v4 front
v4 front
Bare PCB v4 back
v4 back

Proofread before printing: one mistake becomes 150 copies.

Validation · automated checks and human review

  1. Planpin contract in Markdown: which pin → which net
  2. DoAI + I update the schematic & layout in KiCad
  3. Checkcheck design rules + compare connections with the pin contract
  4. Acterror? fix it → new revision (v2 → v3 → v4)
1 · Copper clearance 2 · Trace width 3 · Unconnected pads
PCB v4 · illustrative checks, not live KiCad output or measurements
$ kicad-cli pcb drc … 0 DRC violations · 0 unconnected pads Separate ERC: explain or resolve 3 messages
+ human: check the pin contract, review by Wallie and physical testing

25 of the 30 assembled PCBs work. What a relief.

Failures: ESP32-C3 SuperMini antenna/WiFi issues · assembly errors

Recommended: ESP32-C3 Super Mini Plus V2 with external antenna — same footprint, slightly more expensive.

HARDWARE · 3D PRINTING · 4 MIN

The chassis

Loose parts aren't a robot yet. What do we put them in?

A quick show of hands

Who has designed something in 3D before?

Who knows OpenSCAD?

Who has designed something with it?

Why OpenSCAD?

A 3D model as a text file — program it instead of clicking.

Visual CAD design

  • Draw and edit shapes directly
  • Can be parametric and automated too
  • Excellent tools for 3D design
  • For me: less direct text-based diffs

OpenSCAD

  • Git diffs: see exactly what changed
  • Parameters: pcb_clear = 0.3; → everything adjusts
  • Asserts: check explicit dimensional constraints
  • AI can read and write it: all designs together, ±9,700 lines of text
$ git diff antweight_chassis.scad - pcb_clear = 0.2; + pcb_clear = 0.3; // PCB fit was too tight

For developers: it's just code — version control, review and tests work here too.

Free and open source · Windows, macOS and Linux · Download: openscad.org/downloads.html

OpenSCAD · code first

A 3D model is the output of your code.

1. Dimensions, shapes and subtraction
2. Reuse, imports and checks

Two clicks. First the building blocks, then the robot designs.

1. Dimensions → shapes → subtraction

Millimetres · [x, y, z] · translate moves · difference subtracts

2. Reuse → import → check

assert(wall > 0, "wall must be positive"); guards a requirement, not physical fit.
use <standoffs.scad> loads modules; import() loads STL/3MF or DXF/SVG.

Chassis as code · OpenSCAD

One project, many different designs

Now the designs: nine families, versions side by side.

Example of the final chassis

Click to begin →

Same PCB, different chassis

Basic chassis
v0.2 basic — "hello world"
Chassis v0.4 integrated
v0.4 integrated — integrated enclosure
Racer nordic
Racer nordic — multicolour design

Shared PCB outline, holes, motor and wheel dimensions → every chassis fits the same board.

A standard interface, like LEGO studs.

AFTER THE TECH

Demo & journey

Try driving — and how did this project come together?

Now you know what happens

Who wants to drive?

Live demo · you drive

WebUI
  1. Select your demo robot's WiFi network
  2. Use the password on the card next to the robot
  3. Browser: http://192.168.4.1
  4. Pushing match! 🥊

One driver per robot. Switch only when the robot is stationary.

Detour · the PHP part

roboktober.nl

  • API-first: Laravel API + Filament admin, Vue 3 + Vite
  • Build Hub: parts list, 3D files, firmware and build guide
  • The site as a distribution channel for a physical building project
  • Same workflow: version control, static analysis and tests
Participants building their robots
From instructions on the site to building at the table
Website and build files on GitHub github.com/Hackerspace-drenthe/www.roboktober.nl
  • 🌐 Laravel + Vue
  • 🔌 KiCad PCB
  • 📦 OpenSCAD chassis
  • 📌 Pin contracts
  • 🗺️ Design decisions
  • 🧾 Parts list

Not just the final result: the plans, iterations and mistakes too. Fork it, build it, improve it.

ESP-NOW firmware: WallieOnline. WebUI source code is still to follow.

From first commit to kickoff · July–October

2 Jul
webPlan frozen + first Laravel API + Vue commit
Jul–Aug
PCBDesign, review and order parts
late Aug
productionv4 final — PCBWay sponsorship, 150 boards
Sep–Oct
testingFirmware, chassis and test prints
7 Oct
kickoffFrom preparation to building together
14–28 Oct
eventBuild nights, guidance and Battle Day

About three months of preparation until kickoff; then build nights and Battle Day.

How: AI-assisted development

Detailed plan
goals + constraints
→
AI implements iteratively
→
Test & validate
against the plan
→
Adjust it myself
and refine
↺
  • One plan as the source of truth (PLAN.md + changelog)
  • Validation per domain: DRC/ERC · PHPStan + tests · test prints · code review
  • VS Code + GitHub Copilot, with custom instructions

My responsibility

Set goals and limits.
Make design decisions, check results and correct mistakes.
And the physical work: ordering, soldering, helping participants. Wallie contributed robotics expertise and review.
AI gives one developer a broader reach — if the plan and tests are sound.

TO WRAP UP

Closing

How did it go — and how can you get started?

How did it go? · Battle Day, 28 October

30+visitors · packed house
18robots initially
12robots in the competition
Robots on the workbench
Robots during the building phase
PlaceRobot
1TurfTyrant
2Princess Peach
3Lief Monster · Disc Spinner v0.5
4angry pumpkin car · Limulus v0.1

My 5 working robots went to children without a working robot — so everyone could take part.

From joystick to wheel — the same software principles

  1. Open software and standards as foundations.
  2. Testable requirements as contracts.
  3. Code as a tool — for hardware too.
  4. Tests as proof — in the real world too.

AI assists at every step. You define, check and validate.

If you can program, you can build robots too.
The same principles. Now it moves.

You're still a developer — only now your bug drives away.

How do you get started tomorrow?

🧰 Kit

Parts list + build guide at roboktober.nl

💾 Firmware

WallieOnline on GitHub or the web flasher

🖨️ Chassis

Edit a .scad file and print it
QR code for roboktober.nl
roboktober.nl
Hackerspace Drenthe
Hackerspace Drenthe · come visit!
PCBWay
PCBs sponsored by PCBWay

Questions?

The robots keep moving — come and try driving.

Hackerspace Drenthe

Get the presentation

Nederlands · English · offline downloads

QR code for the presentation index on roboktober.nl

roboktober.nl/app/presentaties/
php-meetup-groningen-2026-11-05/