DEV Community

Cover image for Our Robot Dog Stole My Phone (And I Built It)
Faraaz Ali Khan
Faraaz Ali Khan

Posted on

Our Robot Dog Stole My Phone (And I Built It)

This is a submission for the MLH x DEV Writing Challenge

πŸ† Update: Fido came joint 2nd in the OxHacks 2026 main track, 2nd in Build With Delta (Zed), and won Best Use of Presage (MLH).

A storage basket, an ESP32, a webcam that knows when you're slacking, and a motor driver that died at 5 AM. The story of Fido, from OxHacks 2026.

I put my phone inside the dog and started a focus session.

Then I let my eyes drift away from the screen. Eight seconds, that's all it took. Fido turned towards me. He nudged. I ignored him. He barked. I definitely ignored him.

Then Fido drove away with my phone still inside him.

Fido on the run

That's Fido. Four of us from King's College London built him at OxHacks 2026. He's a robot dog with one job: guard your focus, and if you won't, guard your phone.

Here's how a storage basket from a high-street shop ended up with trust issues ;)


The problem we couldn't stop thinking about

The OxHacks theme was Time.

Everyone's first idea for "Time" is a timer. Pomodoro apps, calendars, to-do lists with AI glued on. We've all downloaded them. We've all ignored them.

Here's the uncomfortable truth we kept landing on: focus tools fail because they live on the device that's distracting you. Screen-time limits are one "Ignore Limit" tap away. You can't out-willpower a phone with the phone.

So what if the consequence was physical? Something you can't swipe away. Something slightly ridiculous. Something with wheels.

What if your phone got taken hostage by a very small, very disappointed dog?

We locked the idea in at 4 PM on Saturday. We had no robot. We had no parts. We had about 15 hours.


How Fido decides you're slacking

You put your phone inside Fido, then start a 25-minute session in the web app. From then on, Fido has a pair of eyes: a webcam running a focus detector on your laptop.

You… Fido…
Look at your work πŸ‘€ Watches, quietly proud
Look away for 8 s straight 🐾 Nudges you back
Keep going for another 6 s πŸ”Š Barks
Keep going for another 8 s πŸƒ Runs away with your phone
Catch him and tap his collar πŸ“± Phone back. Session failed. Think about what you did.

Refocus during a nudge or a bark and he calms down. But once he's escaped, there's no bargaining. Running away can't earn you a completed session.

Catching Fido
Catching Fido. Harder than it looks.

The app across a full session
Waiting for your phone β†’ "Hey, eyes over here." β†’ "Fido ran off with your phone."

The eyes πŸ‘οΈ

The detector uses MediaPipe's face landmarker to estimate where your head is pointing. Everyone sits differently, so it doesn't use fixed angles. It spends the first 30 frames of every session learning what "you, focused" looks like, then measures everything against that:

if pitch_diff > c.phone_pitch_delta or (
    pitch_diff > c.phone_pitch_delta_with_eyes_down and s.eye_down > c.eyes_down_threshold
):
    return f"Looking Down at Phone/Lap (Ξ”Pitch: +{pitch_diff:.2f})", True
if pitch_diff < c.look_up_pitch_delta:
    return f"Looking Up / Daydreaming (Ξ”Pitch: {pitch_diff:.2f})", True
if yaw_diff > c.look_away_yaw_delta:
    return f"Looking Away Sideways (Ξ”Yaw: {yaw_diff:.2f})", True
return "Looking at Screen", False
Enter fullscreen mode Exit fullscreen mode

On top of that, Presage's SmartSpectra SDK tells us when you're talking and gives us 478 face landmarks, which we used to catch prolonged eye closure. That's the 4-second micro-nap that hits around 4 AM. Ask us how we know.

Our first version was unbearable. It barked every time you blinked. Two fixes made it feel fair:

  • Slow to accuse, quick to forgive. You need 8 seconds of sustained distraction before it counts. Look back and the timer drains 3Γ— faster than it filled.
  • No flip-flopping. "Distracted" kicks in instantly, but "focused again" needs 2 straight seconds of clean readings. No more nudge-watch-nudge-watch strobing.

And because a camera pointed at your face all night is a bit creepy: frames never leave your laptop. The backend only ever sees "YES" or "NO" plus a reason string like "Looking Down at Phone/Lap".


The body: a basket, two wheels, and paranoia

Soon it was raining, then dark, and we were on a supply run to Ryman, Flying Tiger and Tesco, looking at every object and asking "could this be a dog?"

Supply run in the rain

The haul
Cardboard, a Ryman bag of possibilities, and packet of chocolate rolls. The chocolate rolls are load-bearing :p

The winner was a storage basket. It's the right size, it's rigid, and it's ventilated (so it's basically a chassis). Inside: an ESP32, a dual motor driver, and two geared motors.

Fido first test drive
First test drive, before Fido had a body.

Soldering inside the basket

A robot built to drive away from people is, by design, a bit dangerous on a crowded hackathon table. So the firmware is paranoid. It asks the backend what to do once a second, and if the answer is late, missing or weird, it stops:

if (WiFi.status() != WL_CONNECTED || millis() - lastReply >= 3000) {
    if (commandFresh) Serial.println("Connection stale -> STOP");
    commandFresh = false;
}
Enter fullscreen mode Exit fullscreen mode

There's also a "Fido can move" switch in the app that works as an emergency stop. Fail closed, always.

And because hackathon Wi-Fi hands out a new IP every time you sneeze, Fido hosts his own Fido-Setup hotspot with a tiny config page. New laptop IP? Connect your phone to the dog, type it in, done. No reflashing.


The trick that let four people build one robot overnight

Two of us were on hardware, two on software, all working at the same time. That only works if nobody is blocked on anybody else.

Our answer was one tiny dictionary:

# Robot hardware acts on this value only, so the hardware
# never needs to know the session rules.
ROBOT_MODES: dict[Phase, str] = {
    Phase.IDLE: "sleep",
    Phase.AWAITING_PHONE: "beckon_dock",
    Phase.FOCUSING: "watch",
    Phase.NUDGE: "nudge",
    Phase.BARKING: "bark",
    Phase.ESCAPED: "run",
    Phase.PAUSED: "rest",
    Phase.COMPLETED: "celebrate",
    Phase.FAILED: "rest",
}
Enter fullscreen mode Exit fullscreen mode

The robot never knows why it's running. It only knows that it should. So at 4 AM, when we decided "Fido shouldn't be able to go straight from running back to watching", the state machine changed and zero lines of firmware did.

Everything else follows one rule: the Flask backend is the single source of truth. The webcam, the robot, your laptop and your phone all read and write one session. The Next.js app streams it live over Server-Sent Events and has no state machine of its own, so two screens can never disagree about how long you have left.

And because the escalation logic runs on an injectable clock, our tests fast-forward time instead of waiting. The whole nudge β†’ bark β†’ escape sequence is tested in milliseconds, and Playwright drives the real journey end to end.


05:00: The motor driver dies

At 4 AM we'd done the grown-up thing: stopped, written every remaining task down, split it into Priority 1/2/3, assigned owners, and drawn up a timeline that ended in "7:00 AM: Completed Product." We felt great.

At 5 AM, the motor driver fried.

Re-soldering the replacement driver

No shops open. Plan in tatters. So we swapped in a replacement, re-soldered it, and carried on. It cost us an hour of a perfect schedule, and it taught us this, now written into our firmware notes in capital letters: power the motors separately, never from the microcontroller, and share a ground.

Fido gets a body

A robot dog needs to look like a dog. So in the last stretch, the basket disappeared under hand-cut cardboard:

  • A head with floppy ears, googly eyes and a drawn-on smile
  • Cardboard panels down the sides, so he looks less like a basket and more like a dog
  • A cardboard lid over the top, closing over the phone inside

Fitting Fido's cardboard lid
Fitting the lid.

Fitting Fido's cardboard face

He also got a small OLED screen that tells you what to do ("DOCK PHONE. Make something that matters.") and an ultrasonic sensor for a nose.

The finished Fido
The finished Fido. Phone goes in, focus comes out.

Fido from the side

πŸ₯š The easter egg

Look at his back end. Fido has a tail, and it wags.

Fido's tail wagging
Nobody asked for this. We built it anyway.

07:00: Judging

Two hours after the motor driver died, Fido was in front of the judges. He came away with three prizes:

  • πŸ₯ˆ Main track: joint 2nd place
  • πŸ₯ˆ Build With Delta by Zed, 2nd place
  • πŸ† Best Use of Presage by MLH: a Fitbit Inspire 3 each, plus a Presage phone stand. Fitting, for a project about where your phone belongs.

Team Fido on stage for Best Use of Presage
Best Use of Presage. Fido came on stage too.

Team Fido on stage for Build With Delta, 2nd place
2nd in Build With Delta, by Zed.


Partner technologies

Presage

Presage's SmartSpectra SDK handles the parts of focus detection that head angle alone can't:

  • Talking: Presage's own talking signal. At least 0.6 s of talking in the last 6 s counts as a distraction.
  • Prolonged eye closure: we compute an eye-aspect ratio from Presage's 478 face landmarks. Both eyes below the threshold for 4 continuous seconds counts as dozing off.
  • One camera, no duplicates: Python owns the only webcam capture and pipes frames to the SDK through a local in-memory pipe, so Presage never opens the camera itself.
  • Bounded credits: the SDK spends credits while it scans, so each face scan is capped at 25 seconds after Start by default, with no automatic paid restarts.

Nothing leaves the laptop except the YES/NO decision and a reason string.

Delta

We used Delta by Zed to write our prompts together. Instead of each of us prompting our coding agents alone, all four of us wrote and refined prompts in one shared space, like a Google Doc with agentic development built in. So every prompt was a team effort, and everyone could see what the agents were being asked to build.


Rewind: how the day actually started

You don't get a robot dog at 5 AM without a 6:37 AM train.

On the train

Sunrise from the train

We got to Oxford two hours early, so we did what any responsible hackathon team does: sightseeing.

Radcliffe Camera

We all also touched the Thames. For reasons.

Touching the Thames

Ducks
The ducks were unimpressed. Fido's first critics.

Then registration, merch, kickoff, and a bubble tea to fuel the 4-hour argument that eventually became Fido.

Kickoff

Oh, and the clocks went back at 2 AM. A hackathon themed Time, on the one night of the year with 25 hours in it. We spent the bonus hour soldering. Obviously.


What we'd tell you before your next hardware hackathon

  1. Agree on a contract between hardware and software in the first hour. For us it was phase β†’ robot mode. It kept four people unblocked all night.
  2. One source of truth. Every "but my screen says…" bug disappears when only one thing owns the state.
  3. Make time injectable. If your logic says "after 8 seconds", your tests shouldn't wait 8 seconds.
  4. Noisy sensors need hysteresis. Turning "on" and turning "off" should have different thresholds.
  5. Robots should fail closed. Stale command = stop. Always.
  6. Bring a spare motor driver. Please. Learn from us.

The humans behind the dog 🐾

Team Fido mid-build

Prizes: Joint 2nd, main track Β· 2nd, Build With Delta (Zed) Β· Best Use of Presage (MLH)

Also entered: Best Handmade Project (Citadel) Β· MLH Γ— DEV Writing Challenge

Code: github.com/itsfaraazak/oxhacks26

Thanks to OxHacks organisers for hosting this amazing hackathon- https://26.oxhack.net/


So, be honest: how long would you last before Fido ran off with your phone? We're guessing under 25 minutes. 🐢

Top comments (0)