Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions include/config.h
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,11 @@ typedef enum {
#define ADV_INTERVAL_MIN 160
#define ADV_INTERVAL_MAX 160

// Extended Advertising 인스턴스 ID
#define ADV_INSTANCE_FIXED 0 // 고정 비콘 (항상)
#define ADV_INSTANCE_SESSION 1 // 출석 비콘 (ACTIVE 시에만)
#define ADV_INSTANCE_GATT 2 // GATT 연결용 (connectable)

// ── NVS 설정 ────────────────────────────────────
#define NVS_NAMESPACE "beacon"
#define NVS_KEY_PSK "psk"
Expand Down
2 changes: 1 addition & 1 deletion platformio.ini
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ build_flags =
-DARDUINO_USB_MODE=1
-DARDUINO_USB_CDC_ON_BOOT=1
-DCONFIG_BT_NIMBLE_EXT_ADV=1
-DCONFIG_BT_NIMBLE_MAX_EXT_ADV_INSTANCES=2
-DCONFIG_BT_NIMBLE_MAX_EXT_ADV_INSTANCES=3

; --- Upload ---
board_upload.flash_size = 16MB
Expand Down
99 changes: 96 additions & 3 deletions src/ble_beacon.ino
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,16 @@

extern uint8_t g_beacon_uuid[];
extern char g_device_name[];
extern beacon_state_t g_state;

// Extended Advertising 인스턴스 ID
#define ADV_INSTANCE_FIXED 0 // 고정 비콘 (항상)
#define ADV_INSTANCE_SESSION 1 // 출석 비콘 (ACTIVE 시에만, Step 5-6에서 구현)
// gatt_server.ino 전방 선언
bool gatt_init();

NimBLEExtAdvertising* g_pAdvertising = nullptr;
TimerHandle_t g_sessionTimer = nullptr;
volatile bool g_session_expired = false; // 타이머 만료 플래그
volatile bool g_session_started = false; // 출석 시작 플래그
volatile uint16_t g_session_duration = 0; // 출석 Duration (로그용)

// iBeacon 광고 데이터를 생성하여 NimBLEExtAdvertisement에 설정
void build_ibeacon_adv(NimBLEExtAdvertisement& adv, const uint8_t* uuid) {
Expand Down Expand Up @@ -52,6 +56,11 @@ void build_ibeacon_adv(NimBLEExtAdvertisement& adv, const uint8_t* uuid) {
bool ble_init_and_start() {
NimBLEDevice::init(g_device_name);

// GATT 서버를 광고보다 먼저 초기화해야 GAP 서비스 충돌이 없음
if (!gatt_init()) {
return false;
}

g_pAdvertising = NimBLEDevice::getAdvertising();
if (!g_pAdvertising) {
Serial.println("ERROR: Extended Advertising 초기화 실패");
Expand All @@ -74,5 +83,89 @@ bool ble_init_and_start() {
}

Serial.println("BLE 고정 비콘 광고 시작됨");

// GATT 연결용 connectable 광고 (디바이스 이름으로 검색 가능)
NimBLEExtAdvertisement connAdv(BLE_HCI_LE_PHY_1M, BLE_HCI_LE_PHY_1M);
connAdv.setLegacyAdvertising(true);
connAdv.setConnectable(true);
connAdv.setScannable(true);
connAdv.setName(g_device_name);
connAdv.setMinInterval(ADV_INTERVAL_MIN);
connAdv.setMaxInterval(ADV_INTERVAL_MAX);

if (!g_pAdvertising->setInstanceData(ADV_INSTANCE_GATT, connAdv)) {
Serial.println("ERROR: GATT 연결용 광고 설정 실패");
return false;
}

if (!g_pAdvertising->start(ADV_INSTANCE_GATT, 0, 0)) {
Serial.println("ERROR: GATT 연결용 광고 시작 실패");
return false;
}

Serial.println("GATT 연결용 광고 시작됨");
return true;
}

// ── 출석 비콘 (세션) ────────────────────────────

// 출석 비콘 광고만 종료. 고정 비콘과 GATT 광고는 독립 인스턴스이므로 영향 없음.
void stop_session_beacon() {
if (g_state != STATE_ACTIVE) return;

g_pAdvertising->stop(ADV_INSTANCE_SESSION);
g_state = STATE_IDLE;
}

// FreeRTOS 타이머 콜백: 플래그만 세팅하고 loop에서 처리
void session_timer_callback(TimerHandle_t xTimer) {
g_session_expired = true;
}

// loop()에서 호출하여 이벤트를 메인 태스크에서 안전하게 출력
void ble_check_events() {
if (g_session_started) {
g_session_started = false;
Serial.print("BLE: 출석 비콘 광고 시작 (");
Serial.print(g_session_duration);
Serial.println("초 후 자동 종료)");
}
if (g_session_expired) {
g_session_expired = false;
stop_session_beacon();
Serial.println("BLE: 출석 비콘 광고 종료, IDLE 복귀");
}
}

// 출석 비콘 광고 시작 (GATT 콜백에서 호출되므로 Serial 출력 없음)
void start_session_beacon(const uint8_t* session_uuid, uint16_t duration_sec) {
// 이미 ACTIVE면 기존 세션 중단
if (g_state == STATE_ACTIVE) {
g_pAdvertising->stop(ADV_INSTANCE_SESSION);
if (g_sessionTimer) {
xTimerStop(g_sessionTimer, 0);
}
}

// 출석 비콘 광고 세트 구성
NimBLEExtAdvertisement sessionAdv(BLE_HCI_LE_PHY_1M, BLE_HCI_LE_PHY_1M);
build_ibeacon_adv(sessionAdv, session_uuid);

if (!g_pAdvertising->setInstanceData(ADV_INSTANCE_SESSION, sessionAdv)) return;
if (!g_pAdvertising->start(ADV_INSTANCE_SESSION, 0, 0)) return;

g_state = STATE_ACTIVE;
g_session_duration = duration_sec;
g_session_started = true; // loop에서 로그 출력

// FreeRTOS 타이머로 자동 종료 설정
if (g_sessionTimer == nullptr) {
g_sessionTimer = xTimerCreate(
"session", pdMS_TO_TICKS(duration_sec * 1000),
pdFALSE, nullptr, session_timer_callback
);
} else {
xTimerChangePeriod(g_sessionTimer, pdMS_TO_TICKS(duration_sec * 1000), 0);
}
xTimerStart(g_sessionTimer, 0);
}
97 changes: 97 additions & 0 deletions src/gatt_server.ino
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
// ── GATT 서버 ───────────────────────────────────
// 관리자 앱이 BLE로 출석 시작 명령을 보내는 인터페이스
// Write Characteristic에 34바이트 페이로드를 쓰면 출석 비콘이 시작됨

#include <Arduino.h>
#include <NimBLEDevice.h>
#include "config.h"

extern uint8_t g_psk[];
extern uint8_t g_service_uuid[];
extern char g_device_name[];
extern beacon_state_t g_state;
extern NimBLEExtAdvertising* g_pAdvertising;

// ble_beacon.ino의 함수 전방 선언
void start_session_beacon(const uint8_t* session_uuid, uint16_t duration_sec);
void stop_session_beacon();

// GATT Write 콜백: 관리자 앱이 출석 시작 명령을 보낼 때 호출됨
class AttendanceCallbacks : public NimBLECharacteristicCallbacks {
void onWrite(NimBLECharacteristic* pCharacteristic, NimBLEConnInfo& connInfo) override {
std::string value = pCharacteristic->getValue();

if (!validate_payload(value.length())) return;

const uint8_t* payload = (const uint8_t*)value.data();
const uint8_t* received_psk;
const uint8_t* session_uuid;
uint16_t duration;

parse_payload(payload, &received_psk, &session_uuid, &duration);

if (!verify_psk(received_psk, g_psk)) return;
if (duration == 0 || duration > 600) return;

// 시리얼 출력 없이 바로 실행 (출력은 loop에서 처리)
start_session_beacon(session_uuid, duration);
}
};

static AttendanceCallbacks attendanceCallbacks;

// 서버 연결/해제 콜백: 클라이언트가 연결하면 connectable 광고가 자동 중단됨
// 연결 해제 시 GATT 광고를 재시작해야 다음 연결이 가능
class ServerCallbacks : public NimBLEServerCallbacks {
void onDisconnect(NimBLEServer* pServer, NimBLEConnInfo& connInfo, int reason) override {
// 연결 해제 시 GATT connectable 광고 재시작
NimBLEExtAdvertisement connAdv(BLE_HCI_LE_PHY_1M, BLE_HCI_LE_PHY_1M);
connAdv.setLegacyAdvertising(true);
connAdv.setConnectable(true);
connAdv.setScannable(true);
connAdv.setName(g_device_name);
connAdv.setMinInterval(ADV_INTERVAL_MIN);
connAdv.setMaxInterval(ADV_INTERVAL_MAX);
g_pAdvertising->setInstanceData(ADV_INSTANCE_GATT, connAdv);
g_pAdvertising->start(ADV_INSTANCE_GATT, 0, 0);
}
};

static ServerCallbacks serverCallbacks;

// GATT 서버 초기화: 서비스와 Write Characteristic 생성
bool gatt_init() {
// 서비스 UUID를 NVS에서 로드한 바이트 배열로 구성
char svc_uuid_str[37];
snprintf(svc_uuid_str, sizeof(svc_uuid_str),
"%02x%02x%02x%02x-%02x%02x-%02x%02x-%02x%02x-%02x%02x%02x%02x%02x%02x",
g_service_uuid[0], g_service_uuid[1], g_service_uuid[2], g_service_uuid[3],
g_service_uuid[4], g_service_uuid[5], g_service_uuid[6], g_service_uuid[7],
g_service_uuid[8], g_service_uuid[9], g_service_uuid[10], g_service_uuid[11],
g_service_uuid[12], g_service_uuid[13], g_service_uuid[14], g_service_uuid[15]);

NimBLEServer* pServer = NimBLEDevice::createServer();
if (!pServer) {
Serial.println("ERROR: GATT 서버 생성 실패");
return false;
}

pServer->setCallbacks(&serverCallbacks);

NimBLEService* pService = pServer->createService(svc_uuid_str);
if (!pService) {
Serial.println("ERROR: GATT 서비스 생성 실패");
return false;
}

NimBLECharacteristic* pCharacteristic = pService->createCharacteristic(
GATT_CHAR_UUID,
NIMBLE_PROPERTY::WRITE
);
pCharacteristic->setCallbacks(&attendanceCallbacks);

pServer->start();

Serial.println("GATT 서버 시작됨");
return true;
}
2 changes: 2 additions & 0 deletions src/main.ino
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ beacon_state_t g_state = STATE_IDLE;
void nvs_load_config();
void check_serial();
bool ble_init_and_start();
void ble_check_events();

// ── Arduino 진입점 ──────────────────────────────

Expand Down Expand Up @@ -56,4 +57,5 @@ void setup() {

void loop() {
check_serial();
ble_check_events();
}