Yugi Tech Lab
Published

Breadboard Monitoring System

This project monitors every breadboard hole without probing using the M5Stack, supporting circuit verification and education.

AdvancedWork in progress20 hours379
Breadboard Monitoring System

Things used in this project

Hardware components

ESP32 Basic Core loT Development Kit V2.7
M5Stack ESP32 Basic Core loT Development Kit V2.7
×1
M5Stack Scroll Unit with Hollow Shaft Encoder
×1
M5Stack AtomS3R-CAM
×1
Freenove ESP32-S3-WROOM Board Lite
×1
New Breadboard (SAD-101)
×1
SparkFun Analog/Digital MUX Breakout - CD74HC4067
SparkFun Analog/Digital MUX Breakout - CD74HC4067
×4
PCF8575
×1
Adafruit Rugged Metal Pushbutton with Red LED Ring - 16mm Red Momentary
×1
SparkFun Qwiic Single Relay
SparkFun Qwiic Single Relay
×1
Level Shifter Board
SparkFun Level Shifter Board
×1

Software apps and online services

Arduino IDE
Arduino IDE

Hand tools and fabrication machines

3D Printer (generic)
3D Printer (generic)
Soldering iron (generic)
Soldering iron (generic)

Story

Read more

Code

M5Stack

Arduino
#include <M5Unified.h>
#include "Arduino.h"
#include "M5UnitScroll.h"
#include <Wire.h>

#define BAUD_RATE       460800
#define BTN_A_PIN       39
#define BTN_B_PIN       38
#define HEADER_SIZE     10
#define VOLTAGE_SIZE_DC 66
#define VOLTAGE_SIZE_OS 2000

// ===== JPEG MCU height =====
static constexpr int MCU_H = 8;

String headerData[HEADER_SIZE];
float voltageData[VOLTAGE_SIZE_OS];    // Shared for DC/OS
float tranData[VOLTAGE_SIZE_OS / 4];   // Shared for DC/OS

M5UnitScroll scroll;

static constexpr size_t IMAGE_BUFFER_SIZE = 1024 * 50;
uint8_t imageBuffer[IMAGE_BUFFER_SIZE];
size_t bufferIndex = 0;

// ===== Tokens changed from String to char array =====
#define MAX_TOKENS (HEADER_SIZE + VOLTAGE_SIZE_OS + 10)
#define TOKEN_LEN  16
char tokens[MAX_TOKENS][TOKEN_LEN];
int tokenCount = 0;
bool btn_scroll_before = 0;

const char* START_MARKER = "START_IMAGE";
String esp_mode = "DC";

enum ImageReceiveState {
  WAITING_FOR_START,
  RECEIVING_IMAGE,
  DISPLAYED_IMAGE
};
ImageReceiveState currentState = WAITING_FOR_START;

unsigned long lastUpdateTime   = 0;
const unsigned long UPDATE_INTERVAL = 100;  // Update interval (ms)
uint32_t frameCount            = 0;

bool prevState_A   = HIGH;
unsigned long lastChange_A = 0;
bool prevState_B   = HIGH;
unsigned long lastChange_B = 0;
const unsigned long debounceMs = 30;

// ===== Display parameters =====
int circleRadius   = 5;
int barHeight      = 8;
int circleYTop     = 17;
int circleYBottom  = 180;
int barTop         = 3;
int barBottom      = 165;
uint8_t col_index  = 1;

// ===== Screen margin and division settings =====
static constexpr int screenW     = 320;
static constexpr int marginLeft  = 10;
static constexpr int marginRight = 10;
static constexpr int numCols     = 30;
static constexpr int contentW    = screenW - marginLeft - marginRight;

int prevBlock = INT32_MIN;

lgfx::LGFX_Sprite sprite(&M5.Display);

// ---- HSV  RGB565 ----
uint16_t HSVtoRGB565(float h, float s, float v) {
  if (s <= 0.0f) {
    uint8_t g = (uint8_t)(v * 255.0f + 0.5f);
    return ((g & 0xF8) << 8) | ((g & 0xFC) << 3) | (g >> 3);
  }

  h = fmodf(h, 360.0f);
  if (h < 0) h += 360.0f;

  float c  = v * s;
  float xh = c * (1.0f - fabsf(fmodf(h / 60.0f, 2.0f) - 1.0f));
  float m  = v - c;
  float rf = 0, gf = 0, bf = 0;

  if (h < 60) {
    rf = c;  gf = xh; bf = 0;
  } else if (h < 120) {
    rf = xh; gf = c;  bf = 0;
  } else if (h < 180) {
    rf = 0;  gf = c;  bf = xh;
  } else if (h < 240) {
    rf = 0;  gf = xh; bf = c;
  } else if (h < 300) {
    rf = xh; gf = 0;  bf = c;
  } else {
    rf = c;  gf = 0;  bf = xh;
  }

  uint8_t R = (uint8_t)((rf + m) * 255.0f + 0.5f);
  uint8_t G = (uint8_t)((gf + m) * 255.0f + 0.5f);
  uint8_t B = (uint8_t)((bf + m) * 255.0f + 0.5f);

  return ((R & 0xF8) << 8) | ((G & 0xFC) << 3) | (B >> 3);
}

// ---- Grid helper functions ----
inline int colStart(int i)   { return marginLeft + (i * contentW) / numCols; }
inline int colEnd(int i)     { return marginLeft + ((i + 1) * contentW) / numCols; }
inline int colWidth(int i)   { return colEnd(i) - colStart(i); }
inline int colCenter(int i)  { return (colStart(i) + colEnd(i) - 1) / 2; }
inline int floorDiv30(int a) { return (a >= 0) ? (a / 30) : -((-a + 29) / 30); }

// ---- JPEG tile rendering ----
inline void drawJpgTileMCUAligned(int srcOffY, int tile_h, int sw) {
  const int offY_aligned = (srcOffY / MCU_H) * MCU_H;
  const int delta        = srcOffY - offY_aligned;
  const int dstY         = -delta;
  const int reqH         = tile_h + delta;

  sprite.drawJpg(imageBuffer, bufferIndex,
                 0, dstY, sw, reqH,
                 0, offY_aligned);
}

// ---- DC mode overlay rendering ----
void drawOverlayAndPush(int16_t encoder_value, String esp_mode) {
  const int screenH = 240;
  const int sw = sprite.width();   // 320
  const int sh = sprite.height();  // 120

  if (esp_mode == "DC") {
    int col = encoder_value % 30;
    if (col < 0) col += 30;

    int block = floorDiv30(encoder_value);
    static int circleY = circleYTop;
    static int circle_row = 0;

    if (prevBlock == INT32_MIN) {
      prevBlock = block;
    } else {
      int wraps = block - prevBlock;
      if (wraps & 1) {
        circleY = (circleY == circleYTop) ? circleYBottom : circleYTop;
        circle_row = (circleY == circleYTop) ? 0 : 1;
      }
      prevBlock = block;
    }

    const int xCenter = colCenter(col);

    for (int y0 = 0; y0 < screenH; y0 += sh) {
      const int tile_h = (y0 + sh <= screenH) ? sh : (screenH - y0);
      sprite.setClipRect(0, 0, sw, tile_h);

      drawJpgTileMCUAligned(y0, tile_h, sw);

      if (circleY >= y0 && circleY < y0 + tile_h) {
        const int dy = circleY - y0;
        sprite.fillCircle(xCenter, dy, circleRadius, HSVtoRGB565(350, 1.0f, 1.0f));
      }

      if (y0 == 0) {
        for (int i = 0; i < numCols; ++i) {
          int sx = colStart(i);
          int w = colWidth(i);
          float c1 = 240 - voltageData[i] * 55;
          uint16_t c1_ = c1 >= 0 ? uint16_t(c1) : uint16_t(360 + c1);
          if (voltageData[i] >= 0.2) {
            sprite.fillRect(sx, barTop, w, barHeight, HSVtoRGB565(c1_, 1.0f, 1.0f));
          }
        }
      }

      if (barBottom >= y0 && barBottom < y0 + tile_h) {
        const int dy = barBottom - y0;
        for (int i = 0; i < numCols; ++i) {
          int sx = colStart(i);
          int w = colWidth(i);
          float c2 = 240 - voltageData[i + 30] * 55;
          uint16_t c2_ = c2 >= 0 ? uint16_t(c2) : uint16_t(360 + c2);
          if (voltageData[i + 30] >= 0.2) {
            sprite.fillRect(sx, dy, w, barHeight, HSVtoRGB565(c2_, 1.0f, 1.0f));
          }
        }
      }

      col_index = col + circle_row * 30;

      if (screenH - 22 >= y0 && screenH - 1 < y0 + tile_h) {
        const int dy = (screenH - 22) - y0;
        sprite.setTextColor(TFT_WHITE, TFT_BLACK);
        sprite.fillRect(0, dy, sw, 22, TFT_BLACK);
        sprite.setCursor(10, dy + 2);
        sprite.printf("index:%d/60", col_index);
        sprite.setCursor(100, dy + 2);
        sprite.printf("vol:%.2f", voltageData[col_index]);
      }

      sprite.clearClipRect();
      sprite.pushSprite(0, y0);
    }
  }
}

// ---- Oscilloscope rendering ----
void drawOscilloscopeFromData() {
  sprite.fillScreen(BLACK);

  int w = sprite.width();
  int h = sprite.height();
  int yMargin     = 25;   // Left margin (for labels)
  int topMargin   = 5;    // Top margin
  int bottomMargin= 15;   // Bottom margin
  int plotHeight  = h - topMargin - bottomMargin;
  int tranLen     = 500;  // Fixed length

  // ==== Y-axis grid & labels ====
  for (int v = 0; v <= 5; v++) {
    int y = topMargin + map(v * 1000, 0, 5000, plotHeight - 1, 0);
    sprite.drawLine(yMargin, y, w - 3, y, TFT_DARKGREY);
    sprite.setTextColor(WHITE, BLACK);
    sprite.setCursor(2, y - sprite.fontHeight() / 2);
    sprite.printf("%dV", v);
  }

  // ==== X-axis grid ====
  int stepX = (w - yMargin) / 10;
  for (int gx = yMargin; gx < w; gx += stepX) {
    sprite.drawLine(gx, topMargin, gx, topMargin + plotHeight, TFT_DARKGREY);
  }

  // ==== Waveform ====
  for (int i = 0; i < tranLen - 1; i++) {
    int x1 = map(i, 0, tranLen - 1, yMargin, w - 1);
    int y1 = topMargin + map(tranData[i] * 1000, 0, 5000, plotHeight - 1, 0);
    int x2 = map(i + 1, 0, tranLen - 1, yMargin, w - 1);
    int y2 = topMargin + map(tranData[i + 1] * 1000, 0, 5000, plotHeight - 1, 0);
    sprite.drawLine(x1, y1, x2, y2, GREEN);
  }

  // ==== "25 ms/div" label outside frame ====
  sprite.setTextColor(WHITE, BLACK);
  sprite.setTextFont(2);
  int textW = sprite.textWidth("25 ms/div");
  int textX = w - textW - 2;
  int textY = topMargin + plotHeight + 2;
  sprite.setCursor(textX, textY);
  sprite.print("25 ms/div");

  sprite.pushSprite(0, 60);
}

// ---- Setup ----
void setup() {
  M5.begin();
  Serial.begin(BAUD_RATE);
  pinMode(BTN_A_PIN, INPUT_PULLUP);
  pinMode(BTN_B_PIN, INPUT_PULLUP);

  scroll.begin(&Wire, SCROLL_ADDR, 21, 22, 400000U);

  M5.Display.setRotation(1);
  M5.Display.fillScreen(BLACK);

  sprite.setColorDepth(16);
  sprite.createSprite(320, 120);

  sprite.fillScreen(TFT_BLACK);
  sprite.setTextFont(2);
  sprite.setTextColor(TFT_WHITE, TFT_BLACK);
  sprite.setCursor(10, 10);
  sprite.print("Waiting for image data...");
  sprite.pushSprite(0, 0);

  randomSeed(micros());
}

// ---- Loop ----
void loop() {
  int16_t encoder_value = scroll.getEncoderValue();
  bool btn_scroll = scroll.getButtonStatus();

  bool currState_A = digitalRead(BTN_A_PIN);
  bool currState_B = digitalRead(BTN_B_PIN);
  bool mode_change = 0;
  unsigned long now = millis();

  // ---- Button A ----
  if (currState_A != prevState_A && (now - lastChange_A) > debounceMs) {
    lastChange_A = now;
    prevState_A = currState_A;
    if (currState_A == LOW) Serial.println("BTNA");
  }


  if (btn_scroll & (btn_scroll != btn_scroll_before)) {
    if (esp_mode == "DC") {
      // === Send to PC when BTNB pressed ===
      Serial.print("OS,");
      Serial.println(col_index);
    } else if (esp_mode == "OS3") {
      Serial.println("DC");
    }
  }
  btn_scroll_before = btn_scroll;

  if (Serial.available()) {
    if (currentState == WAITING_FOR_START) {
      String line = Serial.readStringUntil('\n');
      if (line.indexOf(START_MARKER) != -1) {
        bufferIndex = 0;
        currentState = RECEIVING_IMAGE;
        Serial.println("ACK: Start receiving");
      }
    } else if (currentState == RECEIVING_IMAGE) {
      while (Serial.available() && bufferIndex < IMAGE_BUFFER_SIZE) {
        imageBuffer[bufferIndex++] = Serial.read();
      }
      if (bufferIndex >= 2 && imageBuffer[bufferIndex - 2] == 0xFF && imageBuffer[bufferIndex - 1] == 0xD9) {
        drawOverlayAndPush(encoder_value, esp_mode);
        lastUpdateTime = millis();
        currentState = DISPLAYED_IMAGE;
        Serial.println("ACK: Image displayed");
      }
      if (bufferIndex >= IMAGE_BUFFER_SIZE) {
        sprite.fillScreen(TFT_RED);
        sprite.setCursor(10, 10);
        sprite.print("Buffer overflow!");
        sprite.pushSprite(0, 0);
        bufferIndex = 0;
        currentState = WAITING_FOR_START;
        Serial.println("ERR: Buffer overflow");
      }
    } else if (currentState == DISPLAYED_IMAGE) {
      String line = Serial.readStringUntil('\n');
      line.trim();

      tokenCount = 0;
      int start = 0;
      int commaPos;
      while ((commaPos = line.indexOf(',', start)) != -1 && tokenCount < MAX_TOKENS) {
        String t = line.substring(start, commaPos);
        t.toCharArray(tokens[tokenCount], TOKEN_LEN);
        tokenCount++;
        start = commaPos + 1;
      }
      if (start < line.length() && tokenCount < MAX_TOKENS) {
        String t = line.substring(start);
        t.toCharArray(tokens[tokenCount], TOKEN_LEN);
        tokenCount++;
      }

      esp_mode = String(tokens[0]);

      if (esp_mode == "DC") {
        if (tokenCount >= 66) {
          for (int i = 0; i < HEADER_SIZE; i++) headerData[i] = tokens[i + 1];
          for (int i = 0; i < VOLTAGE_SIZE_DC; i++) {
            voltageData[i] = atof(tokens[10 + i + 1]) / 4095.0f * 5.5f;
          }
        }
      } else if (esp_mode == "OS3") {
        if (tokenCount >= VOLTAGE_SIZE_OS) {
          int j = 0;
          uint8_t phase = (col_index / 15);
          for (int i = 0; i < VOLTAGE_SIZE_OS; i++) {
            voltageData[i] = atof(tokens[10 + i + 1]) / 4095.0f * 5.5f;
            if (i % 4 == phase) {
              tranData[j] = voltageData[i];
              j++;
            }
          }
        }
      }
    }
  }

  if (currentState == DISPLAYED_IMAGE && millis() - lastUpdateTime >= UPDATE_INTERVAL) {
    lastUpdateTime = millis();
    frameCount++;

    if (esp_mode == "DC") {
      drawOverlayAndPush(encoder_value, esp_mode);
    } else if (esp_mode == "OS3") {
      drawOscilloscopeFromData();
    }
  }

  M5.update();
}

Credits

Yugi Tech Lab
1 project • 4 followers

Comments