Hamster Wheel Counter

Thinking Process

To accurately count the number of spins on a hamster wheel using a Micro:bit, I opted to use the onboard magnetic force sensor (magnetometer) instead of adding external sensors.

Understanding the Runner

It turns out hamsters are ultra-marathon runners by nature. On a typical active evening, our hamster logs approximately 2,500 laps.

With a wheel diameter of 15 cm, the travel distance adds up quickly:

  • Circumference: 15cm * pi ~= 47.1cm
  • Total Distance: 2500 spins * 47.1cm ~= 117,750cm

That averages out to roughly 1.18 kilometers (0.73 miles) every single night! Knowing this volume of activity helped confirm that the sensor solution needed to be robust enough to handle thousands of rapid triggers per session without drift.

Code vs Blocks

We had to implement this in JavaScript/TypeScript rather than the Block UI. The primary reason is performance: the magnetic sensor requires a high polling rate to detect the passed magnet when the wheel is spinning quickly. The standard event loop or Block UI logic can sometimes introduce latency or miss these fast transients. By running a tight loop in a background thread (control.inBackground), we ensure reliable detection of the magnetic force (currentForce) changes.

Physical Setup & The Gravity Hack

The setup involves attaching a strong magnet to the back of the wheel and mounting the Micro:bit sensor near the top of the support frame.

This configuration utilizes basic physics to solve a potential “stuck state” problem. The magnet adds weight to one point on the wheel. When the hamster stops running, gravity ensures the wheel rotates until the heavy magnet settles at the bottom. Because our sensor is mounted at the top, the system inherently defaults to an “idle” or “undetected” state when the wheel is at rest. This prevents the sensor from remaining triggered if the hamster happens to stop running right as the magnet passes the sensor.

Thresholding

To account for sensor noise, I implemented a hysteresis system:

  • MAG_THRESHOLD_HIGH: The level required to “trigger” a count (magnet closest).
  • MAG_THRESHOLD_LOW: The level the signal must drop below to reset the detection flag.

This prevents double-counting if the reading jitters near the threshold.

Interaction & Data Logging

The code includes specific logic for user interaction via the onboard buttons:

  1. Button A (Commit & Reset): This acts as the “End Session” button. When pressed:
    • It calculates the average temperature during the run by dividing the accumulated temperature (tempSum) by the number of spins.
    • It uses the datalogger API to save the final spinCount and ActiveTemp to the Micro:bit’s internal flash storage. This persists the data even if power is lost.
    • It resets all counters to zero, ready for the next night’s run.
    • It displays a checkmark to confirm the save was successful.
  2. Button B (Display Toggle): Hamsters are nocturnal.
    • This button toggles the LED display on or off.
    • Turning the display off (isDisplayOn = false) saves power and prevents the bright LEDs from disturbing the hamster, while the background thread continues to count spins silently.

The Code

// --- Configuration ---
const MAG_THRESHOLD_HIGH = 80
const MAG_THRESHOLD_LOW = 40

// --- Variables ---
let spinCount = 0
let magnetDetected = false
let isDisplayOn = true
let currentForce = 0

// Variables for temperature tracking
let tempSum = 0         
let activeTemp = 0      

// --- Setup Data Logger ---
datalogger.setColumnTitles("HamsterSpins", "ActiveTemp")

// --- Sensor Logic (Background Thread) ---
control.inBackground(function () {
    while (true) {
        currentForce = input.magneticForce(Dimension.Strength)

        if (!magnetDetected && currentForce > MAG_THRESHOLD_HIGH) {
            // State 1: Magnet has entered the sensor range
            magnetDetected = true
            spinCount += 1

            // Capture the temp right now (while hamster is running)
            tempSum += input.temperature()

        } else if (magnetDetected && currentForce < MAG_THRESHOLD_LOW) {
            magnetDetected = false
        }

        basic.pause(1)
    }
})

// --- Button A: Save to Database & Reset ---
input.onButtonPressed(Button.A, function () {

    // Calculate average temperature
    if (spinCount > 0) {
        // Divide total accumulated degrees by the number of spins
        activeTemp = tempSum / spinCount
    } else {
        // If no spins happened, just use current temp as a fallback
        activeTemp = input.temperature()
    }

    // 1. Log the data (Spins AND Temp)
    datalogger.log(
        datalogger.createCV("HamsterSpins", spinCount),
        datalogger.createCV("ActiveTemp", activeTemp)
    )

    // 2. Visual confirmation (Checkmark)
    basic.clearScreen()
    basic.showIcon(IconNames.Yes)
    basic.pause(500)

    // 3. Reset count AND temp variables
    spinCount = 0
    tempSum = 0
    basic.clearScreen()
})

// --- Button B: Toggle LED Display ---
input.onButtonPressed(Button.B, function () {
    isDisplayOn = !isDisplayOn
    if (!isDisplayOn) {
        basic.clearScreen()
    }
})

// --- Main Display Loop ---
basic.forever(function () {
    if (isDisplayOn) {
        basic.showNumber(spinCount)
    } else {
        basic.pause(100)
    }
})

Notes mentioning this note

Updated: