BLE를 비롯한 네트워크 분야는 전산학이 탄생한 이후 70여년 동안 끊임없이 발전하며 고인물들의 학문이 돼버렸다. 그러므로 우리는 이번 글에서 BLE에 대해서 딱 알아야 할 내용만 간단하게 알아보고자 한다. 시험보려고 공부하는 게 아니지않는가? 개발하려고 공부하는거지!
BLE 통신방법은 'Subscription-Notification' 방식으로 이뤄진다. 유튜브를 생각하면 아주 쉽게 이해할 수 있다. 우리는 보통 유튜브를 아래와 같이 이용한다.
이해가 좀 됐다면, 아래 정리글로 마무리하자.
지금까지 짧았지만, BLE 통신방법과 구조에 대해서 알아봤다. 우리의 목표는 개발을 하기 위한 최소 지식 습득이므로 기준치는 어느정도 충족시켰다고 판단한다.
이번 챕터에서는 실제로 사용되는 BLE 관련 라이브러리를 두 개 분석할 것이다.
두 라이브러리는 사뭇 분위기가 다르다.
그럼 이제 하나씩 살펴보자.
#include <ArduinoBLE.h>
void setup() {
Serial.begin(9600);
while (!Serial);
// begin initialization
if (!BLE.begin()) {
Serial.println("starting BLE failed!");
while (1);
}
Serial.println("BLE Central scan");
// start scanning for peripheral
BLE.scan();
}
void loop() {
// check if a peripheral has been discovered
BLEDevice peripheral = BLE.available();
if (peripheral) {
// discovered a peripheral
Serial.println("Discovered a peripheral");
Serial.println("-----------------------");
// print address
Serial.print("Address: ");
Serial.println(peripheral.address());
// print the local name, if present
if (peripheral.hasLocalName()) {
Serial.print("Local Name: ");
Serial.println(peripheral.localName());
}
// print the advertised service UUIDs, if present
if (peripheral.hasAdvertisedServiceUuid()) {
Serial.print("Service UUIDs: ");
for (int i = 0; i < peripheral.advertisedServiceUuidCount(); i++) {
Serial.print(peripheral.advertisedServiceUuid(i));
Serial.print(" ");
}
Serial.println();
}
}
}
setup()
문에서는 시리얼모니터 출력을 위한 Serial.begin()
과 BLE 통신을 위한 BLE.begin()
문이 있다. 최대한 기존 arduino API와 형식을 비슷하게 맞춰주기 위한 노력이 보인다.BLE.scan()
이 호출되면 scan을 시작한다.BLE.available()
는 인근에 advertising 중인 peripheral device를 발견했는지 확인해준다.loop()
문에 정의된 print
문들에 따라 여러 정보를 출력하게 된다.이 예제에서는 주변 기기를 발견하더라도 따로 연결하지는 않고 그저 스캔만 주기적으로 계속 수행한다. 스캔을 할 수 있다, 검출한 기기의 MAC 주소와 이름 그리고 UUID를 알아낼 수 있구나 정도만 알면 된다. UUID에 대해서는 후술한다.
#include <ArduinoBLE.h>
const int ledPin = LED_BUILTIN;
BLEService ledService("19B10000-E8F2-537E-4F6C-D104768A1214");
BLEByteCharacteristic switchCharacteristic("19B10001-E8F2-537E-4F6C-D104768A1214", BLERead | BLEWrite);
void setup() {
Serial.begin(9600);
while (!Serial);
pinMode(ledPin, OUTPUT); // use the LED pin as an output
// begin initialization
if (!BLE.begin()) {
Serial.println("starting BLE failed!");
while (1);
}
// set the local name peripheral advertises
BLE.setLocalName("LEDCallback");
// set the UUID for the service this peripheral advertises
BLE.setAdvertisedService(ledService);
// add the characteristic to the service
ledService.addCharacteristic(switchCharacteristic);
// add service
BLE.addService(ledService);
// assign event handlers for connected, disconnected to peripheral
BLE.setEventHandler(BLEConnected, blePeripheralConnectHandler);
BLE.setEventHandler(BLEDisconnected, blePeripheralDisconnectHandler);
// assign event handlers for characteristic
switchCharacteristic.setEventHandler(BLEWritten, switchCharacteristicWritten);
// set an initial value for the characteristic
switchCharacteristic.setValue(0);
// start advertising
BLE.advertise();
Serial.println(("Bluetooth device active, waiting for connections..."));
}
void loop() {
// poll for BLE events
BLE.poll();
}
void blePeripheralConnectHandler(BLEDevice central) {
// central connected event handler
Serial.print("Connected event, central: ");
Serial.println(central.address());
}
void blePeripheralDisconnectHandler(BLEDevice central) {
// central disconnected event handler
Serial.print("Disconnected event, central: ");
Serial.println(central.address());
}
void switchCharacteristicWritten(BLEDevice central, BLECharacteristic characteristic) {
// central wrote new value to characteristic, update LED
Serial.print("Characteristic event, written: ");
if (switchCharacteristic.value()) {
Serial.println("LED on");
digitalWrite(ledPin, HIGH);
} else {
Serial.println("LED off");
digitalWrite(ledPin, LOW);
}
}
한 줄 한 줄 살펴보자
BLEService ledService("19B10000-E8F2-537E-4F6C-D104768A1214");
BLEByteCharacteristic switchCharacteristic("19B10001-E8F2-537E-4F6C-D104768A1214", BLERead | BLEWrite);
Service는 BLEService
클래스의 인스턴스로 정의한다.
Characteristic은 BLE~~Characteristic
클래스의 인스턴스로 정의한다.
Characteristic은 데이터가 저장되는 공간이므로 라이브러리에서는 다양한 데이터 타입을 지원한다.
Service와 Characteristic을 선언할 때 사용되는 저 의미불명의 string은 UUID(universally unique identifier)라고 부르는 unique한 ID다.
Characteristic에는 BLERead | BLEWrite
같은 여러 속성(Attributes)을 부여할 수 있다. 위 예제의 switchCharacteristic
라는 속성은 읽거나 쓸 수 있다.
// set the local name peripheral advertises
BLE.setLocalName("LEDCallback");
// set the UUID for the service this peripheral advertises
BLE.setAdvertisedService(ledService);
// add the characteristic to the service
ledService.addCharacteristic(switchCharacteristic);
// add service
BLE.addService(ledService);
setLocalName
은 BLE device의 이름을 설정하는 부분이다. 우리가 핸드폰에서 블루투스를 키면 이어폰이나 스피커나 애플팬슬의 이름을 확인할 수 있다. 이 이름이 바로 localName
이다.setAdvertisedService()
로 service를 광고할 service 목록에 추가한다.addCharacteristic()
으로 사전에 정의한 characteristic을 service에 추가한다.// assign event handlers for connected, disconnected to peripheral
BLE.setEventHandler(BLEConnected, blePeripheralConnectHandler);
BLE.setEventHandler(BLEDisconnected, blePeripheralDisconnectHandler);
// assign event handlers for characteristic
switchCharacteristic.setEventHandler(BLEWritten, switchCharacteristicWritten);
// set an initial value for the characteristic
switchCharacteristic.setValue(0);
// start advertising
BLE.advertise();
blePeripheralConnectHandler
라는 함수가 호출되도록 만든다.blePeripheralDisconnectHandler
라는 함수가 호출되도록 만든다.switchCharacteristicWritten
라는 함수가 호출되도록 만든다.BLEDevice central
를 가진다.#include <ArduinoBLE.h>
const int buttonPin = 2;
int oldButtonState = LOW;
void setup() {
Serial.begin(9600);
while (!Serial);
// configure the button pin as input
pinMode(buttonPin, INPUT);
// initialize the BLE hardware
BLE.begin();
Serial.println("BLE Central - LED control");
// start scanning for peripherals
BLE.scanForUuid("19b10000-e8f2-537e-4f6c-d104768a1214");
}
void loop() {
// check if a peripheral has been discovered
BLEDevice peripheral = BLE.available();
if (peripheral) {
// discovered a peripheral, print out address, local name, and advertised service
Serial.print("Found: ");
Serial.print(peripheral.address());
Serial.print(peripheral.localName());
Serial.println(peripheral.advertisedServiceUuid());
if (peripheral.localName() != "LED") return;
// stop scanning
BLE.stopScan();
controlLED(peripheral);
// peripheral disconnected, start scanning again
BLE.scanForUuid("19b10000-e8f2-537e-4f6c-d104768a1214");
}
}
void controlLED(BLEDevice peripheral) {
// connect to the peripheral
Serial.println("Connecting ...");
if (peripheral.connect()) {
Serial.println("Connected");
} else {
Serial.println("Failed to connect!");
return;
}
// retrieve the LED characteristic
BLECharacteristic ledCharacteristic = peripheral.characteristic("19b10001-e8f2-537e-4f6c-d104768a1214");
if (!ledCharacteristic) {
Serial.println("Peripheral does not have LED characteristic!");
peripheral.disconnect();
return;
} else if (!ledCharacteristic.canWrite()) {
Serial.println("Peripheral does not have a writable LED characteristic!");
peripheral.disconnect();
return;
}
while (peripheral.connected()) {
// read the button pin
int buttonState = digitalRead(buttonPin);
if (oldButtonState != buttonState) {
// button changed
oldButtonState = buttonState;
if (buttonState) {
Serial.println("button pressed");
ledCharacteristic.writeValue((byte)0x01);
} else {
Serial.println("button released");
ledCharacteristic.writeValue((byte)0x00);
}
}
}
Serial.println("Peripheral disconnected");
}
BLE.scanForUuid("19b10000-e8f2-537e-4f6c-d104768a1214");
을 통해 client는 해당 UUID를 service로 advertising하고 있는 BLE device를 찾게된다. 상대의 UUID를 알고있다면 이런 식으로 연결할 수도 있다. 만일 알지 못한다면, 3.1절의 코드를 참조하면 된다.BLE.stopScan()
으로 scan을 멈춘다.controlLED
함수를 계속 돌게 된다.BLECharacteristic ledCharacteristic = peripheral.characteristic("19b10001-e8f2-537e-4f6c-d104768a1214");
문장을 통해 server의 characteristic을 받아온다. 그리고 적당히 예외처리 해준 뒤에 데이터를 읽는다. (이 코드에서는 따로 정보를 읽지않고, 바로 writeValue()
로 데이터를 써준다.)uint8_t
이다. 정수나 실수를 그대로 넘겨줄 수 없기 때문에 받는 쪽에서는 적절한 캐스팅을 해줘야한다.설명을 편하게 하기 위해 3.2.1절은 server 예제와 notify 예제를, 3.2.2절은 client와 scan 예제를 적당히 합쳐 편집했습니다.
#include <BLEDevice.h>
#include <BLEServer.h>
#include <BLEUtils.h>
#include <BLE2902.h>
uint32_t value = 0;
bool deviceConnected = false;
bool oldDeviceConnected = false;
BLEServer* pServer = NULL;
BLECharacteristic* pCharacteristic = NULL;
#define SERVICE_UUID "4fafc201-1fb5-459e-8fcc-c5c9c331914b"
#define CHARACTERISTIC_UUID "beb5483e-36e1-4688-b7f5-ea07361b26a8"
class MyServerCallbacks: public BLEServerCallbacks {
void onConnect(BLEServer* pServer) { deviceConnected = true; };
void onDisconnect(BLEServer* pServer) { deviceConnected = false; }
};
void setup() {
Serial.begin(115200);
// Create the BLE Device
BLEDevice::init("ESP32");
// Create the BLE Server
pServer = BLEDevice::createServer();
pServer->setCallbacks(new MyServerCallbacks());
// Create the BLE Service
BLEService *pService = pServer->createService(SERVICE_UUID);
// Create a BLE Characteristic
pCharacteristic = pService->createCharacteristic(
CHARACTERISTIC_UUID,
BLECharacteristic::PROPERTY_READ |
BLECharacteristic::PROPERTY_WRITE |
BLECharacteristic::PROPERTY_NOTIFY |
BLECharacteristic::PROPERTY_INDICATE
);
// Create a BLE Descriptor
pCharacteristic->setValue("Hello World says Neil");
pCharacteristic->addDescriptor(new BLE2902());
// Start the service
pService->start();
// Start advertising
BLEAdvertising *pAdvertising = BLEDevice::getAdvertising();
pAdvertising->addServiceUUID(SERVICE_UUID);
pAdvertising->setScanResponse(false);
pAdvertising->setMinPreferred(0x0); // set value to 0x00 to not advertise this parameter
// // functions that help with iPhone connections issue
// pAdvertising->setScanResponse(true);
// pAdvertising->setMinPreferred(0x06);
// pAdvertising->setMinPreferred(0x12);
BLEDevice::startAdvertising();
Serial.println("Waiting a client connection to notify...");
}
void loop() {
// notify changed value
if (deviceConnected) {
pCharacteristic->setValue((uint8_t*)&value, 4);
pCharacteristic->notify();
value++;
delay(3); // bluetooth stack will go into congestion, if too many packets are sent, in 6 hours test i was able to go as low as 3ms
}
// disconnecting
if (!deviceConnected && oldDeviceConnected) {
delay(500); // give the bluetooth stack the chance to get things ready
pServer->startAdvertising(); // restart advertising
Serial.println("start advertising");
oldDeviceConnected = deviceConnected;
}
// connecting
if (deviceConnected && !oldDeviceConnected) {
// do stuff here on connecting
oldDeviceConnected = deviceConnected;
}
}
BLEDevice::init("ESP32");
pServer = BLEDevice::createServer();
pServer->setCallbacks(new MyServerCallbacks());
BLEService *pService = pServer->createService(SERVICE_UUID);
pCharacteristic = pService->createCharacteristic(
CHARACTERISTIC_UUID,
BLECharacteristic::PROPERTY_READ |
BLECharacteristic::PROPERTY_WRITE |
BLECharacteristic::PROPERTY_NOTIFY |
BLECharacteristic::PROPERTY_INDICATE
);
pCharacteristic->setValue("Hello World says Neil");
pCharacteristic->addDescriptor(new BLE2902());
pService->start();
BLEAdvertising *pAdvertising = BLEDevice::getAdvertising();
pAdvertising->addServiceUUID(SERVICE_UUID);
pAdvertising->setScanResponse(false);
pAdvertising->setMinPreferred(0x0);
BLEDevice::startAdvertising();
BLEDevice 헤더파일의 init()
을 통해 BLE device의 localName을 설정할 수 있다.
Server는 BLEServer
클래스의 인스턴스로,
Service는 BLEService
클래스의 인스턴스로,
Characteristic는 BLECharacteristic
클래스의 인스턴스로 선언할 수 있다.
Service는 Server의 createService()
로 만들 수 있고,
Characteristic은 Service의 createCharacteristic()
로 만들 수 있다.
Characteristic에는 속성을 부여할 수 있으며, 읽고, 쓰기가 가능하고, 수정할 때마다 indicate와 notify가 되도록 속성을 부여했다.
->setValue()
로 데이터를 갱신할 수 있다.
abort() was called at PC 0x40173a1f on core 1
라는 오류가 등장할 수도 있으니 setup문에서 setValue()
하는 건 주의하도록 하자.->addDescriptor(new BLE2902())
로 characteristic의 특성을 설명하는 메타데이터를 설정할 수 있다.
->start()
로 Service 설정을 마무리한다. 이제 advertising 관련 설정을 한다.
->setScanResponse()
로 연결 정확도를 향상시킬 수 있다.
->setMinPreferred()
는 advertising 할 때 선호하는 주파수 대역폭과 채널을 설정할 수 있는데, 굳이 손 댈 필요는 없다고 한다. 일부 아이폰에서는 연결 문제가 있으니 0x06으로 설정하라는 말이 있다.
BLEDevice::startAdvertising();
으로 advertising을 시작한다.
if (deviceConnected) { // notify changed value
pCharacteristic->setValue((uint8_t*)&value, 4);
pCharacteristic->notify();
value++;
delay(3);
}
if (!deviceConnected && oldDeviceConnected) { // disconnecting
delay(500); // give the bluetooth stack the chance to get things ready
pServer->startAdvertising(); // restart advertising
Serial.println("start advertising");
oldDeviceConnected = deviceConnected;
}
if (deviceConnected && !oldDeviceConnected) { // connecting
oldDeviceConnected = deviceConnected;
}
loop문에서는 현재 연결 상태에 따라 서로 다른 기능을 수행하게끔 2가지 분기가 있다.
연결이 잘 되고있다면?
deviceConnected
플래그가 true
라면, characteristic의 value를 갱신해준다. notify()
를 호출하는 순서에 주의하자. 갱신 후 알려줘야 맞지, 알려준 뒤 갱신하면 정보가 밀린다.delay(3)
은 client가 올바르게 정보를 수신할 수 있도록 여유 시간을 준다. 레퍼런스에 의하면 3ms면 적절하다고 한다.연결은 끊겼는데, 이전에 연결한 기록이 있다면?
delay(500)
으로 잠시 BLE device가 stack을 정리할 시간을 준다.->startAdvertising
으로 advertising을 재시작한다.연결은 됐는데, 이전에 연결한 기록이 없다면?
별 중요한 분기는 아니다. 그냥 첫 번째 분기에 넣어도 상관없는 문장이다.
이제 연결이 됐으니 기록을 남긴다.
#include <BLEDevice.h>
#include <BLEClient.h>
#include <BLEUtils.h>
static boolean doConnect = false;
static boolean connected = false;
static boolean doScan = false;
static BLEUUID charUUID("beb5483e-36e1-4688-b7f5-ea07361b26a8");
static BLEUUID serviceUUID("4fafc201-1fb5-459e-8fcc-c5c9c331914b");
static BLEAdvertisedDevice* server;
static BLERemoteCharacteristic* remoteCharacteristic;
class MyClientCallback : public BLEClientCallbacks {
void onConnect(BLEClient* pclient) { }
void onDisconnect(BLEClient* pclient) {
connected = false;
Serial.println("onDisconnect");
}
};
class MyAdvertisedDeviceCallbacks: public BLEAdvertisedDeviceCallbacks {
void onResult(BLEAdvertisedDevice advertisedDevice) {
Serial.print("BLE Advertised Device found: ");
Serial.println(advertisedDevice.toString().c_str());
if (advertisedDevice.haveServiceUUID() && advertisedDevice.isAdvertisingService(serviceUUID)) {
BLEDevice::getScan()->stop();
server = new BLEAdvertisedDevice(advertisedDevice);
doConnect = true;
}
doScan = true;
}
};
void notifyCallback(BLERemoteCharacteristic* characteristic, uint8_t* data, size_t len, bool isNotify) {
Serial.print("Notify callback for characteristic ");
Serial.print(characteristic->getUUID().toString().c_str());
Serial.print(" of data length ");
Serial.println(len);
Serial.print("data: ");
Serial.println((char*)data);
}
void connectToServer() {
BLEClient* client = BLEDevice::createClient();
client->setClientCallbacks(new MyClientCallback());
client->connect(server);
BLERemoteService* pRemoteService = client->getService(serviceUUID);
if (pRemoteService == nullptr) {
Serial.print("Failed to find our service UUID: ");
Serial.println(serviceUUID.toString().c_str());
client->disconnect();
return;
}
Serial.println(" - Found our service");
remoteCharacteristic = pRemoteService->getCharacteristic(charUUID);
if (remoteCharacteristic == nullptr) {
Serial.print("Failed to find our characteristic UUID: ");
Serial.println(charUUID.toString().c_str());
client->disconnect();
return;
}
Serial.println(" - Found our characteristic");
if(remoteCharacteristic->canRead()) {
std::string value = remoteCharacteristic->readValue();
Serial.print("The characteristic value was: ");
Serial.println(value.c_str());
}
if(remoteCharacteristic->canNotify())
remoteCharacteristic->registerForNotify(notifyCallback);
connected = true;
}
void setup() {
Serial.begin(115200);
BLEDevice::init("");
BLEScan* pBLEScan = BLEDevice::getScan();
pBLEScan->setAdvertisedDeviceCallbacks(new MyAdvertisedDeviceCallbacks());
// scan interval과 window 사이의 상관관계는 논문 찾아봐야 함.
pBLEScan->setInterval(1349);
pBLEScan->setWindow(449);
pBLEScan->setActiveScan(false);
pBLEScan->start(5, false);
}
void loop() {
if (doConnect == true) {
connectToServer()
doConnect = false;
}
if(!connected && doScan){
// 새로운 연결을 위해 적절한 초기화 과정 넣어주기
BLEDevice::getScan()->start(0);
}
}
Client의 코드도 구조적으로는 ArduinoBLE와 동일하다.
void setup() {
Serial.begin(115200);
BLEDevice::init("");
BLEScan* pBLEScan = BLEDevice::getScan();
pBLEScan->setAdvertisedDeviceCallbacks(new MyAdvertisedDeviceCallbacks());
// scan interval과 window 사이의 상관관계는 논문 찾아봐야 함.
pBLEScan->setInterval(1349);
pBLEScan->setWindow(449);
pBLEScan->setActiveScan(false);
pBLEScan->start(5, false);
}
BLEDevice::init()
으로 Client의 local name을 설정한다.
BLEDevice::getScan()
으로 BLEScan*
타입 scan 객체를 생성한다.
우리는 scan을 할 때 이 객체의 내부값을 수정해서 scan에 대한 환경설정을 진행한다.
Callback 함수를 지정한다. Scan으로 어떤 외부 advertising device를 찾았을 때 호출될 함수를 선언한다.
class MyAdvertisedDeviceCallbacks: public BLEAdvertisedDeviceCallbacks {
void onResult(BLEAdvertisedDevice advertisedDevice) {
Serial.print("BLE Advertised Device found: ");
Serial.println(advertisedDevice.toString().c_str());
if (advertisedDevice.haveServiceUUID() && advertisedDevice.isAdvertisingService(serviceUUID)) {
BLEDevice::getScan()->stop();
server = new BLEAdvertisedDevice(advertisedDevice);
doConnect = true;
}
doScan = true;
}
};
BLEAdvertisedDeviceCallbacks
의 파생 클래스 형태로 callback을 만든다.Serial.print()
문은 객체를 그대로 출력할 수 없다.BLEAdvertisedDevice
클래스에서는 내부적으로 오버라이딩 된 toString()
함수를 지원해서 객체 내부의 정보 (MAC 주소, local name 등)를 std::string
형으로 반환해준다.Serial. print()
문은 std::string
을 그대로 출력할 수 없다..c_str()
함수로 반드시 char array
타입으로 변형한 뒤 출력하자.BLEAdvertisedDevice
에 해당 device를 저장한다. doConnect = true
로 connect할 준비를 마친다.Scan interval과 window 사이의 상관관계에 따라 최적화된 BLE 통신이 가능하다고 하는데, 잠깐 검색해보니 따로 편리한 공식이 있는게 아니고 논문 뿐이라 논문을 찾아봐야 한다. 여기서는 예제를 그대로 따라한다.
setActiveScan(false)
은 Server 코드의 setScanResponse(false)
와 호응된다. Advertising packet을 받아도 딱히 ACK 신호를 보내지 않고, 저쪽도 받을 생각이 없는 상황이다. 저전력에 도움이 된다.
start()
로 scanning을 시작한다.
void loop() {
if (doConnect == true) {
connectToServer()
doConnect = false;
}
if(!connected && doScan){
// 새로운 연결을 위해 적절한 초기화 과정 넣어주기
BLEDevice::getScan()->start(0);
}
}
doConnect = true
가 되어 첫 if문으로 진입한다.connectToServer()
에서 연결을 수립하고, service와 characteristic을 가져온다.doConnect = false
로 해줘서 중복 연결하지 않도록 한다.여기까지 우리는 BLE의 통신과정부터 개발방법까지 모든 것을 얕게 다뤘다. 여기까지 이해했다면, BLE를 사용해서 원하는 개발을 하는 것은 전혀 무리가 아닐 것이라 생각한다.
나는 위 공부 내용을 바탕으로 NodeMCU-32S 모듈 2개를 사용해서 BLE 통신을 이용한 간단한 프로젝트를 진행했다. Arduino Nano 33 IoT를 이용해서도 진행했지만, 안타깝게도 ArduinoBLE 라이브러리는 굉장히 제한적인 보드만 지원하므로 HM-10 같은 모듈이 장착된 보드와 호환되지 않았다. 지금 글을 쓰는 현재로써는 Nano 33 IoT와 HM-10을 연결할 수는 있지만, 데이터를 주고받을 방법이 없다. 참고바라며 이만 글을 마무리한다.
BLECharacteristic = peripheral.characteristic 문장이 잘 이해가 되지 않는데 Client의 characteristic의 데이터를 Server characteristic에 저장시킨다는 의미인가요?
포스트 잘 보았습니다. esp32로 ble 스캔 예제를 보고 있습니다. (아두이노 ide)
pBLEScan->setActiveScan(true); //active scan uses more power, but get results faster
pBLEScan->setInterval(100);
pBLEScan->setWindow(99); // less or equal setInterval value
이 부분이 이해가 잘 안가는데, 액티브스캔후, 인터벌을 100밀리세컨드 넣었는데 그다음 셋윈도우에는 왜 99밀리세컨드만 넣나요? 주석을 보니, 인터벌 값보다 같거나 작다라고 했는데
nano 33 BLE 모듈과 nano 33 IoT 모듈 간의 데이터 송수신이 이루어지나요?
(예를 들어 nano 33 BLE 모듈의 A0 포트에 3.3V 전압이 인가되었을 때 nano 33 IoT에서 그 데이터를 그대로 수신할 수 있는지? 3.3V가 인가되면 1023이 출력되므로 nano 33 IoT 모듈에서도 1023이 출력되는지)