Skip to content

Commit

Permalink
simplify BLE connections and updates
Browse files Browse the repository at this point in the history
  • Loading branch information
bwp91 committed Dec 10, 2024
1 parent 6dedffb commit 14f3bd8
Show file tree
Hide file tree
Showing 5 changed files with 41 additions and 164 deletions.
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,12 @@ This project tries to adhere to [Semantic Versioning](http://semver.org/). In pr
- `MINOR` version when a new device type is added, or when a new feature is added that is backwards-compatible
- `PATCH` version when backwards-compatible bug fixes are implemented

## BETA

### Changed

- simplify BLE connections and updates

## v10.15.0 (2024-12-09)

### Added
Expand Down
171 changes: 22 additions & 149 deletions lib/connection/ble.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,7 @@ import { Buffer } from 'node:buffer'

import btClient from '@stoprocent/noble'

import {
base64ToHex,
generateCodeFromHexValues,
hexToTwoItems,
sleep,
} from '../utils/functions.js'
import { base64ToHex, generateCodeFromHexValues, hexToTwoItems } from '../utils/functions.js'
import platformLang from '../utils/lang-en.js'

/*
Expand All @@ -17,7 +12,6 @@ import platformLang from '../utils/lang-en.js'

export default class {
constructor(platform) {
this.connectedTo = false
this.log = platform.log
this.platform = platform
this.stateChange = false
Expand All @@ -40,169 +34,48 @@ export default class {

// Event and log noble warnings
btClient.on('warning', (message) => {
this.log.debugWarn('[BLE] %s.', message)
})

// Event handler for discovering bluetooth devices
// This should only be each and every time a device update is sent
btClient.on('discover', (device) => {
// Log the address found can be useful for debugging what's working
this.log.debug('[BLE] found device [%s] [%s].', device.address, device.advertisement.localName)

// Look for the device to update at the time
if (!this.accessory || this.accessory.context.bleAddress !== device.address) {
return
}

// Found the device so stop scanning
btClient.stopScanning()

// Make the device global as needed in other functions
this.device = device

// Log that the device has been discovered
this.accessory.logDebug(platformLang.onlineBT)

// Remove previous listeners that may still be intact
this.device.removeAllListeners()

// Add a listener for device disconnect
this.device.on('disconnect', (reason) => {
// Log the disconnection
if (this.accessory) {
this.accessory.logDebug(`${platformLang.offlineBTConn} [${reason || 'unknown'}]`)
} else {
this.log.debug(
'[BLE] [%s] %s [%s].',
this.device ? this.device.address : 'unknown',
platformLang.offlineBTConn,
reason || 'unknown',
)
}

// Un-define the variables used within the class
this.device = undefined
this.connectedTo = false
this.controlChar = undefined
this.accessory = undefined
})

// Reset adapter
btClient.reset()

// Connect to the device
this.accessory.logDebug('attempting to connect')
this.device.connect((error) => {
if (error) {
this.accessory.logWarn(`could not connect as ${error}`)
return
}
// Update the currently-connect-to variable
this.connectedTo = this.accessory.context.bleAddress

// Log the connection
this.accessory.logDebug(platformLang.onlineBTConn)

// Find the noble characteristic we need for controlling the device
this.accessory.logDebug('finding device characteristics')
device.discoverAllServicesAndCharacteristics((error2, services, characteristics) => {
if (error2) {
this.accessory.logWarn(`could not find device characteristics as ${error2}`)
return
}
this.accessory.logDebug('found some device characteristics')
Object.values(characteristics).forEach((char) => {
// Make sure we found the characteristic and make it global for the sendUpdate function
const formattedChar = char.uuid.replace(/-/g, '')
if (formattedChar === '000102030405060708090a0b0c0d2b11') {
this.controlChar = char
this.accessory.logDebug(`found correct characteristic [${formattedChar}]`)
} else {
this.accessory.logDebug(`found different characteristic [${formattedChar}]`)
}
})
if (!this.controlChar) {
this.accessory.logWarn('could not find control characteristic')
}
})
})
this.log.warn('[BLE] %s.', message)
})
}

async updateDevice(accessory, params) {
// This is called by the platform on sending a device update via bluetooth
accessory.logDebug(`starting update with params [${JSON.stringify(params)}]`)
accessory.logDebug(`starting BLE update with params [${JSON.stringify(params)}]`)

// Check the noble state is ready for bluetooth action
if (this.stateChange !== 'poweredOn') {
throw new Error(`${platformLang.bleWrongState} [${this.stateChange}]`)
}

// This is used to time out the request later on if it's taking too much time
// 7 seconds, to take into account AWS control failing as well as the ~10 second HK limit
let doIt = true
accessory.logDebug('starting timer')
setTimeout(() => {
doIt = false
}, 7000)

// Check if we are already connected to a device - and disconnect
if (this.device) {
if (this.connectedTo && this.connectedTo !== accessory.context.bleAddress) {
accessory.logDebug(`disconnecting from [${this.connectedTo}] to connect to [${accessory.context.bleAddress}]`)
await this.device.disconnectAsync()
accessory.logDebug('disconnect successful')
}
// Connect to the accessory
btClient.reset()
accessory.logDebug('attempting connection with device')
const peripheral = await btClient.connectAsync(accessory.context.bleAddress)
accessory.logDebug('connected to device')

// Find the characteristic we need for controlling the device
accessory.logDebug('looking for control characteristic')
const { characteristics } = await peripheral.discoverAllServicesAndCharacteristicsAsync()
const characteristic = Object.values(characteristics).find(char => char.uuid.replace(/-/g, '') === '000102030405060708090a0b0c0d1910')
if (!characteristic) {
accessory.logWarn('could not find control characteristic')
}
accessory.logDebug('found control characteristic')

// Make global the accessory in question which we are sending an update to
this.accessory = accessory

// Start the bluetooth scan to discover this accessory
// Service UUID for future reference 000102030405060708090a0b0c0d1910
accessory.logDebug('starting scan')
await btClient.startScanningAsync()
accessory.logDebug('scanning started')

// We want to wait for the .on('discover') function to find the accessory and the characteristic
accessory.logDebug('starting loop')

while (true) {
if (!doIt) {
accessory.logWarn(`could not find device [${accessory.context.bleAddress}]`)
throw new Error(platformLang.bleTimeout)
}

// Once the characteristic (this.controlChar) has been found then break the loop
if (this.connectedTo === accessory.context.bleAddress && this.controlChar) {
accessory.logDebug('found correct characteristic so breaking loop')
break
}

// Repeat this process every 200ms until the characteristic is available
await sleep(200)
}

// We can be sent either:
// Prepare the command - we can be sent either:
// - a base64 action code (with params.cmd === 'ptReal')
// - an array containing a varied amount of already-hex values
const finalBuffer = params.cmd === 'ptReal'
? Buffer.from(hexToTwoItems(base64ToHex(params.data)).map(byte => `0x${byte}`))
: generateCodeFromHexValues([0x33, params.cmd, params.data], true)

// Log the request if in debug mode
accessory.logDebug(`[BLE] ${platformLang.sendingUpdate} [${finalBuffer.toString('hex')}]`)

// Send the data to the device
await this.controlChar.writeAsync(finalBuffer, true)

// Maybe a slight await here helps (for an unknown reason)
await sleep(100)
accessory.logDebug(`[BLE] ${platformLang.sendingUpdate} [${finalBuffer.toString('hex')}]`)
await characteristic.writeAsync(finalBuffer, true)

// Disconnect from device
await this.device.disconnectAsync()

// Maybe a slight await here helps (for an unknown reason)
await sleep(100)
accessory.logDebug('disconnecting from device')
await peripheral.disconnectAsync()
accessory.logDebug('disconnected from device - all done')
}
}
2 changes: 1 addition & 1 deletion lib/platform.js
Original file line number Diff line number Diff line change
Expand Up @@ -1736,7 +1736,7 @@ export default class {
return true
} catch (err) {
// Bluetooth didn't work or not enabled
accessory.logDebugWarn(`${platformLang.notBLESent} ${parseError(err, [platformLang.bleTimeout])}`)
accessory.logDebugWarn(`${platformLang.notBLESent} ${parseError(err)}`)
}
}
throw new Error(platformLang.noConnMethod)
Expand Down
2 changes: 0 additions & 2 deletions lib/utils/lang-en.js
Original file line number Diff line number Diff line change
Expand Up @@ -115,8 +115,6 @@ export default {
noToken: 'no data received from Govee server whilst obtaining token',
noTokenExists: 'no account token has been retrieved',
offlineBTConn: 'has been reported [disconnected] via BLE',
onlineBT: 'has been reported [discoverable] via BLE',
onlineBTConn: 'has been reported [connected] via BLE',
pluginNotConf: 'Plugin has not been configured',
receivingUpdate: 'receiving update',
sendingUpdate: 'sending update',
Expand Down
24 changes: 12 additions & 12 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

0 comments on commit 14f3bd8

Please sign in to comment.