[IoT ๐ /Homebridge] - Homebridge ์ ๊ธฐ๋งคํธ ํ๋ฌ๊ทธ์ธ ์ ์, ๋ธ๋ฃจํฌ์ค ํจํท ๋ถ์ - 1
[IoT ๐ /Homebridge] - [JS] Homebridge ์ ๊ธฐ๋งคํธ ํ๋ฌ๊ทธ์ธ ์ ์, ํ๋ฌ๊ทธ์ธ ๊ตฌํ - 2
์ง๋๋ฒ์ ๋ง๋ ์ ๊ธฐ๋งคํธ ํ๋ฌ๊ทธ์ธ์ ๊ฐ์ ํด๋ณด๋ ค๊ณ ํ๋ค.
์ ๋ง ๋ง๋ค๋ฉด์ ๊ฝค๋ ๊ณ ์ํ๋ ์์ ์ด์๋๋ฐ,
๋ช ๋ฌ๊ฐ ํจํท ๋ถ์์ ๋ ํด๋ณด๊ณ ์ฐ๊ตฌํด๋ณธ ๋์
์ฐ๊ฒฐ ์์ ์ฑ๋ ๋์ด๊ณ , ๊ธฐ๊ธฐ๋ฅผ ์๋์ผ๋ก ์กฐ์ํ ๋ ์ ์ธ์๋ฆฌ์ ๋๊ธฐํ๋ฅผ ํ ์ ์๊ฒ ๋๋ค.
์๋ง ์ ๋ฐ์ ์ธ ๊ตฌ์กฐ๋ฅผ ์น ๊ฐ์ ์๋ ์์ ์ด ํ์ํ๋ฏ๋ก,
๊ฑฐ์ ์๋ก ์ ์ํ๋๊ฒ๊ณผ ๋ค๋ฅด์ง ์์ ๊ฒ ๊ฐ๋ค.
ํ๋ฌ๊ทธ์ธ ์์
ํ๋ฌ๊ทธ์ธ ์ฝ๋์ ๋๋ถ๋ถ์ ๊ฐ์ ์์ ์ด์ ์ค์ ํ๋๊ฐ, ์ฐ๊ฒฐ ์์ ์ฑ์ด๋ค.
์ ๊ธฐ ๋งคํธ ์ปจํธ๋กค๋ฌ์ node-ble๋ฅผ ์ด์ฉํ ๋ฆฌ๋ ์ค ์๋ฒ๊ฐ
๋ฏธ์ธํ ๋๋ ์ด๋ฅผ ๋์ ์ค๊ฐ ์ค๊ฐ์ ๋ฃ์ด์ค์ผ ์์ ์ ์ ์ง๊ฐ ๋๋ฏ๋ก
๊ทธ ๊ฐ์ ํ๋ํ๋ ์ฐพ๋๋ผ ์์ ์ด ๊ฝค๋ ์ค๋๊ฑธ๋ ธ๋ค.
๋จผ์ , ๋๋ฐ์ด์ค๋ฅผ ํ์ํ๋ ์ค์บ ํจ์๋ฅผ ๊ตฌํํ๋ค.
๋งคํธ์ MAC ์ฃผ์๋ฅผ ์ฐพ์ ๋ ๊น์ง ๋ฐ๋ณตํ๋ค.
async startScanningLoop() {
while (true) {
if (!this.isConnected) {
try {
try { await this.adapter.stopDiscovery(); } catch(e) {}
await this.adapter.startDiscovery();
await sleep(CONFIG.SCAN_DURATION_MS);
await this.adapter.stopDiscovery();
const devices = await this.adapter.devices();
for (const addr of devices) {
if (addr.toUpperCase().replace(/:/g, '') === this.macAddress.toUpperCase()) {
this.device = await this.adapter.getDevice(addr);
await this.connectDevice();
break;
}
}
} catch (e) {}
}
await sleep(CONFIG.RECONNECT_DELAY_MS);
}
}
๋ค์์ผ๋ก ๊ธฐ๊ธฐ์ ๋ฌผ๋ฆฌ์ ์ฐ๊ฒฐ์ ๋ด๋นํ๋ ๋ฉ์๋์ด๋ค.
ํนํ, le-connection-abort-by-local๊ณผ ๊ฐ์ด ๋ธ๋ฃจํฌ์ค ์คํ ์ค๋ฅ ๋ฐ์์
๊ฐ์ ๋ก ์ด๋ํฐ ํ๋์จ์ด๋ฅผ ๋ฆฌ์ ์ํค๋ ๋ช ๋ น์ด๋ฅผ ์๋ฒ์ ์ ์กํ๊ฒ ๊ตฌํํ๋ค.
async connectDevice() {
try {
await this.withTimeout(this.device.connect(), CONFIG.CONNECT_TIMEOUT_MS, "Device Connect");
this.isConnected = true;
this.device.removeAllListeners('disconnect'); // ์ฐ๊ฒฐ ์ ์ค ๊ฐ์ง
this.device.once('disconnect', () => this.cleanup());
await this.discoverCharacteristics();
} catch (e) {
if (e.message.includes('le-connection-abort-by-local')) {
this.abortCount++;
if (this.abortCount >= 3) { // 3ํ ์ค๋ฅ์ ๋ฆฌ์
exec('sudo hciconfig hci0 down && sleep 1 && sudo hciconfig hci0 up');
this.abortCount = 0;
}
}
this.cleanup();
}
}
์ด์ ์ฐ๊ฒฐ ์ฑ๊ณต ํ ์ฒ๋ฆฌ๋ฅผ ํ๋ ๋ฉ์๋์ด๋ค.
์ด ๋ถ๋ถ์ด ์ ์ผ ๊ณต์ ๋ง์ด ๋ค์ธ ๋ถ๋ถ์ธ๋ฐ,
GATT ์์ ํ ๋๊ธฐ, ์ด๊ธฐ ์ธ์ฆ ํจํท, ๋๋ฐ์ด์ค ์ํ ๋ฆฌ์ค๋ ๋ฑ๋ก, ์ํ ํ์ธ ํจํท ๋ฑ
์ ๊ธฐ ๋งคํธ์ ์์ ์ ์ด๊ณ ์ง์์ ์ธ ์ฐ๊ฒฐ์ ์ํด, ํ์ํ ์ฒ๋ฆฌ๋ค์ ๋ชจ๋ ๊ตฌํํ๊ณ
์ค๊ฐ์ค๊ฐ ์ง์ฐ์๊ฐ์ ๋์ด, ์ฐ๊ฒฐ์ด ์์ฃผ ๋๊ธฐ๋ ํ์์ ๋ณด์ํ๋ค.
async discoverCharacteristics() {
try {
const gatt = await this.withTimeout(this.device.gatt(), CONFIG.GATT_TIMEOUT_MS, "GATT Server");
await sleep(CONFIG.GATT_WAIT_MS);
const service = await this.withTimeout(gatt.getPrimaryService(this.serviceUuid), CONFIG.GATT_TIMEOUT_MS, "Primary Service");
this.setChar = await service.getCharacteristic(this.charSetUuid)
this.tempChar = await service.getCharacteristic(this.charTempUuid);
this.timeChar = await service.getCharacteristic(this.charTimeUuid);
await this.writeRaw(this.setChar, Buffer.from(this.initPacketHex, 'hex'));
await sleep(CONFIG.AUTH_WAIT_MS); // ์ธ์ฆํจํท ์ ์ก
await this.tempChar.startNotifications();
this.tempChar.on('valuechanged', (data) => this.handleUpdate(data, 'temp'));
await sleep(CONFIG.NOTIFY_STEP_MS);
await this.timeChar.startNotifications();
this.timeChar.on('valuechanged', (data) => this.handleUpdate(data, 'timer'));
await sleep(CONFIG.NOTIFY_READY_MS); // ์ํ ๋ฆฌ์ค๋ ๋ฑ๋ก
await this.writeRaw(this.tempChar, this.createControlPacket(0x12));
await sleep(CONFIG.POST_INIT_WAIT_MS); // ์ํํ์ธ ํจํท ์ ์ก
this.startPingLoop();
} catch (e) {
this.isConnected = false;
if (this.device) await this.device.disconnect().catch(() => {});
}
}
๊ทธ๋ฆฌ๊ณ ์ค์ ๋ฐ์ดํฐ๋ฅผ ์ ์กํ๋ ๋ถ๋ถ,
๋ธ๋ฃจํฌ์ค ์ ์ก์ ์์ ์ฑ์ ๋์ด๊ธฐ ์ํด, ์ฌ์๋ ๋ก์ง๊ณผ ์ง์ฐ์๊ฐ์ ๋ฃ์๋ค.
async writeRaw(characteristic, packet) {
if (!this.isConnected || !characteristic) return false;
for (let i = 0; i < CONFIG.RETRY_COUNT; i++) {
try {
await characteristic.writeValue(packet, { type: 'command' });
return true;
} catch (e) {
await sleep(CONFIG.WRITE_DELAY_MS);
}
}
return false;
}
๋ง์ง๋ง์ผ๋ก ๊ธฐ๊ธฐ์์ ์ธ์ ์ด ๋๊ธฐ์ง ์๋๋ก,
ํน์ ์๊ฐ๋ง๋ค ์ํ ํ์ธ ํจํท์ ์ ์กํ์ฌ ์ฐ๊ฒฐ์ด ์ ์ง๋๊ฒ ํ๋ค.
startPingLoop() {
this.stopPingLoop();
this.pingInterval = setInterval(async () => {
if (!this.isConnected || !this.tempChar) {
this.stopPingLoop();
return;
}
try {
await this.writeRaw(this.tempChar, this.createControlPacket(0x12));
} catch (e) {}
}, CONFIG.PING_INTERVAL_MS);
}
์ฐ๊ฒฐ ์์ ์ฑ์ ์ด๋ ๊ฒ ๋
์ด์ ์๋์ผ๋ก ๊ธฐ๊ธฐ๋ฅผ ์กฐ์ ์, ๊ธฐ๊ธฐ์ ์ํ๋ณํ๋ฅผ ๊ฐ์งํ๋ ๋ก์ง์ ๊ตฌํํด๋ณด์
๋จผ์ ๋ฐ์ด๋๋ฆฌ ๋ฐ์ดํฐ๋ฅผ ์ค์ ์ซ์๋ก ๋ณํ ํ,
์จ๋ or ํ์ด๋จธ์ ์ ์ธ์๋ฆฌ๋ฅผ ์ ๋ฐ์ดํธ ์ํค๋ ๋ก์ง์ ๊ตฌํํ๋ค.
์๋ชป๋ ํจํท์ ๊ฑธ๋ฌ์ง๊ณ , ์ค๋ณต์ผ๋ก ์ ๋ฐ์ดํธ๋ฅผ ๋ฐฉ์งํ์ฌ ๋ถํ์ํ ์์คํ ๋ถํ๋ฅผ ์ค์๋ค.
handleUpdate(data, type) {
const val = this.parsePacket(data, type);
if (val === null) return;
if (type === 'temp') {
const tempValue = CONFIG.LEVEL_TEMP_MAP[val];
if (tempValue !== undefined) {
if (this.currentState.currentTemp !== tempValue || this.currentState.currentHeatingCoolingState !== (val > 0 ? 1 : 0)) {
this.currentState.currentTemp = tempValue;
this.currentState.targetTemp = tempValue;
this.currentState.currentHeatingCoolingState = (val > 0) ? 1 : 0;
if (val > 0) this.currentState.lastHeatTemp = tempValue;
this.thermostatService.updateCharacteristic(this.Characteristic.CurrentTemperature, tempValue);
this.thermostatService.updateCharacteristic(this.Characteristic.TargetTemperature, tempValue);
this.thermostatService.updateCharacteristic(this.Characteristic.CurrentHeatingCoolingState, this.currentState.currentHeatingCoolingState);
this.thermostatService.updateCharacteristic(this.Characteristic.TargetHeatingCoolingState, this.currentState.currentHeatingCoolingState);
}
}
} else if (type === 'timer') {
if (this.currentState.timerHours !== val) {
this.currentState.timerHours = val;
this.currentState.timerOn = (val > 0);
this.timerService.updateCharacteristic(this.Characteristic.On, this.currentState.timerOn);
this.timerService.updateCharacteristic(this.Characteristic.Brightness, val * CONFIG.BRIGHTNESS_PER_HOUR);
}
}
}
๋ง์ง๋ง์ผ๋ก, ๋ฉ์๋ ์ค๊ฐ์ค๊ฐ ์์ ํํ๋ก ์ ์ธํ
์ง์ฐ์๊ฐ์ด๋ ์ธํฐ๋ฒ ์๊ฐ, ํ์์์ ์๊ฐ ๋ฑ๋ฑ์ ์๋จ์ ํ๋ฒ์ ์ ์ธํ์ฌ
์์น ๋ณ๊ฒฝ์ ์ฉ์ดํ๊ฒ ํ ์ ์๋๋ก ๊ฐ์ ํ๋ค.
๊ทธ๋ฆฌ๊ณ , ๊ธฐ์กด ํ๋ฌ๊ทธ์ธ์ ํ์ผ ์จ๋๊ฐ ์ง๊ด์ ์ด์ง ์์์
15~50๋(5๋์ฉ) > 36๋~42๋๋ก ๋ณ๊ฒฝํ๋ค.
const CONFIG = {
// Timeout, Interval
RECONNECT_DELAY_MS: 20000, // ์ฐ๊ฒฐ ์ ์ค ์ ์ฌ์์๊น์ง ๋๊ธฐ (๊ธฐ๊ธฐ ์ธ์
์ ๋ฆฌ ์๊ฐ)
GATT_TIMEOUT_MS: 10000, // GATT ์๋ฒ ๋ฐ ์๋น์ค ํ์ ํ์์์
PING_INTERVAL_MS: 30000, // Keep-alive ํ ์ฃผ๊ธฐ
...
// Wait Time
GATT_WAIT_MS: 3000, // ์ฐ๊ฒฐ ์ฑ๊ณต ํ GATT ํ์ ์ ๋๊ธฐ (๊ธฐ๊ธฐ ์์ ํ)
AUTH_WAIT_MS: 1000, // ์ธ์ฆ ํจํท ์ ์ก ํ ๋๊ธฐ
...
LEVEL_TEMP_MAP: { 0: 0, 1: 36, 2: 37, 3: 38, 4: 39, 5: 40, 6: 41, 7: 42 },
DEFAULT_HEAT_TEMP: 38,
BRIGHTNESS_PER_HOUR: 100 / 15
};
๊ทธ ์ธ์ ์ ์ด ํจํท ์์ฑ ๋ฐ ์ ์ก์ด๋ ์ ์ธ์๋ฆฌ ์ ์ธ ๋ฑ์
์ด์ ๊ฒ์๊ธ์์ ๊ตฌํํ๋ ๋ก์ง๊ณผ ํฌ๊ฒ ๋ค๋ฅด์ง ์์์ ์๋ตํ๋๋ก ํ๊ฒ ๋ค.
ํ ์คํธ
๊ธฐ์กด์๋ ์ฐ๊ฒฐ๋ ๋ ๊น ๋ง๊น ํ๊ณ , ์ ์ด ์คํจ์จ์ด 70ํผ์ผํธ๊ฐ ๋์๋๋ฐ,
์๋ ์ ์ด๋ ํด๋ณด๊ณ ๋ชจ๋ ๋์์ ์์ฒญ ํด๋ ๋๊น์ด ์์๋ค.
๋ฌด์๋ณด๋ค 3์ผ์งธ ์ง์ ์ฐ๊ฒฐ ํ ์คํธ๋ฅผ ํด๋ ๋๊น์ด ์ ํ ์์๋ค.

๋ง์ง๋ง์ผ๋ก ์๋ก ๊ตฌํํ ๊ธฐ๊ธฐ ์๋์กฐ์ ํ ์คํธ๋ฅผ ํด๋ณด์
๊ธฐ๊ธฐ๋ฅผ ์๋์ผ๋ก ์กฐ์ํด๋ณด๋ฉด,
ํ๋ธ๋ฆฟ์ง ์ ์ธ์๋ฆฌ์๋ ์ ์์ ์ผ๋ก ๋๊ธฐํ ๋๋ ๋ชจ์ต์ ๋ณผ ์ ์๋ค.
์ด๋ ๊ฒ, ๋ถ์์ ํ๋ ํ๋ฌ๊ทธ์ธ์ ์์ฑํ๊ฒ ๋์๋ค.
๋ช ๋ฌ๊ฐ ๋ธ๋ฃจํฌ์ค์ ๋ํด ์ ์ ํ ๋ค์ง๋ฉฐ ๊ณต๋ถํด๋ณด๊ณ ,
ํจํท๋ ๋ถ์ํ๋ฉด์ ๊ณ ์์ข ํ์๋๋ฐ, ์์ฑํ๋๊น ๋๋ฌด ๋ฟ๋ฏํ๋ค.
๋ค์์๋ ๋ ๋์ด๋ ์๋ ์์ ๋ ๋์ ํด ๋ด์ผ๊ฒ ๋ค.
์๋ณธ ๊ฒ์๊ธ
[JS] Homebridge ์ ๊ธฐ๋งคํธ ํ๋ฌ๊ทธ์ธ ๊ฐ์
์ด๋ฒ์๋ ์ ๋ฒ์ ๋ง๋ ์ ๊ธฐ๋งคํธ ํ๋ฌ๊ทธ์ธ์ ๊ฐ์ ํด๋ณด๋ ค๊ณ ํ๋ค. https://blog.naver.com/101artspace/22405...
blog.naver.com