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.
- Start sending after connecting;
- Stop sending when disconnected;
- 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});





