한국어 is below.
BLE GATT server running on ESP32-C3 with multiple PC client implementations — Go GUI (Fyne/Wails) and Python (CLI/GUI). Supports data exchange, GPIO control, and battery monitoring over Bluetooth Low Energy.
| Item | Value |
|---|---|
| Service UUID | 0x00FF |
| Advertising Name | ESP_GATTS_DEMO |
| UUID | Properties | Direction | Purpose |
|---|---|---|---|
0xFF01 |
Read / Write / Notify | Bidirectional | Data exchange, heartbeat |
0xFF02 |
Write / Write-Without-Response | Client → ESP32 | GPIO output control |
2-byte binary:
[pin, val]
e.g. [0x03, 0x00] → GPIO3 LOW (Active Low LED: ON)
[0x03, 0x01] → GPIO3 HIGH (Active Low LED: OFF)
| Item | Detail |
|---|---|
| MCU | ESP32-C3 |
| LED | GPIO3, Active Low (LOW = ON) |
| Connection | USB (flash & monitor) |
GPIO3 ── Resistor(220Ω) ── LED(+) ── GND
┌──────────────────┐ ┌──────────────────────┐
│ ESP32-C3 │ │ Go Client (Windows) │
│ GATT Server │ │ Fyne / Wails GUI │
└────────┬─────────┘ └──────────┬───────────┘
│ │
│ ── Initialization ────────────────────── │
│ 1. Create Service 0x00FF │
│ 2. Add FF01 Char (Read/Write/Notify) │
│ 3. Add FF01 CCCD Descriptor │
│ 4. Add FF02 Char (Write-only, GPIO control) │
│ 5. Start advertising "ESP_GATTS_DEMO" │
│ 6. Initialize GPIO3 output (HIGH = LED OFF) │
│ │
│ ── Connection ────────────────────────── │
│ Scan & Connect │
│ <─────────────────────────────────────────── │
│ │
│ Connection established + MTU negotiation │
│ ──────────────────────────────────────────── >│
│ │
│ ── Service Discovery ─────────────────── │
│ Discover Services/Chars │
│ <─────────────────────────────────────────── │
│ Return FF01, FF02 info │
│ ──────────────────────────────────────────── >│
│ │
│ ── Auto Setup ────────────────────────── │
│ Enable FF01 Notify │
│ <─────────────────────────────────────────── │
│ │
│ ── GPIO Control ──────────────────────── │
│ FF02 Write [03, 00] │
│ <─────────────────────────────────────────── │
│ GPIO3 LOW → LED ON │
│ │
│ FF02 Write [03, 01] │
│ <─────────────────────────────────────────── │
│ GPIO3 HIGH → LED OFF │
│ │
│ ── Keep-Alive (every 30s) ────────────── │
│ FF01 Notify [0x00] │
│ ──────────────────────────────────────────── >│
│ │
└────────┴─────────┘ └──────────┴───────────┘
idf.py set-target esp32c3
idf.py build
idf.py -p PORT flash monitor- FF01: Read/Write/Notify characteristic. Echoes "XIAO OK" on write
- FF02: GPIO control write characteristic. Receives
[pin, val]2 bytes → callsgpio_set_level() - Heartbeat: Sends 1-byte (
0x00) notify via FF01 every 30s to prevent Windows BLE power saving disconnection - GPIO3: Initialized as output on boot, Active Low LED
Three Go client implementations sharing the same BLE logic with different UI frameworks:
| Directory | Framework | Notes |
|---|---|---|
go_fyne/ |
Fyne v2.7.3 | Single .go file, custom widgets (ColorBar/StatusDot), requires MinGW |
go_wails/ |
Wails v2.11.0 + Vanilla JS | WebView2-based, custom CSS dark theme |
go_wails_tailwind/ |
Wails v2.11.0 + Tailwind CSS v3 | WebView2-based, Tailwind utility classes |
| Package | Purpose |
|---|---|
tinygo.org/x/bluetooth v0.14.0 |
BLE (Windows WinRT backend) |
| Go 1.22+ | — |
| Feature | Description |
|---|---|
| BLE Scan | Name/address filter support |
| Auto Connect | FF01 Notify auto-enable, FF02/2A19 auto-discovery |
| Read / Write / Notify | Select characteristic from tree, then operate |
| GPIO Control | GPIO3 LED ON/OFF via FF02 (2 bytes: [pin, val]) |
| Battery Display | Color bar (0x2A19 Notify, received on connect) |
| Connection Status | Color dot indicator |
| Heartbeat | Received via FF01 Notify (handled automatically) |
See each directory's README.md for detailed build instructions.
Two Python clients are available in the python/ directory.
pip install bleakasyncio-based command-line client.
Auto mode (default):
python gatt_client.pyAuto-scans for ESP_GATTS_DEMO → connects → discovers services → enables FF01 Notify → Read → Write (Hello ESP32!) → waits 2s for Notify → exits.
Interactive mode:
python gatt_client.py interactiveRepeatedly writes user input to FF01 after connection. Enter q to quit.
bleak + tkinter (ttk) GUI client. Window size: 1000×720.
| Feature | Description |
|---|---|
| BLE Scan | 5-second scan with name/address filter |
| Connect | Double-click device list or click connect button |
| Service/Char Tree | Auto-displayed after connection |
| Read / Write / Notify | Select characteristic, text/HEX write modes |
| Notify Panel | Received count, timestamp display |
| Log Panel | All operations logged |
python gatt_client_gui.pyRuns asyncio loop in a separate thread, communicating with the UI thread via
queue.Queue.
ble_server/
├── main/
│ ├── gatts_demo.c # ESP32-C3 firmware (GATT server + GPIO control)
│ └── CMakeLists.txt
├── go_fyne/
│ ├── gui_main.go # Fyne GUI client (single file)
│ ├── go.mod / go.sum
│ └── README.md
├── go_wails/
│ ├── main.go / app.go # Wails + Vanilla JS client
│ ├── frontend/ # HTML / CSS / JS
│ ├── go.mod / go.sum
│ └── README.md
├── go_wails_tailwind/
│ ├── main.go / app.go # Wails + Tailwind CSS client
│ ├── frontend/ # HTML / CSS / JS + tailwind.min.css
│ ├── go.mod / go.sum
│ └── README.md
├── python/
│ ├── gatt_client.py # Python CLI client
│ └── gatt_client_gui.py # Python GUI client
└── README.md
- Battery(+) → internal voltage divider → GPIO2 (A0)
- Divider ratio: measured 9.92 (battery 3600mV / ADC 363mV)
- ADC unit: ADC1, channel 2, 12dB attenuation (range 0–3100mV)
Battery(+) ──┬── R_upper ──┬── GPIO2 (ADC)
│ │
[3.6V] [~363mV]
R_lower
│
GND
Note: ESP32-C3 ADC readings differ from multimeter measurements (~700mV vs ~363mV) due to loading effects on high-impedance sources. Use actual ADC readings to calculate the divider ratio.
[10ms esp_timer] → Read ADC once → Update EMA → bat_avg_raw
[30s heartbeat] → Read bat_avg_raw → Convert to voltage → Log battery level (%)
| Parameter | Value | Description |
|---|---|---|
BAT_EMA_ALPHA |
0.01f |
EMA coefficient. Lower = stronger smoothing |
| Sampling interval | 10ms | BAT_TIMER_US = 10000 |
| Effective time constant | ~2s (200 samples) | 1 / BAT_EMA_ALPHA |
| Noise reduction | ~14× | vs single sample |
EMA formula:
EMA = alpha × new_sample + (1 - alpha) × EMA_prev
| Voltage | Level |
|---|---|
4.2V (BAT_FULL_MV) |
100% |
3.0V (BAT_EMPTY_MV) |
0% |
level(%) = (voltage_mV - 3000) / (4200 - 3000) × 100
| Goal | Parameter | Method |
|---|---|---|
| More stable readings | BAT_EMA_ALPHA |
0.01 → 0.005 (halves variance) |
| Faster response | BAT_EMA_ALPHA |
0.01 → 0.05 |
| Recalibrate divider | BAT_DIVIDER |
actual_battery(mV) / ADC_calibrated(mV) |
| Change sampling rate | BAT_TIMER_US |
Unit: microseconds |
- Voltage variance: ±40mV
- Level variance: ±3%
- Cause: Windows BLE stack power saving prevents WriteWithoutResponse from actually transmitting
- Solution: ESP32 heartbeat (30s Notify interval) keeps the connection alive. Go clients prefer acknowledged
Writefirst, falling back toWriteWithoutResponseon failure
- BLE devices stop advertising while connected — this is normal behavior
- After disconnection, ESP32 automatically resumes advertising
ESP32-C3를 BLE GATT 서버로 동작시키고, Windows PC의 Go GUI 클라이언트(Fyne)로 연결하여 데이터 교환 및 GPIO 제어를 수행하는 프로젝트입니다.
| 항목 | 값 |
|---|---|
| Service UUID | 0x00FF |
| 광고 이름 | ESP_GATTS_DEMO |
| UUID | 속성 | 방향 | 용도 |
|---|---|---|---|
0xFF01 |
Read / Write / Notify | 양방향 | 데이터 교환, Heartbeat 수신 |
0xFF02 |
Write / Write-Without-Response | 클라이언트 → ESP32 | GPIO 출력 제어 |
2바이트 바이너리:
[pin, val]
예) [0x03, 0x00] → GPIO3 LOW (Active Low LED: ON)
[0x03, 0x01] → GPIO3 HIGH (Active Low LED: OFF)
| 항목 | 내용 |
|---|---|
| MCU | ESP32-C3 |
| LED | GPIO3, Active Low (LOW = ON) |
| 연결 | USB (플래시 및 모니터) |
GPIO3 ── 저항(220Ω) ── LED(+) ── GND
┌──────────────────┐ ┌──────────────────────┐
│ ESP32-C3 │ │ Go Client (Windows) │
│ GATT Server │ │ Fyne / Wails GUI │
└────────┬─────────┘ └──────────┬───────────┘
│ │
│ ── 초기화 ────────────────────────────── │
│ 1. Service 0x00FF 생성 │
│ 2. FF01 Char 추가 (Read/Write/Notify) │
│ 3. FF01 CCCD Descriptor 추가 │
│ 4. FF02 Char 추가 (Write전용, GPIO제어) │
│ 5. 광고 시작 "ESP_GATTS_DEMO" │
│ 6. GPIO3 출력 초기화 (HIGH = LED OFF) │
│ │
│ ── 연결 ──────────────────────────────── │
│ 스캔 & 연결 │
│ <─────────────────────────────────────────── │
│ │
│ 연결 수립 + MTU 협상 (500) │
│ ──────────────────────────────────────────── >│
│ │
│ ── 서비스 탐색 ────────────────────────── │
│ 서비스/Char 탐색 │
│ <─────────────────────────────────────────── │
│ FF01, FF02 정보 반환 │
│ ──────────────────────────────────────────── >│
│ │
│ ── 자동 설정 ──────────────────────────── │
│ FF01 Notify 활성화 │
│ <─────────────────────────────────────────── │
│ (FF02는 Write 전용, Notify 없음) │
│ │
│ ── GPIO 제어 ──────────────────────────── │
│ FF02 Write [03, 00] │
│ <─────────────────────────────────────────── │
│ GPIO3 LOW → LED ON │
│ │
│ FF02 Write [03, 01] │
│ <─────────────────────────────────────────── │
│ GPIO3 HIGH → LED OFF │
│ │
│ ── Keep-Alive (30초마다) ──────────────── │
│ FF01 Notify [0x00] 전송 │
│ ──────────────────────────────────────────── >│
│ │
└────────┴─────────┘ └──────────┴───────────┘
idf.py set-target esp32c3
idf.py build
idf.py -p PORT flash monitor- FF01: 기존 Read/Write/Notify Characteristic. Write 수신 시 "XIAO OK" 에코 응답
- FF02: GPIO 제어 전용 Write Characteristic.
[pin, val]2바이트 수신 →gpio_set_level()호출 - Heartbeat: 연결 중 30초마다 FF01 Notify로 1바이트(
0x00) 전송 → Windows BLE 절전 방지 - GPIO3: 부팅 시 출력 초기화, Active Low LED
BLE 클라이언트는 3종류의 구현체가 있습니다. 모두 동일한 BLE 로직을 공유하며 UI 프레임워크만 다릅니다.
| 디렉토리 | 프레임워크 | 특징 |
|---|---|---|
go_fyne/ |
Fyne v2.7.3 | 단일 .go 파일, 커스텀 위젯(ColorBar/StatusDot), MinGW 필요 |
go_wails/ |
Wails v2.11.0 + 바닐라 JS | WebView2 기반, 커스텀 CSS 다크 테마 |
go_wails_tailwind/ |
Wails v2.11.0 + Tailwind CSS v3 | WebView2 기반, Tailwind 유틸리티 클래스 |
| 패키지 | 용도 |
|---|---|
tinygo.org/x/bluetooth v0.14.0 |
BLE (Windows WinRT 백엔드) |
| Go 1.22+ | - |
| 기능 | 설명 |
|---|---|
| BLE 스캔 | 이름/주소 필터 지원 |
| 자동 연결 설정 | FF01 Notify 자동 활성화 및 자동 선택, FF02/2A19 자동 탐색 |
| Read / Write / Notify | 트리에서 특성 선택 후 조작 |
| GPIO 제어 | GPIO3 LED ON/OFF (FF02, 2바이트: [pin, val]) |
| 배터리 표시 | 색상 바 (0x2A19 Notify, 연결 시 즉시 수신) |
| 연결 상태 표시 | 색상 도트 |
| Heartbeat 수신 | FF01 Notify로 수신 (자동 처리) |
각 클라이언트의 상세 빌드 방법은 해당 디렉토리의 README.md를 참고하세요.
python/ 디렉토리에 2종류의 Python 클라이언트가 있습니다.
pip install bleakasyncio 기반 커맨드라인 클라이언트.
자동 모드 (기본):
python gatt_client.pyESP_GATTS_DEMO 장치를 자동 스캔 → 연결 → 서비스 탐색 → FF01 Notify 활성화 → Read → Write (Hello ESP32!) → Notify 수신 2초 대기 → 종료
대화형 모드:
python gatt_client.py interactive연결 후 입력한 메시지를 FF01에 반복 Write. q 입력 시 종료.
bleak + tkinter(ttk) 기반 GUI 클라이언트. 창 크기: 1000×720.
| 기능 | 설명 |
|---|---|
| BLE 스캔 | 5초 스캔, 이름/주소 필터 지원 |
| 장치 연결 | 목록 더블클릭 또는 연결 버튼 |
| 서비스/특성 트리 | 연결 후 자동 표시 |
| Read / Write / Notify | 특성 선택 후 조작, 텍스트/HEX 쓰기 모드 |
| Notify 수신 패널 | 수신 건수 카운트, 타임스탬프 표시 |
| 로그 패널 | 모든 동작 기록 |
python gatt_client_gui.pyasyncio 루프를 별도 스레드에서 실행하고
queue.Queue로 UI 스레드와 통신하는 구조.
ble_server/
├── main/
│ ├── gatts_demo.c # ESP32-C3 펌웨어 (GATT 서버 + GPIO 제어)
│ └── CMakeLists.txt
├── go_fyne/
│ ├── gui_main.go # Fyne GUI 클라이언트 (단일 파일)
│ ├── go.mod / go.sum
│ └── README.md
├── go_wails/
│ ├── main.go / app.go # Wails + 바닐라 JS 클라이언트
│ ├── frontend/ # HTML / CSS / JS
│ ├── go.mod / go.sum
│ └── README.md
├── go_wails_tailwind/
│ ├── main.go / app.go # Wails + Tailwind CSS 클라이언트
│ ├── frontend/ # HTML / CSS / JS + tailwind.min.css
│ ├── go.mod / go.sum
│ └── README.md
├── python/
│ ├── gatt_client.py # Python CLI 클라이언트
│ └── gatt_client_gui.py # Python GUI 클라이언트
└── README.md
- 배터리(+) → 내부 전압 분압기 → GPIO2 (A0)
- 분압비: 실측 9.92 (배터리 3600mV / ADC 363mV)
- ADC 유닛: ADC1, 채널 2, 감쇠 12dB (측정범위 0~3100mV)
Battery(+) ──┬── R_upper ──┬── GPIO2 (ADC)
│ │
[3.6V] [~363mV]
R_lower
│
GND
주의: ESP32-C3 ADC는 고임피던스 소스에서 부하 효과로 멀티미터 측정값(~700mV)과 실제 ADC 읽기값(~363mV)이 다름. ADC 실측값 기준으로 분압비를 산출해야 함.
[10ms esp_timer] → ADC 1회 읽기 → EMA 업데이트 → bat_avg_raw
[30초 heartbeat] → bat_avg_raw 읽기 → 전압 변환 → 잔량(%) 로그 출력
| 파라미터 | 값 | 설명 |
|---|---|---|
BAT_EMA_ALPHA |
0.01f |
EMA 계수. 낮을수록 평활화 강도 증가 |
| 샘플링 주기 | 10ms | BAT_TIMER_US = 10000 |
| 유효 평균 시정수 | ~2초 (200샘플) | 1 / BAT_EMA_ALPHA |
| 노이즈 감소 | ~14배 | 단일 샘플 대비 |
EMA 공식:
EMA = alpha × new_sample + (1 - alpha) × EMA_prev
| 전압 | 잔량 |
|---|---|
4.2V (BAT_FULL_MV) |
100% |
3.0V (BAT_EMPTY_MV) |
0% |
level(%) = (voltage_mV - 3000) / (4200 - 3000) × 100
| 목적 | 파라미터 | 방법 |
|---|---|---|
| 더 안정적인 값 | BAT_EMA_ALPHA |
0.01 → 0.005 (변동 절반으로 감소) |
| 더 빠른 응답 | BAT_EMA_ALPHA |
0.01 → 0.05 |
| 분압비 재보정 | BAT_DIVIDER |
실제 배터리(mV) / ADC 캘리브레이션값(mV) |
| 샘플링 속도 변경 | BAT_TIMER_US |
단위: 마이크로초 |
- 전압 변동: ±40mV
- 잔량 변동: ±3%
- 원인: Windows BLE 스택 절전으로 WriteWithoutResponse가 실제 전송되지 않음
- 해결: ESP32 heartbeat(30초 주기 Notify)로 연결 활성 상태 유지. Go 클라이언트는 응답 있는
Write를 우선 사용하고 실패 시WriteWithoutResponse로 재시도
- BLE 기기는 연결 중에는 광고를 중단함 → 정상 동작
- 연결이 끊긴 경우 ESP32는 자동으로 재광고 시작