Skip to content

Repository files navigation

ESP32-C3 BLE GATT Server + Go GUI Client

한국어 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.


GATT Profile

Item Value
Service UUID 0x00FF
Advertising Name ESP_GATTS_DEMO

Characteristics

UUID Properties Direction Purpose
0xFF01 Read / Write / Notify Bidirectional Data exchange, heartbeat
0xFF02 Write / Write-Without-Response Client → ESP32 GPIO output control

0xFF02 Write Protocol

2-byte binary:

[pin, val]

e.g. [0x03, 0x00] → GPIO3 LOW  (Active Low LED: ON)
     [0x03, 0x01] → GPIO3 HIGH (Active Low LED: OFF)

Hardware

Item Detail
MCU ESP32-C3
LED GPIO3, Active Low (LOW = ON)
Connection USB (flash & monitor)

GPIO Pin Layout

GPIO3 ── Resistor(220Ω) ── LED(+) ── GND

How It Works

┌──────────────────┐                          ┌──────────────────────┐
│   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]                           │
         │ ──────────────────────────────────────────── >│
         │                                               │
└────────┴─────────┘                          └──────────┴───────────┘

ESP32-C3 Firmware

Build & Flash

idf.py set-target esp32c3
idf.py build
idf.py -p PORT flash monitor

Key Implementation

  • FF01: Read/Write/Notify characteristic. Echoes "XIAO OK" on write
  • FF02: GPIO control write characteristic. Receives [pin, val] 2 bytes → calls gpio_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

Windows Clients

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

Common Dependencies

Package Purpose
tinygo.org/x/bluetooth v0.14.0 BLE (Windows WinRT backend)
Go 1.22+

Common Features

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.

Python Clients

Two Python clients are available in the python/ directory.

Dependencies

pip install bleak

gatt_client.py (CLI)

asyncio-based command-line client.

Auto mode (default):

python gatt_client.py

Auto-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 interactive

Repeatedly writes user input to FF01 after connection. Enter q to quit.

gatt_client_gui.py (GUI)

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.py

Runs asyncio loop in a separate thread, communicating with the UI thread via queue.Queue.


File Structure

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 Monitoring

Hardware (XIAO ESP32-C3)

  • 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.

Software Architecture

[10ms esp_timer] → Read ADC once → Update EMA → bat_avg_raw
[30s heartbeat]  → Read bat_avg_raw → Convert to voltage → Log battery level (%)

Exponential Moving Average (EMA)

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

Battery Level Calculation

Voltage Level
4.2V (BAT_FULL_MV) 100%
3.0V (BAT_EMPTY_MV) 0%
level(%) = (voltage_mV - 3000) / (4200 - 3000) × 100

Tuning Guide

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

Current Measurement Accuracy

  • Voltage variance: ±40mV
  • Level variance: ±3%

Troubleshooting

GPIO commands stop working after 1+ hour idle

  • 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 Write first, falling back to WriteWithoutResponse on failure

ESP32 not visible in BLE scan

  • 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 제어를 수행하는 프로젝트입니다.


GATT 프로파일

항목
Service UUID 0x00FF
광고 이름 ESP_GATTS_DEMO

Characteristics

UUID 속성 방향 용도
0xFF01 Read / Write / Notify 양방향 데이터 교환, Heartbeat 수신
0xFF02 Write / Write-Without-Response 클라이언트 → ESP32 GPIO 출력 제어

0xFF02 Write 프로토콜

2바이트 바이너리:

[pin, val]

예) [0x03, 0x00] → GPIO3 LOW  (Active Low LED: ON)
    [0x03, 0x01] → GPIO3 HIGH (Active Low LED: OFF)

Hardware

항목 내용
MCU ESP32-C3
LED GPIO3, Active Low (LOW = ON)
연결 USB (플래시 및 모니터)

GPIO 핀 배치

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] 전송                      │
         │ ──────────────────────────────────────────── >│
         │                                               │
└────────┴─────────┘                          └──────────┴───────────┘

ESP32-C3 펌웨어

빌드 및 플래시

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

Windows 클라이언트

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 클라이언트

python/ 디렉토리에 2종류의 Python 클라이언트가 있습니다.

의존성

pip install bleak

gatt_client.py (CLI)

asyncio 기반 커맨드라인 클라이언트.

자동 모드 (기본):

python gatt_client.py

ESP_GATTS_DEMO 장치를 자동 스캔 → 연결 → 서비스 탐색 → FF01 Notify 활성화 → Read → Write (Hello ESP32!) → Notify 수신 2초 대기 → 종료

대화형 모드:

python gatt_client.py interactive

연결 후 입력한 메시지를 FF01에 반복 Write. q 입력 시 종료.

gatt_client_gui.py (GUI)

bleak + tkinter(ttk) 기반 GUI 클라이언트. 창 크기: 1000×720.

기능 설명
BLE 스캔 5초 스캔, 이름/주소 필터 지원
장치 연결 목록 더블클릭 또는 연결 버튼
서비스/특성 트리 연결 후 자동 표시
Read / Write / Notify 특성 선택 후 조작, 텍스트/HEX 쓰기 모드
Notify 수신 패널 수신 건수 카운트, 타임스탬프 표시
로그 패널 모든 동작 기록
python gatt_client_gui.py

asyncio 루프를 별도 스레드에서 실행하고 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

배터리 모니터링

하드웨어 (XIAO ESP32-C3)

  • 배터리(+) → 내부 전압 분압기 → 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 읽기 → 전압 변환 → 잔량(%) 로그 출력

지수 이동 평균 (EMA)

파라미터 설명
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%

Troubleshooting

1시간 이상 방치 후 GPIO 명령이 동작하지 않는 경우

  • 원인: Windows BLE 스택 절전으로 WriteWithoutResponse가 실제 전송되지 않음
  • 해결: ESP32 heartbeat(30초 주기 Notify)로 연결 활성 상태 유지. Go 클라이언트는 응답 있는 Write를 우선 사용하고 실패 시 WriteWithoutResponse로 재시도

ESP32가 BLE 스캔에 보이지 않는 경우

  • BLE 기기는 연결 중에는 광고를 중단함 → 정상 동작
  • 연결이 끊긴 경우 ESP32는 자동으로 재광고 시작

About

ESP32-C3 BLE GATT Server with Go GUI clients (Fyne/Wails) and Python clients — GPIO control, battery monitoring, heartbeat keep-alive

Topics

Resources

Stars

1 star

Watchers

0 watching

Forks

Releases

Contributors

Languages