# 脚手架硬件案例
# WebSocket 协议案例 (TypeScript)
前面已经把 WebSocket 的实例跑起来了, 由于 WebSocket
内置了一个 WebSocket Server
用来模拟硬件
# Websocket-Server
实际上 这个 WebSocket Server 相当简单, 就是收到了 hello
就回复 world
, 收到了 foo
就回复 bar
, 你完全可以按照自己的意思再继续扩展, 或者进一步优化
const { WebSocketServer } = require("ws");
let PORT = 8800;
const wss = new WebSocketServer({
port: PORT,
});
console.log("ws server listen on: %s", PORT);
wss.on("connection", function connection(ws, req) {
console.log("new client connected: %s", req.socket.remoteAddress);
ws.on("message", function message(rawData) {
console.log("received: %s", rawData);
const msg = rawData.toString();
if (msg === "hello") {
ws.send("world");
console.log("reply: %s", "world");
} else if (msg === "foo") {
ws.send("bar");
console.log("reply: %s", "bar");
}
});
});
# DeviceConnectionInterface 硬件连接接口
回到 设备连接这里, websocket
的硬件协议案例源文件在 src/devices/websocket-device.ts
里面
首先 这个 WebsocketDevice
实现了一个 DeviceConnectionInterface
接口
export class WebsocketDevice implements DeviceConnectionInterface {
...
}
这个接口定义了 uCode
调用硬件的接口标准, 具体的接口参数可以查看 API
文档 Interface DeviceConnectionInterface
WebSocketDevice 定义了下面的接口, connect
和 disconnect
, 用户点击 连接 和 断开 的时候分别会调用的接口, 可以看到里面就是标准的 WebSocket 的操作
/**
* 连接设备
* @returns {Promise<void>}
*/
connect(): Promise<void> {
console.log('demo connect device ');
return new Promise((resolve, reject) => {
const ip = 'localhost';
this.ws = new WebSocket(`ws://${ip}:8800`);
this.ws.onopen = () => resolve();
this.ws.onclose = () => {
console.log('ws close');
this.eventbus.dispatchDisconnect();
this.connectStatus = self.UCode.Constant.ConnectStatus.Disconnected;
};
this.ws.onerror = (error) => {
this.eventbus.dispatchDisconnect();
reject();
};
this.ws.onmessage = (ev) => {
this.receiveMsg(ev.data);
};
});
}
disconnect() {
this.ws?.close();
this.eventbus.dispatchDisconnect();
return Promise.resolve();
}
# 收发案例
前面说到, 这个案例, 非常简单, 就是 一发一收, 发 'hello' 就回复 'world', 发 'foo' 就回复 'bar'
该案例简单封装了一个 sendAndWait
方法, 用来模拟同步发送
/**
* 发送并等待, 适合一问一答的协议类型
* @param {string} data
* @param {number} timeout
* @returns {Promise<string>}
*/
sendAndWait(data: string, timeout = 3000) {
return new Promise((resolve, reject) => {
const timeoutDispose = setTimeout(() => {
// 超时处理
disposeObj.dispose?.();
reject(new Error('timeout'));
}, timeout);
const disposeObj = this.onData((evt) => {
// 监听消息会返回一个 dispose
const msg = evt.data;
console.log('receive msg', msg, typeof msg);
clearTimeout(timeoutDispose); // 清空 timeout
disposeObj.dispose?.(); // 收到想要的消息, 清理掉事件
resolve(msg); // 返回消息
});
this.sendMsg(data);
});
}
onData(listener: (data: any) => void) {
this.ws?.addEventListener('message', listener);
return {
dispose: () => this.ws?.removeEventListener('message', listener),
};
}
然后在积木块里面, 直接调用协议里面的 sendAndWait
, 下面这个方法, 在 src/block.ts
里面, 就是这个积木块的 callback
点击积木块的时候, 会判断一下, 硬件设备是否有连接, 如果有连接直接调用 sendAndWait
testReceiveMsg(args: { [key: string]: any }, util: { targetId: string }): Promise<string> {
return new Promise((resolve, reject) => {
const device = self.UCode.extensions.getDevice<WebsocketDevice>(util.targetId);
console.log('Device', device, util);
if (!device?.isConnected()) {
console.log('Device 硬件没有连接');
reject();
} else {
console.log('test-send', args.TEXT);
device
.sendAndWait(args.TEXT)
.then((data) => resolve(data as string))
.catch(reject);
}
});
}
当你输入 foo
的时候, 回复 bar
, 这里都是从 ws-server
里面返回的
脚手架还内置两种硬件协议案例
- SerialPort 串口协议
- Ble 蓝牙协议
其中 串口 和 蓝牙, 是我们封装的协议, WebSocket 是自定义的模板
# 串口协议案例
如果你已经很熟悉串口协议了, 你可以直接跳过, 串口协议是我们封装好的协议, 你可以开箱即用, 引用 SDK 库即可
如果你想先熟悉一下硬件, 可以直接使用我们脚手架内置的串口模板, 附上了 Arduino
和 Micropython
的固件模板
- 这个模板是基于回车符作为分隔符, 可以看到下面代码加亮的行,
\r\n
是分隔符 - 固件协议只有简单的两种指令 发送
foo
回bar
, 发送hello
回world
, 你可以自己试着尝试扩充
# 1. 插件代码
SerialPortProtocol
内置了收发的方法, 因此首要的是继承这个类, 这个类, 集成在我们的 SDK
: @ubtech/ucode-extension-common-sdk
里面
下面的代码都以 TypeScript
为例子
import { CommonProtocols } from "@ubtech/ucode-extension-common-sdk";
import type { HardwareDeviceConstructorArgumentType } from "@ubtech/ucode-extension-common-sdk/types";
const { SerialPortProtocol, getSerialPortDeviceRegister } = CommonProtocols.SerialPort;
继承 SerialPortProtocol
class DemoSerialPortDevice extends SerialPortProtocol {
...
}
SerialPortProtocol
内置了收发的方法 send
, onData
/**
* 数据发送
* @param data 数据
* @param isTopPriority 是否为最高优先级指令。如停止指令,不进入队列,直接发送并清空队列
*/
type send = (data: string | Buffer, isTopPriority = false, encoding?: UCode.DataEncoding) => boolean;
/**
* 监听事件
* @param listener 监听callback
* @returns Disposable
*/
type onData = (listener: (data: string | Buffer) => void): Disposable;
由于协议的收发是异步的, 就是发送和接收不是同步的, 脚手架封装了一个 sendAndWait
用来实现同步收发的例子
看下面高亮的行,就是接收到回车符之后, 把消息提取出来
/**
* 发送并等待, 适合一问一答的协议类型
* @param {string} data
* @param {number} timeout
* @returns {Promise<string>}
*/
sendAndWait(data: string, timeout = 3000) {
return new Promise((resolve, reject) => {
const timeoutDispose = setTimeout(() => {
// 超时处理
dispose.dispose();
reject(new Error('timeout'));
}, timeout);
const dispose = this.onData((data) => {
// 监听消息会返回一个 dispose
const msg = Buffer.from(data).toString();
console.log(msg, msg.length);
if (msg.endsWith('\r\n')) {
// 这里的案例是使用 回车做分隔符
clearTimeout(timeoutDispose); // 清空 timeout
dispose.dispose(); // 收到想要的消息, 清理掉事件
resolve(msg.replace('\r\n', '')); // 返回消息
}
});
this.send(Buffer.from(data));
});
}
Arduino
如果你想继续了解这个协议部分, 下面是 Arduino 固件模板:
任意一款 Arduino
应该都支持
String str = "";
void proceedMsg(String msg) {
if (msg == "hello") {
Serial.println("world");
} else if (msg == "foo") {
Serial.println("bar");
}
}
void setup() {
Serial.begin(115200);
}
void loop() {
if (str != "") {
proceedMsg(str);
str = "";
}
}
void serialEvent() {
while (Serial.available()) {
str = Serial.readStringUntil('\n');
}
}
Arduino 固件烧录
- 首先你需要下载一个 Arduino IDE (opens new window) 根据你的平台, 下载一个最新的即可
- 打开下载好的 IDE, 把上面的代码拷贝到 IDE 里面
- 在菜单栏 [工具] -> [开发板] -> 选中你的 Arduino 型号
- 然后在端口选中你的板子
- 然后回到 IDE, 点击上传, 把代码编译并烧录进你的 Arduino
- 等待烧录结束
- 你可以点击串口调试工具, 测试一下
- 记得 选为换行符 和波特率 115200
# 3. Micropython
这里用的是官方的 Micropython 例子 machine UART (opens new window)
具体每个版本可能会有区别
from machine import UART
uart = UART(1, 115200)
while True:
line = uart.readline()
if line == 'hello':
uart.write('world\r\n')
else if line == 'foo':
uart.write('bar\r\n')
Micropython 固件烧录
由于没有设备, 这里没法写教程, 待补充
由于 Micropython 的板子有很多种, 这里会细分
- Microbit
- ESP
# 其他注意事项
- 串口传输必须指定一个波特率, 这里模板用了
115200
sendAndWait
由于流协议, 以及串口缓冲区的问题, 实际上每条消息不一定是连贯的, 意思就是, 固件发送的消息, 到了uCode
这里有可能会拆分成两段, 或者多段 (时序一定是对的, 但是不一定一次事件触发就是一整条消息)- 真正的协议会远比这个模板复杂得多, 这里只是一个实例
- 串口还有很多的参数, 例如: 队列是否开启, 缓冲区, pid, vid 的过滤筛选, 名字自定义等, 这个会在后面开发指南里面 会详细解释
# 蓝牙协议案例
如果你已经很熟悉蓝牙协议了, 你可以直接跳过, 蓝牙协议是我们封装好的协议, 你可以开箱即用, 引用 SDK 库即可
如果你想先熟悉一下硬件, 可以直接使用我们脚手架内置的蓝牙模板, 如果你的硬件是蓝牙透传串口, 可以参考使用上面的串口固件, 如果不是的话, 可能这里会比较复杂, 因为每个硬件的蓝牙传输接口不太一样, 所以没有办法保证
蓝牙和串口绝大部分情况, 两个协议的使用是类似的, 除了蓝牙的 onData
数据不太一样, 因为蓝牙是有多个特征值, 因此数据体的格式是多了一个 uuid
type CommonBleDataType = {
uuid: BluetoothCharacteristicUUID;
data: Buffer;
};
/**
* 蓝牙接收的数据体
* @typedef {Object} CommonBleDataType
* @property {string} uuid - 蓝牙 read 特征值的 uuid
* @property {Buffer} data - 数据
*/
/**
* 当接收到消息后, 会调用该方法
* @param {CommonBleDataType} data
*/
receiveMsg(data: CommonProtocols.BLE.CommonBleDataType) {
console.log(data.uuid, data.data);
}
蓝牙的参数也有很多, 这里和 串口不太一样, service ID
, characteristics
, 一定要对上, 才能连接成功
export const bleRegister = getUCodeBleDeviceRegister({
DeviceConnection: DemoWebbleDevice,
Options: {
services: {
serviceUUID: "55425401-ff00-1000-8000-00805f9b34fb", // ble 的服务 id
characteristics: [
{
name: "read",
uuid: "55425403-ff00-1000-8000-00805f9b34fb", // notify 的特征id
readable: true,
},
{
name: "write",
uuid: "55425402-ff00-1000-8000-00805f9b34fb", // 写数据的特征id
},
],
},
defaultWriteCharacteristicUUID: "55425402-ff00-1000-8000-00805f9b34fb", // 默认写的特征id
filters: [{ namePrefix: "uKit" }], // 过滤字符,配置后发现设备时将只显示该字符开头的蓝牙设备
queueOptions: {
enable: true, // 数据发送是否启用队列, 可选
interval: 150, // 启用队列时数据发送的间隔
},
},
});