我第一次尝试TensorFlow.js用于微控制器时,我对通过蓝牙转移到Arduino的机器学习模型的事实感到非常兴奋。网站上启用了几秒钟的手势控制!我的兴奋迅速变成了好奇心。它如何实际工作?
为了更好地理解它,我花了时间跳入开源脚本tf4micro-motion-kit.js,该脚本被用作Tiny Motion Trainer项目的一部分。在这篇文章中,我将解释如何通过蓝牙从浏览器转移到Arduino。
如果您对如何使用该技术来创建手势控制的Web应用程序感兴趣,请查看我写的关于此的previous blog post。
蓝牙和gatt
本博客其余部分要理解的核心概念是蓝牙低能(BLE)设备使用所谓的 Services 和特征来回传输数据 ,这是Gatt的一部分(通用属性配置文件)。 Gatt依靠ATT(属性协议)数据协议,该协议使用16位或128位UUID(普遍唯一标识符)来存储服务和特征。
您可以将服务和特性视为可以保存有关设备功能的数据的变量,可以从其他应用程序或设备中读取或写入或写入。
例如,您可以在许多BLE设备上找到的服务称为电池服务。它包含多个特性,例如电池电量和电池电源状态特性。当您购买现成的设备时,这些服务和特征已经为您设置了,您也许可以对他们进行一些阅读或编写请求。
在Arduino上,您可以创建自己的服务和特征。当涉及到能够将机器学习模型上传到董事会时,您可能需要具有具有特征的通用服务,例如传输状态,文件的总长度,培训模型的设置等等。 /p>
这些服务和特征是什么样的?如上所述,它们要么是16位或128位UUID,因此服务ID可能是:
"81c30e5c-0000-4f7d-a886-de3e90749161"
在此示例中,您有32个十六进制值。十六进制值表示为2^4,因此可以保持0或1的值4位。结果,32 x 4 = 128位。
当在Arduino上运行的程序中创建这些服务和特征时,可以使用这些UUID从浏览器中读取它们。
如果您想查看如何创建所有服务和特征,请查看the Arduino upload program。
现在,让我们继续前进。
连接到董事会
首先,董事会必须以某种方式连接到浏览器才能发送或接收数据。在此项目中,该连接是通过蓝牙建立的。使用Web Bluetooth API,您需要从navigator.bluetooth
对象上使用requestDevice
方法开始。如果您喜欢的话,您可以通过一些过滤器传递,以便浏览器仅向您显示您在启动连接时感兴趣的设备。
例如,在tf4micro-motion-kit.js script
中,在Arduino上运行的程序中创建的主服务UUID用作识别设备的过滤器。
const SERVICE_UUID = "81c30e5c-0000-4f7d-a886-de3e90749161";
const device = await navigator.bluetooth.requestDevice({
filters: [{ services: [SERVICE_UUID] }],
});
上面的代码返回承诺,下一步是连接到设备:
const server = await device.gatt.connect();
如果此步骤成功,您可以开始将这些服务和特征的引用存储在变量中,以便以后可以使用它们。例如,使用以下行来存储对主服务的引用:
const service = await server.getPrimaryService(SERVICE_UUID);
,您可以使用以下代码定义不同的特征,例如,使用与program uploaded to the Arduino中定义的ID相同的ID来定义文件长度特征:
const FILE_LENGTH_UUID = "bf88b656-3001-4a61-86e0-769c741026c0";
const fileLengthCharacteristic = await service.getCharacteristic(FILE_LENGTH_UUID);
上面的特征用于参考字节中的机器学习模型的文件大小,因此,当将其传输到arduino时,您可以检查何时传输整个文件。
调用了connect()
方法并将所有引用对您感兴趣的服务和特征的所有参考,董事会已连接到浏览器,您可以继续下一步。
加载机器学习模型
该模型作为.tflite
文件存储在您的应用程序文件夹中。首先,需要使用fetch
API或XMLHTTPRequest加载它。其次,响应需要用作数组缓冲区。
const loadFile = (modelUrl) => {
return new Promise((resolve, reject) => {
const oReq = new XMLHttpRequest();
oReq.open("GET", modelUrl, true);
oReq.responseType = "arraybuffer";
oReq.onload = function () {
const arrayBuffer = oReq.response;
if (arrayBuffer) {
resolve(arrayBuffer);
} else {
reject(new Error("Failed fetching arrayBuffer"));
}
};
oReq.onerror = reject;
oReq.send(null);
});
};
加载文件后,下一步准备要传输的文件。
拆分机器学习模型
Arduino Nano 33 BLE Sense围绕Nordic Semiconductor芯片设计,该芯片的最大传输单元(MTU)为23个字节,这代表了一次可以发送的最大数据包大小。根据this resource的说法,此大小的最大数据吞吐量为128 kbps,因此您需要将机器学习模型分为此大小的数据包,以便能够将其传输到Arduino。
您还需要确保文件的总尺寸对于微控制器而言不太大。
为此,您首先阅读从Arduino中拥有最大文件大小值的特征。
let maximumLengthValue = await fileMaximumLengthCharacteristic.readValue();
let maximumLengthArray = new Uint32Array(maximumLengthValue.buffer);
let maximumLength = maximumLengthArray[0];
// The fileBuffer variable is the model buffer you loaded in the code sample above.
if (fileBuffer.byteLength > maximumLength) {
console.log(
`File length is too long: ${fileContents.byteLength} bytes but maximum is ${maximumLength}.`
);
return;
}
如果文件不长,则可以开始将其分开并传输它。下面的代码详细说明了不同的步骤。
// 1. Start by writing the length value of the model to the file length characteristic on the Arduino.
let fileLengthArray = Int32Array.of(fileContents.byteLength);
await fileLengthCharacteristic.writeValue(fileLengthArray);
// 2. Pass the file buffer and the starting index to the function
sendFileBlock(fileContents, 0)
async function sendFileBlock(fileContents, bytesAlreadySent) {
// 3. Calculate the remaining bytes.
let bytesRemaining = fileContents.byteLength - bytesAlreadySent;
const MAX_BLOCK_LENGTH = 128;
const blockLength = Math.min(bytesRemaining, MAX_BLOCK_LENGTH);
// 4. Transform the content into a Uint8array
const blockView = new Uint8Array(fileContents, bytesAlreadySent, blockLength);
// 5. Write the value to the characteristic on the Arduino
return fileBlockCharacteristic
.writeValue(blockView)
.then((_) => {
// 6. Remove the length of the block already sent from the size of bytes remaining
bytesRemaining -= blockLength;
// 7. If the file has not finished transferring, repeat
if (bytesRemaining > 0 && isFileTransferInProgress) {
console.log(`File block written - ${bytesRemaining} bytes remaining`);
bytesAlreadySent += blockLength;
return sendFileBlock(fileContents, bytesAlreadySent);
}
})
.catch((error) => {
console.log(error);
console.log(
`File block write error with ${bytesRemaining} bytes remaining, see console`
);
});
}
此代码运行后,它将通过数据包传输机器学习模型,直到没有字节为止。即使有效,您也可以添加一个额外的步骤,以确保收到的文件与传输的文件相同。
校验和
可以使用校验和检测文件在浏览器和Arduino之间传输时可能引入的错误。 tf4micro-motion-kit.js
库使用CRC32函数来验证数据完整性。
它为传输的数据计算了32位cyclic redundancy checksum,并在接收端重复此计算。如果两个值不匹配,则数据已经以某种方式损坏了,并且错误消息可能表明传输失败。
我不会深入研究CRC32函数的实现方式,但您可以在the repository中查看源。
Arduino如何运行模型
一旦Arduino收到完整的文件并验证了数据的完整性,设备上的代码使用IMU.readAcceleration
方法从内置的传感器中获取实时输入数据,将数据转换为张量,并提供馈送。它可以预测手势。此代码是用C ++编写的,是您需要上传到Arduino的程序的一部分,因此我不会详细介绍,但是您可以查看this repository中的代码。
结论
通过蓝牙将机器学习模型从浏览器转移到Arduino,以5个步骤完成,包括使用Web蓝牙API将板连接到浏览器,将模型分为块,将它们一个接一个地转移到设备上,运行,运行它具有来自Arduino的内置传感器的实时数据,并在检测到手势后通知前端。
如果您想更深入地研究为Arduino草图编写的整个代码,则可以在this repository中找到它,如果您想查看编写的代码以创建模型Tiny Motion Trainer project is also open-source!
如果您对这项技术建立了任何有趣的东西,请告诉我们!
与此同时,您可以在以下平台上使用Stripe开发人员更新:
ð£关注@StripeDev和our team上的Twitter
ðiscribe我们的Youtube channel
−加入官方Discord server
ð§注册Dev Digest
关于作者
Charlie Gerard是Stripe,creative technologist和Google Developer Expert的开发人员倡导者。她喜欢研究和experimenting with technologies。当她不编码时,她喜欢在户外度过时光,尝试新的啤酒和阅读。