小程序TCP直连机器人控制器教程!

2022-05-23

于微信在小程序库版本 2.18 后开放了 TcpSocket 接口,所以我们可以使用小程序直接与控制器进行 Tcp 通讯了!

本篇文章如果在手机端查看不便,可以到 blaze.inexbot.com 查看全篇哦!

引用库

  • typescript - TypeScript 教程 TypeScript 是 JavaScript 的一个超集;
  • utf-8 - 将 utf8 字符串转换为 Buffer;
  • buffer - 在前端调用 NodeJs 中的 Buffer Api;
  • crc32 - 很好用的 Crc32 校验工具
  • Taro - JD 的跨端开发库,在 Tcp 的封装中使用该库调用微信的 Tcp 接口

封装 NexDriod Tcp 库

消息类型

定义消息接收的类型,包含命令字 command 和 JSON 数据 data。

export interface Message {
  command: number;
  data: Object;
}

创建连接

根据小程序的 wx.createTCPSocket,在 Taro 中调用则需要用 Taro.createTCPSocket

import Taro, { TCPSocket } from "@tarojs/taro";
export default class Tcp {
  private Tcp: TcpSocket = Taro.createTCPSocket();
  private connected: boolean;
  // 单例模式
  static instance: Tcp;
  static getInstance(): Tcp {
    if (!this.instance) {
      this.instance = new Tcp();
    }
    return this.instance;
  }
  // 连接
  public connect(ip: string, port: number): void {
    this.Tcp.connect({ address: ip, port: port });
  }
}

监听状态

我们需要监听 Tcp 的连接、断开、出错等状态,根据 TcpSocket 实例的文档,我们在构造函数中开始监听,否则会出现多次创建监听的错误。

private constructor() {
    // 连接上的回调
    this.Tcp.onConnect(() => {
      this.connected = true;
    });
    // 关闭的回调
    this.Tcp.onClose(() => {
      this.connected = false;
    });
    // 出错的回调
    this.Tcp.onError((result: TCPSocket.onError.CallbackResult) => {
      this.Tcp.close();
    });
    // 收到消息的回调
    this.Tcp.onMessage((result: TCPSocket.onMessage.CallbackResult) => {
      do something ...
    });
    // 停止监听关闭状态的回调
    this.Tcp.offClose(() => {
      console.log("OffClose");
    });
    // 停止监听连接状态的回调
    this.Tcp.offConnect(() => {
      console.log("OffConnect");
    });
    // 停止监听报错状态的回调
    this.Tcp.offError(() => {
      console.log("OffError");
    });
    // 停止监听消息状态的回调
    this.Tcp.offMessage(() => {
      console.log("OffMessage");
    });
  }

设置外部的回调函数

虽然在 Tcp 类里面我们设置好了小程序 API 中各个状态监听的回调函数,但是我们在类外面也需要设置各个状态的回调函数。

private onMessageCallback: Function;
private onConnectedCallback: Function;
private onCloseCallback: Function;
private onErrorCallback: Function;

public setCallback(
    onMessageCallback: Function,
    onConnectedCallback?,
    onCloseCallback?,
    onErrorCallback?
  ): void {
    this.onMessageCallback = onMessageCallback;
    if (onConnectedCallback) {
      this.onConnectedCallback = onConnectedCallback;
    }
    if (onCloseCallback) {
      this.onCloseCallback = onCloseCallback;
    }
    if (onErrorCallback) {
      this.onErrorCallback = onErrorCallback;
    }
  }

然后修改一下构造函数。

private constructor() {
  this.Tcp.onConnect(() => {
    if (this.onConnectedCallback) {
      this.onConnectedCallback();
    }
    this.connected = true;
  });
  this.Tcp.onClose(() => {
    this.connected = false;
    this.onCloseCallback();
  });
  this.Tcp.onError((result: TCPSocket.onError.CallbackResult) => {
    if (this.onErrorCallback) {
      this.onErrorCallback(result);
    }
    this.Tcp.close();
  });
  this.Tcp.onMessage((result: TCPSocket.onMessage.CallbackResult) => {
    this.receiveBuffer(result.message);
  });
  this.Tcp.offClose(() => {
    console.log("OffClose");
  });
  this.Tcp.offConnect(() => {
    console.log("OffConnect");
  });
  this.Tcp.offError(() => {
    console.log("OffError");
  });
  this.Tcp.offMessage(() => {
    console.log("OffMessage");
  });
}

说实在的,那几个 offXXX 回调到底啥时候被调用咱也没搞明白,没见被调用过。

发消息

根据小程序的 API,我们发送消息需要使用 TCPSocket.write 接口.

但是我们将命令字、数据发送给控制器的时候是需要先编码为 Buffer 的,所以先定义编码函数。

import crc32 from "crc32";
// 这个buffer不是NodeJS自带的buffer库,而是引用的第三方提供给浏览器使用的buffer api
import Bf from "buffer/index";
const Buffer = Bf.Buffer;

private encodeMessage(command: number, msg: Object): Bf.Buffer | null {
  try {
    const dataString = JSON.stringify(msg);
    const dataBuffer = Buffer.from(
      new Uint8Array(utf8.setBytesFromString(dataString))
    );
    const dataLength = msg ? dataBuffer.byteLength : 0;
    const headBuffer = Buffer.from([0x4e, 0x66]);
    let lengthBuffer = Buffer.alloc(2);
    lengthBuffer.writeIntBE(dataLength, 0, 2);
    let commandBuffer = Buffer.alloc(2);
    commandBuffer.writeUIntBE(command, 0, 2);
    const toCrc32 = Buffer.concat([lengthBuffer, commandBuffer, dataBuffer]);
    const crc32Buffer: Buffer = crc32(toCrc32);
    const message = Buffer.concat([
      headBuffer,
      lengthBuffer,
      commandBuffer,
      dataBuffer,
      crc32Buffer,
    ]);
    return message;
  } catch (err) {
    console.error(err);
    return null;
  }
}

然后我们就可以拿到编码好的数据,直接发送即可。

public sendMessage(command, msg: Object) {
  if (!this.connected) {
    return { result: false, errMsg: "noConnect" };
  } else {
    const message = this.encodeMessage(command, msg);
    if (message) {
      this.Tcp.write(message);
    }
  }
  return { result: true, errMsg: "" };
}

心跳机制

所有的通讯机制都需要心跳来验证连接可用性。

现在需要考虑一下什么时候要发心跳,什么时候要暂停发心跳。

  1. 连接后开始发;
  2. 断开连接停止发;
  3. 发送消息时先暂停发,等发送消息后 1 秒还没有发新的消息,则继续发心跳。

定义发心跳、停止发心跳、重新开始发心跳的方法

private heartBeatInterval: NodeJS.Timer | null;
private resetHeartBeatTimer: NodeJS.Timeout | null;
// 每1秒发一次
private heartBeat(): void {
  this.heartBeatInterval = setInterval(() => {
    this.sendMessage(0x7266, { time: new Date().getTime() });
  }, 1000);
}
//停止发送
private stopHeartBeat(): void {
  if (this.heartBeatInterval) {
    clearInterval(this.heartBeatInterval);
    this.heartBeatInterval = null;
  }
}
//重新开始发
private resetHeartBeat(): void {
  if (this.heartBeatInterval) {
    this.stopHeartBeat();
  }
  if (this.resetHeartBeatTimer) {
    clearTimeout(this.resetHeartBeatTimer);
    this.resetHeartBeatTimer = null;
  }
  this.resetHeartBeatTimer = setTimeout(() => {
    this.heartBeat();
    this.resetHeartBeatTimer = null;
  }, 1000);
}

然后改造一下连接后、断开连接、发送消息的方法

private constructor(){
  this.Tcp.onConnect(() => {
  if (this.onConnectedCallback) {
      this.onConnectedCallback();
  }
  this.connected = true;
  this.heartBeat();
  });
  this.Tcp.onClose(() => {
  this.stopHeartBeat();
  this.connected = false;
  this.onCloseCallback();
  });
}
public sendMessage(command, msg: Object) {
  this.stopHeartBeat();
  if (!this.connected) {
    return { result: false, errMsg: "noConnect" };
  } else {
    const message = this.encodeMessage(command, msg);
    if (message) {
      this.Tcp.write(message);
    }
  }
  this.resetHeartBeat();
  return { result: true, errMsg: "" };
}

接收消息

现在发送消息的事情已经做完啦!

下面开始搞接收消息。

首先从控制器发过来的消息也都是 Buffer,而且可能会有黏包问题,所以为了解决这个问题,我们定义一个 Buffer 池,发来的消息先全部扔到池子里,然后再从池子里拿出一条一条消息来处理。

private bufferPool: Bf.Buffer = Buffer.alloc(0);

private constructor(){
  this.Tcp.onMessage((result: TCPSocket.onMessage.CallbackResult) => {
    this.receiveBuffer(result.message);
  });
}

private receiveBuffer(buffer: ArrayBuffer): void {
  const newBuffer = Buffer.from(buffer);
  //把消息扔到池子里
  this.bufferPool = Buffer.concat([this.bufferPool, newBuffer]);
  //处理消息
  this.handleBuffer();
}
private handleBuffer(): void {
    //处理池子里的消息
}

那么下面需要把池子里的消息拿出来一条,在池子里删了这一条然后处理这一条,如果处理完了池子里还有剩下的那就继续重复上面的步骤。

private handleBuffer(): void {
    //找到头
  const index = this.bufferPool.indexOf("Nf");
  if (index < 0) {
    return;
  }
  //找到定义长度的地方
  const lengthBuffer = this.bufferPool.slice(index + 2, index + 4);
  const length = lengthBuffer.readUIntBE(0, 2);
  //裁剪出需要的数据
  const buffer = this.bufferPool.slice(index, index + 2 + 2 + 2 + length + 4);
  if (buffer.length < index + 2 + 2 + 2 + length + 4) {
    return;
  }
  this.bufferPool = this.bufferPool.slice(index + 2 + 2 + 2 + length + 4);
  const decodedMessage: Message = this.decodeMessage(buffer);
  this.handleMessage(decodedMessage);
  //重复上面的工作
  this.handleBuffer();
}

private decodedMessage(message: Message){
    //解码消息
}

//处理解码完的消息,也就是把消息传递给前面定义的回调函数
private handleMessage(message: Message): void {
  this.onMessageCallback(message);
}

解码消息

拿到数据 Buffer 后需要解码才能拿到我们需要的命令字和 JSON 数据。

这个过程其实也就是编码的逆过程。

private decodeMessage(buffer: Bf.Buffer): Message {
  const commandBuffer = buffer.slice(4, 6);
  const dataBuffer = buffer.slice(6, buffer.length - 4);
  const command = commandBuffer.readUIntBE(0, 2);
  const dataStr = dataBuffer.toString();
  const data = dataStr ? JSON.parse(dataStr) : {};
  const message: Message = {
    command: command,
    data: data,
  };
  return message;
}

用例

现在我们已经定义好这个小程序中连接控制器并发送、接收数据的类了。那么该使用它了。

import Tcp,{ Message } from "xxxx";
import { TCPSocket } form "@tarojs/taro";

const tcp = Tcp.getInstance();

function onConnected(){
    console.log("yes!");
}

function onClose(){
    console.log("no!");
}

function onError(result: TCPSocket.onError.CallbackResult){
    console.log(result.errMsg);
}

function onMessage(message: Message){
    console.log(message.command,message.data);
}

tcp.setCallback(onMessage, onConnected, onClose, onError);

tcp.connect("192.168.1.13",6001);

tcp.sendMessage(0x2002,{"robot":1});
如果本文有错误请向我们反馈,我们很珍惜您的意见或建议。