# 脚手架硬件案例

# 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 定义了下面的接口, connectdisconnect, 用户点击 连接断开 的时候分别会调用的接口, 可以看到里面就是标准的 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 里面返回的

积木块测试 02

脚手架还内置两种硬件协议案例

  1. SerialPort 串口协议
  2. Ble 蓝牙协议

其中 串口蓝牙, 是我们封装的协议, WebSocket 是自定义的模板

# 串口协议案例

如果你已经很熟悉串口协议了, 你可以直接跳过, 串口协议是我们封装好的协议, 你可以开箱即用, 引用 SDK 库即可

如果你想先熟悉一下硬件, 可以直接使用我们脚手架内置的串口模板, 附上了 ArduinoMicropython 的固件模板

  • 这个模板是基于回车符作为分隔符, 可以看到下面代码加亮的行, \r\n 是分隔符
  • 固件协议只有简单的两种指令 发送 foobar, 发送 helloworld, 你可以自己试着尝试扩充

# 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 固件烧录
  1. 首先你需要下载一个 Arduino IDE (opens new window) 根据你的平台, 下载一个最新的即可
  2. 打开下载好的 IDE, 把上面的代码拷贝到 IDE 里面 Arduino IDE
  3. 在菜单栏 [工具] -> [开发板] -> 选中你的 Arduino 型号 Arduino IDE board
  4. 然后在端口选中你的板子 Arduino IDE port
  5. 然后回到 IDE, 点击上传, 把代码编译并烧录进你的 Arduino Arduino IDE upload
  6. 等待烧录结束 Arduino IDE upload success
  7. 你可以点击串口调试工具, 测试一下 Arduino IDE SerialPort Monitor
  8. 记得 选为换行符 和波特率 115200 Arduino IDE SerialPort Monitor 02

# 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 的板子有很多种, 这里会细分

  1. Microbit
  2. ESP

# 其他注意事项

  1. 串口传输必须指定一个波特率, 这里模板用了 115200
  2. sendAndWait 由于流协议, 以及串口缓冲区的问题, 实际上每条消息不一定是连贯的, 意思就是, 固件发送的消息, 到了 uCode 这里有可能会拆分成两段, 或者多段 (时序一定是对的, 但是不一定一次事件触发就是一整条消息)
  3. 真正的协议会远比这个模板复杂得多, 这里只是一个实例
  4. 串口还有很多的参数, 例如: 队列是否开启, 缓冲区, 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, // 启用队列时数据发送的间隔
    },
  },
});