diff --git a/communication/inc/coap_api.h b/communication/inc/coap_api.h new file mode 100644 index 0000000000..0759832375 --- /dev/null +++ b/communication/inc/coap_api.h @@ -0,0 +1,522 @@ +/* + * Copyright (c) 2023 Particle Industries, Inc. All rights reserved. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation, either + * version 3 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, see . + */ + +#pragma once + +#include + +/** + * Maximum size of payload data that can be sent or received without splitting the request or + * response message into multiple CoAP messages. + */ +#define COAP_BLOCK_SIZE 1024 + +/** + * Invalid request ID. + */ +#define COAP_INVALID_REQUEST_ID 0 + +#define COAP_CODE(_class, _detail) \ + (((_class & 0x07) << 5) | (_detail & 0x1f)) + +#define COAP_CODE_CLASS(_code) \ + ((_code >> 5) & 0x07) + +#define COAP_CODE_DETAIL(_code) \ + (_code & 0x1f) + +/** + * Message. + */ +typedef struct coap_message coap_message; + +/** + * Message option. + */ +typedef struct coap_option coap_option; + +/** + * Callback invoked when the status of the CoAP connection changes. + * + * @param error 0 if the status changed normally, otherwise an error code defined by the + * `system_error_t` enum. + * @param status Current status as defined by the `coap_connection_status` enum. + * @param arg User argument. + * @return 0 on success, otherwise an error code defined by the `system_error_t` enum. + */ +typedef int (*coap_connection_callback)(int error, int status, void* arg); + +/** + * Callback invoked when a request message is received. + * + * The message instance must be destroyed by calling `coap_destroy_message()` when it's no longer + * needed. + * + * @param msg Request message. + * @param uri Request URI. + * @param method Method code as defined by the `coap_method` enum. + * @param req_id Request ID. + * @param arg User argument. + * @return 0 on success, otherwise an error code defined by the `system_error_t` enum. + */ +typedef int (*coap_request_callback)(coap_message* msg, const char* uri, int method, int req_id, void* arg); + +/** + * Callback invoked when a response message is received. + * + * The message instance must be destroyed by calling `coap_destroy_message()` when it's no longer + * needed. + * + * @param msg Response message. + * @param status Response code as defined by the `coap_status` enum. + * @param req_id ID of the request for which this response is being received. + * @param arg User argument. + * @return 0 on success, otherwise an error code defined by the `system_error_t` enum. + */ +typedef int (*coap_response_callback)(coap_message* msg, int status, int req_id, void* arg); + +/** + * Callback invoked when a block of a request or response message has been sent or received. + * + * @param msg Request or response message. + * @param req_id ID of the request that started the message exchange. + * @param arg User argument. + * @return 0 on success, otherwise an error code defined by the `system_error_t` enum. + */ +typedef int (*coap_block_callback)(coap_message* msg, int req_id, void* arg); + +/** + * Callback invoked when a request or response message is acknowledged. + * + * @param req_id ID of the request that started the message exchange. + * @param arg User argument. + * @return 0 on success, otherwise an error code defined by the `system_error_t` enum. + */ +typedef int (*coap_ack_callback)(int req_id, void* arg); + +/** + * Callback invoked when an error occurs while sending a request or response message. + * + * @param error Error code as defined by the `system_error_t` enum. + * @param req_id ID of the request that started the failed message exchange. + * @param arg User argument. + */ +typedef void (*coap_error_callback)(int error, int req_id, void* arg); + +/** + * Method code. + */ +typedef enum coap_method { + COAP_METHOD_GET = COAP_CODE(0, 1), ///< GET method. + COAP_METHOD_POST = COAP_CODE(0, 2), ///< POST method. + COAP_METHOD_PUT = COAP_CODE(0, 3), ///< PUT method. + COAP_METHOD_DELETE = COAP_CODE(0, 4), ///< DELETE method. +} coap_method; + +/** + * Response code. + */ +typedef enum coap_status { + // Success 2.xx + COAP_STATUS_CREATED = COAP_CODE(2, 1), ///< 2.01 Created. + COAP_STATUS_DELETED = COAP_CODE(2, 2), ///< 2.02 Deleted. + COAP_STATUS_VALID = COAP_CODE(2, 3), ///< 2.03 Valid. + COAP_STATUS_CHANGED = COAP_CODE(2, 4), ///< 2.04 Changed. + COAP_STATUS_CONTENT = COAP_CODE(2, 5), ///< 2.05 Content. + // Client Error 4.xx + COAP_STATUS_BAD_REQUEST = COAP_CODE(4, 0), ///< 4.00 Bad Request. + COAP_STATUS_UNAUTHORIZED = COAP_CODE(4, 1), ///< 4.01 Unauthorized. + COAP_STATUS_BAD_OPTION = COAP_CODE(4, 2), ///< 4.02 Bad Option. + COAP_STATUS_FORBIDDEN = COAP_CODE(4, 3), ///< 4.03 Forbidden. + COAP_STATUS_NOT_FOUND = COAP_CODE(4, 4), ///< 4.04 Not Found. + COAP_STATUS_METHOD_NOT_ALLOWED = COAP_CODE(4, 5), ///< 4.05 Method Not Allowed. + COAP_STATUS_NOT_ACCEPTABLE = COAP_CODE(4, 6), ///< 4.06 Not Acceptable. + COAP_STATUS_PRECONDITION_FAILED = COAP_CODE(4, 12), ///< 4.12 Precondition Failed. + COAP_STATUS_REQUEST_ENTITY_TOO_LARGE = COAP_CODE(4, 13), ///< 4.13 Request Entity Too Large. + COAP_STATUS_UNSUPPORTED_CONTENT_FORMAT = COAP_CODE(4, 15), ///< 4.15 Unsupported Content-Format. + // Server Error 5.xx + COAP_STATUS_INTERNAL_SERVER_ERROR = COAP_CODE(5, 0), ///< 5.00 Internal Server Error. + COAP_STATUS_NOT_IMPLEMENTED = COAP_CODE(5, 1), ///< 5.01 Not Implemented. + COAP_STATUS_BAD_GATEWAY = COAP_CODE(5, 2), ///< 5.02 Bad Gateway. + COAP_STATUS_SERVICE_UNAVAILABLE = COAP_CODE(5, 3), ///< 5.03 Service Unavailable. + COAP_STATUS_GATEWAY_TIMEOUT = COAP_CODE(5, 4), ///< 5.04 Gateway Timeout. + COAP_STATUS_PROXYING_NOT_SUPPORTED = COAP_CODE(5, 5) ///< 5.05 Proxying Not Supported. +} coap_status; + +/** + * Option number. + */ +typedef enum coap_option_number { + COAP_OPTION_IF_MATCH = 1, ///< If-Match. + COAP_OPTION_URI_HOST = 3, ///< Uri-Host. + COAP_OPTION_ETAG = 4, ///< ETag. + COAP_OPTION_IF_NONE_MATCH = 5, ///< If-None-Match. + COAP_OPTION_URI_PORT = 7, ///< Uri-Port. + COAP_OPTION_LOCATION_PATH = 8, ///< Location-Path. + COAP_OPTION_URI_PATH = 11, ///< Uri-Path. + COAP_OPTION_CONTENT_FORMAT = 12, ///< Content-Format. + COAP_OPTION_MAX_AGE = 14, ///< Max-Age. + COAP_OPTION_URI_QUERY = 15, ///< Uri-Query. + COAP_OPTION_ACCEPT = 17, ///< Accept. + COAP_OPTION_LOCATION_QUERY = 20, ///< Location-Query. + COAP_OPTION_PROXY_URI = 35, ///< Proxy-Uri. + COAP_OPTION_PROXY_SCHEME = 39, ///< Proxy-Scheme. + COAP_OPTION_SIZE1 = 60 ///< Size1. +} coap_option_number; + +/** + * Content format. + * + * https://www.iana.org/assignments/core-parameters/core-parameters.xhtml#content-formats + */ +typedef enum coap_format { + COAP_FORMAT_TEXT_PLAIN = 0, // text/plain; charset=utf-8 + COAP_FORMAT_OCTET_STREAM = 42, // application/octet-stream + COAP_FORMAT_JSON = 50, // application/json + COAP_FORMAT_CBOR = 60 // application/cbor +} coap_format; + +/** + * Connection status. + */ +typedef enum coap_connection_status { + COAP_CONNECTION_CLOSED = 0, ///< Connection is closed. + COAP_CONNECTION_OPEN = 1 ///< Connection is open. +} coap_connection_status; + +/** + * Result code. + */ +typedef enum coap_result { + COAP_RESULT_WAIT_BLOCK = 1 ///< Waiting for the next block of the message to be sent or received. +} coap_result; + +#ifdef __cplusplus +extern "C" { +#endif + +/** + * Register a connection handler. + * + * @param cb Handler callback. + * @param arg User argument to pass to the callback. + * @param reserved Reserved argument. Must be set to `NULL`. + * @return 0 on success, otherwise an error code defined by the `system_error_t` enum. + */ +int coap_add_connection_handler(coap_connection_callback cb, void* arg, void* reserved); + +/** + * Unregister a connection handler. + * + * @param cb Handler callback. + * @param reserved Reserved argument. Must be set to `NULL`. + */ +void coap_remove_connection_handler(coap_connection_callback cb, void* reserved); + +/** + * Register a handler for incoming requests. + * + * If a handler is already registered for the given combination of URI prefix and method code, it + * will be replaced. + * + * @param uri URI prefix. + * @param method Method code as defined by the `coap_method` enum. + * @param flags Reserved argument. Must be set to 0. + * @param cb Handler callback. + * @param arg User argument to pass to the callback. + * @param reserved Reserved argument. Must be set to `NULL`. + * @return 0 on success, otherwise an error code defined by the `system_error_t` enum. + */ +int coap_add_request_handler(const char* uri, int method, int flags, coap_request_callback cb, void* arg, void* reserved); + +/** + * Unregister a handler for incoming requests. + * + * @param uri URI prefix. + * @param method Method code as defined by the `coap_method` enum. + * @param reserved Reserved argument. Must be set to `NULL`. + */ +void coap_remove_request_handler(const char* uri, int method, void* reserved); + +/** + * Begin sending a request message. + * + * @param[out] msg Request message. + * @param uri Request URI. + * @param method Method code as defined by the `coap_method` enum. + * @param timeout Request timeout in milliseconds. If 0, the default timeout is used. + * @param flags Reserved argument. Must be set to 0. + * @param reserved Reserved argument. Must be set to `NULL`. + * @return ID of the request on success, otherwise an error code defined by the `system_error_t` enum. + */ +int coap_begin_request(coap_message** msg, const char* uri, int method, int timeout, int flags, void* reserved); + +/** + * Finish sending a request message. + * + * If the function call succeeds, the message instance must not be used again with any of the + * functions of this API. + * + * @param msg Request message. + * @param resp_cb Callback to invoke when a response for this request is received. Can be `NULL`. + * @param ack_cb Callback to invoke when the request is acknowledged. Can be `NULL`. + * @param error_cb Callback to invoke when an error occurs while sending the request. Can be `NULL`. + * @param arg User argument to pass to the callbacks. + * @param reserved Reserved argument. Must be set to `NULL`. + * @return 0 on success, otherwise an error code defined by the `system_error_t` enum. + */ +int coap_end_request(coap_message* msg, coap_response_callback resp_cb, coap_ack_callback ack_cb, + coap_error_callback error_cb, void* arg, void* reserved); + +/** + * Begin sending a response message. + * + * @param[out] msg Response message. + * @param status Response code as defined by the `coap_status` enum. + * @param req_id ID of the request which this response is meant for. + * @param flags Reserved argument. Must be set to 0. + * @param reserved Reserved argument. Must be set to `NULL`. + * @return 0 on success, otherwise an error code defined by the `system_error_t` enum. + */ +int coap_begin_response(coap_message** msg, int status, int req_id, int flags, void* reserved); + +/** + * Finish sending a response message. + * + * If the function call succeeds, the message instance must not be used again with any of the + * functions of this API. + * + * @param msg Response message. + * @param ack_cb Callback to invoke when the response is acknowledged. Can be `NULL`. + * @param error_cb Callback to invoke when an error occurs while sending the response. Can be `NULL`. + * @param arg User argument to pass to the callbacks. + * @param reserved Reserved argument. Must be set to `NULL`. + * @return 0 on success, otherwise an error code defined by the `system_error_t` enum. + */ +int coap_end_response(coap_message* msg, coap_ack_callback ack_cb, coap_error_callback error_cb, + void* arg, void* reserved); + +/** + * Destroy a message. + * + * Destroying an outgoing request or response message before `coap_end_request()` or + * `coap_end_response()` is called cancels the message exchange. + * + * @param msg Request or response message. + * @param reserved Reserved argument. Must be set to `NULL`. + */ +void coap_destroy_message(coap_message* msg, void* reserved); + +/** + * Cancel an ongoing request. + * + * Cancelling a request prevents any callbacks associated with the respective message exchange from + * being invoked by the API. + * + * If the caller still owns a message instance associated with the given exchange, it needs to be + * destroyed via `coap_destroy_message()`. + * + * @param req_id ID of the request that started the message exchange. + * @param reserved Reserved argument. Must be set to `NULL`. + */ +void coap_cancel_request(int req_id, void* reserved); + +/** + * Write the payload data of a message. + * + * If the data can't be sent in one message block, the function will return `COAP_RESULT_WAIT_BLOCK`. + * When that happens, the caller must stop writing the payload data until the `block_cb` callback is + * invoked by the system to notify the caller that the next block of the message can be sent. + * + * All message options must be set prior to writing the payload data. + * + * @param msg Request or response message. + * @param data Input buffer. + * @param[in,out] size **in:** Number of bytes to write. + * **out:** Number of bytes written. + * @param block_cb Callback to invoke when the next block of the message can be sent. Can be `NULL`. + * @param error_cb Callback to invoke when an error occurs while sending the current block of the + * message. Can be `NULL`. + * @param arg User argument to pass to the callbacks. + * @param reserved Reserved argument. Must be set to `NULL`. + * @return 0 or `COAP_RESULT_WAIT_BLOCK` on success, otherwise an error code defined by the + * `system_error_t` enum. + */ +int coap_write_payload(coap_message* msg, const char* data, size_t* size, coap_block_callback block_cb, + coap_error_callback error_cb, void* arg, void* reserved); + +/** + * Read the payload data of a message. + * + * If the end of the current message block is reached and more blocks are expected to be received for + * this message, the function will return `COAP_RESULT_WAIT_BLOCK`. The `block_cb` callback will be + * invoked by the system to notify the caller that the next message block is available for reading. + * + * @param msg Request or response message. + * @param[out] data Output buffer. Can be `NULL`. + * @param[in,out] size **in:** Number of bytes to read. + * **out:** Number of bytes read. + * @param block_cb Callback to invoke when the next block of the message is received. Can be `NULL`. + * @param error_cb Callback to invoke when an error occurs while receiving the next block of the + * message. Can be `NULL`. + * @param arg User argument to pass to the callbacks. + * @param reserved Reserved argument. Must be set to `NULL`. + * @return 0 or `COAP_RESULT_WAIT_BLOCK` on success, otherwise an error code defined by the + * `system_error_t` enum. + */ +int coap_read_payload(coap_message* msg, char* data, size_t *size, coap_block_callback block_cb, + coap_error_callback error_cb, void* arg, void* reserved); + +/** + * Read the payload data of the current message block without changing the reading position in it. + * + * @param msg Request or response message. + * @param[out] data Output buffer. Can be `NULL`. + * @param size Number of bytes to read. + * @param reserved Reserved argument. Must be set to `NULL`. + * @return Number of bytes read or an error code defined by the `system_error_t` enum. + */ +int coap_peek_payload(coap_message* msg, char* data, size_t size, void* reserved); + +/** + * Get a message option. + * + * @param[out] opt Message option. If the option with the given number cannot be found, the argument + * is set to `NULL`. + * @param num Option number. + * @param msg Request or response message. + * @param reserved Reserved argument. Must be set to `NULL`. + * @return 0 on success, otherwise an error code defined by the `system_error_t` enum. + */ +int coap_get_option(coap_option** opt, int num, coap_message* msg, void* reserved); + +/** + * Get the next message option. + * + * @param[in,out] opt **in:** Current option. If `NULL`, the first option of the message is returned. + * **out:** Next option. If `NULL`, the option provided is the last option of the message. + * @param[out] num Option number. + * @param msg Request or response message. + * @param reserved Reserved argument. Must be set to `NULL`. + * @return 0 on success, otherwise an error code defined by the `system_error_t` enum. + */ +int coap_get_next_option(coap_option** opt, int* num, coap_message* msg, void* reserved); + +/** + * Get the value of an `uint` option. + * + * @param opt Message option. + * @param[out] val Option value. + * @param reserved Reserved argument. Must be set to `NULL`. + * @return 0 on success, otherwise an error code defined by the `system_error_t` enum. + */ +int coap_get_uint_option_value(const coap_option* opt, unsigned* val, void* reserved); + +/** + * Get the value of an `uint` option as a 64-bit integer. + * + * @param opt Message option. + * @param[out] val Option value. + * @param reserved Reserved argument. Must be set to `NULL`. + * @return 0 on success, otherwise an error code defined by the `system_error_t` enum. + */ +int coap_get_uint64_option_value(const coap_option* opt, uint64_t* val, void* reserved); + +/** + * Get the value of a string option. + * + * The output is null-terminated unless the size of the output buffer is 0. + * + * @param opt Message option. + * @param data Output buffer. Can be `NULL`. + * @param size Size of the buffer. + * @param reserved Reserved argument. Must be set to `NULL`. + * @return On success, the actual size of the option value not including the terminating null (can + * be greater than `size`). Otherwise, an error code defined by the `system_error_t` enum. + */ +int coap_get_string_option_value(const coap_option* opt, char* data, size_t size, void* reserved); + +/** + * Get the value of an opaque option. + * + * @param opt Message option. + * @param data Output buffer. Can be `NULL`. + * @param size Size of the buffer. + * @param reserved Reserved argument. Must be set to `NULL`. + * @return On success, the actual size of the option value (can be greater than `size`). Otherwise, + * an error code defined by the `system_error_t` enum. + */ +int coap_get_opaque_option_value(const coap_option* opt, char* data, size_t size, void* reserved); + +/** + * Add an empty option to a message. + * + * @param msg Request of response message. + * @param num Option number. + * @param reserved Reserved argument. Must be set to `NULL`. + * @return 0 on success, otherwise an error code defined by the `system_error_t` enum. + */ +int coap_add_empty_option(coap_message* msg, int num, void* reserved); + +/** + * Add a `uint` option to a message. + * + * @param msg Request of response message. + * @param num Option number. + * @param val Option value. + * @param reserved Reserved argument. Must be set to `NULL`. + * @return 0 on success, otherwise an error code defined by the `system_error_t` enum. + */ +int coap_add_uint_option(coap_message* msg, int num, unsigned val, void* reserved); + +/** + * Add a `uint` option to a message as a 64-bit integer. + * + * @param msg Request of response message. + * @param num Option number. + * @param val Option value. + * @param reserved Reserved argument. Must be set to `NULL`. + * @return 0 on success, otherwise an error code defined by the `system_error_t` enum. + */ +int coap_add_uint64_option(coap_message* msg, int num, uint64_t val, void* reserved); + +/** + * Add a string option to a message. + * + * @param msg Request of response message. + * @param num Option number. + * @param str Option value. + * @param reserved Reserved argument. Must be set to `NULL`. + * @return 0 on success, otherwise an error code defined by the `system_error_t` enum. + */ +int coap_add_string_option(coap_message* msg, int num, const char* str, void* reserved); + +/** + * Add an opaque option to a message. + * + * @param msg Request of response message. + * @param num Option number. + * @param data Option data. + * @param size Size of the option data. + * @param reserved Reserved argument. Must be set to `NULL`. + * @return 0 on success, otherwise an error code defined by the `system_error_t` enum. + */ +int coap_add_opaque_option(coap_message* msg, int num, const char* data, size_t size, void* reserved); + +#ifdef __cplusplus +} // extern "C" +#endif diff --git a/communication/inc/coap_channel_new.h b/communication/inc/coap_channel_new.h new file mode 100644 index 0000000000..4067d694a9 --- /dev/null +++ b/communication/inc/coap_channel_new.h @@ -0,0 +1,154 @@ +/* + * Copyright (c) 2023 Particle Industries, Inc. All rights reserved. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation, either + * version 3 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, see . + */ + +#include +#include + +#include "message_channel.h" +#include "coap_api.h" +#include "coap.h" // For token_t + +#include "system_tick_hal.h" + +#include "ref_count.h" + +namespace particle::protocol { + +class CoapMessageDecoder; +class Protocol; + +namespace experimental { + +// This class implements the new experimental protocol API that allows the system to interact with +// the server at the CoAP level. It's meant to be used through the functions defined in coap_api.h +class CoapChannel { +public: + enum Result { + HANDLED = 1 // Returned by the handle* methods + }; + + ~CoapChannel(); + + // Methods called by the new CoAP API (coap_api.h) + + int beginRequest(coap_message** msg, const char* uri, coap_method method, int timeout); + int endRequest(coap_message* msg, coap_response_callback respCallback, coap_ack_callback ackCallback, + coap_error_callback errorCallback, void* callbackArg); + + int beginResponse(coap_message** msg, int code, int requestId); + int endResponse(coap_message* msg, coap_ack_callback ackCallback, coap_error_callback errorCallback, + void* callbackArg); + + int writePayload(coap_message* msg, const char* data, size_t& size, coap_block_callback blockCallback, + coap_error_callback errorCallback, void* callbackArg); + int readPayload(coap_message* msg, char* data, size_t& size, coap_block_callback blockCallback, + coap_error_callback errorCallback, void* callbackArg); + int peekPayload(coap_message* msg, char* data, size_t size); + + void destroyMessage(coap_message* msg); + + void cancelRequest(int requestId); + + int addRequestHandler(const char* uri, coap_method method, coap_request_callback callback, void* callbackArg); + void removeRequestHandler(const char* uri, coap_method method); + + int addConnectionHandler(coap_connection_callback callback, void* callbackArg); + void removeConnectionHandler(coap_connection_callback callback); + + // Methods called by the old protocol implementation + + void open(); + void close(int error = SYSTEM_ERROR_COAP_CONNECTION_CLOSED); + + int handleCon(const Message& msg); + int handleAck(const Message& msg); + int handleRst(const Message& msg); + + int run(); + + static CoapChannel* instance(); + +private: + // Channel state + enum class State { + CLOSED, + OPENING, + OPEN, + CLOSING + }; + + enum class MessageType { + REQUEST, // Regular or blockwise request carrying request data + BLOCK_REQUEST, // Blockwise request retrieving a block of response data + RESPONSE // Regular or blockwise response + }; + + enum class MessageState { + NEW, // Message created + READ, // Reading payload data + WRITE, // Writing payload data + WAIT_ACK, // Waiting for an ACK + WAIT_RESPONSE, // Waiting for a response + WAIT_BLOCK, // Waiting for the next message block + DONE // Message exchange completed + }; + + struct CoapMessage; + struct RequestMessage; + struct ResponseMessage; + struct RequestHandler; + struct ConnectionHandler; + + CoapChannel(); // Use instance() + + Message msgBuf_; // Reference to the shared message buffer + ConnectionHandler* connHandlers_; // List of registered connection handlers + RequestHandler* reqHandlers_; // List of registered request handlers + RequestMessage* sentReqs_; // List of requests awaiting a response from the server + RequestMessage* recvReqs_; // List of requests awaiting a response from the device + ResponseMessage* blockResps_; // List of responses for which the next message block is expected to be received + CoapMessage* unackMsgs_; // List of messages awaiting an ACK from the server + Protocol* protocol_; // Protocol instance + State state_; // Channel state + uint32_t lastReqTag_; // Last used request tag + int lastMsgId_; // Last used internal message ID + int curMsgId_; // Internal ID of the message stored in the shared buffer + int sessId_; // Counter incremented every time a new session with the server is started + int pendingCloseError_; // If non-zero, the channel needs to be closed + bool openPending_; // If true, the channel needs to be reopened + + int handleRequest(CoapMessageDecoder& d); + int handleResponse(CoapMessageDecoder& d); + int handleAck(CoapMessageDecoder& d); + + int prepareMessage(const RefCountPtr& msg); + int updateMessage(const RefCountPtr& msg); + int sendMessage(RefCountPtr msg); + void clearMessage(const RefCountPtr& msg); + + int sendAck(int coapId, bool rst = false); + + int handleProtocolError(ProtocolError error); + + void releaseMessageBuffer(); + + system_tick_t millis() const; +}; + +} // namespace experimental + +} // namespace particle::protocol diff --git a/communication/inc/protocol_defs.h b/communication/inc/protocol_defs.h index 6124815618..6885c6d2f3 100644 --- a/communication/inc/protocol_defs.h +++ b/communication/inc/protocol_defs.h @@ -60,6 +60,7 @@ enum ProtocolError IO_ERROR_SOCKET_SEND_FAILED = 33, IO_ERROR_SOCKET_RECV_FAILED = 34, IO_ERROR_REMOTE_END_CLOSED = 35, + COAP_ERROR = 36, // NOTE: when adding more ProtocolError codes, be sure to update toSystemError() in protocol_defs.cpp UNKNOWN = 0x7FFFF }; diff --git a/communication/src/build.mk b/communication/src/build.mk index 4a3a971300..c6bc89ecc4 100644 --- a/communication/src/build.mk +++ b/communication/src/build.mk @@ -38,6 +38,7 @@ CPPSRC += $(TARGET_SRC_PATH)/coap_message_decoder.cpp CPPSRC += $(TARGET_SRC_PATH)/coap_util.cpp CPPSRC += $(TARGET_SRC_PATH)/firmware_update.cpp CPPSRC += $(TARGET_SRC_PATH)/description.cpp +CPPSRC += $(TARGET_SRC_PATH)/coap_channel_new.cpp # ASM source files included in this build. ASRC += diff --git a/communication/src/coap_channel.cpp b/communication/src/coap_channel.cpp index 46fb6128fe..fdffcbb61a 100644 --- a/communication/src/coap_channel.cpp +++ b/communication/src/coap_channel.cpp @@ -20,9 +20,11 @@ #undef LOG_COMPILE_TIME_LEVEL #include "coap_channel.h" +#include "coap_channel_new.h" #include "service_debug.h" #include "messages.h" #include "communication_diagnostic.h" +#include "system_error.h" namespace particle { namespace protocol { @@ -64,6 +66,9 @@ void CoAPMessageStore::message_timeout(CoAPMessage& msg, Channel& channel) if (msg.is_request()) { LOG(ERROR, "CoAP message timeout; ID: %d", (int)msg.get_id()); g_unacknowledgedMessageCounter++; + // XXX: This will cancel _all_ messages with a timeout error, not just the timed out one. + // That's not ideal but should be okay while we're transitioning to the new CoAP API + experimental::CoapChannel::instance()->close(SYSTEM_ERROR_COAP_TIMEOUT); channel.command(MessageChannel::CLOSE); } } @@ -141,6 +146,7 @@ ProtocolError CoAPMessageStore::receive(Message& msg, Channel& channel, system_t } if (msgtype==CoAPType::RESET) { LOG(WARN, "Received RST message; discarding session"); + experimental::CoapChannel::instance()->handleRst(msg); if (coap_msg) { coap_msg->notify_delivered_nak(); } diff --git a/communication/src/coap_channel_new.cpp b/communication/src/coap_channel_new.cpp new file mode 100644 index 0000000000..8f567d4164 --- /dev/null +++ b/communication/src/coap_channel_new.cpp @@ -0,0 +1,1457 @@ +/* + * Copyright (c) 2023 Particle Industries, Inc. All rights reserved. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation, either + * version 3 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, see . + */ + +#if !defined(DEBUG_BUILD) && !defined(UNIT_TEST) +#define NDEBUG // TODO: Define NDEBUG in release builds +#endif + +#include "logging.h" + +LOG_SOURCE_CATEGORY("system.coap") + +#include +#include +#include +#include + +#include "coap_channel_new.h" + +#include "coap_message_encoder.h" +#include "coap_message_decoder.h" +#include "protocol.h" +#include "spark_protocol_functions.h" + +#include "random.h" +#include "scope_guard.h" +#include "check.h" + +#define CHECK_PROTOCOL(_expr) \ + do { \ + auto _r = _expr; \ + if (_r != ::particle::protocol::ProtocolError::NO_ERROR) { \ + return this->handleProtocolError(_r); \ + } \ + } while (false) + +namespace particle::protocol::experimental { + +namespace { + +const size_t MAX_TOKEN_SIZE = sizeof(token_t); // TODO: Support longer tokens +const size_t MAX_TAG_SIZE = 8; // Maximum size of an ETag (RFC 7252) or Request-Tag (RFC 9175) option + +const unsigned BLOCK_SZX = 6; // Value of the SZX field for 1024-byte blocks (RFC 7959, 2.2) +static_assert(COAP_BLOCK_SIZE == 1024); // When changing the block size, make sure to update BLOCK_SZX accordingly + +const unsigned DEFAULT_REQUEST_TIMEOUT = 60000; + +static_assert(COAP_INVALID_REQUEST_ID == 0); // Used by value in the code + +unsigned encodeBlockOption(int num, bool m) { + unsigned opt = (num << 4) | BLOCK_SZX; + if (m) { + opt |= 0x08; + } + return opt; +} + +int decodeBlockOption(unsigned opt, int& num, bool& m) { + unsigned szx = opt & 0x07; + if (szx != BLOCK_SZX) { + // Server is required to use exactly the same block size + return SYSTEM_ERROR_NOT_SUPPORTED; + } + num = opt >> 4; + m = opt & 0x08; + return 0; +} + +bool isValidCoapMethod(int method) { + switch (method) { + case COAP_METHOD_GET: + case COAP_METHOD_POST: + case COAP_METHOD_PUT: + case COAP_METHOD_DELETE: + return true; + default: + return false; + } +} + +// TODO: Use a generic intrusive list container +template>> +inline void addToList(T*& head, E* elem) { + assert(!elem->next && !elem->prev); // Debug-only + elem->prev = nullptr; + elem->next = head; + if (head) { + head->prev = elem; + } + head = elem; +} + +template || std::is_base_of_v>> +inline void removeFromList(T*& head, E* elem) { + assert(elem->next || elem->prev); + if (elem->prev) { + assert(head != elem); + elem->prev->next = elem->next; + } else { + assert(head == elem); + head = static_cast(elem->next); + } + if (elem->next) { + elem->next->prev = elem->prev; + } +#ifndef NDEBUG + elem->next = nullptr; + elem->prev = nullptr; +#endif +} + +template +inline T* findInList(T* head, const F& fn) { + while (head) { + if (fn(head)) { + return head; + } + head = static_cast(head->next); + } + return nullptr; +} + +template +inline void forEachInList(T* head, const F& fn) { + while (head) { + auto next = head->next; // In case the callback deletes the element + fn(head); + head = static_cast(next); + } +} + +template +inline void addRefToList(T*& head, RefCountPtr elem) { + addToList(head, elem.unwrap()); +} + +template +inline void removeRefFromList(T*& head, const RefCountPtr& elem) { + removeFromList(head, elem.get()); + elem->release(); +} + +template +inline RefCountPtr findRefInList(T* head, const F& fn) { + return findInList(head, fn); +} + +template +inline void forEachRefInList(T* head, const F& fn) { + while (head) { + RefCountPtr elem(head); + fn(elem.get()); + head = static_cast(elem->next); + } +} + +} // namespace + +struct CoapChannel::CoapMessage: RefCount { + coap_block_callback blockCallback; // Callback to invoke when a message block is sent or received + coap_ack_callback ackCallback; // Callback to invoke when the message is acknowledged + coap_error_callback errorCallback; // Callback to invoke when an error occurs + void* callbackArg; // User argument to pass to the callbacks + + int id; // Internal message ID + int requestId; // Internal ID of the request that started this message exchange + int sessionId; // ID of the session for which this message was created + + char tag[MAX_TAG_SIZE]; // ETag or Request-Tag option + size_t tagSize; // Size of the ETag or Request-Tag option + int coapId; // CoAP message ID + token_t token; // CoAP token. TODO: Support longer tokens + + std::optional blockIndex; // Index of the current message block + std::optional hasMore; // Whether more blocks are expected for this message + + char* pos; // Current position in the message buffer. If null, no message data has been written to the buffer yet + char* end; // End of the message buffer + size_t prefixSize; // Size of the CoAP framing not including the payload marker + + MessageType type; // Message type + MessageState state; // Message state + + CoapMessage* next; // Next message in the list + CoapMessage* prev; // Previous message in the list + + explicit CoapMessage(MessageType type) : + blockCallback(nullptr), + ackCallback(nullptr), + errorCallback(nullptr), + callbackArg(nullptr), + id(0), + requestId(0), + sessionId(0), + tag(), + tagSize(0), + coapId(0), + token(0), + pos(nullptr), + end(nullptr), + prefixSize(0), + type(type), + state(MessageState::NEW), + next(nullptr), + prev(nullptr) { + } +}; + +// TODO: Use separate message classes for different transfer directions +struct CoapChannel::RequestMessage: CoapMessage { + coap_response_callback responseCallback; // Callback to invoke when a response for this request is received + + ResponseMessage* blockResponse; // Response for which this block request is retrieving data + + system_tick_t timeSent; // Time the request was sent + unsigned timeout; // Request timeout + + coap_method method; // CoAP method code + char uri; // Request URI. TODO: Support longer URIs + + explicit RequestMessage(bool blockRequest = false) : + CoapMessage(blockRequest ? MessageType::BLOCK_REQUEST : MessageType::REQUEST), + responseCallback(nullptr), + blockResponse(nullptr), + timeSent(0), + timeout(0), + method(), + uri(0) { + } +}; + +struct CoapChannel::ResponseMessage: CoapMessage { + RefCountPtr blockRequest; // Request message used to get the last received block of this message + + int status; // CoAP response code + + ResponseMessage() : + CoapMessage(MessageType::RESPONSE), + status(0) { + } +}; + +struct CoapChannel::RequestHandler { + coap_request_callback callback; // Callback to invoke when a request is received + void* callbackArg; // User argument to pass to the callback + + coap_method method; // CoAP method code + char uri; // Request URI. TODO: Support longer URIs + + RequestHandler* next; // Next handler in the list + RequestHandler* prev; // Previous handler in the list + + RequestHandler(char uri, coap_method method, coap_request_callback callback, void* callbackArg) : + callback(callback), + callbackArg(callbackArg), + method(method), + uri(uri), + next(nullptr), + prev(nullptr) { + } +}; + +struct CoapChannel::ConnectionHandler { + coap_connection_callback callback; // Callback to invoke when the connection status changes + void* callbackArg; // User argument to pass to the callback + + bool openFailed; // If true, the user callback returned an error when the connection was opened + + ConnectionHandler* next; // Next handler in the list + ConnectionHandler* prev; // Previous handler in the list + + ConnectionHandler(coap_connection_callback callback, void* callbackArg) : + callback(callback), + callbackArg(callbackArg), + openFailed(false), + next(nullptr), + prev(nullptr) { + } +}; + +CoapChannel::CoapChannel() : + connHandlers_(nullptr), + reqHandlers_(nullptr), + sentReqs_(nullptr), + recvReqs_(nullptr), + blockResps_(nullptr), + unackMsgs_(nullptr), + protocol_(spark_protocol_instance()), + state_(State::CLOSED), + lastReqTag_(Random().gen()), + lastMsgId_(0), + curMsgId_(0), + sessId_(0), + pendingCloseError_(0), + openPending_(false) { +} + +CoapChannel::~CoapChannel() { + if (sentReqs_ || recvReqs_ || blockResps_ || unackMsgs_) { + LOG(ERROR, "Destroying channel while CoAP exchange is in progress"); + } + close(); + forEachInList(connHandlers_, [](auto h) { + delete h; + }); + forEachInList(reqHandlers_, [](auto h) { + delete h; + }); +} + +int CoapChannel::beginRequest(coap_message** msg, const char* uri, coap_method method, int timeout) { + if (timeout < 0 || std::strlen(uri) != 1) { // TODO: Support longer URIs + return SYSTEM_ERROR_INVALID_ARGUMENT; + } + if (state_ != State::OPEN) { + return SYSTEM_ERROR_COAP_CONNECTION_CLOSED; + } + auto req = makeRefCountPtr(); + if (!req) { + return SYSTEM_ERROR_NO_MEMORY; + } + auto msgId = ++lastMsgId_; + req->id = msgId; + req->requestId = msgId; + req->sessionId = sessId_; + req->uri = *uri; + req->method = method; + req->timeout = (timeout > 0) ? timeout : DEFAULT_REQUEST_TIMEOUT; + req->state = MessageState::WRITE; + *msg = reinterpret_cast(req.unwrap()); // Transfer ownership + return msgId; +} + +int CoapChannel::endRequest(coap_message* apiMsg, coap_response_callback respCallback, coap_ack_callback ackCallback, + coap_error_callback errorCallback, void* callbackArg) { + auto msg = RefCountPtr(reinterpret_cast(apiMsg)); + if (msg->type != MessageType::REQUEST) { + return SYSTEM_ERROR_INVALID_ARGUMENT; + } + if (msg->sessionId != sessId_) { + return SYSTEM_ERROR_COAP_REQUEST_CANCELLED; + } + auto req = staticPtrCast(msg); + if (req->state != MessageState::WRITE || req->hasMore.value_or(false)) { + return SYSTEM_ERROR_INVALID_STATE; + } + if (!req->pos) { + if (curMsgId_) { + // TODO: Support asynchronous writing to multiple message instances + LOG(WARN, "CoAP message buffer is already in use"); + releaseMessageBuffer(); + } + CHECK(prepareMessage(req)); + } else if (curMsgId_ != req->id) { + LOG(ERROR, "CoAP message buffer is no longer available"); + return SYSTEM_ERROR_NOT_SUPPORTED; + } + CHECK(sendMessage(req)); + req->responseCallback = respCallback; + req->ackCallback = ackCallback; + req->errorCallback = errorCallback; + req->callbackArg = callbackArg; + req->release(); // Take ownership + return 0; +} + +int CoapChannel::beginResponse(coap_message** msg, int status, int requestId) { + if (state_ != State::OPEN) { + return SYSTEM_ERROR_COAP_CONNECTION_CLOSED; + } + auto req = findRefInList(recvReqs_, [=](auto req) { + return req->id == requestId; + }); + if (!req) { + return SYSTEM_ERROR_COAP_REQUEST_NOT_FOUND; + } + auto resp = makeRefCountPtr(); + if (!resp) { + return SYSTEM_ERROR_NO_MEMORY; + } + resp->id = ++lastMsgId_; + resp->requestId = req->id; + resp->sessionId = sessId_; + resp->token = req->token; + resp->status = status; + resp->state = MessageState::WRITE; + *msg = reinterpret_cast(resp.unwrap()); // Transfer ownership + return 0; +} + +int CoapChannel::endResponse(coap_message* apiMsg, coap_ack_callback ackCallback, coap_error_callback errorCallback, + void* callbackArg) { + auto msg = RefCountPtr(reinterpret_cast(apiMsg)); + if (msg->type != MessageType::RESPONSE) { + return SYSTEM_ERROR_INVALID_ARGUMENT; + } + if (msg->sessionId != sessId_) { + return SYSTEM_ERROR_COAP_REQUEST_CANCELLED; + } + auto resp = staticPtrCast(msg); + if (resp->state != MessageState::WRITE) { + return SYSTEM_ERROR_INVALID_STATE; + } + if (!resp->pos) { + if (curMsgId_) { + // TODO: Support asynchronous writing to multiple message instances + LOG(WARN, "CoAP message buffer is already in use"); + releaseMessageBuffer(); + } + CHECK(prepareMessage(resp)); + } else if (curMsgId_ != resp->id) { + LOG(ERROR, "CoAP message buffer is no longer available"); + return SYSTEM_ERROR_NOT_SUPPORTED; + } + CHECK(sendMessage(resp)); + resp->ackCallback = ackCallback; + resp->errorCallback = errorCallback; + resp->callbackArg = callbackArg; + resp->release(); // Take ownership + return 0; +} + +int CoapChannel::writePayload(coap_message* apiMsg, const char* data, size_t& size, coap_block_callback blockCallback, + coap_error_callback errorCallback, void* callbackArg) { + auto msg = RefCountPtr(reinterpret_cast(apiMsg)); + if (msg->sessionId != sessId_) { + return SYSTEM_ERROR_COAP_REQUEST_CANCELLED; + } + if (msg->state != MessageState::WRITE) { + return SYSTEM_ERROR_INVALID_STATE; + } + bool sendBlock = false; + if (size > 0) { + if (!msg->pos) { + if (curMsgId_) { + // TODO: Support asynchronous writing to multiple message instances + LOG(WARN, "CoAP message buffer is already in use"); + releaseMessageBuffer(); + } + if (msg->blockIndex.has_value()) { + // Writing another message block + assert(msg->type == MessageType::REQUEST); + ++msg->blockIndex.value(); + msg->hasMore = false; + } + CHECK(prepareMessage(msg)); + *msg->pos++ = 0xff; // Payload marker + } else if (curMsgId_ != msg->id) { + LOG(ERROR, "CoAP message buffer is no longer available"); + return SYSTEM_ERROR_NOT_SUPPORTED; + } + auto bytesToWrite = size; + if (msg->pos + bytesToWrite > msg->end) { + if (msg->type != MessageType::REQUEST || !blockCallback) { // TODO: Support blockwise device-to-cloud responses + return SYSTEM_ERROR_TOO_LARGE; + } + bytesToWrite = msg->end - msg->pos; + sendBlock = true; + } + std::memcpy(msg->pos, data, bytesToWrite); + msg->pos += bytesToWrite; + if (sendBlock) { + if (!msg->blockIndex.has_value()) { + msg->blockIndex = 0; + // Add a Request-Tag option + auto tag = ++lastReqTag_; + static_assert(sizeof(tag) <= sizeof(msg->tag)); + std::memcpy(msg->tag, &tag, sizeof(tag)); + msg->tagSize = sizeof(tag); + } + msg->hasMore = true; + CHECK(updateMessage(msg)); // Update or add blockwise transfer options to the message + CHECK(sendMessage(msg)); + } + size = bytesToWrite; + } + msg->blockCallback = blockCallback; + msg->errorCallback = errorCallback; + msg->callbackArg = callbackArg; + return sendBlock ? COAP_RESULT_WAIT_BLOCK : 0; +} + +int CoapChannel::readPayload(coap_message* apiMsg, char* data, size_t& size, coap_block_callback blockCallback, + coap_error_callback errorCallback, void* callbackArg) { + auto msg = RefCountPtr(reinterpret_cast(apiMsg)); + if (msg->sessionId != sessId_) { + return SYSTEM_ERROR_COAP_REQUEST_CANCELLED; + } + if (msg->state != MessageState::READ) { + return SYSTEM_ERROR_INVALID_STATE; + } + bool getBlock = false; + if (size > 0) { + if (msg->pos == msg->end) { + return SYSTEM_ERROR_END_OF_STREAM; + } + if (curMsgId_ != msg->id) { + // TODO: Support asynchronous reading from multiple message instances + LOG(ERROR, "CoAP message buffer is no longer available"); + return SYSTEM_ERROR_NOT_SUPPORTED; + } + auto bytesToRead = std::min(size, msg->end - msg->pos); + if (data) { + std::memcpy(data, msg->pos, bytesToRead); + } + msg->pos += bytesToRead; + if (msg->pos == msg->end) { + releaseMessageBuffer(); + if (msg->hasMore.value_or(false)) { + if (blockCallback) { + assert(msg->type == MessageType::RESPONSE); // TODO: Support cloud-to-device blockwise requests + auto resp = staticPtrCast(msg); + // Send a new request with the original options and updated block number + auto req = resp->blockRequest; + assert(req && req->blockIndex.has_value()); + ++req->blockIndex.value(); + CHECK(prepareMessage(req)); + CHECK(sendMessage(std::move(req))); + resp->state = MessageState::WAIT_BLOCK; + addRefToList(blockResps_, std::move(resp)); + getBlock = true; + } else { + LOG(WARN, "Incomplete read of blockwise response"); + } + } + } + size = bytesToRead; + } + msg->blockCallback = blockCallback; + msg->errorCallback = errorCallback; + msg->callbackArg = callbackArg; + return getBlock ? COAP_RESULT_WAIT_BLOCK : 0; +} + +int CoapChannel::peekPayload(coap_message* apiMsg, char* data, size_t size) { + auto msg = RefCountPtr(reinterpret_cast(apiMsg)); + if (msg->sessionId != sessId_) { + return SYSTEM_ERROR_COAP_REQUEST_CANCELLED; + } + if (msg->state != MessageState::READ) { + return SYSTEM_ERROR_INVALID_STATE; + } + if (size > 0) { + if (msg->pos == msg->end) { + return SYSTEM_ERROR_END_OF_STREAM; + } + if (curMsgId_ != msg->id) { + // TODO: Support asynchronous reading from multiple message instances + LOG(ERROR, "CoAP message buffer is no longer available"); + return SYSTEM_ERROR_NOT_SUPPORTED; + } + size = std::min(size, msg->end - msg->pos); + if (data) { + std::memcpy(data, msg->pos, size); + } + } + return size; +} + +void CoapChannel::destroyMessage(coap_message* apiMsg) { + if (!apiMsg) { + return; + } + auto msg = RefCountPtr::wrap(reinterpret_cast(apiMsg)); // Take ownership + clearMessage(msg); +} + +void CoapChannel::cancelRequest(int requestId) { + if (requestId <= 0) { + return; + } + // Search among the messages for which a user callback may still need to be invoked + RefCountPtr msg = findRefInList(sentReqs_, [=](auto req) { + return req->type != MessageType::BLOCK_REQUEST && req->id == requestId; + }); + if (!msg) { + msg = findRefInList(unackMsgs_, [=](auto msg) { + return msg->type != MessageType::BLOCK_REQUEST && msg->requestId == requestId; + }); + if (!msg) { + msg = findRefInList(blockResps_, [=](auto msg) { + return msg->requestId == requestId; + }); + } + } + if (msg) { + clearMessage(msg); + } +} + +int CoapChannel::addRequestHandler(const char* uri, coap_method method, coap_request_callback callback, void* callbackArg) { + if (!*uri || !callback) { + return SYSTEM_ERROR_INVALID_ARGUMENT; + } + if (std::strlen(uri) != 1) { + return SYSTEM_ERROR_NOT_SUPPORTED; // TODO: Support longer URIs + } + if (state_ != State::CLOSED) { + return SYSTEM_ERROR_INVALID_STATE; + } + auto h = findInList(reqHandlers_, [=](auto h) { + return h->uri == *uri && h->method; + }); + if (h) { + h->callback = callback; + h->callbackArg = callbackArg; + } else { + std::unique_ptr h(new(std::nothrow) RequestHandler(*uri, method, callback, callbackArg)); + if (!h) { + return SYSTEM_ERROR_NO_MEMORY; + } + addToList(reqHandlers_, h.release()); + } + return 0; +} + +void CoapChannel::removeRequestHandler(const char* uri, coap_method method) { + if (std::strlen(uri) != 1) { // TODO: Support longer URIs + return; + } + if (state_ != State::CLOSED) { + LOG(ERROR, "Cannot remove handler while channel is open"); + return; + } + auto h = findInList(reqHandlers_, [=](auto h) { + return h->uri == *uri && h->method == method; + }); + if (h) { + removeFromList(reqHandlers_, h); + delete h; + } +} + +int CoapChannel::addConnectionHandler(coap_connection_callback callback, void* callbackArg) { + if (!callback) { + return SYSTEM_ERROR_INVALID_ARGUMENT; + } + if (state_ != State::CLOSED) { + return SYSTEM_ERROR_INVALID_STATE; + } + auto h = findInList(connHandlers_, [=](auto h) { + return h->callback == callback; + }); + if (!h) { + std::unique_ptr h(new(std::nothrow) ConnectionHandler(callback, callbackArg)); + if (!h) { + return SYSTEM_ERROR_NO_MEMORY; + } + addToList(connHandlers_, h.release()); + } + return 0; +} + +void CoapChannel::removeConnectionHandler(coap_connection_callback callback) { + if (state_ != State::CLOSED) { + LOG(ERROR, "Cannot remove handler while channel is open"); + return; + } + auto h = findInList(connHandlers_, [=](auto h) { + return h->callback == callback; + }); + if (h) { + removeFromList(connHandlers_, h); + delete h; + } +} + +void CoapChannel::open() { + pendingCloseError_ = 0; + if (state_ != State::CLOSED) { + if (state_ == State::CLOSING) { + // open() is being called from a connection handler + openPending_ = true; + } + return; + } + state_ = State::OPENING; + forEachInList(connHandlers_, [](auto h) { + assert(h->callback); + int r = h->callback(0 /* error */, COAP_CONNECTION_OPEN, h->callbackArg); + if (r < 0) { + // XXX: Handler errors are not propagated to the protocol layer. We may want to + // reconsider that + LOG(ERROR, "Connection handler failed: %d", r); + h->openFailed = true; + } + }); + state_ = State::OPEN; + if (pendingCloseError_) { + // close() was called from a connection handler + int error = pendingCloseError_; + pendingCloseError_ = 0; + close(error); // TODO: Call asynchronously + } +} + +void CoapChannel::close(int error) { + if (!error) { + error = SYSTEM_ERROR_COAP_CONNECTION_CLOSED; + } + openPending_ = false; + if (state_ != State::OPEN) { + if (state_ == State::OPENING) { + // close() is being called from a connection handler + pendingCloseError_ = error; + } + return; + } + state_ = State::CLOSING; + // Generate a new session ID to prevent the user code from messing up with the messages during + // the cleanup + ++sessId_; + releaseMessageBuffer(); + // Cancel device requests awaiting a response + forEachRefInList(sentReqs_, [=](auto req) { + if (req->type != MessageType::BLOCK_REQUEST && req->state == MessageState::WAIT_RESPONSE && req->errorCallback) { + req->errorCallback(error, req->id, req->callbackArg); // Callback passed to coap_write_payload() or coap_end_request() + } + req->state = MessageState::DONE; + req->release(); + }); + sentReqs_ = nullptr; + // Cancel device requests and responses awaiting an ACK + forEachRefInList(unackMsgs_, [=](auto msg) { + if (msg->type != MessageType::BLOCK_REQUEST && msg->state == MessageState::WAIT_ACK && msg->errorCallback) { + msg->errorCallback(error, msg->requestId, msg->callbackArg); // Callback passed to coap_write_payload(), coap_end_request() or coap_end_response() + } + msg->state = MessageState::DONE; + msg->release(); + }); + unackMsgs_ = nullptr; + // Cancel transfer of server blockwise responses + forEachRefInList(blockResps_, [=](auto msg) { + assert(msg->state == MessageState::WAIT_BLOCK); + if (msg->errorCallback) { + msg->errorCallback(error, msg->requestId, msg->callbackArg); // Callback passed to coap_read_payload() + } + msg->state = MessageState::DONE; + msg->release(); + }); + blockResps_ = nullptr; + // Cancel server requests awaiting a response + forEachRefInList(recvReqs_, [](auto req) { + // No need to invoke any callbacks for these + req->state = MessageState::DONE; + req->release(); + }); + recvReqs_ = nullptr; + // Invoke connection handlers + forEachInList(connHandlers_, [=](auto h) { + if (!h->openFailed) { + assert(h->callback); + int r = h->callback(error, COAP_CONNECTION_CLOSED, h->callbackArg); + if (r < 0) { + LOG(ERROR, "Connection handler failed: %d", r); + } + } + h->openFailed = false; // Clear the failed state + }); + state_ = State::CLOSED; + if (openPending_) { + // open() was called from a connection handler + openPending_ = false; + open(); // TODO: Call asynchronously + } +} + +int CoapChannel::handleCon(const Message& msgBuf) { + if (curMsgId_) { + // TODO: Support asynchronous reading/writing to multiple message instances + LOG(WARN, "CoAP message buffer is already in use"); + // Contents of the buffer have already been overwritten at this point + releaseMessageBuffer(); + } + msgBuf_ = msgBuf; // Makes a shallow copy + CoapMessageDecoder d; + CHECK(d.decode((const char*)msgBuf_.buf(), msgBuf_.length())); + if (d.type() != CoapType::CON) { + return SYSTEM_ERROR_INVALID_ARGUMENT; + } + int r = 0; + if (isCoapRequestCode(d.code())) { + r = CHECK(handleRequest(d)); + } else { + r = CHECK(handleResponse(d)); + } + return r; // 0 or Result::HANDLED +} + +int CoapChannel::handleAck(const Message& msgBuf) { + if (curMsgId_) { + // TODO: Support asynchronous reading/writing to multiple message instances + LOG(WARN, "CoAP message buffer is already in use"); + // Contents of the buffer have already been overwritten at this point + releaseMessageBuffer(); + } + msgBuf_ = msgBuf; // Makes a shallow copy + CoapMessageDecoder d; + CHECK(d.decode((const char*)msgBuf_.buf(), msgBuf_.length())); + if (d.type() != CoapType::ACK) { + return SYSTEM_ERROR_INVALID_ARGUMENT; + } + return CHECK(handleAck(d)); +} + +int CoapChannel::handleRst(const Message& msgBuf) { + if (curMsgId_) { + // TODO: Support asynchronous reading/writing to multiple message instances + LOG(WARN, "CoAP message buffer is already in use"); + // Contents of the buffer have already been overwritten at this point + releaseMessageBuffer(); + } + msgBuf_ = msgBuf; // Makes a shallow copy + CoapMessageDecoder d; + CHECK(d.decode((const char*)msgBuf_.buf(), msgBuf_.length())); + if (d.type() != CoapType::RST) { + return SYSTEM_ERROR_INVALID_ARGUMENT; + } + auto msg = findRefInList(unackMsgs_, [=](auto msg) { + return msg->coapId == d.id(); + }); + if (!msg) { + return 0; + } + assert(msg->state == MessageState::WAIT_ACK); + if (msg->type == MessageType::BLOCK_REQUEST) { + auto req = staticPtrCast(msg); + auto resp = RefCountPtr(req->blockResponse); // blockResponse is a raw pointer + assert(resp); + if (resp->errorCallback) { + resp->errorCallback(SYSTEM_ERROR_COAP_MESSAGE_RESET, resp->requestId, resp->callbackArg); // Callback passed to coap_read_response() + } + } else if (msg->errorCallback) { // REQUEST or RESPONSE + msg->errorCallback(SYSTEM_ERROR_COAP_MESSAGE_RESET, msg->requestId, msg->callbackArg); // Callback passed to coap_write_payload(), coap_end_request() or coap_end_response() + } + clearMessage(msg); + return Result::HANDLED; +} + +int CoapChannel::run() { + // TODO: ACK timeouts are handled by the old protocol implementation. As of now, the server always + // replies with piggybacked responses so we don't need to handle separate response timeouts either + return 0; +} + +CoapChannel* CoapChannel::instance() { + static CoapChannel channel; + return &channel; +} + +int CoapChannel::handleRequest(CoapMessageDecoder& d) { + if (d.tokenSize() != sizeof(token_t)) { // TODO: Support empty tokens + return 0; + } + // Get the request URI + char uri = '/'; // TODO: Support longer URIs + bool hasUri = false; + bool hasBlockOpt = false; + // TODO: Add a helper function for reconstructing the URI string from CoAP options + auto it = d.options(); + while (it.next()) { + if (it.option() == CoapOption::URI_PATH) { + if (it.size() > 1) { + return 0; // URI is too long, treat as an unrecognized request + } + if (it.size() > 0) { + if (hasUri) { + return 0; // ditto + } + uri = *it.data(); + hasUri = true; + } + } else if (it.option() == CoapOption::BLOCK1) { + hasBlockOpt = true; + } + } + // Find a request handler + auto method = d.code(); + auto h = findInList(reqHandlers_, [=](auto h) { + return h->uri == uri && h->method == method; + }); + if (!h) { + // The new CoAP API is implemented as an extension to the old protocol layer so, technically, + // the request may still be handled elsewhere + return 0; + } + if (hasBlockOpt) { + // TODO: Support cloud-to-device blockwise requests + LOG(WARN, "Received blockwise request"); + CHECK(sendAck(d.id(), true /* rst */)); + return Result::HANDLED; + } + // Acknowledge the request + assert(d.type() == CoapType::CON); // TODO: Support non-confirmable requests + CHECK(sendAck(d.id())); + // Create a message object + auto req = makeRefCountPtr(); + if (!req) { + return SYSTEM_ERROR_NO_MEMORY; + } + auto msgId = ++lastMsgId_; + req->id = msgId; + req->requestId = msgId; + req->sessionId = sessId_; + req->uri = uri; + req->method = static_cast(method); + req->coapId = d.id(); + assert(d.tokenSize() == sizeof(req->token)); + std::memcpy(&req->token, d.token(), d.tokenSize()); + req->pos = const_cast(d.payload()); + req->end = req->pos + d.payloadSize(); + req->state = MessageState::READ; + addRefToList(recvReqs_, req); + // Acquire the message buffer + assert(!curMsgId_); // Cleared in handleCon() + if (req->pos < req->end) { + curMsgId_ = msgId; + } + NAMED_SCOPE_GUARD(releaseMsgBufGuard, { + releaseMessageBuffer(); + }); + // Invoke the request handler + char uriStr[3] = { '/' }; + if (hasUri) { + uriStr[1] = uri; + } + assert(h->callback); + int r = h->callback(reinterpret_cast(req.get()), uriStr, req->method, req->id, h->callbackArg); + if (r < 0) { + LOG(ERROR, "Request handler failed: %d", r); + clearMessage(req); + return Result::HANDLED; + } + req.unwrap(); // Transfer ownership + releaseMsgBufGuard.dismiss(); + return Result::HANDLED; +} + +int CoapChannel::handleResponse(CoapMessageDecoder& d) { + if (d.tokenSize() != sizeof(token_t)) { // TODO: Support empty tokens + return 0; + } + token_t token = 0; + std::memcpy(&token, d.token(), d.tokenSize()); + // Find the request which this response is meant for + auto req = findRefInList(sentReqs_, [=](auto req) { + return req->token == token; + }); + if (!req) { + int r = 0; + if (d.type() == CoapType::CON) { + // Check the unack'd requests as this response could arrive before the ACK. In that case, + // handleResponse() will be called recursively + r = CHECK(handleAck(d)); + } + return r; // 0 or Result::HANDLED + } + assert(req->state == MessageState::WAIT_RESPONSE); + removeRefFromList(sentReqs_, req); + req->state = MessageState::DONE; + if (d.type() == CoapType::CON) { + // Acknowledge the response + CHECK(sendAck(d.id())); + } + // Check if it's a blockwise response + auto resp = RefCountPtr(req->blockResponse); // blockResponse is a raw pointer. If null, a response object hasn't been created yet + const char* etag = nullptr; + size_t etagSize = 0; + int blockIndex = -1; + bool hasMore = false; + auto it = d.options(); + while (it.next()) { + int r = 0; + if (it.option() == CoapOption::BLOCK2) { + r = decodeBlockOption(it.toUInt(), blockIndex, hasMore); + } else if (it.option() == CoapOption::ETAG) { + etag = it.data(); + etagSize = it.size(); + if (etagSize > MAX_TAG_SIZE) { + r = SYSTEM_ERROR_COAP; + } + } + if (r < 0) { + LOG(ERROR, "Failed to decode message options: %d", r); + if (resp) { + if (resp->errorCallback) { + resp->errorCallback(SYSTEM_ERROR_COAP, resp->requestId, resp->callbackArg); + } + clearMessage(resp); + } + return Result::HANDLED; + } + } + if (req->type == MessageType::BLOCK_REQUEST) { + // Received another block of a blockwise response + assert(req->blockIndex.has_value() && resp); + if (blockIndex != req->blockIndex.value() || !etag || etagSize != req->tagSize || + std::memcmp(etag, req->tag, etagSize) != 0) { + auto code = d.code(); + LOG(ERROR, "Blockwise transfer failed: %d.%02d", (int)coapCodeClass(code), (int)coapCodeDetail(code)); + if (resp->errorCallback) { + resp->errorCallback(SYSTEM_ERROR_COAP, resp->requestId, resp->callbackArg); + } + clearMessage(resp); + return Result::HANDLED; + } + resp->blockIndex = blockIndex; + resp->hasMore = hasMore; + resp->pos = const_cast(d.payload()); + resp->end = resp->pos + d.payloadSize(); + assert(resp->state == MessageState::WAIT_BLOCK); + removeRefFromList(blockResps_, resp); + resp->state = MessageState::READ; + // Acquire the message buffer + assert(!curMsgId_); // Cleared in handleCon() + if (resp->pos < resp->end) { + curMsgId_ = resp->id; + } + NAMED_SCOPE_GUARD(releaseMsgBufGuard, { + releaseMessageBuffer(); + }); + // Invoke the block handler + assert(resp->blockCallback); + int r = resp->blockCallback(reinterpret_cast(resp.get()), resp->requestId, resp->callbackArg); + if (r < 0) { + LOG(ERROR, "Message block handler failed: %d", r); + clearMessage(resp); + return Result::HANDLED; + } + releaseMsgBufGuard.dismiss(); + return Result::HANDLED; + } + if (req->blockIndex.has_value() && req->hasMore.value()) { + // Received a response for a non-final block of a blockwise request + auto code = d.code(); + if (code == CoapCode::CONTINUE) { + req->state = MessageState::WRITE; + // Invoke the block handler + assert(req->blockCallback); + int r = req->blockCallback(reinterpret_cast(req.get()), req->id, req->callbackArg); + if (r < 0) { + LOG(ERROR, "Message block handler failed: %d", r); + clearMessage(req); + } + } else { + LOG(ERROR, "Blockwise transfer failed: %d.%02d", (int)coapCodeClass(code), (int)coapCodeDetail(code)); + if (req->errorCallback) { + req->errorCallback(SYSTEM_ERROR_COAP, req->id, req->callbackArg); // Callback passed to coap_write_payload() + } + clearMessage(req); + } + return Result::HANDLED; + } + // Received a regular response or the first block of a blockwise response + if (!req->responseCallback) { + return Result::HANDLED; // :shrug: + } + // Create a message object + resp = makeRefCountPtr(); + if (!resp) { + return SYSTEM_ERROR_NO_MEMORY; + } + resp->id = ++lastMsgId_; + resp->requestId = req->id; + resp->sessionId = sessId_; + resp->coapId = d.id(); + resp->token = token; + resp->status = d.code(); + resp->pos = const_cast(d.payload()); + resp->end = resp->pos + d.payloadSize(); + resp->state = MessageState::READ; + if (blockIndex >= 0) { + // This CoAP implementation requires the server to use a ETag option with all blockwise + // responses. The first block must have an index of 0 + if (blockIndex != 0 || !etagSize) { + LOG(ERROR, "Received invalid blockwise response"); + if (req->errorCallback) { + req->errorCallback(SYSTEM_ERROR_COAP, req->id, req->callbackArg); // Callback passed to coap_end_request() + } + return Result::HANDLED; + } + resp->blockIndex = blockIndex; + resp->hasMore = hasMore; + resp->blockRequest = req; + req->type = MessageType::BLOCK_REQUEST; + req->blockResponse = resp.get(); + req->blockIndex = resp->blockIndex; + req->hasMore = false; + req->tagSize = etagSize; + std::memcpy(req->tag, etag, etagSize); + } + // Acquire the message buffer + assert(!curMsgId_); + if (resp->pos < resp->end) { + curMsgId_ = resp->id; + } + NAMED_SCOPE_GUARD(releaseMsgBufGuard, { + releaseMessageBuffer(); + }); + // Invoke the response handler + int r = req->responseCallback(reinterpret_cast(resp.get()), resp->status, req->id, req->callbackArg); + if (r < 0) { + LOG(ERROR, "Response handler failed: %d", r); + clearMessage(resp); + return Result::HANDLED; + } + resp.unwrap(); // Transfer ownership + releaseMsgBufGuard.dismiss(); + return Result::HANDLED; +} + +int CoapChannel::handleAck(CoapMessageDecoder& d) { + auto msg = findRefInList(unackMsgs_, [=](auto msg) { + return msg->coapId == d.id(); + }); + if (!msg) { + return 0; + } + assert(msg->state == MessageState::WAIT_ACK); + // For a blockwise request, the ACK callback is invoked when the last message block is acknowledged + if (msg->ackCallback && ((msg->type == MessageType::REQUEST && !msg->hasMore.value_or(false)) || + msg->type == MessageType::RESPONSE)) { + int r = msg->ackCallback(msg->requestId, msg->callbackArg); + if (r < 0) { + LOG(ERROR, "ACK handler failed: %d", r); + clearMessage(msg); + return Result::HANDLED; + } + } + if (msg->state == MessageState::WAIT_ACK) { + removeRefFromList(unackMsgs_, msg); + msg->state = MessageState::DONE; + if (msg->type == MessageType::REQUEST || msg->type == MessageType::BLOCK_REQUEST) { + msg->state = MessageState::WAIT_RESPONSE; + addRefToList(sentReqs_, staticPtrCast(msg)); + if (isCoapResponseCode(d.code())) { + CHECK(handleResponse(d)); + } + } + } + return Result::HANDLED; +} + +int CoapChannel::prepareMessage(const RefCountPtr& msg) { + assert(!curMsgId_); + CHECK_PROTOCOL(protocol_->get_channel().create(msgBuf_)); + if (msg->type == MessageType::REQUEST || msg->type == MessageType::BLOCK_REQUEST) { + msg->token = protocol_->get_next_token(); + } + msg->prefixSize = 0; + msg->pos = (char*)msgBuf_.buf(); + CHECK(updateMessage(msg)); + curMsgId_ = msg->id; + return 0; +} + +int CoapChannel::updateMessage(const RefCountPtr& msg) { + assert(curMsgId_ == msg->id); + char prefix[128]; + CoapMessageEncoder e(prefix, sizeof(prefix)); + e.type(CoapType::CON); + e.id(0); // Will be set by the underlying message channel + bool isRequest = msg->type == MessageType::REQUEST || msg->type == MessageType::BLOCK_REQUEST; + if (isRequest) { + auto req = staticPtrCast(msg); + e.code((int)req->method); + } else { + auto resp = staticPtrCast(msg); + e.code(resp->status); + } + e.token((const char*)&msg->token, sizeof(msg->token)); + // TODO: Support user-provided options + if (isRequest) { + auto req = staticPtrCast(msg); + if (req->type == MessageType::BLOCK_REQUEST && req->tagSize > 0) { + // Requesting the next block of a blockwise response + e.option(CoapOption::ETAG /* 4 */, req->tag, req->tagSize); + } + e.option(CoapOption::URI_PATH /* 11 */, &req->uri, 1); // TODO: Support longer URIs + if (req->blockIndex.has_value()) { + // See control vs descriptive usage of the block options in RFC 7959, 2.3 + if (req->type == MessageType::BLOCK_REQUEST) { + auto opt = encodeBlockOption(req->blockIndex.value(), false /* m */); + e.option(CoapOption::BLOCK2 /* 23 */, opt); + } else { + assert(req->hasMore.has_value()); + auto opt = encodeBlockOption(req->blockIndex.value(), req->hasMore.value()); + e.option(CoapOption::BLOCK1 /* 27 */, opt); + } + } + if (req->type == MessageType::REQUEST && req->tagSize > 0) { + // Sending the next block of a blockwise request + e.option(CoapOption::REQUEST_TAG /* 292 */, req->tag, req->tagSize); + } + } // TODO: Support device-to-cloud blockwise responses + auto msgBuf = (char*)msgBuf_.buf(); + size_t newPrefixSize = CHECK(e.encode()); + if (newPrefixSize > sizeof(prefix)) { + LOG(ERROR, "Too many CoAP options"); + return SYSTEM_ERROR_TOO_LARGE; + } + if (msg->prefixSize != newPrefixSize) { + size_t maxMsgSize = newPrefixSize + COAP_BLOCK_SIZE + 1; // Add 1 byte for a payload marker + if (maxMsgSize > msgBuf_.capacity()) { + LOG(ERROR, "No enough space in CoAP message buffer"); + return SYSTEM_ERROR_TOO_LARGE; + } + // Make room for the updated prefix data + size_t suffixSize = msg->pos - msgBuf - msg->prefixSize; // Size of the payload data with the payload marker + std::memmove(msgBuf + newPrefixSize, msgBuf + msg->prefixSize, suffixSize); + msg->pos += (int)newPrefixSize - (int)msg->prefixSize; + msg->end = msgBuf + maxMsgSize; + msg->prefixSize = newPrefixSize; + } + std::memcpy(msgBuf, prefix, msg->prefixSize); + return 0; +} + +int CoapChannel::sendMessage(RefCountPtr msg) { + assert(curMsgId_ == msg->id); + msgBuf_.set_length(msg->pos - (char*)msgBuf_.buf()); + CHECK_PROTOCOL(protocol_->get_channel().send(msgBuf_)); + msg->coapId = msgBuf_.get_id(); + msg->state = MessageState::WAIT_ACK; + msg->pos = nullptr; + addRefToList(unackMsgs_, std::move(msg)); + releaseMessageBuffer(); + return 0; +} + +int CoapChannel::sendAck(int coapId, bool rst) { + Message msg; + CHECK_PROTOCOL(protocol_->get_channel().response(msgBuf_, msg, msgBuf_.capacity() - msgBuf_.length())); + CoapMessageEncoder e((char*)msg.buf(), msg.capacity()); + e.type(rst ? CoapType::RST : CoapType::ACK); + e.code(CoapCode::EMPTY); + e.id(0); // Will be set by the underlying message channel + size_t n = CHECK(e.encode()); + if (n > msg.capacity()) { + LOG(ERROR, "No enough space in CoAP message buffer"); + return SYSTEM_ERROR_TOO_LARGE; + } + msg.set_length(n); + msg.set_id(coapId); + CHECK_PROTOCOL(protocol_->get_channel().send(msg)); + return 0; +} + +void CoapChannel::clearMessage(const RefCountPtr& msg) { + if (!msg || msg->sessionId != sessId_) { + return; + } + switch (msg->state) { + case MessageState::READ: { + if (msg->type == MessageType::REQUEST) { + removeRefFromList(recvReqs_, msg); + } + break; + } + case MessageState::WAIT_ACK: { + removeRefFromList(unackMsgs_, msg); + break; + } + case MessageState::WAIT_RESPONSE: { + assert(msg->type == MessageType::REQUEST); + removeRefFromList(sentReqs_, msg); + break; + } + case MessageState::WAIT_BLOCK: { + assert(msg->type == MessageType::RESPONSE); + auto resp = staticPtrCast(msg); + removeRefFromList(blockResps_, resp); + // Cancel the ongoing block request for this response + auto req = resp->blockRequest; + assert(req); + switch (req->state) { + case MessageState::WAIT_ACK: { + removeRefFromList(unackMsgs_, req); + break; + } + case MessageState::WAIT_RESPONSE: { + removeRefFromList(sentReqs_, req); + break; + } + default: + break; + } + req->state = MessageState::DONE; + break; + } + default: + break; + } + if (curMsgId_ == msg->id) { + releaseMessageBuffer(); + } + msg->state = MessageState::DONE; +} + +int CoapChannel::handleProtocolError(ProtocolError error) { + if (error == ProtocolError::NO_ERROR) { + return 0; + } + LOG(ERROR, "Protocol error: %d", (int)error); + int err = toSystemError(error); + close(err); + error = protocol_->get_channel().command(Channel::CLOSE); + if (error != ProtocolError::NO_ERROR) { + LOG(ERROR, "Channel CLOSE command failed: %d", (int)error); + } + return err; +} + +void CoapChannel::releaseMessageBuffer() { + msgBuf_.clear(); + curMsgId_ = 0; +} + +system_tick_t CoapChannel::millis() const { + return protocol_->get_callbacks().millis(); +} + +} // namespace particle::protocol::experimental + +using namespace particle::protocol::experimental; + +int coap_add_connection_handler(coap_connection_callback cb, void* arg, void* reserved) { + CHECK(CoapChannel::instance()->addConnectionHandler(cb, arg)); + return 0; +} + +void coap_remove_connection_handler(coap_connection_callback cb, void* reserved) { + CoapChannel::instance()->removeConnectionHandler(cb); +} + +int coap_add_request_handler(const char* uri, int method, int flags, coap_request_callback cb, void* arg, void* reserved) { + if (!isValidCoapMethod(method) || flags != 0) { + return SYSTEM_ERROR_INVALID_ARGUMENT; + } + CHECK(CoapChannel::instance()->addRequestHandler(uri, static_cast(method), cb, arg)); + return 0; +} + +void coap_remove_request_handler(const char* uri, int method, void* reserved) { + if (!isValidCoapMethod(method)) { + return; + } + CoapChannel::instance()->removeRequestHandler(uri, static_cast(method)); +} + +int coap_begin_request(coap_message** msg, const char* uri, int method, int timeout, int flags, void* reserved) { + if (!isValidCoapMethod(method) || flags != 0) { + return SYSTEM_ERROR_INVALID_ARGUMENT; + } + auto reqId = CHECK(CoapChannel::instance()->beginRequest(msg, uri, static_cast(method), timeout)); + return reqId; +} + +int coap_end_request(coap_message* msg, coap_response_callback resp_cb, coap_ack_callback ack_cb, + coap_error_callback error_cb, void* arg, void* reserved) { + CHECK(CoapChannel::instance()->endRequest(msg, resp_cb, ack_cb, error_cb, arg)); + return 0; +} + +int coap_begin_response(coap_message** msg, int status, int req_id, int flags, void* reserved) { + CHECK(CoapChannel::instance()->beginResponse(msg, status, req_id)); + return 0; +} + +int coap_end_response(coap_message* msg, coap_ack_callback ack_cb, coap_error_callback error_cb, + void* arg, void* reserved) { + CHECK(CoapChannel::instance()->endResponse(msg, ack_cb, error_cb, arg)); + return 0; +} + +void coap_destroy_message(coap_message* msg, void* reserved) { + CoapChannel::instance()->destroyMessage(msg); +} + +void coap_cancel_request(int req_id, void* reserved) { + CoapChannel::instance()->cancelRequest(req_id); +} + +int coap_write_payload(coap_message* msg, const char* data, size_t* size, coap_block_callback block_cb, + coap_error_callback error_cb, void* arg, void* reserved) { + int r = CHECK(CoapChannel::instance()->writePayload(msg, data, *size, block_cb, error_cb, arg)); + return r; // 0 or COAP_RESULT_WAIT_BLOCK +} + +int coap_read_payload(coap_message* msg, char* data, size_t* size, coap_block_callback block_cb, + coap_error_callback error_cb, void* arg, void* reserved) { + int r = CHECK(CoapChannel::instance()->readPayload(msg, data, *size, block_cb, error_cb, arg)); + return r; // 0 or COAP_RESULT_WAIT_BLOCK +} + +int coap_peek_payload(coap_message* msg, char* data, size_t size, void* reserved) { + size_t n = CHECK(CoapChannel::instance()->peekPayload(msg, data, size)); + return n; +} + +int coap_get_option(coap_option** opt, int num, coap_message* msg, void* reserved) { + return SYSTEM_ERROR_NOT_SUPPORTED; // TODO +} + +int coap_get_next_option(coap_option** opt, int* num, coap_message* msg, void* reserved) { + return SYSTEM_ERROR_NOT_SUPPORTED; // TODO +} + +int coap_get_uint_option_value(const coap_option* opt, unsigned* val, void* reserved) { + return SYSTEM_ERROR_NOT_SUPPORTED; // TODO +} + +int coap_get_uint64_option_value(const coap_option* opt, uint64_t* val, void* reserved) { + return SYSTEM_ERROR_NOT_SUPPORTED; // TODO +} + +int coap_get_string_option_value(const coap_option* opt, char* data, size_t size, void* reserved) { + return SYSTEM_ERROR_NOT_SUPPORTED; // TODO +} + +int coap_get_opaque_option_value(const coap_option* opt, char* data, size_t size, void* reserved) { + return SYSTEM_ERROR_NOT_SUPPORTED; // TODO +} + +int coap_add_empty_option(coap_message* msg, int num, void* reserved) { + return SYSTEM_ERROR_NOT_SUPPORTED; // TODO +} + +int coap_add_uint_option(coap_message* msg, int num, unsigned val, void* reserved) { + return SYSTEM_ERROR_NOT_SUPPORTED; // TODO +} + +int coap_add_uint64_option(coap_message* msg, int num, uint64_t val, void* reserved) { + return SYSTEM_ERROR_NOT_SUPPORTED; // TODO +} + +int coap_add_string_option(coap_message* msg, int num, const char* val, void* reserved) { + return SYSTEM_ERROR_NOT_SUPPORTED; // TODO +} + +int coap_add_opaque_option(coap_message* msg, int num, const char* data, size_t size, void* reserved) { + return SYSTEM_ERROR_NOT_SUPPORTED; // TODO +} diff --git a/communication/src/coap_defs.h b/communication/src/coap_defs.h index 4b266d19eb..d9f0e002f6 100644 --- a/communication/src/coap_defs.h +++ b/communication/src/coap_defs.h @@ -58,6 +58,7 @@ enum class CoapCode { VALID = coapCode(2, 3), CHANGED = coapCode(2, 4), CONTENT = coapCode(2, 5), + CONTINUE = coapCode(2, 31), // RFC 7959, 2.9. Response Codes BAD_REQUEST = coapCode(4, 0), UNAUTHORIZED = coapCode(4, 1), BAD_OPTION = coapCode(4, 2), @@ -65,6 +66,7 @@ enum class CoapCode { NOT_FOUND = coapCode(4, 4), METHOD_NOT_ALLOWED = coapCode(4, 5), NOT_ACCEPTABLE = coapCode(4, 6), + REQUEST_ENTITY_INCOMPLETE = coapCode(4, 8), // RFC 7959, 2.9. Response Codes PRECONDITION_FAILED = coapCode(4, 12), REQUEST_ENTITY_TOO_LARGE = coapCode(4, 13), UNSUPPORTED_CONTENT_FORMAT = coapCode(4, 15), @@ -98,7 +100,9 @@ enum class CoapOption { // RFC 7959, 2.1. The Block2 and Block1 Options; 4. The Size2 and Size1 Options BLOCK2 = 23, BLOCK1 = 27, - SIZE2 = 28 + SIZE2 = 28, + // RFC 9175, 3.2. The Request-Tag Option + REQUEST_TAG = 292 }; PARTICLE_DEFINE_ENUM_COMPARISON_OPERATORS(CoapOption) diff --git a/communication/src/dtls_message_channel.cpp b/communication/src/dtls_message_channel.cpp index 96802b3cae..a8c76a83b4 100644 --- a/communication/src/dtls_message_channel.cpp +++ b/communication/src/dtls_message_channel.cpp @@ -49,6 +49,7 @@ void mbedtls_ssl_update_out_pointers(mbedtls_ssl_context *ssl, mbedtls_ssl_trans #include #include "dtls_session_persist.h" #include "coap_channel.h" +#include "coap_channel_new.h" #include "coap_util.h" #include "platforms.h" @@ -290,6 +291,7 @@ inline int DTLSMessageChannel::send(const uint8_t* data, size_t len) void DTLSMessageChannel::reset_session() { + experimental::CoapChannel::instance()->close(); cancel_move_session(); mbedtls_ssl_session_reset(&ssl_context); sessionPersist.clear(callbacks.save); diff --git a/communication/src/protocol.cpp b/communication/src/protocol.cpp index 3f41a4c84d..8fd93cd426 100644 --- a/communication/src/protocol.cpp +++ b/communication/src/protocol.cpp @@ -31,6 +31,7 @@ LOG_SOURCE_CATEGORY("comm.protocol") #include "chunked_transfer.h" #include "subscriptions.h" #include "functions.h" +#include "coap_channel_new.h" #include "coap_message_decoder.h" #include "coap_message_encoder.h" @@ -214,8 +215,18 @@ ProtocolError Protocol::handle_received_message(Message& message, break; case CoAPMessageType::ERROR: - default: - ; // drop it on the floor + default: { + int r = 0; + if (type == CoAPType::CON) { + r = experimental::CoapChannel::instance()->handleCon(message); + } else if (type == CoAPType::ACK) { + r = experimental::CoapChannel::instance()->handleAck(message); + } + if (r < 0) { + return ProtocolError::COAP_ERROR; + } + break; + } } // all's well @@ -540,6 +551,7 @@ void Protocol::reset() { ack_handlers.clear(); channel.reset(); subscription_msg_ids.clear(); + experimental::CoapChannel::instance()->close(); } /** @@ -641,12 +653,12 @@ ProtocolError Protocol::event_loop(CoAPMessageType::Enum& message_type) if (error) { // bail if and only if there was an error + LOG(ERROR,"Event loop error %d", error); #if HAL_PLATFORM_OTA_PROTOCOL_V3 firmwareUpdate.reset(); #else chunkedTransfer.cancel(); #endif - LOG(ERROR,"Event loop error %d", error); return error; } return error; diff --git a/communication/src/protocol_defs.cpp b/communication/src/protocol_defs.cpp index 6706e5370f..bad232fc22 100644 --- a/communication/src/protocol_defs.cpp +++ b/communication/src/protocol_defs.cpp @@ -73,6 +73,8 @@ system_error_t toSystemError(ProtocolError error) { return SYSTEM_ERROR_OTA; case IO_ERROR_REMOTE_END_CLOSED: return SYSTEM_ERROR_END_OF_STREAM; + case COAP_ERROR: + return SYSTEM_ERROR_COAP; default: return SYSTEM_ERROR_PROTOCOL; // Generic protocol error } diff --git a/hal/src/nRF52840/hal_platform_nrf52840_config.h b/hal/src/nRF52840/hal_platform_nrf52840_config.h index e7d936308e..e0d5566a20 100644 --- a/hal/src/nRF52840/hal_platform_nrf52840_config.h +++ b/hal/src/nRF52840/hal_platform_nrf52840_config.h @@ -104,7 +104,7 @@ #define HAL_PLATFORM_RESUMABLE_OTA (1) -#define HAL_PLATFORM_ERROR_MESSAGES (1) +#define HAL_PLATFORM_ERROR_MESSAGES (0) #define HAL_PLATFORM_PROHIBIT_XIP (1) diff --git a/hal/src/nRF52840/mbedtls/mbedtls_config_platform.h b/hal/src/nRF52840/mbedtls/mbedtls_config_platform.h index 7f6f7fe336..1d5fa90b96 100644 --- a/hal/src/nRF52840/mbedtls/mbedtls_config_platform.h +++ b/hal/src/nRF52840/mbedtls/mbedtls_config_platform.h @@ -2344,7 +2344,7 @@ * * This module is required for SSL/TLS server support. */ -#define MBEDTLS_SSL_SRV_C +//#define MBEDTLS_SSL_SRV_C /** * \def MBEDTLS_SSL_TLS_C diff --git a/proto_defs/internal/ledger.proto b/proto_defs/internal/ledger.proto index de5cbd9a2b..847f7c581a 100644 --- a/proto_defs/internal/ledger.proto +++ b/proto_defs/internal/ledger.proto @@ -10,19 +10,21 @@ import "nanopb.proto"; */ message LedgerInfo { string name = 1 [(nanopb).max_length = 32]; ///< Ledger name. - cloud.ledger.Scope scope = 2; ///< Ledger scope. - cloud.ledger.SyncDirection sync_direction = 3; ///< Sync direction. + bytes scope_id = 2 [(nanopb).max_size = 32]; ///< Scope ID. + cloud.ledger.ScopeType scope_type = 3; ///< Scope type. + cloud.ledger.SyncDirection sync_direction = 4; ///< Sync direction. /** * Time the ledger was last updated, in milliseconds since the Unix epoch. * - * If 0, the time is unknown. + * If not set, the time is unknown. */ - fixed64 last_updated = 4; + optional fixed64 last_updated = 5; /** * Time the ledger was last synchronized with the Cloud, in milliseconds since the Unix epoch. * - * If 0, the ledger has never been synchronized. + * If not set, the ledger has never been synchronized. */ - fixed64 last_synced = 5; - bool sync_pending = 6; ///< Whether the ledger has local changes that need to be synchronized. + optional fixed64 last_synced = 6; + uint32 update_count = 7; ///< Counter incremented every time the ledger is updated. + bool sync_pending = 8; ///< Whether the ledger needs to be synchronized. } diff --git a/proto_defs/shared b/proto_defs/shared index ed36af512c..fb7721b9db 160000 --- a/proto_defs/shared +++ b/proto_defs/shared @@ -1 +1 @@ -Subproject commit ed36af512c65b52f9197773ef02011fd416af79d +Subproject commit fb7721b9dba10da2e762d81c162b5dde7444d5c0 diff --git a/proto_defs/src/cloud/cloud.pb.h b/proto_defs/src/cloud/cloud.pb.h index b635698aa8..a1e198220d 100644 --- a/proto_defs/src/cloud/cloud.pb.h +++ b/proto_defs/src/cloud/cloud.pb.h @@ -17,13 +17,18 @@ typedef enum _particle_cloud_Request_Type { particle_cloud_Request_Type_LEDGER_SET_DATA = 2, particle_cloud_Request_Type_LEDGER_GET_DATA = 3, particle_cloud_Request_Type_LEDGER_SUBSCRIBE = 4, - particle_cloud_Request_Type_LEDGER_NOTIFY_UPDATE = 5 + particle_cloud_Request_Type_LEDGER_NOTIFY_UPDATE = 5, + particle_cloud_Request_Type_LEDGER_RESET_INFO = 6 } particle_cloud_Request_Type; typedef enum _particle_cloud_Response_Result { particle_cloud_Response_Result_OK = 0, - particle_cloud_Response_Result_LEDGER_NOT_FOUND = 1, - particle_cloud_Response_Result_INVALID_SYNC_DIRECTION = 2 + particle_cloud_Response_Result_ERROR = 1, + particle_cloud_Response_Result_LEDGER_NOT_FOUND = 2, + particle_cloud_Response_Result_LEDGER_INVALID_SYNC_DIRECTION = 3, + particle_cloud_Response_Result_LEDGER_SCOPE_CHANGED = 4, + particle_cloud_Response_Result_LEDGER_INVALID_DATA = 5, + particle_cloud_Response_Result_LEDGER_TOO_LARGE_DATA = 6 } particle_cloud_Response_Result; /* Struct definitions */ @@ -44,6 +49,7 @@ typedef struct _particle_cloud_Request { particle_cloud_ledger_GetDataRequest ledger_get_data; particle_cloud_ledger_SubscribeRequest ledger_subscribe; particle_cloud_ledger_NotifyUpdateRequest ledger_notify_update; + particle_cloud_ledger_ResetInfoRequest ledger_reset_info; } data; } particle_cloud_Request; @@ -66,6 +72,7 @@ typedef struct _particle_cloud_Response { particle_cloud_ledger_GetDataResponse ledger_get_data; particle_cloud_ledger_SubscribeResponse ledger_subscribe; particle_cloud_ledger_NotifyUpdateResponse ledger_notify_update; + particle_cloud_ledger_ResetInfoResponse ledger_reset_info; } data; } particle_cloud_Response; @@ -93,12 +100,12 @@ typedef struct _particle_cloud_ServerMovedPermanentlyRequest { /* Helper constants for enums */ #define _particle_cloud_Request_Type_MIN particle_cloud_Request_Type_INVALID -#define _particle_cloud_Request_Type_MAX particle_cloud_Request_Type_LEDGER_NOTIFY_UPDATE -#define _particle_cloud_Request_Type_ARRAYSIZE ((particle_cloud_Request_Type)(particle_cloud_Request_Type_LEDGER_NOTIFY_UPDATE+1)) +#define _particle_cloud_Request_Type_MAX particle_cloud_Request_Type_LEDGER_RESET_INFO +#define _particle_cloud_Request_Type_ARRAYSIZE ((particle_cloud_Request_Type)(particle_cloud_Request_Type_LEDGER_RESET_INFO+1)) #define _particle_cloud_Response_Result_MIN particle_cloud_Response_Result_OK -#define _particle_cloud_Response_Result_MAX particle_cloud_Response_Result_INVALID_SYNC_DIRECTION -#define _particle_cloud_Response_Result_ARRAYSIZE ((particle_cloud_Response_Result)(particle_cloud_Response_Result_INVALID_SYNC_DIRECTION+1)) +#define _particle_cloud_Response_Result_MAX particle_cloud_Response_Result_LEDGER_TOO_LARGE_DATA +#define _particle_cloud_Response_Result_ARRAYSIZE ((particle_cloud_Response_Result)(particle_cloud_Response_Result_LEDGER_TOO_LARGE_DATA+1)) #ifdef __cplusplus @@ -122,6 +129,7 @@ extern "C" { #define particle_cloud_Request_ledger_get_data_tag 4 #define particle_cloud_Request_ledger_subscribe_tag 5 #define particle_cloud_Request_ledger_notify_update_tag 6 +#define particle_cloud_Request_ledger_reset_info_tag 7 #define particle_cloud_Response_result_tag 1 #define particle_cloud_Response_message_tag 2 #define particle_cloud_Response_ledger_get_info_tag 3 @@ -129,6 +137,7 @@ extern "C" { #define particle_cloud_Response_ledger_get_data_tag 5 #define particle_cloud_Response_ledger_subscribe_tag 6 #define particle_cloud_Response_ledger_notify_update_tag 7 +#define particle_cloud_Response_ledger_reset_info_tag 8 #define particle_cloud_ServerMovedPermanentlyRequest_server_addr_tag 1 #define particle_cloud_ServerMovedPermanentlyRequest_server_port_tag 2 #define particle_cloud_ServerMovedPermanentlyRequest_server_pub_key_tag 3 @@ -141,7 +150,8 @@ X(a, STATIC, ONEOF, MESSAGE, (data,ledger_get_info,data.ledger_get_info), X(a, STATIC, ONEOF, MESSAGE, (data,ledger_set_data,data.ledger_set_data), 3) \ X(a, STATIC, ONEOF, MESSAGE, (data,ledger_get_data,data.ledger_get_data), 4) \ X(a, STATIC, ONEOF, MESSAGE, (data,ledger_subscribe,data.ledger_subscribe), 5) \ -X(a, STATIC, ONEOF, MESSAGE, (data,ledger_notify_update,data.ledger_notify_update), 6) +X(a, STATIC, ONEOF, MESSAGE, (data,ledger_notify_update,data.ledger_notify_update), 6) \ +X(a, STATIC, ONEOF, MESSAGE, (data,ledger_reset_info,data.ledger_reset_info), 7) #define particle_cloud_Request_CALLBACK NULL #define particle_cloud_Request_DEFAULT NULL #define particle_cloud_Request_data_ledger_get_info_MSGTYPE particle_cloud_ledger_GetInfoRequest @@ -149,6 +159,7 @@ X(a, STATIC, ONEOF, MESSAGE, (data,ledger_notify_update,data.ledger_notify #define particle_cloud_Request_data_ledger_get_data_MSGTYPE particle_cloud_ledger_GetDataRequest #define particle_cloud_Request_data_ledger_subscribe_MSGTYPE particle_cloud_ledger_SubscribeRequest #define particle_cloud_Request_data_ledger_notify_update_MSGTYPE particle_cloud_ledger_NotifyUpdateRequest +#define particle_cloud_Request_data_ledger_reset_info_MSGTYPE particle_cloud_ledger_ResetInfoRequest #define particle_cloud_Response_FIELDLIST(X, a) \ X(a, STATIC, SINGULAR, SINT32, result, 1) \ @@ -157,7 +168,8 @@ X(a, STATIC, ONEOF, MESSAGE, (data,ledger_get_info,data.ledger_get_info), X(a, STATIC, ONEOF, MESSAGE, (data,ledger_set_data,data.ledger_set_data), 4) \ X(a, STATIC, ONEOF, MESSAGE, (data,ledger_get_data,data.ledger_get_data), 5) \ X(a, STATIC, ONEOF, MESSAGE, (data,ledger_subscribe,data.ledger_subscribe), 6) \ -X(a, STATIC, ONEOF, MESSAGE, (data,ledger_notify_update,data.ledger_notify_update), 7) +X(a, STATIC, ONEOF, MESSAGE, (data,ledger_notify_update,data.ledger_notify_update), 7) \ +X(a, STATIC, ONEOF, MESSAGE, (data,ledger_reset_info,data.ledger_reset_info), 8) #define particle_cloud_Response_CALLBACK pb_default_field_callback #define particle_cloud_Response_DEFAULT NULL #define particle_cloud_Response_data_ledger_get_info_MSGTYPE particle_cloud_ledger_GetInfoResponse @@ -165,6 +177,7 @@ X(a, STATIC, ONEOF, MESSAGE, (data,ledger_notify_update,data.ledger_notify #define particle_cloud_Response_data_ledger_get_data_MSGTYPE particle_cloud_ledger_GetDataResponse #define particle_cloud_Response_data_ledger_subscribe_MSGTYPE particle_cloud_ledger_SubscribeResponse #define particle_cloud_Response_data_ledger_notify_update_MSGTYPE particle_cloud_ledger_NotifyUpdateResponse +#define particle_cloud_Response_data_ledger_reset_info_MSGTYPE particle_cloud_ledger_ResetInfoResponse #define particle_cloud_ServerMovedPermanentlyRequest_FIELDLIST(X, a) \ X(a, CALLBACK, SINGULAR, STRING, server_addr, 1) \ @@ -193,9 +206,9 @@ extern const pb_msgdesc_t particle_cloud_ServerMovedPermanentlyResponse_msg; /* Maximum encoded size of messages (where known) */ /* particle_cloud_Response_size depends on runtime parameters */ /* particle_cloud_ServerMovedPermanentlyRequest_size depends on runtime parameters */ -#if defined(particle_cloud_ledger_GetInfoRequest_size) && defined(particle_cloud_ledger_SetDataRequest_size) && defined(particle_cloud_ledger_SubscribeRequest_size) +#if defined(particle_cloud_ledger_GetInfoRequest_size) && defined(particle_cloud_ledger_SetDataRequest_size) && defined(particle_cloud_ledger_SubscribeRequest_size) && defined(particle_cloud_ledger_NotifyUpdateRequest_size) #define particle_cloud_Request_size (2 + sizeof(union particle_cloud_Request_data_size_union)) -union particle_cloud_Request_data_size_union {char f2[(6 + particle_cloud_ledger_GetInfoRequest_size)]; char f3[(6 + particle_cloud_ledger_SetDataRequest_size)]; char f5[(6 + particle_cloud_ledger_SubscribeRequest_size)]; char f0[45];}; +union particle_cloud_Request_data_size_union {char f2[(6 + particle_cloud_ledger_GetInfoRequest_size)]; char f3[(6 + particle_cloud_ledger_SetDataRequest_size)]; char f5[(6 + particle_cloud_ledger_SubscribeRequest_size)]; char f6[(6 + particle_cloud_ledger_NotifyUpdateRequest_size)]; char f0[79];}; #endif #define particle_cloud_ServerMovedPermanentlyResponse_size 0 diff --git a/proto_defs/src/cloud/ledger.pb.c b/proto_defs/src/cloud/ledger.pb.c index bf38eda05a..37dea4a67a 100644 --- a/proto_defs/src/cloud/ledger.pb.c +++ b/proto_defs/src/cloud/ledger.pb.c @@ -30,6 +30,9 @@ PB_BIND(particle_cloud_ledger_GetDataResponse, particle_cloud_ledger_GetDataResp PB_BIND(particle_cloud_ledger_SubscribeRequest, particle_cloud_ledger_SubscribeRequest, AUTO) +PB_BIND(particle_cloud_ledger_SubscribeRequest_Ledger, particle_cloud_ledger_SubscribeRequest_Ledger, AUTO) + + PB_BIND(particle_cloud_ledger_SubscribeResponse, particle_cloud_ledger_SubscribeResponse, AUTO) @@ -39,9 +42,18 @@ PB_BIND(particle_cloud_ledger_SubscribeResponse_Ledger, particle_cloud_ledger_Su PB_BIND(particle_cloud_ledger_NotifyUpdateRequest, particle_cloud_ledger_NotifyUpdateRequest, AUTO) +PB_BIND(particle_cloud_ledger_NotifyUpdateRequest_Ledger, particle_cloud_ledger_NotifyUpdateRequest_Ledger, AUTO) + + PB_BIND(particle_cloud_ledger_NotifyUpdateResponse, particle_cloud_ledger_NotifyUpdateResponse, AUTO) +PB_BIND(particle_cloud_ledger_ResetInfoRequest, particle_cloud_ledger_ResetInfoRequest, AUTO) + + +PB_BIND(particle_cloud_ledger_ResetInfoResponse, particle_cloud_ledger_ResetInfoResponse, AUTO) + + diff --git a/proto_defs/src/cloud/ledger.pb.h b/proto_defs/src/cloud/ledger.pb.h index 15ed40dd3e..714b8f8c89 100644 --- a/proto_defs/src/cloud/ledger.pb.h +++ b/proto_defs/src/cloud/ledger.pb.h @@ -11,13 +11,13 @@ /* Enum definitions */ /* * - Ledger scope. */ -typedef enum _particle_cloud_ledger_Scope { - particle_cloud_ledger_Scope_SCOPE_UNKNOWN = 0, /* /< Unknown scope. */ - particle_cloud_ledger_Scope_SCOPE_DEVICE = 1, /* /< Device scope. */ - particle_cloud_ledger_Scope_SCOPE_PRODUCT = 2, /* /< Product scope. */ - particle_cloud_ledger_Scope_SCOPE_OWNER = 3 /* /< Owner scope. */ -} particle_cloud_ledger_Scope; + Scope type. */ +typedef enum _particle_cloud_ledger_ScopeType { + particle_cloud_ledger_ScopeType_SCOPE_TYPE_UNKNOWN = 0, /* /< Unknown scope. */ + particle_cloud_ledger_ScopeType_SCOPE_TYPE_DEVICE = 1, /* /< Device scope. */ + particle_cloud_ledger_ScopeType_SCOPE_TYPE_PRODUCT = 2, /* /< Product scope. */ + particle_cloud_ledger_ScopeType_SCOPE_TYPE_OWNER = 3 /* /< Owner scope. */ +} particle_cloud_ledger_ScopeType; /* * Sync direction. */ @@ -44,15 +44,28 @@ typedef struct _particle_cloud_ledger_GetInfoResponse { /* * Ledger info. - If the request contained an unknown ledger name or the requested ledger is inaccessible by the - device, the info about the respective ledger would be omitted. */ + A ledger is omitted in the response if it cannot be found or is not accessible by the device. */ pb_callback_t ledgers; } particle_cloud_ledger_GetInfoResponse; +/* * + Response for `ResetInfoRequest`. */ +typedef struct _particle_cloud_ledger_NotifyUpdateRequest { + pb_callback_t ledgers; +} particle_cloud_ledger_NotifyUpdateRequest; + typedef struct _particle_cloud_ledger_NotifyUpdateResponse { char dummy_field; } particle_cloud_ledger_NotifyUpdateResponse; +typedef struct _particle_cloud_ledger_ResetInfoRequest { + char dummy_field; +} particle_cloud_ledger_ResetInfoRequest; + +typedef struct _particle_cloud_ledger_ResetInfoResponse { + char dummy_field; +} particle_cloud_ledger_ResetInfoResponse; + /* * Get the contents of a remote cloud-to-device ledger. @@ -64,29 +77,29 @@ typedef struct _particle_cloud_ledger_SetDataResponse { /* * Response for `SubscribeRequest`. */ typedef struct _particle_cloud_ledger_SubscribeRequest { - /* * - Ledger info. */ - pb_callback_t ledgers; + pb_callback_t ledgers; /* /< Ledger info. */ } particle_cloud_ledger_SubscribeRequest; /* * - Notify the device that a cloud-to-device ledger was updated. - - This request is sent by the server. */ + Response for `NotifyUpdateRequest`. */ typedef struct _particle_cloud_ledger_SubscribeResponse { - pb_callback_t ledgers; /* /< Ledger name. */ + pb_callback_t ledgers; } particle_cloud_ledger_SubscribeResponse; +typedef PB_BYTES_ARRAY_T(32) particle_cloud_ledger_GetDataRequest_scope_id_t; /* * Response for `GetDataRequest`. */ typedef struct _particle_cloud_ledger_GetDataRequest { /* * + Time the ledger was last updated, in milliseconds since the Unix epoch. + + If not set, the ledger has not yet been assigned any data. */ + char name[33]; /* * Contents of the ledger. - If not specified, the device has the most recent version of the ledger data. */ - char name[33]; - /* * - Time the ledger was last updated, in milliseconds since the Unix epoch. */ + If not set, the device has the most recent version of the ledger data. */ + /* XXX: Device OS currently requires this field to have the highest field number in the message. */ + particle_cloud_ledger_GetDataRequest_scope_id_t scope_id; bool has_last_updated; uint64_t last_updated; } particle_cloud_ledger_GetDataRequest; @@ -99,52 +112,72 @@ typedef struct _particle_cloud_ledger_GetDataRequest { This request is sent by the device. */ typedef struct _particle_cloud_ledger_GetDataResponse { - /* * - Names of the ledgers to subscribe to. */ + bool has_last_updated; + uint64_t last_updated; /* /< Ledgers to subscribe to. */ pb_callback_t data; - uint64_t last_updated; } particle_cloud_ledger_GetDataResponse; +typedef PB_BYTES_ARRAY_T(32) particle_cloud_ledger_GetInfoResponse_Ledger_scope_id_t; /* * Update the contents of a remote device-to-cloud ledger. This request is sent by the device. */ typedef struct _particle_cloud_ledger_GetInfoResponse_Ledger { char name[33]; /* /< Ledger name. */ - particle_cloud_ledger_Scope scope; /* /< Contents of the ledger. */ + particle_cloud_ledger_GetInfoResponse_Ledger_scope_id_t scope_id; /* /< Scope ID. */ /* * Time the ledger was last updated, in milliseconds since the Unix epoch. - If 0, the time is unknown. */ + If not set, the time is unknown. */ + particle_cloud_ledger_ScopeType scope_type; /* * + Contents of the ledger. */ + /* XXX: Device OS currently requires this field to have the highest field number in the message. */ particle_cloud_ledger_SyncDirection sync_direction; + bool has_last_updated; uint64_t last_updated; } particle_cloud_ledger_GetInfoResponse_Ledger; -typedef struct _particle_cloud_ledger_NotifyUpdateRequest { +typedef struct _particle_cloud_ledger_NotifyUpdateRequest_Ledger { char name[33]; uint64_t last_updated; -} particle_cloud_ledger_NotifyUpdateRequest; +} particle_cloud_ledger_NotifyUpdateRequest_Ledger; +typedef PB_BYTES_ARRAY_T(32) particle_cloud_ledger_SetDataRequest_scope_id_t; /* * Response for `SetDataRequest`. */ typedef struct _particle_cloud_ledger_SetDataRequest { char name[33]; - pb_callback_t data; + particle_cloud_ledger_SetDataRequest_scope_id_t scope_id; + bool has_last_updated; uint64_t last_updated; + pb_callback_t data; } particle_cloud_ledger_SetDataRequest; +typedef PB_BYTES_ARRAY_T(32) particle_cloud_ledger_SubscribeRequest_Ledger_scope_id_t; /* * - Response for `NotifyUpdateRequest`. */ + Notify the device that one or more cloud-to-device ledgers were updated. + + This request is sent by the server. */ +typedef struct _particle_cloud_ledger_SubscribeRequest_Ledger { + char name[33]; /* /< Ledger info. */ + particle_cloud_ledger_SubscribeRequest_Ledger_scope_id_t scope_id; +} particle_cloud_ledger_SubscribeRequest_Ledger; + +/* * + Notify the device that it needs to re-request the info about all ledgers in use. + + This request is sent by the server. */ typedef struct _particle_cloud_ledger_SubscribeResponse_Ledger { char name[33]; + bool has_last_updated; uint64_t last_updated; } particle_cloud_ledger_SubscribeResponse_Ledger; /* Helper constants for enums */ -#define _particle_cloud_ledger_Scope_MIN particle_cloud_ledger_Scope_SCOPE_UNKNOWN -#define _particle_cloud_ledger_Scope_MAX particle_cloud_ledger_Scope_SCOPE_OWNER -#define _particle_cloud_ledger_Scope_ARRAYSIZE ((particle_cloud_ledger_Scope)(particle_cloud_ledger_Scope_SCOPE_OWNER+1)) +#define _particle_cloud_ledger_ScopeType_MIN particle_cloud_ledger_ScopeType_SCOPE_TYPE_UNKNOWN +#define _particle_cloud_ledger_ScopeType_MAX particle_cloud_ledger_ScopeType_SCOPE_TYPE_OWNER +#define _particle_cloud_ledger_ScopeType_ARRAYSIZE ((particle_cloud_ledger_ScopeType)(particle_cloud_ledger_ScopeType_SCOPE_TYPE_OWNER+1)) #define _particle_cloud_ledger_SyncDirection_MIN particle_cloud_ledger_SyncDirection_SYNC_DIRECTION_UNKNOWN #define _particle_cloud_ledger_SyncDirection_MAX particle_cloud_ledger_SyncDirection_SYNC_DIRECTION_CLOUD_TO_DEVICE @@ -158,47 +191,61 @@ extern "C" { /* Initializer values for message structs */ #define particle_cloud_ledger_GetInfoRequest_init_default {{{NULL}, NULL}} #define particle_cloud_ledger_GetInfoResponse_init_default {{{NULL}, NULL}} -#define particle_cloud_ledger_GetInfoResponse_Ledger_init_default {"", _particle_cloud_ledger_Scope_MIN, _particle_cloud_ledger_SyncDirection_MIN, 0} -#define particle_cloud_ledger_SetDataRequest_init_default {"", {{NULL}, NULL}, 0} +#define particle_cloud_ledger_GetInfoResponse_Ledger_init_default {"", {0, {0}}, _particle_cloud_ledger_ScopeType_MIN, _particle_cloud_ledger_SyncDirection_MIN, false, 0} +#define particle_cloud_ledger_SetDataRequest_init_default {"", {0, {0}}, false, 0, {{NULL}, NULL}} #define particle_cloud_ledger_SetDataResponse_init_default {0} -#define particle_cloud_ledger_GetDataRequest_init_default {"", false, 0} -#define particle_cloud_ledger_GetDataResponse_init_default {{{NULL}, NULL}, 0} +#define particle_cloud_ledger_GetDataRequest_init_default {"", {0, {0}}, false, 0} +#define particle_cloud_ledger_GetDataResponse_init_default {false, 0, {{NULL}, NULL}} #define particle_cloud_ledger_SubscribeRequest_init_default {{{NULL}, NULL}} +#define particle_cloud_ledger_SubscribeRequest_Ledger_init_default {"", {0, {0}}} #define particle_cloud_ledger_SubscribeResponse_init_default {{{NULL}, NULL}} -#define particle_cloud_ledger_SubscribeResponse_Ledger_init_default {"", 0} -#define particle_cloud_ledger_NotifyUpdateRequest_init_default {"", 0} +#define particle_cloud_ledger_SubscribeResponse_Ledger_init_default {"", false, 0} +#define particle_cloud_ledger_NotifyUpdateRequest_init_default {{{NULL}, NULL}} +#define particle_cloud_ledger_NotifyUpdateRequest_Ledger_init_default {"", 0} #define particle_cloud_ledger_NotifyUpdateResponse_init_default {0} +#define particle_cloud_ledger_ResetInfoRequest_init_default {0} +#define particle_cloud_ledger_ResetInfoResponse_init_default {0} #define particle_cloud_ledger_GetInfoRequest_init_zero {{{NULL}, NULL}} #define particle_cloud_ledger_GetInfoResponse_init_zero {{{NULL}, NULL}} -#define particle_cloud_ledger_GetInfoResponse_Ledger_init_zero {"", _particle_cloud_ledger_Scope_MIN, _particle_cloud_ledger_SyncDirection_MIN, 0} -#define particle_cloud_ledger_SetDataRequest_init_zero {"", {{NULL}, NULL}, 0} +#define particle_cloud_ledger_GetInfoResponse_Ledger_init_zero {"", {0, {0}}, _particle_cloud_ledger_ScopeType_MIN, _particle_cloud_ledger_SyncDirection_MIN, false, 0} +#define particle_cloud_ledger_SetDataRequest_init_zero {"", {0, {0}}, false, 0, {{NULL}, NULL}} #define particle_cloud_ledger_SetDataResponse_init_zero {0} -#define particle_cloud_ledger_GetDataRequest_init_zero {"", false, 0} -#define particle_cloud_ledger_GetDataResponse_init_zero {{{NULL}, NULL}, 0} +#define particle_cloud_ledger_GetDataRequest_init_zero {"", {0, {0}}, false, 0} +#define particle_cloud_ledger_GetDataResponse_init_zero {false, 0, {{NULL}, NULL}} #define particle_cloud_ledger_SubscribeRequest_init_zero {{{NULL}, NULL}} +#define particle_cloud_ledger_SubscribeRequest_Ledger_init_zero {"", {0, {0}}} #define particle_cloud_ledger_SubscribeResponse_init_zero {{{NULL}, NULL}} -#define particle_cloud_ledger_SubscribeResponse_Ledger_init_zero {"", 0} -#define particle_cloud_ledger_NotifyUpdateRequest_init_zero {"", 0} +#define particle_cloud_ledger_SubscribeResponse_Ledger_init_zero {"", false, 0} +#define particle_cloud_ledger_NotifyUpdateRequest_init_zero {{{NULL}, NULL}} +#define particle_cloud_ledger_NotifyUpdateRequest_Ledger_init_zero {"", 0} #define particle_cloud_ledger_NotifyUpdateResponse_init_zero {0} +#define particle_cloud_ledger_ResetInfoRequest_init_zero {0} +#define particle_cloud_ledger_ResetInfoResponse_init_zero {0} /* Field tags (for use in manual encoding/decoding) */ #define particle_cloud_ledger_GetInfoRequest_ledgers_tag 1 #define particle_cloud_ledger_GetInfoResponse_ledgers_tag 1 +#define particle_cloud_ledger_NotifyUpdateRequest_ledgers_tag 1 #define particle_cloud_ledger_SubscribeRequest_ledgers_tag 1 #define particle_cloud_ledger_SubscribeResponse_ledgers_tag 1 #define particle_cloud_ledger_GetDataRequest_name_tag 1 -#define particle_cloud_ledger_GetDataRequest_last_updated_tag 2 -#define particle_cloud_ledger_GetDataResponse_data_tag 1 -#define particle_cloud_ledger_GetDataResponse_last_updated_tag 2 +#define particle_cloud_ledger_GetDataRequest_scope_id_tag 2 +#define particle_cloud_ledger_GetDataRequest_last_updated_tag 3 +#define particle_cloud_ledger_GetDataResponse_last_updated_tag 1 +#define particle_cloud_ledger_GetDataResponse_data_tag 10 #define particle_cloud_ledger_GetInfoResponse_Ledger_name_tag 1 -#define particle_cloud_ledger_GetInfoResponse_Ledger_scope_tag 2 -#define particle_cloud_ledger_GetInfoResponse_Ledger_sync_direction_tag 3 -#define particle_cloud_ledger_GetInfoResponse_Ledger_last_updated_tag 4 -#define particle_cloud_ledger_NotifyUpdateRequest_name_tag 1 -#define particle_cloud_ledger_NotifyUpdateRequest_last_updated_tag 2 +#define particle_cloud_ledger_GetInfoResponse_Ledger_scope_id_tag 2 +#define particle_cloud_ledger_GetInfoResponse_Ledger_scope_type_tag 3 +#define particle_cloud_ledger_GetInfoResponse_Ledger_sync_direction_tag 4 +#define particle_cloud_ledger_GetInfoResponse_Ledger_last_updated_tag 5 +#define particle_cloud_ledger_NotifyUpdateRequest_Ledger_name_tag 1 +#define particle_cloud_ledger_NotifyUpdateRequest_Ledger_last_updated_tag 2 #define particle_cloud_ledger_SetDataRequest_name_tag 1 -#define particle_cloud_ledger_SetDataRequest_data_tag 2 +#define particle_cloud_ledger_SetDataRequest_scope_id_tag 2 #define particle_cloud_ledger_SetDataRequest_last_updated_tag 3 +#define particle_cloud_ledger_SetDataRequest_data_tag 10 +#define particle_cloud_ledger_SubscribeRequest_Ledger_name_tag 1 +#define particle_cloud_ledger_SubscribeRequest_Ledger_scope_id_tag 2 #define particle_cloud_ledger_SubscribeResponse_Ledger_name_tag 1 #define particle_cloud_ledger_SubscribeResponse_Ledger_last_updated_tag 2 @@ -216,16 +263,18 @@ X(a, CALLBACK, REPEATED, MESSAGE, ledgers, 1) #define particle_cloud_ledger_GetInfoResponse_Ledger_FIELDLIST(X, a) \ X(a, STATIC, SINGULAR, STRING, name, 1) \ -X(a, STATIC, SINGULAR, UENUM, scope, 2) \ -X(a, STATIC, SINGULAR, UENUM, sync_direction, 3) \ -X(a, STATIC, SINGULAR, FIXED64, last_updated, 4) +X(a, STATIC, SINGULAR, BYTES, scope_id, 2) \ +X(a, STATIC, SINGULAR, UENUM, scope_type, 3) \ +X(a, STATIC, SINGULAR, UENUM, sync_direction, 4) \ +X(a, STATIC, OPTIONAL, FIXED64, last_updated, 5) #define particle_cloud_ledger_GetInfoResponse_Ledger_CALLBACK NULL #define particle_cloud_ledger_GetInfoResponse_Ledger_DEFAULT NULL #define particle_cloud_ledger_SetDataRequest_FIELDLIST(X, a) \ X(a, STATIC, SINGULAR, STRING, name, 1) \ -X(a, CALLBACK, SINGULAR, BYTES, data, 2) \ -X(a, STATIC, SINGULAR, FIXED64, last_updated, 3) +X(a, STATIC, SINGULAR, BYTES, scope_id, 2) \ +X(a, STATIC, OPTIONAL, FIXED64, last_updated, 3) \ +X(a, CALLBACK, SINGULAR, BYTES, data, 10) #define particle_cloud_ledger_SetDataRequest_CALLBACK pb_default_field_callback #define particle_cloud_ledger_SetDataRequest_DEFAULT NULL @@ -236,20 +285,28 @@ X(a, STATIC, SINGULAR, FIXED64, last_updated, 3) #define particle_cloud_ledger_GetDataRequest_FIELDLIST(X, a) \ X(a, STATIC, SINGULAR, STRING, name, 1) \ -X(a, STATIC, OPTIONAL, FIXED64, last_updated, 2) +X(a, STATIC, SINGULAR, BYTES, scope_id, 2) \ +X(a, STATIC, OPTIONAL, FIXED64, last_updated, 3) #define particle_cloud_ledger_GetDataRequest_CALLBACK NULL #define particle_cloud_ledger_GetDataRequest_DEFAULT NULL #define particle_cloud_ledger_GetDataResponse_FIELDLIST(X, a) \ -X(a, CALLBACK, OPTIONAL, BYTES, data, 1) \ -X(a, STATIC, SINGULAR, FIXED64, last_updated, 2) +X(a, STATIC, OPTIONAL, FIXED64, last_updated, 1) \ +X(a, CALLBACK, OPTIONAL, BYTES, data, 10) #define particle_cloud_ledger_GetDataResponse_CALLBACK pb_default_field_callback #define particle_cloud_ledger_GetDataResponse_DEFAULT NULL #define particle_cloud_ledger_SubscribeRequest_FIELDLIST(X, a) \ -X(a, CALLBACK, REPEATED, STRING, ledgers, 1) +X(a, CALLBACK, REPEATED, MESSAGE, ledgers, 1) #define particle_cloud_ledger_SubscribeRequest_CALLBACK pb_default_field_callback #define particle_cloud_ledger_SubscribeRequest_DEFAULT NULL +#define particle_cloud_ledger_SubscribeRequest_ledgers_MSGTYPE particle_cloud_ledger_SubscribeRequest_Ledger + +#define particle_cloud_ledger_SubscribeRequest_Ledger_FIELDLIST(X, a) \ +X(a, STATIC, SINGULAR, STRING, name, 1) \ +X(a, STATIC, SINGULAR, BYTES, scope_id, 2) +#define particle_cloud_ledger_SubscribeRequest_Ledger_CALLBACK NULL +#define particle_cloud_ledger_SubscribeRequest_Ledger_DEFAULT NULL #define particle_cloud_ledger_SubscribeResponse_FIELDLIST(X, a) \ X(a, CALLBACK, REPEATED, MESSAGE, ledgers, 1) @@ -259,21 +316,37 @@ X(a, CALLBACK, REPEATED, MESSAGE, ledgers, 1) #define particle_cloud_ledger_SubscribeResponse_Ledger_FIELDLIST(X, a) \ X(a, STATIC, SINGULAR, STRING, name, 1) \ -X(a, STATIC, SINGULAR, FIXED64, last_updated, 2) +X(a, STATIC, OPTIONAL, FIXED64, last_updated, 2) #define particle_cloud_ledger_SubscribeResponse_Ledger_CALLBACK NULL #define particle_cloud_ledger_SubscribeResponse_Ledger_DEFAULT NULL #define particle_cloud_ledger_NotifyUpdateRequest_FIELDLIST(X, a) \ +X(a, CALLBACK, REPEATED, MESSAGE, ledgers, 1) +#define particle_cloud_ledger_NotifyUpdateRequest_CALLBACK pb_default_field_callback +#define particle_cloud_ledger_NotifyUpdateRequest_DEFAULT NULL +#define particle_cloud_ledger_NotifyUpdateRequest_ledgers_MSGTYPE particle_cloud_ledger_NotifyUpdateRequest_Ledger + +#define particle_cloud_ledger_NotifyUpdateRequest_Ledger_FIELDLIST(X, a) \ X(a, STATIC, SINGULAR, STRING, name, 1) \ X(a, STATIC, SINGULAR, FIXED64, last_updated, 2) -#define particle_cloud_ledger_NotifyUpdateRequest_CALLBACK NULL -#define particle_cloud_ledger_NotifyUpdateRequest_DEFAULT NULL +#define particle_cloud_ledger_NotifyUpdateRequest_Ledger_CALLBACK NULL +#define particle_cloud_ledger_NotifyUpdateRequest_Ledger_DEFAULT NULL #define particle_cloud_ledger_NotifyUpdateResponse_FIELDLIST(X, a) \ #define particle_cloud_ledger_NotifyUpdateResponse_CALLBACK NULL #define particle_cloud_ledger_NotifyUpdateResponse_DEFAULT NULL +#define particle_cloud_ledger_ResetInfoRequest_FIELDLIST(X, a) \ + +#define particle_cloud_ledger_ResetInfoRequest_CALLBACK NULL +#define particle_cloud_ledger_ResetInfoRequest_DEFAULT NULL + +#define particle_cloud_ledger_ResetInfoResponse_FIELDLIST(X, a) \ + +#define particle_cloud_ledger_ResetInfoResponse_CALLBACK NULL +#define particle_cloud_ledger_ResetInfoResponse_DEFAULT NULL + extern const pb_msgdesc_t particle_cloud_ledger_GetInfoRequest_msg; extern const pb_msgdesc_t particle_cloud_ledger_GetInfoResponse_msg; extern const pb_msgdesc_t particle_cloud_ledger_GetInfoResponse_Ledger_msg; @@ -282,10 +355,14 @@ extern const pb_msgdesc_t particle_cloud_ledger_SetDataResponse_msg; extern const pb_msgdesc_t particle_cloud_ledger_GetDataRequest_msg; extern const pb_msgdesc_t particle_cloud_ledger_GetDataResponse_msg; extern const pb_msgdesc_t particle_cloud_ledger_SubscribeRequest_msg; +extern const pb_msgdesc_t particle_cloud_ledger_SubscribeRequest_Ledger_msg; extern const pb_msgdesc_t particle_cloud_ledger_SubscribeResponse_msg; extern const pb_msgdesc_t particle_cloud_ledger_SubscribeResponse_Ledger_msg; extern const pb_msgdesc_t particle_cloud_ledger_NotifyUpdateRequest_msg; +extern const pb_msgdesc_t particle_cloud_ledger_NotifyUpdateRequest_Ledger_msg; extern const pb_msgdesc_t particle_cloud_ledger_NotifyUpdateResponse_msg; +extern const pb_msgdesc_t particle_cloud_ledger_ResetInfoRequest_msg; +extern const pb_msgdesc_t particle_cloud_ledger_ResetInfoResponse_msg; /* Defines for backwards compatibility with code written before nanopb-0.4.0 */ #define particle_cloud_ledger_GetInfoRequest_fields &particle_cloud_ledger_GetInfoRequest_msg @@ -296,10 +373,14 @@ extern const pb_msgdesc_t particle_cloud_ledger_NotifyUpdateResponse_msg; #define particle_cloud_ledger_GetDataRequest_fields &particle_cloud_ledger_GetDataRequest_msg #define particle_cloud_ledger_GetDataResponse_fields &particle_cloud_ledger_GetDataResponse_msg #define particle_cloud_ledger_SubscribeRequest_fields &particle_cloud_ledger_SubscribeRequest_msg +#define particle_cloud_ledger_SubscribeRequest_Ledger_fields &particle_cloud_ledger_SubscribeRequest_Ledger_msg #define particle_cloud_ledger_SubscribeResponse_fields &particle_cloud_ledger_SubscribeResponse_msg #define particle_cloud_ledger_SubscribeResponse_Ledger_fields &particle_cloud_ledger_SubscribeResponse_Ledger_msg #define particle_cloud_ledger_NotifyUpdateRequest_fields &particle_cloud_ledger_NotifyUpdateRequest_msg +#define particle_cloud_ledger_NotifyUpdateRequest_Ledger_fields &particle_cloud_ledger_NotifyUpdateRequest_Ledger_msg #define particle_cloud_ledger_NotifyUpdateResponse_fields &particle_cloud_ledger_NotifyUpdateResponse_msg +#define particle_cloud_ledger_ResetInfoRequest_fields &particle_cloud_ledger_ResetInfoRequest_msg +#define particle_cloud_ledger_ResetInfoResponse_fields &particle_cloud_ledger_ResetInfoResponse_msg /* Maximum encoded size of messages (where known) */ /* particle_cloud_ledger_GetInfoRequest_size depends on runtime parameters */ @@ -308,11 +389,15 @@ extern const pb_msgdesc_t particle_cloud_ledger_NotifyUpdateResponse_msg; /* particle_cloud_ledger_GetDataResponse_size depends on runtime parameters */ /* particle_cloud_ledger_SubscribeRequest_size depends on runtime parameters */ /* particle_cloud_ledger_SubscribeResponse_size depends on runtime parameters */ -#define particle_cloud_ledger_GetDataRequest_size 43 -#define particle_cloud_ledger_GetInfoResponse_Ledger_size 47 -#define particle_cloud_ledger_NotifyUpdateRequest_size 43 +/* particle_cloud_ledger_NotifyUpdateRequest_size depends on runtime parameters */ +#define particle_cloud_ledger_GetDataRequest_size 77 +#define particle_cloud_ledger_GetInfoResponse_Ledger_size 81 +#define particle_cloud_ledger_NotifyUpdateRequest_Ledger_size 43 #define particle_cloud_ledger_NotifyUpdateResponse_size 0 +#define particle_cloud_ledger_ResetInfoRequest_size 0 +#define particle_cloud_ledger_ResetInfoResponse_size 0 #define particle_cloud_ledger_SetDataResponse_size 0 +#define particle_cloud_ledger_SubscribeRequest_Ledger_size 68 #define particle_cloud_ledger_SubscribeResponse_Ledger_size 43 #ifdef __cplusplus diff --git a/proto_defs/src/control/wifi_new.pb.h b/proto_defs/src/control/wifi_new.pb.h index d5461d4666..de1bc20a23 100644 --- a/proto_defs/src/control/wifi_new.pb.h +++ b/proto_defs/src/control/wifi_new.pb.h @@ -22,7 +22,9 @@ typedef enum _particle_ctrl_wifi_Security { particle_ctrl_wifi_Security_WEP = 1, /* WEP */ particle_ctrl_wifi_Security_WPA_PSK = 2, /* WPA PSK */ particle_ctrl_wifi_Security_WPA2_PSK = 3, /* WPA2 PSK */ - particle_ctrl_wifi_Security_WPA_WPA2_PSK = 4 /* WPA/WPA2 PSK */ + particle_ctrl_wifi_Security_WPA_WPA2_PSK = 4, /* WPA/WPA2 PSK */ + particle_ctrl_wifi_Security_WPA3_PSK = 5, /* WPA3 PSK */ + particle_ctrl_wifi_Security_WPA2_WPA3_PSK = 6 /* WPA2/WPA3 PSK */ } particle_ctrl_wifi_Security; /* * @@ -139,13 +141,14 @@ typedef struct _particle_ctrl_wifi_JoinNewNetworkRequest { particle_ctrl_wifi_Security security; /* Network security */ particle_ctrl_wifi_Credentials credentials; /* Network credentials */ particle_ctrl_Interface interface_config; /* Network interface configuration (IP, mask, DNS etc) */ + bool hidden; } particle_ctrl_wifi_JoinNewNetworkRequest; /* Helper constants for enums */ #define _particle_ctrl_wifi_Security_MIN particle_ctrl_wifi_Security_NO_SECURITY -#define _particle_ctrl_wifi_Security_MAX particle_ctrl_wifi_Security_WPA_WPA2_PSK -#define _particle_ctrl_wifi_Security_ARRAYSIZE ((particle_ctrl_wifi_Security)(particle_ctrl_wifi_Security_WPA_WPA2_PSK+1)) +#define _particle_ctrl_wifi_Security_MAX particle_ctrl_wifi_Security_WPA2_WPA3_PSK +#define _particle_ctrl_wifi_Security_ARRAYSIZE ((particle_ctrl_wifi_Security)(particle_ctrl_wifi_Security_WPA2_WPA3_PSK+1)) #define _particle_ctrl_wifi_CredentialsType_MIN particle_ctrl_wifi_CredentialsType_NO_CREDENTIALS #define _particle_ctrl_wifi_CredentialsType_MAX particle_ctrl_wifi_CredentialsType_PASSWORD @@ -158,7 +161,7 @@ extern "C" { /* Initializer values for message structs */ #define particle_ctrl_wifi_Credentials_init_default {_particle_ctrl_wifi_CredentialsType_MIN, {{NULL}, NULL}} -#define particle_ctrl_wifi_JoinNewNetworkRequest_init_default {{{NULL}, NULL}, {0, {0}}, _particle_ctrl_wifi_Security_MIN, particle_ctrl_wifi_Credentials_init_default, particle_ctrl_Interface_init_default} +#define particle_ctrl_wifi_JoinNewNetworkRequest_init_default {{{NULL}, NULL}, {0, {0}}, _particle_ctrl_wifi_Security_MIN, particle_ctrl_wifi_Credentials_init_default, particle_ctrl_Interface_init_default, 0} #define particle_ctrl_wifi_JoinNewNetworkReply_init_default {0} #define particle_ctrl_wifi_JoinKnownNetworkRequest_init_default {{{NULL}, NULL}} #define particle_ctrl_wifi_JoinKnownNetworkReply_init_default {0} @@ -175,7 +178,7 @@ extern "C" { #define particle_ctrl_wifi_ScanNetworksReply_init_default {{{NULL}, NULL}} #define particle_ctrl_wifi_ScanNetworksReply_Network_init_default {{{NULL}, NULL}, {0, {0}}, _particle_ctrl_wifi_Security_MIN, 0, 0} #define particle_ctrl_wifi_Credentials_init_zero {_particle_ctrl_wifi_CredentialsType_MIN, {{NULL}, NULL}} -#define particle_ctrl_wifi_JoinNewNetworkRequest_init_zero {{{NULL}, NULL}, {0, {0}}, _particle_ctrl_wifi_Security_MIN, particle_ctrl_wifi_Credentials_init_zero, particle_ctrl_Interface_init_zero} +#define particle_ctrl_wifi_JoinNewNetworkRequest_init_zero {{{NULL}, NULL}, {0, {0}}, _particle_ctrl_wifi_Security_MIN, particle_ctrl_wifi_Credentials_init_zero, particle_ctrl_Interface_init_zero, 0} #define particle_ctrl_wifi_JoinNewNetworkReply_init_zero {0} #define particle_ctrl_wifi_JoinKnownNetworkRequest_init_zero {{{NULL}, NULL}} #define particle_ctrl_wifi_JoinKnownNetworkReply_init_zero {0} @@ -216,6 +219,7 @@ extern "C" { #define particle_ctrl_wifi_JoinNewNetworkRequest_security_tag 3 #define particle_ctrl_wifi_JoinNewNetworkRequest_credentials_tag 4 #define particle_ctrl_wifi_JoinNewNetworkRequest_interface_config_tag 5 +#define particle_ctrl_wifi_JoinNewNetworkRequest_hidden_tag 6 /* Struct field encoding specification for nanopb */ #define particle_ctrl_wifi_Credentials_FIELDLIST(X, a) \ @@ -229,7 +233,8 @@ X(a, CALLBACK, SINGULAR, STRING, ssid, 1) \ X(a, STATIC, SINGULAR, BYTES, bssid, 2) \ X(a, STATIC, SINGULAR, UENUM, security, 3) \ X(a, STATIC, SINGULAR, MESSAGE, credentials, 4) \ -X(a, STATIC, SINGULAR, MESSAGE, interface_config, 5) +X(a, STATIC, SINGULAR, MESSAGE, interface_config, 5) \ +X(a, STATIC, SINGULAR, BOOL, hidden, 6) #define particle_ctrl_wifi_JoinNewNetworkRequest_CALLBACK pb_default_field_callback #define particle_ctrl_wifi_JoinNewNetworkRequest_DEFAULT NULL #define particle_ctrl_wifi_JoinNewNetworkRequest_credentials_MSGTYPE particle_ctrl_wifi_Credentials diff --git a/proto_defs/src/ledger.pb.h b/proto_defs/src/ledger.pb.h index 6ed3808028..5082a55f8b 100644 --- a/proto_defs/src/ledger.pb.h +++ b/proto_defs/src/ledger.pb.h @@ -11,23 +11,28 @@ #endif /* Struct definitions */ +typedef PB_BYTES_ARRAY_T(32) particle_firmware_LedgerInfo_scope_id_t; /* * Ledger info. */ typedef struct _particle_firmware_LedgerInfo { char name[33]; /* /< Ledger name. */ - particle_cloud_ledger_Scope scope; /* /< Ledger scope. */ + particle_firmware_LedgerInfo_scope_id_t scope_id; /* /< Scope ID. */ + particle_cloud_ledger_ScopeType scope_type; /* /< Scope type. */ particle_cloud_ledger_SyncDirection sync_direction; /* /< Sync direction. */ /* * Time the ledger was last updated, in milliseconds since the Unix epoch. - If 0, the time is unknown. */ + If not set, the time is unknown. */ + bool has_last_updated; uint64_t last_updated; /* * Time the ledger was last synchronized with the Cloud, in milliseconds since the Unix epoch. - If 0, the ledger has never been synchronized. */ + If not set, the ledger has never been synchronized. */ + bool has_last_synced; uint64_t last_synced; - bool sync_pending; /* /< Whether the ledger has local changes that need to be synchronized. */ + uint32_t update_count; /* /< Counter incremented every time the ledger is updated. */ + bool sync_pending; /* /< Whether the ledger needs to be synchronized. */ } particle_firmware_LedgerInfo; @@ -36,25 +41,29 @@ extern "C" { #endif /* Initializer values for message structs */ -#define particle_firmware_LedgerInfo_init_default {"", _particle_cloud_ledger_Scope_MIN, _particle_cloud_ledger_SyncDirection_MIN, 0, 0, 0} -#define particle_firmware_LedgerInfo_init_zero {"", _particle_cloud_ledger_Scope_MIN, _particle_cloud_ledger_SyncDirection_MIN, 0, 0, 0} +#define particle_firmware_LedgerInfo_init_default {"", {0, {0}}, _particle_cloud_ledger_ScopeType_MIN, _particle_cloud_ledger_SyncDirection_MIN, false, 0, false, 0, 0, 0} +#define particle_firmware_LedgerInfo_init_zero {"", {0, {0}}, _particle_cloud_ledger_ScopeType_MIN, _particle_cloud_ledger_SyncDirection_MIN, false, 0, false, 0, 0, 0} /* Field tags (for use in manual encoding/decoding) */ #define particle_firmware_LedgerInfo_name_tag 1 -#define particle_firmware_LedgerInfo_scope_tag 2 -#define particle_firmware_LedgerInfo_sync_direction_tag 3 -#define particle_firmware_LedgerInfo_last_updated_tag 4 -#define particle_firmware_LedgerInfo_last_synced_tag 5 -#define particle_firmware_LedgerInfo_sync_pending_tag 6 +#define particle_firmware_LedgerInfo_scope_id_tag 2 +#define particle_firmware_LedgerInfo_scope_type_tag 3 +#define particle_firmware_LedgerInfo_sync_direction_tag 4 +#define particle_firmware_LedgerInfo_last_updated_tag 5 +#define particle_firmware_LedgerInfo_last_synced_tag 6 +#define particle_firmware_LedgerInfo_update_count_tag 7 +#define particle_firmware_LedgerInfo_sync_pending_tag 8 /* Struct field encoding specification for nanopb */ #define particle_firmware_LedgerInfo_FIELDLIST(X, a) \ X(a, STATIC, SINGULAR, STRING, name, 1) \ -X(a, STATIC, SINGULAR, UENUM, scope, 2) \ -X(a, STATIC, SINGULAR, UENUM, sync_direction, 3) \ -X(a, STATIC, SINGULAR, FIXED64, last_updated, 4) \ -X(a, STATIC, SINGULAR, FIXED64, last_synced, 5) \ -X(a, STATIC, SINGULAR, BOOL, sync_pending, 6) +X(a, STATIC, SINGULAR, BYTES, scope_id, 2) \ +X(a, STATIC, SINGULAR, UENUM, scope_type, 3) \ +X(a, STATIC, SINGULAR, UENUM, sync_direction, 4) \ +X(a, STATIC, OPTIONAL, FIXED64, last_updated, 5) \ +X(a, STATIC, OPTIONAL, FIXED64, last_synced, 6) \ +X(a, STATIC, SINGULAR, UINT32, update_count, 7) \ +X(a, STATIC, SINGULAR, BOOL, sync_pending, 8) #define particle_firmware_LedgerInfo_CALLBACK NULL #define particle_firmware_LedgerInfo_DEFAULT NULL @@ -64,7 +73,7 @@ extern const pb_msgdesc_t particle_firmware_LedgerInfo_msg; #define particle_firmware_LedgerInfo_fields &particle_firmware_LedgerInfo_msg /* Maximum encoded size of messages (where known) */ -#define particle_firmware_LedgerInfo_size 58 +#define particle_firmware_LedgerInfo_size 98 #ifdef __cplusplus } /* extern "C" */ diff --git a/services/inc/nanopb_misc.h b/services/inc/nanopb_misc.h index 5b5869d4b5..8ddd2ee035 100644 --- a/services/inc/nanopb_misc.h +++ b/services/inc/nanopb_misc.h @@ -28,6 +28,7 @@ extern "C" { #endif // __cplusplus typedef struct lfs_file lfs_file_t; +typedef struct coap_message coap_message; pb_ostream_t* pb_ostream_init(void* reserved); bool pb_ostream_free(pb_ostream_t* stream, void* reserved); @@ -43,6 +44,10 @@ int pb_ostream_from_file(pb_ostream_t* stream, lfs_file_t* file, void* reserved) int pb_istream_from_file(pb_istream_t* stream, lfs_file_t* file, int size, void* reserved); #endif // HAL_PLATFORM_FILESYSTEM +// These functions can only be used if the payload data fits in one CoAP message +int pb_istream_from_coap_message(pb_istream_t* stream, coap_message* msg, void* reserved); +int pb_ostream_from_coap_message(pb_ostream_t* stream, coap_message* msg, void* reserved); + #ifdef SERVICES_NO_NANOPB_LIB #pragma weak pb_ostream_init #pragma weak pb_ostream_free diff --git a/services/inc/preprocessor.h b/services/inc/preprocessor.h index c7fb220275..5c4a56f0c6 100644 --- a/services/inc/preprocessor.h +++ b/services/inc/preprocessor.h @@ -69,7 +69,7 @@ /* PP_COUNT(...) - Expands to a number of arguments. This macro supports up to 98 arguments. + Expands to a number of arguments. PP_COUNT(a, b) // Expands to 2 */ @@ -89,11 +89,11 @@ _61, _62, _63, _64, _65, _66, _67, _68, _69, _70, \ _71, _72, _73, _74, _75, _76, _77, _78, _79, _80, \ _81, _82, _83, _84, _85, _86, _87, _88, _89, _90, \ - _91, _92, _93, _94, _95, _96, _97, _98, \ + _91, _92, _93, _94, _95, _96, _97, _98, _99, \ n, ...) n #define _PP_COUNT_N \ - 98, 97, 96, 95, 94, 93, 92, 91, \ + 99, 98, 97, 96, 95, 94, 93, 92, 91, \ 90, 89, 88, 87, 86, 85, 84, 83, 82, 81, \ 80, 79, 78, 77, 76, 75, 74, 73, 72, 71, \ 70, 69, 68, 67, 66, 65, 64, 63, 62, 61, \ @@ -107,7 +107,7 @@ /* PP_FOR_EACH(macro, data, ...) - Expands macro for each argument. This macro supports up to 30 arguments. + Expands macro for each argument. #define CALL(func, arg) func(arg); PP_FOR_EACH(CALL, foo, 1, 2, 3) // Expands to foo(1); foo(2); foo(3); @@ -320,5 +320,7 @@ _PP_FOR_EACH_97(m, d, _1, _2, _3, _4, _5, _6, _7, _8, _9, _10, _11, _12, _13, _14, _15, _16, _17, _18, _19, _20, _21, _22, _23, _24, _25, _26, _27, _28, _29, _30, _31, _32, _33, _34, _35, _36, _37, _38, _39, _40, _41, _42, _43, _44, _45, _46, _47, _48, _49, _50, _51, _52, _53, _54, _55, _56, _57, _58, _59, _60, _61, _62, _63, _64, _65, _66, _67, _68, _69, _70, _71, _72, _73, _74, _75, _76, _77, _78, _79, _80, _81, _82, _83, _84, _85, _86, _87, _88, _89, _90, _91, _92, _93, _94, _95, _96, _97) m(d, _98) #define _PP_FOR_EACH_99(m, d, _1, _2, _3, _4, _5, _6, _7, _8, _9, _10, _11, _12, _13, _14, _15, _16, _17, _18, _19, _20, _21, _22, _23, _24, _25, _26, _27, _28, _29, _30, _31, _32, _33, _34, _35, _36, _37, _38, _39, _40, _41, _42, _43, _44, _45, _46, _47, _48, _49, _50, _51, _52, _53, _54, _55, _56, _57, _58, _59, _60, _61, _62, _63, _64, _65, _66, _67, _68, _69, _70, _71, _72, _73, _74, _75, _76, _77, _78, _79, _80, _81, _82, _83, _84, _85, _86, _87, _88, _89, _90, _91, _92, _93, _94, _95, _96, _97, _98, _99) \ _PP_FOR_EACH_98(m, d, _1, _2, _3, _4, _5, _6, _7, _8, _9, _10, _11, _12, _13, _14, _15, _16, _17, _18, _19, _20, _21, _22, _23, _24, _25, _26, _27, _28, _29, _30, _31, _32, _33, _34, _35, _36, _37, _38, _39, _40, _41, _42, _43, _44, _45, _46, _47, _48, _49, _50, _51, _52, _53, _54, _55, _56, _57, _58, _59, _60, _61, _62, _63, _64, _65, _66, _67, _68, _69, _70, _71, _72, _73, _74, _75, _76, _77, _78, _79, _80, _81, _82, _83, _84, _85, _86, _87, _88, _89, _90, _91, _92, _93, _94, _95, _96, _97, _98) m(d, _99) +#define _PP_FOR_EACH_100(m, d, _1, _2, _3, _4, _5, _6, _7, _8, _9, _10, _11, _12, _13, _14, _15, _16, _17, _18, _19, _20, _21, _22, _23, _24, _25, _26, _27, _28, _29, _30, _31, _32, _33, _34, _35, _36, _37, _38, _39, _40, _41, _42, _43, _44, _45, _46, _47, _48, _49, _50, _51, _52, _53, _54, _55, _56, _57, _58, _59, _60, _61, _62, _63, _64, _65, _66, _67, _68, _69, _70, _71, _72, _73, _74, _75, _76, _77, _78, _79, _80, _81, _82, _83, _84, _85, _86, _87, _88, _89, _90, _91, _92, _93, _94, _95, _96, _97, _98, _99, _100) \ + _PP_FOR_EACH_99(m, d, _1, _2, _3, _4, _5, _6, _7, _8, _9, _10, _11, _12, _13, _14, _15, _16, _17, _18, _19, _20, _21, _22, _23, _24, _25, _26, _27, _28, _29, _30, _31, _32, _33, _34, _35, _36, _37, _38, _39, _40, _41, _42, _43, _44, _45, _46, _47, _48, _49, _50, _51, _52, _53, _54, _55, _56, _57, _58, _59, _60, _61, _62, _63, _64, _65, _66, _67, _68, _69, _70, _71, _72, _73, _74, _75, _76, _77, _78, _79, _80, _81, _82, _83, _84, _85, _86, _87, _88, _89, _90, _91, _92, _93, _94, _95, _96, _97, _98) m(d, _100) #endif // _PREPROCESSOR_H diff --git a/services/inc/ref_count.h b/services/inc/ref_count.h index 713e4f1263..7e2b158273 100644 --- a/services/inc/ref_count.h +++ b/services/inc/ref_count.h @@ -17,6 +17,7 @@ #pragma once +#include #include #include @@ -27,7 +28,6 @@ namespace particle { /** * Base class for reference counted objects. */ -template class RefCount { public: RefCount() : @@ -106,6 +106,11 @@ class RefCountPtr { return *this; } + template>> + operator RefCountPtr() const { + return RefCountPtr(p_); + } + explicit operator bool() const { return p_; } @@ -132,7 +137,12 @@ class RefCountPtr { template inline RefCountPtr makeRefCountPtr(ArgsT&&... args) { - return RefCountPtr::wrap(new T(std::forward(args)...)); + return RefCountPtr::wrap(new(std::nothrow) T(std::forward(args)...)); +} + +template +inline RefCountPtr staticPtrCast(const RefCountPtr& ptr) { + return static_cast(ptr.get()); } } // namespace particle diff --git a/services/inc/system_error.h b/services/inc/system_error.h index daa82f51c2..b6e71be4bf 100644 --- a/services/inc/system_error.h +++ b/services/inc/system_error.h @@ -49,9 +49,15 @@ (NO_MEMORY, "Memory allocation error", -260), \ (INVALID_ARGUMENT, "Invalid argument", -270), \ (BAD_DATA, "Invalid data format", -280), \ + (ENCODING_FAILED, "Encoding error", -281), \ (OUT_OF_RANGE, "Out of range", -290), \ (DEPRECATED, "Deprecated", -300), \ (COAP, "CoAP error", -1000), /* -1199 ... -1000: CoAP errors */ \ + (COAP_CONNECTION_CLOSED, "Connection closed", -1001), \ + (COAP_MESSAGE_RESET, "Received a RST message", -1002), \ + (COAP_TIMEOUT, "CoAP timeout", -1003), \ + (COAP_REQUEST_NOT_FOUND, "Request not found", -1004), \ + (COAP_REQUEST_CANCELLED, "Request was cancelled", -1005), \ (COAP_4XX, "CoAP: 4xx", -1100), \ (COAP_5XX, "CoAP: 5xx", -1132), \ (AT_NOT_OK, "AT command failure", -1200), /* -1299 ... -1200: AT command errors */ \ @@ -103,16 +109,19 @@ (FILESYSTEM_INVAL, "Invalid parameter", -1910), \ (FILESYSTEM_NOSPC, "No space left in the filesystem", -1911), \ (FILESYSTEM_NOMEM, "Memory allocation error", -1912), \ - (LEDGER, "Ledger error", -2000), /* -2099 ... -2000: Ledger errors */ \ - (LEDGER_NOT_FOUND, "Ledger not found", -2001), \ - (LEDGER_INVALID_FORMAT, "Invalid format of ledger data", -2002), \ - (LEDGER_UNSUPPORTED_FORMAT, "Unsupported format of ledger data", -2003), \ - (LEDGER_READ_ONLY, "Ledger is read only", -2004), \ - (LEDGER_IN_USE, "Ledger is in use", -2005), \ - (LEDGER_TOO_LARGE, "Ledger data is too large", -2006), \ - (LEDGER_INCONSISTENT, "Inconsistent ledger state", -2007), \ - (LEDGER_ENCODING, "Ledger encoding error", -2008), \ - (LEDGER_DECODING, "Ledger decoding error", -2009) + (LEDGER_NOT_FOUND, "Ledger not found", -2000), /* -2099 ... -2000: Ledger errors */ \ + (LEDGER_INVALID_FORMAT, "Invalid format of ledger data", -2001), \ + (LEDGER_UNSUPPORTED_FORMAT, "Unsupported format of ledger data", -2002), \ + (LEDGER_READ_ONLY, "Ledger is read only", -2003), \ + (LEDGER_IN_USE, "Ledger is in use", -2004), \ + (LEDGER_TOO_LARGE, "Ledger data is too large", -2005), \ + (LEDGER_TOO_MANY, "Too many ledgers", -2006), \ + (LEDGER_INCONSISTENT_STATE, "Inconsistent ledger state", -2007), \ + (LEDGER_ENCODING_FAILED, "Ledger encoding error", -2008), \ + (LEDGER_DECODING_FAILED, "Ledger decoding error", -2009), \ + (LEDGER_REQUEST_FAILED, "Ledger request failed", -2010), \ + (LEDGER_INVALID_RESPONSE, "Invalid response from server", -2011), \ + (HAL_RTC_INVALID_TIME, "RTC time is invalid", -3000) /* -3099 ... -3000: HAL errors */ // Expands to enum values for all errors #define SYSTEM_ERROR_ENUM_VALUES(prefix) \ diff --git a/services/inc/time_util.h b/services/inc/time_util.h new file mode 100644 index 0000000000..9c508eb700 --- /dev/null +++ b/services/inc/time_util.h @@ -0,0 +1,43 @@ +/* + * Copyright (c) 2023 Particle Industries, Inc. All rights reserved. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation, either + * version 3 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, see . + */ + +#include + +#include "rtc_hal.h" + +#include "system_error.h" + +namespace particle { + +/** + * Get the number of milliseconds elapsed since the Unix epoch. + * + * @return Number of milliseconds on success, otherwise an error code defined by `system_error_t`. + */ +inline int64_t getMillisSinceEpoch() { + if (!hal_rtc_time_is_valid(nullptr)) { + return SYSTEM_ERROR_HAL_RTC_INVALID_TIME; + } + timeval tv = {}; + int r = hal_rtc_get_time(&tv, nullptr); + if (r < 0) { + return r; + } + return tv.tv_sec * 1000ll + tv.tv_usec / 1000; +} + +} // namespace particle diff --git a/services/src/nanopb_misc.c b/services/src/nanopb_misc.c index 23ad7c0010..06a6102c9a 100644 --- a/services/src/nanopb_misc.c +++ b/services/src/nanopb_misc.c @@ -17,9 +17,12 @@ #include "nanopb_misc.h" +// FIXME: We should perhaps introduce a separate module for utilities like this +#include "../../communication/inc/coap_api.h" #include "system_error.h" #include +#include #if HAL_PLATFORM_FILESYSTEM #include "filesystem.h" @@ -50,6 +53,26 @@ static bool read_file_callback(pb_istream_t* strm, uint8_t* data, size_t size) { #endif // HAL_PLATFORM_FILESYSTEM +static bool read_coap_message_callback(pb_istream_t* strm, uint8_t* data, size_t size) { + size_t n = size; + int r = coap_read_payload((coap_message*)strm->state, data, &n, NULL /* block_cb */, NULL /* error_cb */, + NULL /* arg */, NULL /* reserved */); + if (r != 0 || n != size) { // COAP_RESULT_WAIT_BLOCK is treated as an error + return false; + } + return true; +} + +static bool write_coap_message_callback(pb_ostream_t* strm, const uint8_t* data, size_t size) { + size_t n = size; + int r = coap_write_payload((coap_message*)strm->state, data, &n, NULL /* block_cb */, NULL /* error_cb */, + NULL /* arg */, NULL /* reserved */); + if (r != 0 || n != size) { // COAP_RESULT_WAIT_BLOCK is treated as an error + return false; + } + return true; +} + pb_ostream_t* pb_ostream_init(void* reserved) { return (pb_ostream_t*)calloc(sizeof(pb_ostream_t), 1); } @@ -136,3 +159,23 @@ int pb_istream_from_file(pb_istream_t* stream, lfs_file_t* file, int size, void* } #endif // HAL_PLATFORM_FILESYSTEM + +int pb_istream_from_coap_message(pb_istream_t* stream, coap_message* msg, void* reserved) { + int size = coap_peek_payload(msg, NULL /* data */, SIZE_MAX, NULL /* reserved */); + if (size < 0) { + return size; + } + memset(stream, 0, sizeof(*stream)); + stream->callback = read_coap_message_callback; + stream->state = msg; + stream->bytes_left = size; + return 0; +} + +int pb_ostream_from_coap_message(pb_ostream_t* stream, coap_message* msg, void* reserved) { + memset(stream, 0, sizeof(*stream)); + stream->callback = write_coap_message_callback; + stream->state = msg; + stream->max_size = COAP_BLOCK_SIZE; + return 0; +} diff --git a/system/inc/system_dynalib_ledger.h b/system/inc/system_dynalib_ledger.h index 58bee41f09..ed001d99f1 100644 --- a/system/inc/system_dynalib_ledger.h +++ b/system/inc/system_dynalib_ledger.h @@ -44,6 +44,7 @@ DYNALIB_FN(11, system_ledger, ledger_read, int(ledger_stream*, char*, size_t, vo DYNALIB_FN(12, system_ledger, ledger_write, int(ledger_stream*, const char*, size_t, void*)) DYNALIB_FN(13, system_ledger, ledger_purge, int(const char*, void*)) DYNALIB_FN(14, system_ledger, ledger_purge_all, int(void*)) +DYNALIB_FN(15, system_ledger, ledger_get_names, int(char***, size_t*, void*)) DYNALIB_END(system_ledger) diff --git a/system/inc/system_ledger.h b/system/inc/system_ledger.h index 4349a3f777..b8fde781bc 100644 --- a/system/inc/system_ledger.h +++ b/system/inc/system_ledger.h @@ -29,6 +29,11 @@ */ #define LEDGER_API_VERSION 1 +/** + * Maximum length of a ledger name. + */ +#define LEDGER_MAX_NAME_LENGTH 32 + /** * Maximum size of ledger data. */ @@ -37,13 +42,11 @@ /** * Ledger instance. */ -struct ledger_instance; typedef struct ledger_instance ledger_instance; /** * Stream instance. */ -struct ledger_stream; typedef struct ledger_stream ledger_stream; /** @@ -277,10 +280,22 @@ int ledger_read(ledger_stream* stream, char* data, size_t size, void* reserved); */ int ledger_write(ledger_stream* stream, const char* data, size_t size, void* reserved); +/** + * Get the names of all local ledgers. + * + * @param[out] names Array of ledger names. The calling code is responsible for freeing the allocated + * array as well as its individual elements via `free()`. + * @param[out] count Number of elements in the array. + * @param reserved Reserved argument. Must be set to `NULL`. + * @return 0 on success, otherwise an error code defined by the `system_error_t` enum. + */ +int ledger_get_names(char*** names, size_t* count, void* reserved); + /** * Remove any local data associated with a ledger. * - * The operation will fail if the ledger with the given name is in use. + * The device must not be connected to the Cloud. The operation will fail if the ledger with the + * given name is in use. * * @note The data is not guaranteed to be removed in an irrecoverable way. * @@ -293,7 +308,8 @@ int ledger_purge(const char* name, void* reserved); /** * Remove any local data associated with existing ledgers. * - * The operation will fail if any of the existing ledgers is in use. + * The device must not be connected to the Cloud. The operation will fail if any of the existing + * ledgers is in use. * * @note The data is not guaranteed to be removed in an irrecoverable way. * diff --git a/system/src/system_ledger_internal.cpp b/system/src/ledger/ledger.cpp similarity index 76% rename from system/src/system_ledger_internal.cpp rename to system/src/ledger/ledger.cpp index 2b2a05e13f..79e15a7fbd 100644 --- a/system/src/system_ledger_internal.cpp +++ b/system/src/ledger/ledger.cpp @@ -15,22 +15,23 @@ * License along with this library; if not, see . */ +#if !defined(DEBUG_BUILD) && !defined(UNIT_TEST) +#define NDEBUG // TODO: Define NDEBUG in release builds +#endif + #include "hal_platform.h" #if HAL_PLATFORM_LEDGER #include -#include -#include -#include -#include - -#include "system_ledger_internal.h" +#include -#include "control/common.h" // FIXME: Move Protobuf utilities to another directory -#include "rtc_hal.h" +#include "ledger.h" +#include "ledger_manager.h" +#include "ledger_util.h" #include "file_util.h" +#include "time_util.h" #include "endian_util.h" #include "scope_guard.h" #include "check.h" @@ -41,23 +42,29 @@ #define PB_LEDGER(_name) particle_cloud_ledger_##_name #define PB_INTERNAL(_name) particle_firmware_##_name -static_assert(LEDGER_SCOPE_UNKNOWN == (int)PB_LEDGER(Scope_SCOPE_UNKNOWN) && - LEDGER_SCOPE_DEVICE == (int)PB_LEDGER(Scope_SCOPE_DEVICE) && - LEDGER_SCOPE_PRODUCT == (int)PB_LEDGER(Scope_SCOPE_PRODUCT) && - LEDGER_SCOPE_OWNER == (int)PB_LEDGER(Scope_SCOPE_OWNER)); - -static_assert(LEDGER_SYNC_DIRECTION_UNKNOWN == (int)PB_LEDGER(SyncDirection_SYNC_DIRECTION_UNKNOWN) && - LEDGER_SYNC_DIRECTION_DEVICE_TO_CLOUD == (int)PB_LEDGER(SyncDirection_SYNC_DIRECTION_DEVICE_TO_CLOUD) && - LEDGER_SYNC_DIRECTION_CLOUD_TO_DEVICE == (int)PB_LEDGER(SyncDirection_SYNC_DIRECTION_CLOUD_TO_DEVICE)); +LOG_SOURCE_CATEGORY("system.ledger"); namespace particle { -using control::common::EncodedString; -using control::common::DecodedCString; using fs::FsLock; namespace system { +static_assert(LEDGER_SCOPE_UNKNOWN == (int)PB_LEDGER(ScopeType_SCOPE_TYPE_UNKNOWN) && + LEDGER_SCOPE_DEVICE == (int)PB_LEDGER(ScopeType_SCOPE_TYPE_DEVICE) && + LEDGER_SCOPE_PRODUCT == (int)PB_LEDGER(ScopeType_SCOPE_TYPE_PRODUCT) && + LEDGER_SCOPE_OWNER == (int)PB_LEDGER(ScopeType_SCOPE_TYPE_OWNER)); + +static_assert(LEDGER_SYNC_DIRECTION_UNKNOWN == (int)PB_LEDGER(SyncDirection_SYNC_DIRECTION_UNKNOWN) && + LEDGER_SYNC_DIRECTION_DEVICE_TO_CLOUD == (int)PB_LEDGER(SyncDirection_SYNC_DIRECTION_DEVICE_TO_CLOUD) && + LEDGER_SYNC_DIRECTION_CLOUD_TO_DEVICE == (int)PB_LEDGER(SyncDirection_SYNC_DIRECTION_CLOUD_TO_DEVICE)); + +static_assert(LEDGER_MAX_NAME_LENGTH + 1 == sizeof(PB_INTERNAL(LedgerInfo::name)) && + LEDGER_MAX_NAME_LENGTH + 1 == sizeof(PB_LEDGER(GetInfoResponse_Ledger::name))); + +static_assert(MAX_LEDGER_SCOPE_ID_SIZE == sizeof(PB_INTERNAL(LedgerInfo_scope_id_t::bytes)) && + MAX_LEDGER_SCOPE_ID_SIZE == sizeof(PB_LEDGER(GetInfoResponse_Ledger_scope_id_t::bytes))); + namespace { /* @@ -95,14 +102,12 @@ namespace { are closed, the most recent staged data is moved to "current" and all other files in "staged" are removed. */ -const auto LEDGER_ROOT_DIR = "/usr/ledger"; const auto TEMP_DATA_DIR_NAME = "temp"; const auto STAGED_DATA_DIR_NAME = "staged"; const auto CURRENT_DATA_FILE_NAME = "current"; const unsigned DATA_FORMAT_VERSION = 1; -const size_t MAX_LEDGER_NAME_LEN = 32; // Must match the maximum length in ledger.proto const size_t MAX_PATH_LEN = 127; // Internal result codes @@ -162,52 +167,34 @@ int writeFooter(lfs_t* fs, lfs_file_t* file, size_t dataSize, size_t infoSize, i } int writeLedgerInfo(lfs_t* fs, lfs_file_t* file, const char* ledgerName, const LedgerInfo& info) { + // All fields must be set + assert(info.isScopeTypeSet() && info.isScopeIdSet() && info.isSyncDirectionSet() && info.isDataSizeSet() && + info.isLastUpdatedSet() && info.isLastSyncedSet() && info.isUpdateCountSet() && info.isSyncPendingSet()); PB_INTERNAL(LedgerInfo) pbInfo = {}; size_t n = strlcpy(pbInfo.name, ledgerName, sizeof(pbInfo.name)); if (n >= sizeof(pbInfo.name)) { - return SYSTEM_ERROR_INTERNAL; // The name is longer than specified in ledger.proto + return SYSTEM_ERROR_INTERNAL; // Name is longer than the maximum size specified in ledger.proto } - pbInfo.scope = static_cast(info.scope()); + auto& scopeId = info.scopeId(); + assert(scopeId.size <= sizeof(pbInfo.scope_id.bytes)); + std::memcpy(pbInfo.scope_id.bytes, scopeId.data, scopeId.size); + pbInfo.scope_id.size = scopeId.size; + pbInfo.scope_type = static_cast(info.scopeType()); pbInfo.sync_direction = static_cast(info.syncDirection()); - pbInfo.last_updated = info.lastUpdated(); - pbInfo.last_synced = info.lastSynced(); + if (info.lastUpdated()) { + pbInfo.last_updated = info.lastUpdated(); + pbInfo.has_last_updated = true; + } + if (info.lastSynced()) { + pbInfo.last_synced = info.lastSynced(); + pbInfo.has_last_synced = true; + } + pbInfo.update_count = info.updateCount(); pbInfo.sync_pending = info.syncPending(); n = CHECK(encodeProtobufToFile(file, &PB_INTERNAL(LedgerInfo_msg), &pbInfo)); return n; } -int formatLedgerPath(char* buf, size_t size, const char* ledgerName, const char* fmt = nullptr, ...) { - // Format the prefix part of the path - int n = snprintf(buf, size, "%s/%s/", LEDGER_ROOT_DIR, ledgerName); - if (n < 0) { - return SYSTEM_ERROR_INTERNAL; - } - size_t pos = n; - if (pos >= size) { - return SYSTEM_ERROR_PATH_TOO_LONG; - } - if (fmt) { - // Format the rest of the path - va_list args; - va_start(args, fmt); - n = vsnprintf(buf + pos, size - pos, fmt, args); - va_end(args); - if (n < 0) { - return SYSTEM_ERROR_INTERNAL; - } - pos += n; - if (pos >= size) { - return SYSTEM_ERROR_PATH_TOO_LONG; - } - } - return pos; -} - -inline int getLedgerDirPath(char* buf, size_t size, const char* ledgerName) { - CHECK(formatLedgerPath(buf, size, ledgerName)); - return 0; -} - inline int getTempDirPath(char* buf, size_t size, const char* ledgerName) { CHECK(formatLedgerPath(buf, size, ledgerName, "%s/", TEMP_DATA_DIR_NAME)); return 0; @@ -243,32 +230,11 @@ inline int getCurrentFilePath(char* buf, size_t size, const char* ledgerName) { return 0; } -// Helper functions that transform LittleFS errors to a system error -inline int renameFile(lfs_t* fs, const char* oldPath, const char* newPath) { - CHECK_FS(lfs_rename(fs, oldPath, newPath)); - return 0; -} - -inline int removeFile(lfs_t* fs, const char* path) { - CHECK_FS(lfs_remove(fs, path)); - return 0; -} - -inline int closeFile(lfs_t* fs, lfs_file_t* file) { - CHECK_FS(lfs_file_close(fs, file)); - return 0; -} - -inline int closeDir(lfs_t* fs, lfs_dir_t* dir) { - CHECK_FS(lfs_dir_close(fs, dir)); - return 0; -} - bool isLedgerNameValid(const char* name) { size_t len = 0; char c = 0; while ((c = *name++)) { - if (!(++len <= MAX_LEDGER_NAME_LEN && ((c >= 'a' && c <= 'z') || (c >= '0' && c <= '9') || c == '-'))) { + if (!(++len <= LEDGER_MAX_NAME_LENGTH && ((c >= 'a' && c <= 'z') || (c >= '0' && c <= '9') || c == '-'))) { return false; } } @@ -278,86 +244,31 @@ bool isLedgerNameValid(const char* name) { return true; } -} // namespace - -int LedgerManager::getLedger(const char* name, RefCountPtr& ledger) { - std::lock_guard lock(mutex_); - // Check if the requested ledger is already instantiated - auto found = findLedger(name); - if (found.second) { - ledger = *found.first; - return 0; - } - // Create a new instance - auto lr = makeRefCountPtr(); - if (!lr) { - return SYSTEM_ERROR_NO_MEMORY; - } - int r = lr->init(name); - if (r < 0) { - LOG(ERROR, "Failed to initialize ledger: %d", r); - return r; - } - if (!ledgers_.insert(found.first, lr.get())) { - return SYSTEM_ERROR_NO_MEMORY; - } - ledger = std::move(lr); +// Helper functions that transform LittleFS errors to a system error +inline int renameFile(lfs_t* fs, const char* oldPath, const char* newPath) { + CHECK_FS(lfs_rename(fs, oldPath, newPath)); return 0; } -int LedgerManager::removeLedgerData(const char* name) { - if (!*name) { - return SYSTEM_ERROR_INVALID_ARGUMENT; - } - auto found = findLedger(name); - if (found.second) { - return SYSTEM_ERROR_LEDGER_IN_USE; - } - char path[MAX_PATH_LEN + 1]; - CHECK(getLedgerDirPath(path, sizeof(path), name)); - CHECK(rmrf(path)); +inline int removeFile(lfs_t* fs, const char* path) { + CHECK_FS(lfs_remove(fs, path)); return 0; } -int LedgerManager::removeAllData() { - if (!ledgers_.isEmpty()) { - return SYSTEM_ERROR_LEDGER_IN_USE; - } - CHECK(rmrf(LEDGER_ROOT_DIR)); +inline int closeFile(lfs_t* fs, lfs_file_t* file) { + CHECK_FS(lfs_file_close(fs, file)); return 0; } -std::pair::ConstIterator, bool> LedgerManager::findLedger(const char* name) const { - bool found = false; - auto it = std::lower_bound(ledgers_.begin(), ledgers_.end(), name, [&found](Ledger* lr, const char* name) { - auto r = std::strcmp(lr->name(), name); - if (r == 0) { - found = true; - } - return r < 0; - }); - return std::make_pair(it, found); -} - -void LedgerManager::addLedgerRef(const LedgerBase* ledger) { - std::lock_guard lock(mutex_); - ++ledger->refCount_; -} - -void LedgerManager::releaseLedger(const LedgerBase* ledger) { - std::lock_guard lock(mutex_); - if (--ledger->refCount_ == 0) { - ledgers_.removeOne(const_cast(static_cast(ledger))); - delete ledger; - } +inline int closeDir(lfs_t* fs, lfs_dir_t* dir) { + CHECK_FS(lfs_dir_close(fs, dir)); + return 0; } -LedgerManager* LedgerManager::instance() { - static LedgerManager mgr; - return &mgr; -} +} // namespace -Ledger::Ledger() : +Ledger::Ledger(detail::LedgerSyncContext* ctx) : + LedgerBase(ctx), lastSeqNum_(0), stagedSeqNum_(0), curReaderCount_(0), @@ -366,11 +277,14 @@ Ledger::Ledger() : lastUpdated_(0), lastSynced_(0), dataSize_(0), + updateCount_(0), syncPending_(false), syncCallback_(nullptr), destroyAppData_(nullptr), appData_(nullptr), - scope_(LEDGER_SCOPE_UNKNOWN), + name_(""), + scopeId_(EMPTY_LEDGER_SCOPE_ID), + scopeType_(LEDGER_SCOPE_UNKNOWN), syncDir_(LEDGER_SYNC_DIRECTION_UNKNOWN), inited_(false) { } @@ -386,9 +300,6 @@ int Ledger::init(const char* name) { return SYSTEM_ERROR_INVALID_ARGUMENT; } name_ = name; - if (!name_) { - return SYSTEM_ERROR_NO_MEMORY; - } FsLock fs; int r = CHECK(loadLedgerInfo(fs.instance())); if (r == RESULT_CURRENT_DATA_NOT_FOUND) { @@ -418,7 +329,7 @@ int Ledger::initReader(LedgerReader& reader) { if (!inited_) { return SYSTEM_ERROR_INVALID_STATE; } - CHECK(reader.init(dataSize_, stagedSeqNum_, this)); + CHECK(reader.init(info(), stagedSeqNum_, this)); if (stagedSeqNum_ > 0) { ++stagedReaderCount_; } else { @@ -427,7 +338,7 @@ int Ledger::initReader(LedgerReader& reader) { return 0; } -int Ledger::initWriter(LedgerWriteSource src, LedgerWriter& writer) { +int Ledger::initWriter(LedgerWriter& writer, LedgerWriteSource src) { std::lock_guard lock(*this); if (!inited_) { return SYSTEM_ERROR_INVALID_STATE; @@ -444,15 +355,62 @@ int Ledger::initWriter(LedgerWriteSource src, LedgerWriter& writer) { LedgerInfo Ledger::info() const { std::lock_guard lock(*this); return LedgerInfo() - .scope(scope_) + .scopeType(scopeType_) + .scopeId(scopeId_) .syncDirection(syncDir_) .dataSize(dataSize_) .lastUpdated(lastUpdated_) .lastSynced(lastSynced_) + .updateCount(updateCount_) .syncPending(syncPending_); } -int Ledger::readerClosed(bool staged) { +int Ledger::updateInfo(const LedgerInfo& info) { + std::lock_guard lock(*this); + // Open the file with the current ledger data + char path[MAX_PATH_LEN + 1]; + CHECK(getCurrentFilePath(path, sizeof(path), name_)); + FsLock fs; + lfs_file_t file = {}; + // TODO: Rewriting parts of a file is inefficient with LittleFS. Consider using an append-only + // structure for storing ledger data + CHECK_FS(lfs_file_open(fs.instance(), &file, path, LFS_O_RDWR)); + NAMED_SCOPE_GUARD(closeFileGuard, { + int r = closeFile(fs.instance(), &file); + if (r < 0) { + LOG(ERROR, "Error while closing file: %d", r); + } + }); + // Read the footer + size_t dataSize = 0; + size_t infoSize = 0; + CHECK_FS(lfs_file_seek(fs.instance(), &file, -(int)sizeof(LedgerDataFooter), LFS_SEEK_END)); + CHECK(readFooter(fs.instance(), &file, &dataSize, &infoSize)); + // Write the info section + CHECK_FS(lfs_file_seek(fs.instance(), &file, -(int)(infoSize + sizeof(LedgerDataFooter)), LFS_SEEK_END)); + auto newInfo = this->info().update(info); + size_t newInfoSize = CHECK(writeLedgerInfo(fs.instance(), &file, name_, newInfo)); + // Write the footer + if (newInfoSize != infoSize) { + CHECK(writeFooter(fs.instance(), &file, dataSize, newInfoSize)); + size_t newFileSize = CHECK_FS(lfs_file_tell(fs.instance(), &file)); + CHECK_FS(lfs_file_truncate(fs.instance(), &file, newFileSize)); + } + closeFileGuard.dismiss(); + CHECK_FS(lfs_file_close(fs.instance(), &file)); + // Update the instance + setLedgerInfo(newInfo); + return 0; +} + +void Ledger::notifySynced() { + std::lock_guard lock(*this); + if (syncCallback_) { + syncCallback_(reinterpret_cast(this), appData_); + } +} + +int Ledger::notifyReaderClosed(bool staged) { std::lock_guard lock(*this); if (staged) { --stagedReaderCount_; @@ -492,7 +450,7 @@ int Ledger::readerClosed(bool staged) { } else if (r != stagedSeqNum_) { // The file moved wasn't the most recent one for some reason LOG(ERROR, "Staged ledger data not found"); - result = SYSTEM_ERROR_LEDGER_INCONSISTENT; + result = SYSTEM_ERROR_LEDGER_INCONSISTENT_STATE; } } if (result < 0) { @@ -508,8 +466,8 @@ int Ledger::readerClosed(bool staged) { return result; } -int Ledger::writerClosed(const LedgerInfo& info, LedgerWriteSource src, int tempSeqNum) { - std::lock_guard lock(*this); +int Ledger::notifyWriterClosed(const LedgerInfo& info, LedgerWriteSource src, int tempSeqNum) { + std::unique_lock lock(*this); FsLock fs; // Move the file where appropriate bool newStagedFile = false; @@ -534,7 +492,14 @@ int Ledger::writerClosed(const LedgerInfo& info, LedgerWriteSource src, int temp stagedSeqNum_ = tempSeqNum; ++stagedFileCount_; } - updateLedgerInfo(info); + setLedgerInfo(this->info().update(info)); + if (src == LedgerWriteSource::USER) { + // Unlock the ledger before calling into the manager to avoid a deadlock + fs.unlock(); + lock.unlock(); + LedgerManager::instance()->notifyLedgerChanged(syncContext()); + fs.lock(); // FIXME: FsLock doesn't know when it's unlocked + } return 0; } @@ -584,34 +549,31 @@ int Ledger::loadLedgerInfo(lfs_t* fs) { LOG(ERROR, "Unexpected ledger name"); return SYSTEM_ERROR_LEDGER_INVALID_FORMAT; } - scope_ = static_cast(pbInfo.scope); + assert(pbInfo.scope_id.size <= sizeof(scopeId_.data)); + std::memcpy(scopeId_.data, pbInfo.scope_id.bytes, pbInfo.scope_id.size); + scopeId_.size = pbInfo.scope_id.size; + scopeType_ = static_cast(pbInfo.scope_type); syncDir_ = static_cast(pbInfo.sync_direction); dataSize_ = dataSize; - lastUpdated_ = pbInfo.last_updated; - lastSynced_ = pbInfo.last_synced; + lastUpdated_ = pbInfo.has_last_updated ? pbInfo.last_updated : 0; + lastSynced_ = pbInfo.has_last_synced ? pbInfo.last_synced : 0; + updateCount_ = pbInfo.update_count; syncPending_ = pbInfo.sync_pending; return 0; } -void Ledger::updateLedgerInfo(const LedgerInfo& info) { - if (info.hasScope()) { - scope_ = info.scope(); - } - if (info.hasSyncDirection()) { - syncDir_ = info.syncDirection(); - } - if (info.hasDataSize()) { - dataSize_ = info.dataSize(); - } - if (info.hasLastUpdated()) { - lastUpdated_ = info.lastUpdated(); - } - if (info.hasLastSynced()) { - lastSynced_ = info.lastSynced(); - } - if (info.hasSyncPending()) { - syncPending_ = info.syncPending(); - } +void Ledger::setLedgerInfo(const LedgerInfo& info) { + // All fields must be set + assert(info.isScopeTypeSet() && info.isScopeIdSet() && info.isSyncDirectionSet() && info.isDataSizeSet() && + info.isLastUpdatedSet() && info.isLastSyncedSet() && info.isUpdateCountSet() && info.isSyncPendingSet()); + scopeType_ = info.scopeType(); + scopeId_ = info.scopeId(); + syncDir_ = info.syncDirection(); + dataSize_ = info.dataSize(); + lastUpdated_ = info.lastUpdated(); + lastSynced_ = info.lastSynced(); + updateCount_ = info.updateCount(); + syncPending_ = info.syncPending(); } int Ledger::initCurrentData(lfs_t* fs) { @@ -719,29 +681,43 @@ int Ledger::removeTempData(lfs_t* fs) { return 0; } +void LedgerBase::addRef() const { + LedgerManager::instance()->addLedgerRef(this); +} + +void LedgerBase::release() const { + LedgerManager::instance()->releaseLedger(this); +} + LedgerInfo& LedgerInfo::update(const LedgerInfo& info) { - if (info.hasScope()) { - scope_ = info.scope(); + if (info.scopeType_.has_value()) { + scopeType_ = info.scopeType_.value(); + } + if (info.scopeId_.has_value()) { + scopeId_ = info.scopeId_.value(); } - if (info.hasSyncDirection()) { - syncDir_ = info.syncDirection(); + if (info.syncDir_.has_value()) { + syncDir_ = info.syncDir_.value(); } - if (info.hasDataSize()) { - dataSize_ = info.dataSize(); + if (info.dataSize_.has_value()) { + dataSize_ = info.dataSize_.value(); } - if (info.hasLastUpdated()) { - lastUpdated_ = info.lastUpdated(); + if (info.lastUpdated_.has_value()) { + lastUpdated_ = info.lastUpdated_.value(); } - if (info.hasLastSynced()) { - lastSynced_ = info.lastSynced(); + if (info.lastSynced_.has_value()) { + lastSynced_ = info.lastSynced_.value(); } - if (info.hasSyncPending()) { - syncPending_ = info.syncPending(); + if (info.updateCount_.has_value()) { + updateCount_ = info.updateCount_.value(); + } + if (info.syncPending_.has_value()) { + syncPending_ = info.syncPending_.value(); } return *this; } -int LedgerReader::init(size_t dataSize, int stagedSeqNum, Ledger* ledger) { +int LedgerReader::init(LedgerInfo info, int stagedSeqNum, Ledger* ledger) { char path[MAX_PATH_LEN + 1]; if (stagedSeqNum > 0) { // The most recent data is staged @@ -754,7 +730,7 @@ int LedgerReader::init(size_t dataSize, int stagedSeqNum, Ledger* ledger) { FsLock fs; CHECK_FS(lfs_file_open(fs.instance(), &file_, path, LFS_O_RDONLY)); ledger_ = ledger; - dataSize_ = dataSize; + info_ = std::move(info); open_ = true; return 0; } @@ -763,7 +739,7 @@ int LedgerReader::read(char* data, size_t size) { if (!open_) { return SYSTEM_ERROR_INVALID_STATE; } - size_t bytesToRead = std::min(size, dataSize_ - dataOffs_); + size_t bytesToRead = std::min(size, info_.dataSize() - dataOffs_); if (size > 0 && bytesToRead == 0) { return SYSTEM_ERROR_END_OF_STREAM; } @@ -789,7 +765,7 @@ int LedgerReader::close(bool /* discard */) { LOG(ERROR, "Error while closing file: %d", result); } // Let the ledger flush any data - int r = ledger_->readerClosed(staged_); + int r = ledger_->notifyReaderClosed(staged_); if (r < 0) { LOG(ERROR, "Failed to flush ledger data: %d", r); return r; @@ -864,22 +840,20 @@ int LedgerWriter::close(bool discard) { }); // Prepare the updated ledger info std::lock_guard lock(*ledger_); - auto newInfo = ledger_->info().update(ledgerInfo_); + auto newInfo = ledger_->info().update(info_); newInfo.dataSize(dataSize_); // Can't be overridden - if (!ledgerInfo_.hasLastUpdated()) { - int64_t time = 0; // Time is unknown - if (hal_rtc_time_is_valid(nullptr)) { - timeval tv = {}; - int r = hal_rtc_get_time(&tv, nullptr); - if (r < 0) { - LOG(ERROR, "Failed to get current time: %d", r); - } else { - time = tv.tv_sec * 1000ll + tv.tv_usec / 1000; + newInfo.updateCount(newInfo.updateCount() + 1); // ditto + if (!info_.isLastUpdatedSet()) { + int64_t t = getMillisSinceEpoch(); + if (t < 0) { + if (t != SYSTEM_ERROR_HAL_RTC_INVALID_TIME) { + LOG(ERROR, "Failed to get current time: %d", (int)t); } + t = 0; // Current time is unknown } - newInfo.lastUpdated(time); + newInfo.lastUpdated(t); } - if (!ledgerInfo_.hasSyncPending() && src_ == LedgerWriteSource::USER) { + if (!info_.isSyncPendingSet() && src_ == LedgerWriteSource::USER) { newInfo.syncPending(true); } // Write the info section @@ -889,7 +863,7 @@ int LedgerWriter::close(bool discard) { closeFileGuard.dismiss(); CHECK_FS(lfs_file_close(fs.instance(), &file_)); // Flush the data - int r = ledger_->writerClosed(newInfo, src_, tempSeqNum_); + int r = ledger_->notifyWriterClosed(newInfo, src_, tempSeqNum_); if (r < 0) { LOG(ERROR, "Failed to flush ledger data: %d", r); return r; diff --git a/system/src/system_ledger_internal.h b/system/src/ledger/ledger.h similarity index 67% rename from system/src/system_ledger_internal.h rename to system/src/ledger/ledger.h index 724de1acf6..2ada10b48c 100644 --- a/system/src/system_ledger_internal.h +++ b/system/src/ledger/ledger.h @@ -21,41 +21,55 @@ #if HAL_PLATFORM_LEDGER -#include -#include -#include #include +#include +#include +#include #include "system_ledger.h" + #include "filesystem.h" #include "static_recursive_mutex.h" -#include "c_string.h" #include "ref_count.h" #include "system_error.h" -#include "spark_wiring_vector.h" - namespace particle::system { -class Ledger; -class LedgerInfo; +const auto LEDGER_ROOT_DIR = "/usr/ledger"; + +const size_t MAX_LEDGER_SCOPE_ID_SIZE = 32; + +namespace detail { + +class LedgerSyncContext; + +} // namespace detail + class LedgerManager; -class LedgerStream; class LedgerReader; class LedgerWriter; +class LedgerInfo; enum class LedgerWriteSource { USER, SYSTEM }; +struct LedgerScopeId { + char data[MAX_LEDGER_SCOPE_ID_SIZE]; + size_t size; +}; + +const LedgerScopeId EMPTY_LEDGER_SCOPE_ID = {}; + // The reference counter of a ledger instance is managed by the LedgerManager. We can't safely use a // regular atomic counter, such as RefCount, because the LedgerManager maintains a list of all created // ledger instances that needs to be updated when any of the instances is destroyed. Shared pointers // would work but those can't be used in dynalib interfaces class LedgerBase { public: - LedgerBase() : + explicit LedgerBase(detail::LedgerSyncContext* ctx = nullptr) : + syncCtx_(ctx), refCount_(1) { } @@ -68,48 +82,30 @@ class LedgerBase { LedgerBase& operator=(const LedgerBase&) = delete; -private: - mutable int refCount_; - - friend class LedgerManager; -}; - -class LedgerManager { -public: - LedgerManager(const LedgerBase&) = delete; - - int getLedger(const char* name, RefCountPtr& ledger); - - int removeLedgerData(const char* name); - int removeAllData(); - - LedgerManager& operator=(const LedgerManager&) = delete; +protected: + detail::LedgerSyncContext* syncContext() const { + return syncCtx_; + } - static LedgerManager* instance(); + int& refCount() const { // Called by LedgerManager + return refCount_; + } private: - Vector ledgers_; // Instantiated ledgers - mutable StaticRecursiveMutex mutex_; // Manager lock - - LedgerManager() = default; // Use LedgerManager::instance() - - std::pair::ConstIterator, bool> findLedger(const char* name) const; + detail::LedgerSyncContext* syncCtx_; // Sync context + mutable int refCount_; // Reference count - void addLedgerRef(const LedgerBase* ledger); // Called by LedgerBase::addRef() - void releaseLedger(const LedgerBase* ledger); // Called by LedgerBase::release() - - friend class LedgerBase; + friend class LedgerManager; }; class Ledger: public LedgerBase { public: - Ledger(); + explicit Ledger(detail::LedgerSyncContext* ctx = nullptr); ~Ledger(); int initReader(LedgerReader& reader); - int initWriter(LedgerWriteSource src, LedgerWriter& writer); + int initWriter(LedgerWriter& writer, LedgerWriteSource src); - // Returns a LedgerInfo object with all fields set LedgerInfo info() const; const char* name() const { @@ -140,6 +136,14 @@ class Ledger: public LedgerBase { mutex_.unlock(); } +protected: + int init(const char* name); // Called by LedgerManager + int updateInfo(const LedgerInfo& info); // ditto + void notifySynced(); // ditto + + int notifyReaderClosed(bool staged); // Called by LedgerReader + int notifyWriterClosed(const LedgerInfo& info, LedgerWriteSource src, int tempSeqNum); // Called by LedgerWriter + private: int lastSeqNum_; // Counter incremented every time the ledger is opened for writing int stagedSeqNum_; // Sequence number assigned to the most recent staged ledger data @@ -147,30 +151,27 @@ class Ledger: public LedgerBase { int stagedReaderCount_; // Number of active readers of the staged ledger data int stagedFileCount_; // Number of staged data files created - int64_t lastUpdated_; // Last time the ledger was updated - int64_t lastSynced_; // Last time the ledger was synchronized + int64_t lastUpdated_; // Time the ledger was last time updated + int64_t lastSynced_; // Time the ledger was last synchronized size_t dataSize_; // Size of the ledger data + unsigned updateCount_; // Counter incremented every time the ledger is updated bool syncPending_; // Whether the ledger has local changes that have not yet been synchronized ledger_sync_callback syncCallback_; // Callback to invoke when the ledger has been synchronized ledger_destroy_app_data_callback destroyAppData_; // Destructor for the application data void* appData_; // Application data - CString name_; // Ledger name - ledger_scope scope_; // Ledger scope + const char* name_; // Ledger name (allocated by LedgerManager) + LedgerScopeId scopeId_; // Scope ID + ledger_scope scopeType_; // Scope type ledger_sync_direction syncDir_; // Sync direction bool inited_; // Whether the ledger is initialized mutable StaticRecursiveMutex mutex_; // Ledger lock - int init(const char* name); // Called by LedgerManager - - int readerClosed(bool staged); // Called by LedgerReader - int writerClosed(const LedgerInfo& info, LedgerWriteSource src, int tempSeqNum); // Called by LedgerWriter - int loadLedgerInfo(lfs_t* fs); - void updateLedgerInfo(const LedgerInfo& info); + void setLedgerInfo(const LedgerInfo& info); int initCurrentData(lfs_t* fs); int flushStagedData(lfs_t* fs); @@ -185,17 +186,33 @@ class LedgerInfo { public: LedgerInfo() = default; - LedgerInfo& scope(ledger_scope scope) { - scope_ = scope; + LedgerInfo& scopeType(ledger_scope type) { + scopeType_ = type; return *this; } - ledger_scope scope() const { - return scope_.value_or(LEDGER_SCOPE_UNKNOWN); + ledger_scope scopeType() const { + return scopeType_.value_or(LEDGER_SCOPE_UNKNOWN); + } + + bool isScopeTypeSet() const { + return scopeType_.has_value(); } - bool hasScope() const { - return scope_.has_value(); + LedgerInfo& scopeId(LedgerScopeId id) { + scopeId_ = std::move(id); + return *this; + } + + const LedgerScopeId& scopeId() const { + if (!scopeId_.has_value()) { + return EMPTY_LEDGER_SCOPE_ID; + } + return scopeId_.value(); + } + + bool isScopeIdSet() const { + return scopeId_.has_value(); } LedgerInfo& syncDirection(ledger_sync_direction dir) { @@ -207,7 +224,7 @@ class LedgerInfo { return syncDir_.value_or(LEDGER_SYNC_DIRECTION_UNKNOWN); } - bool hasSyncDirection() const { + bool isSyncDirectionSet() const { return syncDir_.has_value(); } @@ -220,7 +237,7 @@ class LedgerInfo { return dataSize_.value_or(0); } - bool hasDataSize() const { + bool isDataSizeSet() const { return dataSize_.has_value(); } @@ -233,7 +250,7 @@ class LedgerInfo { return lastUpdated_.value_or(0); } - bool hasLastUpdated() const { + bool isLastUpdatedSet() const { return lastUpdated_.has_value(); } @@ -246,10 +263,23 @@ class LedgerInfo { return lastSynced_.value_or(0); } - bool hasLastSynced() const { + bool isLastSyncedSet() const { return lastSynced_.has_value(); } + LedgerInfo& updateCount(unsigned count) { + updateCount_ = count; + return *this; + } + + unsigned updateCount() const { + return updateCount_.value_or(0); + } + + bool isUpdateCountSet() const { + return updateCount_.has_value(); + } + LedgerInfo& syncPending(bool pending) { syncPending_ = pending; return *this; @@ -259,18 +289,25 @@ class LedgerInfo { return syncPending_.value_or(false); } - bool hasSyncPending() const { + bool isSyncPendingSet() const { return syncPending_.has_value(); } LedgerInfo& update(const LedgerInfo& info); private: - // When adding new fields, make sure to update LedgerInfo::update() and Ledger::updateLedgerInfo() + // When adding a new field, make sure to update the following methods and functions: + // LedgerInfo::update() + // Ledger::info() + // Ledger::setLedgerInfo() + // Ledger::loadLedgerInfo() + // writeLedgerInfo() + std::optional scopeId_; std::optional lastUpdated_; std::optional lastSynced_; std::optional dataSize_; - std::optional scope_; + std::optional updateCount_; + std::optional scopeType_; std::optional syncDir_; std::optional syncPending_; }; @@ -288,7 +325,6 @@ class LedgerReader: public LedgerStream { public: LedgerReader() : file_(), - dataSize_(0), dataOffs_(0), staged_(false), open_(false) { @@ -309,22 +345,31 @@ class LedgerReader: public LedgerStream { int close(bool discard = false) override; + RefCountPtr ledger() const { + return ledger_; + } + + const LedgerInfo& info() const { + return info_; + } + bool isOpen() const { return open_; } - LedgerReader& operator=(const LedgerReader&) = default; + LedgerReader& operator=(const LedgerReader&) = delete; + +protected: + int init(LedgerInfo info, int stagedSeqNum, Ledger* ledger); // Called by Ledger private: RefCountPtr ledger_; // Ledger instance + LedgerInfo info_; // Ledger info lfs_file_t file_; // File handle - size_t dataSize_; // Size of the data section of the ledger file size_t dataOffs_; // Current offset in the data section bool staged_; // Whether the data being read is staged bool open_; // Whether the reader is open - int init(size_t dataSize, int stagedSeqNum, Ledger* ledger); // Called by Ledger - friend class Ledger; }; @@ -352,12 +397,12 @@ class LedgerWriter: public LedgerStream { int write(const char* data, size_t size) override; int close(bool discard = false) override; - LedgerInfo& ledgerInfo() { - return ledgerInfo_; + void updateInfo(const LedgerInfo& info) { + info_.update(info); } - const LedgerInfo& ledgerInfo() const { - return ledgerInfo_; + RefCountPtr ledger() const { + return ledger_; } bool isOpen() const { @@ -366,26 +411,27 @@ class LedgerWriter: public LedgerStream { LedgerWriter& operator=(const LedgerWriter&) = delete; +protected: + int init(LedgerWriteSource src, int tempSeqNum, Ledger* ledger); // Called by Ledger + private: RefCountPtr ledger_; // Ledger instance - LedgerInfo ledgerInfo_; // Ledger info updates + LedgerInfo info_; // Ledger info updates lfs_file_t file_; // File handle LedgerWriteSource src_; // Who is writing to the ledger size_t dataSize_; // Size of the data written int tempSeqNum_; // Sequence number assigned to the temporary ledger data bool open_; // Whether the writer is open - int init(LedgerWriteSource src, int tempSeqNum, Ledger* ledger); // Called by Ledger - friend class Ledger; }; -inline void LedgerBase::addRef() const { - LedgerManager::instance()->addLedgerRef(this); +inline bool operator==(const LedgerScopeId& id1, const LedgerScopeId& id2) { + return id1.size == id2.size && std::memcmp(id1.data, id2.data, id1.size) == 0; } -inline void LedgerBase::release() const { - LedgerManager::instance()->releaseLedger(this); +inline bool operator!=(const LedgerScopeId& id1, const LedgerScopeId& id2) { + return !(id1 == id2); } } // namespace particle::system diff --git a/system/src/ledger/ledger_manager.cpp b/system/src/ledger/ledger_manager.cpp new file mode 100644 index 0000000000..d8c91f4cda --- /dev/null +++ b/system/src/ledger/ledger_manager.cpp @@ -0,0 +1,1565 @@ +/* + * Copyright (c) 2023 Particle Industries, Inc. All rights reserved. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation, either + * version 3 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, see . + */ + +#if !defined(DEBUG_BUILD) && !defined(UNIT_TEST) +#define NDEBUG // TODO: Define NDEBUG in release builds +#endif + +#include "hal_platform.h" + +#if HAL_PLATFORM_LEDGER + +#include +#include +#include +#include + +#include + +#include "control/common.h" // FIXME: Move Protobuf utilities to another directory +#include "ledger.h" +#include "ledger_manager.h" +#include "ledger_util.h" +#include "system_ledger.h" +#include "system_cloud.h" + +#include "timer_hal.h" + +#include "nanopb_misc.h" +#include "file_util.h" +#include "time_util.h" +#include "endian_util.h" +#include "scope_guard.h" +#include "check.h" + +#include "cloud/cloud.pb.h" +#include "cloud/ledger.pb.h" + +#define PB_CLOUD(_name) particle_cloud_##_name +#define PB_LEDGER(_name) particle_cloud_ledger_##_name + +static_assert(PB_CLOUD(Response_Result_OK) == 0); // Used by value in the code + +LOG_SOURCE_CATEGORY("system.ledger"); + +namespace particle { + +using control::common::EncodedString; +using fs::FsLock; + +namespace system { + +namespace { + +const unsigned MAX_LEDGER_COUNT = 20; + +const unsigned MIN_SYNC_DELAY = 5000; +const unsigned MAX_SYNC_DELAY = 30000; + +const unsigned MIN_RETRY_DELAY = 30000; +const unsigned MAX_RETRY_DELAY = 5 * 60000; + +const auto REQUEST_URI = "L"; +const auto REQUEST_METHOD = COAP_METHOD_POST; + +const size_t STREAM_BUFFER_SIZE = 128; + +const size_t MAX_PATH_LEN = 127; + +int encodeSetDataRequestPrefix(pb_ostream_t* stream, const char* ledgerName, const LedgerInfo& info) { + // Ledger data may not fit in a single CoAP message. Nanopb streams are synchronous so the + // request is encoded manually + if (!pb_encode_tag(stream, PB_WT_STRING, PB_LEDGER(SetDataRequest_name_tag)) || // name + !pb_encode_string(stream, (const pb_byte_t*)ledgerName, std::strlen(ledgerName))) { + return SYSTEM_ERROR_ENCODING_FAILED; + } + auto& scopeId = info.scopeId(); + if (scopeId.size && (!pb_encode_tag(stream, PB_WT_STRING, PB_LEDGER(SetDataRequest_scope_id_tag)) || // scope_id + !pb_encode_string(stream, (const pb_byte_t*)scopeId.data, scopeId.size))) { + return SYSTEM_ERROR_ENCODING_FAILED; + } + auto lastUpdated = info.lastUpdated(); + if (lastUpdated && (!pb_encode_tag(stream, PB_WT_64BIT, PB_LEDGER(SetDataRequest_last_updated_tag)) || // last_updated + !pb_encode_fixed64(stream, &lastUpdated))) { + return SYSTEM_ERROR_ENCODING_FAILED; + } + auto dataSize = info.dataSize(); + // Encode only the tag and size of the data field. The data itself is encoded by the calling code + if (dataSize && (!pb_encode_tag(stream, PB_WT_STRING, PB_LEDGER(SetDataRequest_data_tag)) || // data + !pb_encode_varint(stream, dataSize))) { + return SYSTEM_ERROR_ENCODING_FAILED; + } + return 0; +} + +int getStreamForSubmessage(coap_message* msg, pb_istream_t* stream, uint32_t fieldTag) { + pb_istream_t s = {}; + CHECK(pb_istream_from_coap_message(&s, msg, nullptr)); + for (;;) { + uint32_t tag = 0; + auto type = pb_wire_type_t(); + bool eof = false; + if (!pb_decode_tag(&s, &type, &tag, &eof)) { + if (eof) { + *stream = s; // Return an empty stream + return 0; + } + return SYSTEM_ERROR_BAD_DATA; + } + if (tag == fieldTag) { + if (type != PB_WT_STRING) { + return SYSTEM_ERROR_BAD_DATA; + } + // Can't use pb_make_string_substream() as the message may contain incomplete data + uint32_t size = 0; + if (!pb_decode_varint32(&s, &size)) { + return SYSTEM_ERROR_BAD_DATA; + } + if (s.bytes_left > size) { + s.bytes_left = size; + } + *stream = s; + return 0; + } + if (!pb_skip_field(&s, type)) { + return SYSTEM_ERROR_BAD_DATA; + } + } +} + +// Returns true if the result code returned by the server indicates that the ledger is or may no +// longer be accessible by the device +inline bool isLedgerAccessError(int result) { + return result == PB_CLOUD(Response_Result_LEDGER_NOT_FOUND) || + result == PB_CLOUD(Response_Result_LEDGER_INVALID_SYNC_DIRECTION) || + result == PB_CLOUD(Response_Result_LEDGER_SCOPE_CHANGED); +} + +inline int closeDir(lfs_t* fs, lfs_dir_t* dir) { // Transforms the LittleFS error to a system error + CHECK_FS(lfs_dir_close(fs, dir)); + return 0; +} + +} // namespace + +namespace detail { + +struct LedgerSyncContext { + char name[LEDGER_MAX_NAME_LENGTH + 1]; // Ledger name + LedgerScopeId scopeId; // Scope ID + Ledger* instance; // Ledger instance. If null, the ledger is not instantiated + ledger_sync_direction syncDir; // Sync direction + int getInfoCount; // Number of GET_INFO requests sent for this ledger + int pendingState; // Pending state flags (LedgerManager::PendingState) + bool syncPending; // Whether the ledger needs to be synchronized + bool taskRunning; // Whether an asynchronous task is running for this ledger + union { + struct { // Fields specific to a device-to-cloud ledger or a ledger with unknown sync direction + uint64_t syncTime; // When to sync the ledger (ticks) + uint64_t forcedSyncTime; // When to force-sync the ledger (ticks) + unsigned updateCount; // Value of the ledger's update counter when the sync started + }; + struct { // Fields specific to a cloud-to-device ledger + uint64_t lastUpdated; // Time the ledger was last updated (Unix time in milliseconds) + }; + }; + + LedgerSyncContext() : + name(), + scopeId(EMPTY_LEDGER_SCOPE_ID), + instance(nullptr), + syncDir(LEDGER_SYNC_DIRECTION_UNKNOWN), + getInfoCount(0), + pendingState(0), + syncPending(false), + taskRunning(false), + syncTime(0), + forcedSyncTime(0), + updateCount(0) { + } + + void updateFromLedgerInfo(const LedgerInfo& info) { + if (info.isSyncDirectionSet()) { + syncDir = info.syncDirection(); + } + if (info.isScopeIdSet()) { + scopeId = info.scopeId(); + } + if (info.isSyncPendingSet()) { + syncPending = info.syncPending(); + } + if (info.isLastUpdatedSet() && syncDir == LEDGER_SYNC_DIRECTION_CLOUD_TO_DEVICE) { + lastUpdated = info.lastUpdated(); + } + } + + void resetDeviceToCloudState() { + syncTime = 0; + forcedSyncTime = 0; + updateCount = 0; + } + + void resetCloudToDeviceState() { + lastUpdated = 0; + } +}; + +} // namespace detail + +LedgerManager::LedgerManager() : + curCtx_(nullptr), + msg_(nullptr), + nextSyncTime_(0), + retryTime_(0), + retryDelay_(0), + bytesInBuf_(0), + state_(State::NEW), + pendingState_(0), + reqId_(COAP_INVALID_REQUEST_ID), + resubscribe_(false) { +} + +LedgerManager::~LedgerManager() { + if (state_ != State::NEW) { + coap_remove_request_handler(REQUEST_URI, REQUEST_METHOD, nullptr); + coap_remove_connection_handler(connectionCallback, nullptr); + } +} + +int LedgerManager::init() { + if (state_ != State::NEW) { + return 0; // Already initialized + } + // Device must not be connected to the cloud + if (spark_cloud_flag_connected()) { + return SYSTEM_ERROR_INVALID_STATE; + } + // TODO: Allow seeking in ledger and CoAP message streams so that an intermediate buffer is not + // needed when streaming ledger data to and from the server + std::unique_ptr buf(new char[STREAM_BUFFER_SIZE]); + if (!buf) { + return SYSTEM_ERROR_NO_MEMORY; + } + // Enumerate local ledgers + LedgerSyncContexts contexts; + FsLock fs; + lfs_dir_t dir = {}; + int r = lfs_dir_open(fs.instance(), &dir, LEDGER_ROOT_DIR); + if (r == 0) { + SCOPE_GUARD({ + int r = closeDir(fs.instance(), &dir); + if (r < 0) { + LOG(ERROR, "Failed to close directory handle: %d", r); + } + }); + lfs_info entry = {}; + while ((r = lfs_dir_read(fs.instance(), &dir, &entry)) == 1) { + if (entry.type != LFS_TYPE_DIR) { + LOG(WARN, "Found unexpected entry in ledger directory"); + continue; + } + if (std::strcmp(entry.name, ".") == 0 || std::strcmp(entry.name, "..") == 0) { + continue; + } + if (contexts.size() >= (int)MAX_LEDGER_COUNT) { + LOG(ERROR, "Maximum number of ledgers reached, skipping ledger: %s", entry.name); + continue; + } + // Load the ledger info + Ledger ledger; + int r = ledger.init(entry.name); + if (r < 0) { + LOG(ERROR, "Failed to initialize ledger: %d", r); + continue; + } + // Create a sync context for the ledger + std::unique_ptr ctx(new(std::nothrow) LedgerSyncContext()); + if (!ctx) { + return SYSTEM_ERROR_NO_MEMORY; + } + size_t n = strlcpy(ctx->name, entry.name, sizeof(ctx->name)); + if (n >= sizeof(ctx->name)) { + return SYSTEM_ERROR_INTERNAL; // Name length is validated in Ledger::init() + } + auto info = ledger.info(); + ctx->scopeId = info.scopeId(); + ctx->syncDir = info.syncDirection(); + ctx->syncPending = info.syncPending(); + if (ctx->syncDir == LEDGER_SYNC_DIRECTION_CLOUD_TO_DEVICE) { + ctx->lastUpdated = info.lastUpdated(); + } + if (!contexts.append(std::move(ctx))) { + return SYSTEM_ERROR_NO_MEMORY; + } + } + CHECK_FS(r); + } else if (r != LFS_ERR_NOENT) { + CHECK_FS(r); // Forward the error + } + CHECK(coap_add_connection_handler(connectionCallback, this, nullptr /* reserved */)); + NAMED_SCOPE_GUARD(removeConnHandler, { + coap_remove_connection_handler(connectionCallback, nullptr /* reserved */); + }); + CHECK(coap_add_request_handler(REQUEST_URI, REQUEST_METHOD, 0 /* flags */, requestCallback, this, nullptr /* reserved */)); + removeConnHandler.dismiss(); + contexts_ = std::move(contexts); + buf_ = std::move(buf); + state_ = State::OFFLINE; + return 0; +} + +int LedgerManager::getLedger(RefCountPtr& ledger, const char* name, bool create) { + std::lock_guard lock(mutex_); + if (state_ == State::NEW) { + return SYSTEM_ERROR_INVALID_STATE; + } + // Check if the requested ledger is already instantiated + bool found = false; + auto it = findContext(name, found); + if (found && (*it)->instance) { + ledger = (*it)->instance; + return 0; + } + std::unique_ptr newCtx; + LedgerSyncContext* ctx = nullptr; + if (!found) { + if (!create) { + return SYSTEM_ERROR_LEDGER_NOT_FOUND; + } + if (contexts_.size() >= (int)MAX_LEDGER_COUNT) { + LOG(ERROR, "Maximum number of ledgers reached"); + return SYSTEM_ERROR_LEDGER_TOO_MANY; + } + // Create a new sync context + newCtx.reset(new(std::nothrow) LedgerSyncContext()); + if (!newCtx) { + return SYSTEM_ERROR_NO_MEMORY; + } + size_t n = strlcpy(newCtx->name, name, sizeof(newCtx->name)); + if (n >= sizeof(newCtx->name)) { + return SYSTEM_ERROR_INVALID_ARGUMENT; + } + ctx = newCtx.get(); + } else { + ctx = it->get(); + } + // Create a ledger instance + auto lr = makeRefCountPtr(ctx); + if (!lr) { + return SYSTEM_ERROR_NO_MEMORY; + } + int r = lr->init(ctx->name); + if (r < 0) { + LOG(ERROR, "Failed to initialize ledger: %d", r); + return r; + } + if (!found) { + it = contexts_.insert(it, std::move(newCtx)); + if (it == contexts_.end()) { + return SYSTEM_ERROR_NO_MEMORY; + } + if (state_ >= State::READY) { + // Request the info about the newly created ledger + setPendingState(ctx, PendingState::GET_INFO); + } + } + ctx->instance = lr.get(); + ledger = std::move(lr); + return 0; +} + +int LedgerManager::getLedgerNames(Vector& namesArg) { + std::lock_guard lock(mutex_); + if (state_ == State::NEW) { + return SYSTEM_ERROR_INVALID_STATE; + } + // Return all ledgers found in the filesystem, not just the usable ones + FsLock fs; + Vector names; + lfs_dir_t dir = {}; + int r = lfs_dir_open(fs.instance(), &dir, LEDGER_ROOT_DIR); + if (r == 0) { + SCOPE_GUARD({ + int r = closeDir(fs.instance(), &dir); + if (r < 0) { + LOG(ERROR, "Failed to close directory handle: %d", r); + } + }); + lfs_info entry = {}; + while ((r = lfs_dir_read(fs.instance(), &dir, &entry)) == 1) { + if (entry.type != LFS_TYPE_DIR) { + LOG(WARN, "Found unexpected entry in ledger directory"); + continue; + } + if (std::strcmp(entry.name, ".") == 0 || std::strcmp(entry.name, "..") == 0) { + continue; + } + CString name(entry.name); + if (!name || !names.append(std::move(name))) { + return SYSTEM_ERROR_NO_MEMORY; + } + } + CHECK_FS(r); + } else if (r != LFS_ERR_NOENT) { + CHECK_FS(r); // Forward the error + } + namesArg = std::move(names); + return 0; +} + +int LedgerManager::removeLedgerData(const char* name) { + if (!*name) { + return SYSTEM_ERROR_INVALID_ARGUMENT; + } + std::lock_guard lock(mutex_); + // TODO: Allow removing ledgers regardless of the device state or if the given ledger is in use + if (state_ != State::OFFLINE) { + return SYSTEM_ERROR_INVALID_STATE; + } + bool found = false; + auto it = findContext(name, found); + if (found) { + if ((*it)->instance) { + return SYSTEM_ERROR_LEDGER_IN_USE; + } + contexts_.erase(it); + } + char path[MAX_PATH_LEN + 1]; + CHECK(formatLedgerPath(path, sizeof(path), name)); + CHECK(rmrf(path)); + return 0; +} + +int LedgerManager::removeAllData() { + std::lock_guard lock(mutex_); + // TODO: Allow removing ledgers regardless of the device state or if the given ledger is in use + if (state_ != State::OFFLINE) { + return SYSTEM_ERROR_INVALID_STATE; + } + for (auto& ctx: contexts_) { + if (ctx->instance) { + return SYSTEM_ERROR_LEDGER_IN_USE; + } + } + contexts_.clear(); + CHECK(rmrf(LEDGER_ROOT_DIR)); + return 0; +} + +void LedgerManager::run() { + std::lock_guard lock(mutex_); + if (state_ == State::FAILED) { + // TODO: Use an RTOS timer + auto now = hal_timer_millis(nullptr); + if (now >= retryTime_) { + LOG(INFO, "Retrying synchronization"); + startSync(); + } + return; + } + int r = processTasks(); + if (r < 0) { + LOG(ERROR, "Failed to process ledger task: %d", r); + handleError(r); + } +} + +int LedgerManager::processTasks() { + if (state_ != State::READY || !pendingState_) { + // Some task is in progress, the manager is in a bad state, or there's nothing to do + return 0; + } + if (pendingState_ & PendingState::GET_INFO) { + LOG(INFO, "Requesting ledger info"); + CHECK(sendGetInfoRequest()); + return 0; + } + if ((pendingState_ & PendingState::SUBSCRIBE) || resubscribe_) { + LOG(INFO, "Subscribing to ledger updates"); + CHECK(sendSubscribeRequest()); + return 0; + } + if (pendingState_ & PendingState::SYNC_TO_CLOUD) { + // TODO: Use an RTOS timer + auto now = hal_timer_millis(nullptr); + if (now >= nextSyncTime_) { + uint64_t t = 0; + LedgerSyncContext* ctx = nullptr; + for (auto& c: contexts_) { + if (c->pendingState & PendingState::SYNC_TO_CLOUD) { + if (now >= c->syncTime) { + ctx = c.get(); + break; + } + if (!t || c->syncTime < t) { + t = c->syncTime; + } + } + } + if (ctx) { + LOG(TRACE, "Synchronizing ledger: %s", ctx->name); + CHECK(sendSetDataRequest(ctx)); + return 0; + } + nextSyncTime_ = t; + } + } + if (pendingState_ & PendingState::SYNC_FROM_CLOUD) { + LedgerSyncContext* ctx = nullptr; + for (auto& c: contexts_) { + if (c->pendingState & PendingState::SYNC_FROM_CLOUD) { + ctx = c.get(); + break; + } + } + if (ctx) { + LOG(TRACE, "Synchronizing ledger: %s", ctx->name); + CHECK(sendGetDataRequest(ctx)); + return 0; + } + } + return 0; +} + +int LedgerManager::notifyConnected() { + if (state_ != State::OFFLINE) { + return SYSTEM_ERROR_INVALID_STATE; + } + LOG(TRACE, "Connected"); + startSync(); + return 0; +} + +void LedgerManager::notifyDisconnected(int /* error */) { + if (state_ == State::OFFLINE) { + return; + } + if (state_ == State::NEW) { + LOG_DEBUG(ERROR, "Unexpected manager state: %d", (int)state_); + return; + } + LOG(TRACE, "Disconnected"); + reset(); + retryDelay_ = 0; + state_ = State::OFFLINE; +} + +int LedgerManager::receiveRequest(coap_message* msg, int reqId) { + if (state_ < State::READY) { + return SYSTEM_ERROR_INVALID_STATE; + } + // Get the request type. XXX: It's assumed that the message fields are encoded in order of their + // field numbers, which is not guaranteed by the Protobuf spec in general + char buf[32] = {}; + size_t n = CHECK(coap_peek_payload(msg, buf, sizeof(buf), nullptr)); + pb_istream_t stream = pb_istream_from_buffer((const pb_byte_t*)buf, n); + uint32_t reqType = 0; + uint32_t fieldTag = 0; + auto fieldType = pb_wire_type_t(); + bool eof = false; + if (!pb_decode_tag(&stream, &fieldType, &fieldTag, &eof)) { + if (!eof) { + return SYSTEM_ERROR_BAD_DATA; + } + // Request type field is missing so its value is 0 + } else if (fieldTag == PB_CLOUD(Request_type_tag)) { + if (fieldType != PB_WT_VARINT || !pb_decode_varint32(&stream, &reqType)) { + return SYSTEM_ERROR_BAD_DATA; + } + } // else: Request type field is missing so its value is 0 + switch (reqType) { + case PB_CLOUD(Request_Type_LEDGER_NOTIFY_UPDATE): { + CHECK(receiveNotifyUpdateRequest(msg, reqId)); + break; + } + case PB_CLOUD(Request_Type_LEDGER_RESET_INFO): { + CHECK(receiveResetInfoRequest(msg, reqId)); + break; + } + default: + LOG(ERROR, "Unknown request type: %d", (int)reqType); + return SYSTEM_ERROR_NOT_SUPPORTED; + } + return 0; +} + +int LedgerManager::receiveNotifyUpdateRequest(coap_message* msg, int /* reqId */) { + LOG(TRACE, "Received update notification"); + pb_istream_t stream = {}; + CHECK(getStreamForSubmessage(msg, &stream, PB_CLOUD(Request_ledger_notify_update_tag))); + PB_LEDGER(NotifyUpdateRequest) pbReq = {}; + pbReq.ledgers.arg = this; + pbReq.ledgers.funcs.decode = [](pb_istream_t* stream, const pb_field_iter_t* /* field */, void** arg) { + auto self = (LedgerManager*)*arg; + PB_LEDGER(NotifyUpdateRequest_Ledger) pbLedger = {}; + if (!pb_decode(stream, &PB_LEDGER(NotifyUpdateRequest_Ledger_msg), &pbLedger)) { + return false; + } + // Get the context of the updated ledger + bool found = false; + auto it = self->findContext(pbLedger.name, found); + if (!found) { + LOG(WARN, "Unknown ledger: %s", pbLedger.name); + return true; // Ignore + } + auto ctx = it->get(); + if (ctx->syncDir != LEDGER_SYNC_DIRECTION_CLOUD_TO_DEVICE) { + if (ctx->syncDir == LEDGER_SYNC_DIRECTION_DEVICE_TO_CLOUD) { + LOG(ERROR, "Received update notification for device-to-cloud ledger: %s", ctx->name); + } else { + LOG(ERROR, "Received update notification for ledger with unknown sync direction: %s", ctx->name); + } + self->setPendingState(ctx, PendingState::GET_INFO); + return true; + } + if (pbLedger.last_updated > ctx->lastUpdated) { + // Schedule a sync for the updated ledger + LOG(TRACE, "Ledger changed: %s", ctx->name); + self->setPendingState(ctx, PendingState::SYNC_FROM_CLOUD); + } + return true; + }; + if (!pb_decode(&stream, &PB_LEDGER(NotifyUpdateRequest_msg), &pbReq)) { + return SYSTEM_ERROR_BAD_DATA; + } + return 0; +} + +int LedgerManager::receiveResetInfoRequest(coap_message* msg, int /* reqId */) { + LOG(WARN, "Received a reset request, re-requesting ledger info"); + for (auto& ctx: contexts_) { + setPendingState(ctx.get(), PendingState::GET_INFO); + } + return 0; +} + +int LedgerManager::receiveResponse(coap_message* msg, int status) { + assert(state_ > State::READY); + auto codeClass = COAP_CODE_CLASS(status); + if (codeClass != 2 && codeClass != 4) { // Success 2.xx or Client Error 4.xx + LOG(ERROR, "Ledger request failed: %d.%02d", (int)codeClass, (int)COAP_CODE_DETAIL(status)); + return SYSTEM_ERROR_LEDGER_REQUEST_FAILED; + } + // Get the protocol-specific result code. XXX: It's assumed that the message fields are encoded + // in order of their field numbers, which is not guaranteed by the Protobuf spec in general + char buf[32] = {}; + int r = coap_peek_payload(msg, buf, sizeof(buf), nullptr); + if (r < 0) { + if (r != SYSTEM_ERROR_END_OF_STREAM) { + return r; + } + r = 0; // Response is empty + } + pb_istream_t stream = pb_istream_from_buffer((const pb_byte_t*)buf, r); + int64_t result = 0; + uint32_t fieldTag = 0; + auto fieldType = pb_wire_type_t(); + bool eof = false; + if (!pb_decode_tag(&stream, &fieldType, &fieldTag, &eof)) { + if (!eof) { + return SYSTEM_ERROR_BAD_DATA; + } + // Result code field is missing so its value is 0 + } else if (fieldTag == PB_CLOUD(Response_result_tag)) { + if (fieldType != PB_WT_VARINT || !pb_decode_svarint(&stream, &result)) { + return SYSTEM_ERROR_BAD_DATA; + } + } // else: Result code field is missing so its value is 0 + if (codeClass != 2) { + if (result == PB_CLOUD(Response_Result_OK)) { + // CoAP response code indicates an error but the protocol-specific result code is OK + return SYSTEM_ERROR_LEDGER_INVALID_RESPONSE; + } + // This is not necessarily a critical error + LOG(WARN, "Ledger request failed: %d.%02d", (int)codeClass, (int)COAP_CODE_DETAIL(status)); + } + switch (state_) { + case State::SYNC_TO_CLOUD: { + CHECK(receiveSetDataResponse(msg, result)); + break; + } + case State::SYNC_FROM_CLOUD: { + CHECK(receiveGetDataResponse(msg, result)); + break; + } + case State::SUBSCRIBE: { + CHECK(receiveSubscribeResponse(msg, result)); + break; + } + case State::GET_INFO: { + CHECK(receiveGetInfoResponse(msg, result)); + break; + } + default: + LOG(ERROR, "Unexpected response"); + return SYSTEM_ERROR_INTERNAL; + } + return 0; +} + +int LedgerManager::receiveSetDataResponse(coap_message* msg, int result) { + assert(state_ == State::SYNC_TO_CLOUD && curCtx_ && curCtx_->syncDir == LEDGER_SYNC_DIRECTION_DEVICE_TO_CLOUD && + curCtx_->taskRunning); + if (result == 0) { + LOG(TRACE, "Sent ledger data: %s", curCtx_->name); + LedgerInfo newInfo; + auto now = CHECK(getMillisSinceEpoch()); + newInfo.lastSynced(now); + RefCountPtr ledger; + CHECK(getLedger(ledger, curCtx_->name)); + // Make sure the ledger can't be changed while we're updating its persistently stored state + // and sync context + std::unique_lock ledgerLock(*ledger); + curCtx_->syncTime = 0; + curCtx_->forcedSyncTime = 0; + if (ledger->info().updateCount() == curCtx_->updateCount) { + newInfo.syncPending(false); + curCtx_->syncPending = false; + } else { + // Ledger changed while being synchronized + assert(curCtx->pendingState & PendingState::SYNC_TO_CLOUD); + updateSyncTime(curCtx_); + } + CHECK(ledger->updateInfo(newInfo)); + ledgerLock.unlock(); + ledger->notifySynced(); // TODO: Invoke asynchronously + // TODO: Reorder the ledger entries so that they're synchronized in a round-robin fashion + } else { + LOG(ERROR, "Failed to sync ledger: %s; result: %d", curCtx_->name, result); + if (!isLedgerAccessError(result)) { + return SYSTEM_ERROR_LEDGER_REQUEST_FAILED; + } + // Ledger may no longer be accessible, re-request its info + LOG(WARN, "Re-requesting ledger info: %s", curCtx_->name); + setPendingState(curCtx_, PendingState::GET_INFO | PendingState::SYNC_TO_CLOUD); + } + curCtx_->taskRunning = false; + curCtx_ = nullptr; + state_ = State::READY; + return 0; +} + +int LedgerManager::receiveGetDataResponse(coap_message* msg, int result) { + assert(state_ == State::SYNC_FROM_CLOUD && curCtx_ && curCtx_->syncDir == LEDGER_SYNC_DIRECTION_CLOUD_TO_DEVICE && + curCtx_->taskRunning && !stream_ && !msg_); + if (result != 0) { + LOG(ERROR, "Failed to sync ledger: %s; result: %d", curCtx_->name, result); + if (!isLedgerAccessError(result)) { + return SYSTEM_ERROR_LEDGER_REQUEST_FAILED; + } + // Ledger may no longer be accessible, re-request its info + LOG(WARN, "Re-requesting ledger info: %s", curCtx_->name); + setPendingState(curCtx_, PendingState::GET_INFO | PendingState::SYNC_FROM_CLOUD); + curCtx_->taskRunning = false; + curCtx_ = nullptr; + state_ = State::READY; + return 0; + } + LOG(TRACE, "Received ledger data: %s", curCtx_->name); + // Open the ledger for writing + RefCountPtr ledger; + CHECK(getLedger(ledger, curCtx_->name)); + std::unique_ptr writer(new(std::nothrow) LedgerWriter()); + CHECK(ledger->initWriter(*writer, LedgerWriteSource::SYSTEM)); + // Ledger data may span multiple CoAP messages. Nanopb streams are synchronous so the response + // is decoded manually + pb_istream_t pbStream = {}; + CHECK(getStreamForSubmessage(msg, &pbStream, PB_CLOUD(Response_ledger_get_data_tag))); + uint64_t lastUpdated = 0; + for (;;) { + uint32_t fieldTag = 0; + auto fieldType = pb_wire_type_t(); + bool eof = false; + if (!pb_decode_tag(&pbStream, &fieldType, &fieldTag, &eof)) { + if (!eof) { + return SYSTEM_ERROR_BAD_DATA; + } + break; + } + if (fieldTag == PB_LEDGER(GetDataResponse_last_updated_tag)) { + if (fieldType != PB_WT_64BIT || !pb_decode_fixed64(&pbStream, &lastUpdated)) { + return SYSTEM_ERROR_BAD_DATA; + } + } else if (fieldTag == PB_LEDGER(GetDataResponse_data_tag)) { + if (!pb_skip_field(&pbStream, PB_WT_VARINT)) { // Skip the field length + return SYSTEM_ERROR_BAD_DATA; + } + // "data" is always the last field in the message. XXX: It's assumed that the message + // fields are encoded in order of their field numbers, which is not guaranteed by the + // Protobuf spec in general + break; + } + } + writer->updateInfo(LedgerInfo().lastUpdated(lastUpdated)); + stream_.reset(writer.release()); + msg_ = msg; + // Read the first chunk of the ledger data + CHECK(receiveLedgerData()); + return 0; +} + +int LedgerManager::receiveSubscribeResponse(coap_message* msg, int result) { + assert(state_ == State::SUBSCRIBE); + if (result != 0) { + LOG(ERROR, "Failed to subscribe to ledger updates; result: %d", result); + if (!isLedgerAccessError(result)) { + return SYSTEM_ERROR_LEDGER_REQUEST_FAILED; + } + // Some of the ledgers may no longer be accessible, re-request the info for the ledgers we + // tried to subscribe to + for (auto& ctx: contexts_) { + if (ctx->taskRunning) { + LOG(WARN, "Re-requesting ledger info: %s", ctx->name); + setPendingState(ctx.get(), PendingState::GET_INFO | PendingState::SUBSCRIBE); + ctx->taskRunning = false; + } + } + state_ = State::READY; + return 0; + } + LOG(INFO, "Subscribed to ledger updates"); + PB_LEDGER(SubscribeResponse) pbResp = {}; + struct DecodeContext { + LedgerManager* self; + int error; + }; + DecodeContext d = { .self = this, .error = 0 }; + pbResp.ledgers.arg = &d; + pbResp.ledgers.funcs.decode = [](pb_istream_t* stream, const pb_field_iter_t* /* field */, void** arg) { + auto d = (DecodeContext*)*arg; + PB_LEDGER(SubscribeResponse_Ledger) pbLedger = {}; + if (!pb_decode(stream, &PB_LEDGER(SubscribeResponse_Ledger_msg), &pbLedger)) { + return false; + } + LOG(TRACE, "Subscribed to ledger updates: %s", pbLedger.name); + bool found = false; + auto it = d->self->findContext(pbLedger.name, found); + if (!found) { + LOG(WARN, "Unknown ledger: %s", pbLedger.name); + return true; // Ignore + } + auto ctx = it->get(); + if (!ctx->taskRunning) { + LOG(ERROR, "Unexpected subscription: %s", ctx->name); + d->error = SYSTEM_ERROR_LEDGER_INVALID_RESPONSE; + return false; + } + ctx->taskRunning = false; + if ((pbLedger.has_last_updated && pbLedger.last_updated > ctx->lastUpdated) || ctx->syncPending) { + d->self->setPendingState(ctx, PendingState::SYNC_FROM_CLOUD); + } + return true; + }; + pb_istream_t stream = {}; + CHECK(getStreamForSubmessage(msg, &stream, PB_CLOUD(Response_ledger_subscribe_tag))); + if (!pb_decode(&stream, &PB_LEDGER(SubscribeResponse_msg), &pbResp)) { + return (d.error < 0) ? d.error : SYSTEM_ERROR_BAD_DATA; + } + for (auto& ctx: contexts_) { + if (ctx->taskRunning) { + LOG(ERROR, "Missing subscription: %s", ctx->name); + return SYSTEM_ERROR_LEDGER_INVALID_RESPONSE; + } + } + resubscribe_ = false; + state_ = State::READY; + return 0; +} + +int LedgerManager::receiveGetInfoResponse(coap_message* msg, int result) { + assert(state_ == State::GET_INFO); + if (result != 0) { + LOG(ERROR, "Failed to get ledger info; result: %d", result); + return SYSTEM_ERROR_LEDGER_REQUEST_FAILED; + } + LOG(INFO, "Received ledger info"); + struct DecodeContext { + LedgerManager* self; + int error; + bool resubscribe; + bool localInfoIsInvalid; + }; + DecodeContext d = { .self = this, .error = 0, .resubscribe = false, .localInfoIsInvalid = false }; + PB_LEDGER(GetInfoResponse) pbResp = {}; + pbResp.ledgers.arg = &d; + pbResp.ledgers.funcs.decode = [](pb_istream_t* stream, const pb_field_iter_t* /* field */, void** arg) { + auto d = (DecodeContext*)*arg; + PB_LEDGER(GetInfoResponse_Ledger) pbLedger = {}; + if (!pb_decode(stream, &PB_LEDGER(GetInfoResponse_Ledger_msg), &pbLedger)) { + return false; + } + LOG(TRACE, "Received ledger info: name: %s; sync direction: %d; scope type: %d;", pbLedger.name, + (int)pbLedger.sync_direction, (int)pbLedger.scope_type); + LOG_PRINT(TRACE, "scope ID: "); + LOG_DUMP(TRACE, pbLedger.scope_id.bytes, pbLedger.scope_id.size); + LOG_PRINT(TRACE, "\r\n"); + RefCountPtr ledger; + d->error = d->self->getLedger(ledger, pbLedger.name); + if (d->error < 0) { + return false; + } + auto ctx = ledger->syncContext(); + assert(ctx); + if (!ctx->taskRunning) { + LOG(ERROR, "Received unexpected ledger info: %s", ctx->name); + d->error = SYSTEM_ERROR_LEDGER_INVALID_RESPONSE; + return false; + } + ctx->taskRunning = false; + LedgerInfo newInfo; + newInfo.syncDirection(static_cast(pbLedger.sync_direction)); + newInfo.scopeType(static_cast(pbLedger.scope_type)); + if (newInfo.syncDirection() == LEDGER_SYNC_DIRECTION_UNKNOWN || newInfo.scopeType() == LEDGER_SCOPE_UNKNOWN) { + LOG(ERROR, "Received ledger info has invalid scope type or sync direction: %s", pbLedger.name); + d->error = SYSTEM_ERROR_LEDGER_INVALID_RESPONSE; + return false; + } + LedgerScopeId remoteScopeId = {}; + if (pbLedger.scope_id.size > sizeof(remoteScopeId.data)) { + d->error = SYSTEM_ERROR_INTERNAL; // This should have been validated by nanopb + return false; + } + std::memcpy(remoteScopeId.data, pbLedger.scope_id.bytes, pbLedger.scope_id.size); + remoteScopeId.size = pbLedger.scope_id.size; + newInfo.scopeId(remoteScopeId); + auto localInfo = ledger->info(); + auto& localScopeId = localInfo.scopeId(); + bool scopeIdChanged = localScopeId != remoteScopeId; + if (scopeIdChanged || localInfo.syncDirection() != newInfo.syncDirection() || localInfo.scopeType() != newInfo.scopeType()) { + if (localInfo.syncDirection() != LEDGER_SYNC_DIRECTION_UNKNOWN) { + if (scopeIdChanged) { + // Device was likely moved to another product which happens to have a ledger + // with the same name + LOG(WARN, "Ledger scope changed: %s", ctx->name); + d->self->clearPendingState(ctx, ctx->pendingState); + if (newInfo.syncDirection() == LEDGER_SYNC_DIRECTION_DEVICE_TO_CLOUD) { + // Do not sync this ledger until it's updated again + newInfo.syncPending(false); + if (localInfo.syncDirection() == LEDGER_SYNC_DIRECTION_CLOUD_TO_DEVICE) { + ctx->resetDeviceToCloudState(); + d->resubscribe = true; + } + } else { + assert(newInfo.syncDirection() == LEDGER_SYNC_DIRECTION_CLOUD_TO_DEVICE); + // Ignore the timestamps when synchronizing this ledger + newInfo.syncPending(true); + if (localInfo.syncDirection() == LEDGER_SYNC_DIRECTION_DEVICE_TO_CLOUD) { + ctx->resetCloudToDeviceState(); + } + d->resubscribe = true; + } + } else { // DEVICE_TO_CLOUD -> CLOUD_TO_DEVICE or vice versa + // This should not normally happen as the sync direction and scope type of an + // existing ledger cannot be changed + LOG(ERROR, "Ledger scope type or sync direction changed: %s", ctx->name); + newInfo.syncDirection(LEDGER_SYNC_DIRECTION_UNKNOWN); + newInfo.scopeType(LEDGER_SCOPE_UNKNOWN); + if (ctx->syncDir == LEDGER_SYNC_DIRECTION_CLOUD_TO_DEVICE) { + ctx->resetDeviceToCloudState(); + } + d->localInfoIsInvalid = true; // Will cause a transition to the failed state + } + } else if (newInfo.syncDirection() == LEDGER_SYNC_DIRECTION_CLOUD_TO_DEVICE) { // UNKNOWN -> CLOUD_TO_DEVICE + if (localInfo.syncPending()) { + LOG(WARN, "Ledger has local changes but its actual sync direction is cloud-to-device: %s", ctx->name); + // Ignore the timestamps when synchronizing this ledger + newInfo.syncPending(true); + } + ctx->resetCloudToDeviceState(); + d->resubscribe = true; + } else if (localInfo.syncPending()) { // UNKNOWN -> DEVICE_TO_CLOUD + d->self->setPendingState(ctx, PendingState::SYNC_TO_CLOUD); + d->self->updateSyncTime(ctx); + } + // Save the new ledger info + d->error = ledger->updateInfo(newInfo); + if (d->error < 0) { + return false; + } + ctx->updateFromLedgerInfo(newInfo); + } + return true; + }; + pb_istream_t stream = {}; + CHECK(getStreamForSubmessage(msg, &stream, PB_CLOUD(Response_ledger_get_info_tag))); + if (!pb_decode(&stream, &PB_LEDGER(GetInfoResponse_msg), &pbResp)) { + return (d.error < 0) ? d.error : SYSTEM_ERROR_BAD_DATA; + } + for (auto& ctx: contexts_) { + if (ctx->taskRunning) { + // Ledger doesn't exist or is no longer accessible by the device + LOG(WARN, "Ledger not found: %s", ctx->name); + if (ctx->syncDir != LEDGER_SYNC_DIRECTION_UNKNOWN) { // DEVICE_TO_CLOUD/CLOUD_TO_DEVICE -> UNKNOWN + LedgerInfo info; + info.syncDirection(LEDGER_SYNC_DIRECTION_UNKNOWN); + info.scopeType(LEDGER_SCOPE_UNKNOWN); + RefCountPtr ledger; + CHECK(getLedger(ledger, ctx->name)); + CHECK(ledger->updateInfo(info)); + if (ctx->syncDir == LEDGER_SYNC_DIRECTION_CLOUD_TO_DEVICE) { + ctx->resetDeviceToCloudState(); + d.resubscribe = true; + } + ctx->updateFromLedgerInfo(info); + } + clearPendingState(ctx.get(), ctx->pendingState); + ctx->taskRunning = false; + } else if (d.resubscribe && ctx->syncDir == LEDGER_SYNC_DIRECTION_CLOUD_TO_DEVICE) { + setPendingState(ctx.get(), PendingState::SUBSCRIBE); + } + } + if (d.resubscribe) { + // Make sure to clear the subscriptions on the server if no ledgers left to subscribe to + resubscribe_ = true; + } + if (d.localInfoIsInvalid) { + return SYSTEM_ERROR_LEDGER_INCONSISTENT_STATE; + } + state_ = State::READY; + return 0; +} + +int LedgerManager::sendSetDataRequest(LedgerSyncContext* ctx) { + assert(state_ == State::READY && (ctx->pendingState & PendingState::SYNC_TO_CLOUD) && + ctx->syncDir == LEDGER_SYNC_DIRECTION_DEVICE_TO_CLOUD && !curCtx_ && !stream_ && !msg_); + // Open the ledger for reading + RefCountPtr ledger; + CHECK(getLedger(ledger, ctx->name)); + std::unique_ptr reader(new(std::nothrow) LedgerReader()); + CHECK(ledger->initReader(*reader)); + auto info = reader->info(); + // Create a request message + coap_message* msg = nullptr; + int reqId = CHECK(coap_begin_request(&msg, REQUEST_URI, REQUEST_METHOD, 0 /* timeout */, 0 /* flags */, nullptr /* reserved */)); + NAMED_SCOPE_GUARD(destroyMsgGuard, { + coap_destroy_message(msg, nullptr); + }); + // Calculate the size of the request's submessage (particle.cloud.ledger.SetDataRequest) + pb_ostream_t pbStream = PB_OSTREAM_SIZING; + CHECK(encodeSetDataRequestPrefix(&pbStream, ctx->name, info)); + size_t submsgSize = pbStream.bytes_written + info.dataSize(); + // Encode the outer request message (particle.cloud.Request) + CHECK(pb_ostream_from_coap_message(&pbStream, msg, nullptr)); + if (!pb_encode_tag(&pbStream, PB_WT_VARINT, PB_CLOUD(Request_type_tag)) || // type + !pb_encode_varint(&pbStream, PB_CLOUD(Request_Type_LEDGER_SET_DATA))) { + return SYSTEM_ERROR_ENCODING_FAILED; + } + if (!pb_encode_tag(&pbStream, PB_WT_STRING, PB_CLOUD(Request_ledger_set_data_tag)) || // ledger_set_data + !pb_encode_varint(&pbStream, submsgSize)) { + return SYSTEM_ERROR_ENCODING_FAILED; + } + CHECK(encodeSetDataRequestPrefix(&pbStream, ctx->name, info)); + // Encode and send the first chunk of the ledger data + stream_.reset(reader.release()); + reqId_ = reqId; + msg_ = msg; + destroyMsgGuard.dismiss(); + CHECK(sendLedgerData()); + // Clear the pending state + clearPendingState(ctx, PendingState::SYNC_TO_CLOUD); + ctx->updateCount = info.updateCount(); + ctx->taskRunning = true; + curCtx_ = ctx; + state_ = State::SYNC_TO_CLOUD; + return 0; +} + +int LedgerManager::sendGetDataRequest(LedgerSyncContext* ctx) { + assert(state_ == State::READY && (ctx->pendingState & PendingState::SYNC_FROM_CLOUD) && + ctx->syncDir == LEDGER_SYNC_DIRECTION_CLOUD_TO_DEVICE && !curCtx_); + // Prepare a request message + PB_CLOUD(Request) pbReq = {}; + pbReq.type = PB_CLOUD(Request_Type_LEDGER_GET_DATA); + pbReq.which_data = PB_CLOUD(Request_ledger_get_data_tag); + size_t n = strlcpy(pbReq.data.ledger_get_data.name, ctx->name, sizeof(pbReq.data.ledger_get_data.name)); + if (n >= sizeof(pbReq.data.ledger_get_data.name)) { + return SYSTEM_ERROR_INTERNAL; + } + if (ctx->scopeId.size > sizeof(pbReq.data.ledger_get_data.scope_id.bytes)) { + return SYSTEM_ERROR_INTERNAL; + } + std::memcpy(pbReq.data.ledger_get_data.scope_id.bytes, ctx->scopeId.data, ctx->scopeId.size); + pbReq.data.ledger_get_data.scope_id.size = ctx->scopeId.size; + // Send the request + coap_message* msg = nullptr; + int reqId = CHECK(coap_begin_request(&msg, REQUEST_URI, REQUEST_METHOD, 0 /* timeout */, 0 /* flags */, nullptr /* reserved */)); + NAMED_SCOPE_GUARD(destroyMsgGuard, { + coap_destroy_message(msg, nullptr); + }); + pb_ostream_t stream = {}; + CHECK(pb_ostream_from_coap_message(&stream, msg, nullptr)); + if (!pb_encode(&stream, &PB_CLOUD(Request_msg), &pbReq)) { + return SYSTEM_ERROR_ENCODING_FAILED; + } + CHECK(coap_end_request(msg, responseCallback, nullptr /* ack_cb */, requestErrorCallback, this, nullptr)); + destroyMsgGuard.dismiss(); + // Clear the pending state + clearPendingState(ctx, PendingState::SYNC_FROM_CLOUD); + ctx->taskRunning = true; + curCtx_ = ctx; + reqId_ = reqId; + state_ = State::SYNC_FROM_CLOUD; + return 0; +} + +int LedgerManager::sendSubscribeRequest() { + assert(state_ == State::READY && (pendingState_ & PendingState::SUBSCRIBE)); + struct EncodeContext { + LedgerManager* self; + int error; + }; + EncodeContext d = { .self = this, .error = 0 }; + PB_CLOUD(Request) pbReq = {}; + pbReq.type = PB_CLOUD(Request_Type_LEDGER_SUBSCRIBE); + pbReq.which_data = PB_CLOUD(Request_ledger_subscribe_tag); + pbReq.data.ledger_subscribe.ledgers.arg = &d; + pbReq.data.ledger_subscribe.ledgers.funcs.encode = [](pb_ostream_t* stream, const pb_field_iter_t* field, void* const* arg) { + // Make sure not to update any state in this callback as it may be called multiple times + auto d = (EncodeContext*)*arg; + for (auto& ctx: d->self->contexts_) { + if (ctx->pendingState & PendingState::SUBSCRIBE) { + PB_LEDGER(SubscribeRequest_Ledger) pbLedger = {}; + size_t n = strlcpy(pbLedger.name, ctx->name, sizeof(pbLedger.name)); + if (n >= sizeof(pbLedger.name) || ctx->scopeId.size > sizeof(pbLedger.scope_id.bytes)) { + d->error = SYSTEM_ERROR_INTERNAL; + return false; + } + std::memcpy(pbLedger.scope_id.bytes, ctx->scopeId.data, ctx->scopeId.size); + pbLedger.scope_id.size = ctx->scopeId.size; + if (!pb_encode_tag_for_field(stream, field) || + !pb_encode_submessage(stream, &PB_LEDGER(SubscribeRequest_Ledger_msg), &pbLedger)) { + return false; + } + } + } + return true; + }; + coap_message* msg = nullptr; + int reqId = CHECK(coap_begin_request(&msg, REQUEST_URI, REQUEST_METHOD, 0 /* timeout */, 0 /* flags */, nullptr /* reserved */)); + NAMED_SCOPE_GUARD(destroyMsgGuard, { + coap_destroy_message(msg, nullptr); + }); + pb_ostream_t stream = {}; + CHECK(pb_ostream_from_coap_message(&stream, msg, nullptr)); + if (!pb_encode(&stream, &PB_CLOUD(Request_msg), &pbReq)) { + return (d.error < 0) ? d.error : SYSTEM_ERROR_ENCODING_FAILED; + } + CHECK(coap_end_request(msg, responseCallback, nullptr /* ack_cb */, requestErrorCallback, this, nullptr)); + destroyMsgGuard.dismiss(); + // Clear the pending state + pendingState_ = 0; + for (auto& ctx: contexts_) { + if (ctx->pendingState & PendingState::SUBSCRIBE) { + ctx->pendingState &= ~PendingState::SUBSCRIBE; + ctx->taskRunning = true; + } + pendingState_ |= ctx->pendingState; + } + reqId_ = reqId; + state_ = State::SUBSCRIBE; + return 0; +} + +int LedgerManager::sendGetInfoRequest() { + assert(state_ == State::READY && (pendingState_ & PendingState::GET_INFO)); + struct EncodeContext { + LedgerManager* self; + int error; + }; + EncodeContext d = { .self = this, .error = 0 }; + PB_CLOUD(Request) pbReq = {}; + pbReq.type = PB_CLOUD(Request_Type_LEDGER_GET_INFO); + pbReq.which_data = PB_CLOUD(Request_ledger_get_info_tag); + pbReq.data.ledger_get_info.ledgers.arg = &d; + pbReq.data.ledger_get_info.ledgers.funcs.encode = [](pb_ostream_t* stream, const pb_field_iter_t* field, void* const* arg) { + // Make sure not to update any state in this callback as it may be called multiple times + auto d = (EncodeContext*)*arg; + for (auto& ctx: d->self->contexts_) { + if (ctx->pendingState & PendingState::GET_INFO) { + // This is to prevent sending GET_INFO requests in a loop if a subsequent SET_DATA or + // SUBSCRIBE request keeps failing with a result code that triggers another GET_INFO + // request. This can only happen due to a server error + if (ctx->getInfoCount >= 10) { + LOG(ERROR, "Sent too many info requests for ledger: %s", ctx->name); + d->error = SYSTEM_ERROR_LEDGER_INCONSISTENT_STATE; + return false; + } + if (!pb_encode_tag_for_field(stream, field) || + !pb_encode_string(stream, (const pb_byte_t*)ctx->name, std::strlen(ctx->name))) { + return false; + } + } + } + return true; + }; + coap_message* msg = nullptr; + int reqId = CHECK(coap_begin_request(&msg, REQUEST_URI, REQUEST_METHOD, 0 /* timeout */, 0 /* flags */, nullptr /* reserved */)); + NAMED_SCOPE_GUARD(destroyMsgGuard, { + coap_destroy_message(msg, nullptr); + }); + pb_ostream_t stream = {}; + CHECK(pb_ostream_from_coap_message(&stream, msg, nullptr)); + if (!pb_encode(&stream, &PB_CLOUD(Request_msg), &pbReq)) { + return (d.error < 0) ? d.error : SYSTEM_ERROR_ENCODING_FAILED; + } + CHECK(coap_end_request(msg, responseCallback, nullptr /* ack_cb */, requestErrorCallback, this, nullptr)); + destroyMsgGuard.dismiss(); + // Clear the pending state + pendingState_ = 0; + for (auto& ctx: contexts_) { + if (ctx->pendingState & PendingState::GET_INFO) { + ctx->pendingState &= ~PendingState::GET_INFO; + ++ctx->getInfoCount; + ctx->taskRunning = true; + } + pendingState_ |= ctx->pendingState; + } + reqId_ = reqId; + state_ = State::GET_INFO; + return 0; +} + +int LedgerManager::sendLedgerData() { + assert(stream_ && msg_); + bool eof = false; + for (;;) { + if (bytesInBuf_ > 0) { + size_t size = bytesInBuf_; + int r = CHECK(coap_write_payload(msg_, buf_.get(), &size, messageBlockCallback, requestErrorCallback, this, nullptr)); + if (r == COAP_RESULT_WAIT_BLOCK) { + assert(size < bytesInBuf_); + bytesInBuf_ -= size; + std::memmove(buf_.get(), buf_.get() + size, bytesInBuf_); + LOG_DEBUG(TRACE, "Waiting current block of ledger data to be sent"); + break; + } + assert(size == bytesInBuf_); + bytesInBuf_ = 0; + } + int r = stream_->read(buf_.get(), STREAM_BUFFER_SIZE); + if (r < 0) { + if (r == SYSTEM_ERROR_END_OF_STREAM) { + eof = true; + break; + } + return r; + } + assert(r != 0); + bytesInBuf_ = r; + } + if (eof) { + CHECK(stream_->close()); + stream_.reset(); + CHECK(coap_end_request(msg_, responseCallback, nullptr /* ack_cb */, requestErrorCallback, this, nullptr)); + msg_ = nullptr; + } + return 0; +} + +int LedgerManager::receiveLedgerData() { + assert(curCtx_ && stream_ && msg_); + bool eof = false; + for (;;) { + if (bytesInBuf_ > 0) { + CHECK(stream_->write(buf_.get(), bytesInBuf_)); + bytesInBuf_ = 0; + } + size_t size = STREAM_BUFFER_SIZE; + int r = coap_read_payload(msg_, buf_.get(), &size, messageBlockCallback, requestErrorCallback, this, nullptr); + if (r < 0) { + if (r == SYSTEM_ERROR_END_OF_STREAM) { + eof = true; + break; + } + return r; + } + bytesInBuf_ = size; + if (r == COAP_RESULT_WAIT_BLOCK) { + LOG_DEBUG(TRACE, "Waiting next block of ledger data to be received"); + break; + } + } + if (eof) { + auto now = CHECK(getMillisSinceEpoch()); + auto writer = static_cast(stream_.get()); + LedgerInfo info; + info.lastSynced(now); + info.syncPending(false); + writer->updateInfo(info); + auto ledger = writer->ledger(); + CHECK(stream_->close()); + stream_.reset(); + coap_destroy_message(msg_, nullptr); + msg_ = nullptr; + reqId_ = COAP_INVALID_REQUEST_ID; + curCtx_->taskRunning = false; + curCtx_ = nullptr; + state_ = State::READY; + ledger->notifySynced(); // TODO: Invoke asynchronously + } + return 0; +} + +int LedgerManager::sendResponse(int result, int reqId) { + coap_message* msg = nullptr; + int code = (result == 0) ? COAP_STATUS_CHANGED : COAP_STATUS_BAD_REQUEST; + CHECK(coap_begin_response(&msg, code, reqId, 0 /* flags */, nullptr /* reserved */)); + NAMED_SCOPE_GUARD(destroyMsgGuard, { + coap_destroy_message(msg, nullptr); + }); + PB_CLOUD(Response) pbResp = {}; + pbResp.result = result; + EncodedString pbMsg(&pbResp.message); + if (result < 0) { + pbMsg.data = get_system_error_message(result); + pbMsg.size = std::strlen(pbMsg.data); + } + CHECK(coap_end_response(msg, nullptr /* ack_cb */, requestErrorCallback, nullptr /* arg */, nullptr /* reserved */)); + destroyMsgGuard.dismiss(); + return 0; +} + +void LedgerManager::setPendingState(LedgerSyncContext* ctx, int state) { + ctx->pendingState |= state; + pendingState_ |= state; +} + +void LedgerManager::clearPendingState(LedgerSyncContext* ctx, int state) { + ctx->pendingState &= ~state; + pendingState_ = 0; + for (auto& ctx: contexts_) { + pendingState_ |= ctx->pendingState; + } +} + +void LedgerManager::updateSyncTime(LedgerSyncContext* ctx) { + auto now = hal_timer_millis(nullptr); + if (!ctx->forcedSyncTime) { + ctx->forcedSyncTime = now + MAX_SYNC_DELAY; + } + ctx->syncTime = std::min(now + MIN_SYNC_DELAY, ctx->forcedSyncTime); + if (!nextSyncTime_) { + nextSyncTime_ = ctx->syncTime; + } +} + +void LedgerManager::startSync() { + assert(state_ == State::OFFLINE || state_ == State::FAILED); + for (auto& ctx: contexts_) { + switch (ctx->syncDir) { + case LEDGER_SYNC_DIRECTION_DEVICE_TO_CLOUD: { + if (ctx->syncPending) { + setPendingState(ctx.get(), PendingState::SYNC_TO_CLOUD); + updateSyncTime(ctx.get()); + } + break; + } + case LEDGER_SYNC_DIRECTION_CLOUD_TO_DEVICE: { + setPendingState(ctx.get(), PendingState::SUBSCRIBE); + break; + } + case LEDGER_SYNC_DIRECTION_UNKNOWN: { + setPendingState(ctx.get(), PendingState::GET_INFO); + break; + } + default: + break; + } + } + state_ = State::READY; +} + +void LedgerManager::reset() { + if (msg_) { + coap_destroy_message(msg_, nullptr); + msg_ = nullptr; + } + if (reqId_ != COAP_INVALID_REQUEST_ID) { + coap_cancel_request(reqId_, nullptr); + reqId_ = COAP_INVALID_REQUEST_ID; + } + if (stream_) { + int r = stream_->close(true /* discard */); + if (r < 0) { + LOG(ERROR, "Failed to close ledger stream: %d", r); + } + stream_.reset(); + } + for (auto& ctx: contexts_) { + if (ctx->syncDir == LEDGER_SYNC_DIRECTION_DEVICE_TO_CLOUD || ctx->syncDir == LEDGER_SYNC_DIRECTION_UNKNOWN) { + ctx->syncTime = 0; + ctx->forcedSyncTime = 0; + } + ctx->getInfoCount = 0; + ctx->pendingState = 0; + ctx->taskRunning = false; + } + pendingState_ = 0; + nextSyncTime_ = 0; + bytesInBuf_ = 0; + curCtx_ = nullptr; +} + +void LedgerManager::handleError(int error) { + if (error < 0 && state_ >= State::READY) { + retryDelay_ = std::clamp(retryDelay_ * 2, MIN_RETRY_DELAY, MAX_RETRY_DELAY); + LOG(ERROR, "Synchronization failed: %d; retrying in %us", error, retryDelay_ / 1000); + reset(); + retryTime_ = hal_timer_millis(nullptr) + retryDelay_; + state_ = State::FAILED; + } +} + +LedgerManager::LedgerSyncContexts::ConstIterator LedgerManager::findContext(const char* name, bool& found) const { + found = false; + auto it = std::lower_bound(contexts_.begin(), contexts_.end(), name, [&found](const auto& ctx, const char* name) { + auto r = std::strcmp(ctx->name, name); + if (r == 0) { + found = true; + } + return r < 0; + }); + return it; +} + +void LedgerManager::notifyLedgerChanged(LedgerSyncContext* ctx) { + std::lock_guard lock(mutex_); + if (ctx->syncDir == LEDGER_SYNC_DIRECTION_DEVICE_TO_CLOUD || ctx->syncDir == LEDGER_SYNC_DIRECTION_UNKNOWN) { + // Mark the ledger as changed but only schedule a sync for it if its actual sync direction is known + ctx->syncPending = true; + if (ctx->syncDir != LEDGER_SYNC_DIRECTION_UNKNOWN && state_ >= State::READY) { + setPendingState(ctx, PendingState::SYNC_TO_CLOUD); + updateSyncTime(ctx); + } + } +} + +void LedgerManager::addLedgerRef(const LedgerBase* ledger) { + std::lock_guard lock(mutex_); + ++ledger->refCount(); +} + +void LedgerManager::releaseLedger(const LedgerBase* ledger) { + std::lock_guard lock(mutex_); + if (--ledger->refCount() == 0) { + auto ctx = ledger->syncContext(); + ctx->instance = nullptr; + delete ledger; + } +} + +int LedgerManager::connectionCallback(int error, int status, void* arg) { + auto self = static_cast(arg); + std::lock_guard lock(self->mutex_); + int r = 0; + switch (status) { + case COAP_CONNECTION_OPEN: { + r = self->notifyConnected(); + break; + } + case COAP_CONNECTION_CLOSED: { + self->notifyDisconnected(error); + break; + } + default: + break; + } + if (r < 0) { + LOG(ERROR, "Failed to handle connection status change: %d", error); + self->handleError(r); + } + return 0; +} + +int LedgerManager::requestCallback(coap_message* msg, const char* uri, int method, int reqId, void* arg) { + SCOPE_GUARD({ + coap_destroy_message(msg, nullptr); + }); + auto self = static_cast(arg); + std::lock_guard lock(self->mutex_); + clear_system_error_message(); + int result = self->receiveRequest(msg, reqId); + if (result < 0) { + LOG(ERROR, "Error while handling request: %d", result); + } + int r = self->sendResponse(result, reqId); + if (r < 0 && r != SYSTEM_ERROR_COAP_REQUEST_NOT_FOUND) { // Response might have been sent already + LOG(ERROR, "Failed to send response: %d", r); + if (result >= 0) { + result = r; + } + } + if (result < 0) { + self->handleError(result); + } + return 0; +} + +int LedgerManager::responseCallback(coap_message* msg, int status, int reqId, void* arg) { + auto self = static_cast(arg); + std::lock_guard lock(self->mutex_); + assert(!self->msg_ && self->reqId_ == reqId); + SCOPE_GUARD({ + if (!self->msg_) { + coap_destroy_message(msg, nullptr); + self->reqId_ = COAP_INVALID_REQUEST_ID; + } // else: Receiving a blockwise response + }); + int r = self->receiveResponse(msg, status); + if (r < 0) { + LOG(ERROR, "Error while handling response: %d", r); + self->handleError(r); + } + return 0; +} + +int LedgerManager::messageBlockCallback(coap_message* msg, int reqId, void* arg) { + auto self = static_cast(arg); + std::lock_guard lock(self->mutex_); + assert(self->msg_ == msg && self->reqId_ == reqId); + int r = 0; + if (self->state_ == State::SYNC_TO_CLOUD) { + r = self->sendLedgerData(); + } else if (self->state_ == State::SYNC_FROM_CLOUD) { + r = self->receiveLedgerData(); + } else { + LOG(ERROR, "Unexpected block message"); + r = SYSTEM_ERROR_INTERNAL; + } + if (r < 0) { + self->handleError(r); + } + return r; +} + +void LedgerManager::requestErrorCallback(int error, int /* reqId */, void* arg) { + auto self = static_cast(arg); + std::lock_guard lock(self->mutex_); + LOG(ERROR, "Request failed: %d", error); + self->handleError(error); +} + +LedgerManager* LedgerManager::instance() { + static LedgerManager mgr; + return &mgr; +} + +} // namespace system + +} // namespace particle + +#endif // HAL_PLATFORM_LEDGER diff --git a/system/src/ledger/ledger_manager.h b/system/src/ledger/ledger_manager.h new file mode 100644 index 0000000000..ff233fc826 --- /dev/null +++ b/system/src/ledger/ledger_manager.h @@ -0,0 +1,163 @@ +/* + * Copyright (c) 2023 Particle Industries, Inc. All rights reserved. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation, either + * version 3 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, see . + */ + +#pragma once + +#include "hal_platform.h" + +#if HAL_PLATFORM_LEDGER + +#include +#include + +#include "coap_api.h" + +#include "c_string.h" +#include "static_recursive_mutex.h" +#include "ref_count.h" + +#include "spark_wiring_vector.h" + +namespace particle::system { + +namespace detail { + +class LedgerSyncContext; + +} // namespace detail + +class Ledger; +class LedgerBase; +class LedgerStream; + +class LedgerManager { +public: + LedgerManager(const LedgerManager&) = delete; + + ~LedgerManager(); + + int init(); + + int getLedger(RefCountPtr& ledger, const char* name, bool create = false); + + int getLedgerNames(Vector& names); + + int removeLedgerData(const char* name); + int removeAllData(); + + void run(); + + LedgerManager& operator=(const LedgerManager&) = delete; + + static LedgerManager* instance(); + +protected: + using LedgerSyncContext = detail::LedgerSyncContext; + + void notifyLedgerChanged(LedgerSyncContext* ctx); // Called by Ledger + + void addLedgerRef(const LedgerBase* ledger); // Called by LedgerBase + void releaseLedger(const LedgerBase* ledger); // ditto + +private: + enum class State { + NEW, // Manager is not initialized + OFFLINE, // Device is offline + FAILED, // Synchronization failed (device is online) + READY, // Ready to run a task + SYNC_TO_CLOUD, // Synchronizing a device-to-cloud ledger + SYNC_FROM_CLOUD, // Synchronizing a cloud-to-device ledger + SUBSCRIBE, // Subscribing to ledger updates + GET_INFO // Getting ledger info + }; + + enum PendingState { + SYNC_TO_CLOUD = 0x01, // Synchronization of a device-to-cloud ledger is pending + SYNC_FROM_CLOUD = 0x02, // Synchronization of a cloud-to-device ledger is pending + SUBSCRIBE = 0x04, // Subscription to updates is pending + GET_INFO = 0x08 // Ledger info is missing + }; + + typedef Vector> LedgerSyncContexts; + + LedgerSyncContexts contexts_; // Preallocated context objects for all known ledgers + std::unique_ptr stream_; // Input or output stream open for the ledger being synchronized + std::unique_ptr buf_; // Intermediate buffer used for piping ledger data + LedgerSyncContext* curCtx_; // Context of the ledger being synchronized + coap_message* msg_; // CoAP request or response that is being sent or received + uint64_t nextSyncTime_; // Time when the next device-to-cloud ledger needs to be synchronized (ticks) + uint64_t retryTime_; // Time when synchronization can be retried (ticks) + unsigned retryDelay_; // Delay before retrying synchronization + size_t bytesInBuf_; // Number of bytes stored in the intermediate buffer + State state_; // Current manager state + int pendingState_; // Pending ledger state flags + int reqId_; // ID of the ongoing CoAP request + bool resubscribe_; // Whether the ledger subcriptions need to be updated + + mutable StaticRecursiveMutex mutex_; // Manager lock + + LedgerManager(); // Use LedgerManager::instance() + + int processTasks(); + + int notifyConnected(); + void notifyDisconnected(int error); + + int receiveRequest(coap_message* msg, int reqId); + int receiveNotifyUpdateRequest(coap_message* msg, int reqId); + int receiveResetInfoRequest(coap_message* msg, int reqId); + + int receiveResponse(coap_message* msg, int status); + int receiveSetDataResponse(coap_message* msg, int result); + int receiveGetDataResponse(coap_message* msg, int result); + int receiveSubscribeResponse(coap_message* msg, int result); + int receiveGetInfoResponse(coap_message* msg, int result); + + int sendSetDataRequest(LedgerSyncContext* ctx); + int sendGetDataRequest(LedgerSyncContext* ctx); + int sendSubscribeRequest(); + int sendGetInfoRequest(); + + int sendLedgerData(); + int receiveLedgerData(); + + int sendResponse(int result, int reqId); + + void setPendingState(LedgerSyncContext* ctx, int state); + void clearPendingState(LedgerSyncContext* ctx, int state); + void updateSyncTime(LedgerSyncContext* ctx); + + void startSync(); + void reset(); + + void handleError(int error); + + LedgerSyncContexts::ConstIterator findContext(const char* name, bool& found) const; + + static int connectionCallback(int error, int status, void* arg); + static int requestCallback(coap_message* msg, const char* uri, int method, int reqId, void* arg); + static int responseCallback(coap_message* msg, int status, int reqId, void* arg); + static int messageBlockCallback(coap_message* msg, int reqId, void* arg); + static void requestErrorCallback(int error, int reqId, void* arg); + + friend class Ledger; + friend class LedgerBase; +}; + +} // namespace particle::system + +#endif // HAL_PLATFORM_LEDGER diff --git a/system/src/ledger/ledger_util.cpp b/system/src/ledger/ledger_util.cpp new file mode 100644 index 0000000000..4bfb96165d --- /dev/null +++ b/system/src/ledger/ledger_util.cpp @@ -0,0 +1,61 @@ +/* + * Copyright (c) 2023 Particle Industries, Inc. All rights reserved. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation, either + * version 3 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, see . + */ + +#include "hal_platform.h" + +#if HAL_PLATFORM_LEDGER + +#include +#include + +#include "ledger.h" +#include "ledger_util.h" + +#include "system_error.h" + +namespace particle::system { + +int formatLedgerPath(char* buf, size_t size, const char* ledgerName, const char* fmt, ...) { + // Format the prefix part of the path + int n = std::snprintf(buf, size, "%s/%s/", LEDGER_ROOT_DIR, ledgerName); + if (n < 0) { + return SYSTEM_ERROR_INTERNAL; + } + size_t pos = n; + if (pos >= size) { + return SYSTEM_ERROR_PATH_TOO_LONG; + } + if (fmt) { + // Format the rest of the path + va_list args; + va_start(args, fmt); + n = vsnprintf(buf + pos, size - pos, fmt, args); + va_end(args); + if (n < 0) { + return SYSTEM_ERROR_INTERNAL; + } + pos += n; + if (pos >= size) { + return SYSTEM_ERROR_PATH_TOO_LONG; + } + } + return pos; +} + +} // namespace particle::system + +#endif // HAL_PLATFORM_LEDGER diff --git a/system/src/ledger/ledger_util.h b/system/src/ledger/ledger_util.h new file mode 100644 index 0000000000..7476b13af9 --- /dev/null +++ b/system/src/ledger/ledger_util.h @@ -0,0 +1,32 @@ +/* + * Copyright (c) 2023 Particle Industries, Inc. All rights reserved. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation, either + * version 3 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, see . + */ + +#pragma once + +#include "hal_platform.h" + +#if HAL_PLATFORM_LEDGER + +#include + +namespace particle::system { + +int formatLedgerPath(char* buf, size_t size, const char* ledgerName, const char* fmt = nullptr, ...); + +} // namespace particle::system + +#endif // HAL_PLATFORM_LEDGER diff --git a/system/src/main.cpp b/system/src/main.cpp index 3ed6872b7c..fb095d3c2f 100644 --- a/system/src/main.cpp +++ b/system/src/main.cpp @@ -27,6 +27,10 @@ */ /* Includes ------------------------------------------------------------------*/ + +// STATIC_ASSERT macro clashes with the nRF SDK +#define NO_STATIC_ASSERT + #include "debug.h" #include "system_event.h" #include "system_mode.h" @@ -63,6 +67,7 @@ #include "spark_wiring_wifi.h" #include "server_config.h" #include "system_network_manager.h" +#include "ledger/ledger_manager.h" // FIXME #include "system_control_internal.h" @@ -762,6 +767,13 @@ void app_setup_and_loop(void) } Network_Setup(threaded); // todo - why does this come before system thread initialization? + if (system_mode() != SAFE_MODE) { + int r = system::LedgerManager::instance()->init(); + if (r < 0) { + LOG(ERROR, "Failed to initialize ledger manager: %d", r); + } + } + #if PLATFORM_THREADING if (threaded) { diff --git a/system/src/system_ledger.cpp b/system/src/system_ledger.cpp index e0b4d2a7c2..80b77bb141 100644 --- a/system/src/system_ledger.cpp +++ b/system/src/system_ledger.cpp @@ -22,10 +22,11 @@ #include #include +#include "ledger/ledger.h" +#include "ledger/ledger_manager.h" #include "system_ledger.h" - -#include "system_ledger_internal.h" #include "system_threading.h" + #include "check.h" using namespace particle; @@ -33,8 +34,8 @@ using namespace particle::system; int ledger_get_instance(ledger_instance** ledger, const char* name, void* reserved) { RefCountPtr lr; - CHECK(LedgerManager::instance()->getLedger(name, lr)); - *ledger = reinterpret_cast(lr.unwrap()); // Transfer ownership to the caller + CHECK(LedgerManager::instance()->getLedger(lr, name, true /* create */)); + *ledger = reinterpret_cast(lr.unwrap()); // Transfer ownership return 0; } @@ -83,7 +84,7 @@ int ledger_get_info(ledger_instance* ledger, ledger_info* info, void* reserved) info->last_updated = srcInfo.lastUpdated(); info->last_synced = srcInfo.lastSynced(); info->data_size = srcInfo.dataSize(); - info->scope = srcInfo.scope(); + info->scope = srcInfo.scopeType(); info->sync_direction = srcInfo.syncDirection(); info->flags = 0; if (srcInfo.syncPending()) { @@ -104,7 +105,7 @@ int ledger_open(ledger_stream** stream, ledger_instance* ledger, int mode, void* return SYSTEM_ERROR_NO_MEMORY; } CHECK(lr->initReader(*r)); - *stream = reinterpret_cast(r.release()); // Transfer ownership to the caller + *stream = reinterpret_cast(r.release()); // Transfer ownership } else if (mode & LEDGER_STREAM_MODE_WRITE) { if (mode & LEDGER_STREAM_MODE_READ) { return SYSTEM_ERROR_NOT_SUPPORTED; @@ -113,7 +114,7 @@ int ledger_open(ledger_stream** stream, ledger_instance* ledger, int mode, void* if (!w) { return SYSTEM_ERROR_NO_MEMORY; } - CHECK(lr->initWriter(LedgerWriteSource::USER, *w)); + CHECK(lr->initWriter(*w, LedgerWriteSource::USER)); *stream = reinterpret_cast(w.release()); } else { return SYSTEM_ERROR_INVALID_ARGUMENT; @@ -143,6 +144,20 @@ int ledger_write(ledger_stream* stream, const char* data, size_t size, void* res return n; } +int ledger_get_names(char*** names, size_t* count, void* reserved) { + Vector namesVec; + CHECK(LedgerManager::instance()->getLedgerNames(namesVec)); + *names = (char**)std::malloc(sizeof(char*) * namesVec.size()); + if (!*names && namesVec.size() > 0) { + return SYSTEM_ERROR_NO_MEMORY; + } + for (int i = 0; i < namesVec.size(); ++i) { + (*names)[i] = namesVec[i].unwrap(); // Transfer ownership + } + *count = namesVec.size(); + return 0; +} + int ledger_purge(const char* name, void* reserved) { CHECK(LedgerManager::instance()->removeLedgerData(name)); return 0; diff --git a/system/src/system_task.cpp b/system/src/system_task.cpp index 81b85127fc..eb13213791 100644 --- a/system/src/system_task.cpp +++ b/system/src/system_task.cpp @@ -19,8 +19,12 @@ #undef LOG_COMPILE_TIME_LEVEL #define LOG_COMPILE_TIME_LEVEL LOG_LEVEL_ALL +// STATIC_ASSERT macro clashes with the nRF SDK +#define NO_STATIC_ASSERT + #include "logging.h" +#include "ledger/ledger_manager.h" #include "spark_wiring_platform.h" #include "spark_wiring_system.h" #include "spark_wiring_usbserial.h" @@ -33,6 +37,7 @@ #include "system_network_internal.h" #include "system_update.h" #include "firmware_update.h" +#include "coap_channel_new.h" #include "spark_macros.h" #include "string.h" #include "core_hal.h" @@ -375,6 +380,7 @@ void handle_cloud_connection(bool force_events) SPARK_CLOUD_CONNECTED = 1; SPARK_CLOUD_HANDSHAKE_NOTIFY_DONE = 0; cloud_failed_connection_attempts = 0; + protocol::experimental::CoapChannel::instance()->open(); CloudDiagnostics::instance()->status(CloudDiagnostics::CONNECTED); system_notify_event(cloud_status, cloud_status_connected); if (system_mode() == SAFE_MODE) { @@ -523,6 +529,8 @@ void Spark_Idle_Events(bool force_events/*=false*/) system::FirmwareUpdate::instance()->process(); + system::LedgerManager::instance()->run(); + if (system_mode() != SAFE_MODE) { manage_listening_mode_flag(); } diff --git a/test/unit_tests/communication/CMakeLists.txt b/test/unit_tests/communication/CMakeLists.txt index a605fed74b..cae4771d93 100644 --- a/test/unit_tests/communication/CMakeLists.txt +++ b/test/unit_tests/communication/CMakeLists.txt @@ -17,6 +17,8 @@ add_executable( ${target_name} ${DEVICE_OS_DIR}/communication/src/firmware_update.cpp ${DEVICE_OS_DIR}/communication/src/description.cpp ${DEVICE_OS_DIR}/communication/src/protocol_util.cpp + ${DEVICE_OS_DIR}/communication/src/protocol_defs.cpp + ${DEVICE_OS_DIR}/communication/src/coap_channel_new.cpp ${DEVICE_OS_DIR}/services/src/system_error.cpp ${DEVICE_OS_DIR}/services/src/jsmn.c ${DEVICE_OS_DIR}/wiring/src/spark_wiring_json.cpp diff --git a/test/unit_tests/communication/hal_stubs.cpp b/test/unit_tests/communication/hal_stubs.cpp index ad6dec73a8..dd27920263 100644 --- a/test/unit_tests/communication/hal_stubs.cpp +++ b/test/unit_tests/communication/hal_stubs.cpp @@ -10,6 +10,12 @@ #include "logging.h" #include "diagnostics.h" +namespace particle::protocol { + +class Protocol; + +} // namespace particle::protocol + extern "C" uint32_t HAL_RNG_GetRandomNumber() { return rand(); @@ -44,3 +50,7 @@ extern "C" void log_write(int level, const char *category, const char *data, siz extern "C" int diag_register_source(const diag_source* src, void* reserved) { return 0; } + +extern "C" particle::protocol::Protocol* spark_protocol_instance(void) { + return nullptr; +} diff --git a/test/unit_tests/util/stream.h b/test/unit_tests/util/stream.h index 4e56227189..6b43b03a7c 100644 --- a/test/unit_tests/util/stream.h +++ b/test/unit_tests/util/stream.h @@ -1,11 +1,13 @@ #ifndef TEST_TOOLS_STREAM_H #define TEST_TOOLS_STREAM_H -#include "spark_wiring_print.h" +#include "spark_wiring_stream.h" #include "check.h" +#include #include +#include namespace test { @@ -27,6 +29,61 @@ class OutputStream: std::string s_; }; +class Stream: public ::Stream { +public: + explicit Stream(std::string data = std::string()) : + data_(std::move(data)), + readPos_(0) { + } + + size_t readBytes(char* data, size_t size) override { + size_t n = std::min(size, data_.size() - readPos_); + std::memcpy(data, data_.data() + readPos_, n); + readPos_ += n; + return n; + } + + int read() override { + uint8_t b; + size_t n = readBytes((char*)&b, 1); + if (n != 1) { + return -1; + } + return b; + } + + int peek() override { + if (data_.size() - readPos_ == 0) { + return -1; + } + return (uint8_t)data_.at(readPos_); + } + + int available() override { + return data_.size() - readPos_; + } + + size_t write(const uint8_t* data, size_t size) override { + data_.append((const char*)data, size); + return size; + } + + size_t write(uint8_t b) override { + return write(&b, 1); + } + + void flush() override { + } + + const std::string& data() const { + return data_; + } + +private: + std::string data_; + size_t readPos_; +}; + } // namespace test // test::OutputStream diff --git a/test/unit_tests/wiring/CMakeLists.txt b/test/unit_tests/wiring/CMakeLists.txt index b2c3ec91d7..d439a865cf 100644 --- a/test/unit_tests/wiring/CMakeLists.txt +++ b/test/unit_tests/wiring/CMakeLists.txt @@ -9,6 +9,7 @@ add_executable( ${target_name} ${DEVICE_OS_DIR}/services/src/jsmn.c ${DEVICE_OS_DIR}/wiring/src/spark_wiring_async.cpp ${DEVICE_OS_DIR}/wiring/src/spark_wiring_print.cpp + ${DEVICE_OS_DIR}/wiring/src/spark_wiring_stream.cpp ${DEVICE_OS_DIR}/wiring/src/spark_wiring_random.cpp ${DEVICE_OS_DIR}/wiring/src/spark_wiring_string.cpp ${DEVICE_OS_DIR}/wiring/src/spark_wiring_wifi.cpp @@ -23,6 +24,7 @@ add_executable( ${target_name} ${DEVICE_OS_DIR}/wiring/src/string_convert.cpp ${TEST_DIR}/util/alloc.cpp ${TEST_DIR}/util/buffer.cpp + ${TEST_DIR}/util/string.cpp ${TEST_DIR}/util/random_old.cpp ${TEST_DIR}/stub/system_network.cpp ${TEST_DIR}/stub/inet_hal_compat.cpp diff --git a/test/unit_tests/wiring/variant.cpp b/test/unit_tests/wiring/variant.cpp index de3be12cd2..05ac4fc264 100644 --- a/test/unit_tests/wiring/variant.cpp +++ b/test/unit_tests/wiring/variant.cpp @@ -1,8 +1,10 @@ #include -#include +#include #include "spark_wiring_variant.h" +#include "util/stream.h" +#include "util/string.h" #include "util/catch.h" using namespace particle; @@ -121,6 +123,19 @@ void checkVariant(Variant& v, const T& expectedValue = T()) { } } +std::string toCbor(const Variant& v) { + test::Stream s; + REQUIRE(encodeToCBOR(v, s) == 0); + return s.data(); +} + +Variant fromCbor(const std::string& data) { + test::Stream s(data); + Variant v; + REQUIRE(decodeFromCBOR(v, s) == 0); + return v; +} + } // namespace TEST_CASE("Variant") { @@ -330,4 +345,130 @@ TEST_CASE("Variant") { v = Variant::fromJSON("{\"a\":1,\"b\":2,\"c\":3}"); checkVariant(v, VariantMap{ { "a", 1 }, { "b", 2 }, { "c", 3 } }); } + + SECTION("encodeVariantToCBOR()") { + using test::toHex; + CHECK(toHex(toCbor(0)) == "00"); + CHECK(toHex(toCbor(1)) == "01"); + CHECK(toHex(toCbor(10)) == "0a"); + CHECK(toHex(toCbor(23)) == "17"); + CHECK(toHex(toCbor(24)) == "1818"); + CHECK(toHex(toCbor(25)) == "1819"); + CHECK(toHex(toCbor(100)) == "1864"); + CHECK(toHex(toCbor(1000)) == "1903e8"); + CHECK(toHex(toCbor(1000000)) == "1a000f4240"); + CHECK(toHex(toCbor(1000000000000ull)) == "1b000000e8d4a51000"); + CHECK(toHex(toCbor(18446744073709551615ull)) == "1bffffffffffffffff"); + CHECK(toHex(toCbor(-9223372036854775807ll - 1)) == "3b7fffffffffffffff"); + CHECK(toHex(toCbor(-1)) == "20"); + CHECK(toHex(toCbor(-10)) == "29"); + CHECK(toHex(toCbor(-100)) == "3863"); + CHECK(toHex(toCbor(-1000)) == "3903e7"); + CHECK(toHex(toCbor(0.0)) == "fa00000000"); // Encoding half-precision floats is not supported + CHECK(toHex(toCbor(-0.0)) == "fa80000000"); // ditto + CHECK(toHex(toCbor(1.0)) == "fa3f800000"); // ditto + CHECK(toHex(toCbor(1.1)) == "fb3ff199999999999a"); + CHECK(toHex(toCbor(1.5)) == "fa3fc00000"); // ditto + CHECK(toHex(toCbor(100000.0)) == "fa47c35000"); + CHECK(toHex(toCbor(16777216.0)) == "fa4b800000"); + CHECK(toHex(toCbor(3.4028234663852886e+38)) == "fa7f7fffff"); + CHECK(toHex(toCbor(1.0e+300)) == "fb7e37e43c8800759c"); + CHECK(toHex(toCbor(1.401298464324817e-45)) == "fa00000001"); + CHECK(toHex(toCbor(1.1754943508222875e-38)) == "fa00800000"); + CHECK(toHex(toCbor(-4.0)) == "fac0800000"); // ditto + CHECK(toHex(toCbor(-4.1)) == "fbc010666666666666"); + CHECK(toHex(toCbor(INFINITY)) == "fa7f800000"); // ditto + CHECK(toHex(toCbor(-INFINITY)) == "faff800000"); // ditto + CHECK(toHex(toCbor(NAN)) == "fb7ff8000000000000"); // For simplicity, NaN is always encoded as a double + CHECK(toHex(toCbor(false)) == "f4"); + CHECK(toHex(toCbor(true)) == "f5"); + CHECK(toHex(toCbor(Variant())) == "f6"); + CHECK(toHex(toCbor("")) == "60"); + CHECK(toHex(toCbor("a")) == "6161"); + CHECK(toHex(toCbor("IETF")) == "6449455446"); + CHECK(toHex(toCbor("\"\\")) == "62225c"); + CHECK(toHex(toCbor("\u00fc")) == "62c3bc"); + CHECK(toHex(toCbor("\u6c34")) == "63e6b0b4"); + CHECK(toHex(toCbor(VariantArray{})) == "80"); + CHECK(toHex(toCbor(VariantArray{1, 2, 3})) == "83010203"); + CHECK(toHex(toCbor(VariantArray{1, VariantArray{2, 3}, VariantArray{4, 5}})) == "8301820203820405"); + CHECK(toHex(toCbor(VariantArray{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25})) == "98190102030405060708090a0b0c0d0e0f101112131415161718181819"); + CHECK(toHex(toCbor(VariantMap{})) == "a0"); + CHECK(toHex(toCbor(VariantMap{{"a", 1}, {"b", VariantArray{2, 3}}})) == "a26161016162820203"); + CHECK(toHex(toCbor(VariantArray{"a", VariantMap{{"b", "c"}}})) == "826161a161626163"); + CHECK(toHex(toCbor(VariantMap{{"a", "A"}, {"b", "B"}, {"c", "C"}, {"d", "D"}, {"e", "E"}})) == "a56161614161626142616361436164614461656145"); + } + + SECTION("decodeVariantFromCBOR") { + using test::fromHex; + CHECK(fromCbor(fromHex("00")) == Variant(0)); + CHECK(fromCbor(fromHex("01")) == Variant(1)); + CHECK(fromCbor(fromHex("0a")) == Variant(10)); + CHECK(fromCbor(fromHex("17")) == Variant(23)); + CHECK(fromCbor(fromHex("1818")) == Variant(24)); + CHECK(fromCbor(fromHex("1819")) == Variant(25)); + CHECK(fromCbor(fromHex("1864")) == Variant(100)); + CHECK(fromCbor(fromHex("1903e8")) == Variant(1000)); + CHECK(fromCbor(fromHex("1a000f4240")) == Variant(1000000)); + CHECK(fromCbor(fromHex("1b000000e8d4a51000")) == Variant(1000000000000ull)); + CHECK(fromCbor(fromHex("1bffffffffffffffff")) == Variant(18446744073709551615ull)); + CHECK(fromCbor(fromHex("3b7fffffffffffffff")) == Variant(-9223372036854775807ll - 1)); + CHECK(fromCbor(fromHex("20")) == Variant(-1)); + CHECK(fromCbor(fromHex("29")) == Variant(-10)); + CHECK(fromCbor(fromHex("3863")) == Variant(-100)); + CHECK(fromCbor(fromHex("3903e7")) == Variant(-1000)); + CHECK(fromCbor(fromHex("f90000")) == Variant(0.0)); + CHECK(fromCbor(fromHex("f98000")) == Variant(-0.0)); + CHECK(fromCbor(fromHex("f93c00")) == Variant(1.0)); + CHECK(fromCbor(fromHex("fb3ff199999999999a")) == Variant(1.1)); + CHECK(fromCbor(fromHex("f93e00")) == Variant(1.5)); + CHECK(fromCbor(fromHex("f97bff")) == Variant(65504.0)); + CHECK(fromCbor(fromHex("fa47c35000")) == Variant(100000.0)); + CHECK(fromCbor(fromHex("fa7f7fffff")) == Variant(3.4028234663852886e+38)); + CHECK(fromCbor(fromHex("fb7e37e43c8800759c")) == Variant(1.0e+300)); + CHECK(fromCbor(fromHex("f90001")) == Variant(5.960464477539063e-8)); + CHECK(fromCbor(fromHex("f90400")) == Variant(0.00006103515625)); + CHECK(fromCbor(fromHex("f9c400")) == Variant(-4.0)); + CHECK(fromCbor(fromHex("fbc010666666666666")) == Variant(-4.1)); + CHECK(fromCbor(fromHex("f97c00")) == Variant(INFINITY)); + CHECK(std::isnan(fromCbor(fromHex("f97e00")).asDouble())); + CHECK(fromCbor(fromHex("f9fc00")) == Variant(-INFINITY)); + CHECK(fromCbor(fromHex("fa7f800000")) == Variant(INFINITY)); + CHECK(std::isnan(fromCbor(fromHex("fa7fc00000")).asDouble())); + CHECK(fromCbor(fromHex("faff800000")) == Variant(-INFINITY)); + CHECK(fromCbor(fromHex("fb7ff0000000000000")) == Variant(INFINITY)); + CHECK(std::isnan(fromCbor(fromHex("fb7ff8000000000000")).asDouble())); + CHECK(fromCbor(fromHex("fbfff0000000000000")) == Variant(-INFINITY)); + CHECK(fromCbor(fromHex("f4")) == Variant(false)); + CHECK(fromCbor(fromHex("f5")) == Variant(true)); + CHECK(fromCbor(fromHex("f6")) == Variant()); + CHECK(fromCbor(fromHex("c074323031332d30332d32315432303a30343a30305a")) == Variant("2013-03-21T20:04:00Z")); + CHECK(fromCbor(fromHex("c11a514b67b0")) == Variant(1363896240)); + CHECK(fromCbor(fromHex("c1fb41d452d9ec200000")) == Variant(1363896240.5)); + CHECK(fromCbor(fromHex("d82076687474703a2f2f7777772e6578616d706c652e636f6d")) == Variant("http://www.example.com")); + CHECK(fromCbor(fromHex("60")) == Variant("")); + CHECK(fromCbor(fromHex("6161")) == Variant("a")); + CHECK(fromCbor(fromHex("6449455446")) == Variant("IETF")); + CHECK(fromCbor(fromHex("62225c")) == Variant("\"\\")); + CHECK(fromCbor(fromHex("62c3bc")) == Variant("\u00fc")); + CHECK(fromCbor(fromHex("63e6b0b4")) == Variant("\u6c34")); + CHECK(fromCbor(fromHex("80")) == VariantArray{}); + CHECK(fromCbor(fromHex("83010203")) == VariantArray{1, 2, 3}); + CHECK(fromCbor(fromHex("8301820203820405")) == VariantArray{1, VariantArray{2, 3}, VariantArray{4, 5}}); + CHECK(fromCbor(fromHex("98190102030405060708090a0b0c0d0e0f101112131415161718181819")) == VariantArray{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25}); + CHECK(fromCbor(fromHex("a0")) == VariantMap{}); + CHECK(fromCbor(fromHex("a26161016162820203")) == VariantMap{{"a", 1}, {"b", VariantArray{2, 3}}}); + CHECK(fromCbor(fromHex("826161a161626163")) == VariantArray{"a", VariantMap{{"b", "c"}}}); + CHECK(fromCbor(fromHex("a56161614161626142616361436164614461656145")) == VariantMap{{"a", "A"}, {"b", "B"}, {"c", "C"}, {"d", "D"}, {"e", "E"}}); + CHECK(fromCbor(fromHex("7f657374726561646d696e67ff")) == Variant("streaming")); + CHECK(fromCbor(fromHex("9fff")) == VariantArray{}); + CHECK(fromCbor(fromHex("9f018202039f0405ffff")) == VariantArray{1, VariantArray{2, 3}, VariantArray{4, 5}}); + CHECK(fromCbor(fromHex("9f01820203820405ff")) == VariantArray{1, VariantArray{2, 3}, VariantArray{4, 5}}); + CHECK(fromCbor(fromHex("83018202039f0405ff")) == VariantArray{1, VariantArray{2, 3}, VariantArray{4, 5}}); + CHECK(fromCbor(fromHex("83019f0203ff820405")) == VariantArray{1, VariantArray{2, 3}, VariantArray{4, 5}}); + CHECK(fromCbor(fromHex("9f0102030405060708090a0b0c0d0e0f101112131415161718181819ff")) == VariantArray{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25}); + CHECK(fromCbor(fromHex("bf61610161629f0203ffff")) == VariantMap{{"a", 1}, {"b", VariantArray{2, 3}}}); + CHECK(fromCbor(fromHex("826161bf61626163ff")) == VariantArray{"a", VariantMap{{"b", "c"}}}); + CHECK(fromCbor(fromHex("bf6346756ef563416d7421ff")) == VariantMap{{"Fun", true}, {"Amt", -2}}); + } } diff --git a/user/tests/app/ledger_test/README.md b/user/tests/app/ledger_test/README.md new file mode 100644 index 0000000000..efd2baad81 --- /dev/null +++ b/user/tests/app/ledger_test/README.md @@ -0,0 +1,51 @@ +# Ledger Test + +This test application provides a control request interface for managing ledgers stored on the device. It is accompanied with a command line utility that provides a user interface for the functionality exposed by the application. + +## Building + +The application is built and flashed like any other test application: +```sh +cd path/to/device-os/main +make -s all program-dfu PLATFORM=boron TEST=app/ledger_test +``` + +## Installation + +The command line utility is internal to this repository and can be executed directly from the source tree. In this case, the package dependencies need to be installed: +```sh +cd path/to/device-os/user/tests/app/ledger_test/cli +npm install +./ledger --help +``` + +Alternatively, `npm link` can be used to create a global symlink to the utility: +```sh +npm link path/to/device-os/user/tests/app/ledger_test/cli +ledger --help +``` + +## Usage + +Make sure a Particle device running the test application is connected to the computer via USB. Note that the command line utility does not support interacting with multiple devices and expects exactly one device to be connected to the computer. + +Enumerating the ledgers stored on the device: +```sh +ledger list +``` + +Setting the contents of a ledger: +```sh +ledger set my_ledger '{ "key1": "value1", "key2": 123 }' +``` + +Getting the contents of a ledger: +```sh +ledger get my_ledger +``` + +See `ledger --help` for the full list of commands supported by the application. + +## Debugging + +Logging output is printed to the device's USB serial interface. The logging level can be changed at run time using the `debug` command. diff --git a/user/tests/app/ledger_test/app/config.cpp b/user/tests/app/ledger_test/app/config.cpp new file mode 100644 index 0000000000..9320e98680 --- /dev/null +++ b/user/tests/app/ledger_test/app/config.cpp @@ -0,0 +1,37 @@ +#include + +#include +#include // For `retained` + +#include "config.h" + +namespace particle::test { + +namespace { + +retained Config g_config; +retained uint32_t g_magic; + +} // namespace + +void Config::setRestoreConnectionFlag() { + wasConnected = Particle.connected(); + restoreConnection = true; +} + +Config& Config::get() { + if (g_magic != 0xcf9addedu) { + g_config = { + .autoConnect = true, + .restoreConnection = false, + .wasConnected = false, + .removeLedger = false, + .removeAllLedgers = false, + .debugEnabled = false + }; + g_magic = 0xcf9addedu; + } + return g_config; +} + +} // namespace particle::test diff --git a/user/tests/app/ledger_test/app/config.h b/user/tests/app/ledger_test/app/config.h new file mode 100644 index 0000000000..8ed1e1ef2e --- /dev/null +++ b/user/tests/app/ledger_test/app/config.h @@ -0,0 +1,22 @@ +#pragma once + +#include + +namespace particle::test { + +struct Config { + bool autoConnect; + bool restoreConnection; + bool wasConnected; + bool removeLedger; + bool removeAllLedgers; + bool debugEnabled; + + char removeLedgerName[LEDGER_MAX_NAME_LENGTH + 1]; + + void setRestoreConnectionFlag(); + + static Config& get(); +}; + +} // namespace particle::test diff --git a/user/tests/app/ledger_test/app/logger.cpp b/user/tests/app/ledger_test/app/logger.cpp new file mode 100644 index 0000000000..c9ca451136 --- /dev/null +++ b/user/tests/app/ledger_test/app/logger.cpp @@ -0,0 +1,80 @@ +#include +#include + +#include +#include +#include + +#include "logger.h" +#include "config.h" + +namespace particle::test { + +namespace { + +const auto LEDGER_CATEGORY = "system.ledger"; +const auto APP_CATEGORY = "app"; + +class AnsiLogHandler: public StreamLogHandler { +public: + using StreamLogHandler::StreamLogHandler; + +protected: + using StreamLogHandler::write; + + void logMessage(const char* msg, LogLevel level, const char* category, const LogAttributes& attr) override { + if (level >= LOG_LEVEL_ERROR) { + stream()->write("\033[31;1m"); // Red, bold + } else if (level >= LOG_LEVEL_WARN) { + stream()->write("\033[33;1m"); // Yellow, bold + } else if (category && std::strcmp(category, APP_CATEGORY) == 0) { + stream()->write("\033[32m"); // Green + } else if (category && std::strcmp(category, LEDGER_CATEGORY) == 0) { + stream()->write("\033[37m"); // White + } else { + stream()->write("\033[90m"); // Gray + } + writingMsg_ = true; + StreamLogHandler::logMessage(msg, level, category, attr); + writingMsg_ = false; + stream()->write("\033[0m"); // Reset + } + + void write(const char* data, size_t size) override { + if (!writingMsg_) { + stream()->write("\033[90m"); // Gray + } + StreamLogHandler::write(data, size); + if (!writingMsg_) { + stream()->write("\033[0m"); // Reset + } + } + +private: + bool writingMsg_ = false; +}; + +std::unique_ptr g_logHandler; + +} // namespace + +int initLogger() { + if (g_logHandler) { + LogManager::instance()->removeHandler(g_logHandler.get()); + g_logHandler.reset(); + } + auto& conf = Config::get(); + auto appLevel = conf.debugEnabled ? LOG_LEVEL_ALL : LOG_LEVEL_INFO; + auto defaultLevel = conf.debugEnabled ? LOG_LEVEL_ALL : LOG_LEVEL_WARN; + std::unique_ptr handler(new(std::nothrow) AnsiLogHandler(Serial, defaultLevel, { + { LEDGER_CATEGORY, appLevel }, + { APP_CATEGORY, appLevel } + })); + if (!handler || !LogManager::instance()->addHandler(handler.get())) { + return Error::NO_MEMORY; + } + g_logHandler = std::move(handler); + return 0; +} + +} // namespace particle::test diff --git a/user/tests/app/ledger_test/app/logger.h b/user/tests/app/ledger_test/app/logger.h new file mode 100644 index 0000000000..9c290f2585 --- /dev/null +++ b/user/tests/app/ledger_test/app/logger.h @@ -0,0 +1,7 @@ +#pragma once + +namespace particle::test { + +int initLogger(); + +} // namespace particle::test diff --git a/user/tests/app/ledger_test/app/main.cpp b/user/tests/app/ledger_test/app/main.cpp new file mode 100644 index 0000000000..7a19295b0e --- /dev/null +++ b/user/tests/app/ledger_test/app/main.cpp @@ -0,0 +1,76 @@ +#include + +#include "request_handler.h" +#include "logger.h" +#include "config.h" + +PRODUCT_VERSION(1) + +SYSTEM_MODE(SEMI_AUTOMATIC) +// SYSTEM_THREAD(ENABLED) + +using namespace particle::test; + +namespace { + +RequestHandler g_reqHandler; + +void onCloudStatus(system_event_t /* event */, int status) { + switch (status) { + case cloud_status_disconnected: { + Log.info("Disconnected"); + break; + } + case cloud_status_connecting: { + Log.info("Connecting"); + break; + } + case cloud_status_connected: { + Log.info("Connected"); + break; + } + case cloud_status_disconnecting: { + Log.info("Disconnecting"); + break; + } + default: + break; + } +} + +} // namespace + +void ctrl_request_custom_handler(ctrl_request* req) { + g_reqHandler.handleRequest(req); +} + +void setup() { + waitFor(Serial.isConnected, 3000); + int r = initLogger(); + SPARK_ASSERT(r == 0); + auto& conf = Config::get(); + if (conf.removeAllLedgers) { + conf.removeAllLedgers = false; + Log.info("Removing all ledgers"); + r = ledger_purge_all(nullptr); + if (r < 0) { + Log.error("ledger_purge_all() failed: %d", r); + } + } + if (conf.removeLedger) { + conf.removeLedger = false; + Log.info("Removing ledger: %s", conf.removeLedgerName); + r = ledger_purge(conf.removeLedgerName, nullptr); + if (r < 0) { + Log.error("ledger_purge() failed: %d", r); + } + } + System.on(cloud_status, onCloudStatus); + if ((!conf.restoreConnection && conf.autoConnect) || (conf.restoreConnection && conf.wasConnected)) { + Particle.connect(); + } + conf.restoreConnection = false; +} + +void loop() { +} diff --git a/user/tests/app/ledger_test/app/request_handler.cpp b/user/tests/app/ledger_test/app/request_handler.cpp new file mode 100644 index 0000000000..a287daac00 --- /dev/null +++ b/user/tests/app/ledger_test/app/request_handler.cpp @@ -0,0 +1,574 @@ +#include +#include + +#include +#include +#include +#include +#include +#include + +#include + +#include +#include +#include + +#include "request_handler.h" +#include "logger.h" +#include "config.h" + +namespace particle::test { + +namespace { + +const size_t LEDGER_READ_BLOCK_SIZE = 1024; + +enum Result { + RESET_PENDING = 1 +}; + +enum BinaryRequestType { + READ = 1, + WRITE = 2 +}; + +struct LedgerAppData { +}; + +// Completion handler for system_ctrl_set_result() +void systemResetCompletionHandler(int result, void* data) { + HAL_Delay_Milliseconds(1000); + System.reset(); +} + +void destroyLedgerAppData(void* appData) { + auto d = static_cast(appData); + delete d; +} + +int getLedgerInfo(ledger_info& info, ledger_instance* ledger) { + int r = ledger_get_info(ledger, &info, nullptr); + if (r < 0) { + Log.error("ledger_get_info() failed: %d", r); + } + return r; +} + +int getLedgerNames(char**& names, size_t& count) { + int r = ledger_get_names(&names, &count, nullptr); + if (r < 0) { + Log.error("ledger_get_names() failed: %d", r); + } + return r; +} + +int openLedgerStream(ledger_stream*& stream, ledger_instance* ledger, int mode) { + int r = ledger_open(&stream, ledger, mode, nullptr); + if (r < 0) { + Log.error("ledger_open() failed: %d", r); + } + return r; +} + +int closeLedgerStream(ledger_stream* stream, int flags = 0) { + int r = ledger_close(stream, flags, nullptr); + if (r < 0) { + Log.error("ledger_close() failed: %d", r); + } + return r; +} + +int readLedgerStream(ledger_stream* stream, char* data, size_t size) { + int r = ledger_read(stream, data, size, nullptr); + if (r < 0) { + Log.error("ledger_read() failed: %d", r); + } + return r; +} + +int writeLedgerStream(ledger_stream* stream, const char* data, size_t size) { + int r = ledger_write(stream, data, size, nullptr); + if (r < 0) { + Log.error("ledger_write() failed: %d", r); + } + return r; +} + +void ledgerSyncCallback(ledger_instance* ledger, void* appData) { + ledger_info info = {}; + info.version = LEDGER_API_VERSION; + int r = getLedgerInfo(info, ledger); + if (r < 0) { + return; + } + Log.info("Ledger synchronized: %s", info.name); +} + +int getLedger(ledger_instance*& ledger, const char* name) { + ledger_instance* lr = nullptr; + int r = ledger_get_instance(&lr, name, nullptr); + if (r < 0) { + Log.error("ledger_get_instance() failed: %d", r); + return r; + } + ledger_lock(lr, nullptr); + SCOPE_GUARD({ + ledger_unlock(lr, nullptr); + }); + auto appData = static_cast(ledger_get_app_data(lr, nullptr)); + if (!appData) { + appData = new(std::nothrow) LedgerAppData(); + if (!appData) { + return Error::NO_MEMORY; + } + ledger_callbacks callbacks = {}; + callbacks.version = LEDGER_API_VERSION; + callbacks.sync = ledgerSyncCallback; + ledger_set_callbacks(lr, &callbacks, nullptr); + ledger_set_app_data(lr, appData, destroyLedgerAppData, nullptr); + ledger_add_ref(lr, nullptr); // Keep the instance around + } + ledger = lr; + return 0; +} + +} // namespace + +class RequestHandler::JsonRequest { +public: + JsonRequest() : + req_(nullptr) { + } + + int init(ctrl_request* req) { + auto d = JSONValue::parse(req->request_data, req->request_size); + if (!d.isObject()) { + return Error::BAD_DATA; + } + data_ = std::move(d); + req_ = req; + return 0; + } + + template + int response(F fn) { + if (!req_) { + return Error::INVALID_STATE; + } + JSONBufferWriter writer(nullptr, 0); + fn(writer); + size_t size = writer.dataSize(); + CHECK(system_ctrl_alloc_reply_data(req_, size, nullptr)); + writer = JSONBufferWriter(req_->reply_data, req_->reply_size); + fn(writer); + if (writer.dataSize() != size) { + return Error::INTERNAL; + } + return 0; + } + + JSONValue get(const char* name) const { + JSONObjectIterator it(data_); + while (it.next()) { + if (it.name() == name) { + return it.value(); + } + } + return JSONValue(); + } + + bool has(const char* name) const { + return get(name).isValid(); + } + +private: + JSONValue data_; + ctrl_request* req_; +}; + +class RequestHandler::BinaryRequest { +public: + BinaryRequest() : + req_(nullptr), + data_(nullptr), + size_(0), + type_(0) { + } + + int init(ctrl_request* req) { + if (req->request_size < 4) { + return Error::BAD_DATA; + } + uint32_t type = 0; + std::memcpy(&type, req->request_data, 4); + type_ = bigEndianToNative(type); + data_ = req->request_data + 4; + size_ = req->request_size - 4; + req_ = req; + return 0; + } + + int allocResponse(char*& data, size_t size) { + CHECK(system_ctrl_alloc_reply_data(req_, size, nullptr)); + data = req_->reply_data; + return 0; + } + + void responseSize(size_t size) { + req_->reply_size = size; + } + + const char* data() const { + return data_; + } + + size_t size() const { + return size_; + } + + int type() const { + return type_; + } + +private: + ctrl_request* req_; + const char* data_; + size_t size_; + int type_; +}; + +RequestHandler::~RequestHandler() { + if (ledgerStream_) { + closeLedgerStream(ledgerStream_, LEDGER_STREAM_CLOSE_DISCARD); + } +} + +void RequestHandler::handleRequest(ctrl_request* req) { + int r = handleRequestImpl(req); + if (r < 0) { + Log.error("Error while handling control request: %d", r); + system_ctrl_alloc_reply_data(req, 0 /* size */, nullptr /* reserved */); + system_ctrl_set_result(req, r, nullptr /* handler */, nullptr /* data */, nullptr /* reserved */); + } +} + +int RequestHandler::handleRequestImpl(ctrl_request* req) { + Log.trace("Received request"); + auto size = req->request_size; + if (!size) { + return Error::NOT_ENOUGH_DATA; + } + int result = 0; + auto data = req->request_data; + if (data[0] == '{') { + Log.write(LOG_LEVEL_TRACE, data, size); + Log.print(LOG_LEVEL_TRACE, "\r\n"); + JsonRequest jsonReq; + CHECK(jsonReq.init(req)); + result = CHECK(handleJsonRequest(jsonReq)); + Log.trace("Sending response"); + if (req->reply_size > 0) { + Log.write(LOG_LEVEL_TRACE, req->reply_data, req->reply_size); + Log.print(LOG_LEVEL_TRACE, "\r\n"); + } + } else { + if (size > 0) { + Log.dump(LOG_LEVEL_TRACE, data, size); + Log.printf(LOG_LEVEL_TRACE, " (%u bytes)\r\n", (unsigned)size); + } + BinaryRequest binReq; + CHECK(binReq.init(req)); + result = CHECK(handleBinaryRequest(binReq)); + Log.trace("Sending response"); + if (req->reply_size > 0) { + Log.dump(LOG_LEVEL_TRACE, req->reply_data, req->reply_size); + Log.printf(LOG_LEVEL_TRACE, " (%u bytes)\r\n", (unsigned)req->reply_size); + } + } + auto handler = (result == Result::RESET_PENDING) ? systemResetCompletionHandler : nullptr; + system_ctrl_set_result(req, result, handler, nullptr /* data */, nullptr /* reserved */); + return 0; +} + +int RequestHandler::handleJsonRequest(JsonRequest& req) { + auto cmd = req.get("cmd").toString(); + if (cmd == "get") { + return get(req); + } else if (cmd == "set") { + return set(req); + } else if (cmd == "touch") { + return touch(req); + } else if (cmd == "list") { + return list(req); + } else if (cmd == "info") { + return info(req); + } else if (cmd == "reset") { + return reset(req); + } else if (cmd == "remove") { + return remove(req); + } else if (cmd == "connect") { + return connect(req); + } else if (cmd == "disconnect") { + return disconnect(req); + } else if (cmd == "auto_connect") { + return autoConnect(req); + } else if (cmd == "debug") { + return debug(req); + } else { + Log.error("Unknown command: \"%s\"", cmd.data()); + return Error::NOT_SUPPORTED; + } +} + +int RequestHandler::handleBinaryRequest(BinaryRequest& req) { + switch (req.type()) { + case BinaryRequestType::READ: + return read(req); + case BinaryRequestType::WRITE: + return write(req); + default: + Log.error("Unknown command: %d", req.type()); + return Error::NOT_SUPPORTED; + } +} + +int RequestHandler::get(JsonRequest& req) { + auto name = req.get("name").toString(); + Log.info("Getting ledger data: %s", name.data()); + if (ledgerStream_) { + Log.warn("\"get\" or \"set\" command is already in progress"); + CHECK(closeLedgerStream(ledgerStream_, LEDGER_STREAM_CLOSE_DISCARD)); + ledgerStream_ = nullptr; + } + ledger_instance* ledger = nullptr; + CHECK(getLedger(ledger, name.data())); + ledger_lock(ledger, nullptr); + SCOPE_GUARD({ + ledger_unlock(ledger, nullptr); + ledger_release(ledger, nullptr); + }); + ledger_info info = {}; + info.version = LEDGER_API_VERSION; + CHECK(getLedgerInfo(info, ledger)); + ledger_stream* stream = nullptr; + CHECK(openLedgerStream(stream, ledger, LEDGER_STREAM_MODE_READ)); + NAMED_SCOPE_GUARD(closeStreamGuard, { + closeLedgerStream(stream, LEDGER_STREAM_CLOSE_DISCARD); + }); + CHECK(req.response([&](auto& w) { + w.beginObject(); + w.name("size").value(info.data_size); + w.endObject(); + })); + if (info.data_size > 0) { + ledgerStream_ = stream; + ledgerBytesLeft_ = info.data_size; + closeStreamGuard.dismiss(); + } + return 0; +} + +int RequestHandler::set(JsonRequest& req) { + auto name = req.get("name").toString(); + Log.info("Setting ledger data: %s", name.data()); + if (ledgerStream_) { + Log.warn("\"get\" or \"set\" command is already in progress"); + CHECK(closeLedgerStream(ledgerStream_, LEDGER_STREAM_CLOSE_DISCARD)); + ledgerStream_ = nullptr; + } + auto size = req.get("size").toInt(); + if (size < 0) { + Log.error("Invalid size of ledger data"); + return Error::BAD_DATA; + } + ledger_instance* ledger = nullptr; + CHECK(getLedger(ledger, name.data())); + SCOPE_GUARD({ + ledger_release(ledger, nullptr); + }); + ledger_stream* stream = nullptr; + CHECK(openLedgerStream(stream, ledger, LEDGER_STREAM_MODE_WRITE)); + if (size > 0) { + ledgerStream_ = stream; + ledgerBytesLeft_ = size; + } else { + CHECK(closeLedgerStream(stream)); + } + return 0; +} + +int RequestHandler::touch(JsonRequest& req) { + auto name = req.get("name").toString(); + Log.info("Getting ledger instance: %s", name.data()); + ledger_instance* ledger = nullptr; + CHECK(getLedger(ledger, name.data())); + ledger_release(ledger, nullptr); + return 0; +} + +int RequestHandler::list(JsonRequest& req) { + Log.info("Enumerating ledgers"); + char** names = nullptr; + size_t count = 0; + CHECK(getLedgerNames(names, count)); + SCOPE_GUARD({ + for (size_t i = 0; i < count; ++i) { + std::free(names[i]); + } + std::free(names); + }); + CHECK(req.response([&](auto& w) { + w.beginArray(); + for (size_t i = 0; i < count; ++i) { + w.value(names[i]); + } + w.endArray(); + })); + return 0; +} + +int RequestHandler::info(JsonRequest& req) { + auto name = req.get("name").toString(); + Log.info("Getting ledger info: %s", name.data()); + ledger_instance* ledger = nullptr; + CHECK(getLedger(ledger, name.data())); + SCOPE_GUARD({ + ledger_release(ledger, nullptr); + }); + ledger_info info = {}; + info.version = LEDGER_API_VERSION; + CHECK(getLedgerInfo(info, ledger)); + CHECK(req.response([&](auto& w) { + w.beginObject(); + w.name("last_updated").value(0); // FIXME + w.name("last_synced").value(0); // FIXME + w.name("data_size").value(info.data_size); + w.name("scope").value(info.scope); + w.name("sync_direction").value(info.sync_direction); + w.endObject(); + })); + return 0; +} + +int RequestHandler::reset(JsonRequest& req) { + Log.info("Resetting device"); + return Result::RESET_PENDING; +} + +int RequestHandler::remove(JsonRequest& req) { + auto& conf = Config::get(); + if (conf.removeLedger || conf.removeAllLedgers) { + Log.warn("\"remove\" command is already in progress"); + } + auto removeAll = req.get("all").toBool(); + if (removeAll) { + conf.removeAllLedgers = true; + } else { + auto name = req.get("name").toString(); + if (name.isEmpty()) { + Log.error("Ledger name is missing"); + return Error::BAD_DATA; + } + size_t n = strlcpy(conf.removeLedgerName, name.data(), sizeof(conf.removeLedgerName)); + if (n >= sizeof(conf.removeLedgerName)) { + Log.error("Ledger name is too long"); + return Error::BAD_DATA; + } + conf.removeLedger = true; + } + conf.setRestoreConnectionFlag(); + return Result::RESET_PENDING; +} + +int RequestHandler::connect(JsonRequest& req) { + Particle.connect(); + return 0; +} + +int RequestHandler::disconnect(JsonRequest& req) { + Network.off(); + return 0; +} + +int RequestHandler::autoConnect(JsonRequest& req) { + auto enabled = req.get("enabled"); + auto& conf = Config::get(); + conf.autoConnect = enabled.isValid() ? enabled.toBool() : true; + if (conf.autoConnect) { + Log.info("Enabled auto-connect"); + Particle.connect(); + } else { + Log.info("Disabled auto-connect"); + } + return 0; +} + +int RequestHandler::debug(JsonRequest& req) { + auto enabled = req.get("enabled"); + auto& conf = Config::get(); + conf.debugEnabled = enabled.isValid() ? enabled.toBool() : true; + if (conf.debugEnabled) { + Log.info("Enabled debug"); + } else { + Log.info("Disabled debug"); + } + CHECK(initLogger()); + return 0; +} + +int RequestHandler::read(BinaryRequest& req) { + int r = readImpl(req); + if (r < 0 && ledgerStream_) { + closeLedgerStream(ledgerStream_, LEDGER_STREAM_CLOSE_DISCARD); + ledgerStream_ = nullptr; + } + return r; +} + +int RequestHandler::readImpl(BinaryRequest& req) { + if (!ledgerStream_) { + Log.error("Ledger is not open"); + return Error::INVALID_STATE; + } + char* data = nullptr; + CHECK(req.allocResponse(data, LEDGER_READ_BLOCK_SIZE)); + size_t n = std::min(ledgerBytesLeft_, LEDGER_READ_BLOCK_SIZE); + CHECK(readLedgerStream(ledgerStream_, data, n)); + req.responseSize(n); + ledgerBytesLeft_ -= n; + if (!ledgerBytesLeft_) { + closeLedgerStream(ledgerStream_); // Don't use CHECK here + ledgerStream_ = nullptr; + } + return 0; +} + +int RequestHandler::write(BinaryRequest& req) { + int r = writeImpl(req); + if (r < 0 && ledgerStream_) { + closeLedgerStream(ledgerStream_, LEDGER_STREAM_CLOSE_DISCARD); + ledgerStream_ = nullptr; + } + return r; +} + +int RequestHandler::writeImpl(BinaryRequest& req) { + if (!ledgerStream_) { + Log.error("Ledger is not open"); + return Error::INVALID_STATE; + } + if (req.size() > ledgerBytesLeft_) { + Log.error("Unexpected size of ledger data"); + return Error::TOO_LARGE; + } + CHECK(writeLedgerStream(ledgerStream_, req.data(), req.size())); + ledgerBytesLeft_ -= req.size(); + if (!ledgerBytesLeft_) { + closeLedgerStream(ledgerStream_); // Don't use CHECK here + ledgerStream_ = nullptr; + } + return 0; +} + +} // namespace particle::test diff --git a/user/tests/app/ledger_test/app/request_handler.h b/user/tests/app/ledger_test/app/request_handler.h new file mode 100644 index 0000000000..f497ec124f --- /dev/null +++ b/user/tests/app/ledger_test/app/request_handler.h @@ -0,0 +1,48 @@ +#pragma once + +#include +#include + +namespace particle::test { + +class RequestHandler { +public: + RequestHandler() : + ledgerStream_(nullptr), + ledgerBytesLeft_(0) { + } + + ~RequestHandler(); + + void handleRequest(ctrl_request* req); + +private: + class JsonRequest; + class BinaryRequest; + + ledger_stream* ledgerStream_; + size_t ledgerBytesLeft_; + + int handleRequestImpl(ctrl_request* req); + int handleJsonRequest(JsonRequest& req); + int handleBinaryRequest(BinaryRequest& req); + + int get(JsonRequest& req); + int set(JsonRequest& req); + int touch(JsonRequest& req); + int list(JsonRequest& req); + int info(JsonRequest& req); + int reset(JsonRequest& req); + int remove(JsonRequest& req); + int connect(JsonRequest& req); + int disconnect(JsonRequest& req); + int autoConnect(JsonRequest& req); + int debug(JsonRequest& req); + + int read(BinaryRequest& req); + int readImpl(BinaryRequest& req); + int write(BinaryRequest& req); + int writeImpl(BinaryRequest& req); +}; + +} // namespace particle::test diff --git a/user/tests/app/ledger_test/build.mk b/user/tests/app/ledger_test/build.mk new file mode 100644 index 0000000000..cc42966a01 --- /dev/null +++ b/user/tests/app/ledger_test/build.mk @@ -0,0 +1 @@ +CPPSRC += $(call target_files,$(MODULE_PATH)/$(USRSRC)/app/,*.cpp) diff --git a/user/tests/app/ledger_test/cli/errors.js b/user/tests/app/ledger_test/cli/errors.js new file mode 100644 index 0000000000..e675d80074 --- /dev/null +++ b/user/tests/app/ledger_test/cli/errors.js @@ -0,0 +1,53 @@ +const fs = require('fs'); +const path = require('path'); + +const deviceOsErrors = (() => { + const headerFile = 'services/inc/system_error.h'; + try { + const src = fs.readFileSync(path.join(__dirname, '../../../../..', headerFile), 'utf-8'); + let r = src.match(/#define\s+SYSTEM_ERRORS\b.*?\n/); + if (!r) { + throw new Error(); + } + let line = r[0]; + let pos = r.index + line.length; + const lines = [line]; + while (/^.*?\\\s*?\n$/.test(line)) { // Ends with '\' followed by a newline + line = src.slice(pos, src.indexOf('\n', pos) + 1); + pos += line.length; + lines.push(line); + } + const errors = new Map(); + for (const line of lines) { + const matches = line.matchAll(/\(\s*(\w+)\s*,\s*"(.*?)"\s*,\s*(-\d+)\s*\)/g); + for (const m of matches) { + const name = m[1]; + const message = m[2]; + const code = Number.parseInt(m[3]); + if (Number.isNaN(code)) { + throw new Error(); + } + errors.set(code, { name, message }); + } + } + if (!errors.size) { + throw new Error(); + } + return errors; + } catch (err) { + console.error(`Failed to parse ${headerFile}`); + return new Map(); + } +})(); + +function deviceOsErrorCodeToString(code) { + const err = deviceOsErrors.get(code); + if (!err) { + return `Unknown error (${code})`; + } + return `${err.message} (${err.name})`; +} + +module.exports = { + deviceOsErrorCodeToString +}; diff --git a/user/tests/app/ledger_test/cli/ledger b/user/tests/app/ledger_test/cli/ledger new file mode 100755 index 0000000000..b7094141e3 --- /dev/null +++ b/user/tests/app/ledger_test/cli/ledger @@ -0,0 +1,424 @@ +#!/usr/bin/env node +const { randomObject, timestampToString } = require('./util'); +const { deviceOsErrorCodeToString } = require('./errors'); +const { name: PACKAGE_NAME, description: PACKAGE_DESC } = require('./package.json'); + +const { getDevices } = require('particle-usb'); +const cbor = require('cbor'); +const parseArgs = require('minimist'); +const _ = require('lodash'); + +const fs = require('fs'); + +const WRITE_BLOCK_SIZE = 1024; +const REQUEST_TYPE = 10; // ctrl_request_type::CTRL_REQUEST_APP_CUSTOM + +const BinaryRequestType = { + READ: 1, + WRITE: 2 +}; + +function printUsage() { + console.log(`\ +${PACKAGE_DESC} + +Usage: + ${PACKAGE_NAME} [arguments...] + +Commands: + get [--expect[=]] + Get ledger data. + set [|--size=] + Set ledger data. \`data\` can be a filename or string containing a JSON document. If neither + \`data\` nor \`size\` is provided, the data is read from the standard input. + touch + Create a ledger if it doesn't exist. + info [] [--raw] + Get ledger info. If \`name\` is not provided, returns the info for all ledgers. + list [--raw] + List the ledgers. + remove |--all + Remove a specific ledger or all ledgers. + connect + Connect to the Cloud. + disconnect + Disconnect from the Cloud. + auto-connect [1|0] + Enable/disable automatic connection to the Cloud. + debug [1|0] + Enable/disable debug logging. + reset + Reset the device. + gen + Generate and print random ledger data of \`size\` bytes. + +Options: + --expect[=] + Exit with an error if the ledger data doesn't match the expected data. \`data\` can be a + filename or JSON string. If not specified, the data is read from the standard input. + --size=, -n + Generate random ledger data of \`size\` bytes. + --all + Run the command for all ledgers. + --raw + Output the raw response data received from the device.`); +} + +function scopeName(scope) { + switch (scope) { + case 0: return 'Unknown'; + case 1: return 'Device'; + case 2: return 'Product'; + case 3: return 'Owner'; + default: return `Unknown (${scope})`; + } +} + +function syncDirectionName(dir) { + switch (dir) { + case 0: return 'Unknown'; + case 1: return 'Device-to-cloud'; + case 2: return 'Cloud-to-device'; + default: return `Unknown (${dir})`; + } +} + +function readJsonObject(arg) { + let data; + try { + if (!arg) { + data = fs.readFileSync(0, 'utf8'); + } else if (arg.trimStart().startsWith('{')) { + data = arg; + } else { + data = fs.readFileSync(arg, 'utf8'); + } + } catch (err) { + throw new Error('Failed to load JSON', { cause: err }); + } + try { + data = JSON.parse(data); + } catch (err) { + throw new Error('Failed to parse JSON', { cause: err }); + } + if (!_.isPlainObject(data)) { + throw new Error('JSON is not an object'); + } + return data; +} + +async function openDevice() { + const devs = await getDevices(); + if (!devs.length) { + throw new Error('No devices found'); + } + if (devs.length !== 1) { + throw new Error('Multiple devices found'); + } + const dev = devs[0]; + await dev.open(); + return dev; +} + +async function sendRequest(dev, data) { + let resp; + try { + resp = await dev.sendControlRequest(REQUEST_TYPE, data); + } catch (err) { + throw new Error('Failed to send control request', { cause: err }); + } + if (resp.result < 0) { + const msg = deviceOsErrorCodeToString(resp.result); + throw new Error(msg); + } + return resp.data || null; +} + +async function sendJsonRequest(dev, data) { + data = Buffer.from(JSON.stringify(data)); + let resp = await sendRequest(dev, data); + if (resp) { + resp = JSON.parse(resp.toString()); + } + return resp; +} + +async function sendBinaryRequest(dev, type, data) { + let buf = Buffer.alloc(4); + buf.writeUInt32BE(type, 0); + if (data) { + buf = Buffer.concat([buf, data]); + } + return await sendRequest(dev, buf); +} + +async function get(args, opts, dev) { + if (!args.length) { + throw new Error('Missing ledger name'); + } + let resp = await sendJsonRequest(dev, { + cmd: 'get', + name: args[0] + }); + const size = resp.size; + let data = Buffer.alloc(0); + while (data.length < size) { + resp = await sendBinaryRequest(dev, BinaryRequestType.READ); + data = Buffer.concat([data, resp]); + } + if (data.length) { + try { + data = await cbor.decodeFirst(data); + } catch (err) { + throw new Error('Failed to parse CBOR', { cause: err }); + } + } else { + data = {}; + } + if (opts.expect !== undefined) { + const expected = readJsonObject(opts.expect); + if (!_.isEqual(data, expected)) { + throw new Error('Ledger data doesn\'t match the expected data'); + } + console.error('Ledger data matches the expected data'); + } else { + console.log(JSON.stringify(data)); + } +} + +async function set(args, opts, dev) { + if (!args.length) { + throw new Error('Missing ledger name'); + } + let obj; + if (opts.size !== undefined) { + obj = await randomObject(opts.size); + } else { + obj = readJsonObject(args[1]); + } + const data = await cbor.encodeAsync(obj); + await sendJsonRequest(dev, { + cmd: 'set', + name: args[0], + size: data.length + }); + let offs = 0; + while (offs < data.length) { + const n = Math.min(data.length - offs, WRITE_BLOCK_SIZE); + await sendBinaryRequest(dev, BinaryRequestType.WRITE, data.slice(offs, offs + n)); + offs += n; + } + if (opts.size !== undefined) { + console.log(JSON.stringify(obj)); + } +} + +async function touch(args, opts, dev) { + if (!args.length) { + throw new Error('Missing ledger name'); + } + await sendJsonRequest(dev, { + cmd: 'touch', + name: args[0] + }); +} + +async function info(args, opts, dev) { + function pad(str) { + return str.padEnd(20); + } + + function formatInfo(info) { + return `\ +${pad('Scope:')}${scopeName(info.scope)} +${pad('Sync direction:')}${syncDirectionName(info.sync_direction)} +${pad('Data size:')}${info.data_size} +${pad('Last update:')}${info.last_updated ? timestampToString(info.last_updated) : 'N/A'} +${pad('Last sync:')}${info.last_synced ? timestampToString(info.last_synced) : 'N/A'}`; + } + + if (args.length > 0) { + const info = await sendJsonRequest(dev, { + cmd: 'info', + name: args[0] + }); + if (opts.raw) { + console.log(JSON.stringify(info)); + } else { + console.log(formatInfo(info)); + } + } else { + const ledgerNames = await sendJsonRequest(dev, { + cmd: 'list' + }); + const infoList = []; + for (const name of ledgerNames) { + const info = await sendJsonRequest(dev, { + cmd: 'info', + name + }); + info.name = name; + infoList.push(info); + } + if (opts.raw) { + console.log(JSON.stringify(infoList)); + } else { + let out = ''; + for (const info of infoList) { + out += `${pad('Name:')}${info.name}\n`; + out += formatInfo(info); + out += '\n\n'; + } + console.log(out.trimRight()); + } + } +} + +async function list(args, opts, dev) { + const resp = await sendJsonRequest(dev, { + cmd: 'list' + }); + if (opts.raw) { + console.log(JSON.stringify(resp)); + } else { + for (const name of resp) { + console.log(name); + } + } +} + +async function remove(args, opts, dev) { + const req = { + cmd: 'remove' + }; + if (opts.all) { + req.all = true; + } else { + if (!args.length) { + throw new Error('Missing ledger name'); + } + req.name = args[0]; + } + await sendJsonRequest(dev, req); +} + +async function connect(args, opts, dev) { + await sendJsonRequest(dev, { + cmd: 'connect' + }); +} + +async function disconnect(args, opts, dev) { + await sendJsonRequest(dev, { + cmd: 'disconnect' + }); +} + +async function autoConnect(args, opts, dev) { + let enabled = true; + if (args.length) { + enabled = !!Number.parseInt(args[0]); + } + await sendJsonRequest(dev, { + cmd: 'auto_connect', + enabled + }); +} + +async function debug(args, opts, dev) { + let enabled = true; + if (args.length) { + enabled = !!Number.parseInt(args[0]); + } + await sendJsonRequest(dev, { + cmd: 'debug', + enabled + }); +} + +async function reset(args, opts, dev) { + await sendJsonRequest(dev, { + cmd: 'reset' + }); +} + +async function gen(args, opts) { + if (!args.length) { + throw new Error('Missing data size'); + } + const size = Number.parseInt(args[0]); + if (Number.isNaN(size)) { + throw new Error('Invalid data size'); + } + const obj = await randomObject(size); + console.log(JSON.stringify(obj)); +} + +async function runCommand(cmd, args, opts) { + let fn; + let needDev = true; + switch (cmd) { + case 'get': fn = get; break; + case 'set': fn = set; break; + case 'touch': fn = touch; break; + case 'info': fn = info; break; + case 'list': fn = list; break; + case 'remove': fn = remove; break; + case 'connect': fn = connect; break; + case 'disconnect': fn = disconnect; break; + case 'auto-connect': fn = autoConnect; break; + case 'debug': fn = debug; break; + case 'reset': fn = reset; break; + case 'gen': fn = gen; needDev = false; break; + default: + throw new Error(`Unknown command: ${cmd}`) + } + let dev; + if (needDev) { + dev = await openDevice(); + } + try { + await fn(args, opts, dev); + } finally { + if (dev) { + await dev.close(); + } + } +} + +async function run() { + let ok = true; + try { + const opts = parseArgs(process.argv.slice(2), { + string: ['_', 'expect'], + boolean: ['all', 'raw', 'help'], + number: ['size'], + alias: { + 'help': 'h', + 'size': 'n' + }, + unknown: arg => { + if (arg.startsWith('-')) { + throw new RangeError(`Unknown argument: ${arg}`); + } + } + }); + if (opts.help) { + printUsage(); + } else { + const args = opts._; + delete opts._; + if (!args.length) { + throw new Error('Missing command name'); + } + const cmd = args.shift(); + await runCommand(cmd, args, opts); + } + } catch (err) { + console.error(err); + ok = false; + } + process.exit(ok ? 0 : 1); +} + +run(); diff --git a/user/tests/app/ledger_test/cli/npm-shrinkwrap.json b/user/tests/app/ledger_test/cli/npm-shrinkwrap.json new file mode 100644 index 0000000000..d4047f31ac --- /dev/null +++ b/user/tests/app/ledger_test/cli/npm-shrinkwrap.json @@ -0,0 +1,393 @@ +{ + "name": "ledger", + "lockfileVersion": 2, + "requires": true, + "packages": { + "": { + "name": "ledger", + "dependencies": { + "cbor": "^9.0.1", + "lodash": "^4.17.21", + "minimist": "^1.2.8", + "particle-usb": "^2.4.1" + }, + "bin": { + "ledger": "ledger" + } + }, + "node_modules/@particle/device-constants": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/@particle/device-constants/-/device-constants-3.3.0.tgz", + "integrity": "sha512-QAx7j77A2ADyVq/vtEzuhrbFM3JXn5gfmC6OpvCuFbLvGoGNcZHBdCaf+y1krrFG9PiWcezNEmhGdkIGtjGsWA==", + "peer": true, + "engines": { + "node": ">=12.x", + "npm": "8.x" + } + }, + "node_modules/@particle/device-os-protobuf": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@particle/device-os-protobuf/-/device-os-protobuf-1.2.1.tgz", + "integrity": "sha512-Y1T7LkZ1LtNAM/DC4nF6kUKpr9DYYvBDRBZikyv0APup01qW8MaB0/Rzdf+4tumHRBKRCA3zFmPGz56ietnTgQ==", + "dependencies": { + "protobufjs": "^6.11.2" + } + }, + "node_modules/@protobufjs/aspromise": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/aspromise/-/aspromise-1.1.2.tgz", + "integrity": "sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ==" + }, + "node_modules/@protobufjs/base64": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/base64/-/base64-1.1.2.tgz", + "integrity": "sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg==" + }, + "node_modules/@protobufjs/codegen": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/@protobufjs/codegen/-/codegen-2.0.4.tgz", + "integrity": "sha512-YyFaikqM5sH0ziFZCN3xDC7zeGaB/d0IUb9CATugHWbd1FRFwWwt4ld4OYMPWu5a3Xe01mGAULCdqhMlPl29Jg==" + }, + "node_modules/@protobufjs/eventemitter": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/eventemitter/-/eventemitter-1.1.0.tgz", + "integrity": "sha512-j9ednRT81vYJ9OfVuXG6ERSTdEL1xVsNgqpkxMsbIabzSo3goCjDIveeGv5d03om39ML71RdmrGNjG5SReBP/Q==" + }, + "node_modules/@protobufjs/fetch": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/fetch/-/fetch-1.1.0.tgz", + "integrity": "sha512-lljVXpqXebpsijW71PZaCYeIcE5on1w5DlQy5WH6GLbFryLUrBD4932W/E2BSpfRJWseIL4v/KPgBFxDOIdKpQ==", + "dependencies": { + "@protobufjs/aspromise": "^1.1.1", + "@protobufjs/inquire": "^1.1.0" + } + }, + "node_modules/@protobufjs/float": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@protobufjs/float/-/float-1.0.2.tgz", + "integrity": "sha512-Ddb+kVXlXst9d+R9PfTIxh1EdNkgoRe5tOX6t01f1lYWOvJnSPDBlG241QLzcyPdoNTsblLUdujGSE4RzrTZGQ==" + }, + "node_modules/@protobufjs/inquire": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/inquire/-/inquire-1.1.0.tgz", + "integrity": "sha512-kdSefcPdruJiFMVSbn801t4vFK7KB/5gd2fYvrxhuJYg8ILrmn9SKSX2tZdV6V+ksulWqS7aXjBcRXl3wHoD9Q==" + }, + "node_modules/@protobufjs/path": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/path/-/path-1.1.2.tgz", + "integrity": "sha512-6JOcJ5Tm08dOHAbdR3GrvP+yUUfkjG5ePsHYczMFLq3ZmMkAD98cDgcT2iA1lJ9NVwFd4tH/iSSoe44YWkltEA==" + }, + "node_modules/@protobufjs/pool": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/pool/-/pool-1.1.0.tgz", + "integrity": "sha512-0kELaGSIDBKvcgS4zkjz1PeddatrjYcmMWOlAuAPwAeccUrPHdUqo/J6LiymHHEiJT5NrF1UVwxY14f+fy4WQw==" + }, + "node_modules/@protobufjs/utf8": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/utf8/-/utf8-1.1.0.tgz", + "integrity": "sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw==" + }, + "node_modules/@types/long": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@types/long/-/long-4.0.2.tgz", + "integrity": "sha512-MqTGEo5bj5t157U6fA/BiDynNkn0YknVdh48CMPkTSpFTVmvao5UQmm7uEF6xBEo7qIMAlY/JSleYaE6VOdpaA==" + }, + "node_modules/@types/node": { + "version": "20.8.8", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.8.8.tgz", + "integrity": "sha512-YRsdVxq6OaLfmR9Hy816IMp33xOBjfyOgUd77ehqg96CFywxAPbDbXvAsuN2KVg2HOT8Eh6uAfU+l4WffwPVrQ==", + "dependencies": { + "undici-types": "~5.25.1" + } + }, + "node_modules/@types/w3c-web-usb": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/@types/w3c-web-usb/-/w3c-web-usb-1.0.9.tgz", + "integrity": "sha512-6EIpb9g9k/SGu59mQ6RW3tedmabtE+N3iGRa98+1CCFuhGt565wLEYKXoEVKTuNrCr2SrgfvBMN5db6hggkzKQ==" + }, + "node_modules/cbor": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/cbor/-/cbor-9.0.1.tgz", + "integrity": "sha512-/TQOWyamDxvVIv+DY9cOLNuABkoyz8K/F3QE56539pGVYohx0+MEA1f4lChFTX79dBTBS7R1PF6ovH7G+VtBfQ==", + "dependencies": { + "nofilter": "^3.1.0" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/lodash": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==" + }, + "node_modules/long": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/long/-/long-4.0.0.tgz", + "integrity": "sha512-XsP+KhQif4bjX1kbuSiySJFNAehNxgLb6hPRGJ9QsUr8ajHkuXGdrHmFUTUUXhDwVX2R5bY4JNZEwbUiMhV+MA==" + }, + "node_modules/minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/node-addon-api": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-7.0.0.tgz", + "integrity": "sha512-vgbBJTS4m5/KkE16t5Ly0WW9hz46swAstv0hYYwMtbG7AznRhNyfLRe8HZAiWIpcHzoO7HxhLuBQj9rJ/Ho0ZA==" + }, + "node_modules/node-gyp-build": { + "version": "4.6.1", + "resolved": "https://registry.npmjs.org/node-gyp-build/-/node-gyp-build-4.6.1.tgz", + "integrity": "sha512-24vnklJmyRS8ViBNI8KbtK/r/DmXQMRiOMXTNz2nrTnAYUwjmEEbnnpB/+kt+yWRv73bPsSPRFddrcIbAxSiMQ==", + "bin": { + "node-gyp-build": "bin.js", + "node-gyp-build-optional": "optional.js", + "node-gyp-build-test": "build-test.js" + } + }, + "node_modules/nofilter": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/nofilter/-/nofilter-3.1.0.tgz", + "integrity": "sha512-l2NNj07e9afPnhAhvgVrCD/oy2Ai1yfLpuo3EpiO1jFTsB4sFz6oIfAfSZyQzVpkZQ9xS8ZS5g1jCBgq4Hwo0g==", + "engines": { + "node": ">=12.19" + } + }, + "node_modules/particle-usb": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/particle-usb/-/particle-usb-2.4.1.tgz", + "integrity": "sha512-xzeVuxFeHcEKwSO9nVdGo7emMRW/pCbUc/Zm/ORCaetFIHWWNCP+7CMrwzrHTDlSvngftrJx2h+qbPqgoKM06Q==", + "dependencies": { + "@particle/device-os-protobuf": "^1.2.1", + "protobufjs": "^6.11.3", + "usb": "^2.11.0" + }, + "engines": { + "node": ">=12", + "npm": "8.x" + }, + "peerDependencies": { + "@particle/device-constants": "^3.1.8" + } + }, + "node_modules/protobufjs": { + "version": "6.11.4", + "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-6.11.4.tgz", + "integrity": "sha512-5kQWPaJHi1WoCpjTGszzQ32PG2F4+wRY6BmAT4Vfw56Q2FZ4YZzK20xUYQH4YkfehY1e6QSICrJquM6xXZNcrw==", + "hasInstallScript": true, + "dependencies": { + "@protobufjs/aspromise": "^1.1.2", + "@protobufjs/base64": "^1.1.2", + "@protobufjs/codegen": "^2.0.4", + "@protobufjs/eventemitter": "^1.1.0", + "@protobufjs/fetch": "^1.1.0", + "@protobufjs/float": "^1.0.2", + "@protobufjs/inquire": "^1.1.0", + "@protobufjs/path": "^1.1.2", + "@protobufjs/pool": "^1.1.0", + "@protobufjs/utf8": "^1.1.0", + "@types/long": "^4.0.1", + "@types/node": ">=13.7.0", + "long": "^4.0.0" + }, + "bin": { + "pbjs": "bin/pbjs", + "pbts": "bin/pbts" + } + }, + "node_modules/undici-types": { + "version": "5.25.3", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.25.3.tgz", + "integrity": "sha512-Ga1jfYwRn7+cP9v8auvEXN1rX3sWqlayd4HP7OKk4mZWylEmu3KzXDUGrQUN6Ol7qo1gPvB2e5gX6udnyEPgdA==" + }, + "node_modules/usb": { + "version": "2.11.0", + "resolved": "https://registry.npmjs.org/usb/-/usb-2.11.0.tgz", + "integrity": "sha512-u5+NZ6DtoW8TIBtuSArQGAZZ/K15i3lYvZBAYmcgI+RcDS9G50/KPrUd3CrU8M92ahyCvg5e0gc8BDvr5Hwejg==", + "hasInstallScript": true, + "dependencies": { + "@types/w3c-web-usb": "^1.0.6", + "node-addon-api": "^7.0.0", + "node-gyp-build": "^4.5.0" + }, + "engines": { + "node": ">=12.22.0 <13.0 || >=14.17.0" + } + } + }, + "dependencies": { + "@particle/device-constants": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/@particle/device-constants/-/device-constants-3.3.0.tgz", + "integrity": "sha512-QAx7j77A2ADyVq/vtEzuhrbFM3JXn5gfmC6OpvCuFbLvGoGNcZHBdCaf+y1krrFG9PiWcezNEmhGdkIGtjGsWA==", + "peer": true + }, + "@particle/device-os-protobuf": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@particle/device-os-protobuf/-/device-os-protobuf-1.2.1.tgz", + "integrity": "sha512-Y1T7LkZ1LtNAM/DC4nF6kUKpr9DYYvBDRBZikyv0APup01qW8MaB0/Rzdf+4tumHRBKRCA3zFmPGz56ietnTgQ==", + "requires": { + "protobufjs": "^6.11.2" + } + }, + "@protobufjs/aspromise": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/aspromise/-/aspromise-1.1.2.tgz", + "integrity": "sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ==" + }, + "@protobufjs/base64": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/base64/-/base64-1.1.2.tgz", + "integrity": "sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg==" + }, + "@protobufjs/codegen": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/@protobufjs/codegen/-/codegen-2.0.4.tgz", + "integrity": "sha512-YyFaikqM5sH0ziFZCN3xDC7zeGaB/d0IUb9CATugHWbd1FRFwWwt4ld4OYMPWu5a3Xe01mGAULCdqhMlPl29Jg==" + }, + "@protobufjs/eventemitter": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/eventemitter/-/eventemitter-1.1.0.tgz", + "integrity": "sha512-j9ednRT81vYJ9OfVuXG6ERSTdEL1xVsNgqpkxMsbIabzSo3goCjDIveeGv5d03om39ML71RdmrGNjG5SReBP/Q==" + }, + "@protobufjs/fetch": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/fetch/-/fetch-1.1.0.tgz", + "integrity": "sha512-lljVXpqXebpsijW71PZaCYeIcE5on1w5DlQy5WH6GLbFryLUrBD4932W/E2BSpfRJWseIL4v/KPgBFxDOIdKpQ==", + "requires": { + "@protobufjs/aspromise": "^1.1.1", + "@protobufjs/inquire": "^1.1.0" + } + }, + "@protobufjs/float": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@protobufjs/float/-/float-1.0.2.tgz", + "integrity": "sha512-Ddb+kVXlXst9d+R9PfTIxh1EdNkgoRe5tOX6t01f1lYWOvJnSPDBlG241QLzcyPdoNTsblLUdujGSE4RzrTZGQ==" + }, + "@protobufjs/inquire": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/inquire/-/inquire-1.1.0.tgz", + "integrity": "sha512-kdSefcPdruJiFMVSbn801t4vFK7KB/5gd2fYvrxhuJYg8ILrmn9SKSX2tZdV6V+ksulWqS7aXjBcRXl3wHoD9Q==" + }, + "@protobufjs/path": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/path/-/path-1.1.2.tgz", + "integrity": "sha512-6JOcJ5Tm08dOHAbdR3GrvP+yUUfkjG5ePsHYczMFLq3ZmMkAD98cDgcT2iA1lJ9NVwFd4tH/iSSoe44YWkltEA==" + }, + "@protobufjs/pool": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/pool/-/pool-1.1.0.tgz", + "integrity": "sha512-0kELaGSIDBKvcgS4zkjz1PeddatrjYcmMWOlAuAPwAeccUrPHdUqo/J6LiymHHEiJT5NrF1UVwxY14f+fy4WQw==" + }, + "@protobufjs/utf8": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/utf8/-/utf8-1.1.0.tgz", + "integrity": "sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw==" + }, + "@types/long": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@types/long/-/long-4.0.2.tgz", + "integrity": "sha512-MqTGEo5bj5t157U6fA/BiDynNkn0YknVdh48CMPkTSpFTVmvao5UQmm7uEF6xBEo7qIMAlY/JSleYaE6VOdpaA==" + }, + "@types/node": { + "version": "20.8.8", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.8.8.tgz", + "integrity": "sha512-YRsdVxq6OaLfmR9Hy816IMp33xOBjfyOgUd77ehqg96CFywxAPbDbXvAsuN2KVg2HOT8Eh6uAfU+l4WffwPVrQ==", + "requires": { + "undici-types": "~5.25.1" + } + }, + "@types/w3c-web-usb": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/@types/w3c-web-usb/-/w3c-web-usb-1.0.9.tgz", + "integrity": "sha512-6EIpb9g9k/SGu59mQ6RW3tedmabtE+N3iGRa98+1CCFuhGt565wLEYKXoEVKTuNrCr2SrgfvBMN5db6hggkzKQ==" + }, + "cbor": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/cbor/-/cbor-9.0.1.tgz", + "integrity": "sha512-/TQOWyamDxvVIv+DY9cOLNuABkoyz8K/F3QE56539pGVYohx0+MEA1f4lChFTX79dBTBS7R1PF6ovH7G+VtBfQ==", + "requires": { + "nofilter": "^3.1.0" + } + }, + "lodash": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==" + }, + "long": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/long/-/long-4.0.0.tgz", + "integrity": "sha512-XsP+KhQif4bjX1kbuSiySJFNAehNxgLb6hPRGJ9QsUr8ajHkuXGdrHmFUTUUXhDwVX2R5bY4JNZEwbUiMhV+MA==" + }, + "minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==" + }, + "node-addon-api": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-7.0.0.tgz", + "integrity": "sha512-vgbBJTS4m5/KkE16t5Ly0WW9hz46swAstv0hYYwMtbG7AznRhNyfLRe8HZAiWIpcHzoO7HxhLuBQj9rJ/Ho0ZA==" + }, + "node-gyp-build": { + "version": "4.6.1", + "resolved": "https://registry.npmjs.org/node-gyp-build/-/node-gyp-build-4.6.1.tgz", + "integrity": "sha512-24vnklJmyRS8ViBNI8KbtK/r/DmXQMRiOMXTNz2nrTnAYUwjmEEbnnpB/+kt+yWRv73bPsSPRFddrcIbAxSiMQ==" + }, + "nofilter": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/nofilter/-/nofilter-3.1.0.tgz", + "integrity": "sha512-l2NNj07e9afPnhAhvgVrCD/oy2Ai1yfLpuo3EpiO1jFTsB4sFz6oIfAfSZyQzVpkZQ9xS8ZS5g1jCBgq4Hwo0g==" + }, + "particle-usb": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/particle-usb/-/particle-usb-2.4.1.tgz", + "integrity": "sha512-xzeVuxFeHcEKwSO9nVdGo7emMRW/pCbUc/Zm/ORCaetFIHWWNCP+7CMrwzrHTDlSvngftrJx2h+qbPqgoKM06Q==", + "requires": { + "@particle/device-os-protobuf": "^1.2.1", + "protobufjs": "^6.11.3", + "usb": "^2.11.0" + } + }, + "protobufjs": { + "version": "6.11.4", + "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-6.11.4.tgz", + "integrity": "sha512-5kQWPaJHi1WoCpjTGszzQ32PG2F4+wRY6BmAT4Vfw56Q2FZ4YZzK20xUYQH4YkfehY1e6QSICrJquM6xXZNcrw==", + "requires": { + "@protobufjs/aspromise": "^1.1.2", + "@protobufjs/base64": "^1.1.2", + "@protobufjs/codegen": "^2.0.4", + "@protobufjs/eventemitter": "^1.1.0", + "@protobufjs/fetch": "^1.1.0", + "@protobufjs/float": "^1.0.2", + "@protobufjs/inquire": "^1.1.0", + "@protobufjs/path": "^1.1.2", + "@protobufjs/pool": "^1.1.0", + "@protobufjs/utf8": "^1.1.0", + "@types/long": "^4.0.1", + "@types/node": ">=13.7.0", + "long": "^4.0.0" + } + }, + "undici-types": { + "version": "5.25.3", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.25.3.tgz", + "integrity": "sha512-Ga1jfYwRn7+cP9v8auvEXN1rX3sWqlayd4HP7OKk4mZWylEmu3KzXDUGrQUN6Ol7qo1gPvB2e5gX6udnyEPgdA==" + }, + "usb": { + "version": "2.11.0", + "resolved": "https://registry.npmjs.org/usb/-/usb-2.11.0.tgz", + "integrity": "sha512-u5+NZ6DtoW8TIBtuSArQGAZZ/K15i3lYvZBAYmcgI+RcDS9G50/KPrUd3CrU8M92ahyCvg5e0gc8BDvr5Hwejg==", + "requires": { + "@types/w3c-web-usb": "^1.0.6", + "node-addon-api": "^7.0.0", + "node-gyp-build": "^4.5.0" + } + } + } +} diff --git a/user/tests/app/ledger_test/cli/package.json b/user/tests/app/ledger_test/cli/package.json new file mode 100644 index 0000000000..d77600007a --- /dev/null +++ b/user/tests/app/ledger_test/cli/package.json @@ -0,0 +1,13 @@ +{ + "name": "ledger", + "description": "Ledger Test Utility", + "private": true, + "main": "ledger", + "bin": "ledger", + "dependencies": { + "cbor": "^9.0.1", + "lodash": "^4.17.21", + "minimist": "^1.2.8", + "particle-usb": "^2.4.1" + } +} diff --git a/user/tests/app/ledger_test/cli/util.js b/user/tests/app/ledger_test/cli/util.js new file mode 100644 index 0000000000..03af13442a --- /dev/null +++ b/user/tests/app/ledger_test/cli/util.js @@ -0,0 +1,105 @@ +const cbor = require('cbor'); + +function randomInt(min, max) { + if (min > max) { + const m = min; + min = max; + max = m; + } + return min + Math.floor(Math.random() * (max - min + 1)); +} + +function randomString(len) { + const alpha = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789_'; + let s = ''; + for (let i = 0; i < len; ++i) { + s += alpha.charAt(Math.floor(Math.random() * alpha.length)); + } + return s; +} + +async function randomObject(size) { + let obj = {}; + let objSize; + + async function set(key, val) { + obj[key] = val; + objSize = (await cbor.encodeAsync(obj)).length; // Synchronous variant of this function doesn't work with large objects + } + async function remove(key) { + delete obj[key]; + objSize = (await cbor.encodeAsync(obj)).length; + } + + // Start with a small non-empty object + let fillKey = randomString(1); + await set(fillKey, ''); + const minObjSize = objSize; + if (size < minObjSize) { + throw new Error(`Minimum data size is ${minObjSize} bytes`); + } + let minEntryLen; + let maxEntryLen; + if (size < 200) { + // Note that everything in this algorithm is completely arbitrary + minEntryLen = 5; + maxEntryLen = 20; + } else if (size < 2000) { + minEntryLen = 10; + maxEntryLen = 50; + } else { + minEntryLen = 30; + maxEntryLen = 120; + } + // Fill the object with random entries + let key; + do { + const entryLen = randomInt(minEntryLen, maxEntryLen); + const keyLen = randomInt(1, Math.ceil(entryLen / 2)); + do { + key = randomString(keyLen); + } while (key in obj); + let val; + const valLen = entryLen - keyLen; + if (valLen > minObjSize && Math.random() < 0.25) { + val = await randomObject(valLen); + } else { + val = randomString(valLen); + } + await set(key, val); + } while (objSize <= size); + await remove(key); + // Object size is at or somewhat below the requested size now + let fillVal = randomString(size - objSize); + await set(fillKey, fillVal); + if (objSize > size) { + // Encoding the value length required some extra bytes + fillVal = randomString(fillVal.length - (objSize - size)); + await set(fillKey, fillVal); + if (objSize < size) { + // The value length was encoded with fewer bytes again. The reserved key is 1 character long + // so extending it by a few characters is unlikely to exceed the requested size + await remove(fillKey); + do { + fillKey = randomString(size - objSize + 1); + } while (fillKey in obj); + await set(fillKey, fillVal); + } + } + // Sanity check + if (objSize != size) { + throw new Error('Failed to generate object of specified size'); + } + return obj; +} + +function timestampToString(time) { + return new Date(time).toISOString(); +} + +module.exports = { + randomInt, + randomString, + randomObject, + timestampToString +}; diff --git a/wiring/inc/spark_wiring_cloud.h b/wiring/inc/spark_wiring_cloud.h index b397587653..242626ffec 100644 --- a/wiring/inc/spark_wiring_cloud.h +++ b/wiring/inc/spark_wiring_cloud.h @@ -35,6 +35,8 @@ #include "spark_wiring_async.h" #include "spark_wiring_flags.h" #include "spark_wiring_platform.h" +#include "spark_wiring_vector.h" +#include "spark_wiring_error.h" #include "spark_wiring_global.h" #include "interrupts_hal.h" #include "system_mode.h" @@ -434,6 +436,7 @@ class CloudClass { static int maxFunctionArgumentSize(); #if Wiring_Ledger + /** * Get a ledger instance. * @@ -441,6 +444,27 @@ class CloudClass { * @return Ledger instance. */ static particle::Ledger ledger(const char* name); + + /** + * Remove any ledgers not in the list from the device. + * + * The device must not be connected to the Cloud. The operation will fail if any of the ledgers + * to be removed is in use. + * + * @note The data is not guaranteed to be removed in an irrecoverable way. + * + * @param ... Ledger names. + * @return 0 on success, otherwise an error code defined by `Error::Type`. + */ + template + static int useLedgers(ArgsT&&... args) { + Vector names; + if (!names.reserve(sizeof...(ArgsT))) { + return particle::Error::NO_MEMORY; + } + return useLedgersImpl(names, std::forward(args)...); + } + #endif // Wiring_Ledger private: @@ -552,6 +576,16 @@ class CloudClass { } return ok; } + + template + static int useLedgersImpl(Vector& names, const char* name, ArgsT&&... args) { + if (!names.append(name)) { + return particle::Error::NO_MEMORY; + } + return useLedgersImpl(names, std::forward(args)...); + } + + static int useLedgersImpl(const Vector& names); }; extern CloudClass Spark __attribute__((deprecated("Spark is now Particle."))); diff --git a/wiring/inc/spark_wiring_ledger.h b/wiring/inc/spark_wiring_ledger.h index 3bd6dc5a35..1ea34900b1 100644 --- a/wiring/inc/spark_wiring_ledger.h +++ b/wiring/inc/spark_wiring_ledger.h @@ -247,6 +247,39 @@ class Ledger { } ///@} + /** + * Get the names of the ledgers stored on the device. + * + * @param[out] names Ledger names. + * @return 0 on success, otherwise an error code defined by `Error::Type`. + */ + static int getNames(Vector& names); + + /** + * Remove any data associated with a ledger from the device. + * + * The device must not be connected to the Cloud. The operation will fail if the ledger with the + * given name is in use. + * + * @note The data is not guaranteed to be removed in an irrecoverable way. + * + * @param name Ledger name. + * @return 0 on success, otherwise an error code defined by `Error::Type`. + */ + static int remove(const char* name); + + /** + * Remove any ledger data from the device. + * + * The device must not be connected to the Cloud. The operation will fail if any of the ledgers + * is in use. + * + * @note The data is not guaranteed to be removed in an irrecoverable way. + * + * @return 0 on success, otherwise an error code defined by `Error::Type`. + */ + static int removeAll(); + friend void swap(Ledger& ledger1, Ledger& ledger2) { using std::swap; swap(ledger1.instance_, ledger2.instance_); diff --git a/wiring/inc/spark_wiring_stream.h b/wiring/inc/spark_wiring_stream.h index 2547aa758e..c821e1cdd1 100644 --- a/wiring/inc/spark_wiring_stream.h +++ b/wiring/inc/spark_wiring_stream.h @@ -79,7 +79,7 @@ class Stream : public Print float parseFloat(); // float version of parseInt - size_t readBytes( char *buffer, size_t length); // read chars from stream into buffer + virtual size_t readBytes( char *buffer, size_t length); // read chars from stream into buffer // terminates if length characters have been read or timeout (see setTimeout) // returns the number of characters placed in the buffer (0 means no valid data found) diff --git a/wiring/inc/spark_wiring_variant.h b/wiring/inc/spark_wiring_variant.h index 7c551c95c6..946d10f63c 100644 --- a/wiring/inc/spark_wiring_variant.h +++ b/wiring/inc/spark_wiring_variant.h @@ -29,6 +29,9 @@ #include "debug.h" +class Stream; +class Print; + namespace spark { class JSONValue; @@ -1034,4 +1037,22 @@ inline bool operator!=(const T& val, const Variant& var) { return var != val; } +/** + * Encode a variant to CBOR. + * + * @param var Variant. + * @param stream Output stream. + * @return 0 on success, otherwise an error code defined by `Error::Type`. + */ +int encodeToCBOR(const Variant& var, Print& stream); + +/** + * Decode a variant from CBOR. + * + * @param[out] var Variant. + * @param stream Input stream. + * @return 0 on success, otherwise an error code defined by `Error::Type`. + */ +int decodeFromCBOR(Variant& var, Stream& stream); + } // namespace particle diff --git a/wiring/src/spark_wiring_cloud.cpp b/wiring/src/spark_wiring_cloud.cpp index 467809a783..aea33fa5bb 100644 --- a/wiring/src/spark_wiring_cloud.cpp +++ b/wiring/src/spark_wiring_cloud.cpp @@ -139,4 +139,26 @@ Ledger CloudClass::ledger(const char* name) { return Ledger(instance, false /* addRef */); } +int CloudClass::useLedgersImpl(const Vector& usedNames) { + Vector allNames; + CHECK(Ledger::getNames(allNames)); + int result = 0; + for (auto& name: allNames) { + bool found = false; + for (auto usedName: usedNames) { + if (name == usedName) { + found = true; + break; + } + } + if (!found) { + int r = Ledger::remove(name); + if (r < 0 && result >= 0) { + result = r; + } + } + } + return result; +} + #endif // Wiring_Ledger diff --git a/wiring/src/spark_wiring_ledger.cpp b/wiring/src/spark_wiring_ledger.cpp index 0d5c22dc5b..3573fc4e9c 100644 --- a/wiring/src/spark_wiring_ledger.cpp +++ b/wiring/src/spark_wiring_ledger.cpp @@ -20,10 +20,11 @@ #if Wiring_Ledger #include +#include #include "spark_wiring_ledger.h" -#include "spark_wiring_print.h" +#include "spark_wiring_stream.h" #include "spark_wiring_error.h" #include "system_task.h" @@ -35,6 +36,95 @@ namespace particle { namespace { +class LedgerStream: public Stream { +public: + explicit LedgerStream(ledger_instance* ledger) : + ledger_(ledger), + stream_(nullptr) { + } + + ~LedgerStream() { + close(LEDGER_STREAM_CLOSE_DISCARD); + } + + int read() override { + uint8_t b; + size_t n = readBytes((char*)&b, 1); + if (n != 1) { + return -1; + } + return b; + } + + size_t readBytes(char* data, size_t size) override { + if (!stream_ || getWriteError() < 0) { + return 0; + } + int r = ledger_read(stream_, data, size, nullptr); + if (r < 0) { + LOG(ERROR, "ledger_read() failed: %d", r); + setWriteError(r); // TODO: Rename to setError() or add an alias + return 0; + } + return size; + } + + size_t write(uint8_t b) override { + return write(&b, 1); + } + + size_t write(const uint8_t* data, size_t size) override { + if (!stream_ || getWriteError() < 0) { + return 0; + } + int r = ledger_write(stream_, (const char*)data, size, nullptr); + if (r < 0) { + LOG(ERROR, "ledger_write() failed: %d", r); + setWriteError(r); + return 0; + } + return size; + } + + int available() override { + return -1; // Not supported + } + + int peek() override { + return -1; // Not supported + } + + void flush() override { + } + + int open(int mode) { + close(LEDGER_STREAM_CLOSE_DISCARD); + int r = ledger_open(&stream_, ledger_, mode, nullptr); + if (r < 0) { + LOG(ERROR, "ledger_open() failed: %d", r); + return r; + } + return 0; + } + + int close(int flags = 0) { + if (!stream_) { + return 0; + } + int r = ledger_close(stream_, flags, nullptr); + if (r < 0) { + LOG(ERROR, "ledger_close() failed: %d", r); + } + clearWriteError(); + stream_ = nullptr; + return r; + } + +private: + ledger_instance* ledger_; + ledger_stream* stream_; +}; + struct LedgerAppData { Ledger::OnSyncFunction onSync; }; @@ -115,63 +205,40 @@ int getLedgerInfo(ledger_instance* ledger, ledger_info& info) { } int setLedgerData(ledger_instance* ledger, const LedgerData& data) { - ledger_stream* stream = nullptr; - int r = ledger_open(&stream, ledger, LEDGER_STREAM_MODE_WRITE, nullptr); + LedgerStream stream(ledger); + CHECK(stream.open(LEDGER_STREAM_MODE_WRITE)); + int r = encodeToCBOR(data.variant(), stream); if (r < 0) { - LOG(ERROR, "ledger_open() failed: %d", r); - return r; - } - NAMED_SCOPE_GUARD(g, { - ledger_close(stream, LEDGER_STREAM_CLOSE_DISCARD, nullptr); - }); - // TODO: Use a binary format - auto json = data.toJSON(); - r = ledger_write(stream, json.c_str(), json.length(), nullptr); - if (r < 0) { - LOG(ERROR, "ledger_write() failed: %d", r); - return r; - } - g.dismiss(); - r = ledger_close(stream, 0, nullptr); - if (r < 0) { - LOG(ERROR, "ledger_close() failed: %d", r); + int err = stream.getWriteError(); + if (err < 0) { + r = err; + } + LOG(ERROR, "Failed to encode ledger data: %d", r); return r; } + CHECK(stream.close()); // Flush the data return 0; } int getLedgerData(ledger_instance* ledger, LedgerData& data) { - ledger_stream* stream = nullptr; - int r = ledger_open(&stream, ledger, LEDGER_STREAM_MODE_READ, nullptr); + LedgerStream stream(ledger); + CHECK(stream.open(LEDGER_STREAM_MODE_READ)); + Variant v; + int r = decodeFromCBOR(v, stream); if (r < 0) { - LOG(ERROR, "ledger_open() failed: %d", r); - return r; - } - NAMED_SCOPE_GUARD(g, { - ledger_close(stream, 0, nullptr); - }); - // TODO: Use a binary format - String str; - OutputStringStream strStream(str); - char buf[128]; - for (;;) { - r = ledger_read(stream, buf, sizeof(buf), nullptr); - if (r < 0) { - if (r == SYSTEM_ERROR_END_OF_STREAM) { - break; - } - LOG(ERROR, "ledger_read() failed: %d", r); - return r; + int err = stream.getWriteError(); + if (err < 0) { + r = err; } - strStream.write((uint8_t*)buf, r); - } - g.dismiss(); - r = ledger_close(stream, 0, nullptr); - if (r < 0) { - LOG(ERROR, "ledger_close() failed: %d", r); + // TODO: Don't log an error if the ledger data is empty + LOG(ERROR, "Failed to decode ledger data: %d", r); return r; } - data = Variant::fromJSON(str); + if (!v.isMap()) { + LOG(ERROR, "Unexpected type of ledger data"); + return Error::BAD_DATA; + } + data = std::move(v); return 0; } @@ -276,6 +343,52 @@ int Ledger::onSync(OnSyncFunction callback) { return setSyncCallback(instance_, std::move(callback)); } +int Ledger::getNames(Vector& namesVec) { + char** names = nullptr; + size_t count = 0; + int r = ledger_get_names(&names, &count, nullptr); + if (r < 0) { + LOG(ERROR, "ledger_get_names() failed: %d", r); + return r; + } + SCOPE_GUARD({ + for (size_t i = 0; i < count; ++i) { + std::free(names[i]); + } + std::free(names); + }); + namesVec.clear(); + if (!namesVec.reserve(count)) { + return Error::NO_MEMORY; + } + for (size_t i = 0; i < count; ++i) { + String name(names[i]); + if (!name.length()) { + return Error::NO_MEMORY; + } + namesVec.append(std::move(name)); + } + return 0; +} + +int Ledger::remove(const char* name) { + int r = ledger_purge(name, nullptr); + if (r < 0) { + LOG(ERROR, "ledger_purge() failed: %d", r); + return r; + } + return 0; +} + +int Ledger::removeAll() { + int r = ledger_purge_all(nullptr); + if (r < 0) { + LOG(ERROR, "ledger_purge_all() failed: %d", r); + return r; + } + return 0; +} + } // namespace particle #endif // Wiring_Ledger diff --git a/wiring/src/spark_wiring_variant.cpp b/wiring/src/spark_wiring_variant.cpp index e34b2fbef5..4e1737a853 100644 --- a/wiring/src/spark_wiring_variant.cpp +++ b/wiring/src/spark_wiring_variant.cpp @@ -15,24 +15,507 @@ * License along with this library; if not, see . */ +#include #include #include +#include +#include #include #include #include "spark_wiring_variant.h" #include "spark_wiring_json.h" -#include "spark_wiring_print.h" +#include "spark_wiring_stream.h" +#include "spark_wiring_error.h" + +#include "endian_util.h" +#include "check.h" namespace particle { namespace { -bool variantFromJSON(const JSONValue& val, Variant& var) { +class DecodingStream { +public: + explicit DecodingStream(Stream& stream) : + stream_(stream) { + } + + int readUint8(uint8_t& val) { + CHECK(read((char*)&val, sizeof(val))); + return 0; + } + + int readUint16Be(uint16_t& val) { + CHECK(read((char*)&val, sizeof(val))); + val = bigEndianToNative(val); + return 0; + } + + int readUint32Be(uint32_t& val) { + CHECK(read((char*)&val, sizeof(val))); + val = bigEndianToNative(val); + return 0; + } + + int readUint64Be(uint64_t& val) { + CHECK(read((char*)&val, sizeof(val))); + val = bigEndianToNative(val); + return 0; + } + + int read(char* data, size_t size) { + size_t n = stream_.readBytes(data, size); + if (n != size) { + return Error::IO; + } + return 0; + } + +private: + Stream& stream_; +}; + +class EncodingStream { +public: + explicit EncodingStream(Print& stream) : + stream_(stream) { + } + + int writeUint8(uint8_t val) { + CHECK(write((const char*)&val, sizeof(val))); + return 0; + } + + int writeUint16Be(uint16_t val) { + val = nativeToBigEndian(val); + CHECK(write((const char*)&val, sizeof(val))); + return 0; + } + + int writeUint32Be(uint32_t val) { + val = nativeToBigEndian(val); + CHECK(write((const char*)&val, sizeof(val))); + return 0; + } + + int writeUint64Be(uint64_t val) { + val = nativeToBigEndian(val); + CHECK(write((const char*)&val, sizeof(val))); + return 0; + } + + int writeFloatBe(float val) { + uint32_t v; + static_assert(sizeof(v) == sizeof(val)); + std::memcpy(&v, &val, sizeof(val)); + v = nativeToBigEndian(v); + CHECK(write((const char*)&v, sizeof(v))); + return 0; + } + + int writeDoubleBe(double val) { + uint64_t v; + static_assert(sizeof(v) == sizeof(val)); + std::memcpy(&v, &val, sizeof(val)); + v = nativeToBigEndian(v); + CHECK(write((const char*)&v, sizeof(v))); + return 0; + } + + int write(const char* data, size_t size) { + size_t n = stream_.write((const uint8_t*)data, size); + if (n != size) { + int err = stream_.getWriteError(); + return (err < 0) ? err : Error::IO; + } + return 0; + } + +private: + Print& stream_; +}; + +struct CborHead { + uint64_t arg; + int type; + int detail; +}; + +int readAndAppendToString(DecodingStream& stream, size_t size, String& str) { + if (!str.reserve(str.length() + size)) { + return Error::NO_MEMORY; + } + char buf[128]; + while (size > 0) { + size_t n = std::min(size, sizeof(buf)); + CHECK(stream.read(buf, n)); + str.concat(buf, n); + size -= n; + } + return 0; +} + +int readCborHead(DecodingStream& stream, CborHead& head) { + uint8_t b; + CHECK(stream.readUint8(b)); + head.type = b >> 5; + head.detail = b & 0x1f; + if (head.detail < 24) { + head.arg = head.detail; + } else { + switch (head.detail) { + case 24: { // 1-byte argument + uint8_t v; + CHECK(stream.readUint8(v)); + head.arg = v; + break; + } + case 25: { // 2-byte argument + uint16_t v; + CHECK(stream.readUint16Be(v)); + head.arg = v; + break; + } + case 26: { // 4-byte argument + uint32_t v; + CHECK(stream.readUint32Be(v)); + head.arg = v; + break; + } + case 27: { // 8-byte argument + CHECK(stream.readUint64Be(head.arg)); + break; + } + case 31: { // Indefinite length indicator or stop code + if (head.type == 0 /* Unsigned integer */ || head.type == 1 /* Negative integer */ || head.type == 6 /* Tagged item */) { + return Error::BAD_DATA; + } + head.arg = 0; + break; + } + default: // Reserved (28-30) + return Error::BAD_DATA; + } + } + return 0; +} + +int writeCborHeadWithArgument(EncodingStream& stream, int type, uint64_t arg) { + type <<= 5; + if (arg < 24) { + CHECK(stream.writeUint8(arg | type)); + } else if (arg <= 0xff) { + CHECK(stream.writeUint8(24 /* 1-byte argument */ | type)); + CHECK(stream.writeUint8(arg)); + } else if (arg <= 0xffff) { + CHECK(stream.writeUint8(25 /* 2-byte argument */ | type)); + CHECK(stream.writeUint16Be(arg)); + } else if (arg <= 0xffffffffu) { + CHECK(stream.writeUint8(26 /* 4-byte argument */ | type)); + CHECK(stream.writeUint32Be(arg)); + } else { + CHECK(stream.writeUint8(27 /* 8-byte argument */ | type)); + CHECK(stream.writeUint64Be(arg)); + } + return 0; +} + +int writeCborUnsignedInteger(EncodingStream& stream, uint64_t val) { + CHECK(writeCborHeadWithArgument(stream, 0 /* Unsigned integer */, val)); + return 0; +} + +int writeCborSignedInteger(EncodingStream& stream, int64_t val) { + if (val < 0) { + val = -(val + 1); + CHECK(writeCborHeadWithArgument(stream, 1 /* Negative integer */, val)); + } else { + CHECK(writeCborHeadWithArgument(stream, 0 /* Unsigned integer */, val)); + } + return 0; +} + +int readCborString(DecodingStream& stream, const CborHead& head, String& str) { + String s; + if (head.detail == 31 /* Indefinite length */) { + for (;;) { + CborHead h; + CHECK(readCborHead(stream, h)); + if (h.type == 7 /* Misc. items */ && h.detail == 31 /* Stop code */) { + break; + } + if (h.type != 3 /* Text string */ || h.detail == 31 /* Indefinite length */) { // Chunks of indefinite length are not permitted + return Error::BAD_DATA; + } + if (h.arg > std::numeric_limits::max()) { + return Error::OUT_OF_RANGE; + } + CHECK(readAndAppendToString(stream, h.arg, s)); + } + } else { + if (head.arg > std::numeric_limits::max()) { + return Error::OUT_OF_RANGE; + } + CHECK(readAndAppendToString(stream, head.arg, s)); + } + str = std::move(s); + return 0; +} + +int writeCborString(EncodingStream& stream, const String& str) { + CHECK(writeCborHeadWithArgument(stream, 3 /* Text string */, str.length())); + CHECK(stream.write(str.c_str(), str.length())); + return 0; +} + +int encodeToCbor(EncodingStream& stream, const Variant& var) { + switch (var.type()) { + case Variant::NULL_: { + CHECK(stream.writeUint8(0xf6 /* null */)); // See RFC 8949, Appendix B + break; + } + case Variant::BOOL: { + auto v = var.value(); + CHECK(stream.writeUint8(v ? 0xf5 /* true */ : 0xf4 /* false */)); + break; + } + case Variant::INT: { + CHECK(writeCborSignedInteger(stream, var.value())); + break; + } + case Variant::UINT: { + CHECK(writeCborUnsignedInteger(stream, var.value())); + break; + } + case Variant::INT64: { + CHECK(writeCborSignedInteger(stream, var.value())); + break; + } + case Variant::UINT64: { + CHECK(writeCborUnsignedInteger(stream, var.value())); + break; + } + case Variant::DOUBLE: { + double d = var.value(); + float f = d; + if (f == d) { + // Encoding with a smaller precision than that of float is not supported + CHECK(stream.writeUint8(0xfa /* Single-precision */)); + CHECK(stream.writeFloatBe(f)); + } else { + CHECK(stream.writeUint8(0xfb /* Double-precision */)); + CHECK(stream.writeDoubleBe(d)); + } + break; + } + case Variant::STRING: { + CHECK(writeCborString(stream, var.value())); + break; + } + case Variant::ARRAY: { + auto& arr = var.value(); + CHECK(writeCborHeadWithArgument(stream, 4 /* Array */, arr.size())); + for (auto& v: arr) { + CHECK(encodeToCbor(stream, v)); + } + break; + } + case Variant::MAP: { + auto& entries = var.value().entries(); + CHECK(writeCborHeadWithArgument(stream, 5 /* Map */, entries.size())); + for (auto& e: entries) { + CHECK(writeCborString(stream, e.first)); + CHECK(encodeToCbor(stream, e.second)); + } + break; + } + default: // Unreachable + return Error::INTERNAL; + } + return 0; +} + +int decodeFromCbor(DecodingStream& stream, const CborHead& head, Variant& var) { + switch (head.type) { + case 0: { // Unsigned integer + if (head.arg <= std::numeric_limits::max()) { + var = (unsigned)head.arg; // 32-bit + } else { + var = head.arg; // 64-bit + } + break; + } + case 1: { // Negative integer + if (head.arg > (uint64_t)std::numeric_limits::max()) { + return Error::OUT_OF_RANGE; + } + int64_t v = -(int64_t)head.arg - 1; + if (v >= std::numeric_limits::min()) { + var = (int)v; // 32-bit + } else { + var = v; // 64-bit + } + break; + } + case 2: { // Byte string + return Error::NOT_SUPPORTED; // Not supported + } + case 3: { // Text string + String s; + CHECK(readCborString(stream, head, s)); + var = std::move(s); + break; + } + case 4: { // Array + VariantArray arr; + int len = -1; + if (head.detail != 31 /* Indefinite length */) { + if (head.arg > (uint64_t)std::numeric_limits::max()) { + return Error::OUT_OF_RANGE; + } + len = head.arg; + if (!arr.reserve(len)) { + return Error::NO_MEMORY; + } + } + for (;;) { + if (len >= 0 && arr.size() == len) { + break; + } + CborHead h; + CHECK(readCborHead(stream, h)); + if (h.type == 7 /* Misc. items */ && h.detail == 31 /* Stop code */) { + if (len >= 0) { + return Error::BAD_DATA; // Unexpected stop code + } + break; + } + Variant v; + CHECK(decodeFromCbor(stream, h, v)); + if (!arr.append(std::move(v))) { + return Error::NO_MEMORY; + } + } + var = std::move(arr); + break; + } + case 5: { // Map + VariantMap map; + int len = -1; + if (head.detail != 31 /* Indefinite length */) { + if (head.arg > (uint64_t)std::numeric_limits::max()) { + return Error::OUT_OF_RANGE; + } + len = head.arg; + if (!map.reserve(len)) { + return Error::NO_MEMORY; + } + } + for (;;) { + if (len >= 0 && map.size() == len) { + break; + } + CborHead h; + CHECK(readCborHead(stream, h)); + if (h.type == 7 /* Misc. items */ && h.detail == 31 /* Stop code */) { + if (len >= 0) { + return Error::BAD_DATA; // Unexpected stop code + } + break; + } + if (h.type != 3 /* Text string */) { + return Error::NOT_SUPPORTED; // Non-string keys are not supported + } + String k; + CHECK(readCborString(stream, h, k)); + Variant v; + CHECK(readCborHead(stream, h)); + CHECK(decodeFromCbor(stream, h, v)); + if (!map.set(std::move(k), std::move(v))) { + return Error::NO_MEMORY; + } + } + var = std::move(map); + break; + } + case 6: { // Tagged item + // Skip all tags + CborHead h; + do { + CHECK(readCborHead(stream, h)); + } while (h.type == 6 /* Tagged item */); + CHECK(decodeFromCbor(stream, h, var)); + break; + } + case 7: { // Misc. items + switch (head.detail) { + case 20: { // false + var = false; + break; + } + case 21: { // true + var = true; + break; + } + case 22: { // null + var = Variant(); + break; + } + case 25: { // Half-precision + // This code was taken from RFC 8949, Appendix D + uint16_t half = head.arg; + unsigned exp = (half >> 10) & 0x1f; + unsigned mant = half & 0x03ff; + double val = 0; + if (exp == 0) { + val = std::ldexp(mant, -24); + } else if (exp != 31) { + val = std::ldexp(mant + 1024, exp - 25); + } else { + val = (mant == 0) ? INFINITY : NAN; + } + if (half & 0x8000) { + val = -val; + } + var = val; + break; + } + case 26: { // Single-precision + uint32_t v = head.arg; + float val; + static_assert(sizeof(val) == sizeof(v)); + std::memcpy(&val, &v, sizeof(v)); + var = val; + break; + } + case 27: { // Double-precision + double val; + static_assert(sizeof(val) == sizeof(head.arg)); + std::memcpy(&val, &head.arg, sizeof(head.arg)); + var = val; + break; + } + default: + if ((head.detail >= 28 && head.detail <= 31) || // Reserved (28-30) or unexpected stop code (31) + (head.detail == 24 && head.arg < 32)) { // Invalid simple value + return Error::BAD_DATA; + } + return Error::NOT_SUPPORTED; // Unassigned simple value (0-19, 32-255) or undefined (23) + } + break; + } + default: // Unreachable + return Error::INTERNAL; + } + return 0; +} + +int decodeFromJson(const JSONValue& val, Variant& var) { switch (val.type()) { case JSONType::JSON_TYPE_INVALID: { - return false; + return Error::INVALID_ARGUMENT; } case JSONType::JSON_TYPE_NULL: { var = Variant(); @@ -66,7 +549,7 @@ bool variantFromJSON(const JSONValue& val, Variant& var) { JSONString jsonStr = val.toString(); String s(jsonStr); if (s.length() != jsonStr.size()) { - return false; + return Error::NO_MEMORY; } var = std::move(s); break; @@ -75,13 +558,11 @@ bool variantFromJSON(const JSONValue& val, Variant& var) { JSONArrayIterator it(val); auto& arr = var.asArray(); if (!arr.reserve(it.count())) { - return false; + return Error::NO_MEMORY; } while (it.next()) { Variant v; - if (!variantFromJSON(it.value(), v)) { - return false; - } + CHECK(decodeFromJson(it.value(), v)); arr.append(std::move(v)); } break; @@ -90,26 +571,24 @@ bool variantFromJSON(const JSONValue& val, Variant& var) { JSONObjectIterator it(val); auto& map = var.asMap(); if (!map.reserve(it.count())) { - return false; + return Error::NO_MEMORY; } while (it.next()) { JSONString jsonKey = it.name(); String k(jsonKey); if (k.length() != jsonKey.size()) { - return false; + return Error::NO_MEMORY; } Variant v; - if (!variantFromJSON(it.value(), v)) { - return false; - } + CHECK(decodeFromJson(it.value(), v)); map.set(std::move(k), std::move(v)); } break; } - default: - return false; + default: // Unreachable + return Error::INTERNAL; } - return true; + return 0; } } // namespace @@ -320,10 +799,25 @@ Variant Variant::fromJSON(const char* json) { Variant Variant::fromJSON(const JSONValue& val) { Variant v; - if (!variantFromJSON(val, v)) { + int r = decodeFromJson(val, v); + if (r < 0) { return Variant(); } return v; } +int encodeToCBOR(const Variant& var, Print& stream) { + EncodingStream s(stream); + CHECK(encodeToCbor(s, var)); + return 0; +} + +int decodeFromCBOR(Variant& var, Stream& stream) { + DecodingStream s(stream); + CborHead h; + CHECK(readCborHead(s, h)); + CHECK(decodeFromCbor(s, h, var)); + return 0; +} + } // namespace particle