Ho acquistato un FlyingBear Ghost 5 con modulo WiFi integrato, ma scopro che non c’è l’interfaccia web. Non so perché non possano aggiungere un’interfaccia utente di base. Poi sono andato a vedere l’interfaccia web delle schede di fascia alta della Makerbase e ho capito che era meglio che non l’avessero sviluppata.
La mia soluzione è stata modificare il firmware per supportare il Web Socket e sviluppare l’interfaccia Web. Questa funzione è compatibile con tutte le schede Makerbase che dispongono di un modulo WiFi MKS.
Il risultato dell’interfaccia Web di BeePrint è in questa schermata.
Ho anche deciso di spiegare tutte le fasi dello sviluppo del progetto ed in quest’articolo qui vorrei esporre l’infrastruttura hardware della scheda MKS WiFi che si interfaccia con la Makerbase Robin Nano.
In questo articolo vorrei spiegare come Makerbase ha sviluppato il protocollo di comunicazione tramite messaggi TCP e una piccola introduzione al plugin MKS WiFi per Cura.
Introduzione
Il mio intento è quello di aggiungere le funzionalità di base di Octoprint che ho sulla mia AnetA8.
Ma il firmware del modulo WiFi MKS originale utilizza il protocollo UDP/TCP e non può essere utilizzato in una normale interfaccia web senza trucchi o funzionalità sperimentali.
Quindi per prima cosa aggiungerò il protocollo WebSocket, e scelgo la stessa libreria di questo tutorial “WebSocket su Arduino, esp8266 e esp32“.
Come per l’TCP il Web Socket lavora con pacchetti di dati asincroni e wrappo la gestione dei messaggi per utilizzare lo stesso protocollo con WebSocket e TCP contemporaneamente.
Protocollo WiFi MKS
MKS fornisce un documento approssimativo sul suo protocollo, le informazioni di base sono queste.
Carica comandi Gcode
Tipo = 0x01
Data=$Gcode Stringa di comando + ” \r\n
”
Response = ok\r\n
(Ogni comando avrà una response con “ok”, e anche l’aggiunta delle informazioni necessarie.)
Ci sono anche alcuni comandi di personalizzazione mks wifi, che non sono i comandi di Marlin:
M20 xxx:Ottieni l’elenco dei file della stampante e l’MCU host restituirà i file e le cartelle gcode nel percorso del file del livello “xxx” (la stringa di caratteri provvisoria non supera i 1024 byte). Se è una cartella, aggiungerà automaticamente il suffisso .DIR. Viene restituito il seguente formato:
Begin file list
abc.gcode
123.gcode
temp.DIR
M23 xxx.gcode:seleziona il file gcode
M24:Se la stampa non è iniziata, questo comando serve per avviare la stampa del file, se la stampa è sospesa, questo comando serve per riprendere la stampa
M25 : pausa stampa
M26:interrompere la stampa
M27 : per ottenere l’avanzamento della stampa, l’MCU host risponderà con questo formato: M27 30\r\n
(30%)
M991:come M105, ottieni la temperatura. L’MCU host risponderebbe in questo formato:T:%d /%d B:%d /%d T0:%d /%d T1:%d /%d @:0 B@:0\r\n
M992:Ottieni l’ora stampata, l’MCU host risponderà in questo formato:M992 10:30:20\r\n
M994 : ottiene il nome e le dimensioni del file in fase di stampa, l’MCU host sarebbe reply as this format: M994 abc.gcode;size
(size indica la dimensione effettiva in byte del file)
M997:Ottieni lo stato corrente della stampante, l’MCU host risponderà in base a diversi stati:
IDLE: “M997 IDLE\r\n”
PRINTING: “M997 PRINTING\r\n”
PAUSED: “M997 PAUSE\r\n”
M998 : cambia il filesystem su USB o Sdcard
M115 : Ottieni le informazioni sul firmware della scheda madre, l’host MCU risponderà in questo formato:FIRMWARE_NAME:MKS Robin Nano V2.1.0\r\n
Risposta: secondo quanto sopra
Carica il primo frammento del file
Tipo = 0x02
Dati:
Segmento | Offset (bytes) | Lunghezza (bytes) | Definizione |
fileNameLen | 0 | 1 | La lunghezza del nome del file da trasferire, non più di 255 |
fileLen | 1 | 4 | La lunghezza del file da trasferire, non più di 2^32 |
fileName | 5 | fileNameLen | La stringa del nome del file da trasferire |
Risposta: no
Carica frammenti di dati di file
Tipo = 0x03
Dati:
Segmento | Offset (bytes) | Lunghezza (bytes) | Definizione |
fragment | 0 | 4 | Il numero di frammento di file, iniziano da 0. Da 0 ~ 14 bit indicano il numero di frammento, il bit 15 indica se è l’ultimo frammento. |
fileData | 4 | Lunghezza dei dati del file frammento | I dati del file non elaborato di questo frammento. La lunghezza dovrebbe corrispondere ai dati del file effettivamente letti. |
Risposta: no
Carica l’elenco degli hotspot WiFi cercati.
Tipo = 0x04
Dati:
Segmento | Offset (byte) | Lunghezza (byte) | Definizione |
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. |
Risposta: no
Quando il modulo wifi riceve il comando “scansione hotspot wifi”, eseguirà la scansione e il feedback di questo messaggio.
Host MCU su ESP WIFI
Configura la rete
Tipo = 0x00
Dati:
Segmento | Offset (bytes) | Lunghezza (bytes) | Definizione |
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 |
Risposta: no
Messaggio di risposta Gcode
Tipo = 0x02
Dati: la stringa non elaborata a cui rispondere
Risposta: no
Quando Host MCU riceve gcode da esp wifi, risponde “ok \ r \ n” e anche l’elenco dei file … utilizzando questo tipo di messaggio
Informazioni sulle eccezioni
Tipo = 0x03
Dati:
Segmento | Offset (bytes) | Lunghezza (bytes) | Definizione |
Exception code | 0 | 1 | 0x01: file transfer error 0x02: file transfer ok Just now only these two codes |
Risposta: no
Configura le informazioni sul cloud
Tipo = 0x04
Dati:
Segmento | Offset (bytes) | Lunghezza (bytes) | Definizione |
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 |
Risposta: no
Svincola l’utente dal modulo wifi
Tipo = 0x05
Dati: nessun dato
Risposta: no
Scansiona gli hotspot Wi-Fi
Tipo = 0x06
Dati: no
Risposta: no
Connessione Wi-Fi
Tipo = 0x09
Dati:
Segmento | Offset (bytes) | Lunghezza (bytes) | Definizione |
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 |
Risposta: no
Pacchetti TCP
Il firmare originale usa il pacchetto TCP per la comunicazione, e questo è davvero limitante.
Plugin WiFi MKS per Cura
Innanzitutto non puoi comunicare con un’interfaccia utente Web, quindi questo modulo può essere utilizzato solo con un client desktop, quindi l’implementazione del client che puoi trovare è il plugin per CURA.
Questo plugin è molto utile, su Cura vai al marketplace e seleziona il plugin MKS WiFi
Dopo l’installazione, vai su Manage printers -> MKS WiFi Plugin --> Add the correct IP
.
Dopo la configurazione in Cura compare un pulsante da inviare direttamente via WiFi.
Esaminare il codice di comunicazione
Il plugin è scritto in python e per inviare usa messaggi TCP, prima di tutto fa la connessione:
self._socket = QTcpSocket()
self._socket.connectToHost(self._address, self._port)
usa questo comando:
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")
che mette in coda i messaggi e quando i dati della coda sono pronti chiama questa funzione che invia il messaggio al 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()
dal lato modulo WiFi MKS puoi trovare il socket tcp inizializzato sulla porta 8080
WiFiServer tcp(8080);
e nel loop trovi tutta la gestione dei messaggi descritta nel protocollo
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"));
}*/
Grazie
- MKS WIFI per Makerbase Robin: schede e come collegare un esp12 o NodeMCU
- MKS WIFI per Makerbase Robin: PCB e come compilare e caricare il firmware
- MKS WIFI per Makerbase Robin: protocollo di comunicazione e plugin Cura
- MKS WIFI per Makerbase Robin: aggiornamento del firmware e nuova funzionalità Web Socket
- MKS WIFI per Makerbase Robin: interfaccia web BeePrint con fotocamera su Flying Bear Ghost