UNPKG

react-native-btsig-telink

Version:

Component implementation for Bluetooth SIG Mesh SDK of Telink

1,257 lines (1,131 loc) 125 kB
const { AppState, NativeModules, DeviceEventEmitter, NativeEventEmitter, Platform } = require('react-native'); const NativeModule = NativeModules.TelinkBtSig; const tinycolor = require("tinycolor2"); const createNewFifo = require('fifo'); const MeshSigModel = require("./MeshSigModel"); const NodeInfo = require("./NodeInfo"); const {CompositionData} = require("./CompositionData"); const PrivateDevice = require("./PrivateDevice"); const Opcode = require("./Opcode"); class TelinkBtSig { static MESH_ADDRESS_MIN = 0x0001; static MESH_ADDRESS_MAX = 0x00FF; // 虽然 bluetooth SIG mesh 协议理论上一个 mesh 网络最多支持 // 32767 个设备,不过一般比如植物照明工厂最多拥有两百多盏灯,差 // 不多刚好一般蓝牙模块提供商代码中所写的最大地址 255 ,而且天猫 // 或小米等相关产品的设备数都在 100 以内,由于设备数越多,需要设 // 备上的 ram 越大,越贵,所以一般蓝牙模块提供商只会批量提供 ram // 刚够用 200 设备数左右的模块。 // 另外,在 `telink_sig_mesh_sdk_v3.3.3.5/app/ios/document/TelinkSigMeshLib开放源代码版本SDK使用以及开发手册.docx` // 中提到地址范围是 1~0x7eff // static MESH_ADDRESS_MAX = 0x7EFF; // static MESH_ADDRESS_MAX = 0x7FFF; // since telink_sig_mesh_sdk_v3.3.3.5 increase the bytes per packet and speed, // act in concert with defaultAllGroupAddress and longCommandParams, // it's better to use meshAddresses in packet instead of group address. static useAddressesInsteadOfGroup = true; static PAR_VER_init = 1; static PAR_VER_useAddressesInsteadOfGroup = 2; static PAR_VER_useDurationInsteadOfAddresses = 3; static GROUP_ADDRESS_MIN = 0xC000; static GROUP_ADDRESS_MAX = 0xC0FF; // `telink_sig_mesh_sdk_v3.3.3.5/app/android/TelinkBleMesh/TelinkBleMesh/TelinkBleMeshDemo/src/main/java/com/telink/ble/mesh/model/Scene.java` // 中提到组地址范围是 C000 - 0xFEFF // static GROUP_ADDRESS_MAX = 0xFEFF; static GROUP_ADDRESS_MASK = 0x00FF; static HUE_MIN = 0; static HUE_MAX = 360; static SATURATION_MIN = 0; static SATURATION_MAX = 100; static BRIGHTNESS_MIN = 42; // 实测灯串不会随着亮度变化而改变颜色的最低亮度,比如 30 的话就代表 30% ,但也不建议为 0 ,否则上层代码如果将颜色作为亮度则会导致上层更改亮度时会把所有颜色变为黑、白或灰色 static BRIGHTNESS_MAX = 100; // 如果 LED 中绿色灯珠容易烧坏,此处记录着实际(也就是经过下面的 whiteBalance 之后)发给灯珠不会烧坏的最大值,比如 65 代表 65/255 // 纯绿色情况下的 g 值 // 100% 153 // 95% 135 // 90% 119 // 85% 103 // 80% 89 // 75% 76 // 70% 65 // 白色情况下的 r g b 值 // 100% 255, 153, 61 // 95% 225, 135, 54 // 90% 199, 119, 47 // 85% 173, 103, 41 // 80% 149, 89, 35 // 75% 127, 76, 30 // 70% 109, 65, 26 // static LED_GREEN_MAX = 103; static LED_GREEN_MAX = 255; static COLOR_TEMP_MIN = 5; static COLOR_TEMP_MAX = 100; static NODE_STATUS_OFF = 0; static NODE_STATUS_ON = 1; static NODE_STATUS_OFFLINE = -1; static RELAY_TIMES_MAX = 16; static DELAY_MS_AFTER_UPDATE_MESH_COMPLETED = 500; static DELAY_MS_COMMAND = 240; static ALARM_CREATE = 0; static ALARM_REMOVE = 1; static ALARM_UPDATE = 2; static ALARM_ENABLE = 3; static ALARM_DISABLE = 4; static ALARM_YEAR_ANY = 0x64; static ALARM_MONTH_ALL = 0b111111111111; static ALARM_DAY_ANY = 0; static ALARM_HOUR_ANY = 0x18; static ALARM_HOUR_RANDOM = 0x19; static ALARM_MINUTE_ANY = 0x3C; static ALARM_MINUTE_CYCLE_15 = 0x3D; static ALARM_MINUTE_CYCLE_20 = 0x3E; static ALARM_MINUTE_RANDOM = 0x3F; static ALARM_SECOND_ANY = 0x3C; static ALARM_SECOND_CYCLE_15 = 0x3D; static ALARM_SECOND_CYCLE_20 = 0x3E; static ALARM_SECOND_RANDOM = 0x3F; static ALARM_WEEK_ALL = 0b1111111; static ALARM_ACTION_TURN_OFF = 0; static ALARM_ACTION_TURN_ON = 1; static ALARM_ACTION_SCENE = 2; static ALARM_ACTION_NO = 0xF; static ALARM_TYPE_DAY = 0; static ALARM_TYPE_WEEK = 1; static commandFifoConsumer = undefined; // ref to getReliableMessageTimeout() in // android/src/main/java/com/telink/ble/mesh/core/networking/NetworkingController.java static DELAY_MS_CMD_RSP_TIMEOUT = 1280; // -1 ref to android/src/main/java/com/telink/ble/mesh/core/message/MeshMessage.java // 0 ref to ((vendorOpcodeResponse & 0xff) != 0) inTelinkSigMeshLib/TelinkSigMeshLib/Utils/SDKLibCommand.m // whether op_rsp is -1 or 0, it only affect APP SDK to determine if the command is rsp, it // will not affect FW because cb_par->op_rsp only can be the definition that match op in // telink_sig_mesh/vendor/comon/generic_model.c static OPCODE_INVALID = (Platform.OS === 'ios') ? 0 : -1; static passthroughMode = undefined; // 通过串口或者说自定义发送数据来控制蓝牙节点 static gamma = [ // gamma 2.4 ,normal color ,据说较暗时颜色经 gamma 校正后会比较准 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 0 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 2, // 16 2, 2, 2, 2, 2, 2, 3, 3, 3, 3, 3, 4, 4, 4, 4, 4, // 32 5, 5, 5, 5, 6, 6, 6, 6, 7, 7, 7, 8, 8, 8, 9, 9, // 48 9, 10, 10, 10, 11, 11, 11, 12, 12, 13, 13, 14, 14, 14, 15, 15, // 64 16, 16, 17, 17, 18, 18, 19, 19, 20, 20, 21, 22, 22, 23, 23, 24, // 80 24, 25, 26, 26, 27, 28, 28, 29, 30, 30, 31, 32, 32, 33, 34, 35, // 96 35, 36, 37, 38, 39, 39, 40, 41, 42, 43, 43, 44, 45, 46, 47, 48, // 112 49, 50, 51, 52, 53, 53, 54, 55, 56, 57, 58, 59, 60, 62, 63, 64, // 128 65, 66, 67, 68, 69, 70, 71, 73, 74, 75, 76, 77, 78, 80, 81, 82, // 144 83, 85, 86, 87, 88, 90, 91, 92, 94, 95, 96, 98, 99, 100, 102, 103, // 160 105, 106, 108, 109, 111, 112, 114, 115, 117, 118, 120, 121, 123, 124, 126, 127, // 176 129, 131, 132, 134, 136, 137, 139, 141, 142, 144, 146, 148, 149, 151, 153, 155, // 192 156, 158, 160, 162, 164, 166, 167, 169, 171, 173, 175, 177, 179, 181, 183, 185, // 208 187, 189, 191, 193, 195, 197, 199, 201, 203, 205, 207, 210, 212, 214, 216, 218, // 224 220, 223, 225, 227, 229, 232, 234, 236, 239, 241, 243, 246, 248, 250, 253, 255 // 240 ]; // static gamma = [ // gamma 2.8 ,vivid color ,据说较明亮时颜色经 gamma 校正后会比较准 // 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // 0 // 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // 16 // 1, 1, 1, 1, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 3, 3, // 32 // 3, 3, 3, 3, 3, 4, 4, 4, 4, 4, 5, 5, 5, 5, 5, 6, // 48 // 6, 6, 6, 7, 7, 7, 7, 8, 8, 8, 8, 9, 9, 9, 10, 10, // 64 // 10, 11, 11, 12, 12, 12, 13, 13, 13, 14, 14, 15, 15, 16, 16, 17, // 80 // 17, 18, 18, 19, 19, 20, 20, 21, 21, 22, 22, 23, 24, 24, 25, 25, // 96 // 26, 27, 27, 28, 29, 29, 30, 31, 31, 32, 33, 34, 34, 35, 36, 37, // 112 // 38, 38, 39, 40, 41, 42, 43, 43, 44, 45, 46, 47, 48, 49, 50, 51, // 128 // 52, 53, 54, 55, 56, 57, 58, 59, 60, 62, 63, 64, 65, 66, 67, 68, // 144 // 70, 71, 72, 73, 75, 76, 77, 78, 80, 81, 82, 84, 85, 87, 88, 89, // 160 // 91, 92, 94, 95, 97, 98, 100, 101, 103, 104, 106, 108, 109, 111, 112, 114, // 176 // 116, 117, 119, 121, 123, 124, 126, 128, 130, 131, 133, 135, 137, 139, 141, 143, // 192 // 145, 147, 149, 151, 153, 155, 157, 159, 161, 163, 165, 167, 169, 171, 173, 176, // 208 // 178, 180, 182, 185, 187, 189, 192, 194, 196, 199, 201, 203, 206, 208, 211, 213, // 224 // 216, 218, 221, 223, 226, 228, 231, 234, 236, 239, 242, 244, 247, 250, 253, 255 // 240 // ]; static whiteBalance = { // 为了让灯串能够在低电压下(就可以低温了)保持足够亮度,需要不再按照 1:6:3 的比例来设置 rgb 的白平衡 r: 1, g: 0.85, b: 0.4, }; // static whiteBalance = { // cooler // r: 1, // g: 0.6, // b: 0.24, // }; // static whiteBalance = { // warmer // r: 1, // g: 0.5, // b: 0.18, // }; static longCommandParams = true; // 可以在一条命令中发送大量数据,无需拆分成许多小命令 static MESH_CMD_ACCESS_LEN_MAX = 380; // 逻辑上能否通过蓝牙模块返回的在线状态或者开关灯等状态推理出在线状态 static hasOnlineStatusNotify = true; // 物理上蓝牙模块是否支持返回在线状态 static hasOnlineStatusNotifyRaw = false; static needRefreshMeshNodesBeforeConfig = false; static canConfigEvenDisconnected = true; static needClaimedBeforeConnect = true; static isClaiming = false; static needSceneSync = true; static del4GroupStillSendOriginGroupAddress = true; static defaultAllGroupAddress = 0xFFFF; static isSetNodeGroupAddrReturnAddresses = false; // 需要如下两个变量来保证韵律期间频发音量转亮度 changeBrightness() 的情况下也能正常 changeScene() , // 否则在两串灯时, All 组,开启韵律后,切换效果很多时候只有一盏灯能切换效果,另外一盏没变 static isSceneCadenceBusy = false; static allowSceneCadence = true; static isCreatingSceneParList = false; // 为 true 时仅仅准备发给设备的数据,而不是真正发给设备 static sceneParList = []; static netKey = '_16_BYTES_NETKEY'; static appKey = '_16_BYTES_APPKEY'; // 测试得:手机 mesh 地址不能设为 0 ,也不能设为 >= 32768 // 分享相同蓝牙设备数据的两台手机各自的 APP 需要不同的手机 mesh 地址,否则无法同时控制设备。 // 而且这样得到了一个额外好处:本来如果每次启动 (仅有 Android ?) APP 时使用与上次连接时相同的 meshAddressOfApp ,则需要 // 很长时间才能连接上设备,除非将设备断电再上电才能不受相同 meshAddressOfApp 的影响,感觉好像设备中保存着上次连接的(或是 // mesh_proxy_filter_add_adr 增加的白名单)手机地址与这次想要连接的手机地址相同的话,就需要很长时间才能连接上设备,而现在 // 每次启动 APP 时使用一个随机 meshAddressOfApp 就能解决这个问题。 static meshAddressOfApp = this.MESH_ADDRESS_MAX + parseInt(Math.random() * 10000, 10); static devices = []; static EXTEND_BEARER_MODE = { // no extension // 42B/s NONE: 0, /// gatt only // 关闭 DLE 功能后, SDK 的 Access 消息是长度大于 15 字节才进行 segment 分包 // 42B/s GATT: 1, /// gatt and adv // 打开 DLE 功能后, SDK 的 Access 消息是长度大于 229 字节才进行 segment 分包 // also need change firmware: // set EXTENDED_ADV_ENABLE to 1 in `vendor/common/mesh_config.h` // (maybe) let is_not_use_extend_adv() return 0 in `vendor/common/mesh_node.c` // 8KB/s GATT_ADV: 2, }; static extendBearerMode = this.EXTEND_BEARER_MODE.NONE; // after telink sig mesh sdk 3.2.1 , by default, not support // FirmwareUpdateInfoGetMessage (0x01B6) and startMeshOta() , only support startGattOta() , // you should register on https://www.bluetooth.com/ then get mesh OTA code from telink, // before that, use getFwVerInNodeInfo() and NativeModule.startOta() instead, // and use valid (even canMeshOta is false) getFirmwareVersion() to get version then use // getNodeInfoWithNewFwVer() to update device.nodeInfo after NativeModule.startOta() resolved static canMeshOta = false; static provisionerSno = 0; static provisionerIvIndex = 0; static otaFileVersionOffset = 4; // 把二进制固件作为一个字节数组看待的话,描述着版本号的第一个字节的数组地址 static otaFileVersionLength = 2; // 二进制固件中描述版本号用了几个字节 static lastSceneSyncMeshAddress = undefined; // why manuallyCheckSystemLocation and manuallyRequestLocationPermissions? // because it's more convenient to deal with location privacy policy of Google Play // default is false static set manuallyCheckSystemLocation(isManually) { if (Platform.OS === 'android') { NativeModule.setManuallyCheckSystemLocation(isManually); } } static get manuallyCheckSystemLocation() { if (Platform.OS === 'android') { return NativeModule.getManuallyCheckSystemLocation(); } } // default is false, if set to true, can use isLocationPermissionsGranted() and requestLocationPermissions() later, // if set to true and on Android 13, also need doInitAfterCheckPermissions() after them and doInit() to avoid crash // cause Android 13 is a shit. static set manuallyRequestLocationPermissions(isManually) { if (Platform.OS === 'android') { NativeModule.setManuallyRequestLocationPermissions(isManually); } } static get manuallyRequestLocationPermissions() { if (Platform.OS === 'android') { return NativeModule.getManuallyRequestLocationPermissions(); } } static _handleAppStateChange(newState) { if (newState === 'active') { this.checkSystemLocation(); } } static doInit() { if (Platform.OS === 'android') { // on Android 13, java onHostResume() will not be invoked, so use it to // call checkSystemLocation(), Android 13 is a shit! AppState.addEventListener('change', this._handleAppStateChange.bind(this)); } NativeModule.doInit(this.netKey, this.appKey, this.meshAddressOfApp, this.devices.map(device => { // for debug // if (device.meshAddress === 1) { // let nodeInfo = NodeInfo.from(this.hexString2ByteArray(device.nodeInfo)); // console.warn(nodeInfo.cpsData.toString()); // } return { ...device, dhmKey: this.hexString2ByteArray(device.dhmKey), nodeInfo: this.hexString2ByteArray(device.nodeInfo), }; // }), this.provisionerSno, this.provisionerIvIndex, // telink sdk 3.1.0 实测发现,不管上面的 ivIndex ,甚至也不用管 sno ,而是每次打开 APP 时将这两者都设为 0 ,然后每次就都可以连接上了。 // 唯一的例外是如果 APP 一直开在那里足够长时间,然后 sno 足够大时让 ivIndex 变成 1 后,就再也连不上了,而按照 // int sno 溢出计算,这个“足够长时间”是好几年,而一般 APP 应用情景不可能连续开启几年,所以这两者都设为 0 就可以了。 // telink sdk 3.1.0 以下现象在 Android 上测了许多次,在 iOS 测得少一点,但也有此现象: // 0、不论之前 sno 是多少,(上面随机 meshAddressOfApp 所提到的额外好处?)再次用 sno 0 (会瞬间由手机上的 sdk store 变为 128)来打开手机,仍然能够连上蓝牙设备 // (然后设备的 sno 从 128 开始 store 回手机,然后每隔 17 秒 store + 129 )。 // 1、代码中写死以 ivIndex 1 进行设备认领、连接,然后重启 APP 进行连接时从打印信息可以看到,先是 retrieve 了 // ivIndex 1 ,然后立即 store 了 ivIndex 1 (这次的 store 不用关心,因为这不是蓝牙设备发来的而是手机上的 sdk // 发来的),然后稍等几秒,就 store 了蓝牙设备发来的 ivIndex 1 并且连接上蓝牙设备。 // 2、此时代码中写死以 ivIndex 0 连接,从打印信息可以看到,先是 retrieve 了 ivIndex 0 ,然后立即 store 了 // ivIndex 0 (这次的 store 不用关心,因为这不是蓝牙设备发来的而是手机上的 sdk 发来的),然后就再也没有然后了 // ……连不上蓝牙设备。 // 3、在上面 1 和 2 步骤中,如果是先以足够大的 ivIndex A 进行认领再以足够小的 ivIndex B 进行连接,且 A - B > 1, // 那么是可以连接上的。 // 4、由于 3 中 A - B > 1 的现象,则可以预测,当 ivIndex 由 0 变为 1 时,那么另外一台很早之前以 ivIndex 0 // 分享出去且不在现场的手机,后续将永远连接不上蓝牙设备。 // }), 0, 0); // telink sdk 3.1.0 上测得如果不设成 129 而设成 0 的话,有时候删除设备时会一直没有任何动静 // telink sdk 3.3.3.5 上懒得再把上面都测一遍了,就这样吧 }), 129, 0, this.extendBearerMode); // NativeModule.setLogLevel(0x1F); this.DELAY_MS_COMMAND = NativeModule.getCommandsQueueIntervalMs(); this.commandFifoBusy = false; this.commandFifoConsumer = { fifo: createNewFifo(), consumer: (fc) => { if (fc.fifo.length) { const commandHandler = fc.fifo.shift(); if (commandHandler) { commandHandler(); } } else { this.commandFifoBusy = false; } }, timer: undefined, } } static doInitAfterCheckPermissions() { if (Platform.OS === 'android') { NativeModule.doInitAfterCheckPermissions(); } } static doDestroy() { const fc = this.commandFifoConsumer; fc.timer && clearTimeout(fc.timer); this.commandFifoBusy = false; this.getOnlineStatueTimer && clearTimeout(this.getOnlineStatueTimer); NativeModule.doDestroy(); if (Platform.OS === 'android') { AppState.removeEventListener('change', this._handleAppStateChange.bind(this)); } } static addListener(eventName, handler) { if (Platform.OS === 'ios') { const TelinkBtEmitter = new NativeEventEmitter(NativeModule); TelinkBtEmitter.addListener(eventName, handler); } else { DeviceEventEmitter.addListener(eventName, handler); } } static removeListener(eventName, handler) { if (Platform.OS === 'ios') { const TelinkBtEmitter = new NativeEventEmitter(NativeModule); TelinkBtEmitter.removeListener(eventName, handler); } else { DeviceEventEmitter.removeListener(eventName, handler); } } static isLocationPermissionsGranted() { if (Platform.OS === 'android') { return NativeModule.isLocationPermissionsGranted(); } } static enableBluetooth() { NativeModule.enableBluetooth(); } static isSystemLocationEnabled() { if (Platform.OS === 'android') { return NativeModule.isSystemLocationEnabled(); } } // will receive event of 'systemLocationEnabled' or 'systemLocationDisabled' static checkSystemLocation() { if (Platform.OS === 'android') { NativeModule.checkSystemLocation(); } } static enableSystemLocation() { NativeModule.enableSystemLocation(); } static requestLocationPermissions() { if (Platform.OS === 'android') { NativeModule.requestLocationPermissions(); } } static resetExtendBearerMode(extendBearerMode = this.extendBearerMode) { this.extendBearerMode = extendBearerMode; NativeModule.resetExtendBearerMode(this.extendBearerMode); } static notModeAutoConnectMesh() { return NativeModule.notModeAutoConnectMesh(); } static autoConnect({}) { NativeModule.autoConnect(); } static async postConnected({ meshAddress, type, immediate = false, }) { // 需要此处,否则在 1 个设备的情况下,走过 // 设置效果、删除设备、认领设备、设置效果 // 这 4 个步骤时,除非重启 APP ,否则因为 // 无法符合 selectNodeToResponseSceneId() // 中 sceneSyncMeshAddress !== this.lastSceneSyncMeshAddress // 的条件而导致 APP 一直没有发出设置同步的命令 this.lastSceneSyncMeshAddress = undefined; this.remind({ meshAddress: this.defaultAllGroupAddress, }) await this.sleepMs(this.DELAY_MS_COMMAND); let changed = false; if (this.passthroughMode) { let preDefinedType = type | 0xf000; for (let mode in this.passthroughMode) { if (this.passthroughMode[mode].includes(preDefinedType)) { if (mode === 'silan') { // 获取 GIF 文件列表的状态 this.sendCommand({ opcode: 0x0211E6, meshAddress: this.defaultAllGroupAddress, valueArray: [0xa1, 5, 1, 1, 0xFF], immediate, }); // 获取 BMP 文件列表的状态 this.sendCommand({ opcode: 0x0211E6, meshAddress: this.defaultAllGroupAddress, valueArray: [0xa1, 5, 1, 2, 0xFF], immediate, }); // 因为上面的 this.remind() 爆闪时导致固件有几个瞬间是处于开灯状态的,所以等待爆闪结束时 // 用 0x0211E1 发起开关灯状态查询,才能得到正确的开关灯状态。 // sleepMs 等待爆闪结束的时间长短,可以通过在关灯情况下再打开 APP 看是否能获得 2 个设备 // 的关灯状态的测试方式来调节,目前测的在调用上面两个 this.sendCommand 后,在 3 个设备 // 的情况下,无需 sleepMs 就能正常 // await this.sleepMs(2000); // 我们项目的固件里将 0x0211E1 返回的 TelinkBtSigNativeModule.onVendorResponse 的 opcode 设为了 0x0211E3 , // 这就是为何下面的 parseVendorResponse() 中有 0x0211E3 存在 this.sendCommand({ opcode: 0x0211E1, meshAddress: this.defaultAllGroupAddress, valueArray: [], immediate, }); changed = true; } break; } } } if (!changed) { if (!this.hasOnlineStatusNotifyRaw) { // 如果后续从蓝牙设备固件代码中得知 telink 也实现了(应该实现了) sig mesh 协议中 // model 之间关联功能,放到这里就是实现了亮度 modle 如果亮度为 <= 0 的话就会关联 // 开关灯 model 为关灯状态,则此处可以不再使用 Opcode.G_ONOFF_GET 而只用 Opcode.LIGHT_CTL_GET 等代替 await this.sendCommandRsp({ opcode: Opcode.G_ONOFF_GET, meshAddress: this.defaultAllGroupAddress, valueArray: [], rspOpcode: Opcode.G_ONOFF_STATUS, immediate, }); // 下面注释掉的 Get Opcode 仅用于测试 // await this.sendCommandRsp({ // opcode: Opcode.G_LEVEL_GET, // meshAddress: this.defaultAllGroupAddress, // valueArray: [], // rspOpcode: Opcode.G_LEVEL_STATUS, // immediate, // }); // await this.sendCommandRsp({ // opcode: Opcode.LIGHTNESS_GET, // meshAddress: this.defaultAllGroupAddress, // valueArray: [], // rspOpcode: Opcode.LIGHTNESS_STATUS, // immediate, // }); // 如 TelinkBtSigNativeModule.java 的 onGetLevelNotify() 中注释所说,使用 onGetCtlNotify() 更简洁 await this.sendCommandRsp({ opcode: Opcode.LIGHT_CTL_GET, meshAddress: this.defaultAllGroupAddress, valueArray: [], rspOpcode: Opcode.LIGHT_CTL_STATUS, immediate, }); // await this.sendCommandRsp({ // opcode: Opcode.LIGHT_CTL_TEMP_GET, // meshAddress: this.defaultAllGroupAddress, // valueArray: [], // rspOpcode: Opcode.LIGHT_CTL_TEMP_STATUS, // immediate, // }); } } } static autoRefreshNotify({ repeatCount, Interval }) {} static idleMode({ disconnect }) { return NativeModule.idleMode(disconnect); } static startScan({ timeoutSeconds = 15, isSingleNode = false, }) { this.isClaiming = false; return NativeModule.startScan(timeoutSeconds, isSingleNode); } static parseVendorResponse(resRaw) { let res; switch (resRaw.opcode) { case 0x0211E3: res = { opcode: 'ONOFF_STATUS', meshAddress: resRaw.meshAddress, isOnline: true, isOn: resRaw.params[0] !== 0, }; break; case 0x0211E7: { res = { meshAddress: resRaw.meshAddress, } const scene = resRaw.params[0] & 0xff; if (scene === 0xa1) { const action = resRaw.params[1] & 0xff; if (action === 6) { res.opcode = 'FILES_STATUS' const filesLength = resRaw.params[3] & 0xff; res.fileType = resRaw.params[4] & 0xff; res.files = []; let offset = 5; for (let i = 0; i < filesLength; i++) { const version = resRaw.params[offset++] & 0xff; const nameLength = resRaw.params[offset++] & 0xff; const name = []; for (let j = 0; j < nameLength; j++) { name.push(resRaw.params[offset++] & 0xff); } res.files.push({ version, name: String.fromCharCode(...name), }) } } else if (action === 7) { res.opcode = 'LOST_CHUNKS_STATUS' res.fileType = resRaw.params[3] & 0xff; res.fileVersion = resRaw.params[4] & 0xff; let offset = 5; const nameLength = resRaw.params[offset++] & 0xff; const name = []; for (let i = 0; i < nameLength; i++) { name.push(resRaw.params[offset++] & 0xff); } res.fileName = String.fromCharCode(...name); const maxChunkLengthLowByte = resRaw.params[offset++] & 0xff; const maxChunkLengthHightByte = resRaw.params[offset++] & 0xff; res.maxChunkLength = (maxChunkLengthHightByte << 8) | maxChunkLengthLowByte; res.lostChunks = []; const lostChunksLength = resRaw.params[offset++] & 0xff; for (let i = 0; i < lostChunksLength; i++) { res.lostChunks.push(resRaw.params[offset++] & 0xff); } } } break; } case 0x0211F6: res = { opcode: 'SCENE_SYNC', meshAddress: resRaw.meshAddress, isOnline: true, isOn: resRaw.params[0] !== 0, sceneID: resRaw.params[1] & 0xFF, sceneSyncTime: resRaw.params[1] && new Date().getTime(), }; break; case 0x0211FB: switch (resRaw.params[0]) { case 1: res = { opcode: 'notificationDataGetVersion', meshAddress: resRaw.meshAddress, // 为了保持兼容性,返回的 version 格式仍然沿用 // telink_sig_mesh_sdk_v3.1.0/firmware/vendor/common/version.h // 中 FW_VERSION_TELINK_RELEASE 的 (VERSION_GET(0x31, 0x42)) 定义方法,所以弃用 // telink_sig_mesh_sdk_v3.3.3.5/firmware/vendor/common/version.h // 中 ((SW_VERSION_SPEC << 4) + (SW_VERSION_MAJOR << 0) 的定义方法 version: String.fromCharCode(resRaw.params[1], resRaw.params[2]), }; break; default: res = {}; break; } break; case 0x0211FF: res = { opcode: 'DEBUG_FW_PRINT', meshAddress: resRaw.meshAddress, params: resRaw.params, }; break; default: res = {}; break; } return res; } static setCommandsQueueIntervalMs(interval) { NativeModule.setCommandsQueueIntervalMs(interval); this.DELAY_MS_COMMAND = interval; } static getCommandsQueueIntervalMs() { return this.DELAY_MS_COMMAND; } static clearCommandFifo({ opcodeImmediate, }) { NativeModule.clearCommandQueue(); let fc = this.commandFifoConsumer; for (let i = 0; i < fc.fifo.length; i++) { const commandHandler = fc.fifo.shift(); if (commandHandler) { commandHandler(opcodeImmediate); } } this.commandFifoBusy = false; } static addCommandFifo(fifoData) { if (this.commandFifoBusy) { const fc = this.commandFifoConsumer; fc.fifo.push(fifoData); } else { this.commandFifoBusy = true; const fc = this.commandFifoConsumer; fc.timer && clearTimeout(fc.timer); fc.fifo.push(fifoData); fc.consumer(fc); } } // 这里 delayMs 之所以可以设置为 0 是因为 telink SDK 中本身自带 native 层队列,当 fc.consumer(fc) 被执行后 // 就会有相应命令被堆入 native 层的队列中,然后 Android 会定时(getCommandsQueueIntervalMs 得 240ms) 从队列 // 中或 iOS 会在前次命令结束后立即 handleResultCallback 从队列中取出一个命令用蓝牙硬件发送出去,这里的 delayMs // 可以控制 js 层 fifo 队列往 native 层队列发送的时机,设为 0 的话可以让蓝牙命令尽快被发出,特别是由于 iOS 的 // handleResultCallback 的关系如果设为 0 的话则在命令众多的情况下测得即使 2 设备时也会比 Android 更快速地被发 // 出,但由于如果命令多到十几个的情况下容易丢几个命令,所以当为 iOS 时这里还是不使用 0 而是就当作这个备注了: // 备注:如果 SDK 没有或取消 native 层队列的话,单独使用 delayMs 配合 js 层 fifo 队列也是可以的。 static setNextFcTimer(delayMs = this.DELAY_MS_COMMAND) { const fc = this.commandFifoConsumer; fc.timer = setTimeout(() => { fc.consumer(fc); }, delayMs); } // telink, why do you have so many ack cmd in queue if there are many device? I have to use {immediate: true} static getCommandQueueLength() { return NativeModule.getCommandQueueLength(); } static getCmdRspTimeoutMs(retryCnt = 2) { return NativeModule.getCommandQueueLength() * this.DELAY_MS_COMMAND + (retryCnt + 1) * this.DELAY_MS_CMD_RSP_TIMEOUT; } // without response, quickly (1/3 time of this.sendCommandRsp below), but despite whether devices received cmd static sendCommand({ opcode, meshAddress, valueArray, // means the MeshMessage.params on Android, IniCommandModel.commandData on iOS rspOpcode = this.OPCODE_INVALID, tidPosition = -1, // if > 0 , means the tid is stored in valueArray[tidPosition - 1] immediate = false, }) { if (immediate) { this.commandFifoConsumer.fifo.length && this.clearCommandFifo({ opcodeImmediate: opcode, }) NativeModule.sendCommand(opcode, meshAddress, valueArray, rspOpcode, tidPosition, true); } else { this.addCommandFifo((opcodeImmediateCancelBy) => { if (opcodeImmediateCancelBy !== undefined) { return; } // if (opcode === 0x0211E6 && valueArray[0] === 0xa1 && valueArray[1] === 0) { // console.log('transfer', meshAddress.toString(16), valueArray[11] + '/' + (valueArray[12] - 1), '0x' + valueArray[8].toString(16)); // } else { // console.log('sendCommand', meshAddress.toString(16), 'opcode:' + opcode.toString(16), valueArray); // } NativeModule.sendCommand(opcode, meshAddress, valueArray, rspOpcode, tidPosition, false); this.setNextFcTimer(rspOpcode === this.OPCODE_INVALID ? undefined : this.getCmdRspTimeoutMs()); }); } } // with Promise response, 3x slower than this.sendCommand() above, but more ensure // // if device fw not response, will cause reject timeout then other command in fifo // can run, so please debug fw or other command just use NativeModule.sendCommand // directly by this.sendCommand(immediate: true) // // if mix many NativeModule.sendCommand and this.sendCommandRsp in a short period, // maybe cause once reject timeout, please adjust your APP code // // here relayTimes AKA responseMax, means native will not resolve Promise until // receive relayTimes response with rspOpcode from device, so APP can // sendCommandRsp({relayTimes: onlineCountInGroup}) where onlineCountInGroup is: // 1 or 0 if MESH_ADDRESS is online or not // online count if GROUP_ADDRESS or defaultAllGroupAddress static sendCommandRsp({ opcode, meshAddress, valueArray, // means the MeshMessage.params on Android, IniCommandModel.commandData on iOS rspOpcode = this.OPCODE_INVALID, relayTimes = 0, // ref to `responseMax = 0` in android/src/main/java/com/telink/ble/mesh/core/message/MeshMessage.java retryCnt = 2, // ref to `DEFAULT_RETRY_CNT = 2` in android/src/main/java/com/telink/ble/mesh/core/message/MeshMessage.java tidPosition = -1, // if > 0 , means the tid is stored in valueArray[tidPosition - 1] immediate = false, }) { if (immediate) { this.clearCommandFifo({ opcodeImmediate: opcode, }) } // use fifo and Promise and this.commandFifoBusy to avoid the BUG: if current // rsp command not get rspMax rsp to means complete, but here comes next rsp // command, then will `reliable message send err: busy` with reliableBusy in // android/src/main/java/com/telink/ble/mesh/core/networking/NetworkingController.java // in another word, APP now can `await this.sendCommandRsp()` or just `this.sendCommandRsp()` // many times quickly(no need wait 240ms), and still ensure every cmd works fine return new Promise((resolve, reject) => this.addCommandFifo((opcodeImmediateCancelBy) => { if (opcodeImmediateCancelBy !== undefined) { reject(new TypeError('opcode ' + opcode.toString(16) + ' is canceled by opcodeImmediate ' + opcodeImmediateCancelBy.toString(16))); return; } const timeout = this.getCmdRspTimeoutMs(retryCnt); let timer = setTimeout(() => { // to ensure exit Promise if `reject(error)` never invoked from native // console.warn('sendCommandRsp @' + meshAddress.toString(16) + ' time out ' + timeout + 'ms'); reject(new TypeError('sendCommandRsp @' + meshAddress.toString(16) + ' opcode ' + opcode.toString(16) + ' time out ' + timeout + 'ms')); this.setNextFcTimer(); }, timeout); NativeModule.sendCommandRsp(opcode, meshAddress, valueArray, rspOpcode, relayTimes, retryCnt, tidPosition, false).then(payload => { clearTimeout(timer); // console.log('sendCommandRsp @' + meshAddress.toString(16) + ' opcode:' + opcode.toString(16) + ' relayTimes:' + relayTimes + ' valueArray:' + valueArray); resolve(payload); this.setNextFcTimer(); }, error => { clearTimeout(timer); // after retry in native (DEFAULT_RETRY_CNT = 2 in android/src/main/java/com/telink/ble/mesh/core/message/MeshMessage.java) // still error then reject here to APP // console.warn('sendCommandRsp @' + meshAddress.toString(16) + ' opcode:' + opcode.toString(16) + ' ' + error); reject(error); this.setNextFcTimer(); }); })); } // 让灯爆闪几下 static remind({ meshAddress, immediate = false, }) { this.sendCommand({ opcode: 0x0211F0, meshAddress, valueArray: [], immediate, }); } static isOnline(status) { return (status) !== this.NODE_STATUS_OFFLINE; } static isOn(status) { return (status) === this.NODE_STATUS_ON; } static async changePower({ meshAddress, meshAddresses = [], value, type, parVer = this.useAddressesInsteadOfGroup === true ? this.PAR_VER_useAddressesInsteadOfGroup : this.PAR_VER_init, delaySec = 0, immediate = true, // ack is true in native code and that will cause delay if changePower again in short time, except immediate is true }) { let changed = false; if (this.passthroughMode) { let preDefinedType = type | 0xf000; for (let mode in this.passthroughMode) { if (this.passthroughMode[mode].includes(preDefinedType)) { if (mode === 'silan') { // 不论这里是 this.OPCODE_INVALID 还是 0x0211E3 ,返回的 TelinkBtSigNativeModule.onVendorResponse 的 opcode 都是 0x0211E3 , // 究其根本原因其实是我们自己的固件代码中写成了只要收到开关灯命令,就一定通过 E3 返回开关灯状态 // this.sendCommand({ // opcode: this.hasOnlineStatusNotifyRaw ? 0x0211E2 : 0x0211E0, // meshAddress, // valueArray: [value], // rspOpcode: 0x0211E3, // immediate, // }); // 按说在 this.hasOnlineStatusNotifyRaw 被 saveOrUpdateJS 事件设为 true 的情况下,只要使用上面 // 的不带返回值的开关命令 E2 即可,但是发现当在界面上快速点击开关的情况下,只有下面的带返回值的开关命 // 令 E0 额外返回的开关状态才能保证开关按钮的状态能够快速切换且能快速地开关灯。 const par = [value]; if (parVer === this.PAR_VER_useAddressesInsteadOfGroup) { par.push(parVer); par.push(meshAddresses.length); par.push(...meshAddresses); if (meshAddresses.length === 0) { // 当 meshAddresses.length 为 0 时,如果不使用下面的语句,则 par 最后一个字节为 0 , // 但不可为 0,否则 E0 命令在固件收到后变成了 1、2、3、4 ... 而非 0 (BUG 或是没有 // TODO: 设置好 tid?),虽然 F3 之类的命令不会如此,所以这里添加一个不为 0 的比如 0xFF par.push(0xFF); } } // NativeModule.sendCommand(0x0211E0, meshAddress, par, 0x0211E3, -1, immediate); // 如果使用下面带 fifo 的 this.sendCommandRsp ,则当 immediate 为 false 且用户短时间内连续点击开关灯 // 时,会导致用户松手后仍然会自动开关灯连续切换一段时间,所以下面 immediate 最好为 true await this.sendCommandRsp({ opcode: 0x0211E0, meshAddress, valueArray: par, rspOpcode: 0x0211E3, immediate, }); changed = true; } break; } } } if (!changed) { NativeModule.changePower(meshAddress, value); } } static setAudioFrequencyHistogram({ meshAddress = this.defaultAllGroupAddress, loudness = 0, value = [ 10, // height of frequency 0 on histogram 90, // height of frequency 1 on histogram 40, // ... 90, 10, 0, 0, 1, ], relayTimes = 7, immediate = false, }) { if (this.allowSceneCadence) { this.isSceneCadenceBusy = true; this.sendCommand({ opcode: 0x0211FA, // 0x0211FA means set without rsp in my product meshAddress, valueArray: [ 3, // 3 means setAudioFrequencyHistogram in my product this.gamma[loudness], value.length, // how many frequency ...value, ], immediate, }); } } static async onOffAudioFrequencyHistogram({ meshAddress = this.defaultAllGroupAddress, value = 0, // 0: off, 1: on relayTimes = 7, immediate = false, }) { if (this.isSceneCadenceBusy) { this.allowSceneCadence = false; // 因音乐图谱功能是不停地(每隔 50ms 左右)在发送 setAudioFrequencyHistogram 蓝牙消息, // 这样关闭音乐图谱功能时,如果不在这里等待足够长时间以便让那些图谱消息发送完成,有时就就无法关闭 await this.sleepMs(this.DELAY_MS_COMMAND); this.isSceneCadenceBusy = false; } this.sendCommand({ opcode: 0x0211FA, // 0x0211FA means set without rsp in my product meshAddress, valueArray: [ 4, // 4 means onOffAudioFrequencyHistogram in my product value, 0, // reserve ], immediate, }); this.allowSceneCadence = true; } static changeBrightness({ meshAddress, hue = 0, saturation = 0, value, type, immediate = false, }) { let changed = false; if (this.passthroughMode) { let preDefinedType = type | 0xf000; for (let mode in this.passthroughMode) { if (this.passthroughMode[mode].includes(preDefinedType)) { if (mode === 'silan') { if (this.allowSceneCadence) { this.isSceneCadenceBusy = true; NativeModule.sendCommand(0x0211F3, meshAddress, [this.gamma[value]], this.OPCODE_INVALID, -1, immediate); } changed = true; } break; } } } if (!changed) { NativeModule.changeBrightness(meshAddress, value); } } static changeColorTemp({ meshAddress, value }) { NativeModule.changeColorTemp(meshAddress, value); } static changeColor({ meshAddress, hue = 0, saturation = 0, value, type, immediate = false, }) { const h = parseInt(65535 * hue / this.HUE_MAX, 10); const s = parseInt(65535 * saturation / this.SATURATION_MAX, 10); const v = parseInt(65535 * value / this.BRIGHTNESS_MAX, 10); NativeModule.changeColor(meshAddress, h, s, v); } static padHexString(string) { if (string.length === 1) { return '0' + string; } else { return string; } } static hexString2ByteArray(string) { let array = []; [].map.call(string, (value, index, str) => { if (index % 2 === 0) { array.push(parseInt(value + str[index + 1], 16)); } }); return array; } static byteArray2HexString(bytes) { return bytes.map(byte => this.padHexString((byte & 0xFF).toString(16))).toString().replace(/,/g, '').toUpperCase(); } static sleepMs(ms) { return new Promise(resolve => setTimeout(resolve, ms)); } // 为了解决颜色不准的问题,最终发现原因是灯串硬件亮度级别是 8 位与手机估计至少 12 位亮度级别 // 存在矛盾,也就是说是灯串硬件等级(成本)较低造成的,光靠调灯串硬件固件是不行的。现在的解决方 // 法是查看 RGB 中的某个分量,如果低于某个值(视灯串 LED 的 gamma 表而定)的就变为 0 之类的方法。 // 具体来说,低于等于 0x30 的就变为 0 ,位于 0x30 和 0x40 之间的就变为 0x40,其它不变。 // // 后来发现颜色不准主要是由于固件中效果变化时 gamma 重复操作导致的,所以下面代码暂时简单禁用 ledFilter3040 static ledFilter3040(value) { if (value <= 0x30) { return 0; } if (value < 0x40) { return 0x40; } return value; } static ledFilterBurnGreen({ r, g, b, scene, }) { if (g > this.LED_GREEN_MAX) { return { r: parseInt(r * this.LED_GREEN_MAX / g, 10), g: this.LED_GREEN_MAX, b: parseInt(b * this.LED_GREEN_MAX / g, 10), } } else if (r === 0 && g === 0 && scene !== 45) { // 因为当前使用的灯串的蓝色偏暗,而又受到白平衡和 LED_GREEN_MAX 的限制,所以这里单独将纯蓝亮度提高到客户满意的 2 倍 let newB = b * 2; if (newB > 122) { newB = 122; } return { r, g, b: newB, } } else { return { r, g, b, } } } static startCreateScenePar() { this.sceneParList = []; this.isCreatingSceneParList = true; } // 按 startCreateScenePar() 、 多次 changeScene() 、 stopCreateScenePar() // 的顺序调用,就可获得播放列表 data ,后续可以将这个 data 作为文件保存到灯串中 static stopCreateScenePar() { this.isCreatingSceneParList = false; const data = this.arrayList2Data(this.sceneParList); this.sceneParList = []; return data; } static arrayList2Data(arrayList) { const listLength = arrayList.length; const sceneParData = [listLength & 0xff, (listLength >>> 8) & 0xff]; const lengthOfListLength = 2; const lengthOfOffsetAndLen = (2 + 2) * listLength; let offset = lengthOfListLength + lengthOfOffsetAndLen; for (let i = 0; i < listLength; i++) { const arrayLength = arrayList[i].length; sceneParData.push(offset & 0xff); sceneParData.push((offset >>> 8) & 0xff); sceneParData.push(arrayLength & 0xff); sceneParData.push((arrayLength >>> 8) & 0xff); offset += arrayLength; } return sceneParData.concat(arrayList.flat()); } static async changeScene({ meshAddress, meshAddresses = [], relayTimes = 0, retryCnt = 2, sceneSyncMeshAddress, scene, sceneMode = 5, // e.g. 二维图片的平移方向 sceneModeOpt = 0, // e.g. 二维图片斜向平移时是否填充空边 sceneRotate = 0, // 图片旋转 sceneMirror = 0, // 图片镜像翻转 sceneDisplayRange = 0, // 显示范围 sceneImageResizeMode = 0, // 图片缩放模式 fileVersion = 0, text = 'flyskywhy', hue = 0, saturation = 0, value, reserve = 0, color, reserveBg = 0, colorBg, reserves = [], colors = [], colorsLength = 1, colorSequence = 1, colorIds = [1, 2, 3, 4, 5], colorBgId = 2, colorId = 1, data = [], isEditingCustom = false, needPostProcessColor = true, speed = -2, // 效果持续播放的毫秒数,如果为 0 ,则表示无限时长(无限循环), // 一般只在 this.isCreatingSceneParList || parVer === this.PAR_VER_useDurationInsteadOfAddresses // 时才需要折腾,于是固件收到的数据中不包含 ms 的也定义为表示无限时长 ms = 10100, bigDataAction, bigDataType = 0, bigDataLostRetry = false, datasIndex, datasCount, chunksIndex, chunksCount, chunk, maxChunkLength = 200, type, parVer = this.useAddressesInsteadOfGroup === true ? this.PAR_VER_useAddressesInsteadOfGroup : this.PAR_VER_init, immediate = false, }) { if (this.isSceneCadenceBusy && !this.isCreatingSceneParList) { this.allowSceneCadence = false; // 因 Cadence 韵律功能是不停地(每隔 50ms 左右)在发送 changeBrightness 蓝牙消息, // 这样切换效果时,如果不在这里等待足够长时间以便让韵律消息发送完成,就无法切换效果 await this.sleepMs(this.DELAY_MS_COMMAND); this.isSceneCadenceBusy = false; } let changed = false; if (this.passthroughMode) { let color3 = color && tinycolor(color).toRgb(); if (!color3) { color3 = tinycolor.fromRatio({ h: hue / this.HUE_MAX, s: saturation / this.SATURATION_MAX, v: value / this.BRIGHTNESS_MAX, }).toRgb(); } else if (needPostProcessColor) { // color3.r = this.ledFilter3040(color3.r); // color3.g = this.ledFilter3040(color3.g); // color3.b = this.ledFilter3040(color3.b); color3.r = parseInt(this.gamma[color3.r] * this.whiteBalance.r, 10); color3.g = parseInt(this.gamma[color3.g] * this.whiteBalance.g, 10); color3.b = parseInt(this.gamma[color3.b] * this.whiteBalance.b, 10); let safeColor = this.ledFilterBurnGreen(color3); color3.r = safeColor.r; color3.g = safeColor.g; color3.b = safeColor.b; } let color3Bg = colorBg && tinycolor(colorBg).toRgb(); if (color3Bg && needPostProcessColor) { // color3Bg.r = this.ledFilter3040(color3Bg.r); // color3Bg.g = this.ledFilter3040(color3Bg.g); // color3Bg.b = this.ledFilter3040(color3Bg.b); color3Bg.r = parseInt(this.gamma[color3Bg.r] * this.whiteBalance.r, 10); color3Bg.g = parseInt(this.gamma[color3Bg.g] * this.whiteBalance.g, 10); color3Bg.b = parseInt(this.gamma[color3Bg.b] * this.whiteBalance.b, 10); let safeColor = this.ledFilterBurnGreen(color3Bg); color3Bg.r = safeColor.r; color3Bg.g = safeColor.g; color3Bg.b = safeColor.b; } let colors3 = []; colors.map((colour, index) => { colors3.push(reserves[index] || 0); // reserve 是固件代码中某个颜色的保留字节(固件代码中每个颜色有 4 个字节) let rgb = tinycolor(colour).toRgb(); if (needPostProcessColor) { // rgb.r = this.ledFilter3040(rgb.r); // rgb.g = this.ledFilter3040(rgb.g); // rgb.b = this.ledFilter3040(rgb.b); rgb.r = parseInt(this.gamma[rgb.r] * this.whiteBalance.r, 10); rgb.g = parseInt(this.gamma[rgb.g] * this.whiteBalance.g, 10); rgb.b = parseInt(this.gamma[rgb.b] * this.whiteBalance.b, 10); let safeColor = this.ledFilterBurnGreen({ ...rgb, scene, }); rgb.r = safeColor.r; rgb.g = safeColor.g; rgb.b = safeColor.b; } colors3.push(rgb.r); colors3.push(rgb.g); colors3.push(rgb.b); }); let preDefinedType = type | 0xf000; for (let mode in this.passthroughMode) { if (this.passthroughMode[mode].includes(preDefinedType)) { if (mode === 'silan') { if (!isEditingCustom) { this.selectNodeToResponseScen