The Trace

Why one button press counts as five

Issue 1,

Gemini 4 Argon launches, Windows 11 24H2 Home and Pro get their final patch, and Synaptics puts Google's open Coral NPU in a microcontroller. Plus: why one button press can count as five, and three ways to fix it.

This week

Google releases Gemini 4 Argon at an introductory price

Google made Gemini 4 Argon, the first model in its Gemini 4 family, generally available on October 13 through the Gemini API and AI Studio, and in the Gemini app for AI Pro and Ultra subscribers. It handles up to 1 million tokens of combined input and output. API pricing starts at $2 per million input tokens and $10 per million output tokens, rising to $4 and $20 after November 30.

If you’re building document or coding tools on it, the next six weeks are a cheap window to test. Budget for the price doubling after that.

Source: Google

Meta publishes Muse Spark Open, an open-weight model you can run yourself

On October 15 Meta released downloadable weights for Muse Spark Open, a mixture-of-experts model with 816 billion parameters in total and 42 billion active at a time. It accepts text, images, video and audio, with a 1-million-token context. Running it takes at least 700 GB of GPU memory, or two M5 Ultra Mac Studios, and the license requires a “Built with Muse Spark Open” credit.

Open weights matter to companies that can’t send their data to a cloud API, though this one needs far more hardware than a typical office server.

Sources: Meta on Hugging Face, TechCrunch

Windows 11 24H2 Home and Pro get their final security update

Microsoft’s October 13 Patch Tuesday fixed a record 1,411 flaws, 70 of them rated critical. One, CVE-2026-96951, is already being exploited: a crafted file can skip Windows’ “Mark of the Web” warning. The same release was the last security update for Windows 11 version 24H2 Home and Pro.

If any office PCs still run 24H2 Home or Pro, install October’s update, then move them to a newer, supported version.

Sources: BleepingComputer, Microsoft lifecycle

A gap in npm’s Trusted Publishing left 1,500 packages open to takeover

Researchers at Bloom Security found that npm’s Trusted Publishing linked each package to its GitHub repository by name rather than by permanent ID. If a repository was renamed, transferred or deleted, someone could claim the freed name and publish new versions of the package. More than 1,500 packages, with over 10 million downloads between them, were exposed. There’s no evidence yet that anyone exploited it. GitHub’s fix makes unvalidated configurations expire after 48 hours.

If you publish to npm, check your packages’ Trusted Publishing settings and confirm every linked repository is still yours.

Sources: Help Net Security, GitHub changelog

Synaptics puts Google’s open Coral NPU into a microcontroller

On October 20 Synaptics announced the Astra SR200 series: microcontrollers that pair Arm Cortex-M85 and M52 cores with Google’s open-source Coral NPU, rated from 200 to 400 GOPS, with up to 8 MB of SRAM. Industrial versions add gigabit Ethernet with TSN and two CAN-FD ports. The chips are sampling now, with no published prices. Separately, the Coralboard SL development board went on sale through DigiKey for $99.79.

Small AI models on microcontroller-class chips mean lower power and no cloud dependency for sensors, wearables and machine monitoring.

Sources: Synaptics via GlobeNewswire, VentureBeat

Agility’s next humanoid robot recharges in nine minutes

Agility Robotics picked UK battery maker Nyobolt for Digit 5. The pack is rated for 90 minutes of work and a 9-minute recharge, a 10-to-1 run-to-charge ratio against 2-to-1 on Digit 4. Agility says that allows more than 20 working hours a day. Early access is expected in the first half of 2027.

In warehouse automation, charging downtime decides how many robots it takes to cover a shift, so faster charging means a smaller fleet.

Source: The Robot Report

Lesson: Why one button press counts as five

You wire a push button to an ESP32, write a few lines to count presses, press it once, and the counter jumps by three. Nothing is broken. The button is bouncing, and every project with a physical switch has to deal with it.

Why it happens

A push button is two pieces of metal snapping together. When they meet, they bounce apart and back several times before settling, usually within a few milliseconds and occasionally over ten or more. To you it’s one press. To a microcontroller reading the pin a thousand times a second, it’s a burst of on-off-on-off, and the same thing happens again when you let go.

Here’s the code most people write first. The button is wired between GPIO 14 and ground and uses the internal pull-up, so the pin reads 1 normally and 0 while the button is held down.

from machine import Pin
import time

button = Pin(14, Pin.IN, Pin.PULL_UP)
presses = 0
previous = button.value()

while True:
    current = button.value()
    if previous == 1 and current == 0:  # a 1-to-0 edge means "pressed"
        presses += 1
        print("Press", presses)
    previous = current
    time.sleep_ms(1)

Every 1-to-0 edge counts as a press, so every bounce counts too. We ran this loop against a simulated button with a few milliseconds of bounce on the way down and on the way up, and one press came out as five.

The fix: wait for the signal to settle

The standard software fix is to believe a new reading only once it has stayed the same for a short window, typically 10 to 50 ms. Any change restarts the clock.

from machine import Pin
import time

button = Pin(14, Pin.IN, Pin.PULL_UP)

DEBOUNCE_MS = 20
last_reading = button.value()
last_change = time.ticks_ms()
stable = last_reading
presses = 0

while True:
    reading = button.value()
    now = time.ticks_ms()
    if reading != last_reading:
        last_reading = reading
        last_change = now  # the pin moved: restart the clock
    elif reading != stable and time.ticks_diff(now, last_change) >= DEBOUNCE_MS:
        stable = reading  # steady for the whole window: accept it
        if stable == 0:
            presses += 1
            print("Press", presses)
    time.sleep_ms(1)

Three variables do the work. last_reading is what the pin said on the previous loop, last_change is when it last moved, and stable is the state we’ve accepted. Bounces keep resetting last_change, so stable only updates once the pin has been quiet for DEBOUNCE_MS. Use time.ticks_diff() rather than plain subtraction: MicroPython’s millisecond counter eventually wraps around to zero, and ticks_diff handles that correctly.

If you’re using interrupts

Polling is fine when your loop runs often. If you’d rather have the button trigger an interrupt, the bounce turns into a burst of interrupts instead. A popular shortcut is to ignore any edge that arrives within, say, 200 ms of the last one. It works in a quick demo and then fails in a subtle way: hold the button longer than the lockout, and the bounce on release produces fresh 1-to-0 edges that count as a second press.

A sturdier pattern lets every edge restart a short one-shot timer, and only reads the pin once the timer finally fires:

from machine import Pin, Timer

button = Pin(14, Pin.IN, Pin.PULL_UP)
settle = Timer(0)
stable = button.value()
presses = 0

def on_settled(timer):
    global stable, presses
    reading = button.value()
    if reading != stable:
        stable = reading
        if stable == 0:
            presses += 1

def on_edge(pin):
    # Every edge pushes the decision another 20 ms into the future.
    settle.init(mode=Timer.ONE_SHOT, period=20, callback=on_settled)

button.irq(trigger=Pin.IRQ_FALLING | Pin.IRQ_RISING, handler=on_edge)

The interrupt handler does almost nothing, which is exactly what you want from an interrupt. The real decision happens 20 ms after the last edge, once the contacts have stopped moving.

Or fix it in hardware

A resistor and capacitor can smooth the bounce out before it ever reaches the pin. A 10 kΩ resistor with a 1 µF capacitor gives a time constant (R × C) of 10 ms. The filtered signal rises slowly, so follow it with a Schmitt-trigger input such as a 74HC14, which turns the slow ramp into one clean switch. The 74HC14 also inverts the signal, so a press reads 1 instead of 0. Hardware debouncing earns its place on long cable runs, electrically noisy machinery, and inputs that feed a hardware counter you can’t filter in code.

The same idea on the web

Debouncing isn’t only for hardware. A search box that queries a server on every keystroke has the same problem: one intent arrives as five events. The fix is the same in spirit: wait until the input has been quiet for a moment.

function debounce(fn, waitMs) {
  let timer;
  return (...args) => {
    clearTimeout(timer);
    timer = setTimeout(() => fn(...args), waitMs);
  };
}

const searchBox = document.querySelector('#search');
searchBox.addEventListener(
  'input',
  debounce((event) => search(event.target.value), 300),
);

Type “hello” at a normal pace and search runs once, 300 ms after the last key, instead of five times.

Which to use

  • A settle window in your main loop is the right default for most projects. It’s simple and easy to test.
  • A timer restarted by interrupts suits devices that sleep or stay busy and still need to react to the button.
  • An RC filter with a Schmitt trigger is for signals that feed hardware directly, or environments full of electrical noise.

To try this on a real board, the Buttons chapter of our free MicroPython starter guide walks through wiring a button to an ESP32 from scratch.

Quiz

5 questions on this issue. Pick an answer to see whether you got it.

  1. Which Windows 11 version received its last security update for Home and Pro editions in October's Patch Tuesday?

  2. Why can one press of a push button register as several presses?

  3. In the polling fix, what happens to the timer each time the pin's reading changes?

  4. Which resistor and capacitor pair gives an RC time constant of about 10 ms?

  5. A search box is debounced at 300 ms. Someone types "hello" with 60 ms between keys. How many times does the search run?