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