AnasSarkiz/BlinkBuddy
This code defines and renders a 24mm square PCB silkscreen with a centered QR code graphic and labeling for open project instructions.
- Version
- 1.0.11
- License
- unset
- Stars
- 0
programming/micropython/main.py
from machine import ADC, I2C, PWM, Pin, freq
from micropython import const
from time import sleep_ms, ticks_diff, ticks_ms
import framebuf
try:
freq(160_000_000)
except Exception as exc:
print("CPU frequency setup skipped:", exc)
SET_CONTRAST = const(0x81)
SET_ENTIRE_ON = const(0xA4)
SET_NORM_INV = const(0xA6)
SET_DISP = const(0xAE)
SET_MEM_ADDR = const(0x20)
SET_COL_ADDR = const(0x21)
SET_PAGE_ADDR = const(0x22)
SET_DISP_START_LINE = const(0x40)
SET_SEG_REMAP = const(0xA0)
SET_MUX_RATIO = const(0xA8)
SET_COM_OUT_DIR = const(0xC0)
SET_DISP_OFFSET = const(0xD3)
SET_COM_PIN_CFG = const(0xDA)
SET_DISP_CLK_DIV = const(0xD5)
SET_PRECHARGE = const(0xD9)
SET_VCOM_DESEL = const(0xDB)
SET_CHARGE_PUMP = const(0x8D)
class SSD1306(framebuf.FrameBuffer):
def __init__(self, width, height, external_vcc=False):
self.width = width
self.height = height
self.external_vcc = external_vcc
self.pages = self.height // 8
self.buffer = bytearray(self.pages * self.width)
super().__init__(self.buffer, self.width, self.height, framebuf.MONO_VLSB)
self.init_display()
def init_display(self):
for cmd in (
SET_DISP | 0x00,
SET_MEM_ADDR,
0x00,
SET_DISP_START_LINE | 0x00,
SET_SEG_REMAP | 0x01,
SET_MUX_RATIO,
self.height - 1,
SET_COM_OUT_DIR | 0x08,
SET_DISP_OFFSET,
0x00,
SET_COM_PIN_CFG,
0x02 if self.height == 32 else 0x12,
SET_DISP_CLK_DIV,
0x80,
SET_PRECHARGE,
0x22 if self.external_vcc else 0xF1,
SET_VCOM_DESEL,
0x30,
SET_CONTRAST,
0xFF,
SET_ENTIRE_ON,
SET_NORM_INV,
SET_CHARGE_PUMP,
0x10 if self.external_vcc else 0x14,
SET_DISP | 0x01,
):
self.write_cmd(cmd)
self.fill(0)
self.show()
def poweroff(self):
self.write_cmd(SET_DISP | 0x00)
def poweron(self):
self.write_cmd(SET_DISP | 0x01)
def contrast(self, contrast):
self.write_cmd(SET_CONTRAST)
self.write_cmd(contrast)
def invert(self, invert):
self.write_cmd(SET_NORM_INV | (invert & 1))
def show(self):
x0 = 0
x1 = self.width - 1
if self.width == 64:
x0 += 32
x1 += 32
self.write_cmd(SET_COL_ADDR)
self.write_cmd(x0)
self.write_cmd(x1)
self.write_cmd(SET_PAGE_ADDR)
self.write_cmd(0)
self.write_cmd(self.pages - 1)
self.write_data(self.buffer)
class SSD1306_I2C(SSD1306):
def __init__(self, width, height, i2c, addr=0x3C, external_vcc=False):
self.i2c = i2c
self.addr = addr
self.temp = bytearray(2)
self.write_list = [b"\x40", None]
super().__init__(width, height, external_vcc)
def write_cmd(self, cmd):
self.temp[0] = 0x80
self.temp[1] = cmd
self.i2c.writeto(self.addr, self.temp)
def write_data(self, buf):
self.write_list[1] = buf
self.i2c.writevto(self.addr, self.write_list)
PIN_LDR_LEFT = const(0)
PIN_LDR_RIGHT = const(1)
PIN_LED = const(3)
PIN_I2C_SDA = const(4)
PIN_I2C_SCL = const(5)
PIN_BUTTON_LEFT = const(6)
PIN_BUTTON_OK = const(7)
PIN_BUTTON_RIGHT = const(18)
PIN_BUZZER = const(10)
OLED_WIDTH = const(128)
OLED_HEIGHT = const(64)
OLED_ADDR_PRIMARY = const(0x3C)
OLED_ADDR_ALT = const(0x3D)
LIS3DH_ADDR = const(0x19)
SCREEN_EYES = const(0)
SCREEN_SENSORS = const(1)
SCREEN_ABOUT = const(2)
BLINK_DURATION_MS = const(170)
IDLE_BLINK_BASE_MS = const(3000)
IDLE_BLINK_SPREAD_MS = const(3000)
class Button:
def __init__(self, pin):
self.pin = Pin(pin, Pin.IN, Pin.PULL_UP)
self.stable = 1
self.last_raw = 1
self.last_change = ticks_ms()
def pressed(self):
raw = self.pin.value()
now = ticks_ms()
if raw != self.last_raw:
self.last_raw = raw
self.last_change = now
if ticks_diff(now, self.last_change) > 35 and raw != self.stable:
self.stable = raw
return raw == 0
return False
class LIS3DH:
CTRL_REG1 = const(0x20)
CTRL_REG4 = const(0x23)
OUT_X_L = const(0x28)
def __init__(self, i2c, found_devices, addr=LIS3DH_ADDR):
self.i2c = i2c
self.addr = addr
self.ready = addr in found_devices
self.last = (0.0, 0.0, 0.0)
if self.ready:
try:
# 50 Hz, all axes enabled.
self.i2c.writeto_mem(addr, self.CTRL_REG1, bytes([0x47]))
# +/-4g, high-resolution mode.
self.i2c.writeto_mem(addr, self.CTRL_REG4, bytes([0x18]))
except OSError as exc:
print("LIS3DH init failed:", exc)
self.ready = False
def read(self):
if not self.ready:
return (0.0, 0.0, 0.0, 0.0)
try:
data = self.i2c.readfrom_mem(self.addr, self.OUT_X_L | 0x80, 6)
except OSError as exc:
print("LIS3DH read failed:", exc)
self.ready = False
return (0.0, 0.0, 0.0, 0.0)
x = self._convert(data[1], data[0])
y = self._convert(data[3], data[2])
z = self._convert(data[5], data[4])
movement = abs(x - self.last[0]) + abs(y - self.last[1]) + abs(z - self.last[2])
self.last = (x, y, z)
return (x, y, z, movement)
@staticmethod
def _convert(msb, lsb):
raw = (msb << 8) | lsb
if raw & 0x8000:
raw -= 65536
return raw / 16384.0
def clamp(value, low, high):
return max(low, min(high, value))
def map_range(value, in_min, in_max, out_min, out_max):
value = clamp(value, in_min, in_max)
return int((value - in_min) * (out_max - out_min) / (in_max - in_min) + out_min)
def configure_adc(pin):
adc = ADC(Pin(pin))
try:
adc.atten(ADC.ATTN_11DB)
except AttributeError:
pass
return adc
def smooth_adc(adc):
total = 0
for _ in range(8):
total += adc.read()
sleep_ms(1)
return total // 8
def set_pwm_duty(pwm, duty_u16):
if hasattr(pwm, "duty_u16"):
pwm.duty_u16(duty_u16)
else:
pwm.duty(int(duty_u16 * 1023 / 65535))
def beep(beep_freq=1800, duration=40):
buzzer.freq(beep_freq)
set_pwm_duty(buzzer, 26000)
sleep_ms(duration)
set_pwm_duty(buzzer, 0)
def display_is_ready():
return display_ready[0] and oled[0] is not None
def safe_show():
if not display_is_ready():
return
try:
oled[0].show()
except OSError as exc:
display_ready[0] = False
print("OLED write failed:", exc)
def draw_text_center(text, y):
if not display_is_ready():
return
x = max(0, (OLED_WIDTH - len(text) * 8) // 2)
oled[0].text(text, x, y)
def draw_eye(cx, cy, offset, closed=False, is_sleepy=False):
if not display_is_ready():
return
if closed:
oled[0].fill_rect(cx - 20, cy, 40, 4, 1)
return
h = 18 if is_sleepy else 28
y = cy - h // 2
oled[0].rect(cx - 24, y, 48, h, 1)
oled[0].fill_rect(cx - 22, y + 1, 44, h - 2, 0)
pupil = 4 if is_sleepy else 6
oled[0].fill_rect(cx + offset - pupil // 2, cy - pupil // 2, pupil, pupil, 1)
def draw_eyes():
if not display_is_ready():
return
closed = ticks_diff(blink_until[0], ticks_ms()) > 0
oled[0].fill(0)
draw_eye(36, 28, eye_offset[0], closed, sleepy[0])
draw_eye(92, 28, eye_offset[0], closed, sleepy[0])
oled[0].text("L", 0, 56)
oled[0].text(str(map_range(ldr_left[0], 0, 4095, 0, 100)) + "%", 10, 56)
oled[0].text("R", 96, 56)
oled[0].text(str(map_range(ldr_right[0], 0, 4095, 0, 100)) + "%", 106, 56)
if sleepy[0]:
draw_text_center("sleepy", 46)
elif movement[0] > 0.55:
draw_text_center("motion", 46)
safe_show()
def draw_sensors():
if not display_is_ready():
return
oled[0].fill(0)
oled[0].text("Sensors", 0, 0)
oled[0].text("Light L: {}%".format(map_range(ldr_left[0], 0, 4095, 0, 100)), 0, 16)
oled[0].text("Light R: {}%".format(map_range(ldr_right[0], 0, 4095, 0, 100)), 0, 28)
oled[0].text("Motion: {:.2f}".format(movement[0]), 0, 40)
oled[0].text("LIS3DH {}".format("OK" if lis.ready else "missing"), 0, 52)
safe_show()
def draw_about():
if not display_is_ready():
return
oled[0].fill(0)
draw_text_center("BlinkBuddy", 8)
draw_text_center("LEFT OK RIGHT", 26)
draw_text_center("eyes follow", 42)
draw_text_center("USB-C flash", 54)
safe_show()
def draw_screen():
if screen[0] == SCREEN_EYES:
draw_eyes()
elif screen[0] == SCREEN_SENSORS:
draw_sensors()
else:
draw_about()
def next_screen():
screen[0] = (screen[0] + 1) % 3
beep(1600, 30)
def previous_screen():
screen[0] = (screen[0] - 1) % 3
beep(1300, 30)
def schedule_idle_blink(now):
variation = (ldr_left[0] + ldr_right[0] + (now // 37)) % IDLE_BLINK_SPREAD_MS
next_idle_blink[0] = now + IDLE_BLINK_BASE_MS + variation
def trigger_blink(now=None, duration=BLINK_DURATION_MS):
if now is None:
now = ticks_ms()
blink_until[0] = now + duration
schedule_idle_blink(now)
print("")
print("BlinkBuddy MicroPython firmware")
print("Pins: LDR L/R=GPIO0/GPIO1, LED=GPIO3, I2C SDA/SCL=GPIO4/GPIO5")
print("Pins: buttons L/OK/R=GPIO6/GPIO7/GPIO18, buzzer=GPIO10")
led = Pin(PIN_LED, Pin.OUT)
led.value(0)
buzzer = PWM(Pin(PIN_BUZZER))
buzzer.freq(1800)
set_pwm_duty(buzzer, 0)
i2c = I2C(0, scl=Pin(PIN_I2C_SCL), sda=Pin(PIN_I2C_SDA), freq=400_000)
i2c_devices = i2c.scan()
print("I2C scan:", [hex(addr) for addr in i2c_devices])
oled = [None]
display_ready = [False]
oled_addr = OLED_ADDR_PRIMARY
if OLED_ADDR_PRIMARY not in i2c_devices and OLED_ADDR_ALT in i2c_devices:
oled_addr = OLED_ADDR_ALT
try:
oled[0] = SSD1306_I2C(OLED_WIDTH, OLED_HEIGHT, i2c, addr=oled_addr)
display_ready[0] = True
oled[0].fill(0)
draw_text_center("BlinkBuddy", 20)
draw_text_center("starting...", 38)
safe_show()
print("OLED OK at", hex(oled_addr))
except OSError as exc:
print("OLED missing or not responding:", exc)
print("Firmware will continue with serial, LED, buzzer, buttons, and sensors.")
btn_left = Button(PIN_BUTTON_LEFT)
btn_ok = Button(PIN_BUTTON_OK)
btn_right = Button(PIN_BUTTON_RIGHT)
adc_left = configure_adc(PIN_LDR_LEFT)
adc_right = configure_adc(PIN_LDR_RIGHT)
lis = LIS3DH(i2c, i2c_devices)
print("LIS3DH:", "ok" if lis.ready else "missing")
screen = [SCREEN_EYES]
ldr_left = [0]
ldr_right = [0]
sleepy = [False]
movement = [0.0]
target_eye_offset = [0]
eye_offset = [0]
blink_until = [0]
next_idle_blink = [0]
last_sensor = ticks_ms()
last_ui = ticks_ms()
last_debug = ticks_ms()
schedule_idle_blink(last_sensor)
beep(1800, 70)
led.value(1)
sleep_ms(160)
led.value(0)
print("BlinkBuddy app running")
while True:
now = ticks_ms()
if btn_left.pressed():
previous_screen()
print("button=LEFT screen={}".format(screen[0]))
if btn_ok.pressed():
trigger_blink(now)
beep(2200, 35)
print("button=OK")
if btn_right.pressed():
next_screen()
print("button=RIGHT screen={}".format(screen[0]))
if ticks_diff(now, next_idle_blink[0]) >= 0:
trigger_blink(now)
if ticks_diff(now, last_sensor) >= 120:
last_sensor = now
ldr_left[0] = smooth_adc(adc_left)
ldr_right[0] = smooth_adc(adc_right)
diff = ldr_right[0] - ldr_left[0]
target_eye_offset[0] = map_range(diff, -1800, 1800, -12, 12)
eye_offset[0] += int((target_eye_offset[0] - eye_offset[0]) / 3)
sleepy[0] = (ldr_left[0] + ldr_right[0]) // 2 < 650
_, _, _, movement[0] = lis.read()
if movement[0] > 0.9:
trigger_blink(now)
led.value((now // 500) % 2)
if ticks_diff(now, last_ui) >= 80:
last_ui = now
draw_screen()
if ticks_diff(now, last_debug) >= 1000:
last_debug = now
print(
"ldr_left={} ldr_right={} eye={} movement={:.2f} lis={} oled={}".format(
ldr_left[0],
ldr_right[0],
target_eye_offset[0],
movement[0],
"ok" if lis.ready else "missing",
"ok" if display_ready[0] else "missing",
)
)