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/src/main.cpp

#include <Arduino.h>
#include <U8g2lib.h>
#include <Wire.h>
#include <Adafruit_LIS3DH.h>
#include <Adafruit_Sensor.h>
#include <cmath>

namespace Pins {
constexpr uint8_t LdrLeft = 0;
constexpr uint8_t LdrRight = 1;
constexpr uint8_t Led = 3;
constexpr uint8_t I2cSda = 4;
constexpr uint8_t I2cScl = 5;
constexpr uint8_t ButtonLeft = 6;
constexpr uint8_t ButtonOk = 7;
constexpr uint8_t ButtonRight = 18;
constexpr uint8_t Buzzer = 10;
} // namespace Pins

constexpr uint8_t Lis3dhAddress = 0x19;
constexpr uint32_t SerialBaud = 115200;
constexpr uint16_t DebounceMs = 35;
constexpr uint16_t UiFrameMs = 80;
constexpr uint16_t SensorFrameMs = 120;
constexpr uint16_t BlinkDurationMs = 170;
constexpr uint16_t IdleBlinkBaseMs = 3000;
constexpr uint16_t IdleBlinkSpreadMs = 3000;

// Large 4-pin I2C OLED. If the screen is blank, try the SSD1306 constructor below.
U8G2_SSD1309_128X64_NONAME0_F_HW_I2C display(U8G2_R0, U8X8_PIN_NONE);
// U8G2_SSD1306_128X64_NONAME_F_HW_I2C display(U8G2_R0, U8X8_PIN_NONE);

Adafruit_LIS3DH lis = Adafruit_LIS3DH();

enum class Screen : uint8_t {
  Eyes = 0,
  Sensors = 1,
  About = 2,
};

struct Button {
  uint8_t pin;
  bool stablePressed = false;
  bool lastRawPressed = false;
  uint32_t lastChangeMs = 0;

  bool update() {
    const bool rawPressed = digitalRead(pin) == LOW;
    const uint32_t now = millis();

    if (rawPressed != lastRawPressed) {
      lastRawPressed = rawPressed;
      lastChangeMs = now;
    }

    if ((now - lastChangeMs) > DebounceMs && rawPressed != stablePressed) {
      stablePressed = rawPressed;
      return stablePressed;
    }

    return false;
  }
};

Button btnLeft{Pins::ButtonLeft};
Button btnOk{Pins::ButtonOk};
Button btnRight{Pins::ButtonRight};

Screen screen = Screen::Eyes;
bool lisReady = false;
int ldrLeftRaw = 0;
int ldrRightRaw = 0;
float accelX = 0;
float accelY = 0;
float accelZ = 0;
float movement = 0;
uint32_t lastUiMs = 0;
uint32_t lastSensorMs = 0;
uint32_t blinkUntilMs = 0;
uint32_t nextIdleBlinkMs = 0;
uint32_t lastSerialMs = 0;

int eyeOffset = 0;
int targetEyeOffset = 0;
bool sleepy = false;

void beep(uint16_t frequency = 1800, uint16_t durationMs = 35) {
  tone(Pins::Buzzer, frequency, durationMs);
}

void nextScreen() {
  screen = static_cast<Screen>((static_cast<uint8_t>(screen) + 1) % 3);
  beep(1600, 30);
}

void previousScreen() {
  const uint8_t current = static_cast<uint8_t>(screen);
  screen = static_cast<Screen>(current == 0 ? 2 : current - 1);
  beep(1300, 30);
}

void scheduleIdleBlink(uint32_t now) {
  const uint32_t variation =
      (static_cast<uint32_t>(ldrLeftRaw + ldrRightRaw) + (now / 37)) %
      IdleBlinkSpreadMs;
  nextIdleBlinkMs = now + IdleBlinkBaseMs + variation;
}

void triggerBlink(uint32_t now, uint16_t durationMs = BlinkDurationMs) {
  blinkUntilMs = now + durationMs;
  scheduleIdleBlink(now);
}

int smoothAnalogRead(uint8_t pin) {
  uint32_t total = 0;
  for (int i = 0; i < 8; i++) {
    total += analogRead(pin);
    delayMicroseconds(250);
  }
  return static_cast<int>(total / 8);
}

void readSensors() {
  ldrLeftRaw = smoothAnalogRead(Pins::LdrLeft);
  ldrRightRaw = smoothAnalogRead(Pins::LdrRight);

  const int diff = ldrRightRaw - ldrLeftRaw;
  targetEyeOffset = constrain(map(diff, -1800, 1800, -12, 12), -12, 12);

  const int averageLight = (ldrLeftRaw + ldrRightRaw) / 2;
  sleepy = averageLight < 650;

  if (lisReady) {
    sensors_event_t event;
    lis.getEvent(&event);
    const float dx = event.acceleration.x - accelX;
    const float dy = event.acceleration.y - accelY;
    const float dz = event.acceleration.z - accelZ;
    movement = fabsf(dx) + fabsf(dy) + fabsf(dz);
    accelX = event.acceleration.x;
    accelY = event.acceleration.y;
    accelZ = event.acceleration.z;

    if (movement > 8.0f) {
      triggerBlink(millis());
    }
  }
}

void drawCenteredText(const char *text, int y, const uint8_t *font) {
  display.setFont(font);
  const int width = display.getStrWidth(text);
  display.drawStr((128 - width) / 2, y, text);
}

void drawEye(int cx, int cy, int pupilOffset, bool closed) {
  if (closed) {
    display.drawRBox(cx - 22, cy - 3, 44, 6, 3);
    return;
  }

  display.drawRFrame(cx - 24, cy - 16, 48, sleepy ? 20 : 32, 10);
  display.drawDisc(cx + pupilOffset, cy, sleepy ? 4 : 7);
}

void drawEyesScreen() {
  const bool blink = millis() < blinkUntilMs;
  eyeOffset += (targetEyeOffset - eyeOffset) / 3;

  display.clearBuffer();
  drawEye(36, 30, eyeOffset, blink);
  drawEye(92, 30, eyeOffset, blink);

  display.setFont(u8g2_font_5x8_tf);
  display.drawStr(2, 62, "L");
  display.setCursor(10, 62);
  display.print(map(ldrLeftRaw, 0, 4095, 0, 100));
  display.print("%");

  display.drawStr(96, 62, "R");
  display.setCursor(104, 62);
  display.print(map(ldrRightRaw, 0, 4095, 0, 100));
  display.print("%");

  if (sleepy) {
    drawCenteredText("sleepy", 58, u8g2_font_5x8_tf);
  } else if (movement > 4.0f) {
    drawCenteredText("motion", 58, u8g2_font_5x8_tf);
  }

  display.sendBuffer();
}

void drawSensorsScreen() {
  display.clearBuffer();
  display.setFont(u8g2_font_6x10_tf);
  display.drawStr(0, 10, "Sensors");

  display.setCursor(0, 24);
  display.print("Light L: ");
  display.print(map(ldrLeftRaw, 0, 4095, 0, 100));
  display.print("%");

  display.setCursor(0, 36);
  display.print("Light R: ");
  display.print(map(ldrRightRaw, 0, 4095, 0, 100));
  display.print("%");

  display.setCursor(0, 48);
  display.print("Motion: ");
  display.print(lisReady ? movement : -1, 1);

  display.setCursor(0, 60);
  display.print(lisReady ? "LIS3DH OK" : "LIS3DH missing");

  display.sendBuffer();
}

void drawAboutScreen() {
  display.clearBuffer();
  drawCenteredText("BlinkBuddy", 14, u8g2_font_7x14B_tf);
  drawCenteredText("LEFT  OK  RIGHT", 32, u8g2_font_6x10_tf);
  drawCenteredText("eyes follow light", 46, u8g2_font_6x10_tf);
  drawCenteredText("USB-C programming", 60, u8g2_font_5x8_tf);
  display.sendBuffer();
}

void drawCurrentScreen() {
  switch (screen) {
  case Screen::Eyes:
    drawEyesScreen();
    break;
  case Screen::Sensors:
    drawSensorsScreen();
    break;
  case Screen::About:
    drawAboutScreen();
    break;
  }
}

void printDebug() {
  Serial.print("ldr_left=");
  Serial.print(ldrLeftRaw);
  Serial.print(" ldr_right=");
  Serial.print(ldrRightRaw);
  Serial.print(" eye=");
  Serial.print(targetEyeOffset);
  Serial.print(" movement=");
  Serial.print(movement, 2);
  Serial.print(" lis=");
  Serial.println(lisReady ? "ok" : "missing");
}

void setup() {
  pinMode(Pins::Led, OUTPUT);
  pinMode(Pins::Buzzer, OUTPUT);
  pinMode(Pins::ButtonLeft, INPUT_PULLUP);
  pinMode(Pins::ButtonOk, INPUT_PULLUP);
  pinMode(Pins::ButtonRight, INPUT_PULLUP);

  analogReadResolution(12);
  analogSetPinAttenuation(Pins::LdrLeft, ADC_11db);
  analogSetPinAttenuation(Pins::LdrRight, ADC_11db);

  Serial.begin(SerialBaud);
  delay(150);

  Wire.begin(Pins::I2cSda, Pins::I2cScl);
  Wire.setClock(400000);

  display.setI2CAddress(0x3C << 1);
  display.begin();
  display.clearBuffer();
  drawCenteredText("BlinkBuddy", 26, u8g2_font_7x14B_tf);
  drawCenteredText("starting...", 44, u8g2_font_6x10_tf);
  display.sendBuffer();

  lisReady = lis.begin(Lis3dhAddress);
  if (lisReady) {
    lis.setRange(LIS3DH_RANGE_4_G);
    lis.setDataRate(LIS3DH_DATARATE_50_HZ);
  }

  beep(1800, 60);
  digitalWrite(Pins::Led, HIGH);
  delay(250);
  digitalWrite(Pins::Led, LOW);

  Serial.println("BlinkBuddy firmware started");
  Serial.println(lisReady ? "LIS3DH detected at 0x19" : "LIS3DH not detected");
  scheduleIdleBlink(millis());
}

void loop() {
  const uint32_t now = millis();

  if (btnLeft.update()) {
    previousScreen();
  }
  if (btnOk.update()) {
    triggerBlink(now);
    beep(2200, 35);
  }
  if (btnRight.update()) {
    nextScreen();
  }

  if (static_cast<int32_t>(now - nextIdleBlinkMs) >= 0) {
    triggerBlink(now);
  }

  if (now - lastSensorMs >= SensorFrameMs) {
    lastSensorMs = now;
    readSensors();
    digitalWrite(Pins::Led, (now / 500) % 2);
  }

  if (now - lastUiMs >= UiFrameMs) {
    lastUiMs = now;
    drawCurrentScreen();
  }

  if (now - lastSerialMs >= 1000) {
    lastSerialMs = now;
    printDebug();
  }
}