Tutorial on Directly Connecting a Robot Controller via TCP Protocol Using a Mini Program

2022-05-23

Since the WeChat mini-program library version 2.18, we can use the TcpSocket interface in the mini-program to communicate with the controller directly via TCP!

If it is inconvenient to view this article on a mobile phone, you can go to blaze.inexbot.com to view the full text!

Quoted Libraries

  • typescript - TypeScript tutorial; TypeScript is a superset of JavaScript;
  • utf-8 - Convert utf8 strings to Buffer;
  • buffer - ;Call the Buffer Api in NodeJs in the front end;
  • crc32 - A very useful Crc32 verification tool
  • Taro - JD's cross-platform development library, using this library to call WeChat's Tcp interface in the encapsulation of Tcp

Encapsulating NexDriod Tcp Library

Message type

Define the type of message received, including the command word "command" and the JSON data "data".

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

Creating connection

According to the Mini Program's wx.createTCPSocket, when calling in Taro, you need to use Taro.createTCPSocket

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

Listening to status

We need to listen to the connection, disconnection, error and other states of Tcp. According to the documentation of the TcpSocket instance, we start listening in the constructor, otherwise there will be errors in creating multiple listeners.

private constructor() {
    // Callback for connection
    this.Tcp.onConnect(() => {
      this.connected = true;
    });
    // Callback for close
    this.Tcp.onClose(() => {
      this.connected = false;
    });
    // Callback for error
    this.Tcp.onError((result: TCPSocket.onError.CallbackResult) => {
      this.Tcp.close();
    });
    // Callback for receiving messages
    this.Tcp.onMessage((result: TCPSocket.onMessage.CallbackResult) => {
      do something ...
    });
    // Callback for stopping listening to the close status
    this.Tcp.offClose(() => {
      console.log("OffClose");
    });
    // Callback for stopping listening to the connection status
    this.Tcp.offConnect(() => {
      console.log("OffConnect");
    });
    // Callback for stopping listening to the error status
    this.Tcp.offError(() => {
      console.log("OffError");
    });
    // Callback for stopping listening to the message status
    this.Tcp.offMessage(() => {
      console.log("OffMessage");
    });
  }

Setting external callback functions

Although we have set the callback functions for listening to various status in the Tcp class according to the small program API, we also need to set the callback functions for various status outside the class.

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;
    }
  }

Then modify the constructor.

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");
  });
}

To be honest, we haven't figured out when those offXXX callbacks are called.

Sending messages

According to the small program API, we need to use the TCPSocket.write interface to send messages.

However, when we send the command and data to the controller, we need to encode them into a Buffer first, so we define an encoding function first.

import crc32 from "crc32";
// This buffer is not the NodeJS built-in buffer library, but a third-party buffer API provided for use in browsers
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;
  }
}

Then we can get the encoded data and send it directly.

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: "" };
}

Heartbeat mechanism

All communication mechanisms require a heartbeat to verify connection availability.

Now we need to consider when to send a heartbeat and when to pause sending a heartbeat.

  1. Start sending after connection;
  2. Stop sending after disconnection;
  3. Pause sending before sending a message. If no new message is sent after 1 second, continue to send a heartbeat.

Define methods for sending heartbeats, stopping sending heartbeats, and restarting sending heartbeats

private heartBeatInterval: NodeJS.Timer | null;
private resetHeartBeatTimer: NodeJS.Timeout | null;
// Send every 1 second
private heartBeat(): void {
  this.heartBeatInterval = setInterval(() => {
    this.sendMessage(0x7266, { time: new Date().getTime() });
  }, 1000);
}
//Stop sending
private stopHeartBeat(): void {
  if (this.heartBeatInterval) {
    clearInterval(this.heartBeatInterval);
    this.heartBeatInterval = null;
  }
}
//Restart sending
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);
}

Then, let's modify the methods for connecting, disconnecting, and sending messages.

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: "" };
}

Receiving messages

Now that we've finished sending messages, let's move on to receiving them!

First of all, the messages sent from the controller are also Buffers, and there may be sticky packet problems, so to solve this problem, we define a Buffer pool, throw all the incoming messages into the pool, and then take out one message at a time from the pool to process it.

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);
  //Throw the messages into the pool
  this.bufferPool = Buffer.concat([this.bufferPool, newBuffer]);
  //Process the message
  this.handleBuffer();
}
private handleBuffer(): void {
    //Process the messages in the pool
}

Then we need to take out one message from the pool, delete it from the pool, and process it. If there are still messages left in the pool after processing, repeat the above steps.

private handleBuffer(): void {
    //Find the header
  const index = this.bufferPool.indexOf("Nf");
  if (index < 0) {
    return;
  }
  //Find the place where the length is defined
  const lengthBuffer = this.bufferPool.slice(index + 2, index + 4);
  const length = lengthBuffer.readUIntBE(0, 2);
  //Cut out the required data
  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);
  //Repeat the above steps
  this.handleBuffer();
}

private decodedMessage(message: Message){
    //Decode the message
}

//Process the decoded message, which is to pass the message to the callback function defined earlier
private handleMessage(message: Message): void {
  this.onMessageCallback(message);
}

Decoding messages

After getting the data Buffer, we need to decode it to get the command and JSON data we need.

This process is actually the reverse of encoding.

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;
}

Example

Now that we've defined the classes for connecting to the controller, sending and receiving data in this small program, it's time to use it.

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});
If there are errors in this article please give us feedback, we value your comments or suggestions.