#include #include #include #include #include #include #include #include #include // === 보안/프로토콜 상수 === #define SERVICE_UUID "6e400001-b5a3-f393-e0a9-e50e24dcca9e" #define CHARACTERISTIC_RX "6e400002-b5a3-f393-e0a9-e50e24dcca9e" #define CHARACTERISTIC_TX "6e400003-b5a3-f393-e0a9-e50e24dcca9e" // [필수] 간단 토큰(WS/BLE 공통). 6자 이상, 빌드 시 변경하세요. static const char* AUTH_TOKEN = "CHANGE_ME_123456"; #define USE_TEST_MODE 0 const char* WIFI_SSID = "YOUR_WIFI_SSID"; const char* WIFI_PASS = "YOUR_WIFI_PASS"; const int motorPin = 5; const int testPin = 26; // 안전 제약 static const uint16_t VIB_MIN_MS = 50; static const uint16_t VIB_MAX_MS = 8000; static const uint16_t VIB_DEFAULT = 2000; static const uint32_t VIB_COOLDOWN_MS = 1000; // 인증/상태 BLECharacteristic* pTxCharacteristic; bool deviceConnected = false; bool alive = false; // 진동 상태 bool vibActive = false; unsigned long vibUntil = 0; unsigned long vibLastEnd = 0; // WS WebSocketsServer wsServer(81); static const size_t MAX_CLIENTS = 4; bool wsAuthed[8] = {false}; // 간단 슬롯 인증 상태(최대 8) // 안전 비교(타이밍 공격 완화) bool secureEquals(const char* a, const char* b){ if(!a || !b) return false; size_t la=strlen(a), lb=strlen(b); if(la!=lb) return false; volatile uint8_t diff=0; for(size_t i=0;i<la;i++) diff |= (uint8_t)(a[i]^b[i]); return diff==0; } // 진동 시작(제약 포함) void vibStart(uint16_t ms = VIB_DEFAULT){ unsigned long now = millis(); if(now < vibLastEnd + VIB_COOLDOWN_MS) return; // 쿨다운 if(ms < VIB_MIN_MS) ms = VIB_MIN_MS; if(ms > VIB_MAX_MS) ms = VIB_MAX_MS; digitalWrite(motorPin, HIGH); vibActive = true; vibUntil = now + ms; } void vibLoop(){ if(vibActive && millis() >= vibUntil){ digitalWrite(motorPin, LOW); vibActive = false; vibLastEnd = millis(); } } // BLE 콜백 class MyServerCallbacks : public BLEServerCallbacks{ void onConnect(BLEServer*) override { deviceConnected = true; } void onDisconnect(BLEServer*) override { deviceConnected = false; } }; // BLE 쓰기 콜백(최초 AUTH 필요) class MyCallbacks : public BLECharacteristicCallbacks{ bool authed = false; void onWrite(BLECharacteristic* ch) override { std::string v = ch->getValue(); if(v.empty()) return; // 길이 제한 if(v.size()>48) v = v.substr(0,48); if(!authed){ if(v.rfind("AUTH:",0)==0){ const char* token = v.c_str()+5; if(strlen(token)>=6 && secureEquals(token, AUTH_TOKEN)){ authed = true; pTxCharacteristic->setValue("AUTH_OK"); pTxCharacteristic->notify(); } } return; } // 인증 이후에만 명령 허용 if(v.rfind("VIBRATE",0)==0){ uint16_t dur = VIB_DEFAULT; size_t pos = v.find(':'); if(pos != std::string::npos){ int val = atoi(v.substr(pos+1).c_str()); if(val > (int)VIB_MIN_MS && val < (int)VIB_MAX_MS) dur = (uint16_t)val; } vibStart(dur); }else if(v=="PING"){ pTxCharacteristic->setValue("PONG"); pTxCharacteristic->notify(); } } }; // WS 이벤트(최초 AUTH 필요) void wsOnEvent(uint8_t num, WStype_t type, uint8_t * payload, size_t len){ if(num >= 8) return; // 가드 switch(type){ case WStype_CONNECTED:{ wsAuthed[num] = false; // 클라이언트 수 제한(선택): 초과 시 바로 닫기 size_t active=0; for(int i=0;i<8;i++) if(wsServer.remoteIP(i)) active++; if(active > MAX_CLIENTS){ wsServer.disconnect(num); } }break; case WStype_TEXT:{ if(!payload || !len) return; if(len>48) len=48; // 길이 제한 String s = String((char*)payload, len); s.trim(); if(!wsAuthed[num]){ if(s.startsWith("AUTH:")){ String token = s.substring(5); if(token.length()>=6 && secureEquals(token.c_str(), AUTH_TOKEN)){ wsAuthed[num]=true; wsServer.sendTXT(num, "AUTH_OK"); } } return; } // 인증 후 if(s.startsWith("VIBRATE")){ uint16_t dur = VIB_DEFAULT; int idx = s.indexOf(':'); if(idx > 0){ int val = s.substring(idx+1).toInt(); if(val > (int)VIB_MIN_MS && val < (int)VIB_MAX_MS) dur = (uint16_t)val; } vibStart(dur); }else if(s=="PING"){ wsServer.sendTXT(num, "PONG"); } }break; case WStype_DISCONNECTED:{ wsAuthed[num]=false; // 안전: 연결 해제 시 모터 OFF 보장 digitalWrite(motorPin, LOW); vibActive=false; }break; default: break; } } #if !USE_TEST_MODE #include "MAX30105.h" #include "heartRate.h" MAX30105 particleSensor; long lastBeat = 0; float bpm = 0.0f; float beatAvg = 0.0f; #endif void setup(){ pinMode(motorPin, OUTPUT); digitalWrite(motorPin, LOW); Serial.begin(115200); #if USE_TEST_MODE pinMode(testPin, INPUT_PULLUP); #else Wire.begin(21,22); #endif // ==== BLE 초기화 ==== BLEDevice::init("PASS_Helper"); // 필요시 난수 접미사 부여 권장 BLEServer* pServer = BLEDevice::createServer(); pServer->setCallbacks(new MyServerCallbacks()); // [옵션] 페어링/보안 수준 강화 (NimBLE 사용 시 더 유연) // BLESecurity *pSecurity = new BLESecurity(); // pSecurity->setAuthenticationMode(ESP_LE_AUTH_REQ_SC_MITM_BOND); // pSecurity->setCapability(ESP_IO_CAP_OUT); // 또는 IO_CAP_NONE // uint32_t passkey = 123456; // 배포 전 변경! // pSecurity->setStaticPIN(passkey); BLEService* pService = pServer->createService(SERVICE_UUID); pTxCharacteristic = pService->createCharacteristic( CHARACTERISTIC_TX, BLECharacteristic::PROPERTY_NOTIFY ); pTxCharacteristic->addDescriptor(new BLE2902()); BLECharacteristic* pRx = pService->createCharacteristic( CHARACTERISTIC_RX, BLECharacteristic::PROPERTY_WRITE ); pRx->setCallbacks(new MyCallbacks()); pService->start(); pServer->getAdvertising()->start(); #if !USE_TEST_MODE if(!particleSensor.begin(Wire, I2C_SPEED_STANDARD)){ alive = false; }else{ particleSensor.setup(); particleSensor.setPulseAmplitudeRed(0x2A); particleSensor.setPulseAmplitudeIR(0x2A); particleSensor.setPulseAmplitudeGreen(0x00); } #endif // ==== Wi-Fi / mDNS ==== WiFi.mode(WIFI_STA); WiFi.begin(WIFI_SSID, WIFI_PASS); unsigned long t0 = millis(); while(WiFi.status() != WL_CONNECTED && millis() - t0 < 10000){ delay(200); } if(WiFi.status() == WL_CONNECTED){ MDNS.begin("pass-helper"); } // ==== WebSocket ==== wsServer.begin(); wsServer.onEvent(wsOnEvent); } unsigned long lastSend = 0; const unsigned long sendEveryMs = 2000; void loop(){ #if USE_TEST_MODE alive = (digitalRead(testPin) == LOW); #else long ir = particleSensor.getIR(); if(checkForBeat(ir)){ long now = millis(); long d = now - lastBeat; lastBeat = now; float instBpm = 60.0f / (d / 1000.0f); if(instBpm > 20 && instBpm < 255){ if(beatAvg <= 1.0f) beatAvg = instBpm; else beatAvg = beatAvg * 0.85f + instBpm * 0.15f; bpm = instBpm; } } alive = (ir > 50000 && beatAvg > 30 && beatAvg < 180); #endif unsigned long now = millis(); if(now - lastSend >= sendEveryMs){ lastSend = now; const char* msg = alive ? "ALIVE" : "DEAD"; if(deviceConnected){ pTxCharacteristic->setValue(msg); pTxCharacteristic->notify(); } // 상태만 브로드캐스트(민감정보 없음) wsServer.broadcastTXT(msg); Serial.println(msg); } wsServer.loop(); vibLoop(); }
'+실생활에 필요한 소프트웨어 > 소프트웨어 키트' 카테고리의 다른 글
| 나음 - 나만의 프로 음료 제조 도구에 들어갈 키트 3 (0) | 2025.10.10 |
|---|---|
| 나음 - 나만의 프로 음료 제조 도구에 들어갈 키트 2 (0) | 2025.10.10 |
| 나음 - 나만의 프로 음료 제조 도구에 들어갈 키트 (2) | 2025.10.07 |