diff --git a/cpp_utils/.gitignore b/cpp_utils/.gitignore index 190415da..6e684992 100644 --- a/cpp_utils/.gitignore +++ b/cpp_utils/.gitignore @@ -1,2 +1 @@ /docs/ -/Arduino/ diff --git a/cpp_utils/Arduino/ESP32_BLE.zip b/cpp_utils/Arduino/ESP32_BLE.zip new file mode 100644 index 00000000..c2537272 Binary files /dev/null and b/cpp_utils/Arduino/ESP32_BLE.zip differ diff --git a/cpp_utils/ArduinoBLE.md b/cpp_utils/ArduinoBLE.md index 9e5169ad..a2cb3be6 100644 --- a/cpp_utils/ArduinoBLE.md +++ b/cpp_utils/ArduinoBLE.md @@ -1,15 +1,13 @@ # Arduino BLE Support -As part of this Github project, we provide libraries for Bluetooth Low Energy (BLE) for the ESP32 Arduino environment. Support for this capability is still in the process of being cooked (as of August 2017). As such you should not build product based on these functions as changes to the API and implementation are extremely likely over the next couple of months. +As part of this Github project, we provide libraries for Bluetooth Low Energy (BLE) for the ESP32 Arduino environment. Support for this capability is still in the process of being cooked (as of September 2017). As such you should not build product based on these functions as changes to the API and implementation are extremely likely over the next couple of months. That said, we now have the ability to produce a driver you can use for testing. This will give you early access to the function while give those who choose to build and maintain the code early feedback on what works, what doesn't and what can be improved from a usage perspective. When complete, the BLE support for ESP32 Arduino will be available as a single ZIP file. The file will be called **ESP32_BLE.zip**. It is this file that will be able to be imported into the Arduino IDE from the `Sketch -> Include Library -> Add .ZIP library`. When initial development of the library has been completed, this ZIP will be placed under some form of release control so that an ordinary Arduino IDE user can simply download this as a unit and install. -However, today, we are choosing **not** to distribute the BLE support as a downloadable library. The reason for this is that we believe that the code base for the BLE support is too fluid and it is being actively worked upon. Each day there will be changes and that means that for any given instance of the ZIP you get, if it is more than a day old, it is likely out of date. +We provide sample builds of the `ESP32_BLE.zip` file in the `Arduino` folder found relative to this directory. -Instead of giving you a fish (a new ESP32_BLE.zip) we are going to teach you to fish so that you yourself can build the very latest ESP32_BLE.zip yourself. Hopefully you will find this quite easy to accomplish. - -Here is the recipe. +The build of the Arduino support will be current as of the date of the ZIP file however should you wish to build your own instance of the ZIP from the source, here is the recipe. 1. Create a new directory called `build` 2. Enter that directory and run `git clone https://github.com/nkolban/esp32-snippets.git` @@ -24,35 +22,16 @@ And here you will find the `ESP32_BLE.zip` that is build from the latest source. If you have previously installed a version of the Arduino BLE Support and need to install a new one, follow the steps above to build yourself a new instance of the `ESP32_BLE.zip` that is ready for installation. I recommend removing the old one before installing the new one. To remove the old one, find the directory where the Arduino IDE stores your libraries (on Linux this is commonly `$HOME/Arduino`). In there you will find a directory called `libraries` and, if you have previously installed the BLE support, you will find a directory called `ESP32_BLE`. Remove that directory. ## Switching on debugging -The BLE support contains extensive internal diagnostics which can be switched on by editing the file called `sdkconfig` and finding the lines which read: +The BLE support contains extensive internal diagnostics which can be switched on by editing the file called `sdkconfig.h` and finding the lines which read: ``` -# -# Log output -# -# CONFIG_LOG_DEFAULT_LEVEL_NONE is not set -CONFIG_LOG_DEFAULT_LEVEL_ERROR=y -# CONFIG_LOG_DEFAULT_LEVEL_WARN is not set -# CONFIG_LOG_DEFAULT_LEVEL_INFO is not set -# CONFIG_LOG_DEFAULT_LEVEL_DEBUG is not set -# CONFIG_LOG_DEFAULT_LEVEL_VERBOSE is not set -CONFIG_LOG_DEFAULT_LEVEL=1 +#define CONFIG_LOG_DEFAULT 1 ``` Change this to: ``` -# -# Log output -# -# CONFIG_LOG_DEFAULT_LEVEL_NONE is not set -# CONFIG_LOG_DEFAULT_LEVEL_ERROR=y -# CONFIG_LOG_DEFAULT_LEVEL_WARN is not set -# CONFIG_LOG_DEFAULT_LEVEL_INFO is not set -# CONFIG_LOG_DEFAULT_LEVEL_DEBUG is not set -CONFIG_LOG_DEFAULT_LEVEL_VERBOSE=y -CONFIG_LOG_DEFAULT_LEVEL=5 -# CONFIG_LOG_COLORS is not set +#define CONFIG_LOG_DEFAULT 5 ``` and rebuild/deploy your project. @@ -60,5 +39,5 @@ and rebuild/deploy your project. This file can be found in your Arduino IDE installation directories at: ``` -/hardware/espressif/esp32/tools/sdk +/hardware/espressif/esp32/tools/sdk/include/config ``` \ No newline at end of file diff --git a/cpp_utils/DesignNotes/WebSockets.md b/cpp_utils/DesignNotes/WebSockets.md index 3187fe89..0a5ef897 100644 --- a/cpp_utils/DesignNotes/WebSockets.md +++ b/cpp_utils/DesignNotes/WebSockets.md @@ -13,11 +13,11 @@ For example, imagine a WebSocket client wants to send a file of 1MByte to the ES This sounds workable ... so let us now think about how we might go about creating an input stream for a WebSocket message. Each WebSocket message starts with a WebSocket frame which contains, amongst other things, the length of the payload data. This means that we know up front how much of the remaining data is payload. This becomes essential as we can't rely on an "end of file" marker in the input stream to indicate the end of the WebSocket payload. The reason for this is that the WebSocket is a TCP stream that will be used to carry multiple sequential messages. -Let us now invent a new class. Let us call it a SocketInputRecordStream. +Let us now invent a new class. Let us call it a SocketInputRecordStreambuf. It will have a constructor of the form: ``` -SocketInputRecordStream(Socket &socket, size_t dataLength, size_t bufferSize=512) +SocketInputRecordStreambuf(Socket &socket, size_t dataLength, size_t bufferSize=512) ``` The `socket` is the TCP/IP socket that we are going to read data from. The `dataLength` is the size of the data we wish to read. The class will extend `std::streambuf`. It will internally maintain a data buffer of size `bufferSize`. Initially, the buffer will be empty. When a read is performed on the stream, a call to `underflow()` will be made (this is a `std::streambuf` virtual function). @@ -27,4 +27,38 @@ Our rules for this class include: * We must **not** read more the `dataLength` bytes from the socket. * We must **indicate** and `EOF` once we have had `dataLength` bytes consumed by a stream reader. * The class must implement a `discard()` method that will discard further bytes from the socket such that the total number of bytes read from the socket will equal `dataLength`. -* Deleting an instance of the class must invoke `discard()`. \ No newline at end of file +* Deleting an instance of the class must invoke `discard()`. + +## File transfer +WebSockets make a great file transfer technology. While this is more an application utilization of the technology than the design of the framework, we'll capture it here. Let us first set the scene. We have a client application that wishes to transmit a file. We will assume that the file is composed of three logical components: + +* The file name +* The file length +* The content of the file + +It would be wrong to expect the client to send the file as one continuous unit in one WebSocket message. The reason for this is that the client would have to have loaded the complete file into its memory buffers in order to send it. As such, we should assume that the client will send the files as one or more "parts" where each part represents a piece of the file. + +We thus invent the following protocol: + +For the first message we have: + +``` ++-----------------------+-----------+----+--------------------+-----------+ +| transferId (32bit/LE) | file name | \0 | length (32bits/LE) | Data .... | ++-----------------------+-----------+----+--------------------+-----------+ +``` + +For subsequent messages we have: + +``` ++-----------------------+-----------+ +| transferId (32bit/LE) | Data .... | ++-----------------------+-----------+ +``` +Let us look at these. + +* `transferId` - An Id that is randomly generated by the client. This is used to associate multiple messages for the same file together. +* `file name` - The name of the file that the client wishes to send. Can include paths. This will be used to determine where on the file system the file will be written. +* `length` - The size of the file in bytes. Knowing the size will allow us to know when the whole file has been received. + +We will create an encapsulation class called `WebSocketFileTransfer`. \ No newline at end of file diff --git a/cpp_utils/FATFS_VFS.cpp b/cpp_utils/FATFS_VFS.cpp index 4143899b..30af8e4d 100644 --- a/cpp_utils/FATFS_VFS.cpp +++ b/cpp_utils/FATFS_VFS.cpp @@ -26,10 +26,10 @@ * @param [in] partitionName The name of the partition used to store the FAT file system. */ FATFS_VFS::FATFS_VFS(std::string mountPath, std::string partitionName) { - m_mountPath = mountPath; + m_mountPath = mountPath; m_partitionName = partitionName; - m_maxFiles = 4; - m_wl_handle = WL_INVALID_HANDLE; + m_maxFiles = 4; + m_wl_handle = WL_INVALID_HANDLE; } // FATFS_VFS diff --git a/cpp_utils/FATFS_VFS.h b/cpp_utils/FATFS_VFS.h index fe9a7e57..24ce4928 100644 --- a/cpp_utils/FATFS_VFS.h +++ b/cpp_utils/FATFS_VFS.h @@ -10,6 +10,7 @@ #include extern "C" { #include +#include } /** * @brief Provide access to the FAT file system on %SPI flash. @@ -42,6 +43,7 @@ class FATFS_VFS { public: FATFS_VFS(std::string mountPath, std::string partitionName); virtual ~FATFS_VFS(); + void mount(); void setMaxFiles(int maxFiles); void unmount(); @@ -49,7 +51,7 @@ class FATFS_VFS { wl_handle_t m_wl_handle; std::string m_mountPath; std::string m_partitionName; - int m_maxFiles; + int m_maxFiles; }; #endif /* COMPONENTS_CPP_UTILS_FATFS_VFS_H_ */ diff --git a/cpp_utils/HttpRequest.cpp b/cpp_utils/HttpRequest.cpp index cd98ecc4..265d4ef9 100644 --- a/cpp_utils/HttpRequest.cpp +++ b/cpp_utils/HttpRequest.cpp @@ -122,7 +122,7 @@ HttpRequest::~HttpRequest() { } // ~HttpRequest -void HttpRequest::close_cpp() { +void HttpRequest::close() { m_clientSocket.close_cpp(); } // close_cpp diff --git a/cpp_utils/HttpRequest.h b/cpp_utils/HttpRequest.h index e48ba75c..398e02ed 100644 --- a/cpp_utils/HttpRequest.h +++ b/cpp_utils/HttpRequest.h @@ -14,7 +14,7 @@ #include "WebSocket.h" #include "HttpParser.h" - +#undef close class HttpRequest { private: @@ -51,10 +51,8 @@ class HttpRequest { static const std::string HTTP_METHOD_POST; static const std::string HTTP_METHOD_PUT; - - - void close_cpp(); - void dump(); + void close(); + void dump(); std::string getBody(); std::string getHeader(std::string name); std::map getHeaders(); diff --git a/cpp_utils/HttpResponse.cpp b/cpp_utils/HttpResponse.cpp index 947b3e62..4063932d 100644 --- a/cpp_utils/HttpResponse.cpp +++ b/cpp_utils/HttpResponse.cpp @@ -50,9 +50,9 @@ void HttpResponse::addHeader(const std::string name, const std::string value) { } // addHeader -void HttpResponse::close_cpp() { - m_request->close_cpp(); -} // close_cpp +void HttpResponse::close() { + m_request->close(); +} // close std::string HttpResponse::getHeader(std::string name) { @@ -74,6 +74,7 @@ std::map HttpResponse::getHeaders() { * @param [in] data The data to send to the partner. */ void HttpResponse::sendData(std::string data) { + // If we haven't yet sent the header of the data, send that now. if (m_headerCommitted == false) { std::ostringstream oss; oss << m_request->getVersion() << " " << m_status << " " << m_statusMessage << lineTerminator; @@ -84,6 +85,8 @@ void HttpResponse::sendData(std::string data) { m_headerCommitted = true; m_request->getSocket().send_cpp(oss.str()); } + + // Send the payload data. m_request->getSocket().send_cpp(data); } // sendData diff --git a/cpp_utils/HttpResponse.h b/cpp_utils/HttpResponse.h index 089fab91..5a9fe2ec 100644 --- a/cpp_utils/HttpResponse.h +++ b/cpp_utils/HttpResponse.h @@ -36,7 +36,7 @@ class HttpResponse { virtual ~HttpResponse(); void addHeader(std::string name, std::string value); - void close_cpp(); + void close(); //std::string getRootPath(); std::string getHeader(std::string name); std::map getHeaders(); diff --git a/cpp_utils/HttpServer.cpp b/cpp_utils/HttpServer.cpp index 9661902a..c342d098 100644 --- a/cpp_utils/HttpServer.cpp +++ b/cpp_utils/HttpServer.cpp @@ -5,6 +5,7 @@ * Author: kolban */ +#include #include "HttpServer.h" #include "SockServ.h" #include "Task.h" @@ -14,6 +15,7 @@ #include "WebSocket.h" static const char* LOG_TAG = "HttpServer"; +#undef close /** * Constructor for HTTP Server */ @@ -30,13 +32,16 @@ HttpServer::~HttpServer() { */ class HttpServerTask: public Task { public: - HttpServerTask(std::string name): Task(name) {}; + HttpServerTask(std::string name): Task(name) { + m_pHttpServer = nullptr; + }; private: HttpServer* m_pHttpServer; // Reference to the HTTP Server /** * @brief Process an incoming HTTP Request + * * We examine each of the path handlers to see if we have a match for the method/path pair. If we do, * we invoke the handler callback passing in both the request and response. * @@ -45,52 +50,81 @@ class HttpServerTask: public Task { * @param [in] request The HTTP request to process. */ void processRequest(HttpRequest &request) { - ESP_LOGD(LOG_TAG, ">> processRequest: Method: %s, Path: %s", - request.getMethod().c_str(), request.getPath().c_str()) + ESP_LOGD("HttpServerTask", ">> processRequest: Method: %s, Path: %s", + request.getMethod().c_str(), request.getPath().c_str()); + for (auto it = m_pHttpServer->m_pathHandlers.begin(); it != m_pHttpServer->m_pathHandlers.end(); ++it) { if (it->match(request.getMethod(), request.getPath())) { - ESP_LOGD(LOG_TAG, "Found a path handler match!!"); + ESP_LOGD("HttpServerTask", "Found a path handler match!!"); if (request.isWebsocket()) { - it->invoke(&request, nullptr); + it->invokePathHandler(&request, nullptr); request.getWebSocket()->startReader(); } else { HttpResponse response(&request); - - it->invoke(&request, &response); + it->invokePathHandler(&request, &response); } return; } // Path handler match } // For each path handler + ESP_LOGD("HttpServerTask", "No Path handler found"); + // If we reach here, then we did not find a handler for the request. + + // Check to see if we have an un-handled WebSocket + if (request.isWebsocket()) { + request.getWebSocket()->close_cpp(); + return; + } + // Serve up the content from the file on the file system ... if found ... - ESP_LOGD(LOG_TAG, "No Path handler found"); + std::ifstream ifStream; + std::string fileName = m_pHttpServer->getRootPath() + request.getPath(); + ESP_LOGD("HttpServerTask", "Opening file: %s", fileName.c_str()); + ifStream.open(fileName, std::ifstream::in | std::ifstream::binary); + + // If we failed to open the requested file, then it probably didn't exist so return a not found. + if (!ifStream.is_open()) { + HttpResponse response(&request); + response.setStatus(HttpResponse::HTTP_STATUS_NOT_FOUND, "Not Found"); + response.sendData(""); + return; + } + + // We now have an open file and want to push the content of that file through to the browser. HttpResponse response(&request); - response.setStatus(HttpResponse::HTTP_STATUS_NOT_FOUND, "Not Found"); - response.sendData(""); + response.setStatus(HttpResponse::HTTP_STATUS_OK, "OK"); + std::stringstream ss; + ss << ifStream.rdbuf(); + response.sendData(ss.str()); + ifStream.close(); } // processRequest /** - * Perform the task handling for server. + * @brief Perform the task handling for server. + * @param [in] data A reference to the HttpServer. */ void run(void* data) { m_pHttpServer = (HttpServer*)data; + + // Create a socket server and start it running. SockServ sockServ(m_pHttpServer->getPort()); sockServ.start(); - ESP_LOGD(LOG_TAG, "Listening on port %d", m_pHttpServer->getPort()); + ESP_LOGD("HttpServerTask", "Listening on port %d", m_pHttpServer->getPort()); + while(1) { - ESP_LOGD(LOG_TAG, "Waiting for new client"); + ESP_LOGD("HttpServerTask", "Waiting for new peer client"); Socket clientSocket = sockServ.waitForNewClient(); - ESP_LOGD(LOG_TAG, "Got a new client"); + ESP_LOGD("HttpServerTask", "HttpServer listening on port %d received a new client connection", m_pHttpServer->getPort()); + HttpRequest request(clientSocket); request.dump(); processRequest(request); if (!request.isWebsocket()) { - request.close_cpp(); + request.close(); } - } // while } // run }; // HttpServerTask @@ -132,6 +166,7 @@ uint16_t HttpServer::getPort() { return m_portNumber; } // getPort + /** * @brief Get the current root path. * @return The current root path. @@ -140,6 +175,7 @@ std::string HttpServer::getRootPath() { return m_rootPath; } // getRootPath + /** * @brief Set the root path for URL file mapping. * @@ -200,7 +236,7 @@ PathHandler::PathHandler(std::string method, std::string pathPattern, * @return True if the path matches. */ bool PathHandler::match(std::string method, std::string path) { - ESP_LOGD(LOG_TAG, "match: %s with %s", m_textPattern.c_str(), path.c_str()); + ESP_LOGD(LOG_TAG, "matching: %s with %s", m_textPattern.c_str(), path.c_str()); if (method != m_method) { return false; } @@ -214,6 +250,6 @@ bool PathHandler::match(std::string method, std::string path) { * @param [in] response An object representing the response. * @return N/A. */ -void PathHandler::invoke(HttpRequest* request, HttpResponse *response) { +void PathHandler::invokePathHandler(HttpRequest* request, HttpResponse *response) { m_requestHandler(request, response); -} // invoke +} // invokePathHandler diff --git a/cpp_utils/HttpServer.h b/cpp_utils/HttpServer.h index 6b173b57..4e4de640 100644 --- a/cpp_utils/HttpServer.h +++ b/cpp_utils/HttpServer.h @@ -23,7 +23,7 @@ class PathHandler { std::string pathPattern, void (*webServerRequestHandler)(HttpRequest* pHttpRequest, HttpResponse* pHttpResponse)); bool match(std::string method, std::string path); - void invoke(HttpRequest* request, HttpResponse* response); + void invokePathHandler(HttpRequest* request, HttpResponse* response); private: std::string m_method; std::regex m_pattern; @@ -38,21 +38,22 @@ class HttpServer { public: HttpServer(); virtual ~HttpServer(); - void addPathHandler(std::string method, + + void addPathHandler(std::string method, std::string pathExpr, void (*webServerRequestHandler)( HttpRequest* pHttpRequest, HttpResponse* pHttpResponse) ); - uint16_t getPort(); + uint16_t getPort(); std::string getRootPath(); - void setRootPath(std::string path); - void start(uint16_t portNumber); + void setRootPath(std::string path); + void start(uint16_t portNumber); private: friend class HttpServerTask; friend class WebSocket; uint16_t m_portNumber; std::vector m_pathHandlers; - std::string m_rootPath; + std::string m_rootPath; // Root path into the file system. }; // HttpServer #endif /* COMPONENTS_CPP_UTILS_HTTPSERVER_H_ */ diff --git a/cpp_utils/JSON.cpp b/cpp_utils/JSON.cpp index 4bebda0a..f7b296f8 100644 --- a/cpp_utils/JSON.cpp +++ b/cpp_utils/JSON.cpp @@ -258,6 +258,17 @@ std::string JsonObject::getString(std::string name) { } // getString +/** + * @brief Determine if the object has the specified item. + * @param [in] name The name of the property to check for presence. + * @return True if the object contains this property. + */ +bool JsonObject::hasItem(std::string name) { + return cJSON_GetObjectItem(m_node, name.c_str()) != nullptr; +} // hasItem + + + /** * @brief Set the named array property. * @param [in] name The name of the property to add. @@ -334,3 +345,4 @@ std::string JsonObject::toString() { free(data); return ret; } // toString + diff --git a/cpp_utils/JSON.h b/cpp_utils/JSON.h index 08915104..bca2d647 100644 --- a/cpp_utils/JSON.h +++ b/cpp_utils/JSON.h @@ -20,11 +20,11 @@ class JsonArray; class JSON { public: static JsonObject createObject(); - static JsonArray createArray(); - static void deleteObject(JsonObject jsonObject); - static void deleteArray(JsonArray jsonArray); + static JsonArray createArray(); + static void deleteObject(JsonObject jsonObject); + static void deleteArray(JsonArray jsonArray); static JsonObject parseObject(std::string text); - static JsonArray parseArray(std::string text); + static JsonArray parseArray(std::string text); }; // JSON @@ -33,17 +33,17 @@ class JSON { */ class JsonArray { public: - JsonArray(cJSON *node); - int getInt(int item); - JsonObject getObject(int item); + JsonArray(cJSON* node); + int getInt(int item); + JsonObject getObject(int item); std::string getString(int item); - bool getBoolean(int item); - double getDouble(int item); - void addBoolean(bool value); - void addDouble(double value); - void addInt(int value); - void addObject(JsonObject value); - void addString(std::string value); + bool getBoolean(int item); + double getDouble(int item); + void addBoolean(bool value); + void addDouble(double value); + void addInt(int value); + void addObject(JsonObject value); + void addString(std::string value); std::string toString(); std::size_t size(); /** @@ -58,23 +58,25 @@ class JsonArray { */ class JsonObject { public: - JsonObject(cJSON *node); - int getInt(std::string name); - JsonObject getObject(std::string name); + JsonObject(cJSON* node); + bool getBoolean(std::string name); + double getDouble(std::string name); + int getInt(std::string name); + JsonObject getObject(std::string name); std::string getString(std::string name); - bool getBoolean(std::string name); - double getDouble(std::string name); - void setArray(std::string name, JsonArray array); - void setBoolean(std::string name, bool value); - void setDouble(std::string name, double value); - void setInt(std::string name, int value); - void setObject(std::string name, JsonObject value); - void setString(std::string name, std::string value); + bool hasItem(std::string name); + void setArray(std::string name, JsonArray array); + void setBoolean(std::string name, bool value); + void setDouble(std::string name, double value); + void setInt(std::string name, int value); + void setObject(std::string name, JsonObject value); + void setString(std::string name, std::string value); std::string toString(); + /** * @brief The underlying cJSON node. */ - cJSON *m_node; + cJSON* m_node; }; // JsonObject diff --git a/cpp_utils/OV7670.cpp b/cpp_utils/OV7670.cpp index c7e395ce..9e4fa7a1 100644 --- a/cpp_utils/OV7670.cpp +++ b/cpp_utils/OV7670.cpp @@ -354,10 +354,12 @@ void OV7670::dump() { } } // dump +/* static void log(char *marker) { ESP_LOGD(LOG_TAG, "%s", marker); FreeRTOS::sleep(100); } +*/ /** * @brief Initialize the camera. diff --git a/cpp_utils/Socket.cpp b/cpp_utils/Socket.cpp index 0d81b696..a4b38a72 100644 --- a/cpp_utils/Socket.cpp +++ b/cpp_utils/Socket.cpp @@ -249,13 +249,15 @@ std::string Socket::readToDelim(std::string delim) { /** * @brief Receive data from the partner. - * + * Receive data from the socket partner. If exact = false, we read as much data as + * is available without blocking up to length. If exact = true, we will block until + * we have received exactly length bytes or there are no more bytes to read. * @param [in] data The buffer into which the received data will be stored. * @param [in] length The size of the buffer. - * @param [in] exact Read exactly this amount? + * @param [in] exact Read exactly this amount. * @return The length of the data received or -1 on an error. */ -int Socket::receive_cpp(uint8_t* data, size_t length, bool exact) { +size_t Socket::receive_cpp(uint8_t* data, size_t length, bool exact) { //ESP_LOGD(LOG_TAG, ">> receive_cpp: length: %d, exact: %d", length, exact); if (exact == false) { int rc = ::recv(m_sock, data, length, 0); @@ -272,6 +274,9 @@ int Socket::receive_cpp(uint8_t* data, size_t length, bool exact) { ESP_LOGE(LOG_TAG, "receive_cpp: %s", strerror(errno)); return 0; } + if (rc == 0) { + break; + } amountToRead -= rc; data+= rc; } @@ -304,7 +309,7 @@ int Socket::receiveFrom_cpp(uint8_t* data, size_t length, struct sockaddr *pAddr */ int Socket::send_cpp(const uint8_t* data, size_t length) const { ESP_LOGD(LOG_TAG, "send_cpp: Raw binary of length: %d", length); - GeneralUtils::hexDump(data, length); + //GeneralUtils::hexDump(data, length); int rc = ::send(m_sock, data, length, 0); if (rc == -1) { ESP_LOGE(LOG_TAG, "send: socket=%d, %s", m_sock, strerror(errno)); @@ -366,29 +371,37 @@ std::string Socket::toString() { * @param [in] dataLength The size of a record. * @param [in] bufferSize The size of the buffer we wish to allocate to hold data. */ -SocketInputRecordStream::SocketInputRecordStream( - Socket* socket, - size_t dataLength, - size_t bufferSize) { - m_pSocket = socket; // The socket we will be reading from +SocketInputRecordStreambuf::SocketInputRecordStreambuf( + Socket socket, + size_t dataLength, + size_t bufferSize) { + m_socket = socket; // The socket we will be reading from m_dataLength = dataLength; // The size of the record we wish to read. m_bufferSize = bufferSize; // The size of the buffer used to hold data m_sizeRead = 0; // The size of data read from the socket m_buffer = new char[bufferSize]; // Create the buffer used to hold the data read from the socket. setg(m_buffer, m_buffer, m_buffer); // Set the initial get buffer pointers to no data. -} // SocketInputRecordStream +} // SocketInputRecordStreambuf + +SocketInputRecordStreambuf::~SocketInputRecordStreambuf() { + delete[] m_buffer; +} // ~SocketInputRecordStreambuf /** * @brief Handle the request to read data from the stream but we need more data from the source. * */ -SocketInputRecordStream::int_type SocketInputRecordStream::underflow() { - return 0; +SocketInputRecordStreambuf::int_type SocketInputRecordStreambuf::underflow() { + if (m_sizeRead >= m_dataLength) { + return EOF; + } + int bytesRead = m_socket.receive_cpp((uint8_t*)m_buffer, m_bufferSize, true); + if (bytesRead == 0) { + return EOF; + } + m_sizeRead += bytesRead; + setg(m_buffer, m_buffer, m_buffer + bytesRead); + return traits_type::to_int_type(*gptr()); } // underflow - - -SocketInputRecordStream::~SocketInputRecordStream() { - delete[] m_buffer; -} // ~SocketInputRecordStream diff --git a/cpp_utils/Socket.h b/cpp_utils/Socket.h index f97ecd28..3219b770 100644 --- a/cpp_utils/Socket.h +++ b/cpp_utils/Socket.h @@ -42,7 +42,7 @@ class Socket { void listen_cpp(uint16_t port, bool isDatagram=false); bool operator<(const Socket& other) const; std::string readToDelim(std::string delim); - int receive_cpp(uint8_t* data, size_t length, bool exact=false); + size_t receive_cpp(uint8_t* data, size_t length, bool exact=false); int receiveFrom_cpp(uint8_t* data, size_t length, struct sockaddr* pAddr); int send_cpp(std::string value) const; int send_cpp(const uint8_t* data, size_t length) const; @@ -56,14 +56,14 @@ class Socket { int m_sock; }; -class SocketInputRecordStream : public std::streambuf { +class SocketInputRecordStreambuf : public std::streambuf { public: - SocketInputRecordStream(Socket* socket, size_t dataLength, size_t bufferSize=512); - ~SocketInputRecordStream(); + SocketInputRecordStreambuf(Socket socket, size_t dataLength, size_t bufferSize=512); + ~SocketInputRecordStreambuf(); int_type underflow(); private: char *m_buffer; - Socket* m_pSocket; + Socket m_socket; size_t m_dataLength; size_t m_bufferSize; size_t m_sizeRead; diff --git a/cpp_utils/WebSocket.cpp b/cpp_utils/WebSocket.cpp index 5c18039c..61192ef4 100644 --- a/cpp_utils/WebSocket.cpp +++ b/cpp_utils/WebSocket.cpp @@ -11,6 +11,12 @@ #include "GeneralUtils.h" #include +extern "C" { + extern uint16_t lwip_ntohs(uint16_t); + extern uint32_t lwip_ntohl(uint32_t); + extern uint16_t lwip_htons(uint16_t); + extern uint32_t lwip_htonl(uint32_t); +} static const char* LOG_TAG = "WebSocket"; // WebSocket op codes as found in a WebSocket frame. @@ -87,108 +93,106 @@ static void dumpFrame(Frame frame) { * incoming asynchronous events. We spawn a task to do this. This is the implementation of that task. */ class WebSocketReader: public Task { +public: + WebSocketReader() { + m_end = false; + } + void end() { + m_end = true; + } +private: + bool m_end; /** * @brief Loop over the web socket waiting for new input. * @param [in] data A pointer to an instance of the WebSocket. */ void run(void* data) { - WebSocket *pWebSocket = (WebSocket*) data; + WebSocket* pWebSocket = (WebSocket*) data; ESP_LOGD("WebSocketReader", "WebSocketReader Task started, socket: %s", pWebSocket->getSocket().toString().c_str()); - uint8_t buffer[1000]; Socket peerSocket = pWebSocket->getSocket(); + Frame frame; while(1) { + if (m_end) { + break; + } ESP_LOGD("WebSocketReader", "Waiting on socket data for socket %s", peerSocket.toString().c_str()); - int length = peerSocket.receive_cpp(buffer, sizeof(buffer)); - if (length == -1 || length == 0) { + int length = peerSocket.receive_cpp((uint8_t*)&frame, sizeof(frame), true); // Read exact + if (length != sizeof(frame)) { ESP_LOGD(LOG_TAG, "Socket read error"); pWebSocket->close_cpp(); return; } ESP_LOGD("WebSocketReader", "Received data from web socket. Length: %d", length); - GeneralUtils::hexDump(buffer, length); - dumpFrame(*(Frame *)buffer); + dumpFrame(frame); // The following section parses the WebSocket frame. - if (length > 0) { - Frame* pFrame = (Frame*)buffer; - uint32_t payloadLen = 0; - uint8_t* pMask = nullptr; - uint8_t* pData; - if (pFrame->len < 126) { - payloadLen = pFrame->len; - pMask = buffer + 2; - } else if (pFrame->len == 126) { - payloadLen = *(uint16_t*)(buffer+2); - pMask = buffer + 4; - } else if (pFrame->len == 127) { - ESP_LOGE(LOG_TAG, "Too much data!"); - return; - } - if (pFrame->mask == 1) { - pData = pMask + 4; - for (int i=0; igetHandler(); - switch(pFrame->opCode) { - case OPCODE_BINARY: { - if (pWebSocketHandler != nullptr) { - pWebSocketHandler->onMessage(payloadData); - } - break; + WebSocketHandler *pWebSocketHandler = pWebSocket->getHandler(); + switch(frame.opCode) { + case OPCODE_TEXT: + case OPCODE_BINARY: { + if (pWebSocketHandler != nullptr) { + WebSocketInputRecordStreambuf streambuf(pWebSocket->getSocket(), payloadLen, frame.mask==1?mask:nullptr); + pWebSocketHandler->onMessage(&streambuf); + //streambuf.discard(); } + break; + } - case OPCODE_CLOSE: { - pWebSocket->m_receivedClose = true; - if (pWebSocketHandler != nullptr) { - pWebSocketHandler->onClose(); - pWebSocket->close_cpp(); - } - break; + case OPCODE_CLOSE: { + pWebSocket->m_receivedClose = true; + if (pWebSocketHandler != nullptr) { + pWebSocketHandler->onClose(); + pWebSocket->close_cpp(); } + break; + } - case OPCODE_CONTINUE: { - break; - } + case OPCODE_CONTINUE: { + break; + } - case OPCODE_PING: { - break; - } + case OPCODE_PING: { + break; + } - case OPCODE_PONG: { - break; - } + case OPCODE_PONG: { + break; + } - case OPCODE_TEXT: { - if (pWebSocketHandler != nullptr) { - pWebSocketHandler->onMessage(payloadData); - } - break; - } - default: { - ESP_LOGD("WebSocketReader", "Unknown opcode: %d", pFrame->opCode); - break; - } - } // Switch opCode - } // Length of data > 0 + default: { + ESP_LOGD("WebSocketReader", "Unknown opcode: %d", frame.opCode); + break; + } + } // Switch opCode + } // While (1) + ESP_LOGD("WebSocketReader", "<< run"); } // run }; // WebSocketReader @@ -198,17 +202,22 @@ class WebSocketReader: public Task { * If no over-riding handler is provided for the "close" event, this method is called. */ void WebSocketHandler::onClose() { - ESP_LOGD(LOG_TAG, ">> WebSocketHandler:onClose()"); + ESP_LOGD("WebSocketHandler", ">> WebSocketHandler:onClose()"); } // onClose /** * @brief The default onData handler. * If no over-riding handler is provided for the "message" event, this method is called. + * A particularly useful pattern for using onMessage is: + * ``` + * std::stringstream buffer; + * buffer << pWebSocketInputRecordStreambuf; + * ``` + * This will read the whole message into the string stream. */ -void WebSocketHandler::onMessage(std::string data) { - ESP_LOGD(LOG_TAG, ">> WebSocketHandler:onMessage(), length: %d", data.length()); - GeneralUtils::hexDump((uint8_t*)data.data(), data.length()); +void WebSocketHandler::onMessage(WebSocketInputRecordStreambuf* pWebSocketInputRecordStreambuf) { + ESP_LOGD("WebSocketHandler", ">> onMessage") } // onData @@ -217,19 +226,22 @@ void WebSocketHandler::onMessage(std::string data) { * If no over-riding handler is provided for the "error" event, this method is called. */ void WebSocketHandler::onError(std::string error) { - ESP_LOGD(LOG_TAG, ">> WebSocketHandler:onError()"); + ESP_LOGD("WebSocketHandler", ">> WebSocketHandler:onError()"); } // onError WebSocket::WebSocket(Socket socket) { - m_receivedClose = false; - m_sentClose = false; - m_socket = socket; - m_pWebSockerReader = new WebSocketReader(); + m_receivedClose = false; + m_sentClose = false; + m_socket = socket; + m_pWebSockerReader = new WebSocketReader(); + m_pWebSocketHandler = nullptr; } // WebSocket WebSocket::~WebSocket() { + m_pWebSockerReader->stop(); + delete m_pWebSockerReader; } // ~WebSocket @@ -240,6 +252,7 @@ void WebSocket::close_cpp(uint16_t status, std::string message) { ESP_LOGD(LOG_TAG, ">> close_cpp(): status: %d, message: %s", status, message.c_str()); if (m_sentClose) { m_socket.close_cpp(); + m_pWebSockerReader->end(); return; } m_sentClose = true; @@ -261,6 +274,7 @@ void WebSocket::close_cpp(uint16_t status, std::string message) { } if (m_receivedClose || rc == 0 || rc == -1) { m_socket.close_cpp(); + m_pWebSockerReader->end(); } } // close_cpp @@ -271,7 +285,7 @@ void WebSocket::close_cpp(uint16_t status, std::string message) { * event received over the network needs to be handled by user code. */ WebSocketHandler* WebSocket::getHandler() { - return &m_webSocketHandler; + return m_pWebSocketHandler; } // getHandler @@ -320,8 +334,8 @@ void WebSocket::send_cpp(std::string data, uint8_t sendType) { * events. An instance of WebSocketHandler is passed in. * */ -void WebSocket::setHandler(WebSocketHandler handler) { - m_webSocketHandler = handler; +void WebSocket::setHandler(WebSocketHandler *pHandler) { + m_pWebSocketHandler = pHandler; } // setHandler @@ -334,3 +348,111 @@ void WebSocket::startReader() { ESP_LOGD(LOG_TAG, ">> startReader: Socket: %s", m_socket.toString().c_str()); m_pWebSockerReader->start(this); } // startReader + + +/** + * @brief Create a Web Socket input record streambuf + * @param [in] socket The socket we will be reading from. + * @param [in] dataLength The size of a record. + * @param [in] bufferSize The size of the buffer we wish to allocate to hold data. + */ +WebSocketInputRecordStreambuf::WebSocketInputRecordStreambuf( + Socket socket, + size_t dataLength, + uint8_t *pMask, + size_t bufferSize) { + m_socket = socket; // The socket we will be reading from + m_dataLength = dataLength; // The size of the record we wish to read. + m_pMask = pMask; + m_bufferSize = bufferSize; // The size of the buffer used to hold data + m_sizeRead = 0; // The size of data read from the socket + m_buffer = new char[bufferSize]; // Create the buffer used to hold the data read from the socket. + + setg(m_buffer, m_buffer, m_buffer); // Set the initial get buffer pointers to no data. +} // WebSocketInputRecordStreambuf + + +/** + * @brief Destructor + */ +WebSocketInputRecordStreambuf::~WebSocketInputRecordStreambuf() { + delete[] m_buffer; + discard(); +} // ~WebSocketInputRecordStreambuf + + +/** + * @brief Discard data for the record that has not yet been read. + * + * We are working on a logical fixed length record in a socket stream. This means that we know in advance + * how big the record should be. If we have read some data from the stream and no longer wish to consume + * any further, we have to discard the remaining bytes in the stream before we can get to process the + * next record. This function discards the remainder of the data. + * + * For example, if our record size is 1000 bytes and we have read 700 bytes and determine that we no + * longer need to continue, we can't just stop. There are still 300 bytes in the socket stream that + * need to be consumed/discarded before we can move on to the next record. + */ +void WebSocketInputRecordStreambuf::discard() { + uint8_t byte; + ESP_LOGD("WebSocketInputRecordStreambuf", ">> discard: Discarding %d bytes", m_dataLength - m_sizeRead); + while(m_sizeRead < m_dataLength) { + m_socket.receive_cpp(&byte, 1); + m_sizeRead++; + } + ESP_LOGD("WebSocketInputRecordStreambuf", "<< discard"); +} // discard + + +/** + * @brief Get the size of the expected record. + * @return The size of the expected record. + */ +size_t WebSocketInputRecordStreambuf::getRecordSize() { + return m_dataLength; +} // getRecordSize + + +/** + * @brief Handle the request to read data from the stream but we need more data from the source. + * + */ +WebSocketInputRecordStreambuf::int_type WebSocketInputRecordStreambuf::underflow() { + ESP_LOGD("WebSocketInputRecordStreambuf", ">> underflow"); + if (m_sizeRead >= getRecordSize()) { + ESP_LOGD("WebSocketInputRecordStreambuf", "<< underflow: Already read maximum"); + return EOF; + } + + // We wish to refill the buffer. We want to read data from the socket. We want to read either + // the size of the buffer to fill it or the maximum number of bytes remaining to be read. + // We will choose which ever is smaller as the number of bytes to read into the buffer. + int remainingBytes = getRecordSize()-m_sizeRead; + size_t sizeToRead; + if (remainingBytes < m_bufferSize) { + sizeToRead = remainingBytes; + } else { + sizeToRead = m_bufferSize; + } + + ESP_LOGD("WebSocketInputRecordStreambuf", "- getting next buffer of data; size request: %d", sizeToRead); + int bytesRead = m_socket.receive_cpp((uint8_t*)m_buffer, sizeToRead, true); + if (bytesRead == 0) { + ESP_LOGD("WebSocketInputRecordStreambuf", "<< underflow: Read 0 bytes"); + return EOF; + } + + if (m_pMask != nullptr) { + for (int i=0; i #include "Socket.h" +class WebSocketReader; + +// +-------------------------------+ +// | WebSocketInputRecordStreambuf | +// +-------------------------------+ +class WebSocketInputRecordStreambuf : public std::streambuf { +public: + WebSocketInputRecordStreambuf( + Socket socket, + size_t dataLength, + uint8_t* pMask=nullptr, + size_t bufferSize=2048); + ~WebSocketInputRecordStreambuf(); + int_type underflow(); + void discard(); + size_t getRecordSize(); +private: + char* m_buffer; + Socket m_socket; + size_t m_dataLength; + size_t m_bufferSize; + size_t m_sizeRead; + uint8_t* m_pMask; +}; + +// +------------------+ +// | WebSocketHandler | +// +------------------+ class WebSocketHandler { public: + virtual ~WebSocketHandler(); virtual void onClose(); - virtual void onMessage(std::string data); + virtual void onMessage(WebSocketInputRecordStreambuf *pWebSocketInputRecordStreambuf); virtual void onError(std::string error); }; -class WebSocketReader; + +// +-----------+ +// | WebSocket | +// +-----------+ class WebSocket { private: friend class WebSocketReader; @@ -26,10 +58,9 @@ class WebSocket { bool m_receivedClose; // True when we have received a close request. bool m_sentClose; // True when we have sent a close request. Socket m_socket; // Partner socket. - WebSocketHandler m_webSocketHandler; + WebSocketHandler *m_pWebSocketHandler; WebSocketReader *m_pWebSockerReader; - public: static const uint16_t CLOSE_NORMAL_CLOSURE = 1000; static const uint16_t CLOSE_GOING_AWAY = 1001; @@ -51,11 +82,12 @@ class WebSocket { WebSocket(Socket socket); virtual ~WebSocket(); - void close_cpp(uint16_t status=CLOSE_NORMAL_CLOSURE, std::string message = ""); + + void close_cpp(uint16_t status=CLOSE_NORMAL_CLOSURE, std::string message = ""); WebSocketHandler* getHandler(); - Socket getSocket(); - void send_cpp(std::string data, uint8_t sendType = SEND_TYPE_BINARY); - void setHandler(WebSocketHandler handler); -}; + Socket getSocket(); + void send_cpp(std::string data, uint8_t sendType = SEND_TYPE_BINARY); + void setHandler(WebSocketHandler *handler); +}; // WebSocket #endif /* COMPONENTS_WEBSOCKET_H_ */ diff --git a/cpp_utils/WebSocketFileTransfer.cpp b/cpp_utils/WebSocketFileTransfer.cpp new file mode 100644 index 00000000..2ada459b --- /dev/null +++ b/cpp_utils/WebSocketFileTransfer.cpp @@ -0,0 +1,112 @@ +/* + * WebSocketFileTransfer.cpp + * + * Created on: Sep 9, 2017 + * Author: kolban + */ +#include +#include +#include +#include +#include "JSON.h" +static const char* LOG_TAG = "WebSocketFileTransfer"; + +#include "WebSocketFileTransfer.h" + +#undef close + +WebSocketFileTransfer::WebSocketFileTransfer() { + m_fileName = ""; + m_length = 0; + m_pWebSocket = nullptr; +} + +WebSocketFileTransfer::~WebSocketFileTransfer() { + // TODO Auto-generated destructor stub +} + + +// Hide the class in an un-named namespace +namespace { + +class FileTransferWebSocketHandler : public WebSocketHandler { +public: + FileTransferWebSocketHandler() { + m_fileName = ""; + m_fileLength = 0; + m_sizeReceived = 0; + m_active = false; + } + +private: + std::string m_fileName; // The name of the file we are receiving. + uint32_t m_fileLength; // We may optionally receive a file length. + uint32_t m_sizeReceived; // The size of the data actually received so far. + bool m_active; // Are we actively processing a file. + std::ofstream m_ofStream; + + virtual void onMessage(WebSocketInputRecordStreambuf* pWebSocketInputRecordStreambuf) { + ESP_LOGD("FileTransferWebSocketHandler", ">> onMessage"); + if (!m_active) { + std::stringstream buffer; + buffer << pWebSocketInputRecordStreambuf; + ESP_LOGD("FileTransferWebSocketHandler", "Data read: %s", buffer.str().c_str()); + JsonObject jo = JSON::parseObject(buffer.str()); + m_fileName = jo.getString("name"); + if (jo.hasItem("length")) { + m_fileLength = jo.getInt("length"); + } + std::string fileName = "/spiflash/" + m_fileName; + if (m_fileName.length() > 0 && m_fileName.substr(m_fileName.size()-1)=="/") { + ESP_LOGD("FileTransferWebSocketHandler", "Is a directory!!"); + fileName = fileName.substr(0, fileName.size()-1); + struct stat statbuf; + if (stat(fileName.c_str(), &statbuf) == 0) { + if (S_ISREG(statbuf.st_mode)) { + ESP_LOGE("FileTransferWebSocketHandler", "File already exists and is a file not a directory!"); + } + } + if (mkdir(fileName.c_str(), 0) != 0) { + ESP_LOGE("FileTransferWebSocketHandler", "Failed to make directory \"%s\", error: %s", fileName.c_str(), strerror(errno)); + } + } else { + m_ofStream.open(fileName, std::ofstream::out | std::ofstream::binary | std::ofstream::trunc); + if (!m_ofStream.is_open()) { + ESP_LOGE("FileTransferWebSocketHandler", "Failed to open file %s", m_fileName.c_str()); + } + } + m_active = true; + ESP_LOGD("FileTransferWebSocketHandler", "Filename: %s, length: %d", fileName.c_str(), m_fileLength); + } else { + // We are about to receive a chunk of file + m_ofStream << pWebSocketInputRecordStreambuf; + /* + std::stringstream bufferStream; + bufferStream << pWebSocketInputRecordStreambuf; + m_sizeReceived += bufferStream.str().length(); + ESP_LOGD("FileTransferWebSocketHandler", "Received %d bytes of file data", bufferStream.str().length()); + if (m_fileLength > 0 && m_sizeReceived > m_fileLength) { + ESP_LOGD("FileTransferWebSocketHandler", + "ERROR: Received a total of %d bytes when only %d bytes expected!", m_sizeReceived, m_fileLength); + } + */ + } + } // onMessage + + + virtual void onClose() { + ESP_LOGD("FileTransferWebSocketHandler", ">> onClose: fileName: %s, sizeReceived: %d", m_fileName.c_str(), m_sizeReceived); + if (m_fileLength > 0 && m_sizeReceived != m_fileLength) { + ESP_LOGD("FileTransferWebSocketHandler", + "ERROR: Transfer finished but we received total of %d bytes and expected %d bytes!", m_sizeReceived, m_fileLength); + } + m_ofStream.close(); + } // onClose +}; // FileTransferWebSocketHandler + +} // End un-named namespace + +void WebSocketFileTransfer::start(WebSocket* pWebSocket) { + ESP_LOGD(LOG_TAG, ">> start"); + pWebSocket->setHandler(new FileTransferWebSocketHandler()); +} // start diff --git a/cpp_utils/WebSocketFileTransfer.h b/cpp_utils/WebSocketFileTransfer.h new file mode 100644 index 00000000..d3db986b --- /dev/null +++ b/cpp_utils/WebSocketFileTransfer.h @@ -0,0 +1,24 @@ +/* + * WebSocketFileTransfer.h + * + * Created on: Sep 9, 2017 + * Author: kolban + */ + +#ifndef COMPONENTS_CPP_UTILS_WEBSOCKETFILETRANSFER_H_ +#define COMPONENTS_CPP_UTILS_WEBSOCKETFILETRANSFER_H_ +#include +#include "WebSocket.h" + +class WebSocketFileTransfer { +private: + std::string m_fileName; + size_t m_length; + WebSocket* m_pWebSocket; +public: + WebSocketFileTransfer(); + virtual ~WebSocketFileTransfer(); + void start(WebSocket *pWebSocket); +}; + +#endif /* COMPONENTS_CPP_UTILS_WEBSOCKETFILETRANSFER_H_ */ diff --git a/filesystems/sendZip/ws_send_zip.js b/filesystems/sendZip/ws_send_zip.js new file mode 100644 index 00000000..67279218 --- /dev/null +++ b/filesystems/sendZip/ws_send_zip.js @@ -0,0 +1,63 @@ +// https://www.npmjs.com/package/adm-zip +// https://nodejs.org/api/stream.html +// https://nodejs.org/api/net.html +//API docs ...https://github.com/websockets/ws/blob/master/doc/ws.md + +const WebSocket = require("ws"); +var AdmZip = require("adm-zip"); + +function *processZip(zipFileName) { + var zip = new AdmZip("./foo.zip"); + var zipEntries = zip.getEntries(); + for (var i=0; i, // The file name to send + * "data": , // The content of the file + * } + * ``` + * @returns N/A + */ +function sendZipEntry(entry) { + const ws = new WebSocket("ws://192.168.1.99:9080/upload"); + ws.on("open", ()=>{ + console.log("Sending file: " + entry.name); + var file = { + "name": entry.name + //"length": 100 + } + ws.send(JSON.stringify(file)); + ws.send(entry.data); + ws.close(); + }); + + ws.on("error", (error)=>{ + console.log("Error: %O", error); + }); + + ws.on("close", (code)=>{ + console.log("WS closed"); + var nextEntry = it.next(); + if (!nextEntry.done) { + sendZipEntry(nextEntry.value); + } + }); +} // sendZipEntry + + +var it = processZip("./foo.zip"); +var nextZip = it.next(); +if (!nextZip.done) { + sendZipEntry(nextZip.value); +}