Standup Timer
Counts down from 5 minutes, flashes at 1, says “fuck off!” after 0:00.
| System | AWTRIX NG Scripts |
|---|---|
| Firmware | AWTRIX 3 |
| Topic | Miscellaneous |
| Built by | Matt Stein |
| File | csKT6Uw8OzqH.ax · 7.3 KB |
| Icons | — |
| Published | 29 Aug 2026 · updated 29 Aug 2026 |
A helpful little timer for practicing your five-minute comedy set. Counts down from five (or otherwise configurable) minutes, flashes as you have one minute left, and makes it clear when your time is up. :)
csKT6Uw8OzqH.ax
# @name Standup Timer
# @desc For practicing fixed-time standup comedy sets.
# @author Matt Stein
# @version 1.0
# @desc SELECT: tap to start/pause, double-tap to reset. Flashes for the
# @desc last 5s before the countdown reaches the configured warning mark.
# @config minutes slider "Minutes" default=5 min=1 max=15 unit=min
# @config flash_min slider "Flash at (min left)" default=1 min=1 max=14 unit=min
# @config icon_id text "Icon ID" default="1010"
# 1-minute warning: five seconds of flashing that finishes right as the
# countdown crosses the threshold, so it reads as a lead-up rather than a
# reaction.
FLASH_ON_MS = 500
FLASH_OFF_MS = 500
FLASH_COUNT = 5
FLASH_TOTAL_MS = FLASH_COUNT * (FLASH_ON_MS + FLASH_OFF_MS)
# Flip on to sanity-check the flash and 0:00 transitions without sitting
# through a full countdown: a run starts 15s before the flash window and the
# flash always ends 5s before 0:00, ignoring the minutes/flash_min config.
TEST_MODE = false
TEST_PRE_FLASH_MS = 15000
TEST_THRESHOLD_MS = 5000
# 0:00 closing sequence: a short black beat, then the message blinks until
# the button starts a new run or resets.
DONE_DIP_MS = 600
DONE_MSG_CHARS = ["F", "U", "C", "K", " ", "O", "F", "F", "!"]
class StandupTimer
var state # "stopped" | "running" | "paused" | "done"
var start_ms
var remaining_ms # frozen remaining time while paused/stopped/done
var warned # flash warning already fired this run
var flashing
var flash_start_ms
var last_press_ms
var pending_toggle
var done_start_ms # when the countdown hit zero, for the 0:00 hold + blink sequence
def init()
self.state = "stopped"
self.start_ms = 0
self.remaining_ms = self.duration_ms()
self.warned = false
self.flashing = false
self.last_press_ms = 0
self.pending_toggle = false
self.done_start_ms = 0
end
# Convenience variables — both configurable from the app's settings in the
# AWTRIX UI, no code edits needed.
def duration_ms()
if TEST_MODE
return TEST_PRE_FLASH_MS + FLASH_TOTAL_MS + TEST_THRESHOLD_MS
end
return store.get("minutes", 5) * 60000
end
def flash_threshold_ms()
if TEST_MODE
return TEST_THRESHOLD_MS
end
return store.get("flash_min", 1) * 60000
end
def on_button(btn)
if btn != "select" return end
var t = now_ms()
if self.pending_toggle && (t - self.last_press_ms) < 400
self.pending_toggle = false
self.reset()
else
self.pending_toggle = true
self.last_press_ms = t
end
end
def reset()
self.state = "stopped"
self.remaining_ms = self.duration_ms()
self.warned = false
self.flashing = false
end
# A single tap after the countdown finishes jumps straight into a fresh run —
# handy for back-to-back practice reps. Double-tap still resets to idle.
def toggle()
if self.state == "stopped" || self.state == "done"
self.state = "running"
self.remaining_ms = self.duration_ms()
self.warned = false
self.start_ms = now_ms()
elif self.state == "running"
self.remaining_ms = self.current_remaining()
self.state = "paused"
elif self.state == "paused"
self.state = "running"
self.start_ms = now_ms() - (self.duration_ms() - self.remaining_ms)
end
end
def current_remaining()
var r = self.duration_ms() - (now_ms() - self.start_ms)
if r < 0
r = 0
end
return r
end
# Draws chars edge-to-edge: measures each glyph, then spreads the leftover
# width evenly across the gaps so the message spans the full panel width
# instead of sitting centered with default spacing. The gap at the space
# between words gets one extra pixel on top of that.
def draw_fill_width(chars, y, color)
var n = size(chars)
if n == 0 return end
var widths = []
var total_w = 0
var i = 0
while i < n
var cw = text_ink_width(chars[i])
widths.push(cw)
total_w += cw
i += 1
end
var gaps = n - 1
var extra = 0
if gaps > 0
extra = int((width() - total_w) / gaps)
if extra < 0
extra = 0
end
end
var x = 0
i = 0
while i < n
text(x, y, chars[i], color)
x += widths[i] + extra
if chars[i] == " "
x += 1
end
i += 1
end
end
def draw()
if self.pending_toggle && (now_ms() - self.last_press_ms) >= 400
self.pending_toggle = false
self.toggle()
end
if self.state == "running"
self.remaining_ms = self.current_remaining()
# Cross into "done" (and red) the instant the displayed time would
# read 0:00, not only once the underlying ms reaches exactly zero —
# otherwise the last fractional second still shows green.
if self.remaining_ms < 1000
self.state = "done"
self.remaining_ms = 0
self.done_start_ms = now_ms()
elif !self.warned && self.remaining_ms <= self.flash_threshold_ms() + FLASH_TOTAL_MS
self.warned = true
self.flashing = true
self.flash_start_ms = now_ms()
end
end
# Once the countdown's been sitting at 0:00 for 5s, drop the readout
# entirely: dip to black briefly, then blink the closing message until
# the button starts a new run or resets.
if self.state == "done" && (now_ms() - self.done_start_ms) >= 5000
clear()
var t2 = now_ms() - self.done_start_ms - 5000
if t2 < DONE_DIP_MS
return
end
var phase = int((t2 - DONE_DIP_MS) / 750) % 2
if phase == 0
self.draw_fill_width(DONE_MSG_CHARS, 6, 0xFF0000)
end
return
end
clear()
var mic = store.get("icon_id", "")
if mic != ""
icon(mic, 0, 0)
end
var total_s = int(self.remaining_ms / 1000)
var m = int(total_s / 60)
var s = total_s % 60
var s_str = str(s)
if s < 10
s_str = "0" + s_str
end
var label = str(m) + ":" + s_str
var color = 0xFFFFFF
if self.state == "running"
# Once the flash warning has fired, the numbers stay yellow for the
# rest of the run — a held cue between the white flash and the red
# 0:00 state, not just green until it suddenly goes red.
if self.warned
color = 0xFFFF00
else
color = 0x00FF00
end
elif self.state == "paused"
color = 0x888888
elif self.state == "done"
color = 0xFF0000
end
# Right-justified: the readout's width varies (0:00 vs 14:59), so anchor
# its right edge instead of its left.
var label_w = text_ink_width(label)
var label_x = width() - 1 - label_w
if self.flashing
var t = now_ms() - self.flash_start_ms
if t >= FLASH_TOTAL_MS
self.flashing = false
else
var cycle = t % (FLASH_ON_MS + FLASH_OFF_MS)
if cycle < FLASH_ON_MS
# Flash the whole panel — fine to blot out the icon — but the
# digits still draw on top below, so they stay legible.
rect_fill(0, 0, width(), height(), 0xFFFFFF)
end
end
end
text(label_x, 6, label, color)
end
end
return StandupTimer()
Install it on your AWTRIX NG
Your browser writes the script straight to the device on your network. It joins the app rotation right away.
Some browsers refuse to talk to a device on your network from a public page. Download the flow and paste it into the Scripts tab of your device, or install it from a terminal:
More flows for AWTRIX NG Scripts or Miscellaneous
Something wrong with this flow? Report it.