I bought a FlyingBear Ghost 5 with integrated WiFi module, but I discover that there is no web interface. I don’t know why they can’t add a basic user interface. Then I went to see the web interface the high-end Makerbase cards and realized that it was better they didn’t develop it.
My solution was to modify the firmware to support the Web Socket and develop the Web interface. This feature is compatible with all Makerbase cards that have an MKS WiFi module.
The result of BeePrint Web interface is in this screenshot.
I also decided to explain all the development phases of the project and in this article here I would like to expose the hardware infrastructure of the MKS WiFi card that interfaces with the Makerbase Robin Nano.
In this article I explain how the Makerbase develop the communication protocol via TCP messages and a little introduction to MKS WiFi plugin for Cura.
Introduction
My intent is to add the basic features of Octoprint that I have on my AnetA8.
But the firmware of the original MKS WiFi module use UDP/TCP protocol and can’t be used in a normal web interface without trick or experimental features.
So for fist I’m going to add WebSocket protocol, and I choiche the same library of this tutorial “WebSocket on Arduino, esp8266 and esp32“.
As for TCP the Web Socket work with asynchronous data packet, and I wrap the message management to use the same protocol with WebSocket and TCP at the same time.
MKS WiFi protocol
MKS give a rough document about his protocol, the basic information are this.
Upload Gcode commands
Type = 0x01
Data=$Gcode Comand String + ”\r\n
”
Reply = ok\r\n
(Every command will be replied with “ok”, and also addition necessary information.)
There also some customize commands of mks wifi, which maybe not the command commands with Marlin:
M20 xxx:Get the printer file list, and the Host MCU will return the gcode files and folders under the file path of the “xxx” layer (the tentative character string does not exceed 1024 bytes). If it is a folder, it will automatically add the .DIR suffix. The following format is returned:
Begin file list
abc.gcode
123.gcode
temp.DIR
M23 xxx.gcode:select the gcode file
M24:If printing has not started, this command is to start printing the file, if printing is suspended, this command is to resume printing
M25: pause printing
M26:stop printing
M27:To get the printing progress, Host MCU would reply with this format: M27 30\r\n
(30%)
M991:like M105, get the temperature. Host MCU would reply as this format:T:%d /%d B:%d /%d T0:%d /%d T1:%d /%d @:0 B@:0\r\n
M992:Get the time has print, Host MCU would reply as this format:M992 10:30:20\r\n
M994: Get the name and size of the file being printing, Host MCU would reply as this format: M994 abc.gcode;size
(size means the actually size in bytes of file)
M997:Get the current printer state, Host MCU would reply according different states:
IDLE: “M997 IDLE\r\n”
PRINTING: “M997 PRINTING\r\n”
PAUSED: “M997 PAUSE\r\n”
M998: switch the filesystem to USB or Sdcard
M115: Get the firmware information of mainboard, Host MCU would reply in this format: FIRMWARE_NAME:MKS Robin Nano V2.1.0\r\n
Reply: according to above
Upload file first fragment
Type = 0x02
Data:
Segment | Offset (bytes) | Length (bytes) | Definition |
fileNameLen | 0 | 1 | The length of filename to be transferred, no more than 255 |
fileLen | 1 | 4 | The length of file to be transferred, no more than 2^32 |
fileName | 5 | fileNameLen | The name string of file to be transferred |
Reply: no
Upload file data fragments
Type = 0x03
Data:
Segment | Offset (bytes) | Length (bytes) | Definition |
fragment | 0 | 4 | The file fragment number, begin from 0. 0~14th bits indicate the fragment number, the 15th bit indicates whether it is the last fragment. |
fileData | 4 | Length of fragment file data | The raw file data of this fragment. The length should be the file data actually read out. |
Reply: no
Upload list of searched WiFi hotspot.
Type = 0x04
Data:
Segment | Offset (bytes) | Length (bytes) | Definition |
hot spotNum | 0 | 1 | Number of scanned WiFi hotspot, no more than 20 |
hot spot Len1 | 1 | 1 | Length of 1st hot spot name, no more than 32 |
hot spot Str1 | 2 | hot spot Len1 | 1st Hotspot Name String |
hot spot Rssi1 | hot spot Len1+2 | 1 | 1st hot spot signal Strength, It’s a negative number, the higher it is, the higher the intensity. |
hot spot Len2 | hot spot Len1+3 | 1 | Length of 2nd hot spot name, no more than 32 |
hot spot Str2 | hot spot Len1+4 | hot spot Len2 | 2nd Hotspot Name String |
hot spot Rssi2 | hot spot Len1+hot spot Len2+4 | 1 | 2nd hot spot signal Strength, It’s a negative number, the higher it is, the higher the intensity. |
hot spot LenN | hot spot Len1+hot spot Len2+……+hot spot Len(N-1)+2*N-1 | 1 | Length of the “N” hot spot name, no more than 32 |
hot spot StrN | hot spot Len1+hot spot Len2+……+hot spot Len(N-1)+2*N | hot spot LenN | The “N” Hot spot Name String |
hot spot RssiN | hot spot Len1+hot spot Len2+……+hot spot LenN+2*N | 1 | The “N” hot spot signal Strength, It’s a negative number, the higher it is, the higher the intensity. |
Reply: no
When the wifi module receive the “scan wifi hotspots” command, it will make the scanning and feekback this message.
Host MCU to ESP WIFI
Configure the network
Type = 0x00
Data:
Segment | Offset (bytes) | Length (bytes) | Definition |
mode | 0 | 1 | 0x01:AP 0x02:Client 0x03:AP+Client(not used yet) |
wifi_name_len | 1 | 1 | Length of wifi name |
wifi_name | 2 | wifi_name_len | Wifi name string, no more than 32 characters |
wifi_key_len | 2+wifi_name_len | 1 | Length of wifi password |
wifi_key | 3+wifi_name_len | wifi_key | Wifi password string, no more than 64 characters |
Reply: no
Gcode reply message
Type = 0x02
Data: the raw string to be replied
Reply: no
When Host MCU receive gcode from esp wifi, it replies “ok\r\n”, and also the file list…using this type of message
Exception information
Type = 0x03
Data:
Segment | Offset (bytes) | Length (bytes) | Definition |
Exception code | 0 | 1 | 0x01: file transfer error 0x02: file transfer ok Just now only these two codes |
Reply: no
Configure the cloud information
Type = 0x04
Data:
Segment | Offset (bytes) | Length (bytes) | Definition |
cloud_enable | 0 | 1 | 0x0a: enable the cloud service 0x05: disable the cloud service |
cloud_host_len | 1 | 1 | Length of Cloud_host |
cloud_host | 2 | cloud_host_len | Url of cloud server, no more than 96 characters |
cloud_port | cloud_host_len+2 | 2 | Port of cloud server |
Reply: no
Unbind the user to wifi module
Type = 0x05
Data: no data
Reply: no
Scan the wifi hot spots
Type = 0x06
Data: no
Reply: no
Wifi connect
Type = 0x09
Data:
Segment | Offset (bytes) | Length (bytes) | Definition |
Operation code | 0 | 1 | 0x01: Connect to the wifi hotspot being configured(The WiFi module should determines that it is not connected and takes action) 0x02: Disconnect linking current WiFi hotspot 0X3: Forget the password of current Wifi |
Reply: no
TCP packets
The original firmare use TCP packet for communication, and this is really limiting.
MKS WiFi Plugin for Cura
First you can’t comunicate to a Web user interface, and so this module can be used only with a desktop client, so the client implementation that you can find is the plugin for CURA.
This plugin is very usefully, on Cura go to marketplace and select MKS WiFi plugin
After the installation, go to Manage printers -> MKS WiFi Plugin --> Add the correct IP
.
After configuration in Cura appear a button to sent directly via WiFi.
Examine communication code
The plugin is written in python and to send use TCP message, first of all do the connection:
self._socket = QTcpSocket()
self._socket.connectToHost(self._address, self._port)
use this command:
def _sendCommand(self, cmd):
in_cmd = "G28" in cmd or "G0" in cmd
if self._ischanging and in_cmd:
return
if self.isBusy() and "M20" in cmd:
return
if self._socket and self._socket.state() == 2 or self._socket.state(
) == 3:
if isinstance(cmd, str):
self._command_queue.put(cmd + "\r\n")
elif isinstance(cmd, list):
for each_command in cmd:
self._command_queue.put(each_command + "\r\n")
that put in a queue the messages and when the queue data are ready call this function that send message to the socket:
def write_socket_data(self):
_send_data = ("M105\r\nM997\r\n", "")[self._command_queue.qsize() > 0]
if self.isBusy():
_send_data += "M994\r\nM992\r\nM27\r\n"
while self._command_queue.qsize() > 0:
_queue_data = self._command_queue.get()
if "M23" in _queue_data:
self._socket.writeData(
_queue_data.encode(sys.getfilesystemencoding()))
continue
if "M24" in _queue_data:
self._socket.writeData(
_queue_data.encode(sys.getfilesystemencoding()))
continue
if self.isBusy() and "M20" in _queue_data:
continue
_send_data += _queue_data
Logger.log("d", "_send_data: \r\n%s" % _send_data)
self._socket.writeData(_send_data.encode(sys.getfilesystemencoding()))
self._socket.flush()
from the MKS WiFi module side you can find the tcp socket initialized on port 8080
WiFiServer tcp(8080);
and in the loop you can find all the management of the messages described in the protocol
if (tcp.hasClient()){
for(i = 0; i < MAX_SRV_CLIENTS; i++){
//find free/disconnected spot
#if 0
if (!serverClients[i] || !serverClients[i].connected()){
if(serverClients[i]) serverClients[i].stop();
serverClients[i] = tcp.available();
continue;
}
#else
if(serverClients[i].connected())
{
serverClients[i].stop();
}
#endif
serverClients[i] = tcp.available();
}
if (tcp.hasClient())
{
//no free/disconnected spot so reject
WiFiClient serverClient = tcp.available();
serverClient.stop();
}
}
memset(dbgStr, 0, sizeof(dbgStr));
for(i = 0; i < MAX_SRV_CLIENTS; i++)
{
if (serverClients[i] && serverClients[i].connected())
{
uint32_t readNum = serverClients[i].available();
if(readNum > FILE_FIFO_SIZE)
{
// net_print((const uint8_t *) "readNum > FILE_FIFO_SIZE\n");
serverClients[i].flush();
// Serial.println("flush");
continue;
}
if(readNum > 0)
{
char * point;
uint8_t readStr[readNum + 1] ;
uint32_t readSize;
readSize = serverClients[i].read(readStr, readNum);
readStr[readSize] = 0;
//transfer file
#if 0
if(strstr((const char *)readStr, "M29") != 0)
{
if(!verification_flag)
{
break;
}
if(transfer_state != TRANSFER_IDLE)
{
rcv_end_flag = true;
net_print((const uint8_t *) "ok\n", strlen((const char *)"ok\n"));
break;
}
}
#endif
if(transfer_file_flag)
{
if(!verification_flag)
{
break;
}
if(gFileFifo.left() >= readSize)
{
gFileFifo.push((char *)readStr, readSize);
transfer_frags += readSize;
}
}
else
{
if(verification_flag)
{
int j = 0;
char cmd_line[100] = {0};
String gcodeM3 = "";
#if 0
if(transfer_state == TRANSFER_BEGIN)
{
if(strstr((const char *)readStr, "M110") != 0)
{
file_fragment = 0;
rcv_end_flag = false;
transfer_file_flag = true;
if(package_file_first(filePath) == 0)
{
/*transfer_state = TRANSFER_READY;
digitalWrite(EspReqTransferPin, LOW);*/
}
else
{
transfer_file_flag = false;
transfer_state = TRANSFER_IDLE;
}
net_print((const uint8_t *) "ok\n", strlen((const char *)"ok\n"));
break;
}
}
#endif
init_queue(&cmd_queue);
cmd_index = 0;
memset(cmd_fifo, 0, sizeof(cmd_fifo));
while(j < readSize)
{
if((readStr[j] == '\r') || (readStr[j] == '\n'))
{
if((cmd_index) > 1)
{
cmd_fifo[cmd_index] = '\n';
cmd_index++;
push_queue(&cmd_queue, cmd_fifo, cmd_index);
}
memset(cmd_fifo, 0, sizeof(cmd_fifo));
cmd_index = 0;
}
else if(readStr[j] == '\0')
break;
else
{
if(cmd_index >= sizeof(cmd_fifo))
{
memset(cmd_fifo, 0, sizeof(cmd_fifo));
cmd_index = 0;
}
cmd_fifo[cmd_index] = readStr[j];
cmd_index++;
}
j++;
do_transfer();
yield();
}
while(pop_queue(&cmd_queue, cmd_line, sizeof(cmd_line)) >= 0)
{
#if 0
point = strstr((const char *)cmd_line, "M28 ");
if(point != 0)
{
if((strstr((const char *)cmd_line, ".g") || strstr((const char *)cmd_line, ".G")))
{
int index = 0;
char *fileName;
point += 3;
while(*(point + index) == ' ')
index++;
memcpy((char *)filePath, (const char *)(point + index), readSize - (int)(point + index - (int)(&cmd_line[0])));
gFileFifo.reset();
transfer_frags = 0;
transfer_state = TRANSFER_BEGIN;
sprintf((char *)dbgStr, "Writing to file:%s\n", (char *)filePath);
net_print((const uint8_t *)dbgStr, strlen((const char *)dbgStr));
}
}
else
#endif
{
/*transfer gcode*/
//Serial.write(cmd_line, readNum);
if((strchr((const char *)cmd_line, 'G') != 0)
|| (strchr((const char *)cmd_line, 'M') != 0)
|| (strchr((const char *)cmd_line, 'T') != 0))
{
if(strchr((const char *)cmd_line, '\n') != 0 )
{
String gcode((const char *)cmd_line);
// sprintf((char *)dbgStr, "read %d: %s\n", readNum, cmd_line);
// net_print((const uint8_t *)dbgStr, strlen((char *)dbgStr));
if(gcode.startsWith("M998") && (M3_TYPE == ROBIN))
{
net_print((const uint8_t *) "ok\r\n", strlen((const char *)"ok\r\n"));
}
else if(gcode.startsWith("M997"))
{
if(gPrinterInf.print_state == PRINTER_IDLE)
// net_print((const uint8_t *) "M997 IDLE\r\n", strlen((const char *)"M997 IDLE\r\n"));
strcpy((char *)dbgStr, "M997 IDLE\r\n");
else if(gPrinterInf.print_state == PRINTER_PRINTING)
// net_print((const uint8_t *) "M997 PRINTING\r\n", strlen((const char *)"M997 PRINTING\r\n"));
strcpy((char *)dbgStr, "M997 PRINTING\r\n");
else if(gPrinterInf.print_state == PRINTER_PAUSE)
//net_print((const uint8_t *) "M997 PAUSE\r\n", strlen((const char *)"M997 PAUSE\r\n"));
strcpy((char *)dbgStr, "M997 PAUSE\r\n");
else
strcpy((char *)dbgStr, "M997 NOT CONNECTED\r\n");
// net_print((const uint8_t *) "ok\r\n", strlen((const char *)"ok\r\n"));
}
else if(gcode.startsWith("M27"))
{
memset(dbgStr, 0, sizeof(dbgStr));
sprintf((char *)dbgStr, "M27 %d\r\n", gPrinterInf.print_file_inf.print_rate);
// net_print((const uint8_t *) dbgStr, strlen((const char *)dbgStr));
// net_print((const uint8_t *) "ok\r\n", strlen((const char *)"ok\r\n"));
}
else if(gcode.startsWith("M992"))
{
memset(dbgStr, 0, sizeof(dbgStr));
sprintf((char *)dbgStr, "M992 %02d:%02d:%02d\r\n",
gPrinterInf.print_file_inf.print_hours, gPrinterInf.print_file_inf.print_mins, gPrinterInf.print_file_inf.print_seconds);
// net_print((const uint8_t *) dbgStr, strlen((const char *)dbgStr));
// net_print((const uint8_t *) "ok\r\n", strlen((const char *)"ok\r\n"));
}
else if(gcode.startsWith("M994"))
{
memset(dbgStr, 0, sizeof(dbgStr));
sprintf((char *)dbgStr, "M994 %s;%d\r\n",
gPrinterInf.print_file_inf.file_name.c_str(), gPrinterInf.print_file_inf.file_size);
// net_print((const uint8_t *) dbgStr, strlen((const char *)dbgStr));
// net_print((const uint8_t *) "ok\r\n", strlen((const char *)"ok\r\n"));
}
else if(gcode.startsWith("M115"))
{
memset(dbgStr, 0, sizeof(dbgStr));
if(M3_TYPE == ROBIN)
strcpy((char *)dbgStr, "FIRMWARE_NAME:Robin\r\n");
else if(M3_TYPE == TFT28)
strcpy((char *)dbgStr, "FIRMWARE_NAME:TFT28/32\r\n");
else if(M3_TYPE == TFT24)
strcpy((char *)dbgStr, "FIRMWARE_NAME:TFT24\r\n");
// net_print((const uint8_t *) dbgStr, strlen((const char *)dbgStr));
// net_print((const uint8_t *) "ok\r\n", strlen((const char *)"ok\r\n"));
}
/*else if(gcode.startsWith("M105"))
{
memset(dbgStr, 0, sizeof(dbgStr));
sprintf((char *)dbgStr, "T:%d /%d B:%d /%d T0:%d /%d T1:%d /%d @:0 B@:0\r\n",
(int)gPrinterInf.curSprayerTemp[0], (int)gPrinterInf.desireSprayerTemp[0], (int)gPrinterInf.curBedTemp, (int)gPrinterInf.desireBedTemp,
(int)gPrinterInf.curSprayerTemp[0], (int)gPrinterInf.desireSprayerTemp[0], (int)gPrinterInf.curSprayerTemp[1], (int)gPrinterInf.desireSprayerTemp[1]);
// net_print((const uint8_t *) dbgStr, strlen((const char *)dbgStr));
// net_print((const uint8_t *) "ok\r\n", strlen((const char *)"ok\r\n"));
}*/
else
{
if(gPrinterInf.print_state == PRINTER_IDLE)
{
if(gcode.startsWith("M23") || gcode.startsWith("M24"))
{
gPrinterInf.print_state = PRINTER_PRINTING;
gPrinterInf.print_file_inf.file_name = "";
gPrinterInf.print_file_inf.file_size = 0;
gPrinterInf.print_file_inf.print_rate = 0;
gPrinterInf.print_file_inf.print_hours = 0;
gPrinterInf.print_file_inf.print_mins = 0;
gPrinterInf.print_file_inf.print_seconds = 0;
printFinishFlag = false;
}
}
gcodeM3.concat(gcode);
}
}
}
}
if(strlen((const char *)dbgStr) > 0)
{
net_print((const uint8_t *) "ok\r\n", strlen((const char *)"ok\r\n"));
net_print((const uint8_t *) dbgStr, strlen((const char *)dbgStr));
memset(dbgStr, 0, sizeof(dbgStr));
}
do_transfer();
yield();
}
if(gcodeM3.length() > 2)
{
package_gcode(gcodeM3, true);
//Serial.write(uart_send_package, sizeof(uart_send_package));
/*transfer_state = TRANSFER_READY;
digitalWrite(EspReqTransferPin, LOW);*/
do_transfer();
socket_busy_stamp = millis();
}
}
}
}
}
}
/* if(strlen((const char *)dbgStr) > 0)
{
net_print((const uint8_t *) dbgStr, strlen((const char *)dbgStr));
net_print((const uint8_t *) "ok\r\n", strlen((const char *)"ok\r\n"));
}*/
Thanks
- MKS WIFI for Makerbase Robin: boards and how to wiring esp12 & NodeMCU
- MKS WIFI for Makerbase Robin: PCB and how to compile & upload firmware
- MKS WIFI for Makerbase Robin: communication protocol and Cura plugin
- MKS WIFI for Makerbase Robin: firmware upgrade and new Web Socket features
- MKS WIFI for Makerbase Robin: BeePrint web interface with Camera on Flying Bear Ghost