Infineon Team
Published © MIT

Oktoberfest Special 🥨Smart Beer Stein using PSOC™6🍻

Gyroscope & Accelerometer powered smart stein for Oktoberfest, using PSOC™ 6 and MicroPython!

IntermediateFull instructions provided6 hours271

Things used in this project

Hardware components

PSOC™ 6 AI Evaluation Kit (CY8CKIT-062S2-AI)
Infineon PSOC™ 6 AI Evaluation Kit (CY8CKIT-062S2-AI)
×1
WS2812B RGB LED Digital Flexibel
×1
Li-Ion Battery 1000mAh
Li-Ion Battery 1000mAh
×1
Glass
×1

Software apps and online services

MicroPython
MicroPython

Hand tools and fabrication machines

3D Printer (generic)
3D Printer (generic)

Story

Read more

Custom parts and enclosures

Beer stein

only the beer stein

Undercup/Board housing

Inner stein

Schematics

Schematics

Code

Main Code

MicroPython
import time, math
from machine import I2C, Pin
import neopixel
from statistics import mean
import urandom as random
import random
import bmi270

# -------------------------
# Helpers
# -------------------------
G = 9.80665
def ms(): return time.ticks_ms()
def dt(a,b): return time.ticks_diff(a,b)

def mag(x,y,z): return math.sqrt(x*x + y*y + z*z)

def angle_deg(ax, ay, az, bx, by, bz):
    am = mag(ax, ay, az)
    bm = mag(bx, by, bz)
    if am == 0 or bm == 0: 
        return 0.0
    c = max(-1.0, min(1.0, (ax*bx + ay*by + az*bz) / (am * bm)))
    return math.degrees(math.acos(c))

def clamp(v, lo, hi): 
    return lo if v < lo else (hi if v > hi else v)

# -------------------------
# NeoPixel matrix (8x32)
# -------------------------
W = 8
H = 32
N = W * H
PIN_NAME = "P9_7"
np = neopixel.NeoPixel(Pin(PIN_NAME), N)

def clear():
    for i in range(N):
        np[i] = (0, 0, 0)

def write():
    np.write()

def set_num(n, color):
    # 1-based indexing per your original drawings
    if 1 <= n <= N:
        np[n - 1] = color

def set_nums(numbers, color):
    for n in numbers:
        if 1 <= n <= N:
            np[n - 1] = color

def scale_color(color, scale):
    r, g, b = color
    return (int(clamp(r * scale, 0, 255)),
            int(clamp(g * scale, 0, 255)),
            int(clamp(b * scale, 0, 255)))

# Animations 

# 1) Dreamy hearts
HEART_BASE = [22,12,4,3,15,19,31,35,36,28]
HEART_STEP = 48  # one step = 6 rows
HEART_COLOR = (255, 0, 80)

def heart_indices_with_multiplier(m, step=HEART_STEP):
    offset = m * step
    shifted = [n + offset for n in HEART_BASE]
    return [nn for nn in shifted if 1 <= nn <= N]

def draw_hearts_multipliers(mults, color=HEART_COLOR, scale=1.0):
    c = scale_color(color, scale)
    for m in mults:
        idxs = heart_indices_with_multiplier(m)
        if idxs:
            set_nums(idxs, c)

class HeartsAnim:
    def __init__(self, group_a=(1,3), group_b=(2,4), steps=16, step_ms=30, color=HEART_COLOR):
        # steps increased, step_ms increased for longer, smoother crossfade
        self.group_a = group_a
        self.group_b = group_b
        self.steps = max(1, steps)
        self.step_ms = step_ms
        self.color = color
        self.t_step = 0
        self.phase_a_to_b = True
        self.last_ms = 0
    def reset(self):
        self.t_step = 0
        self.phase_a_to_b = True
        self.last_ms = 0
    def step(self, now):
        if self.last_ms and dt(now, self.last_ms) < self.step_ms:
            return
        self.last_ms = now
        p = self.t_step / float(self.steps)
        if not self.phase_a_to_b:
            p = 1.0 - p
        alpha_a = p
        alpha_b = 1.0 - p

        clear()
        draw_hearts_multipliers(self.group_a, self.color, alpha_a)
        draw_hearts_multipliers(self.group_b, self.color, alpha_b)
        write()

        self.t_step += 1
        if self.t_step > self.steps:
            self.t_step = 0
            self.phase_a_to_b = not self.phase_a_to_b

# 2) Big pretzel dreamy (breathing + chase + sparkles)
PRETZEL_PATH = [105,103,91,85,77,78,82,95,98,109,110,108,102,87,74,71,54,44,45,46,50,63,66,69,59,55,41]
PRETZEL_BASE_COLOR = (153, 76, 0)
PRETZEL_CHASE_COLOR = (255, 200, 40)
PRETZEL_SPARKLE_COLOR = (255, 229, 204)
class BigPretzelDreamyAnim:
    def __init__(self,
                 step_ms=25,
                 breath_min=0.25, breath_max=0.9, breath_step=0.06,
                 head_speed=1, trail_len=7, tail_decay=0.68,
                 sparkle_prob=0.25, sparkle_decay=0.86,
                 mults=(0,), step_spacing=105):
        self.step_ms = step_ms
        self.breath_min = breath_min
        self.breath_max = breath_max
        self.breath_step = breath_step
        self.head_speed = head_speed
        self.trail_len = trail_len
        self.tail_decay = tail_decay
        self.sparkle_prob = sparkle_prob
        self.sparkle_decay = sparkle_decay
        self.mults = mults
        self.step_spacing = step_spacing

        self.path = PRETZEL_PATH[:]
        self.L = len(self.path)
        self.breath = breath_min
        self.breath_dir = +1
        self.head = 0
        self.sparkle_levels = {}
        self.last_ms = 0

    def reset(self):
        self.breath = self.breath_min
        self.breath_dir = +1
        self.head = 0
        self.sparkle_levels = {}
        self.last_ms = 0

    def path_indices_with_multiplier(self, m, step):
        offset = m * step
        out = []
        for n in self.path:
            nn = n + offset
            if 1 <= nn <= N:
                out.append(nn)
        return out

    def step(self, now):
        if self.L == 0:
            return
        if self.last_ms and dt(now, self.last_ms) < self.step_ms:
            return
        self.last_ms = now

        # Breathing
        base_scale = self.breath
        self.breath += self.breath_dir * self.breath_step
        if self.breath >= self.breath_max:
            self.breath = self.breath_max
            self.breath_dir = -1
        elif self.breath <= self.breath_min:
            self.breath = self.breath_min
            self.breath_dir = +1

        frame_colors = {}
        base_c = scale_color(PRETZEL_BASE_COLOR, base_scale)
        for m in self.mults:
            idxs = self.path_indices_with_multiplier(m, self.step_spacing)
            for idx in idxs:
                frame_colors[idx] = base_c

        # Chase head + trail
        for k in range(self.trail_len):
            pos = (self.head - k) % self.L
            idx = self.path[pos]
            sc = scale_color(PRETZEL_CHASE_COLOR, (self.tail_decay ** k))
            prev = frame_colors.get(idx, (0,0,0))
            frame_colors[idx] = (max(prev[0], sc[0]), max(prev[1], sc[1]), max(prev[2], sc[2]))
        self.head = (self.head + self.head_speed) % self.L

        # Sparkles
        spawn = False
        try:
            spawn = (random.getrandbits(8) / 255.0) < self.sparkle_prob
        except:
            spawn = False
        if spawn:
            try:
                j = random.getrandbits(16) % self.L
            except:
                j = 0
            self.sparkle_levels[self.path[j]] = 1.0

        dead = []
        for idx, level in self.sparkle_levels.items():
            sc = scale_color(PRETZEL_SPARKLE_COLOR, level)
            prev = frame_colors.get(idx, (0,0,0))
            frame_colors[idx] = (max(prev[0], sc[0]), max(prev[1], sc[1]), max(prev[2], sc[2]))
            level *= self.sparkle_decay
            if level < 0.05:
                dead.append(idx)
            else:
                self.sparkle_levels[idx] = level
        for idx in dead:
            self.sparkle_levels.pop(idx, None)

        # Render
        clear()
        for n, c in frame_colors.items():
            set_num(n, c)
        write()

# 3) Small pretzel crossfade
SMALL_PRETZEL_PATH = [86,75,70,59,53,52,62,67,77,91,85,101,100,94,83,103,55,69,59]

def path_indices_with_multiplier(path, m, step):
    offset = m * step
    out = []
    for n in path:
        nn = n + offset
        if 1 <= nn <= N:
            out.append(nn)
    return out

def draw_path_multipliers(path, mults, color, scale=1.0, step=80):
    c = scale_color(color, scale)
    for m in mults:
        idxs = path_indices_with_multiplier(path, m, step)
        if idxs:
            set_nums(idxs, c)

class SmallPretzelAnim:
    def __init__(self,
                 group_a=(-1, 1), group_b=(-2, 2),
                 steps=12, step_ms=25,
                 color=(255, 200, 40), step_spacing=80,
                 background_mults=(0,), background_scale=0.25):
        self.group_a = group_a
        self.group_b = group_b
        self.steps = max(1, steps)
        self.step_ms = step_ms
        self.color = color
        self.step_spacing = step_spacing
        self.background_mults = background_mults
        self.background_scale = background_scale
        self.t_step = 0
        self.phase_a_to_b = True
        self.last_ms = 0
    def reset(self):
        self.t_step = 0
        self.phase_a_to_b = True
        self.last_ms = 0
    def step(self, now):
        if self.last_ms and dt(now, self.last_ms) < self.step_ms:
            return
        self.last_ms = now
        p = self.t_step / float(self.steps)
        if not self.phase_a_to_b:
            p = 1.0 - p
        alpha = p
        beta = 1.0 - p

        clear()
        if self.background_mults and self.background_scale > 0:
            draw_path_multipliers(SMALL_PRETZEL_PATH, self.background_mults, self.color, self.background_scale, step=self.step_spacing)
        draw_path_multipliers(SMALL_PRETZEL_PATH, self.group_a, self.color, alpha, step=self.step_spacing)
        draw_path_multipliers(SMALL_PRETZEL_PATH, self.group_b, self.color, beta,  step=self.step_spacing)
        write()

        self.t_step += 1
        if self.t_step > self.steps:
            self.t_step = 0
            self.phase_a_to_b = not self.phase_a_to_b

# 4) Beer crossfade
BEER_YELLOW_IDX = [100,103,104,102,101,93,84,77,76,75,74,73,88,89,69,59,71]
BEER_WHITE_IDX  = [112,98,95,96,82,80]
BEER_STEP = 48

def beer_indices_with_multiplier(m, step=BEER_STEP):
    offset = m * step
    y = [n + offset for n in BEER_YELLOW_IDX if 1 <= n + offset <= N]
    w = [n + offset for n in BEER_WHITE_IDX  if 1 <= n + offset <= N]
    return y, w

def draw_beer_multipliers(mults, yellow=(255,255,0), white=(255,255,255), scale=1.0, step=BEER_STEP):
    yc = scale_color(yellow, scale)
    wc = scale_color(white,  scale)
    for m in mults:
        y, w = beer_indices_with_multiplier(m, step)
        if y:
            set_nums(y, yc)
        if w:
            set_nums(w, wc)

class BeerAnim:
    def __init__(self,
                 group_a=(1,3), group_b=(2,4),
                 steps=8, step_ms=25,
                 yellow=(255,255,0), white=(255,255,255),
                 step_spacing=BEER_STEP,
                 background_mults=(), background_scale=0.25):
        self.group_a = group_a
        self.group_b = group_b
        self.steps = max(1, steps)
        self.step_ms = step_ms
        self.yellow = yellow
        self.white = white
        self.step_spacing = step_spacing
        self.background_mults = background_mults
        self.background_scale = background_scale
        self.t_step = 0
        self.phase_a_to_b = True
        self.last_ms = 0
    def reset(self):
        self.t_step = 0
        self.phase_a_to_b = True
        self.last_ms = 0
    def step(self, now):
        if self.last_ms and dt(now, self.last_ms) < self.step_ms:
            return
        self.last_ms = now
        p = self.t_step / float(self.steps)
        if not self.phase_a_to_b:
            p = 1.0 - p
        alpha = p
        beta = 1.0 - p

        clear()
        if self.background_mults and self.background_scale > 0:
            draw_beer_multipliers(self.background_mults, self.yellow, self.white, self.background_scale, step=self.step_spacing)
        draw_beer_multipliers(self.group_a, self.yellow, self.white, alpha, step=self.step_spacing)
        draw_beer_multipliers(self.group_b, self.yellow, self.white, beta,  step=self.step_spacing)
        write()

        self.t_step += 1
        if self.t_step > self.steps:
            self.t_step = 0
            self.phase_a_to_b = not self.phase_a_to_b


# BMI270 thresholds and setup

SAMPLE_MS = 20  # ~50 Hz

# --- thresholds ---
LIFT_TILT_DEG   = 15.0
MOVE_GYRO_DPS   = 60.0
TABLE_GYRO_QUIET= 12.0
TABLE_JERK_QUIET= 0.08
PUTDOWN_DWELL   = 800
STICKY_AFTER_MOVE = 1000

DRINK_TILT_DEG  = 55.0
DRINK_HOLD_MS   = 400
DRINK_EXIT_HYS  = 12.0

CHEERS_GYRO_MIN       = 80.0  # dps
CHEERS_JERK_THRESHOLD = 0.05   # g/s
CHEERS_MIN_INTERVAL   = 900   # ms between cheers
CHEERS_DURATION_MS    = 2500  

i2c = I2C(scl='P0_2', sda='P0_3')
bmi = bmi270.BMI270(i2c)

# --- calibration ---
N_CAL = 64
print("Calibrating... keep stein flat & still.")
sx = sy = sz = 0.0
for _ in range(N_CAL):
    ax, ay, az = bmi.accel()
    sx += ax; sy += ay; sz += az
    time.sleep_ms(SAMPLE_MS)
g0x, g0y, g0z = sx/N_CAL, sy/N_CAL, sz/N_CAL
print("Calibration done.")

# --- state ---
state = "ON_TABLE"; print("ON_TABLE")
drink_since = None
putdown_since = None
last_motion = ms()
on_table_since = ms()
last_cheers = -999999
cheers_since = None

ax0, ay0, az0 = bmi.accel()
prev_acc_mag = mag(ax0, ay0, az0)
prev_az = az0
# --- buffers for smoothing ---
gyro_buffer = []
jerk_z_buffer = []

# -------------------------
# Animator manager
# -------------------------
class Animator:
    def __init__(self):
        self.anims = {
            "CHEERS":    HeartsAnim(group_a=(1,3), group_b=(2,4), steps=16, step_ms=30, color=HEART_COLOR),
            "DRINKING":  BeerAnim(group_a=(-1,1,3), group_b=(2,0), steps=4, step_ms=25,
                                   yellow=(255,255,0), white=(255,255,255), step_spacing=48,
                                   background_mults=(), background_scale=0.25),
            "PICKED_UP": SmallPretzelAnim(group_a=(-1,1), group_b=(-2,2,0), steps=4, step_ms=25,
                                          color=(255, 200, 40), step_spacing=80,
                                          background_mults=(0,), background_scale=0.25),
            "ON_TABLE":  BigPretzelDreamyAnim(mults=(0, 2), step_spacing=48, step_ms=25, breath_min=0.25, breath_max=0.9, breath_step=0.06,
                                             head_speed=1, trail_len=7, tail_decay=0.68,
                                             sparkle_prob=0.25, sparkle_decay=0.86),
        }
        self.current_state = None
        self.current_anim = None

    def set_state(self, new_state):
        if new_state != self.current_state:
            self.current_state = new_state
            self.current_anim = self.anims.get(new_state)
            if self.current_anim:
                self.current_anim.reset()

    def step(self, now):
        if self.current_anim:
            self.current_anim.step(now)

animator = Animator()
animator.set_state(state)

# Main loop
while True:
    t = ms()

    # Read sensors
    ax, ay, az = bmi.accel()
    gx, gy, gz = bmi.gyro()
    gyro_mag = mag(gx, gy, gz)

    acc_mag = mag(ax, ay, az)
    jerk_g = abs(acc_mag - prev_acc_mag) / G
    prev_acc_mag = acc_mag

    tilt = angle_deg(ax, ay, az, g0x, g0y, g0z)
    acc_z_g = az / G
    jerk_z = abs(az - prev_az) / G
    prev_az = az

    # Update buffers
    gyro_buffer.append(gyro_mag)
    jerk_z_buffer.append(jerk_z)
    if len(gyro_buffer) > 3: gyro_buffer.pop(0)
    if len(jerk_z_buffer) > 3: jerk_z_buffer.pop(0)

    gyro_avg = mean(gyro_buffer)
    jerk_z_avg = mean(jerk_z_buffer)

    # Motion bookkeeping for sticky-on-table logic
    if (gyro_mag > TABLE_GYRO_QUIET) or (jerk_g > TABLE_JERK_QUIET):
        last_motion = t

    # ---------- CHEERS: peak detection ----------
    if (max(gyro_buffer) >= CHEERS_GYRO_MIN and
        max(jerk_z_buffer) > CHEERS_JERK_THRESHOLD and
        t - last_cheers > CHEERS_MIN_INTERVAL):
        last_cheers = t
        cheers_since = t
        state = "CHEERS"
        animator.set_state(state)
        drink_since = None
        putdown_since = None
        print("CHEERS")

    # ---------- Exit CHEERS after duration ----------
    if state == "CHEERS" and cheers_since is not None:
        if (t - cheers_since) >= CHEERS_DURATION_MS:
            cheers_since = None
            if tilt >= DRINK_TILT_DEG:
                state = "DRINKING"; print("DRINKING")
            elif tilt >= LIFT_TILT_DEG or gyro_mag >= MOVE_GYRO_DPS:
                state = "PICKED_UP"; print("PICKED_UP")
            else:
                state = "ON_TABLE"; print("ON_TABLE")
            animator.set_state(state)

    # ---------- DRINKING detection ----------
    if state != "CHEERS":
        if tilt >= DRINK_TILT_DEG:
            if drink_since is None:
                drink_since = t
            elif (t - drink_since) >= DRINK_HOLD_MS and state != "DRINKING":
                state = "DRINKING"; print("DRINKING")
                animator.set_state(state)
                putdown_since = None
        else:
            drink_since = None
            if state == "DRINKING" and tilt <= (DRINK_TILT_DEG - DRINK_EXIT_HYS):
                state = "PICKED_UP"; print("PICKED_UP")
                animator.set_state(state)

    # ---------- PICKED_UP / ON_TABLE ----------
    if state not in ["DRINKING", "CHEERS"]:
        if (tilt >= LIFT_TILT_DEG) or (gyro_mag >= MOVE_GYRO_DPS):
            if state != "PICKED_UP":
                state = "PICKED_UP"; print("PICKED_UP")
                animator.set_state(state)
            putdown_since = None
            on_table_since = t
        else:
            quiet_upright = (tilt <= LIFT_TILT_DEG and gyro_mag <= TABLE_GYRO_QUIET and jerk_g <= TABLE_JERK_QUIET)
            sticky_ok = (t - last_motion) >= STICKY_AFTER_MOVE
            if quiet_upright and sticky_ok:
                if putdown_since is None:
                    putdown_since = t
                elif (t - putdown_since) >= PUTDOWN_DWELL:
                    if state != "ON_TABLE":
                        state = "ON_TABLE"; print("ON_TABLE")
                        animator.set_state(state)
                    putdown_since = None
                    on_table_since = t
            else:
                putdown_since = None

    # Step the current animation (non-blocking)
    animator.step(t)

    # Pace sensor loop
    time.sleep_ms(SAMPLE_MS)

Credits

Infineon Team
113 projects • 184 followers

Comments