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
path: root/src
diff options
context:
space:
mode:
authorDavid Crocker <dcrocker@eschertech.com>2017-02-17 12:04:02 +0300
committerDavid Crocker <dcrocker@eschertech.com>2017-02-17 12:04:02 +0300
commit7df2717ebd680abf1b541dae800cfdc3b6298a02 (patch)
treeefbd040d87f55b9ffafb3c918e5b589130f4ff7c /src
parent8e5c83a186d1ee17cc9d295089dffae79dcbff7e (diff)
Version 1.18alpha2
Initial implementation of babysteppnig Fixed some issues with file upload and FTP networking on Duet Ethernet (but FTP notr working yet) Pass the name of the firmware file to IAP Duet Ethernet and Duet 085 now each have their own Webserver modules
Diffstat (limited to 'src')
-rw-r--r--src/Configuration.h1
-rw-r--r--src/Duet/Lwip/lwipopts.h2
-rw-r--r--src/Duet/Webserver.cpp (renamed from src/Webserver/Webserver.cpp)5
-rw-r--r--src/Duet/Webserver.h (renamed from src/Webserver/Webserver.h)0
-rw-r--r--src/DuetNG/DuetEthernet/Network.cpp41
-rw-r--r--src/DuetNG/DuetEthernet/Network.h7
-rw-r--r--src/DuetNG/DuetEthernet/NetworkTransaction.cpp2
-rw-r--r--src/DuetNG/DuetEthernet/Socket.cpp17
-rw-r--r--src/DuetNG/DuetEthernet/Webserver.cpp2966
-rw-r--r--src/DuetNG/DuetEthernet/Webserver.h386
-rw-r--r--src/DuetNG/DuetEthernet/Wiznet/Ethernet/WizSpi.cpp3
-rw-r--r--src/DuetNG/TMC2660.cpp40
-rw-r--r--src/GCodes/GCodeBuffer.cpp2
-rw-r--r--src/GCodes/GCodes.cpp92
-rw-r--r--src/GCodes/GCodes.h8
-rw-r--r--src/GCodes/GCodes2.cpp4
-rw-r--r--src/Movement/DDA.cpp165
-rw-r--r--src/Movement/Move.cpp15
-rw-r--r--src/Movement/Move.h12
-rw-r--r--src/Platform.cpp65
-rw-r--r--src/Platform.h2
-rw-r--r--src/RADDS/Pins_radds.h8
-rw-r--r--src/RepRapFirmware.cpp2
-rw-r--r--src/Reprap.cpp32
-rw-r--r--src/Version.h4
25 files changed, 3622 insertions, 259 deletions
diff --git a/src/Configuration.h b/src/Configuration.h
index 51238fd1..dd1dc3e0 100644
--- a/src/Configuration.h
+++ b/src/Configuration.h
@@ -136,7 +136,6 @@ const float SILLY_Z_VALUE = -9999.0; // Millimetres
// String lengths
-//const size_t LONG_STRING_LENGTH = 1024;
const size_t FORMAT_STRING_LENGTH = 256;
const size_t MACHINE_NAME_LENGTH = 40;
const size_t PASSWORD_LENGTH = 20;
diff --git a/src/Duet/Lwip/lwipopts.h b/src/Duet/Lwip/lwipopts.h
index 2a4cfc2a..3fa944fe 100644
--- a/src/Duet/Lwip/lwipopts.h
+++ b/src/Duet/Lwip/lwipopts.h
@@ -94,7 +94,7 @@ a lot of data that needs to be copied, this should be set high. */
/* --------- IGMP options ---------- */
-#define LWIP_IGMP 1
+#define LWIP_IGMP 1 /* looks like this is needed for mdns support */
#ifdef __SAM3X8E__
# define LWIP_RAND trueRandom
diff --git a/src/Webserver/Webserver.cpp b/src/Duet/Webserver.cpp
index 620e6246..20c8c1e2 100644
--- a/src/Webserver/Webserver.cpp
+++ b/src/Duet/Webserver.cpp
@@ -651,13 +651,13 @@ void Webserver::HttpInterpreter::DoFastUpload()
platform->Message(GENERIC_MESSAGE, "Error: Could not write upload data!\n");
CancelUpload();
- while (!network->Lock());
+ while (!network->Lock()) { }
SendJsonResponse("upload");
return;
}
}
}
- while (!network->Lock());
+ while (!network->Lock()) { }
}
// See if the upload has finished
@@ -2196,7 +2196,6 @@ void Webserver::FtpInterpreter::ProcessLine()
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();
diff --git a/src/Webserver/Webserver.h b/src/Duet/Webserver.h
index 3e4f2c30..3e4f2c30 100644
--- a/src/Webserver/Webserver.h
+++ b/src/Duet/Webserver.h
diff --git a/src/DuetNG/DuetEthernet/Network.cpp b/src/DuetNG/DuetEthernet/Network.cpp
index db79de62..5738be75 100644
--- a/src/DuetNG/DuetEthernet/Network.cpp
+++ b/src/DuetNG/DuetEthernet/Network.cpp
@@ -71,7 +71,7 @@ void Network::Spin(bool full)
usingDhcp = (ipAddress[0] == 0 && ipAddress[1] == 0 && ipAddress[2] == 0 && ipAddress[3] == 0);
if (usingDhcp)
{
- debugPrintf("Link established, getting IP address\n");
+// debugPrintf("Link established, getting IP address\n");
// IP address is all zeros, so use DHCP
DHCP_init(DhcpSocketNumber, hostname);
lastTickMillis = millis();
@@ -79,7 +79,7 @@ void Network::Spin(bool full)
}
else
{
- debugPrintf("Link established, network running\n");
+// debugPrintf("Link established, network running\n");
InitSockets();
state = NetworkState::active;
}
@@ -100,7 +100,7 @@ void Network::Spin(bool full)
const DhcpRunResult ret = DHCP_run();
if (ret == DhcpRunResult::DHCP_IP_ASSIGN)
{
- debugPrintf("IP address obtained, network running\n");
+// debugPrintf("IP address obtained, network running\n");
getSIPR(ipAddress);
// Send mDNS announcement so that some routers can perform hostname mapping
// if this board is connected via a non-IGMP capable WiFi bridge (like the TP-Link WR701N)
@@ -111,7 +111,7 @@ void Network::Spin(bool full)
}
else
{
- debugPrintf("Lost phy link\n");
+// debugPrintf("Lost phy link\n");
DHCP_stop();
TerminateSockets();
state = NetworkState::establishingLink;
@@ -135,7 +135,7 @@ void Network::Spin(bool full)
const DhcpRunResult ret = DHCP_run();
if (ret == DhcpRunResult::DHCP_IP_CHANGED)
{
- debugPrintf("IP address changed\n");
+// debugPrintf("IP address changed\n");
getSIPR(ipAddress);
}
}
@@ -152,7 +152,7 @@ void Network::Spin(bool full)
}
else if (full)
{
- debugPrintf("Lost phy link\n");
+// debugPrintf("Lost phy link\n");
if (usingDhcp)
{
DHCP_stop();
@@ -204,7 +204,7 @@ void Network::Stop()
{
DHCP_stop();
}
- digitalWrite(EspResetPin, LOW); // put the ESP back into reset
+ digitalWrite(EspResetPin, LOW); // put the W5500 back into reset
state = NetworkState::disabled;
}
}
@@ -334,34 +334,29 @@ NetworkTransaction *Network::GetTransaction(Connection conn)
void Network::OpenDataPort(Port port)
{
- sockets[FtpSocketNumber].Init(FtpSocketNumber, port);
- sockets[FtpSocketNumber].Poll(false);
+ sockets[FtpDataSocketNumber].Init(FtpDataSocketNumber, port);
+ sockets[FtpDataSocketNumber].Poll(false);
}
Port Network::GetDataPort() const
{
- return sockets[FtpSocketNumber].GetLocalPort();
+ return sockets[FtpDataSocketNumber].GetLocalPort();
}
// Close FTP data port and purge associated PCB
void Network::CloseDataPort()
{
- sockets[FtpSocketNumber].Close();
+ sockets[FtpDataSocketNumber].Close();
}
-bool Network::AcquireFTPTransaction()
+bool Network::AcquireTransaction(size_t socketNumber)
{
- return sockets[FtpSocketNumber].AcquireTransaction();
-}
-
-bool Network::AcquireDataTransaction()
-{
- return sockets[FtpDataSocketNumber].AcquireTransaction();
-}
-
-bool Network::AcquireTelnetTransaction()
-{
- return sockets[TelnetSocketNumber].AcquireTransaction();
+ const bool success = sockets[socketNumber].AcquireTransaction();
+// if (success)
+// {
+// currentTransactionSocketNumber = socketNumber;
+// }
+ return success;
}
void Network::InitSockets()
diff --git a/src/DuetNG/DuetEthernet/Network.h b/src/DuetNG/DuetEthernet/Network.h
index 1a479a40..ef51dc72 100644
--- a/src/DuetNG/DuetEthernet/Network.h
+++ b/src/DuetNG/DuetEthernet/Network.h
@@ -65,9 +65,9 @@ public:
void SaveFTPConnection() {}
void SaveTelnetConnection() {}
- bool AcquireFTPTransaction();
- bool AcquireDataTransaction();
- bool AcquireTelnetTransaction();
+ bool AcquireFTPTransaction() { return AcquireTransaction(FtpSocketNumber); }
+ bool AcquireDataTransaction() { return AcquireTransaction(FtpDataSocketNumber); }
+ bool AcquireTelnetTransaction() { return AcquireTransaction(TelnetSocketNumber); }
void Defer(NetworkTransaction *tr);
@@ -90,6 +90,7 @@ private:
void InitSockets();
void TerminateSockets();
+ bool AcquireTransaction(size_t socketNumber);
Platform *platform;
float longWait;
diff --git a/src/DuetNG/DuetEthernet/NetworkTransaction.cpp b/src/DuetNG/DuetEthernet/NetworkTransaction.cpp
index f6fa3a76..a45deb5e 100644
--- a/src/DuetNG/DuetEthernet/NetworkTransaction.cpp
+++ b/src/DuetNG/DuetEthernet/NetworkTransaction.cpp
@@ -160,7 +160,7 @@ void NetworkTransaction::SetFileToWrite(FileStore *file)
}
else if (file != nullptr)
{
- debugPrintf("Want to write file but can't write\n");
+// debugPrintf("Want to write file but can't write\n");
file->Close();
}
}
diff --git a/src/DuetNG/DuetEthernet/Socket.cpp b/src/DuetNG/DuetEthernet/Socket.cpp
index d913300c..cb8ecc05 100644
--- a/src/DuetNG/DuetEthernet/Socket.cpp
+++ b/src/DuetNG/DuetEthernet/Socket.cpp
@@ -147,10 +147,17 @@ bool Socket::ReadBuffer(const char *&buffer, size_t &len)
// Poll a socket to see if it needs to be serviced
void Socket::Poll(bool full)
{
- // Recycle any receive buffers that are now empty
- while (receivedData != nullptr && receivedData->IsEmpty())
+ // The mechanism used by class OutputBuffer and now by NetworkBuffer of marking data taken as soon as we return a pointer to it
+ // is DANGEROUS and will have to be rewritten for RTOS. We need to recycle empty buffers, otherwise multiple file uploads get stalled.
+ // However, we MUST NOT do this until the data had DEFINITELY been finished with. Temporarily use this conditional to avoid a bug
+ // with data corruption when this is not the case.
+ if (full)
{
- receivedData = receivedData->Release(); // discard empty buffer at head of chain
+ // Recycle any receive buffers that are now empty
+ while (receivedData != nullptr && receivedData->IsEmpty())
+ {
+ receivedData = receivedData->Release(); // discard empty buffer at head of chain
+ }
}
switch(getSn_SR(socketNum))
@@ -193,7 +200,7 @@ void Socket::Poll(bool full)
else
{
// This should not happen
- debugPrintf("ERROR:currentTransation should be null but isn't\n");
+// debugPrintf("ERROR:currentTransation should be null but isn't\n");
}
}
state = SocketState::connected;
@@ -374,7 +381,7 @@ void Socket::DiscardReceivedData()
}
}
-// The webserver calls this to tell the socket that it needs a transaction, e.g. for sending a Telnet os FTP response.
+// The webserver calls this to tell the socket that it needs a transaction, e.g. for sending a Telnet or FTP response.
// An empty transaction will do.
// Return true if we can do it, false if the connection is closed or closing.
bool Socket::AcquireTransaction()
diff --git a/src/DuetNG/DuetEthernet/Webserver.cpp b/src/DuetNG/DuetEthernet/Webserver.cpp
new file mode 100644
index 00000000..20c8c1e2
--- /dev/null
+++ b/src/DuetNG/DuetEthernet/Webserver.cpp
@@ -0,0 +1,2966 @@
+/****************************************************************************************************
+
+ 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"
+#include "Webserver.h"
+#include "NetworkTransaction.h"
+#include "Platform.h"
+#include "Network.h"
+#include "RepRap.h"
+#include "GCodes/GCodes.h"
+#include "PrintMonitor.h"
+
+//***************************************************************************************************
+
+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 = NoConnection;
+
+ // 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;
+ const 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 TransactionStatus::released: type = "released"; break;
+ case TransactionStatus::connected: type = "connected"; break;
+ case TransactionStatus::receiving: type = "receiving"; break;
+ case TransactionStatus::sending: type = "sending"; break;
+ case TransactionStatus::disconnected: type = "disconnected"; break;
+ case TransactionStatus::deferred: type = "deferred"; break;
+ case TransactionStatus::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 == TransactionStatus::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 == TransactionStatus::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 = NoConnection;
+ 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 = NoConnection;
+ break;
+ }
+ }
+ }
+ }
+ else if (readingConnection != NoConnection)
+ {
+ // 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");
+ Network::Terminate(readingConnection);
+ }
+ 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(Connection conn)
+{
+ // Inform protocol handlers that this connection has been lost
+ const uint16_t localPort = Network::GetLocalPort(conn);
+ 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, Network::GetRemotePort(conn));
+ }
+ interpreter->ConnectionLost(conn);
+
+ // Don't process any more data from this connection if has gone down
+ if (readingConnection == conn)
+ {
+ readingConnection = NoConnection;
+ }
+}
+
+
+//********************************************************************************************
+//
+//********************** 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 = NoConnection;
+ 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 = (int)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.
+ // Buffer size must be a multiple of the 512b sector size.
+#ifdef DUET_NG
+ static const size_t writeBufLength = 8192;
+#else
+ static const size_t writeBufLength = 2048;
+#endif
+ static uint32_t writeBufStorage[writeBufLength/4]; // aligned buffer for file writes
+ static size_t writeBufIndex;
+ char* const writeBuf = reinterpret_cast<char *>(writeBufStorage);
+
+ if (uploadedBytes == 0)
+ {
+ writeBufIndex = 0;
+ }
+
+ while (len != 0)
+ {
+ const 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)
+ {
+ const 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 = nullptr;
+ 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;
+ }
+ }
+
+ // 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);
+ strcat(nameBuf, ".gz");
+ fileToSend = platform->GetFileStore(platform->GetWebDir(), nameBuf, false);
+ if (fileToSend != nullptr)
+ {
+ zip = true;
+ }
+ }
+
+ // 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")))
+ {
+ 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)
+ {
+ 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
+ Network::Terminate(webserver->currentTransaction->GetConnection());
+ return;
+ }
+
+ bool keepOpen = false;
+ bool mayKeepOpen;
+ if (numQualKeys == 0)
+ {
+ GetJsonResponse(command, jsonResponse, mayKeepOpen);
+ }
+ else
+ {
+ GetJsonResponse(command, jsonResponse, mayKeepOpen);
+ }
+
+ // Check special cases of deferred requests (rr_fileinfo) and rejected messages
+ NetworkTransaction *transaction = webserver->currentTransaction;
+ if (transaction->GetStatus() == TransactionStatus::deferred || transaction->GetStatus() == TransactionStatus::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, bool& keepOpen)
+{
+ keepOpen = false; // assume we don't want to persist the connection
+
+ if (StringEquals(request, "connect") && GetKeyValue("password") != nullptr)
+ {
+ if (IsAuthenticated() || reprap.CheckPassword(GetKeyValue("password")))
+ {
+ // 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 (GetKeyValue("type") != nullptr)
+ {
+ // New-style JSON status responses
+ type = atoi(GetKeyValue("type"));
+ 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") && GetKeyValue("gcode") != nullptr)
+ {
+ LoadGcodeBuffer(GetKeyValue("gcode"));
+ response->printf("{\"buff\":%u}", GetGCodeBufferSpace());
+ }
+ else if (StringEquals(request, "upload"))
+ {
+ response->printf("{\"err\":%d}", (uploadedBytes == postFileLength) ? 0 : 1);
+ }
+ else if (StringEquals(request, "delete") && GetKeyValue("name") != nullptr)
+ {
+ bool ok = platform->GetMassStorage()->Delete(FS_PREFIX, GetKeyValue("name"));
+ response->printf("{\"err\":%d}", (ok) ? 0 : 1);
+ }
+ else if (StringEquals(request, "filelist") && GetKeyValue("dir") != nullptr)
+ {
+ OutputBuffer::Release(response);
+ response = reprap.GetFilelistResponse(GetKeyValue("dir"));
+ }
+ else if (StringEquals(request, "files"))
+ {
+ const char* dir = GetKeyValue("dir");
+ if (dir == nullptr)
+ {
+ dir = platform->GetGCodeDir();
+ }
+ const char* const flagDirsVal = GetKeyValue("flagDirs");
+ const bool flagDirs = flagDirsVal != nullptr && atoi(flagDirsVal) == 1;
+ OutputBuffer::Release(response);
+ response = reprap.GetFilesResponse(dir, flagDirs);
+ }
+ else if (StringEquals(request, "fileinfo"))
+ {
+ if (deferredRequestConnection != NoConnection)
+ {
+ // Don't allow multiple deferred requests to be processed at once
+ webserver->currentTransaction->Defer(DeferralMode::ResetData);
+ }
+ else
+ {
+ const char* const nameVal = GetKeyValue("name");
+ if (nameVal != nullptr)
+ {
+ // Regular rr_fileinfo?name=xxx call
+ strncpy(filenameBeingProcessed, nameVal, 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"))
+ {
+ const char* const oldVal = GetKeyValue("old");
+ const char* const newVal = GetKeyValue("new");
+ bool success = false;
+ if (oldVal != nullptr && newVal != nullptr)
+ {
+ success = platform->GetMassStorage()->Rename(oldVal, newVal);
+ }
+ response->printf("{\"err\":%d}", (success) ? 0 : 1);
+ }
+ else if (StringEquals(request, "mkdir"))
+ {
+ const char* dirVal = GetKeyValue("dir");
+ bool success = false;
+ if (dirVal != nullptr)
+ {
+ success = (platform->GetMassStorage()->MakeDirectory(dirVal));
+ }
+ response->printf("{\"err\":%d}", (success) ? 0 : 1);
+ }
+ else if (StringEquals(request, "config"))
+ {
+ OutputBuffer::Release(response);
+ response = reprap.GetConfigResponse();
+ }
+ else
+ {
+ RejectMessage("Unknown request", 500);
+ }
+}
+
+const char* Webserver::HttpInterpreter::GetKeyValue(const char *key) const
+{
+ for (size_t i = 0; i < numQualKeys; ++i)
+ {
+ if (StringEquals(qualifiers[i].key, key))
+ {
+ return qualifiers[i].value;
+ }
+ }
+ return nullptr;
+}
+
+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(Connection conn)
+{
+ // Make sure deferred requests are cancelled
+ if (deferredRequestConnection == conn)
+ {
+ reprap.GetPrintMonitor()->StopParsing(filenameBeingProcessed);
+ deferredRequestConnection = NoConnection;
+ }
+
+ // If we couldn't read an entire request from a connection, reset our state here again
+ if (webserver->readingConnection == conn)
+ {
+ 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 = Network::GetRemoteIP(conn);
+ const uint16_t remotePort = Network::GetRemotePort(conn);
+ 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 (Network::IsConnected(deferredRequestConnection))
+ {
+ // 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"))
+ {
+ const bool isUploadRequest = (StringEquals(commandWords[1], KO_START "upload"))
+ || (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 Connection 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 = NoConnection;
+
+ // 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(Connection conn)
+{
+ connectedClients--;
+
+ if (Network::GetLocalPort(conn) != 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 && !Network::IsTerminated(conn))
+ {
+ 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 */
+ 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(Connection conn)
+{
+ 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 == TransactionStatus::acquired)
+ {
+ SendGCodeReply();
+ return false;
+ }
+
+ // Is this connection still live? Check that for deferred requests
+ if (status == TransactionStatus::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;
+}
+
+// vim: ts=4:sw=4
diff --git a/src/DuetNG/DuetEthernet/Webserver.h b/src/DuetNG/DuetEthernet/Webserver.h
new file mode 100644
index 00000000..3e4f2c30
--- /dev/null
+++ b/src/DuetNG/DuetEthernet/Webserver.h
@@ -0,0 +1,386 @@
+/****************************************************************************************************
+
+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 Knockout.js and Jquery.js. See:
+
+http://knockoutjs.com/
+
+http://jquery.com/
+
+-----------------------------------------------------------------------------------------------------
+
+Version 0.2
+
+10 May 2013
+
+Adrian Bowyer
+RepRap Professional Ltd
+http://reprappro.com
+
+Licence: GPL
+
+****************************************************************************************************/
+
+#ifndef WEBSERVER_H
+#define WEBSERVER_H
+
+#include "NetworkDefs.h"
+#include "RepRapFirmware.h"
+#include "MessageType.h"
+#include "Storage/FileData.h"
+
+/* Generic values */
+
+const size_t gcodeBufferLength = 512; // size of our gcode ring buffer, preferably a power of 2
+
+/* 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
+{
+ HTTP,
+ Telnet
+};
+
+// 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(Connection conn /*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:
+
+ 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:
+
+ friend class Platform;
+ friend class ProtocolInterpreter;
+
+ Webserver(Platform* p, Network *n);
+ void Init();
+ void Spin();
+ void Exit();
+ void Diagnostics(MessageType mtype);
+
+ bool GCodeAvailable(const WebSource source) const;
+ char ReadGCode(const WebSource source);
+ void HandleGCodeReply(const WebSource source, OutputBuffer *reply);
+ void HandleGCodeReply(const WebSource source, const char *reply);
+ 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;
+
+ void ConnectionLost(Connection conn /*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(Connection conn /*const ConnectionState *cs*/) override;
+ 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, 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();
+ const char* GetKeyValue(const char *key) const; // return the value of the specified key, or nullptr if not present
+
+ // 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)
+ volatile Connection 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(Connection conn /*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(Connection conn /*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;
+ volatile Connection 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/DuetNG/DuetEthernet/Wiznet/Ethernet/WizSpi.cpp b/src/DuetNG/DuetEthernet/Wiznet/Ethernet/WizSpi.cpp
index e095c26b..c11f41a5 100644
--- a/src/DuetNG/DuetEthernet/Wiznet/Ethernet/WizSpi.cpp
+++ b/src/DuetNG/DuetEthernet/Wiznet/Ethernet/WizSpi.cpp
@@ -234,9 +234,6 @@ namespace WizSpi
#endif
}
-// The remaining functions are speed-critical, so use full optimisation
-#pragma GCC optimize ("O3")
-
// Wait for transmit buffer empty, returning true if timed out
static inline bool waitForTxReady()
{
diff --git a/src/DuetNG/TMC2660.cpp b/src/DuetNG/TMC2660.cpp
index fbf96771..f9a5220c 100644
--- a/src/DuetNG/TMC2660.cpp
+++ b/src/DuetNG/TMC2660.cpp
@@ -8,7 +8,7 @@
#include "RepRapFirmware.h"
#include "TMC2660.h"
-#if !defined(PROTOTYPE_1)
+const float MaximumMotorCurrent = 2500.0;
static size_t numTmc2660Drivers;
@@ -239,50 +239,16 @@ void TmcDriverState::SetMicrostepping(uint32_t shift, bool interpolate)
// Set the motor current
void TmcDriverState::SetCurrent(float current)
{
-#if defined(DUET_NG) && !defined(PROTOTYPE_1)
-
// The current sense resistor on the production Duet WiFi is 0.051 ohms.
// This gives us a range of 101mA to 3.236A in 101mA steps in the high sensitivity range (VSENSE = 1)
drvConfReg |= TMC_DRVCONF_VSENSE; // this should always be set, but send it again just in case
SpiSendWord(drvConfReg);
- const uint32_t iCurrent = static_cast<uint32_t>(constrain<float>(current, 100.0, 2000.0));
+ const uint32_t iCurrent = static_cast<uint32_t>(constrain<float>(current, 100.0, MaximumMotorCurrent));
const uint32_t csBits = (32 * iCurrent - 1600)/3236; // formula checked by simulation on a spreadsheet
sgcsConfReg &= ~TMC_SGCSCONF_CS_MASK;
sgcsConfReg |= TMC_SGCSCONF_CS(csBits);
SpiSendWord(sgcsConfReg);
-
-#else
-
- // The current sense resistor is 0.1 ohms on the evaluation board.
- // This gives us a range of 95mA to 3.05A in 95mA steps when VSENSE is low (but max allowed RMS current is 2A),
- // or 52mA to 1.65A in 52mA steps when VSENSE is high.
- if (current > 1650.0)
- {
- // Need VSENSE = 0, but set up the current first to avoid temporarily exceeding the 2A rating
- const uint32_t iCurrent = (current > 2000.0) ? 2000 : (uint32_t)current;
- const uint32_t csBits = (32 * iCurrent - 1500)/3050; // formula checked by simulation on a spreadsheet
- sgcsConfReg &= ~TMC_SGCSCONF_CS_MASK;
- sgcsConfReg |= TMC_SGCSCONF_CS(csBits);
- SpiSendWord(sgcsConfReg);
-
- drvConfReg &= ~TMC_DRVCONF_VSENSE;
- SpiSendWord(drvConfReg);
- }
- else
- {
- // Use VSENSE = 1
- drvConfReg |= TMC_DRVCONF_VSENSE;
- SpiSendWord(drvConfReg);
-
- const uint32_t iCurrent = (current < 50) ? 50 : (uint32_t)current;
- const uint32_t csBits = (32 * iCurrent - 800)/1650; // formula checked by simulation on a spreadsheet
- sgcsConfReg &= ~TMC_SGCSCONF_CS_MASK;
- sgcsConfReg |= TMC_SGCSCONF_CS(csBits);
- SpiSendWord(sgcsConfReg);
- }
-
- #endif
}
// Enable or disable the driver
@@ -460,8 +426,6 @@ namespace TMC2660
}; // end namespace
-#endif
-
// End
diff --git a/src/GCodes/GCodeBuffer.cpp b/src/GCodes/GCodeBuffer.cpp
index ee852627..e8120abc 100644
--- a/src/GCodes/GCodeBuffer.cpp
+++ b/src/GCodes/GCodeBuffer.cpp
@@ -134,7 +134,7 @@ bool GCodeBuffer::Put(char c)
gcodeBuffer[gcodePointer++] = c;
if (gcodePointer >= (int)GCODE_LENGTH)
{
- reprap.GetPlatform()->Message(GENERIC_MESSAGE, "Error: G-Code buffer length overflow.\n");
+ reprap.GetPlatform()->MessageF(GENERIC_MESSAGE, "Error: G-Code buffer '$s' length overflow\n", identity);
gcodePointer = 0;
gcodeBuffer[0] = 0;
}
diff --git a/src/GCodes/GCodes.cpp b/src/GCodes/GCodes.cpp
index 85c41f54..fb0a518d 100644
--- a/src/GCodes/GCodes.cpp
+++ b/src/GCodes/GCodes.cpp
@@ -151,6 +151,7 @@ void GCodes::Reset()
toolChangeRestorePoint.Init();
ClearMove();
+ ClearBabyStepping();
for (size_t i = 0; i < MaxTriggers; ++i)
{
@@ -173,6 +174,11 @@ void GCodes::Reset()
}
}
+void GCodes::ClearBabyStepping()
+{
+ pendingBabyStepZOffset = currentBabyStepZOffset = 0.0;
+}
+
bool GCodes::DoingFileMacro() const
{
return fileGCode->IsDoingFileMacro();
@@ -756,14 +762,14 @@ void GCodes::DoFilePrint(GCodeBuffer& gb, StringRef& reply)
gb.Init(); // mark buffer as empty
- // Don't close the file until all moves have been completed, in case the print gets paused.
- // Also, this keeps the state as 'Printing' until the print really has finished.
- if (LockMovementAndWaitForStandstill(gb))
+ if (gb.MachineState().previous == nullptr)
{
- fd.Close();
- if (gb.MachineState().previous == nullptr)
+ // Finished printing SD card file
+ // Don't close the file until all moves have been completed, in case the print gets paused.
+ // Also, this keeps the state as 'Printing' until the print really has finished.
+ if (LockMovementAndWaitForStandstill(gb))
{
- // Finished printing SD card file
+ fd.Close();
UnlockAll(gb);
reprap.GetPrintMonitor()->StoppedPrint();
if (platform->Emulating() == marlin)
@@ -772,21 +778,22 @@ void GCodes::DoFilePrint(GCodeBuffer& gb, StringRef& reply)
HandleReply(gb, false, "Done printing file");
}
}
- else
+ }
+ else
+ {
+ // Finished a macro or finished processing config.g
+ fd.Close();
+ if (runningConfigFile)
{
- // Finished a macro or finished processing config.g
- if (runningConfigFile)
- {
- CopyConfigFinalValues(gb);
- runningConfigFile = false;
- }
- Pop(gb);
- gb.Init();
- if (gb.GetState() == GCodeState::normal)
- {
- UnlockAll(gb);
- HandleReply(gb, false, "");
- }
+ CopyConfigFinalValues(gb);
+ runningConfigFile = false;
+ }
+ Pop(gb);
+ gb.Init();
+ if (gb.GetState() == GCodeState::normal)
+ {
+ UnlockAll(gb);
+ HandleReply(gb, false, "");
}
}
return;
@@ -953,9 +960,6 @@ bool GCodes::LockMovementAndWaitForStandstill(const GCodeBuffer& gb)
return false;
}
- // Allow movement again
- reprap.GetMove()->ResumeMoving();
-
// Get the current positions. These may not be the same as the ones we remembered from last time if we just did a special move.
reprap.GetMove()->GetCurrentUserPosition(moveBuffer.coords, 0, reprap.GetCurrentXAxes());
memcpy(moveBuffer.initialCoords, moveBuffer.coords, numAxes * sizeof(moveBuffer.initialCoords[0]));
@@ -1142,6 +1146,7 @@ unsigned int GCodes::LoadMoveBufferFromGCode(GCodeBuffer& gb, int moveType)
}
else if (moveType == 0)
{
+ moveArg += currentBabyStepZOffset;
if (axis == Z_AXIS && isRetracted)
{
moveArg += retractHop; // handle firmware retraction on layer change
@@ -1351,7 +1356,7 @@ bool GCodes::DoArcMove(GCodeBuffer& gb, bool clockwise)
}
else
{
- moveBuffer.coords[Z_AXIS] = zParam;
+ moveBuffer.coords[Z_AXIS] = zParam + currentBabyStepZOffset + retractHop; // handle firmware retraction on layer change
if (currentTool != nullptr)
{
moveBuffer.coords[Z_AXIS] -= currentTool->GetOffset()[Z_AXIS];
@@ -1448,6 +1453,7 @@ bool GCodes::ReadMove(RawMove& m)
}
m = moveBuffer;
+
if (segmentsLeft == 1)
{
// If there is just 1 segment left, it doesn't matter if it is an arc move or not, just move to the end position
@@ -1496,6 +1502,31 @@ bool GCodes::ReadMove(RawMove& m)
--segmentsLeft;
}
+
+ // Check for pending baby stepping
+ if (m.moveType == 0 && pendingBabyStepZOffset != 0.0)
+ {
+ // Calculate the move length, to see how much new babystepping is appropriate for this move
+ float xMoveLength = 0.0;
+ for (size_t drive = 0; drive < numAxes; ++drive)
+ {
+ if ((arcAxesMoving & (1 << drive)) != 0)
+ {
+ xMoveLength = max<float>(xMoveLength, fabs(m.coords[drive] - m.initialCoords[drive]));
+ }
+ }
+ const float distance = sqrtf(fsquare(xMoveLength) + fsquare(m.coords[Y_AXIS] - m.initialCoords[Y_AXIS]) + fsquare(m.coords[Z_AXIS] - m.initialCoords[Z_AXIS]));
+
+ // The maximum Z speed change due to baby stepping that we allow is the Z jerk rate, to avoid slowing the print down too much
+ const float minMoveTime = distance/m.feedRate;
+ const float maxBabyStepping = minMoveTime * platform->ConfiguredInstantDv(Z_AXIS);
+ const float babySteppingToDo = constrain<float>(pendingBabyStepZOffset, -maxBabyStepping, maxBabyStepping);
+ m.coords[Z_AXIS] += babySteppingToDo;
+ moveBuffer.initialCoords[Z_AXIS] = m.coords[Z_AXIS];
+ moveBuffer.coords[Z_AXIS] += babySteppingToDo;
+ pendingBabyStepZOffset -= babySteppingToDo;
+ currentBabyStepZOffset += babySteppingToDo;
+ }
return true;
}
@@ -1603,9 +1634,9 @@ bool GCodes::SetPositions(GCodeBuffer& gb)
if (gb.Seen('Z'))
{
const float babystepAmount = gb.GetFValue() * distanceScale;
- if (fabs(babystepAmount) <= 1.0) // limit babystepping to 1mm
+ if (fabs(babystepAmount) <= 1.0) // limit babystepping to 1mm
{
- reprap.GetMove()->Babystep(babystepAmount);
+ pendingBabyStepZOffset += babystepAmount;
}
}
}
@@ -1629,8 +1660,9 @@ bool GCodes::SetPositions(GCodeBuffer& gb)
{
return false;
}
+ ClearBabyStepping(); // G92 on any axis clears pending babystepping
}
- else if (segmentsLeft != 0) // wait for previous move to be taken so that GetCurrentUserPosition returns the correct value
+ else if (segmentsLeft != 0) // wait for previous move to be taken so that GetCurrentUserPosition returns the correct value
{
return false;
}
@@ -1748,6 +1780,7 @@ bool GCodes::DoHome(GCodeBuffer& gb, StringRef& reply, bool& error)
if (reprap.GetMove()->IsDeltaMode())
{
SetAllAxesNotHomed();
+ ClearBabyStepping();
DoFileMacro(gb, HOME_DELTA_G, true);
}
else
@@ -1762,6 +1795,10 @@ bool GCodes::DoHome(GCodeBuffer& gb, StringRef& reply, bool& error)
}
}
+ if (toBeHomed == 0 || (toBeHomed & (1u << Z_AXIS)) != 0)
+ {
+ ClearBabyStepping();
+ }
if (toBeHomed == 0 || toBeHomed == ((1u << numAxes) - 1))
{
// Homing everything
@@ -2537,7 +2574,6 @@ bool GCodes::DoDwellTime(float dwell)
if (platform->Time() - dwellTime >= 0.0)
{
dwellWaiting = false;
- reprap.GetMove()->ResumeMoving();
return true;
}
return false;
diff --git a/src/GCodes/GCodes.h b/src/GCodes/GCodes.h
index 6a32aa87..015ab22e 100644
--- a/src/GCodes/GCodes.h
+++ b/src/GCodes/GCodes.h
@@ -225,10 +225,12 @@ private:
bool WriteConfigOverrideFile(StringRef& reply, const char *fileName) const; // Write the config-override file
void CopyConfigFinalValues(GCodeBuffer& gb); // Copy the feed rate etc. from the daemon to the input channels
+ void ClearBabyStepping();
+
static uint32_t LongArrayToBitMap(const long *arr, size_t numEntries); // Convert an array of longs to a bit map
- Platform* const platform; // The RepRap machine
- Webserver* const webserver; // The web server class
+ Platform* const platform; // The RepRap machine
+ Webserver* const webserver; // The web server class
GCodeBuffer* gcodeSources[6]; // The various sources of gcodes
@@ -293,6 +295,8 @@ private:
float lastDefaultFanSpeed; // Last speed given in a M106 command with on fan number
float speedFactor; // speed factor, including the conversion from mm/min to mm/sec, normally 1/60
float extrusionFactors[MaxExtruders]; // extrusion factors (normally 1.0)
+ float currentBabyStepZOffset; // The accumulated Z offset due to baby stepping requests
+ float pendingBabyStepZOffset; // The amount of additional baby stepping requested but not yet acted on
// Z probe
float lastProbedZ; // the last height at which the Z probe stopped
diff --git a/src/GCodes/GCodes2.cpp b/src/GCodes/GCodes2.cpp
index 0d136eb6..104a4570 100644
--- a/src/GCodes/GCodes2.cpp
+++ b/src/GCodes/GCodes2.cpp
@@ -214,6 +214,8 @@ bool GCodes::HandleGcode(GCodeBuffer& gb, StringRef& reply)
{
return false;
}
+
+ ClearBabyStepping();
if (reprap.GetMove()->IsDeltaMode() && !AllAxesAreHomed())
{
reply.copy("Must home a delta printer before bed probing");
@@ -235,6 +237,8 @@ bool GCodes::HandleGcode(GCodeBuffer& gb, StringRef& reply)
return false;
}
+ ClearBabyStepping();
+
// Try to execute bed.g
if (!DoFileMacro(gb, BED_EQUATION_G, reprap.GetMove()->IsDeltaMode()))
{
diff --git a/src/Movement/DDA.cpp b/src/Movement/DDA.cpp
index 9afd2b1e..c0c1cde4 100644
--- a/src/Movement/DDA.cpp
+++ b/src/Movement/DDA.cpp
@@ -259,7 +259,7 @@ bool DDA::Init(const GCodes::RawMove &nextMove, bool doMotorMapping)
{
if (delta > 0)
{
- isPrintingMove = true; // we have both movement and extrusion
+ isPrintingMove = true; // we have both XY movement and extrusion
}
if (nextMove.usePressureAdvance)
{
@@ -304,81 +304,6 @@ bool DDA::Init(const GCodes::RawMove &nextMove, bool doMotorMapping)
}
totalDistance = Normalise(directionVector, DRIVES, numAxes);
- if (isDeltaMovement)
- {
- // The following are only needed when doing delta movements. We could defer computing them until Prepare(), which would make simulation faster.
- a2plusb2 = fsquare(directionVector[X_AXIS]) + fsquare(directionVector[Y_AXIS]);
- cKc = (int32_t)(directionVector[Z_AXIS] * DriveMovement::Kc);
-
- const float initialX = prev->GetEndCoordinate(X_AXIS, false);
- const float initialY = prev->GetEndCoordinate(Y_AXIS, false);
- const DeltaParameters& dparams = move->GetDeltaParams();
- const float diagonalSquared = fsquare(dparams.GetDiagonal());
- const float a2b2D2 = a2plusb2 * diagonalSquared;
-
- for (size_t drive = 0; drive < DELTA_AXES; ++drive)
- {
- const float A = initialX - dparams.GetTowerX(drive);
- const float B = initialY - dparams.GetTowerY(drive);
- const float stepsPerMm = reprap.GetPlatform()->DriveStepsPerUnit(drive);
- DriveMovement& dm = ddm[drive];
- const float aAplusbB = A * directionVector[X_AXIS] + B * directionVector[Y_AXIS];
- const float dSquaredMinusAsquaredMinusBsquared = diagonalSquared - fsquare(A) - fsquare(B);
- float h0MinusZ0 = sqrtf(dSquaredMinusAsquaredMinusBsquared);
- dm.mp.delta.hmz0sK = (int32_t)(h0MinusZ0 * stepsPerMm * DriveMovement::K2);
- dm.mp.delta.minusAaPlusBbTimesKs = -(int32_t)(aAplusbB * stepsPerMm * DriveMovement::K2);
- dm.mp.delta.dSquaredMinusAsquaredMinusBsquaredTimesKsquaredSsquared =
- (int64_t)(dSquaredMinusAsquaredMinusBsquared * fsquare(stepsPerMm * DriveMovement::K2));
-
- // Calculate the distance at which we need to reverse direction.
- if (a2plusb2 <= 0.0)
- {
- // Pure Z movement. We can't use the main calculation because it divides by a2plusb2.
- dm.direction = (directionVector[Z_AXIS] >= 0.0);
- dm.reverseStartStep = dm.totalSteps + 1;
- }
- else
- {
- // The distance to reversal is the solution to a quadratic equation. One root corresponds to the carriages being below the bed,
- // the other root corresponds to the carriages being above the bed.
- const float drev = ((directionVector[Z_AXIS] * sqrt(a2b2D2 - fsquare(A * directionVector[Y_AXIS] - B * directionVector[X_AXIS])))
- - aAplusbB)/a2plusb2;
- if (drev > 0.0 && drev < totalDistance) // if the reversal point is within range
- {
- // Calculate how many steps we need to move up before reversing
- const float hrev = directionVector[Z_AXIS] * drev + sqrt(dSquaredMinusAsquaredMinusBsquared - 2 * drev * aAplusbB - a2plusb2 * fsquare(drev));
- int32_t numStepsUp = (int32_t)((hrev - h0MinusZ0) * stepsPerMm);
-
- // We may be almost at the peak height already, in which case we don't really have a reversal.
- if (numStepsUp < 1 || (dm.direction && (uint32_t)numStepsUp <= dm.totalSteps))
- {
- dm.reverseStartStep = dm.totalSteps + 1;
- }
- else
- {
- dm.reverseStartStep = (uint32_t)numStepsUp + 1;
-
- // Correct the initial direction and the total number of steps
- if (dm.direction)
- {
- // Net movement is up, so we will go up a bit and then down by a lesser amount
- dm.totalSteps = (2 * numStepsUp) - dm.totalSteps;
- }
- else
- {
- // Net movement is down, so we will go up first and then down by a greater amount
- dm.direction = true;
- dm.totalSteps = (2 * numStepsUp) + dm.totalSteps;
- }
- }
- }
- else
- {
- dm.reverseStartStep = dm.totalSteps + 1;
- }
- }
- }
- }
}
else
{
@@ -424,7 +349,7 @@ bool DDA::Init(const GCodes::RawMove &nextMove, bool doMotorMapping)
// for diagonal moves. On a delta, this is not OK and any movement in the XY plane should be limited to the X/Y axis values, which we assume to be equal.
if (isDeltaMovement)
{
- const float xyFactor = sqrtf(fsquare(normalisedDirectionVector[X_AXIS]) + fsquare(normalisedDirectionVector[X_AXIS]));
+ const float xyFactor = sqrtf(fsquare(normalisedDirectionVector[X_AXIS]) + fsquare(normalisedDirectionVector[Y_AXIS]));
const float maxSpeed = reprap.GetPlatform()->MaxFeedrates()[X_AXIS];
if (requestedSpeed * xyFactor > maxSpeed)
{
@@ -766,15 +691,93 @@ float DDA::CalcTime() const
// This must not be called with interrupts disabled, because it calls Platform::EnableDrive.
void DDA::Prepare()
{
-//debugPrintf("Prep\n");
+ if (isDeltaMovement)
+ {
+ // This code used to be in DDA::Init but we moved it here so that we can implement babystepping in it,
+ // also it speeds up simulation because we no longer execute this code when simulating.
+ // However, this code assumes that the previous move in the DDA ring is the previously-executed move, because it fetches the X and Y end coordinates from that move.
+ // Therefore the Move code must not store a new move in that entry until this one has been prepared! (It took me ages to track this down.)
+ // Ideally we would store the initial X any Y coordinates in the DDA, but we need to be economical with memory in the Duet 06/085 build.
+ a2plusb2 = fsquare(directionVector[X_AXIS]) + fsquare(directionVector[Y_AXIS]);
+ cKc = (int32_t)(directionVector[Z_AXIS] * DriveMovement::Kc);
+
+ const float initialX = prev->GetEndCoordinate(X_AXIS, false);
+ const float initialY = prev->GetEndCoordinate(Y_AXIS, false);
+ const DeltaParameters& dparams = reprap.GetMove()->GetDeltaParams();
+ const float diagonalSquared = fsquare(dparams.GetDiagonal());
+ const float a2b2D2 = a2plusb2 * diagonalSquared;
+
+ for (size_t drive = 0; drive < DELTA_AXES; ++drive)
+ {
+ const float A = initialX - dparams.GetTowerX(drive);
+ const float B = initialY - dparams.GetTowerY(drive);
+ const float stepsPerMm = reprap.GetPlatform()->DriveStepsPerUnit(drive);
+ DriveMovement& dm = ddm[drive];
+ const float aAplusbB = A * directionVector[X_AXIS] + B * directionVector[Y_AXIS];
+ const float dSquaredMinusAsquaredMinusBsquared = diagonalSquared - fsquare(A) - fsquare(B);
+ const float h0MinusZ0 = sqrtf(dSquaredMinusAsquaredMinusBsquared);
+ dm.mp.delta.hmz0sK = (int32_t)(h0MinusZ0 * stepsPerMm * DriveMovement::K2);
+ dm.mp.delta.minusAaPlusBbTimesKs = -(int32_t)(aAplusbB * stepsPerMm * DriveMovement::K2);
+ dm.mp.delta.dSquaredMinusAsquaredMinusBsquaredTimesKsquaredSsquared =
+ (int64_t)(dSquaredMinusAsquaredMinusBsquared * fsquare(stepsPerMm * DriveMovement::K2));
+
+ // Calculate the distance at which we need to reverse direction.
+ if (a2plusb2 <= 0.0)
+ {
+ // Pure Z movement. We can't use the main calculation because it divides by a2plusb2.
+ dm.direction = (directionVector[Z_AXIS] >= 0.0);
+ dm.reverseStartStep = dm.totalSteps + 1;
+ }
+ else
+ {
+ // The distance to reversal is the solution to a quadratic equation. One root corresponds to the carriages being below the bed,
+ // the other root corresponds to the carriages being above the bed.
+ const float drev = ((directionVector[Z_AXIS] * sqrt(a2b2D2 - fsquare(A * directionVector[Y_AXIS] - B * directionVector[X_AXIS])))
+ - aAplusbB)/a2plusb2;
+ if (drev > 0.0 && drev < totalDistance) // if the reversal point is within range
+ {
+ // Calculate how many steps we need to move up before reversing
+ const float hrev = directionVector[Z_AXIS] * drev + sqrt(dSquaredMinusAsquaredMinusBsquared - 2 * drev * aAplusbB - a2plusb2 * fsquare(drev));
+ const int32_t numStepsUp = (int32_t)((hrev - h0MinusZ0) * stepsPerMm);
+
+ // We may be almost at the peak height already, in which case we don't really have a reversal.
+ if (numStepsUp < 1 || (dm.direction && (uint32_t)numStepsUp <= dm.totalSteps))
+ {
+ dm.reverseStartStep = dm.totalSteps + 1;
+ }
+ else
+ {
+ dm.reverseStartStep = (uint32_t)numStepsUp + 1;
+
+ // Correct the initial direction and the total number of steps
+ if (dm.direction)
+ {
+ // Net movement is up, so we will go up a bit and then down by a lesser amount
+ dm.totalSteps = (2 * numStepsUp) - dm.totalSteps;
+ }
+ else
+ {
+ // Net movement is down, so we will go up first and then down by a greater amount
+ dm.direction = true;
+ dm.totalSteps = (2 * numStepsUp) + dm.totalSteps;
+ }
+ }
+ }
+ else
+ {
+ dm.reverseStartStep = dm.totalSteps + 1;
+ }
+ }
+ }
+ }
PrepParams params;
params.decelStartDistance = totalDistance - decelDistance;
// Convert the accelerate/decelerate distances to times
- float accelStopTime = (topSpeed - startSpeed)/acceleration;
- float decelStartTime = accelStopTime + (params.decelStartDistance - accelDistance)/topSpeed;
- float totalTime = decelStartTime + (topSpeed - endSpeed)/acceleration;
+ const float accelStopTime = (topSpeed - startSpeed)/acceleration;
+ const float decelStartTime = accelStopTime + (params.decelStartDistance - accelDistance)/topSpeed;
+ const float totalTime = decelStartTime + (topSpeed - endSpeed)/acceleration;
clocksNeeded = (uint32_t)(totalTime * stepClockRate);
diff --git a/src/Movement/Move.cpp b/src/Movement/Move.cpp
index e2334a45..c3d74d9f 100644
--- a/src/Movement/Move.cpp
+++ b/src/Movement/Move.cpp
@@ -47,8 +47,6 @@ void Move::Init()
} while (dda != ddaRingAddPointer);
currentDda = nullptr;
- addNoMoreMoves = false;
- babysteppingLeft = 0.0;
stepErrors = 0;
numLookaheadUnderruns = numPrepareUnderruns = 0;
@@ -145,9 +143,11 @@ void Move::Spin()
// See if we can add another move to the ring
if (
#if SUPPORT_ROLAND
- !reprap.GetRoland()->Active() &&
+ !reprap.GetRoland()->Active() &&
#endif
- !addNoMoreMoves && ddaRingAddPointer->GetState() == DDA::empty)
+ ddaRingAddPointer->GetState() == DDA::empty
+ && ddaRingAddPointer->GetNext()->GetState() != DDA::provisional // function Prepare needs to access the endpoints in the previous move, so don't change them
+ )
{
// In order to react faster to speed and extrusion rate changes, only add more moves if the total duration of
// all un-frozen moves is less than 2 seconds, or the total duration of all but the first un-frozen move is
@@ -389,13 +389,6 @@ FilePosition Move::PausePrint(float positions[DRIVES], float& pausedFeedRate, ui
return fPos;
}
-// Request babystepping
-void Move::Babystep(float zMovement)
-{
- babysteppingLeft += zMovement;
- // TODO use this value somewhere
-}
-
uint32_t maxReps = 0;
#if 0
diff --git a/src/Movement/Move.h b/src/Movement/Move.h
index d23090ca..fcfef155 100644
--- a/src/Movement/Move.h
+++ b/src/Movement/Move.h
@@ -53,7 +53,6 @@ public:
void Interrupt(); // The hardware's (i.e. platform's) interrupt should call this.
void InterruptTime(); // Test function - not used
bool AllMovesAreFinished(); // Is the look-ahead ring empty? Stops more moves being added as well.
- void ResumeMoving(); // Allow moves to be added after a call to AllMovesAreFinished()
void DoLookAhead(); // Run the look-ahead procedure
void HitLowStop(size_t axis, DDA* hitDDA); // What to do when a low endstop is hit
void HitHighStop(size_t axis, DDA* hitDDA); // What to do when a high endstop is hit
@@ -121,8 +120,6 @@ public:
HeightMap& AccessBedProbeGrid() { return grid; } // Access the bed probing grid
- void Babystep(float zMovement); // Request babystepping
-
private:
enum class IdleState : uint8_t { idle, busy, timing };
@@ -154,7 +151,6 @@ private:
DDA* volatile ddaRingGetPointer;
DDA* ddaRingCheckPointer;
- bool addNoMoreMoves; // If true, allow no more moves to be added to the look-ahead
bool active; // Are we live and running?
uint8_t simulationMode; // Are we simulating, or really printing?
bool waitingForMove; // True if we are waiting for a new move
@@ -198,8 +194,6 @@ private:
int coreXYMode; // 0 = Cartesian, 1 = CoreXY, 2 = CoreXZ, 3 = CoreYZ
float axisFactors[MAX_AXES]; // How much further the motors need to move for each axis movement, on a CoreXY/CoreXZ/CoreYZ machine
unsigned int stepErrors; // count of step errors, for diagnostics
-
- float babysteppingLeft; // the amount of Z babystepping left to do
};
//******************************************************************************************************
@@ -219,15 +213,9 @@ inline bool Move::NoLiveMovement() const
// Then call ResumeMoving() otherwise nothing more will ever happen.
inline bool Move::AllMovesAreFinished()
{
- addNoMoreMoves = true;
return NoLiveMovement();
}
-inline void Move::ResumeMoving()
-{
- addNoMoreMoves = false;
-}
-
// Start the next move. Must be called with interrupts disabled, to avoid a race with the step ISR.
inline bool Move::StartNextMove(uint32_t startTime)
pre(ddaRingGetPointer->GetState() == DDA::frozen)
diff --git a/src/Platform.cpp b/src/Platform.cpp
index 7b9e479f..6946c4c6 100644
--- a/src/Platform.cpp
+++ b/src/Platform.cpp
@@ -113,7 +113,7 @@ void setup()
SCB->CCR |= SCB_CCR_DIV_0_TRP_Msk;
// When doing a software reset, we disable the NRST input (User reset) to prevent the negative-going pulse that gets generated on it
- // being held in the capacitor and changing the reset reason form Software to User. So enable it again here. We hope that the reset signal
+ // being held in the capacitor and changing the reset reason from Software to User. So enable it again here. We hope that the reset signal
// will have gone away by now.
#ifndef RSTC_MR_KEY_PASSWD
// Definition of RSTC_MR_KEY_PASSWD is missing in the SAM3X ASF files
@@ -828,8 +828,6 @@ void Platform::UpdateFirmware()
#if (SAM4S || SAM4E)
// The EWP command is not supported for non-8KByte sectors in the SAM4 series.
// So we have to unlock and erase the complete 64Kb sector first.
- // TODO save the NVRAM area and restore it later
-
flash_unlock(IAP_FLASH_START, IAP_FLASH_END, nullptr, nullptr);
flash_erase_sector(IAP_FLASH_START);
@@ -936,7 +934,7 @@ void Platform::UpdateFirmware()
Message(FIRMWARE_UPDATE_MESSAGE, "Updating main firmware\n");
// Allow time for the firmware update message to be sent
- uint32_t now = millis();
+ const uint32_t now = millis();
while (FlushMessages() && millis() - now < 2000) { }
// Step 2 - Let the firmware do whatever is necessary before we exit this program
@@ -946,15 +944,29 @@ void Platform::UpdateFirmware()
// This does essentially what the Atmel AT02333 paper suggests (see 3.2.2 ff)
// Disable all IRQs
+ SysTick->CTRL = SysTick_CTRL_CLKSOURCE_Msk; // disable the system tick exception
cpu_irq_disable();
- for(size_t i = 0; i < 8; i++)
+ for (size_t i = 0; i < 8; i++)
+ {
+ NVIC->ICER[i] = 0xFFFFFFFF; // Disable IRQs
+ NVIC->ICPR[i] = 0xFFFFFFFF; // Clear pending IRQs
+ }
+
+ // Newer versions of iap4e.bin reserve space above the stack for us to pass the firmware filename
+ static const char filename[] = "0:/sys/" IAP_FIRMWARE_FILE;
+ const uint32_t topOfStack = *reinterpret_cast<uint32_t *>(IAP_FLASH_START);
+ if (topOfStack + sizeof(filename) <=
+#if (SAM4S || SAM4E)
+ IRAM_ADDR + IRAM_SIZE
+#else
+ IRAM1_ADDR + IRAM1_SIZE
+#endif
+ )
{
- NVIC->ICER[i] = 0xFFFFFFFF; // Disable IRQs
- NVIC->ICPR[i] = 0xFFFFFFFF; // Clear pending IRQs
+ memcpy(reinterpret_cast<char*>(topOfStack), filename, sizeof(filename));
}
- // Our SAM3X doesn't support disabling the watchdog, so leave it running.
- // The IAP binary will kick it as soon as it's started
+ wdt_restart(WDT); // kick the watchdog one last time
// Modify vector table location
__DSB();
@@ -963,12 +975,13 @@ void Platform::UpdateFirmware()
__DSB();
__ISB();
- // Reset stack pointer, enable IRQs again and start the new IAP binary
- __set_MSP(*(uint32_t *)IAP_FLASH_START);
cpu_irq_enable();
- void *entryPoint = (void *)(*(uint32_t *)(IAP_FLASH_START + 4));
- goto *entryPoint;
+ __asm volatile ("mov r3, %0" : : "r" (IAP_FLASH_START) : "r3");
+ __asm volatile ("ldr sp, [r3]");
+ __asm volatile ("ldr r1, [r3, #4]");
+ __asm volatile ("orr r1, r1, #1");
+ __asm volatile ("bx r1");
}
// Send the beep command to the aux channel. There is no flow control on this port, so it can't block for long.
@@ -1008,7 +1021,7 @@ float Platform::Time()
void Platform::Exit()
{
// Close all files
- for(size_t i = 0; i < MAX_FILES; i++)
+ for (size_t i = 0; i < MAX_FILES; i++)
{
while (files[i]->inUse)
{
@@ -1024,6 +1037,14 @@ void Platform::Exit()
// Stop processing data. Don't try to send a message because it will probably never get there.
active = false;
+
+ // Close down USB and serial ports
+ SERIAL_MAIN_DEVICE.end();
+ SERIAL_AUX_DEVICE.end();
+#ifdef SERIAL_AUX2_DEVICE
+ SERIAL_AUX2_DEVICE.end();
+#endif
+
}
Compatibility Platform::Emulating() const
@@ -1455,13 +1476,19 @@ void Platform::Diagnostics(MessageType mtype)
Message(mtype, "Last software reset code ");
if (slot >= 0 && srdBuf[slot].magic == SoftwareResetData::magicValue)
{
- scratchString.Clear();
- for (size_t i = 0; i < ARRAY_SIZE(srdBuf[slot].stack); ++i)
+ // Our format buffer is only 256 characters long, so the next 2 lines must be written separately
+ MessageF(mtype, "0x%04x, HFSR 0x%08x, CFSR 0x%08x, ICSR 0x%08x, BFAR 0x%08x, SP 0x%08x\n",
+ srdBuf[slot].resetReason, srdBuf[slot].hfsr, srdBuf[slot].cfsr, srdBuf[slot].icsr, srdBuf[slot].bfar, srdBuf[slot].sp);
+ if (srdBuf[slot].sp != 0xFFFFFFFF)
{
- scratchString.catf(" %08x", srdBuf[slot].stack[i]);
+ // We saved a stack dump, so print it
+ scratchString.Clear();
+ for (size_t i = 0; i < ARRAY_SIZE(srdBuf[slot].stack); ++i)
+ {
+ scratchString.catf(" %08x", srdBuf[slot].stack[i]);
+ }
+ MessageF(mtype, "Stack:%s\n", scratchString.Pointer());
}
- MessageF(mtype, "0x%04x, HFSR 0x%08x, CFSR 0x%08x, ICSR 0x%08x, BFAR 0x%08x, SP 0x%08x\nStack:%s\n",
- srdBuf[slot].resetReason, srdBuf[slot].hfsr, srdBuf[slot].cfsr, srdBuf[slot].icsr, srdBuf[slot].bfar, srdBuf[slot].sp, scratchString.Pointer());
MessageF(mtype, "Spinning module during software reset: %s, available RAM %u bytes (slot %d)\n",
moduleName[srdBuf[slot].resetReason & 0x0F], srdBuf[slot].neverUsedRam, slot);
}
diff --git a/src/Platform.h b/src/Platform.h
index 869d05d0..cb0be103 100644
--- a/src/Platform.h
+++ b/src/Platform.h
@@ -167,7 +167,7 @@ enum class SoftwareResetReason : uint16_t
inUsbOutput = 0x4000 // this bit is or'ed in if we were in USB output at the time
};
-// Enumeration to describe various tests we do in response to the M111 command
+// Enumeration to describe various tests we do in response to the M122 command
enum class DiagnosticTestType : int
{
TestWatchdog = 1001, // test that we get a watchdog reset if the tick interrupt stops
diff --git a/src/RADDS/Pins_radds.h b/src/RADDS/Pins_radds.h
index ced457c3..9fba50da 100644
--- a/src/RADDS/Pins_radds.h
+++ b/src/RADDS/Pins_radds.h
@@ -5,7 +5,7 @@
const size_t NumFirmwareUpdateModules = 1;
#define IAP_UPDATE_FILE "iapradds.bin"
-#define IAP_FIRMWARE_FILE "RepRapFirmware.bin"
+#define IAP_FIRMWARE_FILE "RepRapFirmware-RADDS.bin"
// Default board type
#define DEFAULT_BOARD_TYPE BoardType::RADDS_15
@@ -32,7 +32,7 @@ const int8_t HEATERS = 4;
// defaultPidKis[HEATERS] = {HEATERS_(5.0, 0.1, 0.1, 0.1, 0.1, 0.1)};
#define HEATERS_(a,b,c,d,e,f,g,h) { a,b,c,d }
-const size_t MAX_AXES = 6; // FIXME The maximum number of movement axes in the machine, usually just X, Y and Z, <= DRIVES
+const size_t MAX_AXES = 6; // The maximum number of movement axes in the machine, usually just X, Y and Z, <= DRIVES
const size_t MIN_AXES = 3; // The minimum and default number of axes
const size_t MaxExtruders = DRIVES - MIN_AXES; // The maximum number of extruders
@@ -41,11 +41,10 @@ const size_t NUM_SERIAL_CHANNELS = 2;
#define SERIAL_MAIN_DEVICE SerialUSB
#define SERIAL_AUX_DEVICE Serial1
-// The numbers of entries in each array must correspond with the values of DRIVES, AXES, or HEATERS. Set values to -1 to flag unavailability.
+// The numbers of entries in each array must correspond with the values of DRIVES, AXES, or HEATERS. Set values to NoPin to flag unavailability.
// DRIVES
// X Y Z E1 E2 E3 E4 E5
const Pin ENABLE_PINS[DRIVES] = { 26, 22, 15, 62, 65, 49, 37, 31 };
-const bool ENABLE_VALUES[DRIVES] = { false, false, false, false, false, false, false, false };
// A15 D04 B25 A02 B19 C12 C03 D06
const Pin STEP_PINS[DRIVES] = { 24, 17, 2, 61, 64, 51, 35, 29 };
const Pin DIRECTION_PINS[DRIVES] = { 23, 16, 3, 60, 63, 53, 33, 27 };
@@ -110,7 +109,6 @@ const Pin Z_PROBE_PIN = 5; // RADDS "ADC" pin
const Pin Z_PROBE_MOD_PIN = 34;
// Use a PWM capable pin
-// Firmware uses SamNonDue so feel free to use a non-Arduino pin
const size_t NUM_FANS = 2;
const Pin COOLING_FAN_PINS[NUM_FANS] = { 9, 8 }; // Fan 0, Fan 1
diff --git a/src/RepRapFirmware.cpp b/src/RepRapFirmware.cpp
index 97ea7f37..c29712b3 100644
--- a/src/RepRapFirmware.cpp
+++ b/src/RepRapFirmware.cpp
@@ -186,7 +186,7 @@ const char *moduleName[] =
// Utilities and storage not part of any class
-static char scratchStringBuffer[150]; // this is now used only for short messages; needs to be long enough to print delta parameters and 12 words of stack (132 bytes)
+static char scratchStringBuffer[170]; // this needs to be long enough to print delta parameters and 18 words of stack (162 bytes)
StringRef scratchString(scratchStringBuffer, ARRAY_SIZE(scratchStringBuffer));
// For debug use
diff --git a/src/Reprap.cpp b/src/Reprap.cpp
index 76cb970d..d2ac20ca 100644
--- a/src/Reprap.cpp
+++ b/src/Reprap.cpp
@@ -490,28 +490,24 @@ unsigned int RepRap::GetNumberOfContiguousTools() const
void RepRap::Tick()
{
- if (active)
+ if (active && !resetting)
{
- Platform::KickWatchdog();
- if (!resetting)
+ platform->Tick();
+ ++ticksInSpinState;
+ if (ticksInSpinState >= 20000) // if we stall for 20 seconds, save diagnostic data and reset
{
- platform->Tick();
- ++ticksInSpinState;
- if (ticksInSpinState >= 20000) // if we stall for 20 seconds, save diagnostic data and reset
+ resetting = true;
+ for(size_t i = 0; i < HEATERS; i++)
{
- resetting = true;
- for(size_t i = 0; i < HEATERS; i++)
- {
- platform->SetHeater(i, 0.0);
- }
- for(size_t i = 0; i < DRIVES; i++)
- {
- platform->DisableDrive(i);
- // We can't set motor currents to 0 here because that requires interrupts to be working, and we are in an ISR
- }
-
- platform->SoftwareReset((uint16_t)SoftwareResetReason::stuckInSpin);
+ platform->SetHeater(i, 0.0);
+ }
+ for(size_t i = 0; i < DRIVES; i++)
+ {
+ platform->DisableDrive(i);
+ // We can't set motor currents to 0 here because that requires interrupts to be working, and we are in an ISR
}
+
+ platform->SoftwareReset((uint16_t)SoftwareResetReason::stuckInSpin);
}
}
}
diff --git a/src/Version.h b/src/Version.h
index d7aa8890..adf9827e 100644
--- a/src/Version.h
+++ b/src/Version.h
@@ -9,11 +9,11 @@
#define SRC_VERSION_H_
#ifndef VERSION
-# define VERSION "1.17e"
+# define VERSION "1.18alpha2"
#endif
#ifndef DATE
-# define DATE "2017-02-10"
+# define DATE "2017-02-17"
#endif
#define AUTHORS "reprappro, dc42, chrishamm, t3p3, dnewman"