01 PROJECT FlipForm
01 PROJECT FlipForm
|
02
02
< Re-orientable flip clock for time, focus, and play >
< Re-orientable flip clock for time, focus, and play >
03
03
_ Spring 2025 / Prototyped Fall 2025 _
_ Spring 2025 / Prototyped Fall 2025 _
04
04
log.begin {
Time doesn’t stand still. Flipform responds.
In landscape, it is a digital flip-style clock: clean, animated, and always ticking.
Rotated upright, it becomes a Pomodoro timer, hourglass, or mini-game interface for quick, focused breaks.
Its custom OLED display adapts to your style (minimal, retro, 8bit and more), adding rhythm, presence, and a spark of fun to the workspace.
"A screen that keeps time—and occasionally plays with it."
}
< view log >


+ PROTOTYPE
+ PROTOTYPE
+ CODE




+ CODE
import time, math, gc import board, busio, displayio, terminalio from fourwire import FourWire from adafruit_ili9341 import ILI9341 from adafruit_display_text import label from adafruit_display_shapes.rect import Rect from adafruit_mpu6050 import MPU6050 # ======================================================= # CONFIG # ======================================================= HOURGLASS_DURATION = 10.0 FRAME_INTERVAL = 0.016 IMU_INTERVAL = 0.2 PITCH_ENTER_THRESH = -50.0 # enter hourglass PITCH_EXIT_THRESH = -20.0 # back to clock ENTER_DEBOUNCE_READS = 2 CLOCK_X_NUDGE = 10 RED = 0xFF0000 BLACK = 0x000000 CLOCK_W, CLOCK_H = 320, 240 HOUR_W, HOUR_H = 320, 240 # ======================================================= # DISPLAY SETUP # ======================================================= displayio.release_displays() spi = busio.SPI(clock=board.GP14, MOSI=board.GP15) tft_cs, tft_dc, tft_rst = board.GP13, board.GP12, board.GP11 bus = FourWire(spi, command=tft_dc, chip_select=tft_cs, reset=tft_rst) display = ILI9341(bus, width=CLOCK_W, height=CLOCK_H, rotation=0) # Clock UI clock_group = displayio.Group() display.root_group = clock_group clock_group.append(Rect(0, 0, CLOCK_W, CLOCK_H, fill=BLACK)) clock_label = label.Label( terminalio.FONT, text="09:41", color=RED, scale=10, anchor_point=(0.5, 0.5), anchored_position=(CLOCK_W // 2 + CLOCK_X_NUDGE, CLOCK_H // 2) ) clock_group.append(clock_label) # Hourglass UI hourglass_group = displayio.Group() hg_bitmap = displayio.Bitmap(HOUR_W, HOUR_H, 2) hg_palette = displayio.Palette(2) hg_palette[0] = BLACK hg_palette[1] = RED hg_tile = displayio.TileGrid(hg_bitmap, pixel_shader=hg_palette) hourglass_group.append(hg_tile) fill_rect = None def hg_refill_fast(): """Instant fill using Rect, and sync it into bitmap in one step.""" global fill_rect if fill_rect is not None and fill_rect in hourglass_group: hourglass_group.remove(fill_rect) fill_rect = Rect(0, 0, HOUR_W, HOUR_H, fill=RED) hourglass_group.append(fill_rect) hg_bitmap.fill(1) gc.collect() def hg_switch_to_bitmap(): """Remove rect so bitmap underneath is now visible.""" global fill_rect if fill_rect is not None and fill_rect in hourglass_group: hourglass_group.remove(fill_rect) fill_rect = None def hg_clear_columns(prev_cols, new_cols): """Smooth continuous depletion left→right.""" if new_cols <= prev_cols: return if new_cols > HOUR_W: new_cols = HOUR_W # flipped direction: left→right for x in range(prev_cols, new_cols): if x >= HOUR_W: break for y in range(HOUR_H): hg_bitmap[x, y] = 0 # ======================================================= # IMU SETUP (MPU6050) # ======================================================= i2c = busio.I2C(board.GP17, board.GP16) mpu = MPU6050(i2c) def read_orientation(): """Return roll, pitch, yaw (deg) computed from accelerometer only.""" try: ax, ay, az = mpu.acceleration roll = math.degrees(math.atan2(ay, az)) pitch = math.degrees(math.atan2(-ax, math.sqrt(ay * ay + az * az))) yaw = math.degrees(math.atan2(ay, ax)) # pseudo yaw return roll, pitch, yaw except Exception: return None, None, None # ======================================================= # STATE # ======================================================= mode = "clock" start_hour, start_minute = 9, 41 manual_time_start = time.monotonic() print("System initialized — global reference active.") last_imu_read = 0 enter_consec = 0 hourglass_start = 0 cleared_cols = 0 can_enter = True can_exit = False bitmap_visible = False # ======================================================= # MAIN LOOP # ======================================================= while True: now = time.monotonic() if mode == "clock": if now - last_imu_read > IMU_INTERVAL: roll, pitch, yaw = read_orientation() if pitch is not None: print("Roll: {:6.1f}°, Pitch: {:6.1f}°, Yaw: {:6.1f}°".format(roll, pitch, yaw)) if can_enter and pitch <= PITCH_ENTER_THRESH: enter_consec += 1 else: enter_consec = 0 if can_enter and enter_consec >= ENTER_DEBOUNCE_READS: mode = "hourglass" display.root_group = hourglass_group hg_refill_fast() bitmap_visible = False cleared_cols = 0 hourglass_start = now can_enter = False can_exit = True print("→ Hourglass started (Pitch {:.1f}°)".format(pitch)) last_imu_read = now # update clock display mins = int((now - manual_time_start) / 60) h = (start_hour + (start_minute + mins) // 60) % 24 m = (start_minute + mins) % 60 clock_label.text = f"{h:02d}:{m:02d}" elif mode == "hourglass": elapsed = now - hourglass_start if not bitmap_visible and elapsed > 0.3: hg_switch_to_bitmap() bitmap_visible = True if elapsed >= HOURGLASS_DURATION: hg_clear_columns(cleared_cols, HOUR_W) cleared_cols = HOUR_W roll, pitch, yaw = read_orientation() if pitch is not None: print("[Hourglass] Roll: {:6.1f}°, Pitch: {:6.1f}°, Yaw: {:6.1f}°".format(roll, pitch, yaw)) if can_exit and pitch >= PITCH_EXIT_THRESH: display.root_group = clock_group mode = "clock" can_enter = True can_exit = False print("→ Back to clock (Pitch {:.1f}°)".format(pitch)) else: if bitmap_visible: target_cols = int((elapsed / HOURGLASS_DURATION) * HOUR_W) if target_cols > cleared_cols: hg_clear_columns(cleared_cols, target_cols) cleared_cols = target_cols time.sleep(FRAME_INTERVAL)
import time, math, gc import board, busio, displayio, terminalio from fourwire import FourWire from adafruit_ili9341 import ILI9341 from adafruit_display_text import label from adafruit_display_shapes.rect import Rect from adafruit_mpu6050 import MPU6050 # ======================================================= # CONFIG # ======================================================= HOURGLASS_DURATION = 10.0 FRAME_INTERVAL = 0.016 IMU_INTERVAL = 0.2 PITCH_ENTER_THRESH = -50.0 # enter hourglass PITCH_EXIT_THRESH = -20.0 # back to clock ENTER_DEBOUNCE_READS = 2 CLOCK_X_NUDGE = 10 RED = 0xFF0000 BLACK = 0x000000 CLOCK_W, CLOCK_H = 320, 240 HOUR_W, HOUR_H = 320, 240 # ======================================================= # DISPLAY SETUP # ======================================================= displayio.release_displays() spi = busio.SPI(clock=board.GP14, MOSI=board.GP15) tft_cs, tft_dc, tft_rst = board.GP13, board.GP12, board.GP11 bus = FourWire(spi, command=tft_dc, chip_select=tft_cs, reset=tft_rst) display = ILI9341(bus, width=CLOCK_W, height=CLOCK_H, rotation=0) # Clock UI clock_group = displayio.Group() display.root_group = clock_group clock_group.append(Rect(0, 0, CLOCK_W, CLOCK_H, fill=BLACK)) clock_label = label.Label( terminalio.FONT, text="09:41", color=RED, scale=10, anchor_point=(0.5, 0.5), anchored_position=(CLOCK_W // 2 + CLOCK_X_NUDGE, CLOCK_H // 2) ) clock_group.append(clock_label) # Hourglass UI hourglass_group = displayio.Group() hg_bitmap = displayio.Bitmap(HOUR_W, HOUR_H, 2) hg_palette = displayio.Palette(2) hg_palette[0] = BLACK hg_palette[1] = RED hg_tile = displayio.TileGrid(hg_bitmap, pixel_shader=hg_palette) hourglass_group.append(hg_tile) fill_rect = None def hg_refill_fast(): """Instant fill using Rect, and sync it into bitmap in one step.""" global fill_rect if fill_rect is not None and fill_rect in hourglass_group: hourglass_group.remove(fill_rect) fill_rect = Rect(0, 0, HOUR_W, HOUR_H, fill=RED) hourglass_group.append(fill_rect) hg_bitmap.fill(1) gc.collect() def hg_switch_to_bitmap(): """Remove rect so bitmap underneath is now visible.""" global fill_rect if fill_rect is not None and fill_rect in hourglass_group: hourglass_group.remove(fill_rect) fill_rect = None def hg_clear_columns(prev_cols, new_cols): """Smooth continuous depletion left→right.""" if new_cols <= prev_cols: return if new_cols > HOUR_W: new_cols = HOUR_W # flipped direction: left→right for x in range(prev_cols, new_cols): if x >= HOUR_W: break for y in range(HOUR_H): hg_bitmap[x, y] = 0 # ======================================================= # IMU SETUP (MPU6050) # ======================================================= i2c = busio.I2C(board.GP17, board.GP16) mpu = MPU6050(i2c) def read_orientation(): """Return roll, pitch, yaw (deg) computed from accelerometer only.""" try: ax, ay, az = mpu.acceleration roll = math.degrees(math.atan2(ay, az)) pitch = math.degrees(math.atan2(-ax, math.sqrt(ay * ay + az * az))) yaw = math.degrees(math.atan2(ay, ax)) # pseudo yaw return roll, pitch, yaw except Exception: return None, None, None # ======================================================= # STATE # ======================================================= mode = "clock" start_hour, start_minute = 9, 41 manual_time_start = time.monotonic() print("System initialized — global reference active.") last_imu_read = 0 enter_consec = 0 hourglass_start = 0 cleared_cols = 0 can_enter = True can_exit = False bitmap_visible = False # ======================================================= # MAIN LOOP # ======================================================= while True: now = time.monotonic() if mode == "clock": if now - last_imu_read > IMU_INTERVAL: roll, pitch, yaw = read_orientation() if pitch is not None: print("Roll: {:6.1f}°, Pitch: {:6.1f}°, Yaw: {:6.1f}°".format(roll, pitch, yaw)) if can_enter and pitch <= PITCH_ENTER_THRESH: enter_consec += 1 else: enter_consec = 0 if can_enter and enter_consec >= ENTER_DEBOUNCE_READS: mode = "hourglass" display.root_group = hourglass_group hg_refill_fast() bitmap_visible = False cleared_cols = 0 hourglass_start = now can_enter = False can_exit = True print("→ Hourglass started (Pitch {:.1f}°)".format(pitch)) last_imu_read = now # update clock display mins = int((now - manual_time_start) / 60) h = (start_hour + (start_minute + mins) // 60) % 24 m = (start_minute + mins) % 60 clock_label.text = f"{h:02d}:{m:02d}" elif mode == "hourglass": elapsed = now - hourglass_start if not bitmap_visible and elapsed > 0.3: hg_switch_to_bitmap() bitmap_visible = True if elapsed >= HOURGLASS_DURATION: hg_clear_columns(cleared_cols, HOUR_W) cleared_cols = HOUR_W roll, pitch, yaw = read_orientation() if pitch is not None: print("[Hourglass] Roll: {:6.1f}°, Pitch: {:6.1f}°, Yaw: {:6.1f}°".format(roll, pitch, yaw)) if can_exit and pitch >= PITCH_EXIT_THRESH: display.root_group = clock_group mode = "clock" can_enter = True can_exit = False print("→ Back to clock (Pitch {:.1f}°)".format(pitch)) else: if bitmap_visible: target_cols = int((elapsed / HOURGLASS_DURATION) * HOUR_W) if target_cols > cleared_cols: hg_clear_columns(cleared_cols, target_cols) cleared_cols = target_cols time.sleep(FRAME_INTERVAL)