解析noble-winrt

24 阅读2分钟

背景

在js项目中需要操作蓝牙设备。找到一个window上使用的js类库(noble-winrt - npm). 今天就来扒一扒noble-winrt的实现方式,以备查看。

如何在js中启动BleServe.exe

  • 使用spawn(BLE_SERVER_EXE, [""]),启动进程,并传递初始参数。

let BLE_SERVER_EXE = path.resolve(__dirname, "prebuilt", "BLEServer.exe");
console.log(process.env.EE_SERVER_ENV);
if (process.env.EE_SERVER_ENV === "local") {
  // electron开发模式下
  BLE_SERVER_EXE = path.resolve(__dirname, "prebuilt", "BLEServer.exe");
} else {
  // electron发布模式下
  const CURRENT_EXE_PATH = process.execPath;
  const directoryPath = path.dirname(CURRENT_EXE_PATH);
  BLE_SERVER_EXE = path.join(directoryPath, "resources", "prebuilt", "BLEServer.exe");
}


  init() {
    this._deviceMap = {};
    this._requestId = 0;
    this._requests = {};
    this._subscriptions = {};
    this._bleServer = spawn(BLE_SERVER_EXE, [""]);
    this._bleServer.stdout.pipe(new nativeMessage.Input()).on("data", (data) => {
      this._processMessage(data);
    });
    this._bleServer.stderr.on("data", (data) => {
      console.error("BLEServer:", data);
    });
    this._bleServer.on("close", (code) => {
      this.state = "poweredOff";
      this.emit("stateChange", this.state);
    });
  }
  

BleServe.exe

  • 需要依赖BLE_SERVER_EXE,具体路径在noble-winrt-xd\prebuilt\BLEServer.exe中。

  • BLEServer.exe是一个控制台程序,启动后会在控制台输入窗口输出,也可以在控制台窗口输入命令。

  • 输入的格式是nativeMessage.Input()。

  • nativeMessage.Input() 是 chrome-native-messaging 库中的一个类,用于处理 Chrome Native Messaging 协议的输入消息。这个库通常用于在 Chrome 扩展或 Electron 应用中与本地应用程序进行通信。nativeMessage.Input() 会解析从标准输入(stdin)接收到的数据,并将其转换为 JavaScript 对象。

接收BLEServer.exe的控制台输出

- 精简的代码
    // this._bleServer.stdout.pipe(new nativeMessage.Input()).on("data", (data) => {
    //   this._processMessage(data);
    // });
    - 为方便知道原理,增加地址打印
    this._bleServer.stdout.on("data", (data) => {
      const stringRecv = data.toString("hex");
      console.log("BLEServer:", stringRecv);
      const input = new nativeMessage.Input();
      input.write(data); // 假设 nativeMessage.Input 有 write 方法
      input.on("data", (parsedData) => {
        console.log(`BLEServer nativeMessage = ${JSON.stringify(parsedData)}`);
        this._processMessage(parsedData);
      });
    });

比如收到数据 BLEServer: 110000007b225f74797065223a225374617274227d,转为nativeMessage就是{"_type":"Start"}.

image.png

  • BLEServer是c代码,处理速度比较快;所以会出现多条命令粘连的情况。使用nativeMessage.Input会把命令格式化单个的json命令,还能保证数据不会丢失和不完整。 1744974233158.png

发送数据到BleServe.ext

  _sendMessage(message) {
    debug("out:", message);
    const dataBuf = Buffer.from(JSON.stringify(message), "utf-8");
    const lenBuf = Buffer.alloc(4);
    lenBuf.writeInt32LE(dataBuf.length, 0);
    this._bleServer.stdin.write(lenBuf);
    this._bleServer.stdin.write(dataBuf);
  }
  • 注意message是json对象,先对它进行序列化,转为字符串。然后获取字符串的长度,小端模式写入lenBuf。
  • 写入数据到控制台,先写数据体长度,再写数据体。

异步转为同步方式

  • 比如方法notify用于接收notify通知类型的数据。
  • 通过requestId这个关键key保存{ resolve, reject }回调方法。
  • 当收到通知时,通过this._requests[message._id]获取到回调方法。

  notify(address, service, characteristic, notify) {
    this._sendRequest({
      cmd: notify ? "subscribe" : "unsubscribe",
      device: this._deviceMap[address],
      service: toWindowsUuid(service),
      characteristic: toWindowsUuid(characteristic),
    })
      .then((result) => {
        if (notify) {
          this._subscriptions[result] = { address, service, characteristic };
        } else {
          // TODO - remove from subscriptions
        }
        this.emit("notify", address, service, characteristic, notify);
      })
      .catch((err) => this.emit("notify", address, service, characteristic, err));
  }
  
  _sendRequest(message) {
    return new Promise((resolve, reject) => {
      const requestId = this._requestId++;
      this._requests[requestId] = { resolve, reject };
      this._sendMessage(Object.assign({}, message, { _id: requestId }));
    });
  }
  
     case "response":
        if (this._requests[message._id]) {
          if (message.error) {
            this._requests[message._id].reject(new Error(message.error));
          } else {
            this._requests[message._id].resolve(message.result);
          }
          delete this._requests[message._id];
        }
        break;