



안녕하세요, EyeD 배지를 만나보세요
사람들이 실제로 주목할 수 있는 컨퍼런스 배지를 만들고 싶어서, ESP32-C6로 구동되는 착용 가능한 8×16 RGB LED 스마트 배지인 EyeD 배지를 만들었습니다.
지루한 라미네이트 ID 카드 대신, 128픽셀 RGB LED 매트릭스로 교체했습니다. 전체 배지는 작은 리튬 폴리머 배터리로 구동되어, 컨퍼런스, 해커톤 또는 메이커 이벤트에서 하루 종일 착용하기에 편안합니다.
제가 가장 좋아하는 기능 중 하나는 시작 애니메이션입니다. 배지가 켜지자마자, 거대한 애니메이션 눈이 나타나 방을 둘러보고, 가끔 사람들에게 깜빡입니다. EyeD라는 이름은 여기서 유래되었으며, 다시 바라보는 눈이 있는 ID 배지입니다.
물론, 그 눈은 단지 대기 애니메이션일 뿐입니다.
또한 Bluetooth 저전력 지원을 추가하여, 배지를 어떤 전화, 태블릿 또는 노트북에서든 제어할 수 있게 했습니다. 설치할 앱이 필요 없습니다—브라우저에서 웹 페이지를 열고 연결하기만 하면 됩니다.
웹 인터페이스에서 이름, 소셜 핸들 또는 원하는 사용자 정의 메시지를 디스플레이에 스크롤할 수 있습니다. 매트릭스 비, 불, 플라즈마 효과 등 15개 이상의 내장 애니메이션도 추가했으며, 무지개 꼬리를 가진 완전한 냐옹 고양이 애니메이션도 포함되어 있습니다.
더 창의적인 것을 위해, 배지에서 직접 픽셀 아트를 생성할 수 있는 드로우 모드를 추가했습니다. 한 번에 하나의 픽셀씩, 원하는 색상으로 만들 수 있습니다. 웹 인터페이스에는 LED 매트릭스의 실시간 미리보기도 포함되어 있어, 아래를 보지 않고도 배지가 표시하는 내용을 항상 확인할 수 있습니다.
EyeD 배지의 목표는 단순한 컨퍼런스 ID를 상호작용적이고 재미있는 것으로 바꾸는 것이었습니다. 단순히 이름을 보여주는 대신, 주목을 끌고 대화를 시작하며 원하는 대로 사용자 정의할 수 있는 착용 가능한 디스플레이가 됩니다.
STEP 1. 준비물
- ESP32-C6 개발 보드
- 8×16 WS2812B RGB LED 매트릭스 패널
- 리튬 폴리머 배터리
- 3D 프린트 케이스
- 슬라이드 스위치
STEP 2. 하드웨어 - 맞춤형 RGB 매트릭스



LED 매트릭스에 대해, 저는 조금 특별한 것을 원했습니다.
다행히도, 제가 찾고 있던 디자인과 정확히 일치하는 Arnov Sharma의 프로젝트를 발견했습니다. 그에게 완전 조립된 샘플을 제공할 수 있는지 문의했고, 이 빌드를 위해 하나를 받았습니다.
이 매트릭스는 16x8 구성으로 되어 있으며 OXPLOW 레이아웃으로 배치되어 있습니다. OXPLOW는 한 행에서 LEDs가 한 방향으로 가고, 다음 행에서는 반대 방향으로 가는 매트릭스 유형으로, 이 레이아웃은 부스트로페돈(boustrophedon)이라고도 불립니다.
또 다른 레이아웃은 세르펜타인 레이아웃으로, 이 레이아웃의 LEDs는 뱀처럼 연속적인 체인으로 배치되어 있어 세르펜타인이라는 이름이 붙었습니다.
이 매트릭스는 FAST LED 라이브러리를 통해 제어되지만, ADAFRUIT의 Neopixel 라이브러리나 Smart Matrix 라이브러리 등 기존의 여러 라이브러리로도 작동할 수 있습니다.
STEP 3. 하드웨어 - XIAO ESP32 C6

EyeD 배지를 구동하는 것은 Seeed Studio XIAO ESP32-C6로, 크기가 단 21 × 17.8 mm로 엄청나게 컴팩트한 개발 보드입니다. 엄청나게 작은 크기에도 불구하고, 연결된 웨어러블에 필요한 모든 것을 갖추고 있습니다. 이 보드의 핵심은 Espressif의 ESP32-C6로, 최대 160 MHz에서 작동하는 32비트 RISC-V 프로세서를 특징으로 하며, Wi-Fi 6, Bluetooth 5 (LE), Zigbee 및 Thread와 같은 현대적인 무선 연결 기능을 갖추고 있어 향후 IoT 애플리케이션을 위한 Matter 준비가 되어 있습니다.
이 프로젝트의 하드웨어 설정은 매우 간단합니다. 전체 8×16 RGB LED 매트릭스는 데이터 신호를 위한 단일 GPIO 핀(D0)으로 구동되며, 전원과 접지와 함께 사용됩니다. 보드의 통합 LiPo 배터리 충전 회로와 우수한 저전력 성능 덕분에, 배지는 추가 전원 관리 하드웨어 없이도 작은 재충전 가능한 배터리로 하루 종일 작동할 수 있습니다.
주요 사양
- 마이크로컨트롤러: Espressif ESP32-C6
- CPU: 32비트 RISC-V 프로세서, 최대 160 MHz
- 보드 크기: 21 × 17.8 mm
- 무선 연결:
- Wi-Fi 6 (2.4 GHz, IEEE 802.11 b/g/n/ax)
- Bluetooth 5 (저전력)
- Zigbee
- Thread
- Matter 호환
- SRAM: 512 KB
- 플래시 메모리: 4 MB
- USB: 프로그래밍 및 전원을 위한 USB Type-C
- GPIO: 11개의 다기능 GPIO 핀
- 안테나: 외부 안테나를 위한 U.FL 커넥터가 있는 온보드 PCB 안테나
- 배터리 지원: 내장 LiPo 배터리 충전 회로
- 저전력: 마이크로암 범위의 깊은 수면 전류
- LED 매트릭스 인터페이스: 단일 데이터 핀(D0)이 모든 128 RGB LED를 제어
STEP 4. 전원 공급 장치

EyeD 배지를 전원 공급하기 위해 3.7 V 650 mAh LiPo 배터리를 선택했습니다. Seeed Studio XIAO ESP32-C6가 단일 셀 LiPo 배터리에 대한 내장 지원과 온보드 충전 회로를 제공하기 때문에 쉬운 선택이었습니다.
XIAO는 보드 뒷면에 전용 배터리 패드를 제공하여 LiPo 셀을 외부 충전 모듈이나 전원 관리 보드 없이 직접 납땜할 수 있게 합니다.
STEP 5. 배지 디자인







처음부터 제 목표는 EyeD 배지가 단순한 전자 프로젝트가 아닌 실제 컨퍼런스 ID 배지처럼 느껴지도록 만드는 것이었습니다. 이는 일반 ID 카드와 동일한 전체 형태와 비율을 가진 인클로저를 설계하는 것을 의미하며, 표준 랜야드나 배지 스트랩을 부착할 수 있도록 상단에 장착 루프가 포함되어야 했습니다.
저는 8×16 RGB LED 매트릭스, 650 mAh LiPo 셀, Seeed Studio XIAO ESP32-C6, 슬라이드 전원 스위치를 포함한 모든 주요 구성 요소를 Fusion 360에서 모델링하는 것부터 시작했습니다. 정확한 모델이 있으면 모든 것을 깔끔하게 맞출 수 있는 컴팩트한 인클로저를 설계하는 것이 훨씬 쉬워졌습니다.
인클로저의 앞면은 LED 매트릭스를 안전하게 고정하는 프레임입니다. 매트릭스는 네 개의 M2 나사를 사용하여 앞에서 장착되어 깔끔한 외관을 유지하면서도 단단히 부착됩니다.
뒷면에는 각 구성 요소를 위한 전용 장착 위치를 설계했습니다. LiPo 배터리는 LED 매트릭스 뒤에 위치하고, XIAO ESP32-C6는 인클로저의 하단 근처에 장착됩니다. 그 옆에는 슬라이드 스위치를 위한 슬롯이 있어 인클로저를 열지 않고도 하단 가장자리에서 배지를 켜거나 끌 수 있습니다.
조립을 마무리하기 위해 모든 전자 장치를 감싸고 일상적인 사용 중에 보호하는 분리 가능한 후면 커버를 설계했습니다. 마지막으로, 인클로저 상단에 강화된 루프가 있어 표준 ID 카드 스트랩이나 랜야드를 부착할 수 있어 착용 가능한 컨퍼런스 배지의 외관과 기능을 완성합니다.
STEP 6. 3D 프린트 부품

모델을 최종 확정한 후, 두 부품은 메쉬 파일로 내보내졌고, Sage Green PLA를 사용하여 3D 프린팅되었습니다.
두 부품은 0.4 mm 노즐, 0.2 mm 층 높이, 25% 인필로 인쇄되었으며, 지지가 필요한 부품에는 트리 서포트를 사용했습니다.
STEP 7. 매트릭스 & 샤오 조립

XIAO MCU와 LED 매트릭스 보드의 조립 과정은 매우 간단했습니다. 여기서 우리는 LED 매트릭스의 VCC 단자를 XIAO의 3.3V 핀에 연결하고, GND를 GND에 연결하며, LED 매트릭스의 DIN 핀을 XIAO의 D0 핀에 연결했습니다.
STEP 8. 데모 실행


이 코드는 이 프로젝트를 위한 것이며, 간단한 것입니다.
#include <Adafruit_NeoPixel.h>
#ifdef __AVR__
#include <avr/power.h> // 16 MHz Adafruit Trinket에 필요
#endif
#define PIN 0 // Trinket 또는 Gemma에서, 이를 1로 변경하는 것이 좋습니다
#define NUMPIXELS 128 // 인기 있는 NeoPixel 링 크기
Adafruit_NeoPixel pixels(NUMPIXELS, PIN, NEO_GRB + NEO_KHZ800);
#define DELAYVAL 80 // 픽셀 간의 지연 시간 (밀리초 단위)
void setup() {
#if defined(__AVR_ATtiny85__) && (F_CPU == 16000000)
clock_prescale_set(clock_div_1);
#endif
pixels.begin(); // NeoPixel 스트립 객체 초기화 (필수)
}
void loop() {
pixels.clear(); // 모든 픽셀 색상을 '꺼짐'으로 설정
for(int i=0; i<NUMPIXELS; i++) { // 각 픽셀에 대해...
pixels.setPixelColor(i, pixels.Color(0, 150, 0));
pixels.show(); // 업데이트된 픽셀 색상을 하드웨어에 전송합니다.
delay(DELAYVAL); // 루프를 통과하기 전 잠시 대기
}
}
이 데모 스케치는 Adafruit의 NeoPixel 라이브러리를 사용한 주소 지정 가능한 LED의 "헬로 월드"입니다 - 실제 펌웨어를 로드하기 전에 배선이 작동하는지 증명하기 위해 처음 실행했습니다.
STEP 9. 매트릭스 및 프레임 조립




- 매트릭스와 프레임 조립은 XIAO를 프레임의 상단에서 하단으로 통과시키는 것으로 시작합니다.
- 다음으로, LED 매트릭스를 상단에서 제자리에 위치시키고, 장착 구멍과 정렬한 후, 네 개의 M2 나사를 사용하여 보드를 고정합니다.
STEP 10. XIAO ESP32 C6 배치


XIAO는 제자리에 위치해 있으며, XIAO와 LED 매트릭스를 연결하는 전선이 깔끔하게 정렬되어 있습니다. 세 개의 전선 모두 프레임 본체 안쪽에 정리되어 있어 깔끔하고 컴팩트한 조립이 가능합니다.
STEP 11. 전원 조립





- LiPo 셀의 장착 영역 근처에 양면 테이프를 붙입니다. 핀셋을 사용하여 테이프 커버를 제거한 후, LiPo 셀을 제자리에 놓습니다. 배터리를 부드럽게 눌러 고정합니다.
- LiPo 셀의 양극 단자는 XIAO의 배터리 양극 패드에 납땜되어 있습니다.
- 다음으로, 슬라이드 스위치를 XIAO 바로 옆의 지정된 위치에 놓습니다. LiPo 셀의 음극 단자를 슬라이드 스위치의 공통(COM) 단자에 납땜합니다. 그런 다음 슬라이드 스위치의 NC 단자를 XIAO의 배터리 음극 패드에 연결합니다. 여기서 우리는 본질적으로 배터리의 음극 단자와 XIAO의 배터리 음극 입력 사이에 전원 스위치를 추가한 것입니다.
- 마지막으로, XIAO를 다시 장착 위치에 놓습니다.
STEP 12. 최종 조립



최종 조립을 위해 우리는 뚜껑 부분을 가져와 인클로저의 뒷면에 놓습니다. 그냥 제자리에 눌러주면 설계된 공차와 간격 덕분에 추가 고정 장치 없이도 단단하고 안전하게 맞습니다.
이로써 조립 과정이 마무리되었으며, 이제 사용할 준비가 된 견고하고 완전히 조립된 LED 매트릭스 배지가 있습니다.
STEP 13. 주요 코드
두 개의 파일이 전체 프로젝트를 구성합니다.
badge_firmware.ino는 ESP32-C6에서 실행되며, 블루투스를 통해 덜 듣는 것을 구동합니다.
index.html은 웹 블루투스를 통해 연결되는 제어 페이지이며, 화면에서 패널을 시뮬레이션합니다.
PART 1 — 펌웨어 (`badge_firmware.ino`)
cpp
#define DATA_PIN 0 // 매트릭스 데이터 와이어가 연결된 GPIO
#define WIDTH 8 // 논리적 너비, 착용 시 (세로)
#define HEIGHT 16 // 논리적 높이, 착용 시
#define PANEL_W 16 // 패널이 물리적으로 연결된 방식
#define PANEL_H 8
#define NUM_LEDS (PANEL_W * PANEL_H) // 128
#define SERPENTINE false // 홀수 행이 오른쪽에서 왼쪽으로 연결된 경우 true
#define ROTATE_180 false // 거꾸로 장착된 경우 모든 것을 뒤집음
#define NAME_TEXT "YOURNAME" // "이름" 모드가 스크롤하는 내용
#define SCROLL_MS 70 // 스크롤 단계당 ms (낮을수록 빠름)
#define EYES_DWELL_MS 8000 // 이름 스크롤 사이에 눈이 표시되는 시간
패널의 핵심 아이디어는 물리적으로 16 너비 x 8 높이의 스트립 체인이라는 것이지만, 배지는 수직으로 착용되므로 모든 그리기 코드는 논리적 8x16 세로 캔버스에서 생각하며, 하나의 함수 'XY'
아래는 물리적 배선으로 변환됩니다. 애니메이션 코드에서는 하드웨어 세부정보를 변경하지 마십시오.
cpp
CRGB leds[NUM_LEDS]; // 프레임 버퍼: 128 RGB 픽셀 FastLED가 출력합니다.
CRGB fgColor = ...; // 현재 배지 색상 (C: 명령으로 변경됨)
uint8_t brightness = 30; // 전역 밝기 (B: 명령으로 변경됨)
모드 상태
cpp
enum Mode { MODE_NAME, MODE_EYES, MODE_TEXT, MODE_RAIN, ... MODE_NYAN, MODE_DRAW };
volatile Mode mode = MODE_EYES; // 배지가 눈 모드로 부팅됨
String bleText = ""; // BLE를 통해 수신된 마지막 텍스트
volatile bool newBleMsg = false; // 플래그: "BLE 메시지가 대기 중입니다."
String bleBuf = ""; // 대기 중인 메시지 자체
volatile은 중요합니다. 이 모든 것은 블루투스 콜백에서 작성되며, 이는 다른 작업에서 실행되고 loop()에서 읽습니다. 이는 컴파일러에게 이 값이 뒤에서 변경될 수 있음을 알려주며, 캐시하지 않도록 합니다.
enum 숫자는 웹페이지가 보낸 숫자와 정확히 일치합니다. 'M:16' = 인덱스 16 = 'Mode_NYAN'
XY(x, y) 좌표 변환기
파일에서 가장 중요한 10줄입니다. 모든 것이 세로 좌표로 소모되며, 이는 하나의 논리적 (x,y)를 체인에서 물리적 LED의 인덱스로 변환합니다.
cpp
uint16_t XY(int8_t x, int8_t y) {
if (x < 0 || x >= WIDTH || y < 0 || y >= HEIGHT) return NUM_LEDS; // 그리드 외 가드
#if ROTATE_180
x = WIDTH - 1 - x; y = HEIGHT - 1 - y; // 선택적 180° 뒤집기
#endif
int8_t px = HEIGHT - 1 - y; // 세로를 가로 패널로 90° 회전
int8_t py = x;
if (SERPENTINE && (py & 1)) // 지그재그 패널: 홀수 행이 반전됨
return py * PANEL_W + (PANEL_W - 1 - px);
return py * PANEL_W + px; // 직선으로 연결된 패널
}
그리드 외 좌표는 "num_leds"를 반환하며, 이는 다음 함수가 안전하게 거부하는 버퍼 바로 뒤의 인덱스입니다. 따라서 애니메이션은 가장자리를 부분적으로 그릴 수 있으며 충돌하지 않습니다. 90° 회전은 텍스트가 전기적으로 옆으로 된 패널에 떨어질 수 있는 이유입니다.
SetPx(x, y, Colour)
cpp
void setPx(int8_t x, int8_t y, CRGB c) {
uint16_t i = XY(x, y);
if (i < NUM_LEDS) leds[i] = c;
}
픽셀을 안전하게 밝히는 유일한 방법은 좌표를 변환하고 가드를 확인한 후 프레임 버퍼에 쓰는 것입니다. 누군가 'FastLED.show()'를 호출하기 전까지는 물리적 렌즈에 아무것도 나타나지 않습니다.
폰트
'font5x7[]' 및 'fontCol()'
font5x7[]는 공백부터 'Z'까지의 각 ASCII 문자에 대해 5바이트 항목의 테이블입니다. 각 바이트는 문자의 한 열이며, 바이트의 각 비트는 한 행입니다. 비트 0 = 상단, 따라서 문자 A는 5열 x 7행의 켜짐/꺼짐 비트로 구성됩니다.
cpp
uint8_t fontCol(char c, uint8_t col) {
if (c >= 'a' && c <= 'z') c -= 32; // 소문자 → 대문자
if (c < 32 || c > 90) c = '?'; // 알 수 없는 문자는 ?로 출력
return pgm_read_byte(&font5x7[c - 32][col]);
}
PROGMEM / 'pgm_read_byte'는 테이블을 RAM 대신 플래시에 저장합니다.
스크롤러 'drawScrollFrame()'
수직으로 떨어지는 텍스트의 한 프레임을 그리며, 이를 한 픽셀만큼 이동시킵니다.
cpp
int scrollOffset; // 떨어진 거리
int textPixelHeight(const String &s) { return s.length() * 8; } // 7px 문자 + 1 간격
bool drawScrollFrame(const String &s) {
FastLED.clear();
for (uint16_t ci = 0; ci < s.length(); ci++) {
int cy = scrollOffset - (ci + 1) * 8; // 문자 0이 떨어짐을 이끈다
if (cy <= -8 || cy >= HEIGHT) continue; // 화면에 없는 문자는 건너뛴다
for (uint8_t col = 0; col < 5; col++) {
uint8_t bits = fontCol(s[ci], col);
for (uint8_t row = 0; row < 7; row++)
if (bits & (1 << row)) setPx(1 + col, cy + row, fgColor);
}
}
FastLED.show();
scrollOffset++;
if (scrollOffset > textPixelHeight(s) + HEIGHT) { scrollOffset = 0; return true; }
return false;
}
`cy = scrollOffset - (ci+1)*8`는 메시지의 첫 번째 문자가 먼저 들어오고 떨어지는 것을 이끄는 트릭으로, 다음 레이어가 그 위에 쌓여서 메시지를 순서대로 읽을 수 있게 합니다.
문자는 'x= 1..5'에서 8픽셀 너비로 중앙에 그려집니다.
한 번의 전체 패스가 끝나면 'true'를 반환하며, 이름 모드에서는 이를 사용하여 눈으로 돌아갑니다.
눈 — `drawEyesFrame()`
대기 애니메이션 하나의 큰 눈. 세 개의 작은 상태 기계가 동시에 실행됩니다.
cpp
int8_t pupilX, pupilTargetX; // 동공의 위치 / 원하는 위치 (-1..1)
uint8_t blinkPhase; // 0 = 열림, 1..12 = 중간 깜빡임 프레임 카운터
uint32_t nextBlinkAt, nextLookAt; // 다음 깜빡임 / 시선 이동 시간
각 프레임
- 깜빡임 스케줄러 - 'nextBlinkAt' 시간이 지나면 깜빡임을 시작합니다. 'blinkPhase'는 1에서 12까지 카운트합니다. 열림은 6에서 8까지 닫히고, 0-6까지 열리며, 새로운 랜덤 깜빡임 시간 1.5에서 5.5초가 선택됩니다.
- 시선 스케줄러 - 0.9에서 3.1초마다 새로운 'pupilTargetX' -1, 0 또는 1을 선택합니다. 동공은 매 프레임마다 한 픽셀씩 부드럽게 이동하며 순간이동하지 않습니다.
- 윤곽 그리기 - 타원 방정식으로 그립니다.
cpp
const float cx = 3.5f, cy = 7.5f, a = 4.0f; // 중심과 수평 반지름
float b = 0.6f + 3.2f * openness / 6.0f; // 수직 반지름은 깜빡임에 따라 변합니다.
...
float d = dx*dx + dy*dy; // 중심에서의 정규화된 거리
if (d <= 1.0f && d >= 0.45f) setPx(x, y, fgColor);
'd ≤ 1'인 점은 타원 내부에 있으며, `d ≥ 0.45`는 오직 링, 즉 윤곽만 유지합니다. 'b'를 줄이면 타원이 평평해져서 = 눈꺼풀이 닫히는 것입니다.
- 홍채 + 동공 - 어두운 'fgColour'의 둥근 4x4 패치 (`iris.nscale8_video(80)` = 30% 밝기. 눈이 충분히 열려 있을 때만 그려지는 2x2 빨간 동공입니다. (`openness >= 3`).
애니메이션
각 애니메이션은 단일 프레임을 그리며 'loop()'가 이를 반복 호출합니다. 일반적인 트릭들입니다.
- 'fadeToBlackBy(leds, Num_LEDS, n)' - 전체 프레임을 한 단계 어둡게 하며, 그 위에 매 프레임마다 모션 트레일을 생성합니다.
- 'sin8/beatsin8/`inoise8'—FastLED의 빠른 정수 파형 노이즈 생성기로, 느린 부동 소수점 대신 사용됩니다.
- 비 (Rain) (`drawRainFrame`) — 각 열에 대해 하나의 "드롭 헤드" (`rainHead[x]`). 매 프레임마다 모든 것을 페이드 아웃하고, 헤드를 흰색으로 그린 후 그 뒤의 픽셀을 'fgColour'로 설정하여 헤드를 아래로 이동시킵니다. 바닥을 지나면 헤드는 음수로 설정되어 오프스크린 카운트다운이 되며, 열은 무작위로 staggered한 시간에 재시작됩니다.
- 플라즈마 (Plasma) (`drawPlasmaFrame`) - 각 픽셀에 대해 위치와 시간의 세 개의 사인파를 추가하고, 그 합을 색조 흐르는 색 블롭으로 사용합니다. 상태가 전혀 없습니다. 순수한 수학입니다 'millis()'의.
- 불 (Fire) (`drawFireFrame`) - 고전적인 열 시뮬레이션입니다. 'heat [x] [y]' 그리드의 각 셀은 무작위 비트를 가지며, 열은 위로 떠오르고 각 셀은 아래 셀의 가중 평균이 됩니다. 무작위 스파크가 바닥 행에서 점화되며, 'HeatColour()'는 열을 0-255에서 검정-빨강-주황-흰색으로 매핑합니다.
- 스파클 (Sparkle) ('drawSparkleFrame') - 페이드 후, 일정 확률로 하나의 'fgColour' 점을 떨어뜨리고 가끔 무작위 위치에 흰색 점을 추가합니다. 반짝임.
- 형상 (Shapes) (`drawShapesFrame`) - 0.3초에서 0.9초마다 무작위 윤곽 사각형, 십자, 다이아몬드를 무작위 색조의 무작위 위치에 찍습니다. 전역 페이드가 그것들을 녹입니다.
- 생명의 게임 (Game of Life) (`drawLifeFrame`) - 랩어라운드 그리드에서 실제 Conway 규칙을 적용하여 각 셀의 8개의 이웃을 계산합니다. 살아있는 셀은 2-3에서 생존하고, 빈 셀은 정확히 3에서 태어납니다. 인구가 사라지거나 120세대 후에 자동으로 재시드되어 얼어붙은 패턴을 피합니다. 죽어가는 셀은 페이드 트레일을 남깁니다. 왜냐하면 프레임이 반만 페이드되기 때문입니다.
- 바운스 (Bounce) (`drawBounceFrame`) - 두 개의 공이 부동 위치와 속도를 가지며, 매 프레임마다 속도를 추가하고 벽에 부딪히면 부호가 반전됩니다. 하나는 흰색, 하나는 'fgColour'로 두 개 모두 특성을 남깁니다.
- 이퀄라이저 (Equalizer) (`drawEqFrame`) - 각 열마다, 'inoise8' 부드러운 무작위 노이즈가 목표 막대 높이를 선택하고, 막대는 매 프레임마다 그 방향으로 한 픽셀 이동합니다. 그 완화가 음악처럼 보이게 만듭니다. 높이에 따라 색상은 초록색 바닥, 호박색 중간, 빨간색 정점입니다.
- 레이더 (Radar) (`drawRadarFrame`) - 최대 3개의 'ripples'가 있으며, 각 ripples는 중심과 반경을 가지며 매 프레임마다 성장합니다. 제곱 거리 `(r-1)²`와 `r²` 사이에 있는 픽셀들이 확장되는 링을 형성합니다.
- 혜성 (Comet) (`drawCometFrame`) - 'beatsin8'에서 좌우로 스윕하는 전체 높이 막대, 부드러운 사인 바운스, 색조가 천천히 회전하며, 페이드에서 트레일이 생성됩니다.
- 별 (Stars) (`drawStarsFrame`) - 14개의 별, 각 별은 위치, 밝기 및 밝기 속도를 가집니다. 최대에 도달하면 서서히 어두워지고 0에 도달하면 새로운 무작위 위치에서 다시 생성됩니다. 약간 파란색이 섞인 흰색입니다.
- 펄스 (Pulse) (`drawPulseFrame`) - 전체 패널이 `beatsin8(16, 8, 160)`로 숨을 쉬며, 어두움과 밝음 사이에서 느린 사인을 제공합니다. 'fgColour'에 적용됩니다.
- 냐옹 (Nyan) (`drawNyanFrame`) - 각 셀에 대해 'PROGMEM' 테이블에 있는 손으로 그린 8x9 스프라이트입니다.
cpp
int8_t nyanWob(uint32_t t, uint8_t y) {
uint8_t s = sin8((uint8_t)(t / 6 + y * 32)); // 시간에 따른 사인, 각 행마다 위상 이동
return (s > 170) ? 1 : (s < 86 ? -1 : 0); // -1 / 0 / +1로 양자화
}
위상은 'y'에 따라 달라지므로, 오프셋은 파도의 흔적을 따라 내려갑니다. 고양이 자체가 상단 트레일 행의 오프셋을 설정하므로, 동기화되어 bobbing합니다. 스프링클은 250ms 타이머에서 색상을 교환합니다 = 반짝임.
블루투스 전체 BLE 섹션
배지는 Nordic UART Service NUS를 사용합니다. 고정 UUID를 가진 BLE를 통한 사실상의 표준 직렬 포트입니다. 모든 BLE 터미널 앱이 이를 사용할 수 있으며, 우리의 웹 페이지뿐만 아니라 가능합니다.
cpp
#define NUS_SERVICE "6E400001-..." // 서비스
#define NUS_RX "6E400002-..." // 전화가 명령을 여기에 씁니다
#define NUS_TX "6E400003-..." // 배지가 다시 알림을 보낼 수 있습니다 (현재는 사용되지 않음)
hexVal(c)는 하나의 헥스 문자 ('0'-'9', 'a'-'f', 'A'-'F')를 숫자로 변환합니다. 그리기 명령을 파싱하는 데 사용됩니다.
RxCallback::onWrite는 BLE 쓰기가 도착하는 즉시 실행됩니다.
cpp
if (v[0]=='P' && v[1]==':') { // 그리기 명령 → 지금 처리
// P:xyRRGGBB → x, y (각각 하나의 헥스 자리) 및 6자리 색상을 파싱합니다
setPx(x, y, CRGB(...)); // 프레임 버퍼에 직접 칠합니다
// P:C → FastLED.clear()
drawDirty = true; // loop()에 "무언가 변경되었습니다, 보여주세요"라고 알립니다
return;
}
bleBuf = v; newBleMsg = true; // 나머지는 loop()를 기다립니다
왜 두 경로인가? 일반 명령은 드물기 때문에 (버튼 한 번 누르기) 단일 메시지 버퍼로 충분하다. 그러나 드래그할 때 그리기는 메시지를 폭발적으로 전송하므로, 각 메시지가 'loop()'가 수신하기를 기다리면 일부는 읽히기 전에 'bleBuf'를 덮어쓰게 되어 픽셀이 떨어질 수 있다. 콜백에서 직접 그리기를 하면 손실 없이 처리된다.
SrvCallback::onDisconnect - 전화가 연결이 끊어지면 한 작업이 광고를 재시작하므로 배지가 즉시 다시 검색 가능해진다.
setupBLE() - 비율을 부팅하고 이름(EyeD Badge)을 설정하며, RX 쓰기 및 Tx 알림 특성을 가진 NUS 서비스를 생성하고, 콜백을 연결하고, 광고를 시작한다.
handleBleMsg() - 'newBleMsg'가 설정될 때 'loop'에서 호출된다. 명령을 파싱한다:
|메시지 | 의미 |
|---------------- |---------|
| `B:40` | 밝기 1-255 |
| `C:FF0044` | `fgColor` 설정 (hex RGB) |
| `M:5` | 모드 5로 전환; `M:17`은 그리기 캔버스를 지운다 |
| `P:xyRRGGBB` | (콜백에서 이전에 처리됨) 픽셀 x,y를 그린다 |
| `P:C` | 그리기 캔버스를 지운다 |
| 기타 모든 것 | 텍스트로 처리 → 스크롤한다 (모드 2) |
setup() 및 loop()
setup() - 한 번 실행된다. FastLED로 LED 스트립을 등록하고 ('WS2812B', 'GRB' 색상 순서), 밝기를 설정하고, BLE를 시작하고, 하드웨어 엔트로피에서 난수 생성기를 초기화하고, 비를 랜덤화한다.
loop() - 하트비트.
cpp
if (newBleMsg) handleBleMsg(); // 대기 중인 명령을 먼저 적용한다
switch (mode) { ... } // 그런 다음 현재 모드의 한 프레임을 그린다
- MODE_NAME - `NAME_TEXT`를 스크롤한다; 패스가 완료되면 눈으로 전환한다
- EYES_DWELL_MS - 그런 다음 다시 돌아온다. 이것이 대기 전환이다.
- MODE_EYES` - 눈 프레임; 이름 모드에서 온 경우에만 자동으로 이름으로 돌아간다 (`eyesUntil != 0`). 눈으로 바로 부팅하면 영원히 눈 모드에 머무른다.
- MODE_TEXT - 도착한 BLE 텍스트를 스크롤한다 (기본값은 "HELLO").
- MODE_CYCLE - 하위 모드 목록과 10초 타이머; `runAnim(subs[sub])` 현재 모드를 재생하고, 타이머는 다음으로 진행된다.
- MODE_DRAW - 자체적으로 그리지 않는다. BLE 콜백이 `drawDirty`를 설정할 때 캔버스는 `leds[]`에 존재하며, 하나의 `show()`로 출력한다. 이것이 당신의 그리기가 지속되는 이유이다 — 당신이 떠날 때까지 아무것도 그것을 지우지 않는다. 나머지는 `runAnim(mode)`이다.
- runAnim(m) - 올바른 'draw...Frame()'를 호출하는 조회 및 모드별 프레임 지연 (플라즈마는 30 ms, 생명의 게임은 140 ms에서 느긋하게 실행된다.. 일반 경로와 사이클 모드에서 공유되므로 타이밍이 한 곳에 존재한다)
웹 페이지 'index.html'
하나의 독립형 파일 - 스타일, 마크업 및 세 개의 JS 섹션 (BLE, 시뮬레이터, 그리기), 프레임워크 없음, 빌드 단계 없음.
페이지 구조 'HTML'
- 헤더 - 제목과 상태 점 + '대기 / 연결됨' 레이블
- <canvas id="cv"> - 8x16 LED 미리보기 320x640 = LED당 40 px
- 연결 버튼 - 연결하기 전 유일하게 활성화된 것
- 제어 - 연결될 때까지 모든 것이 흐릿하게 ('숨김') 처리된다:
- 텍스트 입력 + 전송
- 모드 버튼 (JS 목록에서 생성됨)
- 그리기 섹션: 슬라이더 토글, 색상 견본, 사용자 정의 색상, 지우기
- 밝기 슬라이더
- 색상 견본
로그 - 하단의 온라인 상태 / 디버그 출력
BLE 섹션
js
const NUS_SERVICE='6e400001-...', NUS_RX='6e400002-...';
let device=null, rxChar=null; // 연결된 장치와 그 RX 특성
setUI(connected) - 모든 시각 요소를 대기 및 연결 상태 간에 전환하며 버튼 텍스트, 빛나는 점, 제어 흐림 해제 등을 처리한다. 연결이 끊어지면 그리기 모드를 끄고 미리보기를 지운다.
toggleConnect() - 전체 연결 동작
js
device = await navigator.bluetooth.requestDevice({
filters: [{ namePrefix: 'EyeD' }], // 선택기에서 배지만
optionalServices: [NUS_SERVICE]
});
const sv = await (await device.gatt.connect()).getPrimaryService(NUS_SERVICE);
rxChar = await sv.getCharacteristic(NUS_RX); // 쓰기 파이프를 가져온다
또한 배지가 범위를 벗어날 경우 UI가 재설정되도록 'gattserverdisconnected' 리스너를 등록한다. 브라우저에 'navigator.bluetooth'가 없는 경우 (iPhone Safari), 대신 'Bluefy 사용' 힌트를 기록한다.
- sendCmd(cmd) - 문자열을 바이트로 인코딩하고 'rxChar'에 쓴다.
- 20바이트 청크 - 기본 BLE 연결에 대한 안전한 페이로드 크기.
- 'sendText()' / 'uiBrightness()' / 'pickMode()' / 'pickColor()' - 얇은 래퍼가 입력을 읽고 일치하는 명령을 전송하며 (`M:`, `B:`, `C:` 또는 원시 텍스트), 어떤 버튼이 강조 표시되는지 업데이트하고 로컬 시뮬레이터에서 변경 사항을 반영하여 미리보기가 배지와 일치하도록 한다.
js
const MODES=[['name',0,'scroll'],['eyes',1,'eyes'],...];
각 항목 = 버튼 레이블 펌웨어 모드 번호, 시뮬레이터 애니메이션 이름 버튼은 이 목록에서 생성됩니다. 펌웨어에 모드를 추가하고, 여기 한 줄을 추가하세요. 완료.
LED 시뮬레이터
미리보기는 비디오가 아닙니다 - 자바스크립트로 모든 펌웨어 애니메이션을 재구현하여 같은 종류의 버퍼에 그립니다.
js
const W=8, H=16;
let buf = new Float32Array(W*H*3); // JS "프레임 버퍼", RGB 0..1 부동소수점
function setP(x,y,r,g,b) { ... } // = 펌웨어 setPx
function fade(f) { ... } // = fadeToBlackBy
function clearBuf() { ... } // = FastLED.clear
- FDATA — 펌웨어와 동일한 5×7 폰트, JS 객체로.
- S— 애니메이션 함수의 객체: `S.scroll`, `S.eyes`, `S.rain`, `S.fire`, `S.nyan`, ... 각각은 펌웨어 쌍의 라인별 번역(같은 상수, 같은 파형 수학)으로, 미리보기와 배지가 시각적으로 동기화됩니다.
- S.breathe - 대기 상태에서 버퍼를 지웁니다 = 모든 LED가 어두워집니다.
- simSet(mode, label) - 시뮬레이션 모드를 전환하고 캔버스 아래의 작은 레이블을 업데이트합니다.
렌더 루프 - ('frame()' + 'draw()'), 'requestAnimationFrame'에 의해 구동됩니다.
- IV - 각 모드를 프레임 간격에 매핑(펌웨어의 모드별을 반영하되 캔버스를 전체 60fps로 다시 렌더링합니다).
이것이 두 파일 간의 전체 인터페이스입니다 — 여섯 개의 메시지 형태.
코드 링크 - 여기
STEP 14. 결과











여기 결과물인 EyeD 배지가 모든 사람을 지켜보고 있습니다.
매일매일, 그것은 제 목걸이에 걸려 눈을 깜빡이며 돌아다니고, 가끔씩 정확한 순간에 누군가를 소름끼치게 합니다.
누군가 그것에 대해 물어보면(항상 물어봅니다), 저는 제 전화기를 꺼내 두 번 탭하여 연결하고, 배지에 그들의 이름이 스크롤되게 하거나 드로우 모드를 열어 그들이 제 가슴에 픽셀을 그리게 합니다. 그 파티 트릭은 한 번도 실패한 적이 없습니다.
불, 매트릭스 비, 이퀄라이저, 그리고 흔들리는 무지개 꼬리를 가진 냐옹 고양이가 나머지 주목을 처리합니다.
버전 2에서 이 프로젝트에 추가할 수 있는 것이 무엇인지 알려주세요. 아마도 LED, XIAO 및 기타 모든 것을 단일 맞춤형 PCB에 통합하는 버전을 디자인할 수 있을 것입니다. 현재 PCB 디자인을 배우고 있으며, 그것은 제가 도전하고 싶은 확실한 도전이 될 것입니다.
하지만 이 프로젝트는 여기까지입니다. 다시 한 번, 이 빌드에 사용된 LED 매트릭스를 제공해 주신 Arnov Sharma에게 특별히 감사드리며, 여기까지 함께 해주신 모든 분들께 감사드립니다.
안녕!
원문: https://www.instructables.com/Eye-D-Badge/