Mini Program Tutorial: Direct TCP Connection to the Robot Controller!

Since WeChat opened up the TcpSocket interface in mini program library version 2.18, we can now use a mini program to communicate directly with the controller over TCP!

5/23/2022

Since WeChat opened up the TcpSocket interface in mini program library version 2.18, we can now use a mini program to communicate directly with the controller over TCP!

If this article is inconvenient to read on a mobile device, you can view the full version at blaze.inexbot.com!

Referenced Libraries

  • typescript - TypeScript tutorial; TypeScript is a superset of JavaScript;
  • utf-8 - Converts UTF-8 strings to Buffer;
  • buffer - Calls the Node.js Buffer API on the frontend;
  • crc32 - A handy CRC32 checksum utility
  • Taro - JD's cross-platform development library; used in the TCP wrapper to call WeChat's TCP interface

Wrapping the NexDroid TCP Library

Message Types

Define the type of received messages, which contains the command word command and JSON data data.

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

Creating a Connection

In a mini program, wx.createTCPSocket is used; in Taro, you call Taro.createTCPSocket instead.

import Taro, { TCPSocket } from "@tarojs/taro";
export default class Tcp {
  private Tcp: TcpSocket = Taro.createTCPSocket();
  private connected: boolean;
  // Singleton pattern
  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 for Status

We need to listen for TCP connection, disconnection, error, and other statuses. According to the TcpSocket instance documentation, we start listening in the constructor; otherwise, the listener may be created multiple times and cause errors.

private constructor() {
// Connect callback
this.Tcp.onConnect(() => {
  this.connected = true;
});
// Close callback
this.Tcp.onClose(() => {
  this.connected = false;
});
// Error callback
this.Tcp.onError((result: TCPSocket.onError.CallbackResult) => {
  this.Tcp.close();
});
// Message received callback
this.Tcp.onMessage((result: TCPSocket.onMessage.CallbackResult) => {
  do something ...
});
// Stop listening for the close event
this.Tcp.offClose(() => {
  console.log("OffClose");
});
// Stop listening for the connect event
this.Tcp.offConnect(() => {
  console.log("OffConnect");
});
// Stop listening for the error event
this.Tcp.offError(() => {
  console.log("OffError");
});
// Stop listening for the message event
this.Tcp.offMessage(() => {
  console.log("OffMessage");
});
  }

Setting External Callbacks

Although we have set up the callbacks for each status listener of the mini program API inside the Tcp class, we also need to set callbacks for each 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 still haven't figured out when those offXXX callbacks are actually invoked — we've never seen them called.

Sending Messages

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

However, before sending the command word and data to the controller, we need to encode them into a Buffer, so let's define an encoding function first.

import crc32 from "crc32";
// This buffer is not Node.js's built-in buffer library, but a third-party buffer API provided for 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

Every communication mechanism needs a heartbeat to verify that the connection is still alive.

Now we need to consider when to send heartbeats and when to pause them.

  1. Start sending after connecting;
  2. Stop sending when disconnected;
  3. Pause before sending a message; if no new message is sent within 1 second after the last one, resume heartbeats.

Define methods for starting, stopping, and restarting heartbeats.

private heartBeatInterval: NodeJS.Timer | null;
private resetHeartBeatTimer: NodeJS.Timeout | null;
// Send once every 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 modify the connect, disconnect, and send-message methods.

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

Sending messages is done!

Now let's handle receiving messages.

Messages sent from the controller are also Buffers, and there may be sticky-packet issues. To solve this, we define a Buffer pool: all incoming messages are first thrown into the pool, then taken out one by one for processing.

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

Next, take one message out of the pool, remove 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;
  }
  // Locate where the length is defined
  const lengthBuffer = this.bufferPool.slice(index + 2, index + 4);
  const length = lengthBuffer.readUIntBE(0, 2);
  // Extract 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
  this.handleBuffer();
}

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

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

Decoding Messages

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

This process is essentially 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;
}

Usage Example

Now that we have defined the class for connecting to the controller and sending/receiving data in the mini 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});

文档反馈

Mini Program Tutorial: Direct TCP Connection to the Robot Controller! - iNexBot