Welcome to mirror list, hosted at ThFree Co, Russian Federation.

github.com/Duet3D/RepRapFirmware.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--Driver/DuetDriverFiles.zipbin0 -> 5491 bytes
-rw-r--r--Release/Duet-0.6-0.8.5/Edge/DuetWebControl-1.14-b4.zipbin0 -> 476488 bytes
-rw-r--r--Release/Duet-0.6-0.8.5/Edge/RepRapFirmware-1.17dev6.binbin312940 -> 0 bytes
-rw-r--r--Release/Duet-0.6-0.8.5/Edge/RepRapFirmware-1.17dev8.binbin319748 -> 0 bytes
-rw-r--r--Release/Duet-0.6-0.8.5/Edge/RepRapFirmware-1.17rc2.binbin319228 -> 0 bytes
-rw-r--r--Release/Duet-0.6-0.8.5/Edge/RepRapFirmware-1.17rc3.bin (renamed from Release/Duet-0.6-0.8.5/Edge/RepRapFirmware-1.17rc1.bin)bin319052 -> 319500 bytes
-rw-r--r--Release/Duet-WiFi/Edge/DuetWebControl-1.14-b4.binbin0 -> 3125248 bytes
-rw-r--r--Release/Duet-WiFi/Edge/DuetWiFiFirmware-1.17dev6.binbin262292 -> 0 bytes
-rw-r--r--Release/Duet-WiFi/Edge/DuetWiFiFirmware-1.17dev8.binbin269308 -> 0 bytes
-rw-r--r--Release/Duet-WiFi/Edge/DuetWiFiFirmware-1.17rc1.binbin268532 -> 0 bytes
-rw-r--r--Release/Duet-WiFi/Edge/DuetWiFiFirmware-1.17rc3.bin (renamed from Release/Duet-WiFi/Edge/DuetWiFiFirmware-1.17rc2.bin)bin268556 -> 269028 bytes
-rw-r--r--src/Configuration.h4
-rw-r--r--src/Duet/Webserver.cpp13
-rw-r--r--src/DuetNG/DuetEthernet/Network.cpp753
-rw-r--r--src/DuetNG/DuetEthernet/Network.h139
-rw-r--r--src/DuetNG/DuetEthernet/Webserver.cpp2918
-rw-r--r--src/DuetNG/DuetEthernet/Webserver.h336
-rw-r--r--src/GCodes/GCodeMachineState.h4
-rw-r--r--src/GCodes/GCodes.cpp41
-rw-r--r--src/GCodes/GCodes.h5
-rw-r--r--src/GCodes/GCodes2.cpp12
-rw-r--r--src/Movement/Grid.cpp42
-rw-r--r--src/Movement/Grid.h11
-rw-r--r--src/Movement/Move.cpp95
-rw-r--r--src/Movement/Move.h5
-rw-r--r--src/Platform.cpp12
-rw-r--r--src/Platform.h2
-rw-r--r--src/PrintMonitor.cpp107
-rw-r--r--src/Storage/FileStore.cpp30
29 files changed, 4321 insertions, 208 deletions
diff --git a/Driver/DuetDriverFiles.zip b/Driver/DuetDriverFiles.zip
new file mode 100644
index 00000000..938a79b6
--- /dev/null
+++ b/Driver/DuetDriverFiles.zip
Binary files differ
diff --git a/Release/Duet-0.6-0.8.5/Edge/DuetWebControl-1.14-b4.zip b/Release/Duet-0.6-0.8.5/Edge/DuetWebControl-1.14-b4.zip
new file mode 100644
index 00000000..21a5a602
--- /dev/null
+++ b/Release/Duet-0.6-0.8.5/Edge/DuetWebControl-1.14-b4.zip
Binary files differ
diff --git a/Release/Duet-0.6-0.8.5/Edge/RepRapFirmware-1.17dev6.bin b/Release/Duet-0.6-0.8.5/Edge/RepRapFirmware-1.17dev6.bin
deleted file mode 100644
index 02868e7c..00000000
--- a/Release/Duet-0.6-0.8.5/Edge/RepRapFirmware-1.17dev6.bin
+++ /dev/null
Binary files differ
diff --git a/Release/Duet-0.6-0.8.5/Edge/RepRapFirmware-1.17dev8.bin b/Release/Duet-0.6-0.8.5/Edge/RepRapFirmware-1.17dev8.bin
deleted file mode 100644
index 22630754..00000000
--- a/Release/Duet-0.6-0.8.5/Edge/RepRapFirmware-1.17dev8.bin
+++ /dev/null
Binary files differ
diff --git a/Release/Duet-0.6-0.8.5/Edge/RepRapFirmware-1.17rc2.bin b/Release/Duet-0.6-0.8.5/Edge/RepRapFirmware-1.17rc2.bin
deleted file mode 100644
index 738c97bf..00000000
--- a/Release/Duet-0.6-0.8.5/Edge/RepRapFirmware-1.17rc2.bin
+++ /dev/null
Binary files differ
diff --git a/Release/Duet-0.6-0.8.5/Edge/RepRapFirmware-1.17rc1.bin b/Release/Duet-0.6-0.8.5/Edge/RepRapFirmware-1.17rc3.bin
index b608dc5a..870df9a7 100644
--- a/Release/Duet-0.6-0.8.5/Edge/RepRapFirmware-1.17rc1.bin
+++ b/Release/Duet-0.6-0.8.5/Edge/RepRapFirmware-1.17rc3.bin
Binary files differ
diff --git a/Release/Duet-WiFi/Edge/DuetWebControl-1.14-b4.bin b/Release/Duet-WiFi/Edge/DuetWebControl-1.14-b4.bin
new file mode 100644
index 00000000..31bfb5e8
--- /dev/null
+++ b/Release/Duet-WiFi/Edge/DuetWebControl-1.14-b4.bin
Binary files differ
diff --git a/Release/Duet-WiFi/Edge/DuetWiFiFirmware-1.17dev6.bin b/Release/Duet-WiFi/Edge/DuetWiFiFirmware-1.17dev6.bin
deleted file mode 100644
index cee77991..00000000
--- a/Release/Duet-WiFi/Edge/DuetWiFiFirmware-1.17dev6.bin
+++ /dev/null
Binary files differ
diff --git a/Release/Duet-WiFi/Edge/DuetWiFiFirmware-1.17dev8.bin b/Release/Duet-WiFi/Edge/DuetWiFiFirmware-1.17dev8.bin
deleted file mode 100644
index afdf372e..00000000
--- a/Release/Duet-WiFi/Edge/DuetWiFiFirmware-1.17dev8.bin
+++ /dev/null
Binary files differ
diff --git a/Release/Duet-WiFi/Edge/DuetWiFiFirmware-1.17rc1.bin b/Release/Duet-WiFi/Edge/DuetWiFiFirmware-1.17rc1.bin
deleted file mode 100644
index 56222091..00000000
--- a/Release/Duet-WiFi/Edge/DuetWiFiFirmware-1.17rc1.bin
+++ /dev/null
Binary files differ
diff --git a/Release/Duet-WiFi/Edge/DuetWiFiFirmware-1.17rc2.bin b/Release/Duet-WiFi/Edge/DuetWiFiFirmware-1.17rc3.bin
index cce46a24..447d9b8c 100644
--- a/Release/Duet-WiFi/Edge/DuetWiFiFirmware-1.17rc2.bin
+++ b/Release/Duet-WiFi/Edge/DuetWiFiFirmware-1.17rc3.bin
Binary files differ
diff --git a/src/Configuration.h b/src/Configuration.h
index ead50a21..dc6890d2 100644
--- a/src/Configuration.h
+++ b/src/Configuration.h
@@ -28,11 +28,11 @@ Licence: GPL
// Firmware name is now defined in the Pins file
#ifndef VERSION
-# define VERSION "1.17RC2"
+# define VERSION "1.17RC3"
#endif
#ifndef DATE
-# define DATE "2016-12-18"
+# define DATE "2016-12-21"
#endif
#define AUTHORS "reprappro, dc42, zpl, t3p3, dnewman"
diff --git a/src/Duet/Webserver.cpp b/src/Duet/Webserver.cpp
index 293b77e0..18cad76b 100644
--- a/src/Duet/Webserver.cpp
+++ b/src/Duet/Webserver.cpp
@@ -696,7 +696,7 @@ void Webserver::HttpInterpreter::DoFastUpload()
void Webserver::HttpInterpreter::SendFile(const char* nameOfFileToSend, bool isWebFile)
{
NetworkTransaction *transaction = webserver->currentTransaction;
- FileStore *fileToSend;
+ FileStore *fileToSend = nullptr;
bool zip = false;
if (isWebFile)
@@ -709,10 +709,9 @@ void Webserver::HttpInterpreter::SendFile(const char* nameOfFileToSend, bool isW
nameOfFileToSend = INDEX_PAGE_FILE;
}
}
- fileToSend = platform->GetFileStore(platform->GetWebDir(), nameOfFileToSend, false);
- // If we failed to open the file, see if we can open a file with the same name and a ".gz" extension
- if (fileToSend == nullptr && !StringEndsWith(nameOfFileToSend, ".gz") && strlen(nameOfFileToSend) + 3 <= FILENAME_LENGTH)
+ // Try to open a gzipped version of the file first
+ if (!StringEndsWith(nameOfFileToSend, ".gz") && strlen(nameOfFileToSend) + 3 <= FILENAME_LENGTH)
{
char nameBuf[FILENAME_LENGTH + 1];
strcpy(nameBuf, nameOfFileToSend);
@@ -724,6 +723,12 @@ void Webserver::HttpInterpreter::SendFile(const char* nameOfFileToSend, bool isW
}
}
+ // If that failed, try to open the normal version of the file
+ if (fileToSend == nullptr)
+ {
+ fileToSend = platform->GetFileStore(platform->GetWebDir(), nameOfFileToSend, false);
+ }
+
// If we still couldn't find the file and it was an HTML file, return the 404 error page
if (fileToSend == nullptr && (StringEndsWith(nameOfFileToSend, ".html") || StringEndsWith(nameOfFileToSend, ".htm")))
{
diff --git a/src/DuetNG/DuetEthernet/Network.cpp b/src/DuetNG/DuetEthernet/Network.cpp
index 094795d7..27deb978 100644
--- a/src/DuetNG/DuetEthernet/Network.cpp
+++ b/src/DuetNG/DuetEthernet/Network.cpp
@@ -213,4 +213,757 @@ void Network::SetHostname(const char *name)
}
}
+bool Network::Lock()
+{
+ //TODO
+ return true;
+}
+
+void Network::Unlock()
+{
+ //TODO
+}
+
+bool Network::InLwip() const
+{
+ //TODO
+ return false;
+}
+
+// This is called by the web server to get the next networking transaction.
+//
+// If cs is NULL, the transaction from the head of readyTransactions will be retrieved.
+// If cs is not NULL, the first transaction with the matching connection will be returned.
+//
+// This method also ensures that the retrieved transaction is moved to the first item of
+// readyTransactions, so that a subsequent call with a NULL cs parameter will return exactly
+// the same instance.
+NetworkTransaction *Network::GetTransaction(const ConnectionState *cs)
+{
+#if 1
+ return nullptr;
+#else
+ // See if there is any transaction at all
+ NetworkTransaction *transaction = readyTransactions;
+ if (transaction == nullptr)
+ {
+ return nullptr;
+ }
+
+ // If no specific connection is specified or if the first item already matches the
+ // connection we are looking for, just return it
+ if (cs == nullptr || transaction->GetConnection() == cs)
+ {
+ return transaction;
+ }
+
+ // We are looking for a specific transaction, but it's not the first item.
+ // Search for it and move it to the head of readyTransactions
+ NetworkTransaction *previous = transaction;
+ for(NetworkTransaction *item = transaction->next; item != nullptr; item = item->next)
+ {
+ if (item->GetConnection() == cs)
+ {
+ previous->next = item->next;
+ item->next = readyTransactions;
+ readyTransactions = item;
+ return item;
+ }
+ previous = item;
+ }
+
+ // We failed to find a valid transaction for the given connection
+ return nullptr;
+#endif
+}
+
+void Network::OpenDataPort(uint16_t port)
+{
+ //TODO
+#if 0
+ closingDataPort = false;
+ tcp_pcb* pcb = tcp_new();
+ tcp_bind(pcb, IP_ADDR_ANY, port);
+ ftp_pasv_pcb = tcp_listen(pcb);
+ tcp_accept(ftp_pasv_pcb, conn_accept);
+#endif
+}
+
+uint16_t Network::GetDataPort() const
+{
+#if 1
+ return 0; //TODO
+#else
+ return (closingDataPort || (ftp_pasv_pcb == nullptr) ? 0 : ftp_pasv_pcb->local_port);
+#endif
+}
+
+// Close FTP data port and purge associated PCB
+void Network::CloseDataPort()
+{
+ //TODO
+#if 0
+ // See if it's already being closed
+ if (closingDataPort)
+ {
+ return;
+ }
+ closingDataPort = true;
+
+ // Close remote connection of our data port or do it as soon as the last packet has been sent
+ if (dataCs != nullptr)
+ {
+ NetworkTransaction *mySendingTransaction = dataCs->sendingTransaction;
+ if (mySendingTransaction != nullptr)
+ {
+ mySendingTransaction->Close();
+ return;
+ }
+ }
+
+ // We can close it now, so do it here
+ if (ftp_pasv_pcb != nullptr)
+ {
+ tcp_accept(ftp_pasv_pcb, nullptr);
+ tcp_close(ftp_pasv_pcb);
+ ftp_pasv_pcb = nullptr;
+ }
+ closingDataPort = false;
+#endif
+}
+
+// These methods keep track of our connections in case we need to send to one of them
+void Network::SaveDataConnection()
+{
+ //TODO
+#if 0
+ dataCs = readyTransactions->cs;
+#endif
+}
+
+void Network::SaveFTPConnection()
+{
+ //TODO
+#if 0
+ ftpCs = readyTransactions->cs;
+#endif
+}
+
+void Network::SaveTelnetConnection()
+{
+ //TODO
+#if 0
+ telnetCs = readyTransactions->cs;
+#endif
+}
+
+bool Network::AcquireFTPTransaction()
+{
+#if 1
+ return false; //TODO
+#else
+ return AcquireTransaction(ftpCs);
+#endif
+}
+
+bool Network::AcquireDataTransaction()
+{
+#if 1
+ return false; //TODO
+#else
+ return AcquireTransaction(dataCs);
+#endif
+}
+
+bool Network::AcquireTelnetTransaction()
+{
+#if 1
+ return false; //TODO
+#else
+ return AcquireTransaction(telnetCs);
+#endif
+}
+
+//***************************************************************************************************
+
+// ConnectionState class
+
+#if 0
+void ConnectionState::Init(tcp_pcb *p)
+{
+ pcb = p;
+ localPort = p->local_port;
+ remoteIPAddress = p->remote_ip.addr;
+ remotePort = p->remote_port;
+ next = nullptr;
+ sendingTransaction = nullptr;
+ persistConnection = true;
+ isTerminated = false;
+}
+#endif
+
+void ConnectionState::Terminate()
+{
+ //TODO
+#if 0
+ if (pcb != nullptr)
+ {
+ tcp_abort(pcb);
+ }
+#endif
+}
+
+//***************************************************************************************************
+// NetworkTransaction class
+
+NetworkTransaction::NetworkTransaction(NetworkTransaction *n) : next(n), status(released)
+{
+ sendStack = new OutputStack();
+}
+
+void NetworkTransaction::Set(pbuf *p, ConnectionState *c, TransactionStatus s)
+{
+ cs = c;
+// pb = readingPb = p;
+ status = s;
+// inputPointer = 0;
+ sendBuffer = nullptr;
+ fileBeingSent = nullptr;
+ closeRequested = false;
+ nextWrite = nullptr;
+ dataAcknowledged = false;
+}
+
+bool NetworkTransaction::HasMoreDataToRead() const
+{
+ //TODO
+ return false;
+}
+
+// Read one char from the NetworkTransaction
+bool NetworkTransaction::Read(char& b)
+{
+#if 1
+ return false;
+#else
+ if (readingPb == nullptr)
+ {
+ b = 0;
+ return false;
+ }
+
+ b = ((const char*)readingPb->payload)[inputPointer++];
+ if (inputPointer == readingPb->len)
+ {
+ readingPb = readingPb->next;
+ inputPointer = 0;
+ }
+ return true;
+#endif
+}
+
+// Read data from the NetworkTransaction and return true on success
+bool NetworkTransaction::ReadBuffer(const char *&buffer, size_t &len)
+{
+#if 1
+ return false;
+#else
+ if (readingPb == nullptr)
+ {
+ return false;
+ }
+
+ if (inputPointer >= readingPb->len)
+ {
+ readingPb = readingPb->next;
+ inputPointer = 0;
+ if (readingPb == nullptr)
+ {
+ return false;
+ }
+ }
+
+ buffer = (const char*)readingPb->payload + inputPointer;
+ len = readingPb->len - inputPointer;
+ readingPb = readingPb->next;
+ inputPointer = 0;
+ return true;
+#endif
+}
+
+void NetworkTransaction::Write(char b)
+{
+ if (CanWrite())
+ {
+ if (sendBuffer == nullptr && !OutputBuffer::Allocate(sendBuffer))
+ {
+ // Should never get here
+ return;
+ }
+ sendBuffer->cat(b);
+ }
+}
+
+void NetworkTransaction::Write(const char* s)
+{
+ if (CanWrite())
+ {
+ if (sendBuffer == nullptr && !OutputBuffer::Allocate(sendBuffer))
+ {
+ // Should never get here
+ return;
+ }
+ sendBuffer->cat(s);
+ }
+}
+
+void NetworkTransaction::Write(StringRef ref)
+{
+ Write(ref.Pointer(), ref.strlen());
+}
+
+void NetworkTransaction::Write(const char* s, size_t len)
+{
+ if (CanWrite())
+ {
+ if (sendBuffer == nullptr && !OutputBuffer::Allocate(sendBuffer))
+ {
+ // Should never get here
+ return;
+ }
+ sendBuffer->cat(s, len);
+ }
+}
+
+void NetworkTransaction::Write(OutputBuffer *buffer)
+{
+ if (CanWrite())
+ {
+ // Note we use an individual stack here, because we don't want to link different
+ // OutputBuffers for different destinations together...
+ sendStack->Push(buffer);
+ }
+ else
+ {
+ // Don't keep buffers we can't send...
+ OutputBuffer::ReleaseAll(buffer);
+ }
+}
+
+void NetworkTransaction::Write(OutputStack *stack)
+{
+ if (stack != nullptr)
+ {
+ if (CanWrite())
+ {
+ sendStack->Append(stack);
+ }
+ else
+ {
+ stack->ReleaseAll();
+ }
+ }
+}
+
+void NetworkTransaction::Printf(const char* fmt, ...)
+{
+ if (CanWrite() && (sendBuffer != nullptr || OutputBuffer::Allocate(sendBuffer)))
+ {
+ va_list p;
+ va_start(p, fmt);
+ sendBuffer->vprintf(fmt, p);
+ va_end(p);
+ }
+}
+
+void NetworkTransaction::SetFileToWrite(FileStore *file)
+{
+ if (CanWrite())
+ {
+ fileBeingSent = file;
+ }
+ else if (file != nullptr)
+ {
+ file->Close();
+ }
+}
+
+// Send exactly one TCP window of data and return true when this transaction can be released
+bool NetworkTransaction::Send()
+{
+#if 1
+ return true;
+#else
+ // Free up this transaction if the connection is supposed to be closed
+ if (closeRequested)
+ {
+ reprap.GetNetwork()->ConnectionClosed(cs, true); // This will release the transaction too
+ return false;
+ }
+
+ // Fill up the TCP window with some data chunks from our OutputBuffer instances
+ size_t bytesBeingSent = 0, bytesLeftToSend = TCP_WND;
+ while (sendBuffer != nullptr && bytesLeftToSend > 0)
+ {
+ size_t copyLength = min<size_t>(bytesLeftToSend, sendBuffer->BytesLeft());
+ memcpy(sendingWindow + bytesBeingSent, sendBuffer->Read(copyLength), copyLength);
+ bytesBeingSent += copyLength;
+ bytesLeftToSend -= copyLength;
+
+ if (sendBuffer->BytesLeft() == 0)
+ {
+ sendBuffer = OutputBuffer::Release(sendBuffer);
+ if (sendBuffer == nullptr)
+ {
+ sendBuffer = sendStack->Pop();
+ }
+ }
+ }
+
+ // We also intend to send a file, so check if we can fill up the TCP window
+ if (sendBuffer == nullptr && bytesLeftToSend != 0 && fileBeingSent != nullptr)
+ {
+ // For HSMCI efficiency, read from the file in multiples of 4 bytes except at the end.
+ // This ensures that the second and subsequent chunks can be DMA'd directly into sendingWindow.
+ size_t bytesToRead = bytesLeftToSend & (~3);
+ if (bytesToRead != 0)
+ {
+ int bytesRead = fileBeingSent->Read(sendingWindow + bytesBeingSent, bytesToRead);
+ if (bytesRead > 0)
+ {
+ bytesBeingSent += bytesRead;
+ }
+
+ if (bytesRead != (int)bytesToRead)
+ {
+ fileBeingSent->Close();
+ fileBeingSent = nullptr;
+ }
+ }
+ }
+
+ if (bytesBeingSent == 0)
+ {
+ // If we have no data to send, this connection can be closed next time
+ if (!cs->persistConnection && nextWrite == nullptr)
+ {
+ Close();
+ return false;
+ }
+
+ // We want to send data from another transaction as well, so only free up this one
+ cs->sendingTransaction = nextWrite;
+ return true;
+ }
+
+ // The TCP window has been filled up as much as possible, so send it now. There is no need to check
+ // the available space in the SNDBUF queue, because we really write only one TCP window at once.
+ writeResult = tcp_write(cs->pcb, sendingWindow, bytesBeingSent, 0);
+ if (ERR_IS_FATAL(writeResult))
+ {
+ reprap.GetPlatform()->MessageF(HOST_MESSAGE, "Network: Failed to write data in Send (code %d)\n", writeResult);
+ tcp_abort(cs->pcb);
+ return false;
+ }
+
+ outputResult = tcp_output(cs->pcb);
+ if (ERR_IS_FATAL(outputResult))
+ {
+ reprap.GetPlatform()->MessageF(HOST_MESSAGE, "Network: Failed to output data in Send (code %d)\n", outputResult);
+ tcp_abort(cs->pcb);
+ return false;
+ }
+
+ if (outputResult != ERR_OK && reprap.Debug(moduleNetwork))
+ {
+ reprap.GetPlatform()->MessageF(HOST_MESSAGE, "Network: tcp_output resulted in error code %d\n", outputResult);
+ }
+
+ // Set LwIP callbacks for ACK and retransmission handling
+ tcp_poll(cs->pcb, conn_poll, TCP_WRITE_TIMEOUT / TCP_SLOW_INTERVAL / TCP_MAX_SEND_RETRIES);
+ tcp_sent(cs->pcb, conn_sent);
+
+ // Set all values for the send process
+ sendingConnection = cs;
+ sendingRetries = 0;
+ sendingWindowSize = sentDataOutstanding = bytesBeingSent;
+ return false;
+#endif
+}
+
+// This is called by the Webserver to send output data to a client. If keepConnectionAlive is set to false,
+// the current connection will be terminated once everything has been sent.
+void NetworkTransaction::Commit(bool keepConnectionAlive)
+{
+#if 0
+ // If the connection has been terminated (e.g. RST received while writing upload data), discard this transaction
+ if (!IsConnected() || status == released)
+ {
+ Discard();
+ return;
+ }
+
+ // Free buffer holding the incoming data and prepare some values for the sending process
+ FreePbuf();
+ cs->persistConnection = keepConnectionAlive;
+ if (sendBuffer == nullptr)
+ {
+ sendBuffer = sendStack->Pop();
+ }
+ status = sending;
+
+ // Unlink the item(s) from the list of ready transactions
+ if (keepConnectionAlive)
+ {
+ // Our connection is still of interest, remove only this transaction from the list
+ NetworkTransaction *previous = nullptr;
+ for(NetworkTransaction *item = reprap.GetNetwork()->readyTransactions; item != nullptr; item = item->next)
+ {
+ if (item == this)
+ {
+ if (previous == nullptr)
+ {
+ reprap.GetNetwork()->readyTransactions = next;
+ }
+ else
+ {
+ previous->next = next;
+ }
+ break;
+ }
+ previous = item;
+ }
+ }
+ else
+ {
+ // We will close this connection soon, stop receiving data from this PCB
+ tcp_recv(cs->pcb, nullptr);
+
+ // Also remove all ready transactions pointing to our ConnectionState
+ NetworkTransaction *previous = nullptr, *item = reprap.GetNetwork()->readyTransactions;
+ while (item != nullptr)
+ {
+ if (item->cs == cs)
+ {
+ if (item == this)
+ {
+ // Only unlink this item
+ if (previous == nullptr)
+ {
+ reprap.GetNetwork()->readyTransactions = next;
+ }
+ else
+ {
+ previous->next = next;
+ }
+ item = next;
+ }
+ else
+ {
+ // Remove all others
+ item->Discard();
+ item = (previous == nullptr) ? reprap.GetNetwork()->readyTransactions : previous->next;
+ }
+ }
+ else
+ {
+ previous = item;
+ item = item->next;
+ }
+ }
+ }
+
+ // Enqueue this transaction, so it's sent in the right order
+ NetworkTransaction *mySendingTransaction = cs->sendingTransaction;
+ if (mySendingTransaction == nullptr)
+ {
+ cs->sendingTransaction = this;
+ reprap.GetNetwork()->AppendTransaction(&reprap.GetNetwork()->writingTransactions, this);
+ }
+ else
+ {
+ while (mySendingTransaction->nextWrite != nullptr)
+ {
+ mySendingTransaction = mySendingTransaction->nextWrite;
+ }
+ mySendingTransaction->nextWrite = this;
+ }
+#endif
+}
+
+// Call this to perform some networking tasks while processing deferred requests,
+// and to move this transaction and all transactions that are associated with its
+// connection to the end of readyTransactions. There are three ways to do this:
+//
+// 1) DeferOnly: Do not modify any of the processed data and don't send an ACK.
+// This will ensure that zero-window packets are sent back to the client
+// 2) ResetData: Reset the read pointers and acknowledge that the data has been processed
+// 3) DiscardData: Free the processed data, acknowledge it and append this transaction as
+// an empty item again without payload (i.e. without pbufs)
+//
+void NetworkTransaction::Defer(DeferralMode mode)
+{
+#if 0
+ if (mode == DeferralMode::ResetData)
+ {
+ // Reset the reading pointers and send an ACK
+ inputPointer = 0;
+ readingPb = pb;
+ if (IsConnected() && pb != nullptr && !dataAcknowledged)
+ {
+ tcp_recved(cs->pcb, pb->tot_len);
+ dataAcknowledged = true;
+ }
+ }
+ else if (mode == DeferralMode::DiscardData)
+ {
+ // Discard the incoming data, because we don't need to process it any more
+ FreePbuf();
+ }
+
+ status = deferred;
+
+ // Unlink this transaction from the list of ready transactions and append it again
+ Network *network = reprap.GetNetwork();
+ NetworkTransaction *item, *previous = nullptr;
+ for(item = network->readyTransactions; item != nullptr; item = item->next)
+ {
+ if (item == this)
+ {
+ if (previous == nullptr)
+ {
+ network->readyTransactions = next;
+ }
+ else
+ {
+ previous->next = next;
+ }
+ break;
+ }
+ previous = item;
+ }
+ network->AppendTransaction(&network->readyTransactions, this);
+
+ // Append all other transactions that are associated to this connection, so that the
+ // Webserver gets a chance to deal with all connected clients even while multiple
+ // deferred requests are present in the list.
+ item = network->readyTransactions;
+ previous = nullptr;
+ while (item != this)
+ {
+ if (item->cs == cs)
+ {
+ NetworkTransaction *nextItem = item->next;
+ if (previous == nullptr)
+ {
+ network->readyTransactions = item->next;
+ network->AppendTransaction(&network->readyTransactions, item);
+ }
+ else
+ {
+ previous->next = item->next;
+ network->AppendTransaction(&network->readyTransactions, item);
+ }
+ item = nextItem;
+ }
+ else
+ {
+ previous = item;
+ item = item->next;
+ }
+ }
+#endif
+}
+
+
+// This method should be called if we don't want to send data to the client and if we
+// don't want to interfere with the connection state. May also be called from ISR!
+void NetworkTransaction::Discard()
+{
+#if 0
+ // Can we do anything?
+ if (status == released)
+ {
+ // No - don't free up released items multiple times
+ return;
+ }
+
+ // Free up some resources
+ FreePbuf();
+
+ if (fileBeingSent != nullptr)
+ {
+ fileBeingSent->Close();
+ fileBeingSent = nullptr;
+ }
+
+ OutputBuffer::ReleaseAll(sendBuffer);
+ sendStack->ReleaseAll();
+
+ // Unlink this transactions from the list of ready transactions and free it. It is then appended to the list of
+ // free transactions because we don't want to risk reusing it when the ethernet ISR processes incoming data
+ NetworkTransaction *previous = nullptr;
+ for(NetworkTransaction *item = reprap.GetNetwork()->readyTransactions; item != nullptr; item = item->next)
+ {
+ if (item == this)
+ {
+ if (previous == nullptr)
+ {
+ reprap.GetNetwork()->readyTransactions = next;
+ }
+ else
+ {
+ previous->next = next;
+ }
+ break;
+ }
+ previous = item;
+ }
+ reprap.GetNetwork()->AppendTransaction(&reprap.GetNetwork()->freeTransactions, this);
+ bool callDisconnectHandler = (cs != nullptr && status == disconnected);
+ status = released;
+
+ // Call disconnect event if this transaction indicates a graceful disconnect and if the connection
+ // still persists (may not be the case if a RST packet was received before)
+ if (callDisconnectHandler)
+ {
+ if (reprap.Debug(moduleNetwork))
+ {
+ reprap.GetPlatform()->Message(HOST_MESSAGE, "Network: Discard() is handling a graceful disconnect\n");
+ }
+ reprap.GetNetwork()->ConnectionClosed(cs, false);
+ }
+#endif
+}
+
+uint32_t NetworkTransaction::GetRemoteIP() const
+{
+ return (cs != nullptr) ? cs->GetRemoteIP() : 0;
+}
+
+uint16_t NetworkTransaction::GetRemotePort() const
+{
+ return (cs != nullptr) ? cs->GetRemotePort() : 0;
+}
+
+uint16_t NetworkTransaction::GetLocalPort() const
+{
+ return (cs != nullptr) ? cs->GetLocalPort() : 0;
+}
+
+void NetworkTransaction::Close()
+{
+#if 0
+ tcp_pcb *pcb = cs->pcb;
+ tcp_recv(pcb, nullptr);
+ closeRequested = true;
+#endif
+}
+
+bool ConnectionState::IsConnected() const
+{
+ //TODO
+ return false;
+}
+
// End
diff --git a/src/DuetNG/DuetEthernet/Network.h b/src/DuetNG/DuetEthernet/Network.h
index 88edd5f0..a2e1b4d7 100644
--- a/src/DuetNG/DuetEthernet/Network.h
+++ b/src/DuetNG/DuetEthernet/Network.h
@@ -16,21 +16,114 @@ Separated out from Platform.h by dc42 and extended by zpl
#include "MessageType.h"
-// Return code definitions
-const uint32_t rcNumber = 0x0000FFFF;
-const uint32_t rcJson = 0x00010000;
-const uint32_t rcKeepOpen = 0x00020000;
-
+const uint8_t MAC_ADDRESS[6] = { 0xBE, 0xEF, 0xDE, 0xAD, 0xFE, 0xED }; // Need some sort of default...
const uint8_t IP_ADDRESS[4] = { 192, 168, 1, 10 }; // Need some sort of default...
const uint8_t NET_MASK[4] = { 255, 255, 255, 0 };
const uint8_t GATE_WAY[4] = { 192, 168, 1, 1 };
-const uint8_t MAC_ADDRESS[6] = { 0xBE, 0xEF, 0xDE, 0xAD, 0xFE, 0xED }; // Need some sort of default...
+
const uint16_t DEFAULT_HTTP_PORT = 80;
+const uint16_t FTP_PORT = 21;
+const uint16_t TELNET_PORT = 23;
-class TransactionBuffer;
-class WifiFirmwareUploader;
class Platform;
+struct tcp_pcb;
+struct pbuf;
+
+class NetworkTransaction;
+
+// ConnectionState structure that we use to track TCP connections. It is usually combined with NetworkTransactions.
+struct ConnectionState
+{
+// tcp_pcb *volatile pcb; // Connection PCB
+ uint16_t localPort, remotePort; // Copy of the local and remote ports, because the PCB may be unavailable
+ uint32_t remoteIPAddress; // Same for the remote IP address
+ NetworkTransaction * volatile sendingTransaction; // NetworkTransaction that is currently sending via this connection
+ ConnectionState * volatile next; // Next ConnectionState in this list
+ bool persistConnection; // Do we expect this connection to stay alive?
+ volatile bool isTerminated; // Will be true if the connection has gone down unexpectedly (TCP RST)
+
+// void Init(tcp_pcb *p);
+ uint16_t GetLocalPort() const { return localPort; }
+ uint32_t GetRemoteIP() const { return remoteIPAddress; }
+ uint16_t GetRemotePort() const { return remotePort; }
+ bool IsConnected() const; // { return pcb != nullptr; }
+ bool IsTerminated() const { return isTerminated; }
+ void Terminate();
+};
+
+// Assign a status to each NetworkTransaction
+enum TransactionStatus
+{
+ released,
+ connected,
+ receiving,
+ sending,
+ disconnected,
+ deferred,
+ acquired
+};
+
+// How is a deferred request supposed to be handled?
+enum class DeferralMode
+{
+ DeferOnly, // don't change anything, because we want to read more of it next time
+ ResetData, // keep the data and reset all reading pointers allowing us to process it again
+ DiscardData // discard all incoming data and re-enqueue the empty transaction
+};
+
+// Start with a class to hold input and output from the network that needs to be responded to.
+// This includes changes in the connection state, e.g. connects and disconnects.
+class NetworkTransaction
+{
+ public:
+ friend class Network;
+
+ NetworkTransaction(NetworkTransaction* n);
+ void Set(pbuf *p, ConnectionState* c, TransactionStatus s);
+ TransactionStatus GetStatus() const { return status; }
+ bool IsConnected() const;
+
+ bool HasMoreDataToRead() const; // { return readingPb != nullptr; }
+ bool Read(char& b);
+ bool ReadBuffer(const char *&buffer, size_t &len);
+ void Write(char b);
+ void Write(const char* s);
+ void Write(StringRef ref);
+ void Write(const char* s, size_t len);
+ void Write(OutputBuffer *buffer);
+ void Write(OutputStack *stack);
+ void Printf(const char *fmt, ...);
+ void SetFileToWrite(FileStore *file);
+
+ ConnectionState *GetConnection() const { return cs; }
+ uint16_t GetLocalPort() const;
+ uint32_t GetRemoteIP() const;
+ uint16_t GetRemotePort() const;
+
+ void Commit(bool keepConnectionAlive);
+ void Defer(DeferralMode mode);
+ void Discard();
+
+ private:
+ bool CanWrite() const;
+ bool Send();
+ void Close();
+
+ ConnectionState* cs;
+ NetworkTransaction* volatile next; // next NetworkTransaction in the list we are in
+ NetworkTransaction* volatile nextWrite; // next NetworkTransaction queued to write to assigned connection
+// pbuf *pb, *readingPb; // received packet queue and a pointer to the pbuf being read from
+// size_t inputPointer; // amount of data already taken from the first packet buffer
+
+ OutputBuffer *sendBuffer;
+ OutputStack *sendStack;
+ FileStore * volatile fileBeingSent;
+
+ volatile TransactionStatus status;
+ volatile bool closeRequested, dataAcknowledged;
+};
+
// The main network class that drives the network.
class Network
{
@@ -60,7 +153,9 @@ public:
void Start();
void Stop();
- bool InLwip() const { return false; }
+ bool Lock();
+ void Unlock();
+ bool InLwip() const;
void Enable();
void Disable();
@@ -71,6 +166,22 @@ public:
void SetHostname(const char *name);
+ // Interfaces for the Webserver
+
+ NetworkTransaction *GetTransaction(const ConnectionState *cs = nullptr);
+
+ void OpenDataPort(uint16_t port);
+ uint16_t GetDataPort() const;
+ void CloseDataPort();
+
+ void SaveDataConnection();
+ void SaveFTPConnection();
+ void SaveTelnetConnection();
+
+ bool AcquireFTPTransaction();
+ bool AcquireDataTransaction();
+ bool AcquireTelnetTransaction();
+
private:
void SetupSpi();
@@ -96,4 +207,14 @@ private:
bool activated;
};
+inline bool NetworkTransaction::IsConnected() const
+{
+ return (cs != nullptr && cs->IsConnected());
+}
+
+inline bool NetworkTransaction::CanWrite() const
+{
+ return (IsConnected() && status != released);
+}
+
#endif
diff --git a/src/DuetNG/DuetEthernet/Webserver.cpp b/src/DuetNG/DuetEthernet/Webserver.cpp
index 2a1740e8..293b77e0 100644
--- a/src/DuetNG/DuetEthernet/Webserver.cpp
+++ b/src/DuetNG/DuetEthernet/Webserver.cpp
@@ -1,56 +1,2952 @@
-/*
- * Webserver.cpp
- *
- * Created on: 13 Dec 2016
- * Author: David
- */
+/****************************************************************************************************
+
+ RepRapFirmware - Webserver
+
+ This class serves a single-page web applications to the attached network. This page forms the user's
+ interface with the RepRap machine. This software interprests returned values from the page and uses it
+ to generate G Codes, which it sends to the RepRap. It also collects values from the RepRap like
+ temperature and uses those to construct the web page.
+
+ The page itself - reprap.htm - uses Jquery.js to perform AJAX. See:
+
+ http://jquery.com/
+
+ -----------------------------------------------------------------------------------------------------
+
+ Version 0.2
+
+ 10 May 2013
+
+ Adrian Bowyer
+ RepRap Professional Ltd
+ http://reprappro.com
+
+ Licence: GPL
+
+ -----------------------------------------------------------------------------------------------------
+
+ The supported requests are GET requests for files (for which the root is the www directory on the
+ SD card), and the following. These all start with "/rr_". Ordinary files used for the web interface
+ must not have names starting "/rr_" or they will not be found. Times should be generally specified
+ in the format YYYY-MM-DDTHH:MM:SS so the firmware can parse them.
+
+ rr_connect?password=xxx&time=yyy
+ Sent by the web interface software to establish an initial connection, indicating that
+ any state variables relating to the web interface (e.g. file upload in progress) should
+ be reset. This only happens if the password could be verified.
+
+ rr_fileinfo Returns file information about the file being printed.
+
+ rr_fileinfo?name=xxx
+ Returns file information about a file on the SD card or a JSON-encapsulated response
+ with err = 1 if the passed filename was invalid.
+
+ rr_status New-style status response, in which temperatures, axis positions and extruder positions
+ are returned in separate variables. Another difference is that extruder positions are
+ returned as absolute positions instead of relative to the previous gcode. A client
+ may also request different status responses by specifying the "type" keyword, followed
+ by a custom status response type. Also see "M105 S1".
+
+ rr_filelist?dir=xxx
+ Returns a JSON-formatted list of all the files in xxx including the type and size in the
+ following format: "files":[{"type":'f/d',"name":"xxx",size:yyy},...]
+
+ rr_files?dir=xxx&flagDirs={1/0} [DEPRECATED]
+ Returns a listing of the filenames in the /gcode directory of the SD card. 'dir' is a
+ directory path relative to the root of the SD card. If the 'dir' variable is not present,
+ it defaults to the /gcode directory. If flagDirs is set to 1, all directories will be
+ prefixed by an asterisk.
+
+ rr_reply Returns the last-known G-code reply as plain text (not encapsulated as JSON).
+
+ rr_configfile [DEPRECATED]
+ Sends the config file as plain text (not encapsulated as JSON either).
+
+ rr_download?name=xxx
+ Download a specified file from the SD card
+
+ rr_upload?name=xxx&time=yyy
+ Upload a specified file using a POST request. The payload of this request has to be
+ the file content. Only one file may be uploaded at once. When the upload has finished,
+ a JSON response with the variable "err" will be returned, which will be 0 if the job
+ has finished without problems, it will be set to 1 otherwise.
+
+ rr_delete?name=xxx
+ Delete file xxx. Returns err (zero if successful).
+
+ rr_mkdir?dir=xxx
+ Create a new directory xxx. Return err (zero if successful).
+
+ rr_move?old=xxx&new=yyy
+ Rename an old file xxx to yyy. May also be used to move a file to another directory.
+
+ ****************************************************************************************************/
#include "RepRapFirmware.h"
-Webserver::Webserver(Platform* p, Network *n)
- : seq(0)
-{
+//***************************************************************************************************
+
+const char* overflowResponse = "overflow";
+const char* badEscapeResponse = "bad escape";
+
+//********************************************************************************************
+//
+//**************************** Generic Webserver implementation ******************************
+//
+//********************************************************************************************
+
+
+// Constructor and initialisation
+Webserver::Webserver(Platform* p, Network *n) : platform(p), network(n), webserverActive(false)
+{
+ httpInterpreter = new HttpInterpreter(p, this, n);
+ ftpInterpreter = new FtpInterpreter(p, this, n);
+ telnetInterpreter = new TelnetInterpreter(p, this, n);
}
void Webserver::Init()
{
+ // initialise the webserver class
+ longWait = platform->Time();
+ webserverActive = true;
+ readingConnection = nullptr;
+ // initialise all protocol handlers
+ httpInterpreter->ResetState();
+ ftpInterpreter->ResetState();
+ telnetInterpreter->ResetState();
}
+// Deal with input/output from/to the client (if any)
void Webserver::Spin()
{
+ if (webserverActive)
+ {
+ // Check if we can actually send something back to the client
+ if (OutputBuffer::GetBytesLeft(nullptr) == 0)
+ {
+ platform->ClassReport(longWait);
+ return;
+ }
+
+ // We must ensure that we have exclusive access to LWIP
+ if (!network->Lock())
+ {
+ platform->ClassReport(longWait);
+ return;
+ }
+
+ // Allow each ProtocolInterpreter to do something
+ httpInterpreter->Spin();
+ ftpInterpreter->Spin();
+ telnetInterpreter->Spin();
+
+ // See if we have new data to process
+ currentTransaction = network->GetTransaction(readingConnection);
+ if (currentTransaction != nullptr)
+ {
+ // Take care of different protocol types here
+ ProtocolInterpreter *interpreter;
+ uint16_t localPort = currentTransaction->GetLocalPort();
+ switch (localPort)
+ {
+ case FTP_PORT: /* FTP */
+ interpreter = ftpInterpreter;
+ break;
+ case TELNET_PORT: /* Telnet */
+ interpreter = telnetInterpreter;
+ break;
+
+ default: /* HTTP and FTP data */
+ if (localPort == network->GetHttpPort())
+ {
+ interpreter = httpInterpreter;
+ }
+ else
+ {
+ interpreter = ftpInterpreter;
+ }
+ break;
+ }
+
+ // See if we have to print some debug info
+ if (reprap.Debug(moduleWebserver))
+ {
+ const char *type;
+ switch (currentTransaction->GetStatus())
+ {
+ case released: type = "released"; break;
+ case connected: type = "connected"; break;
+ case receiving: type = "receiving"; break;
+ case sending: type = "sending"; break;
+ case disconnected: type = "disconnected"; break;
+ case deferred: type = "deferred"; break;
+ case acquired: type = "acquired"; break;
+ default: type = "unknown"; break;
+ }
+ platform->MessageF(HOST_MESSAGE, "Incoming transaction: Type %s at local port %d (remote port %d)\n",
+ type, localPort, currentTransaction->GetRemotePort());
+ }
+
+ // For protocols other than HTTP it is important to send a HELO message
+ TransactionStatus status = currentTransaction->GetStatus();
+ if (status == connected)
+ {
+ interpreter->ConnectionEstablished();
+ }
+ // Graceful disconnects are handled here, because prior NetworkTransactions might still contain valid
+ // data. That's why it's a bad idea to close these connections immediately in the Network class.
+ else if (status == disconnected)
+ {
+ // This will call the disconnect events and effectively close the connection
+ currentTransaction->Discard();
+ }
+ // Check for fast uploads via this connection
+ else if (interpreter->DoingFastUpload())
+ {
+ interpreter->DoFastUpload();
+ }
+ // Process other messages (if we can)
+ else if (interpreter->CanParseData())
+ {
+ readingConnection = currentTransaction->GetConnection();
+ for(size_t i = 0; i < TCP_MSS / 3; i++)
+ {
+ char c;
+ if (currentTransaction->Read(c))
+ {
+ // Each ProtocolInterpreter must take care of the current NetworkTransaction by
+ // calling either Commit(), Discard() or Defer()
+ if (interpreter->CharFromClient(c))
+ {
+ readingConnection = nullptr;
+ break;
+ }
+ }
+ else
+ {
+ // We ran out of data before finding a complete request. This happens when the incoming
+ // message length exceeds the TCP MSS. Notify the current ProtocolInterpreter about this,
+ // which will remove the current transaction too
+ interpreter->NoMoreDataAvailable();
+ readingConnection = nullptr;
+ break;
+ }
+ }
+ }
+ }
+ else if (readingConnection != nullptr)
+ {
+ // We failed to find a transaction for a reading connection.
+ // This should never happen, but if it does, terminate this connection instantly
+ platform->Message(HOST_MESSAGE, "Error: Transaction for reading connection not found\n");
+ readingConnection->Terminate();
+ }
+ network->Unlock(); // unlock LWIP again
+ }
+ platform->ClassReport(longWait);
}
void Webserver::Exit()
{
+ httpInterpreter->CancelUpload();
+ ftpInterpreter->CancelUpload();
+ //telnetInterpreter->CancelUpload(); // Telnet doesn't support fast file uploads
+ platform->Message(HOST_MESSAGE, "Webserver class exited.\n");
+ webserverActive = false;
}
void Webserver::Diagnostics(MessageType mtype)
{
-
+ platform->Message(mtype, "=== Webserver ===\n");
+ httpInterpreter->Diagnostics(mtype);
+ ftpInterpreter->Diagnostics(mtype);
+ telnetInterpreter->Diagnostics(mtype);
}
bool Webserver::GCodeAvailable(const WebSource source) const
{
+ switch (source)
+ {
+ case WebSource::HTTP:
+ return httpInterpreter->GCodeAvailable();
+
+ case WebSource::Telnet:
+ return telnetInterpreter->GCodeAvailable();
+ }
+
return false;
}
char Webserver::ReadGCode(const WebSource source)
{
+ switch (source)
+ {
+ case WebSource::HTTP:
+ return httpInterpreter->ReadGCode();
+
+ case WebSource::Telnet:
+ return telnetInterpreter->ReadGCode();
+ }
+
return 0;
}
void Webserver::HandleGCodeReply(const WebSource source, OutputBuffer *reply)
{
+ switch (source)
+ {
+ case WebSource::HTTP:
+ httpInterpreter->HandleGCodeReply(reply);
+ break;
+ case WebSource::Telnet:
+ telnetInterpreter->HandleGCodeReply(reply);
+ break;
+ }
}
void Webserver::HandleGCodeReply(const WebSource source, const char *reply)
{
+ switch (source)
+ {
+ case WebSource::HTTP:
+ httpInterpreter->HandleGCodeReply(reply);
+ break;
+
+ case WebSource::Telnet:
+ telnetInterpreter->HandleGCodeReply(reply);
+ break;
+ }
+}
+
+uint16_t Webserver::GetGCodeBufferSpace(const WebSource source) const
+{
+ switch (source)
+ {
+ case WebSource::HTTP:
+ return httpInterpreter->GetGCodeBufferSpace();
+
+ case WebSource::Telnet:
+ return telnetInterpreter->GetGCodeBufferSpace();
+ }
+
+ return 0;
+}
+
+// Handle immediate disconnects here (cs will be freed after this call)
+// May be called by ISR, but not while LwIP is NOT locked
+void Webserver::ConnectionLost(const ConnectionState *cs)
+{
+ // Inform protocol handlers that this connection has been lost
+ uint16_t localPort = cs->GetLocalPort();
+ ProtocolInterpreter *interpreter;
+ switch (localPort)
+ {
+ case FTP_PORT: /* FTP */
+ interpreter = ftpInterpreter;
+ break;
+
+ case TELNET_PORT: /* Telnet */
+ interpreter = telnetInterpreter;
+ break;
+
+ default: /* HTTP and FTP data */
+ if (localPort == network->GetHttpPort())
+ {
+ interpreter = httpInterpreter;
+ break;
+ }
+ else if (localPort == network->GetDataPort())
+ {
+ interpreter = ftpInterpreter;
+ break;
+ }
+
+ platform->MessageF(GENERIC_MESSAGE, "Error: Webserver should handle disconnect event at local port %d, but no handler was found!\n", localPort);
+ return;
+ }
+
+ // Print some debug information and notify the protocol interpreter
+ if (reprap.Debug(moduleWebserver))
+ {
+ platform->MessageF(HOST_MESSAGE, "ConnectionLost called for local port %d (remote port %d)\n", localPort, cs->GetRemotePort());
+ }
+ interpreter->ConnectionLost(cs);
+
+ // Don't process any more data from this connection if has gone down
+ if (readingConnection == cs)
+ {
+ readingConnection = nullptr;
+ }
+}
+
+
+//********************************************************************************************
+//
+//********************** Generic Procotol Interpreter implementation *************************
+//
+//********************************************************************************************
+
+ProtocolInterpreter::ProtocolInterpreter(Platform *p, Webserver *ws, Network *n)
+ : platform(p), webserver(ws), network(n)
+{
+ uploadState = notUploading;
+ filenameBeingUploaded[0] = 0;
+}
+
+void ProtocolInterpreter::Spin()
+{
+ // Check if anything went wrong while writing upload data, and delete the file again if that is the case
+ if (uploadState == uploadError)
+ {
+ if (fileBeingUploaded.IsLive())
+ {
+ fileBeingUploaded.Close();
+ }
+ if (filenameBeingUploaded[0] != 0)
+ {
+ platform->GetMassStorage()->Delete(FS_PREFIX, filenameBeingUploaded);
+ }
+
+ uploadState = notUploading;
+ filenameBeingUploaded[0] = 0;
+ }
+}
+
+void ProtocolInterpreter::ConnectionEstablished()
+{
+ // Don't care about incoming connections by default
+ webserver->currentTransaction->Discard();
+}
+
+void ProtocolInterpreter::NoMoreDataAvailable()
+{
+ // Request is not complete yet, but don't care. Interpreters that do not explicitly
+ // overwrite this method don't support more than one connected client anyway
+ webserver->currentTransaction->Discard();
+}
+
+// Start writing to a new file
+bool ProtocolInterpreter::StartUpload(FileStore *file, const char *fileName)
+{
+ if (file != nullptr)
+ {
+ fileBeingUploaded.Set(file);
+ strncpy(filenameBeingUploaded, fileName, ARRAY_SIZE(filenameBeingUploaded));
+ filenameBeingUploaded[ARRAY_UPB(filenameBeingUploaded)] = 0;
+
+ uploadState = uploadOK;
+ return true;
+ }
+
+ platform->Message(GENERIC_MESSAGE, "Error: Could not open file while starting upload!\n");
+ return false;
+}
+
+void ProtocolInterpreter::CancelUpload()
+{
+ if (uploadState == uploadOK)
+ {
+ // Do the file handling next time when Spin is called
+ uploadState = uploadError;
+ }
+}
+
+void ProtocolInterpreter::DoFastUpload()
+{
+ NetworkTransaction *transaction = webserver->currentTransaction;
+
+ const char *buffer;
+ size_t len;
+ if (transaction->ReadBuffer(buffer, len))
+ {
+ // See if we can output a debug message
+ if (reprap.Debug(moduleWebserver))
+ {
+ platform->MessageF(HOST_MESSAGE, "Writing %u bytes of upload data\n", len);
+ }
+
+ // Writing data usually takes a while, so keep LwIP running while this is being done
+ network->Unlock();
+ if (!fileBeingUploaded.Write(buffer, len))
+ {
+ platform->Message(GENERIC_MESSAGE, "Error: Could not write upload data!\n");
+ CancelUpload();
+
+ while (!network->Lock());
+ transaction->Commit(false);
+ return;
+ }
+ while (!network->Lock());
+ }
+
+ if (uploadState != uploadOK || !transaction->HasMoreDataToRead())
+ {
+ transaction->Discard();
+ }
+}
+
+bool ProtocolInterpreter::FinishUpload(uint32_t fileLength)
+{
+ // Flush remaining data for FSO
+ if (uploadState == uploadOK && !fileBeingUploaded.Flush())
+ {
+ uploadState = uploadError;
+ platform->Message(GENERIC_MESSAGE, "Error: Could not flush remaining data while finishing upload!\n");
+ }
+
+ // Check the file length is as expected
+ if (uploadState == uploadOK && fileLength != 0 && fileBeingUploaded.Length() != fileLength)
+ {
+ uploadState = uploadError;
+ platform->MessageF(GENERIC_MESSAGE, "Error: Uploaded file size is different (%u vs. expected %u bytes)!\n", fileBeingUploaded.Length(), fileLength);
+ }
+
+ // Close the file
+ if (fileBeingUploaded.IsLive())
+ {
+ fileBeingUploaded.Close();
+ }
+
+ // Delete the file again if an error has occurred
+ if (uploadState == uploadError && filenameBeingUploaded[0] != 0)
+ {
+ platform->GetMassStorage()->Delete(FS_PREFIX, filenameBeingUploaded);
+ }
+
+ // Clean up again
+ bool success = (uploadState == uploadOK);
+ uploadState = notUploading;
+ filenameBeingUploaded[0] = 0;
+ return success;
+}
+
+
+//********************************************************************************************
+//
+// *********************** HTTP interpreter for the Webserver class **************************
+//
+//********************************************************************************************
+
+
+
+Webserver::HttpInterpreter::HttpInterpreter(Platform *p, Webserver *ws, Network *n)
+ : ProtocolInterpreter(p, ws, n), state(doingCommandWord), numSessions(0), clientsServed(0)
+{
+ gcodeReadIndex = gcodeWriteIndex = 0;
+ gcodeReply = new OutputStack();
+ deferredRequestConnection = nullptr;
+ seq = 0;
+}
+
+void Webserver::HttpInterpreter::Diagnostics(MessageType mt)
+{
+ platform->MessageF(mt, "HTTP sessions: %d of %d\n", numSessions, maxHttpSessions);
+}
+
+void Webserver::HttpInterpreter::Spin()
+{
+ // Deal with aborted uploads
+ ProtocolInterpreter::Spin();
+
+ // Verify HTTP sessions
+ const uint32_t now = millis();
+ for(int i = numSessions - 1; i >= 0; i--)
+ {
+ if (sessions[i].isPostUploading)
+ {
+ // Check for cancelled POST uploads
+ if (uploadState != uploadOK)
+ {
+ sessions[i].isPostUploading = false;
+ sessions[i].lastQueryTime = millis();
+ }
+ }
+ else if ((now - sessions[i].lastQueryTime) > httpSessionTimeout)
+ {
+ // Check for timed out sessions
+ for(size_t k = i + 1; k < numSessions; k++)
+ {
+ memcpy(&sessions[k - 1], &sessions[k], sizeof(HttpSession));
+ }
+ numSessions--;
+ clientsServed++; // assume the disconnected client hasn't fetched the G-Code reply yet
+ }
+ }
+
+ // If we cannot send the G-Code reply to anyone, we may free up some run-time space by dumping it
+ if (numSessions == 0 || clientsServed >= numSessions)
+ {
+ while (!gcodeReply->IsEmpty())
+ {
+ OutputBuffer::ReleaseAll(gcodeReply->Pop());
+ }
+ clientsServed = 0;
+ }
+
+}
+
+// File Uploads
+
+bool Webserver::HttpInterpreter::DoingFastUpload() const
+{
+ uint32_t remoteIP = webserver->currentTransaction->GetRemoteIP();
+ uint16_t remotePort = webserver->currentTransaction->GetRemotePort();
+ for(size_t i = 0; i < numSessions; i++)
+ {
+ if (sessions[i].ip == remoteIP && sessions[i].isPostUploading)
+ {
+ // There is only one session per IP address...
+ return (sessions[i].postPort == remotePort);
+ }
+ }
+ return false;
+}
+
+void Webserver::HttpInterpreter::DoFastUpload()
+{
+ NetworkTransaction *transaction = webserver->currentTransaction;
+
+ // Write some data on the SD card
+ const char *buffer;
+ size_t len;
+ if (transaction->ReadBuffer(buffer, len))
+ {
+ network->Unlock();
+ // Write data in sector-aligned chunks. This also means that the buffer in fatfs is only used to hold the FAT.
+ static const size_t writeBufLength = 2048; // use a multiple of the 512b sector size
+ static uint32_t writeBufStorage[writeBufLength/4]; // aligned buffer for file writes
+ static size_t writeBufIndex;
+ char* const writeBuf = (char *)writeBufStorage;
+
+ if (uploadedBytes == 0)
+ {
+ writeBufIndex = 0;
+ }
+
+ while (len != 0)
+ {
+ size_t lengthToCopy = min<size_t>(writeBufLength - writeBufIndex, len);
+ memcpy(writeBuf + writeBufIndex, buffer, lengthToCopy);
+ writeBufIndex += lengthToCopy;
+ uploadedBytes += lengthToCopy;
+ buffer += lengthToCopy;
+ len -= lengthToCopy;
+ if (writeBufIndex == writeBufLength || uploadedBytes >= postFileLength)
+ {
+ bool success = fileBeingUploaded.Write(writeBuf, writeBufIndex);
+ writeBufIndex = 0;
+ if (!success)
+ {
+ platform->Message(GENERIC_MESSAGE, "Error: Could not write upload data!\n");
+ CancelUpload();
+
+ while (!network->Lock());
+ SendJsonResponse("upload");
+ return;
+ }
+ }
+ }
+ while (!network->Lock());
+ }
+
+ // See if the upload has finished
+ if (uploadState == uploadOK && uploadedBytes >= postFileLength)
+ {
+ // Reset POST upload state for this client
+ uint32_t remoteIP = transaction->GetRemoteIP();
+ for(size_t i = 0; i < numSessions; i++)
+ {
+ if (sessions[i].ip == remoteIP && sessions[i].isPostUploading)
+ {
+ sessions[i].isPostUploading = false;
+ sessions[i].lastQueryTime = millis();
+ break;
+ }
+ }
+
+ // Grab a copy of the filename and finish this upload
+ char filename[FILENAME_LENGTH];
+ strncpy(filename, filenameBeingUploaded, FILENAME_LENGTH);
+ FinishUpload(postFileLength);
+
+ // Update the file timestamp if it was specified before
+ if (fileLastModified != 0)
+ {
+ (void)platform->GetMassStorage()->SetLastModifiedTime(nullptr, filename, fileLastModified);
+ }
+
+ // Eventually send the JSON response
+ SendJsonResponse("upload");
+ }
+ else if (uploadState != uploadOK || !transaction->HasMoreDataToRead())
+ {
+ // We cannot read any more, discard the transaction again
+ transaction->Discard();
+ }
+}
+
+// Output to the client
+
+// Start sending a file or a JSON response.
+void Webserver::HttpInterpreter::SendFile(const char* nameOfFileToSend, bool isWebFile)
+{
+ NetworkTransaction *transaction = webserver->currentTransaction;
+ FileStore *fileToSend;
+ bool zip = false;
+
+ if (isWebFile)
+ {
+ if (nameOfFileToSend[0] == '/')
+ {
+ ++nameOfFileToSend; // all web files are relative to the /www folder, so remove the leading '/'
+ if (nameOfFileToSend[0] == 0)
+ {
+ nameOfFileToSend = INDEX_PAGE_FILE;
+ }
+ }
+ fileToSend = platform->GetFileStore(platform->GetWebDir(), nameOfFileToSend, false);
+
+ // If we failed to open the file, see if we can open a file with the same name and a ".gz" extension
+ if (fileToSend == nullptr && !StringEndsWith(nameOfFileToSend, ".gz") && strlen(nameOfFileToSend) + 3 <= FILENAME_LENGTH)
+ {
+ char nameBuf[FILENAME_LENGTH + 1];
+ strcpy(nameBuf, nameOfFileToSend);
+ strcat(nameBuf, ".gz");
+ fileToSend = platform->GetFileStore(platform->GetWebDir(), nameBuf, false);
+ if (fileToSend != nullptr)
+ {
+ zip = true;
+ }
+ }
+
+ // If we still couldn't find the file and it was an HTML file, return the 404 error page
+ if (fileToSend == nullptr && (StringEndsWith(nameOfFileToSend, ".html") || StringEndsWith(nameOfFileToSend, ".htm")))
+ {
+ nameOfFileToSend = FOUR04_PAGE_FILE;
+ fileToSend = platform->GetFileStore(platform->GetWebDir(), nameOfFileToSend, false);
+ }
+
+ if (fileToSend == nullptr)
+ {
+ RejectMessage("not found", 404);
+ return;
+ }
+ transaction->SetFileToWrite(fileToSend);
+ }
+ else
+ {
+ fileToSend = platform->GetFileStore(FS_PREFIX, nameOfFileToSend, false);
+ if (fileToSend == nullptr)
+ {
+ nameOfFileToSend = FOUR04_PAGE_FILE;
+ fileToSend = platform->GetFileStore(platform->GetWebDir(), nameOfFileToSend, false);
+ if (fileToSend == nullptr)
+ {
+ RejectMessage("not found", 404);
+ return;
+ }
+ }
+ transaction->SetFileToWrite(fileToSend);
+ }
+
+ transaction->Write("HTTP/1.1 200 OK\n");
+
+ // Don't cache files served by rr_download
+ if (!isWebFile)
+ {
+ transaction->Write("Cache-Control: no-cache, no-store, must-revalidate\n");
+ transaction->Write("Pragma: no-cache\n");
+ transaction->Write("Expires: 0\n");
+ }
+
+ const char* contentType;
+ if (StringEndsWith(nameOfFileToSend, ".png"))
+ {
+ contentType = "image/png";
+ }
+ else if (StringEndsWith(nameOfFileToSend, ".ico"))
+ {
+ contentType = "image/x-icon";
+ }
+ else if (StringEndsWith(nameOfFileToSend, ".js"))
+ {
+ contentType = "application/javascript";
+ }
+ else if (StringEndsWith(nameOfFileToSend, ".css"))
+ {
+ contentType = "text/css";
+ }
+ else if (StringEndsWith(nameOfFileToSend, ".htm") || StringEndsWith(nameOfFileToSend, ".html"))
+ {
+ contentType = "text/html";
+ }
+ else if (StringEndsWith(nameOfFileToSend, ".zip"))
+ {
+ contentType = "application/zip";
+ zip = true;
+ }
+ else if (StringEndsWith(nameOfFileToSend, ".g") || StringEndsWith(nameOfFileToSend, ".gc") || StringEndsWith(nameOfFileToSend, ".gcode"))
+ {
+ contentType = "text/plain";
+ }
+ else
+ {
+ contentType = "application/octet-stream";
+ }
+ transaction->Printf("Content-Type: %s\n", contentType);
+
+ if (zip && fileToSend != nullptr)
+ {
+ transaction->Write("Content-Encoding: gzip\n");
+ transaction->Printf("Content-Length: %lu\n", fileToSend->Length());
+ }
+
+ transaction->Write("Connection: close\n\n");
+ transaction->Commit(false);
+}
+
+void Webserver::HttpInterpreter::SendGCodeReply()
+{
+ // Do we need to keep the G-Code reply for other clients?
+ bool clearReply = false;
+ if (!gcodeReply->IsEmpty())
+ {
+ clientsServed++;
+ if (clientsServed < numSessions)
+ {
+ // Yes - make sure the Network class doesn't discard its buffers yet
+ // NB: This must happen here, because NetworkTransaction::Write() might already release OutputBuffers
+ gcodeReply->IncreaseReferences(1);
+ }
+ else
+ {
+ // No - clean up again later
+ clearReply = true;
+ }
+
+ if (reprap.Debug(moduleWebserver))
+ {
+ platform->MessageF(HOST_MESSAGE, "Sending G-Code reply to client %d of %d (length %u)\n", clientsServed, numSessions, gcodeReply->DataLength());
+ }
+ }
+
+ // Send the whole G-Code reply as plain text to the client
+ NetworkTransaction *transaction = webserver->currentTransaction;
+ transaction->Write("HTTP/1.1 200 OK\n");
+ transaction->Write("Cache-Control: no-cache, no-store, must-revalidate\n");
+ transaction->Write("Pragma: no-cache\n");
+ transaction->Write("Expires: 0\n");
+ transaction->Write("Content-Type: text/plain\n");
+ transaction->Printf("Content-Length: %u\n", gcodeReply->DataLength());
+ transaction->Write("Connection: close\n\n");
+ transaction->Write(gcodeReply);
+ transaction->Commit(false);
+
+ // Possibly clean up the G-code reply once again
+ if (clearReply)
+ {
+ gcodeReply->Clear();
+ }
+}
+
+void Webserver::HttpInterpreter::SendJsonResponse(const char* command)
+{
+ // Try to authorize the user automatically to retain compatibility with the old web interface
+ if (!IsAuthenticated() && reprap.NoPasswordSet())
+ {
+ Authenticate();
+ }
+
+ // Update the authentication status and try handle "text/plain" requests here
+ if (IsAuthenticated())
+ {
+ UpdateAuthentication();
+
+ if (StringEquals(command, "reply")) // rr_reply
+ {
+ SendGCodeReply();
+ return;
+ }
+
+ if (StringEquals(command, "configfile")) // rr_configfile [DEPRECATED]
+ {
+ const char *configPath = platform->GetMassStorage()->CombineName(platform->GetSysDir(), platform->GetConfigFile());
+ char fileName[FILENAME_LENGTH];
+ strncpy(fileName, configPath, FILENAME_LENGTH);
+
+ SendFile(fileName, false);
+ return;
+ }
+
+ if (StringEquals(command, "download") && StringEquals(qualifiers[0].key, "name"))
+ {
+ SendFile(qualifiers[0].value, false);
+ return;
+ }
+ }
+
+ // Try to process a request for JSON responses
+ OutputBuffer *jsonResponse;
+ if (!OutputBuffer::Allocate(jsonResponse))
+ {
+ // Reset the connection immediately if we cannot write any data. Should never happen
+ webserver->currentTransaction->GetConnection()->Terminate();
+ return;
+ }
+
+ bool keepOpen = false;
+ bool mayKeepOpen;
+ if (numQualKeys == 0)
+ {
+ GetJsonResponse(command, jsonResponse, "", "", 0, mayKeepOpen);
+ }
+ else
+ {
+ GetJsonResponse(command, jsonResponse, qualifiers[0].key, qualifiers[0].value, qualifiers[1].key - qualifiers[0].value - 1, mayKeepOpen);
+ }
+
+ // Check special cases of deferred requests (rr_fileinfo) and rejected messages
+ NetworkTransaction *transaction = webserver->currentTransaction;
+ if (transaction->GetStatus() == deferred || transaction->GetStatus() == sending)
+ {
+ OutputBuffer::Release(jsonResponse);
+ return;
+ }
+
+ // Send the JSON response
+
+ if (mayKeepOpen)
+ {
+ // Check that the browser wants to persist the connection too
+ for (size_t i = 0; i < numHeaderKeys; ++i)
+ {
+ if (StringEquals(headers[i].key, "Connection"))
+ {
+ // Comment out the following line to disable persistent connections
+ keepOpen = StringEquals(headers[i].value, "keep-alive");
+ break;
+ }
+ }
+ }
+
+ transaction->Write("HTTP/1.1 200 OK\n");
+ transaction->Write("Cache-Control: no-cache, no-store, must-revalidate\n");
+ transaction->Write("Pragma: no-cache\n");
+ transaction->Write("Expires: 0\n");
+ transaction->Write("Content-Type: application/json\n");
+ transaction->Printf("Content-Length: %u\n", (jsonResponse != nullptr) ? jsonResponse->Length() : 0);
+ transaction->Printf("Connection: %s\n\n", keepOpen ? "keep-alive" : "close");
+ transaction->Write(jsonResponse);
+
+ transaction->Commit(keepOpen);
+}
+
+//----------------------------------------------------------------------------------------------------
+
+// Input from the client
+
+// Get the Json response for this command.
+// 'value' is null-terminated, but we also pass its length in case it contains embedded nulls, which matters when uploading files.
+void Webserver::HttpInterpreter::GetJsonResponse(const char* request, OutputBuffer *&response, const char* key, const char* value, size_t valueLength, bool& keepOpen)
+{
+ keepOpen = false; // assume we don't want to persist the connection
+
+ if (StringEquals(request, "connect") && StringEquals(key, "password"))
+ {
+ if (IsAuthenticated() || reprap.CheckPassword(value))
+ {
+ // Password OK
+ if (Authenticate())
+ {
+ // See if we can update the current RTC date and time
+ if (numQualKeys > 1 && StringEquals(qualifiers[1].key, "time") && !platform->IsDateTimeSet())
+ {
+ struct tm timeInfo;
+ memset(&timeInfo, 0, sizeof(timeInfo));
+ if (strptime(qualifiers[1].value, "%Y-%m-%dT%H:%M:%S", &timeInfo) != nullptr)
+ {
+ time_t newTime = mktime(&timeInfo);
+ platform->SetDateTime(newTime);
+ }
+ }
+
+ // Client has been logged in
+ response->printf("{\"err\":0,\"sessionTimeout\":%u,\"boardType\":\"%s\"}", httpSessionTimeout, platform->GetBoardString());
+ }
+ else
+ {
+ // No more HTTP sessions available
+ response->copy("{\"err\":2}");
+ }
+ }
+ else
+ {
+ // Wrong password
+ response->copy("{\"err\":1}");
+ }
+ }
+ else if (!IsAuthenticated())
+ {
+ RejectMessage("Not authorized", 500);
+ }
+ else if (StringEquals(request, "disconnect"))
+ {
+ response->printf("{\"err\":%d}", RemoveAuthentication() ? 0 : 1);
+ }
+ else if (StringEquals(request, "status"))
+ {
+ int type = 0;
+ if (StringEquals(key, "type"))
+ {
+ // New-style JSON status responses
+ type = atoi(value);
+ if (type < 1 || type > 3)
+ {
+ type = 1;
+ }
+
+ OutputBuffer::Release(response);
+ response = reprap.GetStatusResponse(type, ResponseSource::HTTP);
+ }
+ else
+ {
+ // Deprecated
+ OutputBuffer::Release(response);
+ response = reprap.GetLegacyStatusResponse(1, 0);
+ }
+ }
+ else if (StringEquals(request, "gcode") && StringEquals(key, "gcode"))
+ {
+ LoadGcodeBuffer(value);
+ response->printf("{\"buff\":%u}", GetGCodeBufferSpace());
+ }
+ else if (StringEquals(request, "upload"))
+ {
+ response->printf("{\"err\":%d}", (uploadedBytes == postFileLength) ? 0 : 1);
+ }
+ else if (StringEquals(request, "delete") && StringEquals(key, "name"))
+ {
+ bool ok = platform->GetMassStorage()->Delete(FS_PREFIX, value);
+ response->printf("{\"err\":%d}", (ok) ? 0 : 1);
+ }
+ else if (StringEquals(request, "filelist"))
+ {
+ OutputBuffer::Release(response);
+ response = reprap.GetFilelistResponse(value);
+ }
+ else if (StringEquals(request, "files"))
+ {
+ const char* dir = (StringEquals(key, "dir")) ? value : platform->GetGCodeDir();
+ bool flagDirs = false;
+ if (numQualKeys >= 2)
+ {
+ if (StringEquals(qualifiers[1].key, "flagDirs"))
+ {
+ flagDirs = StringEquals(qualifiers[1].value, "1");
+ }
+ }
+ OutputBuffer::Release(response);
+ response = reprap.GetFilesResponse(dir, flagDirs);
+ }
+ else if (StringEquals(request, "fileinfo"))
+ {
+ if (deferredRequestConnection != nullptr)
+ {
+ // Don't allow multiple deferred requests to be processed at once
+ webserver->currentTransaction->Defer(DeferralMode::ResetData);
+ }
+ else
+ {
+ if (StringEquals(qualifiers[0].key, "name"))
+ {
+ // Regular rr_fileinfo?name=xxx call
+ strncpy(filenameBeingProcessed, value, ARRAY_SIZE(filenameBeingProcessed));
+ filenameBeingProcessed[ARRAY_UPB(filenameBeingProcessed)] = 0;
+ }
+ else
+ {
+ // Simple rr_fileinfo call to get info about the file being printed
+ filenameBeingProcessed[0] = 0;
+ }
+
+ deferredRequestConnection = webserver->currentTransaction->GetConnection();
+ ProcessDeferredRequest();
+ }
+ }
+ else if (StringEquals(request, "move"))
+ {
+ if (numQualKeys >= 2)
+ {
+ if (StringEquals(key, "old") && StringEquals(qualifiers[1].key, "new"))
+ {
+ response->printf("{\"err\":%d}", platform->GetMassStorage()->Rename(value, qualifiers[1].value) ? 0 : 1);
+ }
+ else
+ {
+ response->printf("{\"err\":1}");
+ }
+ }
+ else
+ {
+ response->printf("{\"err\":1}");
+ }
+ }
+ else if (StringEquals(request, "mkdir") && StringEquals(key, "dir"))
+ {
+ bool ok = (platform->GetMassStorage()->MakeDirectory(value));
+ response->printf("{\"err\":%d}", (ok) ? 0 : 1);
+ }
+ else if (StringEquals(request, "config"))
+ {
+ OutputBuffer::Release(response);
+ response = reprap.GetConfigResponse();
+ }
+ else
+ {
+ RejectMessage("Unknown request", 500);
+ }
+}
+
+void Webserver::HttpInterpreter::ResetState()
+{
+ clientPointer = 0;
+ state = doingCommandWord;
+ numCommandWords = 0;
+ numQualKeys = 0;
+ numHeaderKeys = 0;
+ commandWords[0] = clientMessage;
+}
+
+void Webserver::HttpInterpreter::NoMoreDataAvailable()
+{
+ RejectMessage("Incomplete or too long HTTP request", 500);
+}
+
+// May be called from ISR!
+void Webserver::HttpInterpreter::ConnectionLost(const ConnectionState *cs)
+{
+ // Make sure deferred requests are cancelled
+ if (deferredRequestConnection == cs)
+ {
+ reprap.GetPrintMonitor()->StopParsing(filenameBeingProcessed);
+ deferredRequestConnection = nullptr;
+ }
+
+ // If we couldn't read an entire request from a connection, reset our state here again
+ if (webserver->readingConnection == cs)
+ {
+ ResetState();
+ }
+
+ // Deal with aborted POST uploads. Note that we also check the remote port here,
+ // because the client *might* have two instances of the web interface running.
+ if (uploadState == uploadOK)
+ {
+ const uint32_t remoteIP = cs->GetRemoteIP();
+ const uint16_t remotePort = cs->GetRemotePort();
+ for(size_t i = 0; i < numSessions; i++)
+ {
+ if (sessions[i].ip == remoteIP && sessions[i].isPostUploading && sessions[i].postPort == remotePort)
+ {
+ if (reprap.Debug(moduleWebserver))
+ {
+ platform->MessageF(HOST_MESSAGE, "POST upload for '%s' has been cancelled!\n", filenameBeingUploaded);
+ }
+ sessions[i].isPostUploading = false;
+ CancelUpload();
+ break;
+ }
+ }
+ }
+}
+
+bool Webserver::HttpInterpreter::CanParseData()
+{
+ // We want to send a response, but we need memory for that. Check if we have to truncate the G-Code reply
+ while (OutputBuffer::GetBytesLeft(nullptr) < minHttpResponseSize)
+ {
+ if (gcodeReply->IsEmpty())
+ {
+ // We cannot truncate any G-Code reply and don't have enough free space, try again later
+ return false;
+ }
+
+ if (OutputBuffer::Truncate(gcodeReply->GetFirstItem(), minHttpResponseSize) == 0)
+ {
+ // Truncating didn't work out, but see if we can free up a few more bytes by releasing the first reply item
+ OutputBuffer::ReleaseAll(gcodeReply->Pop());
+ }
+ }
+
+ // Are we still processing a deferred request?
+ if (deferredRequestConnection == webserver->currentTransaction->GetConnection())
+ {
+ if (deferredRequestConnection->IsConnected())
+ {
+ // Process more of this request. If it doesn't finish this time, it will be appended to the list
+ // of ready transactions again, which will ensure it can be processed later again
+ ProcessDeferredRequest();
+ }
+ else
+ {
+ // Don't bother with this request if the connection has been closed.
+ // We expect a "disconnected" transaction to report this later, so don't clean up anything here
+ webserver->currentTransaction->Discard();
+ }
+ return false;
+ }
+
+ return true;
+}
+
+// Process a character from the client
+// Rewritten as a state machine by dc42 to increase capability and speed, and reduce RAM requirement.
+// On entry:
+// There is space for at least 1 character in clientMessage.
+// On return:
+// If we return false:
+// We want more characters. There is space for at least 1 character in clientMessage.
+// If we return true:
+// We have processed the message and sent the reply. No more characters may be read from this message.
+// Whenever this calls ProcessMessage:
+// The first line has been split up into words. Variables numCommandWords and commandWords give the number of words we found
+// and the pointers to each word. The second word is treated specially. It is assumed to be a filename followed by an optional
+// qualifier comprising key/value pairs. Both may include %xx escapes, and the qualifier may include + to mean space. We store
+// a pointer to the filename without qualifier in commandWords[1]. We store the qualifier key/value pointers in array 'qualifiers'
+// and the number of them in numQualKeys.
+// The remaining lines have been parsed as header name/value pairs. Pointers to them are stored in array 'headers' and the number
+// of them in numHeaders.
+// If one of our arrays is about to overflow, or the message is not in a format we expect, then we call RejectMessage with an
+// appropriate error code and string.
+bool Webserver::HttpInterpreter::CharFromClient(char c)
+{
+ switch(state)
+ {
+ case doingCommandWord:
+ switch(c)
+ {
+ case '\n':
+ clientMessage[clientPointer++] = 0;
+ ++numCommandWords;
+ numHeaderKeys = 0;
+ headers[0].key = clientMessage + clientPointer;
+ state = doingHeaderKey;
+ break;
+ case '\r':
+ break;
+ case ' ':
+ case '\t':
+ clientMessage[clientPointer++] = 0;
+ if (numCommandWords < maxCommandWords)
+ {
+ ++numCommandWords;
+ commandWords[numCommandWords] = clientMessage + clientPointer;
+ if (numCommandWords == 1)
+ {
+ state = doingFilename;
+ }
+ }
+ else
+ {
+ return RejectMessage("too many command words");
+ }
+ break;
+ default:
+ clientMessage[clientPointer++] = c;
+ break;
+ }
+ break;
+
+ case doingFilename:
+ switch(c)
+ {
+ case '\n':
+ clientMessage[clientPointer++] = 0;
+ ++numCommandWords;
+ numQualKeys = 0;
+ numHeaderKeys = 0;
+ headers[0].key = clientMessage + clientPointer;
+ state = doingHeaderKey;
+ break;
+ case '?':
+ clientMessage[clientPointer++] = 0;
+ ++numCommandWords;
+ numQualKeys = 0;
+ qualifiers[0].key = clientMessage + clientPointer;
+ state = doingQualifierKey;
+ break;
+ case '%':
+ state = doingFilenameEsc1;
+ break;
+ case '\r':
+ break;
+ case ' ':
+ case '\t':
+ clientMessage[clientPointer++] = 0;
+ if (numCommandWords < maxCommandWords)
+ {
+ ++numCommandWords;
+ commandWords[numCommandWords] = clientMessage + clientPointer;
+ state = doingCommandWord;
+ }
+ else
+ {
+ return RejectMessage("too many command words");
+ }
+ break;
+ default:
+ clientMessage[clientPointer++] = c;
+ break;
+ }
+ break;
+
+ case doingQualifierKey:
+ switch(c)
+ {
+ case '=':
+ clientMessage[clientPointer++] = 0;
+ qualifiers[numQualKeys].value = clientMessage + clientPointer;
+ ++numQualKeys;
+ state = doingQualifierValue;
+ break;
+ case '\n': // key with no value
+ case ' ':
+ case '\t':
+ case '\r':
+ case '%': // none of our keys needs escaping, so treat an escape within a key as an error
+ case '&': // key with no value
+ return RejectMessage("bad qualifier key");
+ default:
+ clientMessage[clientPointer++] = c;
+ break;
+ }
+ break;
+
+ case doingQualifierValue:
+ switch(c)
+ {
+ case '\n':
+ clientMessage[clientPointer++] = 0;
+ qualifiers[numQualKeys].key = clientMessage + clientPointer; // so that we can read the whole value even if it contains a null
+ numHeaderKeys = 0;
+ headers[0].key = clientMessage + clientPointer;
+ state = doingHeaderKey;
+ break;
+ case ' ':
+ case '\t':
+ clientMessage[clientPointer++] = 0;
+ qualifiers[numQualKeys].key = clientMessage + clientPointer; // so that we can read the whole value even if it contains a null
+ commandWords[numCommandWords] = clientMessage + clientPointer;
+ state = doingCommandWord;
+ break;
+ case '\r':
+ break;
+ case '%':
+ state = doingQualifierValueEsc1;
+ break;
+ case '&':
+ // Another variable is coming
+ clientMessage[clientPointer++] = 0;
+ qualifiers[numQualKeys].key = clientMessage + clientPointer; // so that we can read the whole value even if it contains a null
+ if (numQualKeys < maxQualKeys)
+ {
+ state = doingQualifierKey;
+ }
+ else
+ {
+ return RejectMessage("too many keys in qualifier");
+ }
+ break;
+ case '+':
+ clientMessage[clientPointer++] = ' ';
+ break;
+ default:
+ clientMessage[clientPointer++] = c;
+ break;
+ }
+ break;
+
+ case doingFilenameEsc1:
+ case doingQualifierValueEsc1:
+ if (c >= '0' && c <= '9')
+ {
+ decodeChar = (c - '0') << 4;
+ state = (HttpState)(state + 1);
+ }
+ else if (c >= 'A' && c <= 'F')
+ {
+ decodeChar = (c - ('A' - 10)) << 4;
+ state = (HttpState)(state + 1);
+ }
+ else
+ {
+ return RejectMessage(badEscapeResponse);
+ }
+ break;
+
+ case doingFilenameEsc2:
+ case doingQualifierValueEsc2:
+ if (c >= '0' && c <= '9')
+ {
+ clientMessage[clientPointer++] = decodeChar | (c - '0');
+ state = (HttpState)(state - 2);
+ }
+ else if (c >= 'A' && c <= 'F')
+ {
+ clientMessage[clientPointer++] = decodeChar | (c - ('A' - 10));
+ state = (HttpState)(state - 2);
+ }
+ else
+ {
+ return RejectMessage(badEscapeResponse);
+ }
+ break;
+
+ case doingHeaderKey:
+ switch(c)
+ {
+ case '\n':
+ if (clientMessage + clientPointer == headers[numHeaderKeys].key) // if the key hasn't started yet, then this is the blank line at the end
+ {
+ if (ProcessMessage())
+ {
+ return true;
+ }
+ }
+ else
+ {
+ return RejectMessage("unexpected newline");
+ }
+ break;
+ case '\r':
+ break;
+ case ':':
+ if (numHeaderKeys == maxHeaders - 1)
+ {
+ return RejectMessage("too many header key-value pairs");
+ }
+ clientMessage[clientPointer++] = 0;
+ headers[numHeaderKeys].value = clientMessage + clientPointer;
+ ++numHeaderKeys;
+ state = expectingHeaderValue;
+ break;
+ default:
+ clientMessage[clientPointer++] = c;
+ break;
+ }
+ break;
+
+ case expectingHeaderValue:
+ if (c == ' ' || c == '\t')
+ {
+ break; // ignore spaces between header key and value
+ }
+ state = doingHeaderValue;
+ // no break
+
+ case doingHeaderValue:
+ if (c == '\n')
+ {
+ state = doingHeaderContinuation;
+ }
+ else if (c != '\r')
+ {
+ clientMessage[clientPointer++] = c;
+ }
+ break;
+
+ case doingHeaderContinuation:
+ switch(c)
+ {
+ case ' ':
+ case '\t':
+ // It's a continuation of the previous value
+ clientMessage[clientPointer++] = c;
+ state = doingHeaderValue;
+ break;
+ case '\n':
+ // It's the blank line
+ clientMessage[clientPointer] = 0;
+ if (ProcessMessage())
+ {
+ return true;
+ }
+ // no break
+ case '\r':
+ break;
+ default:
+ // It's a new key
+ if (clientPointer + 3 <= ARRAY_SIZE(clientMessage))
+ {
+ clientMessage[clientPointer++] = 0;
+ headers[numHeaderKeys].key = clientMessage + clientPointer;
+ clientMessage[clientPointer++] = c;
+ state = doingHeaderKey;
+ }
+ else
+ {
+ return RejectMessage(overflowResponse);
+ }
+ break;
+ }
+ break;
+
+ default:
+ break;
+ }
+
+ if (clientPointer == ARRAY_SIZE(clientMessage))
+ {
+ return RejectMessage(overflowResponse);
+ }
+ return false;
+}
+
+// Process the message received so far. We have reached the end of the headers.
+// Return true if the message is complete, false if we want to continue receiving data (i.e. postdata)
+bool Webserver::HttpInterpreter::ProcessMessage()
+{
+ if (reprap.Debug(moduleWebserver))
+ {
+ platform->MessageF(HOST_MESSAGE, "HTTP req, command words {", numCommandWords);
+ for (size_t i = 0; i < numCommandWords; ++i)
+ {
+ platform->MessageF(HOST_MESSAGE, " %s", commandWords[i]);
+ }
+ platform->Message(HOST_MESSAGE, " }, parameters {");
+
+ for (size_t i = 0; i < numQualKeys; ++i)
+ {
+ platform->MessageF(HOST_MESSAGE, " %s=%s", qualifiers[i].key, qualifiers[i].value);
+ }
+ platform->Message(HOST_MESSAGE, " }\n");
+ }
+
+ if (numCommandWords < 2)
+ {
+ return RejectMessage("too few command words");
+ }
+
+ if (StringEquals(commandWords[0], "GET"))
+ {
+ if (StringStartsWith(commandWords[1], KO_START))
+ {
+ SendJsonResponse(commandWords[1] + KO_FIRST);
+ }
+ else if (commandWords[1][0] == '/' && StringStartsWith(commandWords[1] + 1, KO_START))
+ {
+ SendJsonResponse(commandWords[1] + 1 + KO_FIRST);
+ }
+ else
+ {
+ SendFile(commandWords[1], true);
+ }
+
+ ResetState();
+ return true;
+ }
+ else if (IsAuthenticated() && StringEquals(commandWords[0], "POST"))
+ {
+ bool isUploadRequest = (StringEquals(commandWords[1], KO_START "upload"));
+ isUploadRequest |= (commandWords[1][0] == '/' && StringEquals(commandWords[1] + 1, KO_START "upload"));
+ if (isUploadRequest)
+ {
+ if (numQualKeys > 0 && StringEquals(qualifiers[0].key, "name"))
+ {
+ // We cannot upload more than one file at once
+ if (IsUploading())
+ {
+ return RejectMessage("cannot upload more than one file at once");
+ }
+
+ // See how many bytes we expect to read
+ bool contentLengthFound = false;
+ for(size_t i=0; i<numHeaderKeys; i++)
+ {
+ if (StringEquals(headers[i].key, "Content-Length"))
+ {
+ postFileLength = atoi(headers[i].value);
+ contentLengthFound = true;
+ break;
+ }
+ }
+
+ // Start POST file upload
+ if (!contentLengthFound)
+ {
+ return RejectMessage("invalid POST upload request");
+ }
+
+ // Start a new file upload
+ FileStore *file = platform->GetFileStore(FS_PREFIX, qualifiers[0].value, true);
+ if (!StartUpload(file, qualifiers[0].value))
+ {
+ return RejectMessage("could not start file upload");
+ }
+
+ // Try to get the last modified file date and time
+ if (numQualKeys > 1 && StringEquals(qualifiers[1].key, "time"))
+ {
+ struct tm timeInfo;
+ memset(&timeInfo, 0, sizeof(timeInfo));
+ if (strptime(qualifiers[1].value, "%Y-%m-%dT%H:%M:%S", &timeInfo) != nullptr)
+ {
+ fileLastModified = mktime(&timeInfo);
+ }
+ else
+ {
+ fileLastModified = 0;
+ }
+ }
+ else
+ {
+ fileLastModified = 0;
+ }
+
+ if (reprap.Debug(moduleWebserver))
+ {
+ platform->MessageF(HOST_MESSAGE, "Start uploading file %s length %lu\n", qualifiers[0].value, postFileLength);
+ }
+ uploadedBytes = 0;
+
+ // Keep track of the connection that is now uploading
+ uint32_t remoteIP = webserver->currentTransaction->GetRemoteIP();
+ uint16_t remotePort = webserver->currentTransaction->GetRemotePort();
+ for(size_t i = 0; i < numSessions; i++)
+ {
+ if (sessions[i].ip == remoteIP)
+ {
+ sessions[i].postPort = remotePort;
+ sessions[i].isPostUploading = true;
+ break;
+ }
+ }
+
+ ResetState();
+ return true;
+ }
+ }
+ return RejectMessage("only rr_upload is supported for POST requests");
+ }
+ else
+ {
+ return RejectMessage("Unknown message type or not authenticated");
+ }
+}
+
+// Reject the current message. Always returns true to indicate that we should stop reading the message.
+bool Webserver::HttpInterpreter::RejectMessage(const char* response, unsigned int code)
+{
+ platform->MessageF(HOST_MESSAGE, "Webserver: rejecting message with: %s\n", response);
+
+ NetworkTransaction *transaction = webserver->currentTransaction;
+ transaction->Printf("HTTP/1.1 %u %s\nConnection: close\n\n", code, response);
+ transaction->Commit(false);
+
+ ResetState();
+
+ return true;
+}
+
+// Authenticate current IP and return true on success
+bool Webserver::HttpInterpreter::Authenticate()
+{
+ if (numSessions < maxHttpSessions)
+ {
+ sessions[numSessions].ip = webserver->currentTransaction->GetRemoteIP();
+ sessions[numSessions].lastQueryTime = millis();
+ sessions[numSessions].isPostUploading = false;
+ numSessions++;
+ return true;
+ }
+ return false;
+}
+
+bool Webserver::HttpInterpreter::IsAuthenticated() const
+{
+ const uint32_t remoteIP = webserver->currentTransaction->GetRemoteIP();
+ for(size_t i = 0; i < numSessions; i++)
+ {
+ if (sessions[i].ip == remoteIP)
+ {
+ return true;
+ }
+ }
+ return false;
+}
+
+void Webserver::HttpInterpreter::UpdateAuthentication()
+{
+ const uint32_t remoteIP = webserver->currentTransaction->GetRemoteIP();
+ for(size_t i = 0; i < numSessions; i++)
+ {
+ if (sessions[i].ip == remoteIP)
+ {
+ sessions[i].lastQueryTime = millis();
+ break;
+ }
+ }
+}
+
+bool Webserver::HttpInterpreter::RemoveAuthentication()
+{
+ const uint32_t remoteIP = webserver->currentTransaction->GetRemoteIP();
+ for(int i=(int)numSessions - 1; i>=0; i--)
+ {
+ if (sessions[i].ip == remoteIP)
+ {
+ if (sessions[i].isPostUploading)
+ {
+ // Don't allow sessions with active POST uploads to be removed
+ return false;
+ }
+
+ for (size_t k = i + 1; k < numSessions; ++k)
+ {
+ memcpy(&sessions[k - 1], &sessions[k], sizeof(HttpSession));
+ }
+ numSessions--;
+ return true;
+ }
+ }
+ return false;
+}
+
+// Process a received string of gcodes
+void Webserver::HttpInterpreter::LoadGcodeBuffer(const char* gc)
+{
+ char gcodeTempBuf[GCODE_LENGTH];
+ uint16_t gtp = 0;
+ bool inComment = false;
+ for (;;)
+ {
+ char c = *gc++;
+ if (c == 0)
+ {
+ gcodeTempBuf[gtp] = 0;
+ ProcessGcode(gcodeTempBuf);
+ return;
+ }
+
+ if (c == '\n')
+ {
+ gcodeTempBuf[gtp] = 0;
+ ProcessGcode(gcodeTempBuf);
+ gtp = 0;
+ inComment = false;
+ }
+ else
+ {
+ if (c == ';')
+ {
+ inComment = true;
+ }
+
+ if (gtp == ARRAY_UPB(gcodeTempBuf))
+ {
+ // gcode is too long, we haven't room for another character and a null
+ if (c != ' ' && !inComment)
+ {
+ platform->Message(HOST_MESSAGE, "Error: GCode local buffer overflow in HTTP webserver.\n");
+ return;
+ }
+ // else we're either in a comment or the current character is a space.
+ // If we're in a comment, we'll silently truncate it.
+ // If the current character is a space, we'll wait until we see a non-comment character before reporting an error,
+ // in case the next character is end-of-line or the start of a comment.
+ }
+ else
+ {
+ gcodeTempBuf[gtp++] = c;
+ }
+ }
+ }
+}
+
+// Process a null-terminated gcode
+// We intercept one M Codes so we can deal with emergencies. That
+// way things don't get out of sync, and - as a file name can contain
+// a valid G code (!) - confusion is avoided.
+void Webserver::HttpInterpreter::ProcessGcode(const char* gc)
+{
+ if (StringStartsWith(gc, "M112") && !isdigit(gc[4])) // emergency stop
+ {
+ reprap.EmergencyStop();
+ gcodeReadIndex = gcodeWriteIndex; // clear the buffer
+ reprap.GetGCodes()->Reset();
+ }
+ else
+ {
+ StoreGcodeData(gc, strlen(gc) + 1);
+ }
+}
+
+// Process a received string of gcodes
+void Webserver::HttpInterpreter::StoreGcodeData(const char* data, uint16_t len)
+{
+ if (len > GetGCodeBufferSpace())
+ {
+ platform->Message(HOST_MESSAGE, "Error: GCode buffer overflow in HTTP Webserver!\n");
+ }
+ else
+ {
+ uint16_t remaining = gcodeBufferLength - gcodeWriteIndex;
+ if (len <= remaining)
+ {
+ memcpy(gcodeBuffer + gcodeWriteIndex, data, len);
+ }
+ else
+ {
+ memcpy(gcodeBuffer + gcodeWriteIndex, data, remaining);
+ memcpy(gcodeBuffer, data + remaining, len - remaining);
+ }
+ gcodeWriteIndex = (gcodeWriteIndex + len) % gcodeBufferLength;
+ }
+}
+
+// Feeding G Codes to the GCodes class
+char Webserver::HttpInterpreter::ReadGCode()
+{
+ char c;
+ if (gcodeReadIndex == gcodeWriteIndex)
+ {
+ c = 0;
+ }
+ else
+ {
+ c = gcodeBuffer[gcodeReadIndex];
+ gcodeReadIndex = (gcodeReadIndex + 1u) % gcodeBufferLength;
+ }
+ return c;
+}
+
+// Handle a G Code reply from the GCodes class
+void Webserver::HttpInterpreter::HandleGCodeReply(OutputBuffer *reply)
+{
+ if (reply != nullptr)
+ {
+ if (numSessions > 0)
+ {
+ // FIXME: This might cause G-code responses to be sent twice to fast HTTP clients, but
+ // I (chrishamm) cannot think of a nicer way to deal with slow clients at the moment...
+ gcodeReply->Push(reply);
+ clientsServed = 0;
+ seq++;
+ }
+ else
+ {
+ // Don't use buffers that may never get released...
+ OutputBuffer::ReleaseAll(reply);
+ }
+ }
+}
+
+void Webserver::HttpInterpreter::HandleGCodeReply(const char *reply)
+{
+ if (numSessions > 0)
+ {
+ OutputBuffer *buffer = gcodeReply->GetLastItem();
+ if (buffer == nullptr || buffer->IsReferenced())
+ {
+ if (!OutputBuffer::Allocate(buffer))
+ {
+ // No more space available, stop here
+ return;
+ }
+ gcodeReply->Push(buffer);
+ }
+
+ buffer->cat(reply);
+ clientsServed = 0;
+ seq++;
+ }
+}
+
+// Called to process a deferred request and takes care of the current Webserver transaction
+void Webserver::HttpInterpreter::ProcessDeferredRequest()
+{
+ OutputBuffer *jsonResponse = nullptr;
+ const ConnectionState *lastDeferredConnection = deferredRequestConnection;
+
+ // At the moment only file info requests are deferred.
+ // Parsing the file may take a while, so keep LwIP running while we're waiting
+ network->Unlock();
+ bool gotFileInfo = reprap.GetPrintMonitor()->GetFileInfoResponse(filenameBeingProcessed, jsonResponse);
+ while (!network->Lock());
+
+ // Because LwIP was unlocked before, there is a chance that the ConnectionLost() call has already
+ // stopped the file parsing. Check this special case here
+ if (lastDeferredConnection == deferredRequestConnection)
+ {
+ NetworkTransaction *transaction = webserver->currentTransaction;
+ if (gotFileInfo)
+ {
+ deferredRequestConnection = nullptr;
+
+ // Got it - send the response now
+ transaction->Write("HTTP/1.1 200 OK\n");
+ transaction->Write("Cache-Control: no-cache, no-store, must-revalidate\n");
+ transaction->Write("Pragma: no-cache\n");
+ transaction->Write("Expires: 0\n");
+ transaction->Write("Content-Type: application/json\n");
+ transaction->Printf("Content-Length: %u\n", (jsonResponse != nullptr) ? jsonResponse->Length() : 0);
+ transaction->Printf("Connection: close\n\n");
+ transaction->Write(jsonResponse);
+
+ transaction->Commit(false);
+ }
+ else
+ {
+ // File hasn't been fully parsed yet, try again later
+ transaction->Defer(DeferralMode::DiscardData);
+ }
+ }
+ else
+ {
+ // Clean up again if we cannot send the response at all
+ OutputBuffer::ReleaseAll(jsonResponse);
+ }
+}
+
+//********************************************************************************************
+//
+//************************* FTP interpreter for the Webserver class **************************
+//
+//********************************************************************************************
+
+Webserver::FtpInterpreter::FtpInterpreter(Platform *p, Webserver *ws, Network *n)
+ : ProtocolInterpreter(p, ws, n), state(authenticating), clientPointer(0)
+{
+ connectedClients = 0;
+ strcpy(currentDir, "/");
+}
+
+void Webserver::FtpInterpreter::Diagnostics(MessageType mt)
+{
+ platform->MessageF(mt, "FTP connections: %d, state %d\n", connectedClients, state);
+}
+
+void Webserver::FtpInterpreter::ConnectionEstablished()
+{
+ connectedClients++;
+ if (reprap.Debug(moduleWebserver))
+ {
+ platform->Message(HOST_MESSAGE, "Webserver: FTP connection established!\n");
+ }
+
+ // Is this a new connection on the data port?
+ NetworkTransaction *transaction = webserver->currentTransaction;
+ if (transaction->GetLocalPort() != FTP_PORT)
+ {
+ if (state == waitingForPasvPort)
+ {
+ // Yes - save it for the main request
+ network->SaveDataConnection();
+ state = pasvPortConnected;
+ transaction->Discard();
+ }
+ else
+ {
+ // Should never get here...
+ transaction->Commit(false);
+ }
+ return;
+ }
+
+ // A client is trying to connect to the main FTP port
+ switch (state)
+ {
+ case idle:
+ case authenticated: // added by DC because without it, we can't transfer any files with FileZilla
+ // We can safely deal with one connection on the main FTP port
+ state = authenticating;
+ SendReply(220, "RepRapFirmware FTP server", true);
+ break;
+
+ default:
+ // But don't allow multiple ones, this could mess things up
+ SendReply(421, "Only one client can be connected at a time.", false);
+ return;
+ }
+}
+
+// May be called from ISR!
+void Webserver::FtpInterpreter::ConnectionLost(const ConnectionState *cs)
+{
+ connectedClients--;
+
+ if (cs->GetLocalPort() != FTP_PORT)
+ {
+ // Did everything work out? Usually this is only called for uploads
+ if (network->AcquireFTPTransaction())
+ {
+ webserver->currentTransaction = network->GetTransaction();
+ if (state == doingPasvIO)
+ {
+ if (uploadState != uploadError && !cs->IsTerminated())
+ {
+ SendReply(226, "Transfer complete.");
+ FinishUpload(0);
+ }
+ else
+ {
+ SendReply(526, "Transfer failed!");
+ }
+ }
+ else
+ {
+ SendReply(550, "Lost data connection!");
+ }
+ }
+
+ // Close the data port and reset our state again
+ network->CloseDataPort();
+ CancelUpload();
+ state = authenticated;
+ }
+
+ if (connectedClients == 0)
+ {
+ // Last one gone now...
+ ResetState();
+ }
+}
+
+bool Webserver::FtpInterpreter::CharFromClient(char c)
+{
+ if (clientPointer == ARRAY_UPB(clientMessage))
+ {
+ clientPointer = 0;
+ platform->Message(HOST_MESSAGE, "Webserver: Buffer overflow in FTP server!\n");
+ return true;
+ }
+
+ switch (c)
+ {
+ case 0:
+ break;
+
+ case '\r':
+ case '\n':
+ clientMessage[clientPointer++] = 0;
+
+ if (reprap.Debug(moduleWebserver))
+ {
+ platform->MessageF(HOST_MESSAGE, "FtpInterpreter::ProcessLine called with state %d:\n%s\n", state, clientMessage);
+ }
+
+ if (clientPointer > 1) // only process a new line if we actually received data
+ {
+ ProcessLine();
+ clientPointer = 0;
+ return true;
+ }
+
+ if (reprap.Debug(moduleWebserver))
+ {
+ platform->Message(HOST_MESSAGE, "FtpInterpreter::ProcessLine call finished.\n");
+ }
+
+ clientPointer = 0;
+ break;
+
+ default:
+ clientMessage[clientPointer++] = c;
+ break;
+ }
+
+ return false;
+}
+
+void Webserver::FtpInterpreter::ResetState()
+{
+ clientPointer = 0;
+ strcpy(currentDir, "/");
+
+ network->CloseDataPort();
+ CancelUpload();
+
+ state = idle;
+}
+
+bool Webserver::FtpInterpreter::DoingFastUpload() const
+{
+ return (IsUploading() && webserver->currentTransaction->GetLocalPort() == network->GetDataPort());
+}
+
+// return true if an error has occurred, false otherwise
+void Webserver::FtpInterpreter::ProcessLine()
+{
+ switch (state)
+ {
+ case idle:
+ case authenticating:
+ // don't check the user name
+ if (StringStartsWith(clientMessage, "USER"))
+ {
+ SendReply(331, "Please specify the password.");
+ }
+ // but check the password
+ else if (StringStartsWith(clientMessage, "PASS"))
+ {
+ char pass[PASSWORD_LENGTH];
+ int pass_length = 0;
+ bool reading_pass = false;
+ for(size_t i = 4; i < clientPointer && i < PASSWORD_LENGTH + 3; i++)
+ {
+ reading_pass |= (clientMessage[i] != ' ' && clientMessage[i] != '\t');
+ if (reading_pass)
+ {
+ pass[pass_length++] = clientMessage[i];
+ }
+ }
+ pass[pass_length] = 0;
+
+ if (reprap.CheckPassword(pass))
+ {
+ state = authenticated;
+ SendReply(230, "Login successful.");
+ }
+ else
+ {
+ SendReply(530, "Login incorrect.", false);
+ }
+ }
+ // if it's different, send response 500 to indicate we don't know the code (might be AUTH or so)
+ else
+ {
+ SendReply(500, "Unknown login command.");
+ }
+
+ break;
+
+ case authenticated:
+ // get system type
+ if (StringEquals(clientMessage, "SYST"))
+ {
+ SendReply(215, "UNIX Type: L8");
+ }
+ // get features
+ else if (StringEquals(clientMessage, "FEAT"))
+ {
+ SendFeatures();
+ }
+ // get current dir
+ else if (StringEquals(clientMessage, "PWD"))
+ {
+ NetworkTransaction *transaction = webserver->currentTransaction;
+ transaction->Printf("257 \"%s\"\r\n", currentDir);
+ transaction->Commit(true);
+ }
+ // set current dir
+ else if (StringStartsWith(clientMessage, "CWD"))
+ {
+ ReadFilename(3);
+ ChangeDirectory(filename);
+ }
+ // change to parent of current directory
+ else if (StringEquals(clientMessage, "CDUP"))
+ {
+ ChangeDirectory("..");
+ }
+ // switch transfer mode (sends response, but doesn't have any effects)
+ else if (StringStartsWith(clientMessage, "TYPE"))
+ {
+ for(size_t i = 4; i < clientPointer; i++)
+ {
+ if (clientMessage[i] == 'I')
+ {
+ SendReply(200, "Switching to Binary mode.");
+ return;
+ }
+
+ if (clientMessage[i] == 'A')
+ {
+ SendReply(200, "Switching to ASCII mode.");
+ return;
+ }
+ }
+
+ SendReply(500, "Unknown command.");
+ }
+ // enter passive mode mode
+ else if (StringEquals(clientMessage, "PASV"))
+ {
+ /* get local IP address */
+ const uint8_t * const ip_address = network->GetIPAddress();
+
+ /* open random port > 1023 */
+ //rand(); // TRNG doesn't require this
+ uint16_t pasv_port = random(1024, 65535);
+ network->OpenDataPort(pasv_port);
+ portOpenTime = millis();
+ state = waitingForPasvPort;
+
+ /* send FTP response */
+ NetworkTransaction *transaction = webserver->currentTransaction;
+ transaction->Printf("227 Entering Passive Mode (%d,%d,%d,%d,%d,%d)\r\n",
+ ip_address[0], ip_address[1], ip_address[2], ip_address[3],
+ pasv_port / 256, pasv_port % 256);
+ transaction->Commit(true);
+ }
+ // PASV commands are not supported in this state
+ else if (StringEquals(clientMessage, "LIST") || StringStartsWith(clientMessage, "RETR") || StringStartsWith(clientMessage, "STOR"))
+ {
+ SendReply(425, "Use PASV first.");
+ }
+ // delete file
+ else if (StringStartsWith(clientMessage, "DELE"))
+ {
+ ReadFilename(4);
+ if (platform->GetMassStorage()->Delete(currentDir, filename))
+ {
+ SendReply(250, "Delete operation successful.");
+ }
+ else
+ {
+ SendReply(550, "Delete operation failed.");
+ }
+ }
+ // delete directory
+ else if (StringStartsWith(clientMessage, "RMD"))
+ {
+ ReadFilename(3);
+ if (platform->GetMassStorage()->Delete(currentDir, filename))
+ {
+ SendReply(250, "Remove directory operation successful.");
+ }
+ else
+ {
+ SendReply(550, "Remove directory operation failed.");
+ }
+ }
+ // make new directory
+ else if (StringStartsWith(clientMessage, "MKD"))
+ {
+ ReadFilename(3);
+ const char *location = (filename[0] == '/')
+ ? filename
+ : platform->GetMassStorage()->CombineName(currentDir, filename);
+
+ if (platform->GetMassStorage()->MakeDirectory(location))
+ {
+ NetworkTransaction *transaction = webserver->currentTransaction;
+ transaction->Printf("257 \"%s\" created\r\n", location);
+ transaction->Commit(true);
+ }
+ else
+ {
+ SendReply(550, "Create directory operation failed.");
+ }
+ }
+ // rename file or directory
+ else if (StringStartsWith(clientMessage, "RNFR"))
+ {
+ ReadFilename(4);
+ if (filename[0] != '/')
+ {
+ const char *temp = platform->GetMassStorage()->CombineName(currentDir, filename);
+ strncpy(filename, temp, FILENAME_LENGTH);
+ filename[FILENAME_LENGTH - 1] = 0;
+ }
+
+ if (platform->GetMassStorage()->FileExists(filename))
+ {
+ SendReply(350, "Ready to RNTO.");
+ }
+ else
+ {
+ SendReply(550, "Invalid file or directory.");
+ }
+ }
+ else if (StringStartsWith(clientMessage, "RNTO"))
+ {
+ // Copy origin path to temp oldFilename and read new path
+ char oldFilename[FILENAME_LENGTH];
+ strncpy(oldFilename, filename, FILENAME_LENGTH);
+ oldFilename[FILENAME_LENGTH - 1] = 0;
+ ReadFilename(4);
+
+ const char *newFilename = platform->GetMassStorage()->CombineName(currentDir, filename);
+ if (platform->GetMassStorage()->Rename(oldFilename, newFilename))
+ {
+ SendReply(250, "Rename successful.");
+ }
+ else
+ {
+ SendReply(500, "Could not rename file or directory.");
+ }
+ }
+ // no op
+ else if (StringEquals(clientMessage, "NOOP"))
+ {
+ SendReply(200, "NOOP okay.");
+ }
+ // end connection
+ else if (StringEquals(clientMessage, "QUIT"))
+ {
+ SendReply(221, "Goodbye.", false);
+ ResetState();
+ }
+ // unknown
+ else
+ {
+ SendReply(500, "Unknown command.");
+ }
+
+ break;
+
+ case waitingForPasvPort:
+ if (millis() - portOpenTime > ftpPasvPortTimeout)
+ {
+ SendReply(425, "Failed to establish connection.");
+
+ network->CloseDataPort();
+ state = authenticated;
+ }
+ else
+ {
+ webserver->currentTransaction->Defer(DeferralMode::ResetData);
+ }
+
+ break;
+
+ case pasvPortConnected:
+ // save current connection state so we can send '226 Transfer complete.' when ConnectionLost() is called
+ network->SaveFTPConnection();
+
+ // list directory entries
+ if (StringEquals(clientMessage, "LIST"))
+ {
+ if (network->AcquireDataTransaction())
+ {
+ // send announcement via ftp main port
+ SendReply(150, "Here comes the directory listing.");
+
+ // send directory listing via data port
+ NetworkTransaction *dataTransaction = network->GetTransaction();
+
+ FileInfo fileInfo;
+ if (platform->GetMassStorage()->FindFirst(currentDir, fileInfo))
+ {
+ do {
+ // Example for a typical UNIX-like file list:
+ // "drwxr-xr-x 2 ftp ftp 0 Apr 11 2013 bin\r\n"
+ const char dirChar = (fileInfo.isDirectory) ? 'd' : '-';
+ const struct tm * const timeInfo = gmtime(&fileInfo.lastModified);
+ dataTransaction->Printf("%crw-rw-rw- 1 ftp ftp %13lu %s %02d %04d %s\r\n",
+ dirChar, fileInfo.size, platform->GetMassStorage()->GetMonthName(timeInfo->tm_mon + 1),
+ timeInfo->tm_mday, timeInfo->tm_year + 1900, fileInfo.fileName);
+ } while (platform->GetMassStorage()->FindNext(fileInfo));
+ }
+
+ dataTransaction->Commit(false);
+ state = doingPasvIO;
+ }
+ else
+ {
+ SendReply(500, "Unknown error.");
+ network->CloseDataPort();
+ state = authenticated;
+ }
+ }
+ // upload a file
+ else if (StringStartsWith(clientMessage, "STOR"))
+ {
+ ReadFilename(4);
+
+ FileStore *file = platform->GetFileStore(currentDir, filename, true);
+ if (StartUpload(file, filename))
+ {
+ SendReply(150, "OK to send data.");
+ state = doingPasvIO;
+ }
+ else
+ {
+ SendReply(550, "Failed to open file.");
+ network->CloseDataPort();
+ state = authenticated;
+ }
+ }
+ // download a file
+ else if (StringStartsWith(clientMessage, "RETR"))
+ {
+ ReadFilename(4);
+
+ FileStore *file = platform->GetFileStore(currentDir, filename, false);
+ if (file == nullptr)
+ {
+ SendReply(550, "Failed to open file.");
+ }
+ else
+ {
+ if (network->AcquireDataTransaction())
+ {
+ // send announcement via main ftp port
+ NetworkTransaction *transaction = webserver->currentTransaction;
+ transaction->Printf("150 Opening data connection for %s (%lu bytes).\r\n", filename, file->Length());
+ transaction->Commit(true);
+
+ // send the file via data port
+ NetworkTransaction *dataTransaction = network->GetTransaction();
+ dataTransaction->SetFileToWrite(file);
+ dataTransaction->Commit(false);
+ state = doingPasvIO;
+ }
+ else
+ {
+ file->Close();
+ SendReply(500, "Unknown error.");
+ network->CloseDataPort();
+ state = authenticated;
+ }
+ }
+ }
+ // unknown command
+ else
+ {
+ SendReply(500, "Unknown command.");
+ network->CloseDataPort();
+ state = authenticated;
+ }
+
+ break;
+
+ case doingPasvIO:
+ // abort current transfer
+ if (StringEquals(clientMessage, "ABOR"))
+ {
+ if (IsUploading())
+ {
+ CancelUpload();
+ SendReply(226, "ABOR successful.");
+ }
+ else
+ {
+ network->CloseDataPort();
+ SendReply(226, "ABOR successful.");
+ }
+ }
+ // unknown command
+ else
+ {
+ SendReply(500, "Unknown command.");
+ network->CloseDataPort();
+ state = authenticated;
+ }
+
+ break;
+ }
+}
+
+void Webserver::FtpInterpreter::SendReply(int code, const char *message, bool keepConnection)
+{
+ NetworkTransaction *transaction = webserver->currentTransaction;
+ transaction->Printf("%d %s\r\n", code, message);
+ transaction->Commit(keepConnection);
+}
+
+void Webserver::FtpInterpreter::SendFeatures()
+{
+ NetworkTransaction *transaction = webserver->currentTransaction;
+ transaction->Write("211-Features:\r\n");
+ transaction->Write("PASV\r\n"); // support PASV mode
+ transaction->Write("211 End\r\n");
+ transaction->Commit(true);
+}
+
+void Webserver::FtpInterpreter::ReadFilename(uint16_t start)
+{
+ int filenameLength = 0;
+ bool readingPath = false;
+ for(int i = start; i < (int)clientPointer && filenameLength < (int)(FILENAME_LENGTH - 1); i++)
+ {
+ switch (clientMessage[i])
+ {
+ // ignore quotes
+ case '"':
+ case '\'':
+ break;
+
+ // skip whitespaces unless the actual filename is being read
+ case ' ':
+ case '\t':
+ if (readingPath)
+ {
+ filename[filenameLength++] = clientMessage[i];
+ }
+ break;
+
+ // read path name
+ default:
+ readingPath = true;
+ filename[filenameLength++] = clientMessage[i];
+ break;
+ }
+ }
+ filename[filenameLength] = 0;
+}
+
+void Webserver::FtpInterpreter::ChangeDirectory(const char *newDirectory)
+{
+ char combinedPath[FILENAME_LENGTH];
+
+ if (newDirectory[0] != 0)
+ {
+ /* Prepare the new directory path */
+ if (newDirectory[0] == '/') // absolute path
+ {
+ strncpy(combinedPath, newDirectory, FILENAME_LENGTH);
+ combinedPath[FILENAME_LENGTH - 1] = 0;
+ }
+ else // relative path
+ {
+ if (StringEquals(newDirectory, "..")) // go up
+ {
+ if (StringEquals(currentDir, "/"))
+ {
+ // we're already at the root, so we can't go up any more
+ SendReply(550, "Failed to change directory.");
+ return;
+ }
+ else
+ {
+ strncpy(combinedPath, currentDir, FILENAME_LENGTH);
+ for(int i=strlen(combinedPath) -2; i>=0; i--)
+ {
+ if (combinedPath[i] == '/')
+ {
+ combinedPath[i +1] = 0;
+ break;
+ }
+ }
+ }
+ }
+ else // go to child directory
+ {
+ strncpy(combinedPath, currentDir, FILENAME_LENGTH);
+ if (strlen(currentDir) > 1)
+ {
+ strncat(combinedPath, "/", FILENAME_LENGTH - strlen(combinedPath) - 1);
+ }
+ strncat(combinedPath, newDirectory, FILENAME_LENGTH - strlen(combinedPath) - 1);
+ }
+ }
+
+ /* Make sure the new path does not end with a '/', because FatFs won't see the directory otherwise */
+ if (StringEndsWith(combinedPath, "/") && strlen(combinedPath) > 1)
+ {
+ combinedPath[strlen(combinedPath) -1] = 0;
+ }
+
+ /* Verify path and change it */
+ if (platform->GetMassStorage()->DirectoryExists(combinedPath))
+ {
+ strncpy(currentDir, combinedPath, FILENAME_LENGTH);
+ SendReply(250, "Directory successfully changed.");
+ }
+ else
+ {
+ SendReply(550, "Failed to change directory.");
+ }
+ }
+ else
+ {
+ SendReply(550, "Failed to change directory.");
+ }
+}
+
+
+//********************************************************************************************
+//
+//*********************** Telnet interpreter for the Webserver class *************************
+//
+//********************************************************************************************
+
+Webserver::TelnetInterpreter::TelnetInterpreter(Platform *p, Webserver *ws, Network *n)
+ : ProtocolInterpreter(p, ws, n), connectedClients(0), processNextLine(false), gcodeReadIndex(0), gcodeWriteIndex(0), gcodeReply(nullptr)
+{
+ ResetState();
+}
+
+void Webserver::TelnetInterpreter::Diagnostics(MessageType mt)
+{
+ platform->MessageF(mt, "Telnet connections: %d, state %d\n", connectedClients, state);
+}
+
+void Webserver::TelnetInterpreter::ConnectionEstablished()
+{
+ connectedClients++;
+ NetworkTransaction *transaction = network->GetTransaction();
+
+ // Only one client may be connected via Telnet at once, so check this first
+ if (state != idle)
+ {
+ transaction->Write("Sorry, only one client may be connected via Telnet at once.\r\n");
+ transaction->Commit(false);
+ return;
+ }
+ state = justConnected;
+ connectTime = millis();
+
+ // Check whether we need a password to log in
+ if (reprap.NoPasswordSet())
+ {
+ // Don't send a login prompt if no password is set, so we don't mess up Pronterface
+ transaction->Discard();
+ }
+ else
+ {
+ transaction->Write("RepRapFirmware Telnet interface\r\n\r\n");
+ transaction->Write("Please enter your password:\r\n");
+ transaction->Write("> ");
+ transaction->Commit(true);
+ }
+}
+
+// May be called from ISR!
+void Webserver::TelnetInterpreter::ConnectionLost(const ConnectionState *cs)
+{
+ connectedClients--;
+ if (connectedClients == 0)
+ {
+ ResetState();
+
+ // Don't save up output buffers if they can't be sent
+ OutputBuffer::ReleaseAll(gcodeReply);
+ gcodeReply = nullptr;
+ }
+}
+
+bool Webserver::TelnetInterpreter::CanParseData()
+{
+ // Is this an acquired transaction using which we can send the G-code reply?
+ TransactionStatus status = webserver->currentTransaction->GetStatus();
+ if (status == acquired)
+ {
+ SendGCodeReply();
+ return false;
+ }
+
+ // Is this connection still live? Check that for deferred requests
+ if (status == deferred && !webserver->currentTransaction->IsConnected())
+ {
+ webserver->currentTransaction->Discard();
+ return false;
+ }
+
+ // In order to support TCP streaming mode, check if we can store any more data at this time
+ if (GetGCodeBufferSpace() < clientPointer + 1)
+ {
+ webserver->currentTransaction->Defer(DeferralMode::DeferOnly);
+ return false;
+ }
+
+ // If that works and if the next line hasn't been processed yet, do it now
+ if (processNextLine)
+ {
+ return !ProcessLine();
+ }
+
+ // Otherwise just parse the next request
+ return true;
+}
+
+bool Webserver::TelnetInterpreter::CharFromClient(char c)
+{
+ // If this is likely to be a Telnet setup message (with some garbage in it), dump the first
+ // received packet and move on to the next state
+ if (state == justConnected)
+ {
+ if (reprap.NoPasswordSet())
+ {
+ state = authenticated;
+ network->SaveTelnetConnection();
+ }
+ else
+ {
+ state = authenticating;
+ }
+
+ if (millis() - connectTime < telnetSetupDuration)
+ {
+ network->GetTransaction()->Discard();
+ return true;
+ }
+ }
+
+ // Otherwise try to read one line at a time
+ switch (c)
+ {
+ case 0:
+ break;
+
+ case '\b':
+ // Allow backspace for pure Telnet clients like PuTTY
+ if (clientPointer != 0)
+ {
+ clientPointer--;
+ }
+ break;
+
+ case '\r':
+ case '\n':
+ if (clientPointer != 0)
+ {
+ // This line is complete, do we have enough space left to store it?
+ clientMessage[clientPointer] = 0;
+ if (GetGCodeBufferSpace() < clientPointer + 1)
+ {
+ // No - defer this transaction, so we can process more of it next time
+ webserver->currentTransaction->Defer(DeferralMode::DeferOnly);
+ processNextLine = true;
+ return true;
+ }
+
+ // Yes - try to process it
+ return ProcessLine();
+ }
+ break;
+
+ default:
+ clientMessage[clientPointer++] = c;
+
+ // Make sure we don't overflow the line buffer
+ if (clientPointer == ARRAY_UPB(clientMessage))
+ {
+ clientPointer = 0;
+ platform->Message(HOST_MESSAGE, "Webserver: Buffer overflow in Telnet server!\n");
+ return true;
+ }
+ break;
+ }
+
+ return false;
+}
+
+void Webserver::TelnetInterpreter::ResetState()
+{
+ state = idle;
+ connectTime = 0;
+ clientPointer = 0;
+ gcodeReadIndex = gcodeWriteIndex; // clear the buffer
+}
+
+// Usually we should not try to send any data here, because that would purge the packet's
+// payload and mess with TCP streaming mode if Pronterface is used. However, under special
+// circumstances this must happen and in this case this method must always return true.
+bool Webserver::TelnetInterpreter::ProcessLine()
+{
+ processNextLine = false;
+ clientPointer = 0;
+
+ NetworkTransaction *transaction = network->GetTransaction();
+ switch (state)
+ {
+ case idle:
+ case justConnected:
+ // Should never get here...
+ // no break
+
+ case authenticating:
+ if (reprap.CheckPassword(clientMessage))
+ {
+ network->SaveTelnetConnection();
+ state = authenticated;
+
+ transaction->Write("Log in successful!\r\n");
+ transaction->Commit(true);
+ }
+ else
+ {
+ transaction->Write("Invalid password.\r\n> ");
+ transaction->Commit(true);
+ }
+ return true;
+
+ case authenticated:
+ // Special commands for Telnet
+ if (StringEquals(clientMessage, "exit") || StringEquals(clientMessage, "quit"))
+ {
+ transaction->Write("Goodbye.\r\n");
+ transaction->Commit(false);
+ return true;
+ }
+ // All other codes are stored for the GCodes class
+ ProcessGcode(clientMessage);
+ break;
+ }
+ return false;
+}
+
+// Process a null-terminated gcode
+// We intercept one M Codes so we can deal with emergencies. That
+// way things don't get out of sync, and - as a file name can contain
+// a valid G code (!) - confusion is avoided.
+void Webserver::TelnetInterpreter::ProcessGcode(const char* gc)
+{
+ if (StringStartsWith(gc, "M112") && !isdigit(gc[4])) // emergency stop
+ {
+ reprap.EmergencyStop();
+ gcodeReadIndex = gcodeWriteIndex; // clear the buffer
+ reprap.GetGCodes()->Reset();
+ }
+ else
+ {
+ StoreGcodeData(gc, strlen(gc) + 1);
+ }
+}
+
+// Process a received string of gcodes
+void Webserver::TelnetInterpreter::StoreGcodeData(const char* data, uint16_t len)
+{
+ if (len > GetGCodeBufferSpace())
+ {
+ platform->Message(HOST_MESSAGE, "Error: GCode buffer overflow in Telnet Webserver!\n");
+ }
+ else
+ {
+ uint16_t remaining = gcodeBufferLength - gcodeWriteIndex;
+ if (len <= remaining)
+ {
+ memcpy(gcodeBuffer + gcodeWriteIndex, data, len);
+ }
+ else
+ {
+ memcpy(gcodeBuffer + gcodeWriteIndex, data, remaining);
+ memcpy(gcodeBuffer, data + remaining, len - remaining);
+ }
+ gcodeWriteIndex = (gcodeWriteIndex + len) % gcodeBufferLength;
+ }
+}
+
+// Feeding G Codes to the GCodes class
+char Webserver::TelnetInterpreter::ReadGCode()
+{
+ char c;
+ if (gcodeReadIndex == gcodeWriteIndex)
+ {
+ c = 0;
+ }
+ else
+ {
+ c = gcodeBuffer[gcodeReadIndex];
+ gcodeReadIndex = (gcodeReadIndex + 1u) % gcodeBufferLength;
+ }
+ return c;
+}
+
+// Handle a G-Code reply from the GCodes class; replace \n with \r\n
+void Webserver::TelnetInterpreter::HandleGCodeReply(OutputBuffer *reply)
+{
+ if (reply != nullptr && state >= authenticated)
+ {
+ if (!network->AcquireTelnetTransaction())
+ {
+ // We must be able to send the response to the client on the next Spin call
+ return;
+ }
+
+ // We need a valid OutputBuffer to start the conversion from NL to CRNL
+ if (gcodeReply == nullptr)
+ {
+ OutputBuffer *buffer;
+ if (!OutputBuffer::Allocate(buffer))
+ {
+ OutputBuffer::Truncate(reply, OUTPUT_BUFFER_SIZE);
+ if (!OutputBuffer::Allocate(buffer))
+ {
+ // If we're really short on memory, release the G-Code reply instantly
+ OutputBuffer::ReleaseAll(reply);
+ return;
+ }
+ }
+ gcodeReply = buffer;
+ }
+
+ // Write entire content to new output buffers, but this time with \r\n instead of \n
+ do {
+ const char *data = reply->Data();
+ for(size_t i = 0; i < reply->DataLength(); i++)
+ {
+ if (*data == '\n')
+ {
+ gcodeReply->cat('\r');
+ }
+
+ gcodeReply->cat(*data);
+ data++;
+ }
+ reply = OutputBuffer::Release(reply);
+ } while (reply != nullptr);
+ }
+ else
+ {
+ // Don't store buffers that may never get released...
+ OutputBuffer::ReleaseAll(reply);
+ }
+}
+
+void Webserver::TelnetInterpreter::HandleGCodeReply(const char *reply)
+{
+ if (reply != nullptr && state >= authenticated)
+ {
+ if (!network->AcquireTelnetTransaction())
+ {
+ // We must be able to send the response to the client on the next Spin call
+ return;
+ }
+
+ // We need a valid OutputBuffer to start the conversion from NL to CRNL
+ if (gcodeReply == nullptr)
+ {
+ OutputBuffer *buffer;
+ if (!OutputBuffer::Allocate(buffer))
+ {
+ // No more space available to store this reply, stop here
+ return;
+ }
+ gcodeReply = buffer;
+ }
+
+ // Write entire content to new output buffers, but this time with \r\n instead of \n
+ while (*reply != 0)
+ {
+ if (*reply == '\n' && gcodeReply->cat('\r') == 0)
+ {
+ // No more space available, stop here
+ return;
+ }
+ if (gcodeReply->cat(*reply) == 0)
+ {
+ // No more space available, stop here
+ return;
+ }
+ reply++;
+ };
+ }
+}
+
+void Webserver::TelnetInterpreter::SendGCodeReply()
+{
+ NetworkTransaction *transaction = webserver->currentTransaction;
+
+ if (gcodeReply == nullptr)
+ {
+ transaction->Discard();
+ }
+ else
+ {
+ transaction->Write(gcodeReply);
+ transaction->Commit(true);
+ }
+ gcodeReply = nullptr;
}
-// End
+// vim: ts=4:sw=4
diff --git a/src/DuetNG/DuetEthernet/Webserver.h b/src/DuetNG/DuetEthernet/Webserver.h
index c03388c2..1b4975bc 100644
--- a/src/DuetNG/DuetEthernet/Webserver.h
+++ b/src/DuetNG/DuetEthernet/Webserver.h
@@ -30,8 +30,40 @@ Licence: GPL
#ifndef WEBSERVER_H
#define WEBSERVER_H
-#include <cstdint>
-#include <cstddef>
+/* Generic values */
+
+const size_t gcodeBufferLength = 512; // size of our gcode ring buffer, preferably a power of 2
+const unsigned int TCP_MSS = 1460;
+
+class ConnectionState;
+class NetworkTransaction;
+
+/* HTTP */
+
+#define KO_START "rr_"
+#define KO_FIRST 3
+
+const uint16_t webMessageLength = TCP_MSS; // maximum length of the web message we accept after decoding
+const size_t minHttpResponseSize = 768; // minimum number of bytes required for an HTTP response
+
+const size_t maxCommandWords = 4; // max number of space-separated words in the command
+const size_t maxQualKeys = 5; // max number of key/value pairs in the qualifier
+const size_t maxHeaders = 16; // max number of key/value pairs in the headers
+
+const size_t maxHttpSessions = 8; // maximum number of simultaneous HTTP sessions
+const uint32_t httpSessionTimeout = 8000; // HTTP session timeout in milliseconds
+
+/* FTP */
+
+const uint16_t ftpMessageLength = 128; // maximum line length for incoming FTP commands
+const uint32_t ftpPasvPortTimeout = 10000; // maximum time to wait for an FTP data connection in milliseconds
+
+/* Telnet */
+
+const uint32_t telnetSetupDuration = 4000; // ignore the first Telnet request within this duration (in ms)
+
+
+class Webserver;
// List of protocols that can execute G-Codes
enum class WebSource
@@ -40,20 +72,56 @@ enum class WebSource
Telnet
};
-const uint16_t gcodeBufferLength = 512; // size of our gcode ring buffer, preferably a power of 2
-const uint16_t webMessageLength = 2000; // maximum length of the web message we accept after decoding
-const size_t maxQualKeys = 5; // max number of key/value pairs in the qualifier
-const size_t maxHttpSessions = 8; // maximum number of simultaneous HTTP sessions
-const uint32_t httpSessionTimeout = 20000; // HTTP session timeout in milliseconds
+// This is the abstract class for all supported protocols
+// Any inherited class should implement a state machine to increase performance and reduce memory usage.
+class ProtocolInterpreter
+{
+ public:
+
+ ProtocolInterpreter(Platform *p, Webserver *ws, Network *n);
+ virtual ~ProtocolInterpreter() { } // to keep Eclipse happy
+ virtual void Diagnostics(MessageType mtype) = 0;
+ virtual void Spin();
+
+ virtual void ConnectionEstablished();
+ virtual void ConnectionLost(const ConnectionState *cs) { }
+ virtual bool CanParseData();
+ virtual bool CharFromClient(const char c) = 0;
+ virtual void NoMoreDataAvailable();
+
+ virtual bool DoingFastUpload() const;
+ virtual void DoFastUpload();
+ void CancelUpload(); // may be called from ISR!
+
+ protected:
-class Platform;
-class Network;
+ Platform *platform;
+ Webserver *webserver;
+ Network *network;
+
+ // Information for file uploading
+ enum UploadState
+ {
+ notUploading, // no upload in progress
+ uploadOK, // upload in progress, no error so far
+ uploadError // upload in progress but had error
+ };
+
+ UploadState uploadState;
+ FileData fileBeingUploaded;
+ char filenameBeingUploaded[FILENAME_LENGTH];
+
+ bool StartUpload(FileStore *file, const char *fileName);
+ bool IsUploading() const;
+ bool FinishUpload(uint32_t fileLength);
+};
class Webserver
{
-public:
+ public:
friend class Platform;
+ friend class ProtocolInterpreter;
Webserver(Platform* p, Network *n);
void Init();
@@ -65,12 +133,252 @@ public:
char ReadGCode(const WebSource source);
void HandleGCodeReply(const WebSource source, OutputBuffer *reply);
void HandleGCodeReply(const WebSource source, const char *reply);
- uint32_t GetReplySeq() const { return seq; }
+ uint32_t GetReplySeq() const;
+
// Returns the available G-Code buffer space of the HTTP interpreter (may be dropped in a future version)
- uint16_t GetGCodeBufferSpace(const WebSource source) const { return 0; }
+ uint16_t GetGCodeBufferSpace(const WebSource source) const;
+
+ void ConnectionLost(const ConnectionState *cs);
+ void ConnectionError();
+
+ protected:
+
+ class HttpInterpreter : public ProtocolInterpreter
+ {
+ public:
+
+ HttpInterpreter(Platform *p, Webserver *ws, Network *n);
+ void Spin();
+ void Diagnostics(MessageType mtype) override;
+ void ConnectionLost(const ConnectionState *cs);
+ bool CanParseData() override;
+ bool CharFromClient(const char c) override;
+ void NoMoreDataAvailable() override;
+ void ResetState();
+ void ResetSessions();
+
+ bool DoingFastUpload() const override;
+ void DoFastUpload();
+
+ bool GCodeAvailable() const;
+ char ReadGCode();
+ void HandleGCodeReply(OutputBuffer *reply);
+ void HandleGCodeReply(const char *reply);
+ uint16_t GetGCodeBufferSpace() const;
+ uint32_t GetReplySeq() const;
+
+ private:
+
+ // HTTP server state enumeration. The order is important, in particular xxxEsc1 must follow xxx, and xxxEsc2 must follow xxxEsc1.
+ // We assume that qualifier keys do not contain escapes, because none of ours needs to be encoded. If we are sent escapes in the key,
+ // it won't do any harm, but the key won't be recognised even if it would be valid were it decoded.
+ enum HttpState
+ {
+ doingCommandWord, // receiving a word in the first line of the HTTP request
+ doingFilename, // receiving the filename (second word in the command line)
+ doingFilenameEsc1, // received '%' in the filename (e.g. we are being asked for a filename with spaces in it)
+ doingFilenameEsc2, // received '%' and one hex digit in the filename
+ doingQualifierKey, // receiving a key name in the HTTP request
+ doingQualifierValue, // receiving a key value in the HTTP request
+ doingQualifierValueEsc1, // received '%' in the qualifier
+ doingQualifierValueEsc2, // received '%' and one hex digit in the qualifier
+ doingHeaderKey, // receiving a header key
+ expectingHeaderValue, // expecting a header value
+ doingHeaderValue, // receiving a header value
+ doingHeaderContinuation // received a newline after a header value
+ };
+ HttpState state;
+
+ struct KeyValueIndices
+ {
+ const char* key;
+ const char* value;
+ };
+
+ void SendFile(const char* nameOfFileToSend, bool isWebFile);
+ void SendGCodeReply();
+ void SendJsonResponse(const char* command);
+ void GetJsonResponse(const char* request, OutputBuffer *&response, const char* key, const char* value, size_t valueLength, bool& keepOpen);
+ bool ProcessMessage();
+ bool RejectMessage(const char* s, unsigned int code = 500);
+
+ // Buffers for processing HTTP input
+ char clientMessage[webMessageLength + 3]; // holds the command, qualifier, and headers
+ size_t clientPointer; // current index into clientMessage
+ char decodeChar;
+
+ const char* commandWords[maxCommandWords];
+ KeyValueIndices qualifiers[maxQualKeys + 1]; // offsets into clientQualifier of the key/value pairs, the +1 is needed so that values can contain nulls
+ KeyValueIndices headers[maxHeaders]; // offsets into clientHeader of the key/value pairs
+ size_t numCommandWords;
+ size_t numQualKeys; // number of qualifier keys we have found, <= maxQualKeys
+ size_t numHeaderKeys; // number of keys we have found, <= maxHeaders
+
+ // HTTP sessions
+ struct HttpSession
+ {
+ uint32_t ip;
+ uint32_t lastQueryTime;
+ bool isPostUploading;
+ uint16_t postPort;
+ };
+
+ HttpSession sessions[maxHttpSessions];
+ uint8_t numSessions;
+ uint8_t clientsServed;
+
+ bool Authenticate();
+ bool IsAuthenticated() const;
+ void UpdateAuthentication();
+ bool RemoveAuthentication();
-private:
- uint32_t seq;
+ // Deal with incoming G-Codes
+
+ char gcodeBuffer[gcodeBufferLength];
+ uint16_t gcodeReadIndex, gcodeWriteIndex; // head and tail indices into gcodeBuffer
+
+ void LoadGcodeBuffer(const char* gc);
+ void ProcessGcode(const char* gc);
+ void StoreGcodeData(const char* data, uint16_t len);
+
+ // Responses from GCodes class
+
+ uint32_t seq; // Sequence number for G-Code replies
+ OutputStack *gcodeReply;
+
+ // File uploads
+ uint32_t postFileLength, uploadedBytes; // How many POST bytes do we expect and how many have already been written?
+ time_t fileLastModified;
+
+ // Deferred requests (rr_fileinfo)
+ ConnectionState * volatile deferredRequestConnection; // Which connection expects a response for a deferred request?
+ char filenameBeingProcessed[FILENAME_LENGTH]; // The filename being processed (for rr_fileinfo)
+
+ void ProcessDeferredRequest();
+ };
+ HttpInterpreter *httpInterpreter;
+
+ class FtpInterpreter : public ProtocolInterpreter
+ {
+ public:
+
+ FtpInterpreter(Platform *p, Webserver *ws, Network *n);
+ void Diagnostics(MessageType mtype) override;
+
+ void ConnectionEstablished() override;
+ void ConnectionLost(const ConnectionState *cs) override;
+ bool CharFromClient(const char c) override;
+ void ResetState();
+
+ bool DoingFastUpload() const override;
+
+ private:
+
+ enum FtpState
+ {
+ idle, // no client connected
+ authenticating, // not logged in
+ authenticated, // logged in
+ waitingForPasvPort, // waiting for connection to be established on PASV port
+ pasvPortConnected, // client connected to PASV port, ready to send data
+ doingPasvIO // client is connected and data is being transferred
+ };
+ FtpState state;
+ uint8_t connectedClients;
+
+ char clientMessage[ftpMessageLength];
+ size_t clientPointer;
+
+ char filename[FILENAME_LENGTH];
+ char currentDir[FILENAME_LENGTH];
+
+ uint32_t portOpenTime;
+
+ void ProcessLine();
+ void SendReply(int code, const char *message, bool keepConnection = true);
+ void SendFeatures();
+
+ void ReadFilename(uint16_t start);
+ void ChangeDirectory(const char *newDirectory);
+ };
+ FtpInterpreter *ftpInterpreter;
+
+ class TelnetInterpreter : public ProtocolInterpreter
+ {
+ public:
+
+ TelnetInterpreter(Platform *p, Webserver *ws, Network *n);
+ void Diagnostics(MessageType mtype) override;
+
+ void ConnectionEstablished() override;
+ void ConnectionLost(const ConnectionState *cs) override;
+ bool CanParseData() override;
+ bool CharFromClient(const char c) override;
+ void ResetState();
+
+ bool GCodeAvailable() const;
+ char ReadGCode();
+ void HandleGCodeReply(OutputBuffer *reply);
+ void HandleGCodeReply(const char *reply);
+ uint16_t GetGCodeBufferSpace() const;
+
+ void SendGCodeReply();
+
+ private:
+
+ enum TelnetState
+ {
+ idle, // not connected
+ justConnected, // not logged in, but the client has just connected
+ authenticating, // not logged in
+ authenticated // logged in
+ };
+ TelnetState state;
+ uint8_t connectedClients;
+ uint32_t connectTime;
+
+ bool processNextLine;
+ char clientMessage[GCODE_LENGTH];
+ size_t clientPointer;
+
+ bool ProcessLine();
+
+ // Deal with incoming G-Codes
+
+ char gcodeBuffer[gcodeBufferLength];
+ uint16_t gcodeReadIndex, gcodeWriteIndex; // head and tail indices into gcodeBuffer
+
+ void ProcessGcode(const char* gc);
+ void StoreGcodeData(const char* data, uint16_t len);
+
+ // Converted response from GCodes class (NL -> CRNL)
+
+ OutputBuffer * volatile gcodeReply;
+ };
+ TelnetInterpreter *telnetInterpreter;
+
+ private:
+
+ Platform* platform;
+ Network* network;
+ bool webserverActive;
+ NetworkTransaction *currentTransaction;
+ ConnectionState * volatile readingConnection;
+
+ float longWait;
};
+inline bool ProtocolInterpreter::CanParseData() { return true; }
+inline bool ProtocolInterpreter::DoingFastUpload() const { return false; }
+inline bool ProtocolInterpreter::IsUploading() const { return uploadState != notUploading; }
+
+inline uint32_t Webserver::GetReplySeq() const { return httpInterpreter->GetReplySeq(); }
+
+inline uint16_t Webserver::HttpInterpreter::GetGCodeBufferSpace() const { return (gcodeReadIndex - gcodeWriteIndex - 1u) % gcodeBufferLength; }
+inline bool Webserver::HttpInterpreter::GCodeAvailable() const { return gcodeReadIndex != gcodeWriteIndex; }
+inline uint32_t Webserver::HttpInterpreter::GetReplySeq() const { return seq; }
+
+inline uint16_t Webserver::TelnetInterpreter::GetGCodeBufferSpace() const { return (gcodeReadIndex - gcodeWriteIndex - 1u) % gcodeBufferLength; }
+inline bool Webserver::TelnetInterpreter::GCodeAvailable() const { return gcodeReadIndex != gcodeWriteIndex; }
+
#endif
diff --git a/src/GCodes/GCodeMachineState.h b/src/GCodes/GCodeMachineState.h
index 74d41965..db8fdd01 100644
--- a/src/GCodes/GCodeMachineState.h
+++ b/src/GCodes/GCodeMachineState.h
@@ -21,9 +21,7 @@ enum class GCodeState : uint8_t
normal, // not doing anything and ready to process a new GCode
waitingForMoveToComplete, // doing a homing move, so we must wait for it to finish before processing another GCode
homing,
- setBed1,
- setBed2,
- setBed3,
+ setBed,
// These next 3 must be contiguous
toolChange1,
diff --git a/src/GCodes/GCodes.cpp b/src/GCodes/GCodes.cpp
index 81f59e43..a4f249d7 100644
--- a/src/GCodes/GCodes.cpp
+++ b/src/GCodes/GCodes.cpp
@@ -101,6 +101,7 @@ void GCodes::Init()
retractLength = retractExtra = retractHop = 0.0;
retractSpeed = unRetractSpeed = 600.0;
+ isRetracted = false;
}
// This is called from Init and when doing an emergency stop
@@ -233,23 +234,14 @@ void GCodes::Spin()
}
break;
- case GCodeState::setBed1:
- reprap.GetMove()->SetIdentityTransform();
- probeCount = 0;
- gb.SetState(GCodeState::setBed2);
- // no break
-
- case GCodeState::setBed2:
+ case GCodeState::setBed:
+ if (DoSingleZProbeAtPoint(gb, probeCount, 0.0))
{
- int numProbePoints = reprap.GetMove()->NumberOfXYProbePoints();
- if (DoSingleZProbeAtPoint(gb, probeCount, 0.0))
+ probeCount++;
+ if (probeCount >= reprap.GetMove()->NumberOfXYProbePoints())
{
- probeCount++;
- if (probeCount >= numProbePoints)
- {
- reprap.GetMove()->FinishedBedProbing(0, reply);
- gb.SetState(GCodeState::normal);
- }
+ reprap.GetMove()->FinishedBedProbing(0, reply);
+ gb.SetState(GCodeState::normal);
}
}
break;
@@ -1078,12 +1070,19 @@ unsigned int GCodes::LoadMoveBufferFromGCode(GCodeBuffer& gb, int moveType)
{
moveArg += moveBuffer.coords[axis];
}
- else if (currentTool != nullptr && moveType == 0)
+ else if (moveType == 0)
{
- moveArg -= currentTool->GetOffset()[axis]; // adjust requested position to compensate for tool offset
+ if (axis == Z_AXIS && isRetracted)
+ {
+ moveArg += retractHop; // handle firmware retraction on layer change
+ }
+ if (currentTool != nullptr)
+ {
+ moveArg -= currentTool->GetOffset()[axis]; // adjust requested position to compensate for tool offset
+ }
}
- if (axis < Z_AXIS && moveType == 0)
+ if (axis != Z_AXIS && moveType == 0)
{
const HeightMap& heightMap = reprap.GetMove()->AccessBedProbeGrid();
if (heightMap.UsingHeightMap())
@@ -1352,6 +1351,7 @@ bool GCodes::DoCannedCycleMove(GCodeBuffer& gb, EndstopChecks ce)
{
return true; // stack overflow
}
+ gb.MachineState().state = gb.MachineState().previous->state; // stay in the same state
for (size_t drive = 0; drive < DRIVES; drive++)
{
@@ -1569,7 +1569,7 @@ bool GCodes::DoHome(GCodeBuffer& gb, StringRef& reply, bool& error)
// probes the bed height, and records the Z coordinate probed. If you want to program any general
// internal canned cycle, this shows how to do it.
// On entry, probePointIndex specifies which of the points this is.
-bool GCodes::DoSingleZProbeAtPoint(GCodeBuffer& gb, int probePointIndex, float heightAdjust)
+bool GCodes::DoSingleZProbeAtPoint(GCodeBuffer& gb, size_t probePointIndex, float heightAdjust)
{
reprap.GetMove()->SetIdentityTransform(); // It doesn't matter if these are called repeatedly
@@ -3001,7 +3001,7 @@ void GCodes::StartToolChange(GCodeBuffer& gb, bool inM109)
// Retract or un-retract filament, returning true if movement has been queued, false if this needs to be called again
bool GCodes::RetractFilament(GCodeBuffer& gb, bool retract)
{
- if (retractLength != 0.0 || retractHop != 0.0 || (!retract && retractExtra != 0.0))
+ if (retract != isRetracted && (retractLength != 0.0 || retractHop != 0.0 || (!retract && retractExtra != 0.0)))
{
const Tool *tool = reprap.GetCurrentTool();
if (tool != nullptr)
@@ -3039,6 +3039,7 @@ bool GCodes::RetractFilament(GCodeBuffer& gb, bool retract)
segmentsLeft = 1;
}
}
+ isRetracted = retract;
}
return true;
}
diff --git a/src/GCodes/GCodes.h b/src/GCodes/GCodes.h
index 04a8aeb3..fa145eef 100644
--- a/src/GCodes/GCodes.h
+++ b/src/GCodes/GCodes.h
@@ -166,7 +166,7 @@ private:
bool DoDwell(GCodeBuffer& gb); // Wait for a bit
bool DoDwellTime(float dwell); // Really wait for a bit
bool DoHome(GCodeBuffer& gb, StringRef& reply, bool& error); // Home some axes
- bool DoSingleZProbeAtPoint(GCodeBuffer& gb, int probePointIndex, float heightAdjust); // Probe at a given point
+ bool DoSingleZProbeAtPoint(GCodeBuffer& gb, size_t probePointIndex, float heightAdjust); // Probe at a given point
bool DoSingleZProbe(GCodeBuffer& gb, StringRef& reply, bool reportOnly, float heightAdjust); // Probe where we are
int DoZProbe(GCodeBuffer& gb, float distance); // Do a Z probe cycle up to the maximum specified distance
bool SetSingleZProbeAtAPosition(GCodeBuffer& gb, StringRef& reply); // Probes at a given position - see the comment at the head of the function itself
@@ -262,7 +262,7 @@ private:
const char* eofString; // What's at the end of an HTML file?
uint8_t eofStringCounter; // Check the...
uint8_t eofStringLength; // ... EoF string as we read.
- int probeCount; // Counts multiple probe points
+ size_t probeCount; // Counts multiple probe points
int8_t cannedCycleMoveCount; // Counts through internal (i.e. not macro) canned cycle moves
bool cannedCycleMoveQueued; // True if a canned cycle move has been set
float longWait; // Timer for things that happen occasionally (seconds)
@@ -287,6 +287,7 @@ private:
float retractSpeed; // retract speed in mm/min
float unRetractSpeed; // un=retract speed in mm/min
float retractHop; // Z hop when retracting
+ bool isRetracted; // true if filament has been firmware-retracted
// Triggers
Trigger triggers[MaxTriggers]; // Trigger conditions
diff --git a/src/GCodes/GCodes2.cpp b/src/GCodes/GCodes2.cpp
index 9524110c..34c0baab 100644
--- a/src/GCodes/GCodes2.cpp
+++ b/src/GCodes/GCodes2.cpp
@@ -220,7 +220,9 @@ bool GCodes::HandleGcode(GCodeBuffer& gb, StringRef& reply)
// If we get here then we are not on a delta printer and there is no bed.g file
if (GetAxisIsHomed(X_AXIS) && GetAxisIsHomed(Y_AXIS))
{
- gb.SetState(GCodeState::setBed1); // no bed.g file, so use the coordinates specified by M557
+ probeCount = 0;
+ reprap.GetMove()->SetIdentityTransform();
+ gb.SetState(GCodeState::setBed); // no bed.g file, so use the coordinates specified by M557
}
else
{
@@ -1958,14 +1960,14 @@ bool GCodes::HandleMcode(GCodeBuffer& gb, StringRef& reply)
case 376: // Set taper height
{
- HeightMap& heightMap = reprap.GetMove()->AccessBedProbeGrid();
+ Move *move = reprap.GetMove();
if (gb.Seen('H'))
{
- heightMap.SetTaperHeight(gb.GetFValue());
+ move->SetTaperHeight(gb.GetFValue());
}
- else if (heightMap.GetTaperHeight() > 0.0)
+ else if (move->GetTaperHeight() > 0.0)
{
- reply.printf("Bed compensation taper height is %.1fmm", heightMap.GetTaperHeight());
+ reply.printf("Bed compensation taper height is %.1fmm", move->GetTaperHeight());
}
else
{
diff --git a/src/Movement/Grid.cpp b/src/Movement/Grid.cpp
index 4aa21f7d..ac1709ca 100644
--- a/src/Movement/Grid.cpp
+++ b/src/Movement/Grid.cpp
@@ -109,7 +109,7 @@ void GridDefinition::PrintError(StringRef& r) const
// Increase the version number in the following string whenever we change the format of the height map file.
const char *HeightMap::HeightMapComment = "RepRapFirmware height map file v1";
-HeightMap::HeightMap(float *heightStorage) : gridHeights(heightStorage), useMap(false), useTaper(false) { }
+HeightMap::HeightMap(float *heightStorage) : gridHeights(heightStorage), useMap(false) { }
void HeightMap::SetGrid(const GridDefinition& gd)
{
@@ -316,46 +316,6 @@ void HeightMap::UseHeightMap(bool b)
useMap = b && def.IsValid();
}
-void HeightMap::SetTaperHeight(float h)
-{
- useTaper = (h > 1.0);
- if (useTaper)
- {
- taperHeight = h;
- recipTaperHeight = 1.0/h;
- }
-}
-
-// Compute the height error at the specified point i.e. value that needs to be added to the Z coordinate
-float HeightMap::ComputeHeightError(float x, float y, float z) const
-{
- if (!useMap || (useTaper && z >= taperHeight))
- {
- return 0.0;
- }
-
- const float rawError = GetInterpolatedHeightError(x, y);
- return (useTaper) ? (taperHeight - z) * recipTaperHeight * rawError : rawError;
-}
-
-// Compute the inverse height error at the specified point i.e. value that needs to be subtracted form the Z coordinate
-float HeightMap::ComputeInverseHeightError(float x, float y, float z) const
-{
- if (!useMap)
- {
- return 0.0;
- }
-
- const float rawError = GetInterpolatedHeightError(x, y);
- if (!useTaper || rawError > taperHeight) // need check on rawError to avoid possible divide by zero
- {
- return rawError;
- }
-
- const float zreq = (z - rawError)/(1.0 - (rawError * recipTaperHeight));
- return (zreq >= taperHeight) ? 0.0 : z - zreq;
-}
-
// Compute the height error at the specified point
float HeightMap::GetInterpolatedHeightError(float x, float y) const
{
diff --git a/src/Movement/Grid.h b/src/Movement/Grid.h
index 89f332fa..6dba8b38 100644
--- a/src/Movement/Grid.h
+++ b/src/Movement/Grid.h
@@ -68,8 +68,7 @@ public:
const GridDefinition& GetGrid() const { return def; }
void SetGrid(const GridDefinition& gd);
- float ComputeHeightError(float x, float y, float z) const; // Compute the height error at the specified point
- float ComputeInverseHeightError(float x, float y, float z) const; // Compute the inverse height error at the specified point
+ float GetInterpolatedHeightError(float x, float y) const; // Compute the interpolated height error at the specified point
void ClearGridHeights(); // Clear all grid height corrections
void SetGridHeight(size_t xIndex, size_t yIndex, float height); // Set the height of a grid point
@@ -82,8 +81,6 @@ public:
void UseHeightMap(bool b);
bool UsingHeightMap() const { return useMap; }
- float GetTaperHeight() const { return (useTaper) ? taperHeight : 0.0; }
- void SetTaperHeight(float h);
unsigned int GetStatistics(float& mean, float& deviation) const; // Return number of points probed, mean and RMS deviation
@@ -93,17 +90,11 @@ private:
GridDefinition def;
float *gridHeights; // The map of grid heights, must have at least MaxGridProbePoints entries
uint32_t gridHeightSet[MaxGridProbePoints/32]; // Bitmap of which heights are set
- float taperHeight; // Height over which we taper
- float recipTaperHeight; // Reciprocal of the taper height
bool useMap; // True to do bed compensation
- bool useTaper; // True to taper off the compensation
uint32_t GetMapIndex(uint32_t xIndex, uint32_t yIndex) const { return (yIndex * def.NumXpoints()) + xIndex; }
bool IsHeightSet(uint32_t index) const { return (gridHeightSet[index/32] & (1 << (index & 31))) != 0; }
- float GetInterpolatedHeightError(float x, float y) const // Compute the interpolated height error at the specified point
- pre(useMap);
-
float GetHeightError(uint32_t xIndex, uint32_t yIndex) const;
float InterpolateX(uint32_t xIndex, uint32_t yIndex, float xFrac) const;
float InterpolateY(uint32_t xIndex, uint32_t yIndex, float yFrac) const;
diff --git a/src/Movement/Move.cpp b/src/Movement/Move.cpp
index e32bcecf..a2fec5ca 100644
--- a/src/Movement/Move.cpp
+++ b/src/Movement/Move.cpp
@@ -88,6 +88,7 @@ void Move::Init()
xRectangle = 1.0/(0.8 * reprap.GetPlatform()->AxisMaximum(X_AXIS));
yRectangle = xRectangle;
+ useTaper = false;
longWait = reprap.GetPlatform()->Time();
idleTimeout = DEFAULT_IDLE_TIMEOUT;
@@ -641,46 +642,51 @@ void Move::InverseTransform(float xyzPoint[MAX_AXES], uint32_t xAxes) const
// Do the bed transform AFTER the axis transform
void Move::BedTransform(float xyzPoint[MAX_AXES], uint32_t xAxes) const
{
- float zCorrection = 0.0;
- const size_t numAxes = reprap.GetGCodes()->GetNumAxes();
- unsigned int numXAxes = 0;
-
- // Transform the Z coordinate based on the average correction for each axis used as an X axis.
- // We are assuming that the tool Y offsets are small enough to be ignored.
- for (uint32_t axis = 0; axis < numAxes; ++axis)
+ if (!useTaper || xyzPoint[Z_AXIS] < taperHeight)
{
- if ((xAxes & (1u << axis)) != 0)
+ float zCorrection = 0.0;
+ const size_t numAxes = reprap.GetGCodes()->GetNumAxes();
+ unsigned int numXAxes = 0;
+
+ // Transform the Z coordinate based on the average correction for each axis used as an X axis.
+ // We are assuming that the tool Y offsets are small enough to be ignored.
+ for (uint32_t axis = 0; axis < numAxes; ++axis)
{
- const float xCoord = xyzPoint[axis];
- switch(numBedCompensationPoints)
+ if ((xAxes & (1u << axis)) != 0)
{
- case 0:
- zCorrection += grid.ComputeHeightError(xCoord, xyzPoint[Y_AXIS], xyzPoint[Z_AXIS]);
- break;
+ const float xCoord = xyzPoint[axis];
+ switch(numBedCompensationPoints)
+ {
+ case 0:
+ zCorrection += grid.GetInterpolatedHeightError(xCoord, xyzPoint[Y_AXIS]);
+ break;
- case 3:
- zCorrection += aX * xCoord + aY * xyzPoint[Y_AXIS] + aC;
- break;
+ case 3:
+ zCorrection += aX * xCoord + aY * xyzPoint[Y_AXIS] + aC;
+ break;
- case 4:
- zCorrection += SecondDegreeTransformZ(xCoord, xyzPoint[Y_AXIS]);
- break;
+ case 4:
+ zCorrection += SecondDegreeTransformZ(xCoord, xyzPoint[Y_AXIS]);
+ break;
- case 5:
- zCorrection += TriangleZ(xCoord, xyzPoint[Y_AXIS]);
- break;
+ case 5:
+ zCorrection += TriangleZ(xCoord, xyzPoint[Y_AXIS]);
+ break;
- default:
- break;
+ default:
+ break;
+ }
+ ++numXAxes;
}
- ++numXAxes;
}
+
+ if (numXAxes > 1)
+ {
+ zCorrection /= numXAxes; // take an average
+ }
+
+ xyzPoint[Z_AXIS] += (useTaper) ? (taperHeight - xyzPoint[Z_AXIS]) * recipTaperHeight * zCorrection : zCorrection;
}
- if (numXAxes > 1)
- {
- zCorrection /= numXAxes; // take an average
- }
- xyzPoint[Z_AXIS] += zCorrection;
}
// Invert the bed transform BEFORE the axis transform
@@ -700,7 +706,7 @@ void Move::InverseBedTransform(float xyzPoint[MAX_AXES], uint32_t xAxes) const
switch(numBedCompensationPoints)
{
case 0:
- zCorrection += grid.ComputeInverseHeightError(xCoord, xyzPoint[Y_AXIS], xyzPoint[Z_AXIS]);
+ zCorrection += grid.GetInterpolatedHeightError(xCoord, xyzPoint[Y_AXIS]);
break;
case 3:
@@ -721,11 +727,24 @@ void Move::InverseBedTransform(float xyzPoint[MAX_AXES], uint32_t xAxes) const
++numXAxes;
}
}
+
if (numXAxes > 1)
{
- zCorrection /= numXAxes; // take an average
+ zCorrection /= numXAxes; // take an average
+ }
+
+ if (!useTaper || zCorrection >= taperHeight) // need check on zCorrection to avoid possible divide by zero
+ {
+ xyzPoint[Z_AXIS] -= zCorrection;
+ }
+ else
+ {
+ const float zreq = (xyzPoint[Z_AXIS] - zCorrection)/(1.0 - (zCorrection * recipTaperHeight));
+ if (zreq < taperHeight)
+ {
+ xyzPoint[Z_AXIS] = zreq;
+ }
}
- xyzPoint[Z_AXIS] -= zCorrection;
}
void Move::SetIdentityTransform()
@@ -734,6 +753,16 @@ void Move::SetIdentityTransform()
grid.ClearGridHeights();
}
+void Move::SetTaperHeight(float h)
+{
+ useTaper = (h > 1.0);
+ if (useTaper)
+ {
+ taperHeight = h;
+ recipTaperHeight = 1.0/h;
+ }
+}
+
float Move::AxisCompensation(int8_t axis) const
{
switch(axis)
diff --git a/src/Movement/Move.h b/src/Movement/Move.h
index de92f0ad..e427176a 100644
--- a/src/Movement/Move.h
+++ b/src/Movement/Move.h
@@ -79,6 +79,8 @@ public:
void SetIdentityTransform(); // Cancel the bed equation; does not reset axis angle compensation
void Transform(float move[], uint32_t xAxes) const; // Take a position and apply the bed and the axis-angle compensations
void InverseTransform(float move[], uint32_t xAxes) const; // Go from a transformed point back to user coordinates
+ float GetTaperHeight() const { return (useTaper) ? taperHeight : 0.0; }
+ void SetTaperHeight(float h);
void Diagnostics(MessageType mtype); // Report useful stuff
@@ -178,6 +180,9 @@ private:
float tanXY, tanYZ, tanXZ; // Axis compensation - 90 degrees + angle gives angle between axes
int numBedCompensationPoints; // The number of points we are actually using for bed compensation, 0 means identity bed transform
float xRectangle, yRectangle; // The side lengths of the rectangle used for second-degree bed compensation
+ float taperHeight; // Height over which we taper
+ float recipTaperHeight; // Reciprocal of the taper height
+ bool useTaper; // True to taper off the compensation
HeightMap grid; // Grid definition and height map for G29 bed probing. The probe heights are stored in zBedProbePoints, see above.
diff --git a/src/Platform.cpp b/src/Platform.cpp
index e35a1be9..972f7877 100644
--- a/src/Platform.cpp
+++ b/src/Platform.cpp
@@ -2130,7 +2130,8 @@ void Platform::SetFanValue(size_t fan, float speed)
// Enable or disable the fan that shares its PWM pin with the last heater. Called when we disable or enable the last heater.
void Platform::EnableSharedFan(bool enable)
{
- fans[NUM_FANS - 1].Init((enable) ? COOLING_FAN_PINS[NUM_FANS - 1] : NoPin, FansHardwareInverted());
+ const size_t sharedFanNumber = NUM_FANS - 1;
+ fans[sharedFanNumber].Init((enable) ? COOLING_FAN_PINS[sharedFanNumber] : NoPin, FansHardwareInverted(sharedFanNumber));
}
#endif
@@ -2148,13 +2149,14 @@ float Platform::GetFanRPM()
: 0.0; // else assume fan is off or tacho not connected
}
-bool Platform::FansHardwareInverted() const
+bool Platform::FansHardwareInverted(size_t fanNumber) const
{
#if defined(DUET_NG) || defined(__RADDS__)
return false;
#else
- // The cooling fan output pin gets inverted on a Duet 0.6 or 0.7
- return board == BoardType::Duet_06 || board == BoardType::Duet_07;
+ // The cooling fan output pin gets inverted on a Duet 0.6 or 0.7.
+ // We allow a second fan controlled by a mosfet on the PC4 pin, which is not inverted.
+ return fanNumber == 0 && (board == BoardType::Duet_06 || board == BoardType::Duet_07);
#endif
}
@@ -2162,7 +2164,7 @@ void Platform::InitFans()
{
for (size_t i = 0; i < NUM_FANS; ++i)
{
- fans[i].Init(COOLING_FAN_PINS[i], FansHardwareInverted());
+ fans[i].Init(COOLING_FAN_PINS[i], FansHardwareInverted(i));
}
if (NUM_FANS > 1)
diff --git a/src/Platform.h b/src/Platform.h
index dfdc3e5b..0cde3f1d 100644
--- a/src/Platform.h
+++ b/src/Platform.h
@@ -760,7 +760,7 @@ private:
Pin coolingFanRpmPin; // we currently support only one fan RPM input
float lastRpmResetTime;
void InitFans();
- bool FansHardwareInverted() const;
+ bool FansHardwareInverted(size_t fanNumber) const;
// Serial/USB
diff --git a/src/PrintMonitor.cpp b/src/PrintMonitor.cpp
index b2181ee4..102ba4aa 100644
--- a/src/PrintMonitor.cpp
+++ b/src/PrintMonitor.cpp
@@ -396,68 +396,53 @@ bool PrintMonitor::GetFileInfo(const char *directory, const char *fileName, GCod
{
// Slic3r and S3D
const char* generatedByString = "generated by ";
- char* pos = strstr(buf, generatedByString);
+ const char* introString = "";
+ const char* pos = strstr(buf, generatedByString);
if (pos != nullptr)
{
pos += strlen(generatedByString);
- size_t i = 0;
- while (i < ARRAY_SIZE(parsedFileInfo.generatedBy) - 1 && *pos >= ' ')
+ }
+ else
+ {
+ // KISSlicer
+ pos = strstr(buf, "; KISSlicer");
+ if (pos != nullptr)
+ {
+ pos += 2;
+ }
+ else
{
- char c = *pos++;
- if (c == '"' || c == '\\')
+ // Cura (old)
+ const char* slicedAtString = ";Sliced at: ";
+ pos = strstr(buf, slicedAtString);
+ if (pos != nullptr)
{
- // Need to escape the quote-mark for JSON
- if (i > ARRAY_SIZE(parsedFileInfo.generatedBy) - 3)
+ pos += strlen(slicedAtString);
+ introString = "Cura at ";
+ }
+ else
+ {
+ // Cura (new)
+ const char* generatedWithString = ";Generated with ";
+ pos = strstr(buf, generatedWithString);
+ if (pos != nullptr)
{
- break;
+ pos += strlen(generatedWithString);
}
- parsedFileInfo.generatedBy[i++] = '\\';
}
- parsedFileInfo.generatedBy[i++] = c;
}
- parsedFileInfo.generatedBy[i] = 0;
}
- // Cura
- const char* slicedAtString = ";Sliced at: ";
- pos = strstr(buf, slicedAtString);
if (pos != nullptr)
{
- pos += strlen(slicedAtString);
- strcpy(parsedFileInfo.generatedBy, "Cura at ");
- size_t i = 8;
+ strcpy(parsedFileInfo.generatedBy, introString);
+ size_t i = strlen(introString);
while (i < ARRAY_SIZE(parsedFileInfo.generatedBy) - 1 && *pos >= ' ')
{
- char c = *pos++;
- if (c == '"' || c == '\\')
- {
- if (i > ARRAY_SIZE(parsedFileInfo.generatedBy) - 3)
- {
- break;
- }
- parsedFileInfo.generatedBy[i++] = '\\';
- }
- parsedFileInfo.generatedBy[i++] = c;
+ parsedFileInfo.generatedBy[i++] = *pos++;
}
parsedFileInfo.generatedBy[i] = 0;
}
-
- // KISSlicer
- const char* kisslicerStart = "; KISSlicer";
- if (StringStartsWith(buf, kisslicerStart))
- {
- size_t stringLength = 0;
- for(size_t i = 2; i < ARRAY_UPB(parsedFileInfo.generatedBy); i++)
- {
- if (buf[i] == '\r' || buf[i] == '\n')
- {
- break;
- }
-
- parsedFileInfo.generatedBy[stringLength++] = buf[i];
- }
- parsedFileInfo.generatedBy[stringLength] = 0;
- }
}
headerInfoComplete &= (parsedFileInfo.generatedBy[0] != 0);
@@ -638,7 +623,9 @@ bool PrintMonitor::GetFileInfoResponse(const char *filename, OutputBuffer *&resp
ch = ',';
}
}
- response->catf("],\"generatedBy\":\"%s\"}", info.generatedBy);
+ response->cat("],\"generatedBy\":");
+ response->EncodeString(info.generatedBy, ARRAY_SIZE(info.generatedBy), false);
+ response->cat("}");
}
else
{
@@ -1110,14 +1097,38 @@ unsigned int PrintMonitor::FindFilamentUsed(const char* buf, size_t len, float *
}
if (isDigit(*p))
{
- char* q;
- filamentUsed[filamentsFound] = strtod(p, &q); // S3D reports filament usage in mm, no conversion needed
+ filamentUsed[filamentsFound] = strtod(p, nullptr); // S3D reports filament usage in mm, no conversion needed
+ ++filamentsFound;
+ }
+ }
+ }
+
+ // Look for filament usage as generated by recent KISSlicer versions
+ if (!filamentsFound)
+ {
+ const char *filamentLengthStr = "; Ext ";
+ p = buf;
+ while (filamentsFound < maxFilaments && (p = strstr(p, filamentLengthStr)) != nullptr)
+ {
+ p += strlen(filamentLengthStr);
+ while(isdigit(*p))
+ {
+ ++p;
+ }
+ while(strchr(" :=\t", *p) != nullptr)
+ {
+ ++p;
+ }
+
+ if (isDigit(*p))
+ {
+ filamentUsed[filamentsFound] = strtod(p, nullptr);
++filamentsFound;
}
}
}
- // Special case: KISSlicer only generates the filament volume, so we need to calculate the length from it
+ // Special case: Old KISSlicer only generates the filament volume, so we need to calculate the length from it
if (!filamentsFound)
{
const char *filamentVolumeStr = "; Estimated Build Volume: ";
diff --git a/src/Storage/FileStore.cpp b/src/Storage/FileStore.cpp
index 680032f8..cc8207fd 100644
--- a/src/Storage/FileStore.cpp
+++ b/src/Storage/FileStore.cpp
@@ -47,6 +47,36 @@ bool FileStore::Open(const char* directory, const char* fileName, bool write)
writing = write;
lastBufferEntry = FileBufLen;
+ // Try to create the path of this file if we want to write to it
+ if (writing)
+ {
+ char filePathBuffer[FILENAME_LENGTH];
+ StringRef filePath(filePathBuffer, FILENAME_LENGTH);
+ filePath.copy(location);
+
+ bool isVolume = isdigit(filePath[0]);
+ for(size_t i = 1; i < filePath.strlen(); i++)
+ {
+ if (filePath[i] == '/')
+ {
+ if (isVolume)
+ {
+ isVolume = false;
+ continue;
+ }
+
+ filePath[i] = 0;
+ if (!platform->GetMassStorage()->DirectoryExists(filePath.Pointer()) && !platform->GetMassStorage()->MakeDirectory(filePath.Pointer()))
+ {
+ platform->MessageF(GENERIC_MESSAGE, "Failed to create directory %s while trying to open file %s\n",
+ filePath.Pointer(), location);
+ return false;
+ }
+ filePath[i] = '/';
+ }
+ }
+ }
+
FRESULT openReturn = f_open(&file, location, (writing) ? FA_CREATE_ALWAYS | FA_WRITE : FA_OPEN_EXISTING | FA_READ);
if (openReturn != FR_OK)
{