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/coap_util.h b/communication/inc/coap_util.h
new file mode 100644
index 0000000000..f3824402fc
--- /dev/null
+++ b/communication/inc/coap_util.h
@@ -0,0 +1,102 @@
+/*
+ * Copyright (c) 2022 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 "coap_api.h"
+#include "logging.h"
+
+namespace particle {
+
+/**
+ * Smart pointer for CoAP message instances.
+ */
+class CoapMessagePtr {
+public:
+ CoapMessagePtr() :
+ msg_(nullptr) {
+ }
+
+ explicit CoapMessagePtr(coap_message* msg) :
+ msg_(msg) {
+ }
+
+ CoapMessagePtr(const CoapMessagePtr&) = delete;
+
+ CoapMessagePtr(CoapMessagePtr&& ptr) :
+ msg_(ptr.msg_) {
+ ptr.msg_ = nullptr;
+ }
+
+ ~CoapMessagePtr() {
+ coap_destroy_message(msg_, nullptr);
+ }
+
+ coap_message* get() const {
+ return msg_;
+ }
+
+ coap_message* release() {
+ auto msg = msg_;
+ msg_ = nullptr;
+ return msg;
+ }
+
+ void reset(coap_message* msg = nullptr) {
+ coap_destroy_message(msg_, nullptr);
+ msg_ = msg;
+ }
+
+ CoapMessagePtr& operator=(const CoapMessagePtr&) = delete;
+
+ CoapMessagePtr& operator=(CoapMessagePtr&& ptr) {
+ coap_destroy_message(msg_, nullptr);
+ msg_ = ptr.msg_;
+ ptr.msg_ = nullptr;
+ return *this;
+ }
+
+ explicit operator bool() const {
+ return msg_;
+ }
+
+ friend void swap(CoapMessagePtr& ptr1, CoapMessagePtr& ptr2) {
+ auto msg = ptr1.msg_;
+ ptr1.msg_ = ptr2.msg_;
+ ptr2.msg_ = msg;
+ }
+
+private:
+ coap_message* msg_;
+};
+
+namespace protocol {
+
+/**
+ * Log the contents of a CoAP message.
+ *
+ * @param level Logging level.
+ * @param category Logging category.
+ * @param data Message data.
+ * @param size Message size.
+ * @param logPayload Whether to log the payload data of the message.
+ */
+void logCoapMessage(LogLevel level, const char* category, const char* data, size_t size, bool logPayload = false);
+
+} // namespace protocol
+
+} // namespace particle
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..3cdce9f6ac
--- /dev/null
+++ b/communication/src/coap_channel_new.cpp
@@ -0,0 +1,1474 @@
+/*
+ * 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;
+}
+
+// Invokes a function object for each element in the list. It's not safe to use this function if the
+// function object may delete multiple elements of the list and not just the element for which it
+// was called
+template
+inline void forEachInList(T* head, const F& fn) {
+ while (head) {
+ // Store the pointer to the next element in case the callback deletes the current element
+ auto next = head->next;
+ fn(head);
+ head = static_cast(next);
+ }
+}
+
+// Adds a reference-counted object to a list and increments the reference counter
+template
+inline void addRefToList(T*& head, RefCountPtr elem) {
+ addToList(head, elem.unwrap());
+}
+
+// Removes a reference counted object from the list and decrements the reference counter
+template
+inline void removeRefFromList(T*& head, const RefCountPtr& elem) {
+ removeFromList(head, elem.get());
+ // Release the unmanaged reference to the object
+ 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) {
+ // Prevent the object from being deleted by the callback
+ 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;
+ // Transfer ownership over the message to the calling code
+ *msg = reinterpret_cast(req.unwrap());
+ 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;
+ // Take ownership over the message by releasing the reference to the message that was previously
+ // managed by the calling code
+ req->release();
+ 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;
+ // Transfer ownership over the message to the calling code
+ *msg = reinterpret_cast(resp.unwrap());
+ 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;
+ // Take ownership over the message by releasing the reference to the message that was previously
+ // managed by the calling code
+ resp->release();
+ 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;
+ // Release the unmanaged reference to the message
+ 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;
+ }
+ // Transfer ownership over the message to called code
+ req.unwrap();
+ 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;
+ }
+ // Transfer ownership over the message to called code
+ resp.unwrap();
+ 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 7fbe7c3413..f3a35005e0 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/crypto/inc/mbedtls_config_default.h b/crypto/inc/mbedtls_config_default.h
index e88946153f..79c28216d5 100644
--- a/crypto/inc/mbedtls_config_default.h
+++ b/crypto/inc/mbedtls_config_default.h
@@ -32,6 +32,9 @@
#include "platforms.h"
+// For size_t
+#include
+
#if defined(_MSC_VER) && !defined(_CRT_SECURE_NO_DEPRECATE)
#define _CRT_SECURE_NO_DEPRECATE 1
#endif
diff --git a/hal/inc/hal_platform.h b/hal/inc/hal_platform.h
index 362fa736b5..d5d94fd8a0 100644
--- a/hal/inc/hal_platform.h
+++ b/hal/inc/hal_platform.h
@@ -622,4 +622,8 @@
#define HAL_PLATFORM_AUTOMATIC_CONNECTION_MANAGEMENT (0)
#endif // HAL_PLATFORM_AUTOMATIC_CONNECTION_MANAGEMENT
+#ifndef HAL_PLATFORM_LEDGER
+#define HAL_PLATFORM_LEDGER (1)
+#endif // HAL_PLATFORM_LEDGER
+
#endif /* HAL_PLATFORM_H */
diff --git a/hal/network/ncp/cellular/cellular_network_manager.cpp b/hal/network/ncp/cellular/cellular_network_manager.cpp
index 60756d09a0..62a14b7c0e 100644
--- a/hal/network/ncp/cellular/cellular_network_manager.cpp
+++ b/hal/network/ncp/cellular/cellular_network_manager.cpp
@@ -76,7 +76,7 @@ int loadConfig(CellularConfig* conf) {
DecodedCString dExtApn(&pbConf.external_sim.apn);
DecodedCString dExtUser(&pbConf.external_sim.user);
DecodedCString dExtPwd(&pbConf.external_sim.password);
- const int r = decodeMessageFromFile(&file, PB(CellularConfig_fields), &pbConf);
+ const int r = decodeProtobufFromFile(&file, PB(CellularConfig_fields), &pbConf);
if (r < 0) {
LOG(ERROR, "Unable to parse network settings");
*conf = CellularConfig(); // Using default settings
@@ -168,7 +168,7 @@ int saveConfig(const CellularConfig& conf) {
}
}
pbConf.active_sim = (PB_CELLULAR(SimType))conf.activeSim;
- CHECK(encodeMessageToFile(&file, PB(CellularConfig_fields), &pbConf));
+ CHECK(encodeProtobufToFile(&file, PB(CellularConfig_fields), &pbConf));
LOG(TRACE, "Updated file: %s", CONFIG_FILE);
ok = true;
return 0;
diff --git a/hal/network/ncp/wifi/wifi_network_manager.cpp b/hal/network/ncp/wifi/wifi_network_manager.cpp
index c54c60bf5e..857d1333fd 100644
--- a/hal/network/ncp/wifi/wifi_network_manager.cpp
+++ b/hal/network/ncp/wifi/wifi_network_manager.cpp
@@ -105,7 +105,7 @@ int loadConfig(Vector* networks) {
}
return true;
};
- const int r = decodeMessageFromFile(&file, PB(WifiConfig_fields), &pbConf);
+ const int r = decodeProtobufFromFile(&file, PB(WifiConfig_fields), &pbConf);
if (r < 0) {
LOG(ERROR, "Unable to parse network settings");
networks->clear();
@@ -164,7 +164,7 @@ int saveConfig(const Vector& networks) {
}
return true;
};
- CHECK(encodeMessageToFile(&file, PB(WifiConfig_fields), &pbConf));
+ CHECK(encodeProtobufToFile(&file, PB(WifiConfig_fields), &pbConf));
LOG(TRACE, "Updated file: %s", CONFIG_FILE);
ok = true;
return 0;
diff --git a/hal/shared/filesystem.h b/hal/shared/filesystem.h
index 9b0970d69f..0cf0d008bd 100644
--- a/hal/shared/filesystem.h
+++ b/hal/shared/filesystem.h
@@ -81,7 +81,7 @@ int filesystem_to_system_error(int error);
namespace particle { namespace fs {
struct FsLock {
- FsLock(filesystem_t* fs = filesystem_get_instance(FILESYSTEM_INSTANCE_DEFAULT, nullptr))
+ explicit FsLock(filesystem_t* fs = filesystem_get_instance(FILESYSTEM_INSTANCE_DEFAULT, nullptr))
: fs_(fs) {
lock();
}
diff --git a/hal/src/gcc/hal_platform_config.h b/hal/src/gcc/hal_platform_config.h
index 55cd21acf2..509a9939f1 100644
--- a/hal/src/gcc/hal_platform_config.h
+++ b/hal/src/gcc/hal_platform_config.h
@@ -65,3 +65,5 @@
#endif
#define HAL_PLATFORM_FREERTOS (0)
+
+#define HAL_PLATFORM_LEDGER (0)
diff --git a/hal/src/nRF52840/mbedtls/mbedtls_config_platform.h b/hal/src/nRF52840/mbedtls/mbedtls_config_platform.h
index 6e1d9dee1c..9878bb2dd0 100644
--- a/hal/src/nRF52840/mbedtls/mbedtls_config_platform.h
+++ b/hal/src/nRF52840/mbedtls/mbedtls_config_platform.h
@@ -1415,6 +1415,16 @@
*/
#define MBEDTLS_SSL_RAW_PUBLIC_KEY_SUPPORT
+/**
+ * \def MBEDTLS_SSL_DISABLE_PARSE_CERTIFICATE
+ *
+ * When using MBEDTLS_SSL_RAW_PUBLIC_KEY_SUPPORT, disable
+ * support for parsing X509 certificates entirely.
+ */
+#if PLATFORM_ID == PLATFORM_TRACKER
+#define MBEDTLS_SSL_DISABLE_PARSE_CERTIFICATE
+#endif // PLATFORM_ID == PLATFORM_TRACKER
+
/**
* \def MBEDTLS_ZLIB_SUPPORT
*
diff --git a/hal/src/newhal/hal_platform_config.h b/hal/src/newhal/hal_platform_config.h
index 193722885d..631230076c 100644
--- a/hal/src/newhal/hal_platform_config.h
+++ b/hal/src/newhal/hal_platform_config.h
@@ -66,3 +66,5 @@
#endif
#define HAL_PLATFORM_FREERTOS (0)
+
+#define HAL_PLATFORM_LEDGER (0)
diff --git a/hal/src/rtl872x/concurrent_hal.cpp b/hal/src/rtl872x/concurrent_hal.cpp
index 8ef804e57f..08425167da 100644
--- a/hal/src/rtl872x/concurrent_hal.cpp
+++ b/hal/src/rtl872x/concurrent_hal.cpp
@@ -434,6 +434,7 @@ int os_semaphore_give(os_semaphore_t semaphore, bool reserved)
*/
int os_timer_create(os_timer_t* timer, unsigned period, void (*callback)(os_timer_t timer), void* const timer_id, bool one_shot, void* reserved)
{
+ // TODO: Return an error if the period is 0 (see https://www.freertos.org/FreeRTOS-timers-xTimerCreate.html)
*timer = xTimerCreate((_CREATE_NAME_TYPE*)"", period, !one_shot, timer_id, reinterpret_cast(callback));
return *timer==NULL;
}
diff --git a/modules/argon/system-part1/module_system_part1_export.ld b/modules/argon/system-part1/module_system_part1_export.ld
index 736aadb455..ca90d18c97 100644
--- a/modules/argon/system-part1/module_system_part1_export.ld
+++ b/modules/argon/system-part1/module_system_part1_export.ld
@@ -30,3 +30,4 @@ PROVIDE (link_dynalib_location_hal_posix_syscall = platform_system_part1_dyna
PROVIDE (link_dynalib_location_hal_storage = platform_system_part1_dynalib_table_flash_start + 112);
PROVIDE (link_dynalib_location_hal_watchdog = platform_system_part1_dynalib_table_flash_start + 116);
PROVIDE (link_dynalib_location_system_asset_manager = platform_system_part1_dynalib_table_flash_start + 120);
+PROVIDE (link_dynalib_location_system_ledger = platform_system_part1_dynalib_table_flash_start + 124);
diff --git a/modules/b5som/system-part1/module_system_part1_export.ld b/modules/b5som/system-part1/module_system_part1_export.ld
index d74ad590dd..94cd2f1df0 100644
--- a/modules/b5som/system-part1/module_system_part1_export.ld
+++ b/modules/b5som/system-part1/module_system_part1_export.ld
@@ -30,3 +30,4 @@ PROVIDE (link_dynalib_location_hal_posix_syscall = platform_system_part1_dyna
PROVIDE (link_dynalib_location_hal_storage = platform_system_part1_dynalib_table_flash_start + 112);
PROVIDE (link_dynalib_location_hal_watchdog = platform_system_part1_dynalib_table_flash_start + 116);
PROVIDE (link_dynalib_location_system_asset_manager = platform_system_part1_dynalib_table_flash_start + 120);
+PROVIDE (link_dynalib_location_system_ledger = platform_system_part1_dynalib_table_flash_start + 124);
diff --git a/modules/boron/system-part1/module_system_part1_export.ld b/modules/boron/system-part1/module_system_part1_export.ld
index d74ad590dd..94cd2f1df0 100644
--- a/modules/boron/system-part1/module_system_part1_export.ld
+++ b/modules/boron/system-part1/module_system_part1_export.ld
@@ -30,3 +30,4 @@ PROVIDE (link_dynalib_location_hal_posix_syscall = platform_system_part1_dyna
PROVIDE (link_dynalib_location_hal_storage = platform_system_part1_dynalib_table_flash_start + 112);
PROVIDE (link_dynalib_location_hal_watchdog = platform_system_part1_dynalib_table_flash_start + 116);
PROVIDE (link_dynalib_location_system_asset_manager = platform_system_part1_dynalib_table_flash_start + 120);
+PROVIDE (link_dynalib_location_system_ledger = platform_system_part1_dynalib_table_flash_start + 124);
diff --git a/modules/msom/system-part1/module_system_part1_export.ld b/modules/msom/system-part1/module_system_part1_export.ld
index a4bdb08ba0..276c81bf66 100644
--- a/modules/msom/system-part1/module_system_part1_export.ld
+++ b/modules/msom/system-part1/module_system_part1_export.ld
@@ -29,4 +29,5 @@ PROVIDE (link_dynalib_location_offset_hal_storage = 104);
PROVIDE (link_dynalib_location_offset_hal_cellular = 108);
PROVIDE (link_dynalib_location_offset_hal_watchdog = 112);
PROVIDE (link_dynalib_location_offset_hal_backup_ram = 116);
-PROVIDE (link_dynalib_location_offset_system_asset_manager = 120);
\ No newline at end of file
+PROVIDE (link_dynalib_location_offset_system_asset_manager = 120);
+PROVIDE (link_dynalib_location_offset_system_ledger = 124);
diff --git a/modules/shared/nRF52840/inc/system-part1/export_system.inc b/modules/shared/nRF52840/inc/system-part1/export_system.inc
index 657c1392ee..45e0b117da 100644
--- a/modules/shared/nRF52840/inc/system-part1/export_system.inc
+++ b/modules/shared/nRF52840/inc/system-part1/export_system.inc
@@ -27,3 +27,4 @@
#include "system_dynalib_net.h"
#include "system_dynalib_cloud.h"
#include "system_dynalib_asset_manager.h"
+#include "system_dynalib_ledger.h"
diff --git a/modules/shared/nRF52840/inc/system-part1/module_system_part1.inc b/modules/shared/nRF52840/inc/system-part1/module_system_part1.inc
index 28b2193781..4904d22bc5 100644
--- a/modules/shared/nRF52840/inc/system-part1/module_system_part1.inc
+++ b/modules/shared/nRF52840/inc/system-part1/module_system_part1.inc
@@ -59,6 +59,10 @@ DYNALIB_TABLE_EXTERN(hal_watchdog);
DYNALIB_TABLE_EXTERN(system_asset_manager);
+#if HAL_PLATFORM_LEDGER
+DYNALIB_TABLE_EXTERN(system_ledger);
+#endif
+
// strange that this is needed given that the entire block is scoped extern "C"
// without it, the section name doesn't match *.system_part2_module as expected in the linker script
extern "C" __attribute__((externally_visible)) const void* const system_part1_module[];
@@ -118,6 +122,9 @@ extern "C" __attribute__((externally_visible)) const void* const system_part1_mo
, DYNALIB_TABLE_NAME(hal_watchdog)
#endif // HAL_PLATFORM_HW_WATCHDOG
, DYNALIB_TABLE_NAME(system_asset_manager)
+#if HAL_PLATFORM_LEDGER
+ , DYNALIB_TABLE_NAME(system_ledger)
+#endif
};
#include "system_part1_loader.c"
diff --git a/modules/shared/rtl872x/inc/system-part1/export_system.inc b/modules/shared/rtl872x/inc/system-part1/export_system.inc
index 657c1392ee..45e0b117da 100644
--- a/modules/shared/rtl872x/inc/system-part1/export_system.inc
+++ b/modules/shared/rtl872x/inc/system-part1/export_system.inc
@@ -27,3 +27,4 @@
#include "system_dynalib_net.h"
#include "system_dynalib_cloud.h"
#include "system_dynalib_asset_manager.h"
+#include "system_dynalib_ledger.h"
diff --git a/modules/shared/rtl872x/inc/system-part1/module_system_part1.inc b/modules/shared/rtl872x/inc/system-part1/module_system_part1.inc
index 659a78034a..0f7450f774 100644
--- a/modules/shared/rtl872x/inc/system-part1/module_system_part1.inc
+++ b/modules/shared/rtl872x/inc/system-part1/module_system_part1.inc
@@ -50,6 +50,9 @@ DYNALIB_TABLE_EXTERN(hal_watchdog);
#endif // HAL_PLATFORM_HW_WATCHDOG
DYNALIB_TABLE_EXTERN(hal_backup_ram);
DYNALIB_TABLE_EXTERN(system_asset_manager);
+#if HAL_PLATFORM_LEDGER
+DYNALIB_TABLE_EXTERN(system_ledger);
+#endif
// strange that this is needed given that the entire block is scoped extern "C"
// without it, the section name doesn't match *.system_part2_module as expected in the linker script
@@ -97,8 +100,11 @@ extern "C" __attribute__((externally_visible)) const void* const system_part1_mo
#if HAL_PLATFORM_HW_WATCHDOG
, DYNALIB_TABLE_NAME(hal_watchdog)
#endif // HAL_PLATFORM_HW_WATCHDOG
- , DYNALIB_TABLE_NAME(hal_backup_ram),
- DYNALIB_TABLE_NAME(system_asset_manager)
+ , DYNALIB_TABLE_NAME(hal_backup_ram)
+ , DYNALIB_TABLE_NAME(system_asset_manager)
+#if HAL_PLATFORM_LEDGER
+ , DYNALIB_TABLE_NAME(system_ledger)
+#endif
};
#include "system_part1_loader.c"
diff --git a/modules/tracker/system-part1/module_system_part1_export.ld b/modules/tracker/system-part1/module_system_part1_export.ld
index b073c60180..e3165fddfb 100644
--- a/modules/tracker/system-part1/module_system_part1_export.ld
+++ b/modules/tracker/system-part1/module_system_part1_export.ld
@@ -31,3 +31,4 @@ PROVIDE (link_dynalib_location_hal_wlan = platform_system_part1_dyna
PROVIDE (link_dynalib_location_hal_storage = platform_system_part1_dynalib_table_flash_start + 116);
PROVIDE (link_dynalib_location_hal_watchdog = platform_system_part1_dynalib_table_flash_start + 120);
PROVIDE (link_dynalib_location_system_asset_manager = platform_system_part1_dynalib_table_flash_start + 124);
+PROVIDE (link_dynalib_location_system_ledger = platform_system_part1_dynalib_table_flash_start + 128);
diff --git a/modules/trackerm/system-part1/module_system_part1_export.ld b/modules/trackerm/system-part1/module_system_part1_export.ld
index f1d18afa4b..276c81bf66 100644
--- a/modules/trackerm/system-part1/module_system_part1_export.ld
+++ b/modules/trackerm/system-part1/module_system_part1_export.ld
@@ -30,3 +30,4 @@ PROVIDE (link_dynalib_location_offset_hal_cellular = 108);
PROVIDE (link_dynalib_location_offset_hal_watchdog = 112);
PROVIDE (link_dynalib_location_offset_hal_backup_ram = 116);
PROVIDE (link_dynalib_location_offset_system_asset_manager = 120);
+PROVIDE (link_dynalib_location_offset_system_ledger = 124);
diff --git a/modules/tron/system-part1/module_system_part1_export.ld b/modules/tron/system-part1/module_system_part1_export.ld
index b5cf86753d..1634b555fc 100644
--- a/modules/tron/system-part1/module_system_part1_export.ld
+++ b/modules/tron/system-part1/module_system_part1_export.ld
@@ -28,4 +28,5 @@ PROVIDE (link_dynalib_location_offset_hal_posix_syscall = 100);
PROVIDE (link_dynalib_location_offset_hal_storage = 104);
PROVIDE (link_dynalib_location_offset_hal_watchdog = 108);
PROVIDE (link_dynalib_location_offset_hal_backup_ram = 112);
-PROVIDE (link_dynalib_location_offset_system_asset_manager = 116);
\ No newline at end of file
+PROVIDE (link_dynalib_location_offset_system_asset_manager = 116);
+PROVIDE (link_dynalib_location_offset_system_ledger = 120);
diff --git a/proto_defs/.gitignore b/proto_defs/.gitignore
new file mode 100644
index 0000000000..ef81b1e243
--- /dev/null
+++ b/proto_defs/.gitignore
@@ -0,0 +1 @@
+/.venv/
diff --git a/proto_defs/gen_proto.sh b/proto_defs/gen_proto.sh
index e95b747b7c..37a7cba0e5 100755
--- a/proto_defs/gen_proto.sh
+++ b/proto_defs/gen_proto.sh
@@ -1,17 +1,5 @@
#!/bin/bash
-# Note for macOS users:
-#
-# 1. The following dependencies are required to build the protocol files using the nanopb plugin:
-#
-# brew install protobuf python
-# pip2 install protobuf
-#
-# 2. Make sure your system Python can find Python modules installed via Homebrew:
-#
-# mkdir -p ~/Library/Python/2.7/lib/python/site-packages
-# echo 'import site; site.addsitedir("/usr/local/lib/python2.7/site-packages")' >> ~/Library/Python/2.7/lib/python/site-packages/homebrew.pth
-
set -e
DEVICE_OS_DIR="$(cd "$(dirname "$0")/.." && pwd)"
@@ -32,7 +20,14 @@ gen_proto() {
--nanopb_out="${DEST_DIR}" "$1"
}
-# Control requests
+# Create a virtual environment
+python3 -m venv "$PROTO_DEFS_DIR/.venv"
+source "$PROTO_DEFS_DIR/.venv/bin/activate"
+
+# Install dependencies
+pip3 install protobuf
+
+# Compile control request definitions
gen_proto "${SHARED_DIR}/control/extensions.proto"
gen_proto "${SHARED_DIR}/control/common.proto"
gen_proto "${SHARED_DIR}/control/config.proto"
@@ -44,10 +39,11 @@ gen_proto "${SHARED_DIR}/control/network_old.proto"
gen_proto "${SHARED_DIR}/control/storage.proto"
gen_proto "${SHARED_DIR}/control/cloud.proto"
-# Cloud protocol
+# Compile cloud protocol definitions
gen_proto "${SHARED_DIR}/cloud/cloud.proto"
gen_proto "${SHARED_DIR}/cloud/describe.proto"
gen_proto "${SHARED_DIR}/cloud/ledger.proto"
-# Internal definitions
+# Compile internal definitions
gen_proto "${INTERNAL_DIR}/network_config.proto"
+gen_proto "${INTERNAL_DIR}/ledger.proto"
diff --git a/proto_defs/internal/ledger.proto b/proto_defs/internal/ledger.proto
new file mode 100644
index 0000000000..847f7c581a
--- /dev/null
+++ b/proto_defs/internal/ledger.proto
@@ -0,0 +1,30 @@
+syntax = "proto3";
+
+package particle.firmware;
+
+import "cloud/ledger.proto";
+import "nanopb.proto";
+
+/**
+ * Ledger info.
+ */
+message LedgerInfo {
+ string name = 1 [(nanopb).max_length = 32]; ///< Ledger name.
+ 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 not set, the time is unknown.
+ */
+ optional fixed64 last_updated = 5;
+ /**
+ * Time the ledger was last synchronized with the Cloud, in milliseconds since the Unix epoch.
+ *
+ * If not set, the ledger has never been 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/src/ledger.pb.c b/proto_defs/src/ledger.pb.c
new file mode 100644
index 0000000000..b1369806cb
--- /dev/null
+++ b/proto_defs/src/ledger.pb.c
@@ -0,0 +1,12 @@
+/* Automatically generated nanopb constant definitions */
+/* Generated by nanopb-0.4.5 */
+
+#include "ledger.pb.h"
+#if PB_PROTO_HEADER_VERSION != 40
+#error Regenerate this file with the current version of nanopb generator.
+#endif
+
+PB_BIND(particle_firmware_LedgerInfo, particle_firmware_LedgerInfo, AUTO)
+
+
+
diff --git a/proto_defs/src/ledger.pb.h b/proto_defs/src/ledger.pb.h
new file mode 100644
index 0000000000..5082a55f8b
--- /dev/null
+++ b/proto_defs/src/ledger.pb.h
@@ -0,0 +1,82 @@
+/* Automatically generated nanopb header */
+/* Generated by nanopb-0.4.5 */
+
+#ifndef PB_PARTICLE_FIRMWARE_LEDGER_PB_H_INCLUDED
+#define PB_PARTICLE_FIRMWARE_LEDGER_PB_H_INCLUDED
+#include
+#include "cloud/ledger.pb.h"
+
+#if PB_PROTO_HEADER_VERSION != 40
+#error Regenerate this file with the current version of nanopb generator.
+#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_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 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 not set, the ledger has never been synchronized. */
+ bool has_last_synced;
+ uint64_t last_synced;
+ 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;
+
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+/* Initializer values for message structs */
+#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_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, 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
+
+extern const pb_msgdesc_t particle_firmware_LedgerInfo_msg;
+
+/* Defines for backwards compatibility with code written before nanopb-0.4.0 */
+#define particle_firmware_LedgerInfo_fields &particle_firmware_LedgerInfo_msg
+
+/* Maximum encoded size of messages (where known) */
+#define particle_firmware_LedgerInfo_size 98
+
+#ifdef __cplusplus
+} /* extern "C" */
+#endif
+
+#endif
diff --git a/services/inc/debug_util.h b/services/inc/debug_util.h
new file mode 100644
index 0000000000..c0731a7adc
--- /dev/null
+++ b/services/inc/debug_util.h
@@ -0,0 +1,65 @@
+/*
+ * 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
+
+#include "hal_platform.h"
+
+/**
+ * Watchpoint type.
+ */
+typedef enum watchpoint_type {
+ WATCHPOINT_TYPE_WRITE = 0, ///< Write access.
+ WATCHPOINT_TYPE_READ = 1, ///< Read access.
+ WATCHPOINT_TYPE_READ_WRITE = 2 ///< Read/write access.
+} watchpoint_type;
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+#if HAL_PLATFORM_NRF52840
+
+/**
+ * Set a watchpoint.
+ *
+ * @param addr Region address. Must be aligned by `size`.
+ * @param size Region size. Must be a power of 2.
+ * @param type Watchpoint type as defined by the `watchpoint_type` enum.
+ * @returns Index of the newly set watchpoint or an error code defined by the `system_error_t` enum.
+ */
+int set_watchpoint(const void* addr, size_t size, int type); // TODO: Inline or export via a dynalib
+
+/**
+ * Clear a watchpoint.
+ *
+ * @param idx Watchpoint index.
+ */
+void clear_watchpoint(int idx);
+
+/**
+ * Generate a breakpoint exception.
+ */
+void breakpoint();
+
+#endif // HAL_PLATFORM_NRF52840
+
+#ifdef __cplusplus
+} // extern "C"
+#endif
diff --git a/services/inc/file_util.h b/services/inc/file_util.h
index fc5995192f..c2bcd8f42d 100644
--- a/services/inc/file_util.h
+++ b/services/inc/file_util.h
@@ -26,8 +26,8 @@ namespace particle {
int openFile(lfs_file_t* file, const char* path, unsigned flags = LFS_O_RDWR);
int dumpFile(const char* path);
-int decodeMessageFromFile(lfs_file_t* file, const pb_msgdesc_t* desc, void* msg);
-int encodeMessageToFile(lfs_file_t* file, const pb_msgdesc_t* desc, const void* msg);
+int decodeProtobufFromFile(lfs_file_t* file, const pb_msgdesc_t* desc, void* msg, int size = -1);
+int encodeProtobufToFile(lfs_file_t* file, const pb_msgdesc_t* desc, const void* msg);
int rmrf(const char* path);
int mkdirp(const char* path);
diff --git a/services/inc/nanopb_misc.h b/services/inc/nanopb_misc.h
index 9be397a7f7..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);
@@ -39,10 +40,14 @@ bool pb_ostream_from_buffer_ex(pb_ostream_t* stream, pb_byte_t *buf, size_t bufs
bool pb_istream_from_buffer_ex(pb_istream_t* stream, const pb_byte_t *buf, size_t bufsize, void* reserved);
#if HAL_PLATFORM_FILESYSTEM
-bool pb_ostream_from_file(pb_ostream_t* stream, lfs_file_t* file, void* reserved);
-bool pb_istream_from_file(pb_istream_t* stream, lfs_file_t* file, void* reserved);
+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
new file mode 100644
index 0000000000..7e2b158273
--- /dev/null
+++ b/services/inc/ref_count.h
@@ -0,0 +1,148 @@
+/*
+ * 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
+#include
+#include
+
+static_assert(std::atomic_int::is_always_lock_free, "std::atomic is not always lock-free");
+
+namespace particle {
+
+/**
+ * Base class for reference counted objects.
+ */
+class RefCount {
+public:
+ RefCount() :
+ count_(1) {
+ }
+
+ RefCount(const RefCount&) = delete;
+
+ virtual ~RefCount() = default;
+
+ void addRef() const {
+ count_.fetch_add(1, std::memory_order_relaxed);
+ }
+
+ void release() const {
+ if (count_.fetch_sub(1, std::memory_order_acq_rel) == 1) {
+ delete this;
+ }
+ }
+
+ RefCount& operator=(const RefCount&) = delete;
+
+private:
+ mutable std::atomic_int count_;
+};
+
+/**
+ * Smart pointer for reference counted objects.
+ */
+template
+class RefCountPtr {
+public:
+ RefCountPtr() :
+ RefCountPtr(nullptr) {
+ }
+
+ RefCountPtr(T* ptr) :
+ RefCountPtr(ptr, true /* addRef */) {
+ }
+
+ RefCountPtr(const RefCountPtr& ptr) :
+ RefCountPtr(ptr.p_) {
+ }
+
+ RefCountPtr(RefCountPtr&& ptr) :
+ RefCountPtr() {
+ swap(*this, ptr);
+ }
+
+ ~RefCountPtr() {
+ if (p_) {
+ p_->release();
+ }
+ }
+
+ T* get() const {
+ return p_;
+ }
+
+ T* unwrap() {
+ auto p = p_;
+ p_ = nullptr;
+ return p;
+ }
+
+ T* operator->() const {
+ return p_;
+ }
+
+ T& operator*() const {
+ return *p_;
+ }
+
+ RefCountPtr& operator=(RefCountPtr ptr) {
+ swap(*this, ptr);
+ return *this;
+ }
+
+ template>>
+ operator RefCountPtr() const {
+ return RefCountPtr(p_);
+ }
+
+ explicit operator bool() const {
+ return p_;
+ }
+
+ static RefCountPtr wrap(T* ptr) {
+ return RefCountPtr(ptr, false /* addRef */);
+ }
+
+ friend void swap(RefCountPtr& ptr1, RefCountPtr& ptr2) {
+ using std::swap;
+ swap(ptr1.p_, ptr2.p_);
+ }
+
+private:
+ T* p_;
+
+ RefCountPtr(T* p, bool addRef) :
+ p_(p) {
+ if (p_ && addRef) {
+ p_->addRef();
+ }
+ }
+};
+
+template
+inline RefCountPtr makeRefCountPtr(ArgsT&&... 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 5f3b9252c1..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 */ \
@@ -102,7 +108,20 @@
(FILESYSTEM_FBIG, "File too large", -1909), \
(FILESYSTEM_INVAL, "Invalid parameter", -1910), \
(FILESYSTEM_NOSPC, "No space left in the filesystem", -1911), \
- (FILESYSTEM_NOMEM, "Memory allocation error", -1912)
+ (FILESYSTEM_NOMEM, "Memory allocation error", -1912), \
+ (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/debug_util.cpp b/services/src/debug_util.cpp
new file mode 100644
index 0000000000..75727cc70e
--- /dev/null
+++ b/services/src/debug_util.cpp
@@ -0,0 +1,94 @@
+/*
+ * 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 "debug_util.h"
+
+#include "system_error.h"
+#include "logging.h"
+
+#if HAL_PLATFORM_NRF52840
+
+#include
+
+int set_watchpoint(const void* addr, size_t size, int type) {
+ size_t numComp = (DWT->CTRL >> 28) & 0x0f;
+ if (!numComp) {
+ LOG(ERROR, "Watchpoints are not supported");
+ return SYSTEM_ERROR_NOT_SUPPORTED;
+ }
+ // TODO: Support more than one watchpoint
+ if (DWT->FUNCTION0) {
+ LOG(ERROR, "Watchpoint is already set");
+ return SYSTEM_ERROR_ALREADY_EXISTS;
+ }
+ if (!size || (size & (size - 1)) != 0) {
+ LOG(ERROR, "Size is not a power of 2");
+ return SYSTEM_ERROR_INVALID_ARGUMENT;
+ }
+ // Determine the mask size
+ DWT->MASK0 = 0x1f;
+ uint32_t maxMaskBits = DWT->MASK0;
+ uint32_t maskBits = 0;
+ while (size & 1) {
+ ++maskBits;
+ size >>= 1;
+ }
+ if (maskBits > maxMaskBits) {
+ LOG(ERROR, "Size is too large");
+ return SYSTEM_ERROR_INVALID_ARGUMENT;
+ }
+ // Address must be aligned by size
+ auto addrVal = (uintptr_t)addr;
+ if ((addrVal & ((1 << maskBits) - 1)) != 0) {
+ LOG(ERROR, "Address is not aligned");
+ return SYSTEM_ERROR_INVALID_ARGUMENT;
+ }
+ uint32_t func = 0;
+ switch (type) {
+ case WATCHPOINT_TYPE_READ:
+ func = 0x05;
+ break;
+ case WATCHPOINT_TYPE_WRITE:
+ func = 0x06;
+ break;
+ case WATCHPOINT_TYPE_READ_WRITE:
+ func = 0x07;
+ break;
+ default:
+ LOG(ERROR, "Invalid watchpoint type");
+ return SYSTEM_ERROR_INVALID_ARGUMENT;
+ }
+ DWT->COMP0 = addrVal;
+ DWT->MASK0 = maskBits;
+ DWT->FUNCTION0 = func; // Enables the watchpoint
+ return 0;
+}
+
+void clear_watchpoint(int idx) {
+ // TODO: Support more than one watchpoint
+ if (idx == 0) {
+ DWT->FUNCTION0 = 0;
+ }
+}
+
+void breakpoint() {
+ asm("BKPT");
+}
+
+#endif // HAL_PLATFORM_NRF52840
diff --git a/services/src/file_util.cpp b/services/src/file_util.cpp
index 8e64636c9f..7d7a6c76cf 100644
--- a/services/src/file_util.cpp
+++ b/services/src/file_util.cpp
@@ -166,26 +166,29 @@ int dumpFile(const char* path) {
return 0;
}
-int decodeMessageFromFile(lfs_file_t* file, const pb_msgdesc_t* desc, void* msg) {
+int decodeProtobufFromFile(lfs_file_t* file, const pb_msgdesc_t* desc, void* msg, int size) {
+ // TODO: nanopb is no longer exported as a dynalib so there's no need for allocating its objects
+ // on the heap
const auto strm = pb_istream_init(nullptr);
CHECK_TRUE(strm, SYSTEM_ERROR_NO_MEMORY);
SCOPE_GUARD({
pb_istream_free(strm, nullptr);
});
- CHECK_TRUE(pb_istream_from_file(strm, file, nullptr), SYSTEM_ERROR_FILE);
+ CHECK(pb_istream_from_file(strm, file, size, nullptr));
+ size = strm->bytes_left;
CHECK_TRUE(pb_decode(strm, desc, msg), SYSTEM_ERROR_FILE);
- return 0;
+ return size;
}
-int encodeMessageToFile(lfs_file_t* file, const pb_msgdesc_t* desc, const void* msg) {
+int encodeProtobufToFile(lfs_file_t* file, const pb_msgdesc_t* desc, const void* msg) {
const auto strm = pb_ostream_init(nullptr);
CHECK_TRUE(strm, SYSTEM_ERROR_NO_MEMORY);
SCOPE_GUARD({
pb_ostream_free(strm, nullptr);
});
- CHECK_TRUE(pb_ostream_from_file(strm, file, nullptr), SYSTEM_ERROR_FILE);
+ CHECK(pb_ostream_from_file(strm, file, nullptr));
CHECK_TRUE(pb_encode(strm, desc, msg), SYSTEM_ERROR_FILE);
- return 0;
+ return strm->bytes_written;
}
int rmrf(const char* path) {
diff --git a/services/src/nanopb_misc.c b/services/src/nanopb_misc.c
index 092e5f9183..4d503b7f1a 100644
--- a/services/src/nanopb_misc.c
+++ b/services/src/nanopb_misc.c
@@ -17,7 +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"
@@ -48,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);
}
@@ -96,39 +121,65 @@ bool pb_istream_from_buffer_ex(pb_istream_t* stream, const pb_byte_t *buf, size_
#if HAL_PLATFORM_FILESYSTEM
-bool pb_ostream_from_file(pb_ostream_t* stream, lfs_file_t* file, void* reserved) {
+int pb_ostream_from_file(pb_ostream_t* stream, lfs_file_t* file, void* reserved) {
if (!stream || !file) {
- return false;
+ return SYSTEM_ERROR_INVALID_ARGUMENT;
}
filesystem_t* const fs = filesystem_get_instance(FILESYSTEM_INSTANCE_DEFAULT, NULL);
if (!fs) {
- return false;
+ return SYSTEM_ERROR_FILESYSTEM;
}
memset(stream, 0, sizeof(pb_ostream_t));
stream->callback = write_file_callback;
stream->state = file;
stream->max_size = SIZE_MAX;
- return true;
+ return 0;
}
-bool pb_istream_from_file(pb_istream_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) {
if (!stream || !file) {
- return false;
+ return SYSTEM_ERROR_INVALID_ARGUMENT;
}
- filesystem_t* const fs = filesystem_get_instance(FILESYSTEM_INSTANCE_DEFAULT, NULL);
- if (!fs) {
- return false;
- }
- const lfs_soff_t pos = lfs_file_tell(&fs->instance, file);
- const lfs_soff_t size = lfs_file_size(&fs->instance, file);
- if (pos < 0 || size < 0) {
- return false;
+ if (size < 0) {
+ filesystem_t* const fs = filesystem_get_instance(FILESYSTEM_INSTANCE_DEFAULT, NULL);
+ if (!fs) {
+ return SYSTEM_ERROR_FILESYSTEM;
+ }
+ lfs_soff_t pos = lfs_file_tell(&fs->instance, file);
+ if (pos < 0) {
+ return filesystem_to_system_error(pos);
+ }
+ lfs_soff_t sz = lfs_file_size(&fs->instance, file);
+ if (sz < 0) {
+ return filesystem_to_system_error(sz);
+ }
+ size = sz - pos;
}
memset(stream, 0, sizeof(pb_istream_t));
stream->callback = read_file_callback;
stream->state = file;
- stream->bytes_left = size - pos;
- return true;
+ stream->bytes_left = size;
+ return 0;
}
#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-dynalib/src/system_dynalib_ledger.c b/system-dynalib/src/system_dynalib_ledger.c
new file mode 100644
index 0000000000..4ded8b7a96
--- /dev/null
+++ b/system-dynalib/src/system_dynalib_ledger.c
@@ -0,0 +1,3 @@
+#define DYNALIB_IMPORT
+
+#include "system_dynalib_ledger.h"
diff --git a/system/inc/active_object.h b/system/inc/active_object.h
index edcf618d10..af2af4e90f 100644
--- a/system/inc/active_object.h
+++ b/system/inc/active_object.h
@@ -499,13 +499,19 @@ class ISRTaskQueue {
struct Task {
TaskFunc func;
Task* next; // Next element in the queue
+ Task* prev; // Previous element in the queue
explicit Task(TaskFunc func = nullptr) :
func(func),
- next(nullptr) {
+ next(nullptr),
+ prev(nullptr) {
}
virtual ~Task() = default;
+
+ // Task objects are not copyable nor movable
+ Task(const Task&) = delete;
+ Task& operator=(const Task&) = delete;
};
ISRTaskQueue() :
@@ -513,7 +519,25 @@ class ISRTaskQueue {
lastTask_(nullptr) {
}
+ /**
+ * Add a task object to the queue.
+ *
+ * Calling this method is a no-op if the task object is already in the queue.
+ */
void enqueue(Task* task);
+
+ /**
+ * Remove a task object from the queue.
+ *
+ * Calling this method is a no-op if the task object is not in the queue.
+ */
+ void remove(Task* task);
+
+ /**
+ * Process the next task in the queue.
+ *
+ * @return `true` if the task was processed, or `false` if the queue was empty.
+ */
bool process();
private:
diff --git a/system/inc/system_dynalib_ledger.h b/system/inc/system_dynalib_ledger.h
new file mode 100644
index 0000000000..ed001d99f1
--- /dev/null
+++ b/system/inc/system_dynalib_ledger.h
@@ -0,0 +1,51 @@
+/*
+ * 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 "dynalib.h"
+
+#ifdef DYNALIB_EXPORT
+#include "system_ledger.h"
+#endif
+
+DYNALIB_BEGIN(system_ledger)
+
+DYNALIB_FN(0, system_ledger, ledger_get_instance, int(ledger_instance**, const char*, void*))
+DYNALIB_FN(1, system_ledger, ledger_add_ref, void(ledger_instance*, void*))
+DYNALIB_FN(2, system_ledger, ledger_release, void(ledger_instance*, void*))
+DYNALIB_FN(3, system_ledger, ledger_lock, void(ledger_instance*, void*))
+DYNALIB_FN(4, system_ledger, ledger_unlock, void(ledger_instance*, void*))
+DYNALIB_FN(5, system_ledger, ledger_set_callbacks, void(ledger_instance*, const ledger_callbacks*, void*))
+DYNALIB_FN(6, system_ledger, ledger_set_app_data, void(ledger_instance*, void*, ledger_destroy_app_data_callback, void*))
+DYNALIB_FN(7, system_ledger, ledger_get_app_data, void*(ledger_instance*, void*))
+DYNALIB_FN(8, system_ledger, ledger_get_info, int(ledger_instance*, ledger_info*, void*))
+DYNALIB_FN(9, system_ledger, ledger_open, int(ledger_stream**, ledger_instance*, int, void*))
+DYNALIB_FN(10, system_ledger, ledger_close, int(ledger_stream*, int, void*))
+DYNALIB_FN(11, system_ledger, ledger_read, int(ledger_stream*, char*, size_t, void*))
+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)
+
+#endif // HAL_PLATFORM_LEDGER
diff --git a/system/inc/system_ledger.h b/system/inc/system_ledger.h
new file mode 100644
index 0000000000..b8fde781bc
--- /dev/null
+++ b/system/inc/system_ledger.h
@@ -0,0 +1,325 @@
+/*
+ * 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
+
+/**
+ * Ledger API version.
+ */
+#define LEDGER_API_VERSION 1
+
+/**
+ * Maximum length of a ledger name.
+ */
+#define LEDGER_MAX_NAME_LENGTH 32
+
+/**
+ * Maximum size of ledger data.
+ */
+#define LEDGER_MAX_DATA_SIZE 16384
+
+/**
+ * Ledger instance.
+ */
+typedef struct ledger_instance ledger_instance;
+
+/**
+ * Stream instance.
+ */
+typedef struct ledger_stream ledger_stream;
+
+/**
+ * Callback invoked when a ledger has been synchronized with the Cloud.
+ *
+ * @param ledger Ledger instance.
+ * @param app_data Application data.
+ */
+typedef void (*ledger_sync_callback)(ledger_instance* ledger, void* app_data);
+
+/**
+ * Callback invoked to destroy the application data associated with a ledger instance.
+ *
+ * @param app_data Application data.
+ */
+typedef void (*ledger_destroy_app_data_callback)(void* app_data);
+
+/**
+ * Ledger scope.
+ */
+typedef enum ledger_scope {
+ LEDGER_SCOPE_UNKNOWN = 0, ///< Unknown scope.
+ LEDGER_SCOPE_DEVICE = 1, ///< Device scope.
+ LEDGER_SCOPE_PRODUCT = 2, ///< Product scope.
+ LEDGER_SCOPE_OWNER = 3 ///< Owner scope.
+} ledger_scope;
+
+/**
+ * Sync direction.
+ */
+typedef enum ledger_sync_direction {
+ LEDGER_SYNC_DIRECTION_UNKNOWN = 0, ///< Unknown direction.
+ LEDGER_SYNC_DIRECTION_DEVICE_TO_CLOUD = 1, ///< Device to cloud.
+ LEDGER_SYNC_DIRECTION_CLOUD_TO_DEVICE = 2 ///< Cloud to device.
+} ledger_sync_direction;
+
+/**
+ * Ledger info flags.
+ */
+typedef enum ledger_info_flag {
+ LEDGER_INFO_SYNC_PENDING = 0x01 ///< Ledger has changes that have not yet been synchronized with the Cloud.
+} ledger_info_flag;
+
+/**
+ * Stream mode flags.
+ */
+typedef enum ledger_stream_mode {
+ LEDGER_STREAM_MODE_READ = 0x01, ///< Open for reading.
+ LEDGER_STREAM_MODE_WRITE = 0x02 ///< Open for writing.
+} ledger_stream_mode;
+
+/**
+ * Stream close flags.
+ */
+typedef enum ledger_stream_close_flag {
+ LEDGER_STREAM_CLOSE_DISCARD = 0x01 ///< Discard any written data.
+} ledger_stream_close_flag;
+
+/**
+ * Ledger callbacks.
+ */
+typedef struct ledger_callbacks {
+ int version; ///< API version. Must be set to `LEDGER_API_VERSION`.
+ /**
+ * Callback invoked when the ledger has been synchronized with the Cloud.
+ */
+ ledger_sync_callback sync;
+} ledger_callbacks;
+
+/**
+ * Ledger info.
+ */
+typedef struct ledger_info {
+ int version; ///< API version. Must be set to `LEDGER_API_VERSION`.
+ const char* name; ///< Ledger name.
+ /**
+ * Time the ledger was last updated, in milliseconds since the Unix epoch.
+ *
+ * If 0, the time is unknown.
+ */
+ int64_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.
+ */
+ int64_t last_synced;
+ size_t data_size; ///< Size of the ledger data in bytes.
+ int scope; ///< Ledger scope as defined by the `ledger_scope` enum.
+ int sync_direction; ///< Synchronization direction as defined by the `ledger_sync_direction` enum.
+ int flags; ///< Flags defined by the `ledger_info_flag` enum.
+} ledger_info;
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+/**
+ * Get a ledger instance.
+ *
+ * @param ledger[out] Ledger instance.
+ * @param name Ledger name.
+ * @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_instance(ledger_instance** ledger, const char* name, void* reserved);
+
+/**
+ * Increment a ledger's reference count.
+ *
+ * @param ledger Ledger instance.
+ * @param reserved Reserved argument. Must be set to `NULL`.
+ */
+void ledger_add_ref(ledger_instance* ledger, void* reserved);
+
+/**
+ * Decrement a ledger's reference count.
+ *
+ * The ledger instance is destroyed when its reference count reaches 0.
+ *
+ * @param ledger Ledger instance.
+ * @param reserved Reserved argument. Must be set to `NULL`.
+ */
+void ledger_release(ledger_instance* ledger, void* reserved);
+
+/**
+ * Lock a ledger instance.
+ *
+ * The instance can be locked recursively by the same thread.
+ *
+ * @param ledger Ledger instance.
+ * @param reserved Reserved argument. Must be set to `NULL`.
+ */
+void ledger_lock(ledger_instance* ledger, void* reserved);
+
+/**
+ * Unlock a ledger instance.
+ *
+ * @param ledger Ledger instance.
+ * @param reserved Reserved argument. Must be set to `NULL`.
+ */
+void ledger_unlock(ledger_instance* ledger, void* reserved);
+
+/**
+ * Set the ledger callbacks.
+ *
+ * All callbacks are invoked in the system thread.
+ *
+ * @param ledger Ledger instance.
+ * @param callbacks Ledger callbacks. Can be set to `NULL` to clear all currently registered callbacks.
+ * @param reserved Reserved argument. Must be set to `NULL`.
+ */
+void ledger_set_callbacks(ledger_instance* ledger, const ledger_callbacks* callbacks, void* reserved);
+
+/**
+ * Attach application-specific data to a ledger instance.
+ *
+ * If a destructor callback is provided, it will be invoked when the ledger instance is destroyed.
+ * The calling code is responsible for destroying the old application data if it was already set for
+ * this ledger instance.
+ *
+ * @param ledger Ledger instance.
+ * @param app_data Application data.
+ * @param destroy Destructor callback. Can be `NULL`.
+ * @param reserved Reserved argument. Must be set to `NULL`.
+ */
+void ledger_set_app_data(ledger_instance* ledger, void* app_data, ledger_destroy_app_data_callback destroy,
+ void* reserved);
+
+/**
+ * Get application-specific data associated with a ledger instance.
+ *
+ * @param ledger Ledger instance.
+ * @param reserved Reserved argument. Must be set to `NULL`.
+ * @return Application data.
+ */
+void* ledger_get_app_data(ledger_instance* ledger, void* reserved);
+
+/**
+ * Get ledger info.
+ *
+ * @param ledger Ledger instance.
+ * @param[out] info Ledger info to be populated.
+ * @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_info(ledger_instance* ledger, ledger_info* info, void* reserved);
+
+/**
+ * Open a ledger for reading or writing.
+ *
+ * @param[out] stream Stream instance.
+ * @param ledger Ledger instance.
+ * @param mode Flags defined by the `ledger_stream_mode` enum.
+ * @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_open(ledger_stream** stream, ledger_instance* ledger, int mode, void* reserved);
+
+/**
+ * Close a stream.
+ *
+ * The stream instance is destroyed even if an error occurs while closing the stream.
+ *
+ * @param stream Stream instance.
+ * @param flags Flags defined by the `ledger_stream_close_flag` enum.
+ * @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_close(ledger_stream* stream, int flags, void* reserved);
+
+/**
+ * Read from a stream.
+ *
+ * @param stream Stream instance.
+ * @param[out] data Output buffer.
+ * @param size Maximum 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 ledger_read(ledger_stream* stream, char* data, size_t size, void* reserved);
+
+/**
+ * Write to a stream.
+ *
+ * @param stream Stream instance.
+ * @param data Input buffer.
+ * @param size Number of bytes to write.
+ * @param reserved Reserved argument. Must be set to `NULL`.
+ * @return Number of bytes written or an error code defined by the `system_error_t` enum.
+ */
+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 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.
+ * @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_purge(const char* name, void* reserved);
+
+/**
+ * Remove any local data associated with existing ledgers.
+ *
+ * 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.
+ *
+ * @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_purge_all(void* reserved);
+
+#ifdef __cplusplus
+} // extern "C"
+#endif
+
+#endif // HAL_PLATFORM_LEDGER
diff --git a/system/inc/system_task.h b/system/inc/system_task.h
index 38d3947025..184ed2e230 100644
--- a/system/inc/system_task.h
+++ b/system/inc/system_task.h
@@ -32,8 +32,12 @@
#include "wlan_hal.h"
#ifdef __cplusplus
+
+#include
+
extern "C" {
-#endif
+
+#endif // defined(__cplusplus)
uint32_t HAL_NET_SetNetWatchDog(uint32_t timeOutInuS);
void Network_Setup(bool threaded);
@@ -117,7 +121,51 @@ typedef int (*system_task_fn)();
int system_isr_task_queue_free_memory(void *ptrToFree);
#ifdef __cplusplus
+
+} // extern "C"
+
+namespace particle::system {
+
+/**
+ * Allocate an object in the system pool.
+ *
+ * This function can be called from an ISR.
+ *
+ * @tparam T Object type.
+ * @param ... Arguments to pass to the constructor of `T`.
+ * @return Pointer to the object.
+ *
+ * @see `systemPoolDelete`
+ */
+template
+inline T* systemPoolNew(ArgsT&&... args) {
+ auto p = static_cast(system_pool_alloc(sizeof(T), nullptr /* reserved */));
+ if (p) {
+ new(p) T(std::forward(args)...);
+ }
+ return p;
}
-#endif
+
+/**
+ * Destroy an object allocated in the system pool.
+ *
+ * This function can be called from an ISR.
+ *
+ * @tparam T Object type.
+ * @param p Pointer to the object.
+ *
+ * @see `systemPoolNew`
+ */
+template
+inline void systemPoolDelete(T* p) {
+ if (p) {
+ p->~T();
+ system_pool_free(p, nullptr /* reserved */);
+ }
+}
+
+} // namespace particle::system
+
+#endif // defined(__cplusplus)
#endif /*__SPARK_WLAN_H*/
diff --git a/system/src/active_object.cpp b/system/src/active_object.cpp
index 3d4a16a9b5..6cc6361314 100644
--- a/system/src/active_object.cpp
+++ b/system/src/active_object.cpp
@@ -98,6 +98,9 @@ void ActiveObjectBase::run_active_object(void* data)
void ISRTaskQueue::enqueue(Task* task) {
ATOMIC_BLOCK() {
+ if (task->next || task->prev || lastTask_ == task) {
+ return; // Task object is already in the queue
+ }
// Add task object to the queue
if (lastTask_) {
lastTask_->next = task;
@@ -105,6 +108,7 @@ void ISRTaskQueue::enqueue(Task* task) {
firstTask_ = task;
}
task->next = nullptr;
+ task->prev = lastTask_;
lastTask_ = task;
}
// FIXME: some other feature flag?
@@ -113,16 +117,41 @@ void ISRTaskQueue::enqueue(Task* task) {
#endif // HAL_PLATFORM_SOCKET_IOCTL_NOTIFY
}
+void ISRTaskQueue::remove(Task* task) {
+ ATOMIC_BLOCK() {
+ auto next = task->next;
+ auto prev = task->prev;
+ if (next) {
+ next->prev = prev;
+ task->next = nullptr;
+ } else if (lastTask_ == task) {
+ lastTask_ = prev;
+ }
+ if (prev) {
+ prev->next = next;
+ task->prev = nullptr;
+ } else if (firstTask_ == task) {
+ firstTask_ = next;
+ }
+ }
+}
+
bool ISRTaskQueue::process() {
- Task* task = nullptr;
if (!firstTask_) {
return false;
}
+ Task* task = nullptr;
ATOMIC_BLOCK() {
+ if (!firstTask_) {
+ return false;
+ }
// Take task object from the queue
task = firstTask_;
firstTask_ = task->next;
- if (!firstTask_) {
+ if (firstTask_) {
+ firstTask_->prev = nullptr;
+ task->next = nullptr;
+ } else {
lastTask_ = nullptr;
}
}
diff --git a/system/src/ledger/ledger.cpp b/system/src/ledger/ledger.cpp
new file mode 100644
index 0000000000..b1eca0a047
--- /dev/null
+++ b/system/src/ledger/ledger.cpp
@@ -0,0 +1,891 @@
+/*
+ * 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 "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"
+
+#include "cloud/ledger.pb.h" // Cloud protocol definitions
+#include "ledger.pb.h" // Internal definitions
+
+#define PB_LEDGER(_name) particle_cloud_ledger_##_name
+#define PB_INTERNAL(_name) particle_firmware_##_name
+
+LOG_SOURCE_CATEGORY("system.ledger");
+
+namespace particle {
+
+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 {
+
+/*
+ The ledger directory is organized as follows:
+
+ /usr/ledger/
+ |
+ +--- my-ledger/ - Files of the ledger called "my-ledger"
+ | |
+ | +--- staged/ - Fully written ledger data files
+ | | +--- 0001
+ | | +--- 0002
+ | | +--- ...
+ | |
+ | +--- temp/ - Partially written ledger data files
+ | | +--- 0003
+ | | +--- 0004
+ | | +--- ...
+ | |
+ | +--- current - Current ledger data
+ |
+ +--- ...
+
+ When a ledger is opened for writing, a temporary file for the new ledger data is created in the
+ "temp" directory. When writing is finished, if there are active readers of the ledger data, the
+ temporary file is moved to the "staged" directory, otherwise the file is moved to "current" to
+ replace the current ledger data.
+
+ When a ledger is opened for reading, it is checked whether the most recent ledger data is stored
+ in "current" or a file in the "staged" directory. The ledger data is then read from the respective
+ file.
+
+ The ledger instance tracks the readers of the current and staged data to ensure that the relevant
+ files are modified or removed only when the last reader accessing them is closed. When all readers
+ are closed, the most recent staged data is moved to "current" and all other files in "staged" are
+ removed.
+*/
+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_PATH_LEN = 127;
+
+// Internal result codes
+enum Result {
+ CURRENT_DATA_NOT_FOUND = 1
+};
+
+/*
+ The layout of a ledger data file:
+
+ Field | Size | Description
+ ----------+-----------+------------
+ data | data_size | Contents of the ledger in an application-specific format
+ info | info_size | Protobuf-encoded ledger info (particle.firmware.LedgerInfo)
+ data_size | 4 | Size of the "data" field (unsigned integer)
+ info_size | 4 | Size of the "info" field (unsigned integer)
+ version | 4 | Format version number (unsigned integer)
+
+ All integer fields are encoded in little-endian byte order.
+*/
+struct LedgerDataFooter {
+ uint32_t dataSize;
+ uint32_t infoSize;
+ uint32_t version;
+} __attribute__((packed));
+
+int readFooter(lfs_t* fs, lfs_file_t* file, size_t* dataSize = nullptr, size_t* infoSize = nullptr, int* version = nullptr) {
+ LedgerDataFooter f = {};
+ size_t n = CHECK_FS(lfs_file_read(fs, file, &f, sizeof(f)));
+ if (n != sizeof(f)) {
+ LOG(ERROR, "Unexpected end of ledger data file");
+ return SYSTEM_ERROR_LEDGER_INVALID_FORMAT;
+ }
+ if (dataSize) {
+ *dataSize = littleEndianToNative(f.dataSize);
+ }
+ if (infoSize) {
+ *infoSize = littleEndianToNative(f.infoSize);
+ }
+ if (version) {
+ *version = littleEndianToNative(f.version);
+ }
+ return n;
+}
+
+int writeFooter(lfs_t* fs, lfs_file_t* file, size_t dataSize, size_t infoSize, int version = DATA_FORMAT_VERSION) {
+ LedgerDataFooter f = {};
+ f.dataSize = nativeToLittleEndian(dataSize);
+ f.infoSize = nativeToLittleEndian(infoSize);
+ f.version = nativeToLittleEndian(version);
+ size_t n = CHECK_FS(lfs_file_write(fs, file, &f, sizeof(f)));
+ if (n != sizeof(f)) {
+ LOG(ERROR, "Unexpected number of bytes written");
+ return SYSTEM_ERROR_FILESYSTEM;
+ }
+ return n;
+}
+
+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; // Name is longer than the maximum size specified in ledger.proto
+ }
+ 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());
+ 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;
+}
+
+inline int getTempDirPath(char* buf, size_t size, const char* ledgerName) {
+ CHECK(formatLedgerPath(buf, size, ledgerName, "%s/", TEMP_DATA_DIR_NAME));
+ return 0;
+}
+
+inline int getTempFilePath(char* buf, size_t size, const char* ledgerName, int seqNum) {
+ CHECK(formatLedgerPath(buf, size, ledgerName, "%s/%04d", TEMP_DATA_DIR_NAME, seqNum));
+ return 0;
+}
+
+inline int getTempFilePath(char* buf, size_t size, const char* ledgerName, const char* fileName) {
+ CHECK(formatLedgerPath(buf, size, ledgerName, "%s/%s", TEMP_DATA_DIR_NAME, fileName));
+ return 0;
+}
+
+inline int getStagedDirPath(char* buf, size_t size, const char* ledgerName) {
+ CHECK(formatLedgerPath(buf, size, ledgerName, "%s/", STAGED_DATA_DIR_NAME));
+ return 0;
+}
+
+inline int getStagedFilePath(char* buf, size_t size, const char* ledgerName, int seqNum) {
+ CHECK(formatLedgerPath(buf, size, ledgerName, "%s/%04d", STAGED_DATA_DIR_NAME, seqNum));
+ return 0;
+}
+
+inline int getStagedFilePath(char* buf, size_t size, const char* ledgerName, const char* fileName) {
+ CHECK(formatLedgerPath(buf, size, ledgerName, "%s/%s", STAGED_DATA_DIR_NAME, fileName));
+ return 0;
+}
+
+inline int getCurrentFilePath(char* buf, size_t size, const char* ledgerName) {
+ CHECK(formatLedgerPath(buf, size, ledgerName, "%s", CURRENT_DATA_FILE_NAME));
+ return 0;
+}
+
+bool isLedgerNameValid(const char* name) {
+ size_t len = 0;
+ char c = 0;
+ while ((c = *name++)) {
+ if (!(++len <= LEDGER_MAX_NAME_LENGTH && ((c >= 'a' && c <= 'z') || (c >= '0' && c <= '9') || c == '-'))) {
+ return false;
+ }
+ }
+ if (len == 0) {
+ return false;
+ }
+ return true;
+}
+
+// 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;
+}
+
+} // namespace
+
+Ledger::Ledger(detail::LedgerSyncContext* ctx) :
+ LedgerBase(ctx),
+ lastSeqNum_(0),
+ stagedSeqNum_(0),
+ curReaderCount_(0),
+ stagedReaderCount_(0),
+ stagedFileCount_(0),
+ lastUpdated_(0),
+ lastSynced_(0),
+ dataSize_(0),
+ updateCount_(0),
+ syncPending_(false),
+ syncCallback_(nullptr),
+ destroyAppData_(nullptr),
+ appData_(nullptr),
+ name_(""),
+ scopeId_(EMPTY_LEDGER_SCOPE_ID),
+ scopeType_(LEDGER_SCOPE_UNKNOWN),
+ syncDir_(LEDGER_SYNC_DIRECTION_UNKNOWN),
+ inited_(false) {
+}
+
+Ledger::~Ledger() {
+ if (appData_ && destroyAppData_) {
+ destroyAppData_(appData_);
+ }
+}
+
+int Ledger::init(const char* name) {
+ if (!isLedgerNameValid(name)) {
+ return SYSTEM_ERROR_INVALID_ARGUMENT;
+ }
+ name_ = name;
+ FsLock fs;
+ int r = CHECK(loadLedgerInfo(fs.instance()));
+ if (r == Result::CURRENT_DATA_NOT_FOUND) {
+ // Initialize the ledger directory
+ char path[MAX_PATH_LEN + 1];
+ CHECK(getTempDirPath(path, sizeof(path), name_));
+ CHECK(mkdirp(path));
+ CHECK(getStagedDirPath(path, sizeof(path), name_));
+ CHECK(mkdirp(path));
+ // Create a file for the current ledger data
+ CHECK(initCurrentData(fs.instance()));
+ } else {
+ // Clean up and recover staged data
+ CHECK(removeTempData(fs.instance()));
+ r = CHECK(flushStagedData(fs.instance()));
+ if (r > 0) {
+ // Found staged data. Reload the ledger info
+ CHECK(loadLedgerInfo(fs.instance()));
+ }
+ }
+ inited_ = true;
+ return 0;
+}
+
+int Ledger::initReader(LedgerReader& reader) {
+ // Reference counter of this ledger instance is managed by the LedgerManager so if it needs
+ // to be incremented it's important to do so before acquiring a lock on the ledger instance
+ // to avoid a deadlock
+ RefCountPtr ledgerPtr(this);
+ std::lock_guard lock(*this);
+ if (!inited_) {
+ return SYSTEM_ERROR_INVALID_STATE;
+ }
+ CHECK(reader.init(info(), stagedSeqNum_, std::move(ledgerPtr)));
+ if (stagedSeqNum_ > 0) {
+ ++stagedReaderCount_;
+ } else {
+ ++curReaderCount_;
+ }
+ return 0;
+}
+
+int Ledger::initWriter(LedgerWriter& writer, LedgerWriteSource src) {
+ RefCountPtr ledgerPtr(this); // See initReader()
+ std::lock_guard lock(*this);
+ if (!inited_) {
+ return SYSTEM_ERROR_INVALID_STATE;
+ }
+ // It's allowed to write to a ledger while its sync direction is unknown
+ if (src == LedgerWriteSource::USER && syncDir_ != LEDGER_SYNC_DIRECTION_DEVICE_TO_CLOUD &&
+ syncDir_ != LEDGER_SYNC_DIRECTION_UNKNOWN) {
+ return SYSTEM_ERROR_LEDGER_READ_ONLY;
+ }
+ CHECK(writer.init(src, ++lastSeqNum_, std::move(ledgerPtr)));
+ return 0;
+}
+
+LedgerInfo Ledger::info() const {
+ std::lock_guard lock(*this);
+ return LedgerInfo()
+ .scopeType(scopeType_)
+ .scopeId(scopeId_)
+ .syncDirection(syncDir_)
+ .dataSize(dataSize_)
+ .lastUpdated(lastUpdated_)
+ .lastSynced(lastSynced_)
+ .updateCount(updateCount_)
+ .syncPending(syncPending_);
+}
+
+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_;
+ } else {
+ --curReaderCount_;
+ }
+ // Check if there's staged data and if it can be flushed
+ int result = 0;
+ if (stagedSeqNum_ > 0 && curReaderCount_ == 0 && stagedReaderCount_ == 0) {
+ // To save on RAM usage, we don't count how many readers are open for each individual file,
+ // which may result in the staged directory containing multiple unused files with outdated
+ // ledger data. Typically, however, when the last reader is closed, there will be at most
+ // one file with staged data as the application never keeps ledger streams open for long.
+ // The only slow reader is the system which streams ledger data to the server
+ FsLock fs;
+ if (stagedFileCount_ == 1) {
+ // Fast track: there's one file with staged data. Move it to "current"
+ char srcPath[MAX_PATH_LEN + 1];
+ char destPath[MAX_PATH_LEN + 1];
+ CHECK(getStagedFilePath(srcPath, sizeof(srcPath), name_, stagedSeqNum_));
+ CHECK(getCurrentFilePath(destPath, sizeof(destPath), name_));
+ result = renameFile(fs.instance(), srcPath, destPath);
+ if (result < 0) {
+ // The filesystem is full or broken, or somebody messed up the staged directory
+ LOG(ERROR, "Failed to rename file: %d", result);
+ int r = removeFile(fs.instance(), srcPath);
+ if (r < 0) {
+ LOG(WARN, "Failed to remove file: %d", r);
+ }
+ }
+ } else {
+ // The staged directory contains outdated files. Clean those up and move the most recent
+ // file to "current"
+ int r = flushStagedData(fs.instance());
+ if (r < 0) {
+ result = r;
+ } 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_STATE;
+ }
+ }
+ if (result < 0) {
+ // Try recovering some known consistent state
+ int r = loadLedgerInfo(fs.instance());
+ if (r < 0) {
+ LOG(ERROR, "Failed to recover ledger state");
+ }
+ }
+ stagedSeqNum_ = 0;
+ stagedFileCount_ = 0;
+ }
+ return result;
+}
+
+int Ledger::notifyWriterClosed(const LedgerInfo& info, int tempSeqNum) {
+ std::unique_lock lock(*this);
+ FsLock fs;
+ // Move the file where appropriate
+ bool newStagedFile = false;
+ char srcPath[MAX_PATH_LEN + 1];
+ char destPath[MAX_PATH_LEN + 1];
+ CHECK(getTempFilePath(srcPath, sizeof(srcPath), name_, tempSeqNum));
+ if (curReaderCount_ == 0 && stagedReaderCount_ == 0) {
+ // Nobody is reading anything. Move to "current"
+ CHECK(getCurrentFilePath(destPath, sizeof(destPath), name_));
+ } else if (stagedSeqNum_ > 0 && stagedReaderCount_ == 0) {
+ // There's staged data and nobody is reading it. Replace it
+ CHECK(getStagedFilePath(destPath, sizeof(destPath), name_, stagedSeqNum_));
+ } else {
+ // Create a new file in "staged". Note that in this case, "current" can't be replaced with
+ // the new data even if nobody is reading it, as otherwise it would get overwritten with
+ // outdated data if the device resets before the staged directory is cleaned up
+ CHECK(getStagedFilePath(destPath, sizeof(destPath), name_, tempSeqNum));
+ newStagedFile = true;
+ }
+ CHECK_FS(lfs_rename(fs.instance(), srcPath, destPath));
+ if (newStagedFile) {
+ stagedSeqNum_ = tempSeqNum;
+ ++stagedFileCount_;
+ }
+ setLedgerInfo(this->info().update(info));
+ return 0;
+}
+
+int Ledger::loadLedgerInfo(lfs_t* fs) {
+ // Open the file with the current ledger data
+ char path[MAX_PATH_LEN + 1];
+ CHECK(getCurrentFilePath(path, sizeof(path), name_));
+ lfs_file_t file = {};
+ int r = lfs_file_open(fs, &file, path, LFS_O_RDONLY);
+ if (r < 0) {
+ if (r == LFS_ERR_NOENT) {
+ return Result::CURRENT_DATA_NOT_FOUND;
+ }
+ CHECK_FS(r); // Forward the error
+ }
+ SCOPE_GUARD({
+ int r = closeFile(fs, &file);
+ if (r < 0) {
+ LOG(ERROR, "Error while closing file: %d", r);
+ }
+ });
+ // Read the footer
+ size_t dataSize = 0;
+ size_t infoSize = 0;
+ int version = 0;
+ CHECK_FS(lfs_file_seek(fs, &file, -(int)sizeof(LedgerDataFooter), LFS_SEEK_END));
+ CHECK(readFooter(fs, &file, &dataSize, &infoSize, &version));
+ if (version != DATA_FORMAT_VERSION) {
+ LOG(ERROR, "Unsupported version of ledger data format: %d", version);
+ return SYSTEM_ERROR_LEDGER_UNSUPPORTED_FORMAT;
+ }
+ size_t fileSize = CHECK_FS(lfs_file_size(fs, &file));
+ if (fileSize != dataSize + infoSize + sizeof(LedgerDataFooter)) {
+ LOG(ERROR, "Unexpected size of ledger data file");
+ return SYSTEM_ERROR_LEDGER_INVALID_FORMAT;
+ }
+ // Skip the data section
+ CHECK_FS(lfs_file_seek(fs, &file, dataSize, LFS_SEEK_SET));
+ // Read the info section
+ PB_INTERNAL(LedgerInfo) pbInfo = {};
+ r = decodeProtobufFromFile(&file, &PB_INTERNAL(LedgerInfo_msg), &pbInfo, infoSize);
+ if (r < 0) {
+ LOG(ERROR, "Failed to parse ledger info: %d", r);
+ return SYSTEM_ERROR_LEDGER_INVALID_FORMAT;
+ }
+ if (std::strcmp(pbInfo.name, name_) != 0) {
+ LOG(ERROR, "Unexpected ledger name");
+ return SYSTEM_ERROR_LEDGER_INVALID_FORMAT;
+ }
+ 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.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::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) {
+ // Create a file for the current ledger data
+ char path[MAX_PATH_LEN + 1];
+ CHECK(getCurrentFilePath(path, sizeof(path), name_));
+ lfs_file_t file = {};
+ CHECK_FS(lfs_file_open(fs, &file, path, LFS_O_WRONLY | LFS_O_CREAT | LFS_O_EXCL));
+ SCOPE_GUARD({
+ int r = closeFile(fs, &file);
+ if (r < 0) {
+ LOG(ERROR, "Error while closing file: %d", r);
+ }
+ });
+ // Write the info section
+ size_t infoSize = CHECK(writeLedgerInfo(fs, &file, name_, info()));
+ // Write the footer
+ CHECK(writeFooter(fs, &file, 0 /* dataSize */, infoSize));
+ return 0;
+}
+
+int Ledger::flushStagedData(lfs_t* fs) {
+ char path[MAX_PATH_LEN + 1];
+ CHECK(getStagedDirPath(path, sizeof(path), name_));
+ lfs_dir_t dir = {};
+ CHECK_FS(lfs_dir_open(fs, &dir, path));
+ SCOPE_GUARD({
+ int r = closeDir(fs, &dir);
+ if (r < 0) {
+ LOG(ERROR, "Failed to close directory handle: %d", r);
+ }
+ });
+ // Find the latest staged data
+ char fileName[32] = {};
+ int maxSeqNum = 0;
+ lfs_info entry = {};
+ int r = 0;
+ while ((r = lfs_dir_read(fs, &dir, &entry)) == 1) {
+ if (entry.type != LFS_TYPE_REG) {
+ if (entry.type != LFS_TYPE_DIR || (std::strcmp(entry.name, ".") != 0 && std::strcmp(entry.name, "..") != 0)) {
+ LOG(WARN, "Found unexpected entry in ledger directory");
+ }
+ continue;
+ }
+ char* end = nullptr;
+ int seqNum = std::strtol(entry.name, &end, 10);
+ if (end != entry.name + std::strlen(entry.name)) {
+ LOG(WARN, "Found unexpected entry in ledger directory");
+ continue;
+ }
+ if (seqNum > maxSeqNum) {
+ if (maxSeqNum > 0) {
+ // Older staged files must be removed before the most recent file is moved to
+ // "current", otherwise "current" would get overwritten with outdated data if
+ // the device resets before the staged directory is cleaned up
+ CHECK(getStagedFilePath(path, sizeof(path), name_, fileName));
+ CHECK_FS(lfs_remove(fs, path));
+ }
+ size_t n = strlcpy(fileName, entry.name, sizeof(fileName));
+ if (n >= sizeof(fileName)) {
+ return SYSTEM_ERROR_INTERNAL;
+ }
+ maxSeqNum = seqNum;
+ } else {
+ CHECK(getStagedFilePath(path, sizeof(path), name_, entry.name));
+ CHECK_FS(lfs_remove(fs, path));
+ }
+ }
+ CHECK_FS(r);
+ if (maxSeqNum > 0) {
+ // Replace "current" with the found file
+ char destPath[MAX_PATH_LEN + 1];
+ CHECK(getCurrentFilePath(destPath, sizeof(destPath), name_));
+ CHECK(getStagedFilePath(path, sizeof(path), name_, fileName));
+ CHECK_FS(lfs_rename(fs, path, destPath));
+ return maxSeqNum;
+ }
+ return 0;
+}
+
+int Ledger::removeTempData(lfs_t* fs) {
+ char path[MAX_PATH_LEN + 1];
+ CHECK(getTempDirPath(path, sizeof(path), name_));
+ lfs_dir_t dir = {};
+ CHECK_FS(lfs_dir_open(fs, &dir, path));
+ SCOPE_GUARD({
+ int r = closeDir(fs, &dir);
+ if (r < 0) {
+ LOG(ERROR, "Failed to close directory handle: %d", r);
+ }
+ });
+ lfs_info entry = {};
+ int r = 0;
+ while ((r = lfs_dir_read(fs, &dir, &entry)) == 1) {
+ if (entry.type != LFS_TYPE_REG) {
+ if (entry.type != LFS_TYPE_DIR || (std::strcmp(entry.name, ".") != 0 && std::strcmp(entry.name, "..") != 0)) {
+ LOG(WARN, "Found unexpected entry in ledger directory");
+ }
+ continue;
+ }
+ CHECK(getTempFilePath(path, sizeof(path), name_, entry.name));
+ CHECK_FS(lfs_remove(fs, path));
+ }
+ CHECK_FS(r);
+ 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.scopeType_.has_value()) {
+ scopeType_ = info.scopeType_.value();
+ }
+ if (info.scopeId_.has_value()) {
+ scopeId_ = info.scopeId_.value();
+ }
+ if (info.syncDir_.has_value()) {
+ syncDir_ = info.syncDir_.value();
+ }
+ if (info.dataSize_.has_value()) {
+ dataSize_ = info.dataSize_.value();
+ }
+ if (info.lastUpdated_.has_value()) {
+ lastUpdated_ = info.lastUpdated_.value();
+ }
+ if (info.lastSynced_.has_value()) {
+ lastSynced_ = info.lastSynced_.value();
+ }
+ if (info.updateCount_.has_value()) {
+ updateCount_ = info.updateCount_.value();
+ }
+ if (info.syncPending_.has_value()) {
+ syncPending_ = info.syncPending_.value();
+ }
+ return *this;
+}
+
+int LedgerReader::init(LedgerInfo info, int stagedSeqNum, RefCountPtr ledger) {
+ char path[MAX_PATH_LEN + 1];
+ if (stagedSeqNum > 0) {
+ // The most recent data is staged
+ CHECK(getStagedFilePath(path, sizeof(path), ledger->name(), stagedSeqNum));
+ staged_ = true;
+ } else {
+ CHECK(getCurrentFilePath(path, sizeof(path), ledger->name()));
+ }
+ // Open the respective file
+ FsLock fs;
+ CHECK_FS(lfs_file_open(fs.instance(), &file_, path, LFS_O_RDONLY));
+ ledger_ = std::move(ledger);
+ info_ = std::move(info);
+ open_ = true;
+ return 0;
+}
+
+int LedgerReader::read(char* data, size_t size) {
+ if (!open_) {
+ return SYSTEM_ERROR_INVALID_STATE;
+ }
+ size_t bytesToRead = std::min(size, info_.dataSize() - dataOffs_);
+ if (size > 0 && bytesToRead == 0) {
+ return SYSTEM_ERROR_END_OF_STREAM;
+ }
+ FsLock fs;
+ size_t n = CHECK_FS(lfs_file_read(fs.instance(), &file_, data, bytesToRead));
+ if (n != bytesToRead) {
+ LOG(ERROR, "Unexpected end of ledger data file");
+ return SYSTEM_ERROR_LEDGER_INVALID_FORMAT;
+ }
+ dataOffs_ += n;
+ return n;
+}
+
+int LedgerReader::close(bool /* discard */) {
+ if (!open_) {
+ return 0;
+ }
+ // Consider the reader closed regardless if any of the operations below fails
+ open_ = false;
+ FsLock fs;
+ int result = closeFile(fs.instance(), &file_);
+ if (result < 0) {
+ LOG(ERROR, "Error while closing file: %d", result);
+ }
+ // Let the ledger flush any data. Avoid holding a lock on the filesystem while trying to acquire
+ // a lock on the ledger instance
+ fs.unlock();
+ int r = ledger_->notifyReaderClosed(staged_);
+ fs.lock();
+ if (r < 0) {
+ LOG(ERROR, "Failed to flush ledger data: %d", r);
+ return r;
+ }
+ return result;
+}
+
+int LedgerWriter::init(LedgerWriteSource src, int tempSeqNum, RefCountPtr ledger) {
+ // Create a temporary file
+ char path[MAX_PATH_LEN + 1];
+ CHECK(getTempFilePath(path, sizeof(path), ledger->name(), tempSeqNum));
+ FsLock fs;
+ CHECK_FS(lfs_file_open(fs.instance(), &file_, path, LFS_O_WRONLY | LFS_O_CREAT | LFS_O_EXCL));
+ ledger_ = std::move(ledger);
+ tempSeqNum_ = tempSeqNum;
+ src_ = src;
+ open_ = true;
+ return 0;
+}
+
+int LedgerWriter::write(const char* data, size_t size) {
+ if (!open_) {
+ return SYSTEM_ERROR_INVALID_STATE;
+ }
+ if (src_ == LedgerWriteSource::USER && dataSize_ + size > LEDGER_MAX_DATA_SIZE) {
+ LOG(ERROR, "Ledger data is too long");
+ return SYSTEM_ERROR_LEDGER_TOO_LARGE;
+ }
+ FsLock fs;
+ size_t n = CHECK_FS(lfs_file_write(fs.instance(), &file_, data, size));
+ if (n != size) {
+ LOG(ERROR, "Unexpected number of bytes written");
+ return SYSTEM_ERROR_FILESYSTEM;
+ }
+ dataSize_ += n;
+ return n;
+}
+
+int LedgerWriter::close(bool discard) {
+ if (!open_) {
+ return 0;
+ }
+ // Consider the writer closed regardless if any of the operations below fails
+ open_ = false;
+ // Lock the ledger instance first then the filesystem, otherwise a deadlock is possible if the
+ // LedgerManager has locked the ledger instance already but is waiting on the filesystem lock
+ std::unique_lock lock(*ledger_);
+ FsLock fs;
+ if (discard) {
+ // Remove the temporary file
+ char path[MAX_PATH_LEN + 1];
+ CHECK(getTempFilePath(path, sizeof(path), ledger_->name(), tempSeqNum_));
+ int r = closeFile(fs.instance(), &file_);
+ if (r < 0) {
+ LOG(ERROR, "Error while closing file: %d", r);
+ }
+ CHECK_FS(lfs_remove(fs.instance(), path));
+ return r;
+ }
+ NAMED_SCOPE_GUARD(removeFileGuard, {
+ char path[MAX_PATH_LEN + 1];
+ int r = getTempFilePath(path, sizeof(path), ledger_->name(), tempSeqNum_);
+ if (r >= 0) {
+ r = removeFile(fs.instance(), path);
+ }
+ if (r < 0) {
+ LOG(ERROR, "Failed to remove file: %d", r);
+ }
+ });
+ NAMED_SCOPE_GUARD(closeFileGuard, { // Will run before removeFileGuard
+ int r = closeFile(fs.instance(), &file_);
+ if (r < 0) {
+ LOG(ERROR, "Error while closing file: %d", r);
+ }
+ });
+ // Prepare the updated ledger info
+ auto newInfo = ledger_->info().update(info_);
+ newInfo.dataSize(dataSize_); // Can't be overridden
+ 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(t);
+ }
+ if (src_ == LedgerWriteSource::USER && !info_.isSyncPendingSet()) {
+ newInfo.syncPending(true);
+ }
+ // Write the info section
+ size_t infoSize = CHECK(writeLedgerInfo(fs.instance(), &file_, ledger_->name(), newInfo));
+ // Write the footer
+ CHECK(writeFooter(fs.instance(), &file_, dataSize_, infoSize));
+ closeFileGuard.dismiss();
+ CHECK_FS(lfs_file_close(fs.instance(), &file_));
+ // Flush the data. Keep the ledger instance locked so that the ledger state is updated atomically
+ // in the filesystem and RAM. TODO: Finalize writing to the temporary ledger file in Ledger rather
+ // than in LedgerWriter
+ int r = ledger_->notifyWriterClosed(newInfo, tempSeqNum_);
+ if (r < 0) {
+ LOG(ERROR, "Failed to flush ledger data: %d", r);
+ return r;
+ }
+ removeFileGuard.dismiss();
+ if (src_ == LedgerWriteSource::USER) {
+ // Avoid holding any locks when calling into the manager
+ fs.unlock();
+ lock.unlock();
+ LedgerManager::instance()->notifyLedgerChanged(ledger_->syncContext());
+ fs.lock(); // FIXME: FsLock doesn't know when it's unlocked
+ }
+ return 0;
+}
+
+} // namespace system
+
+} // namespace particle
+
+#endif // HAL_PLATFORM_LEDGER
diff --git a/system/src/ledger/ledger.h b/system/src/ledger/ledger.h
new file mode 100644
index 0000000000..70283e27ec
--- /dev/null
+++ b/system/src/ledger/ledger.h
@@ -0,0 +1,440 @@
+/*
+ * 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
+#include
+
+#include "system_ledger.h"
+
+#include "filesystem.h"
+#include "static_recursive_mutex.h"
+#include "ref_count.h"
+#include "system_error.h"
+
+namespace particle::system {
+
+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 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:
+ explicit LedgerBase(detail::LedgerSyncContext* ctx = nullptr) :
+ syncCtx_(ctx),
+ refCount_(1) {
+ }
+
+ LedgerBase(const LedgerBase&) = delete;
+
+ virtual ~LedgerBase() = default;
+
+ void addRef() const;
+ void release() const;
+
+ LedgerBase& operator=(const LedgerBase&) = delete;
+
+protected:
+ detail::LedgerSyncContext* syncContext() const { // Called by LedgerManager and LedgerWriter
+ return syncCtx_;
+ }
+
+ int& refCount() const { // Called by LedgerManager
+ return refCount_;
+ }
+
+private:
+ detail::LedgerSyncContext* syncCtx_; // Sync context
+ mutable int refCount_; // Reference count
+
+ friend class LedgerManager;
+ friend class LedgerWriter;
+};
+
+class Ledger: public LedgerBase {
+public:
+ explicit Ledger(detail::LedgerSyncContext* ctx = nullptr);
+ ~Ledger();
+
+ int initReader(LedgerReader& reader);
+ int initWriter(LedgerWriter& writer, LedgerWriteSource src);
+
+ LedgerInfo info() const;
+
+ const char* name() const {
+ return name_; // Immutable
+ }
+
+ void setSyncCallback(ledger_sync_callback callback) {
+ std::lock_guard lock(*this);
+ syncCallback_ = callback;
+ }
+
+ void setAppData(void* data, ledger_destroy_app_data_callback destroy) {
+ std::lock_guard lock(*this);
+ appData_ = data;
+ destroyAppData_ = destroy;
+ }
+
+ void* appData() const {
+ std::lock_guard lock(*this);
+ return appData_;
+ }
+
+ void lock() const {
+ mutex_.lock();
+ }
+
+ void unlock() const {
+ 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, 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
+ int curReaderCount_; // Number of active readers of the current ledger data
+ int stagedReaderCount_; // Number of active readers of the staged ledger data
+ int stagedFileCount_; // Number of staged data files created
+
+ 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
+
+ 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 loadLedgerInfo(lfs_t* fs);
+ void setLedgerInfo(const LedgerInfo& info);
+
+ int initCurrentData(lfs_t* fs);
+ int flushStagedData(lfs_t* fs);
+ int removeTempData(lfs_t* fs);
+
+ friend class LedgerManager;
+ friend class LedgerReader;
+ friend class LedgerWriter;
+};
+
+class LedgerInfo {
+public:
+ LedgerInfo() = default;
+
+ LedgerInfo& scopeType(ledger_scope type) {
+ scopeType_ = type;
+ return *this;
+ }
+
+ ledger_scope scopeType() const {
+ return scopeType_.value_or(LEDGER_SCOPE_UNKNOWN);
+ }
+
+ bool isScopeTypeSet() const {
+ return scopeType_.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) {
+ syncDir_ = dir;
+ return *this;
+ }
+
+ ledger_sync_direction syncDirection() const {
+ return syncDir_.value_or(LEDGER_SYNC_DIRECTION_UNKNOWN);
+ }
+
+ bool isSyncDirectionSet() const {
+ return syncDir_.has_value();
+ }
+
+ LedgerInfo& dataSize(size_t size) {
+ dataSize_ = size;
+ return *this;
+ }
+
+ size_t dataSize() const {
+ return dataSize_.value_or(0);
+ }
+
+ bool isDataSizeSet() const {
+ return dataSize_.has_value();
+ }
+
+ LedgerInfo& lastUpdated(int64_t time) {
+ lastUpdated_ = time;
+ return *this;
+ }
+
+ int64_t lastUpdated() const {
+ return lastUpdated_.value_or(0);
+ }
+
+ bool isLastUpdatedSet() const {
+ return lastUpdated_.has_value();
+ }
+
+ LedgerInfo& lastSynced(int64_t time) {
+ lastSynced_ = time;
+ return *this;
+ }
+
+ int64_t lastSynced() const {
+ return lastSynced_.value_or(0);
+ }
+
+ 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;
+ }
+
+ bool syncPending() const {
+ return syncPending_.value_or(false);
+ }
+
+ bool isSyncPendingSet() const {
+ return syncPending_.has_value();
+ }
+
+ LedgerInfo& update(const LedgerInfo& info);
+
+private:
+ // 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 updateCount_;
+ std::optional scopeType_;
+ std::optional syncDir_;
+ std::optional syncPending_;
+};
+
+class LedgerStream {
+public:
+ virtual ~LedgerStream() = default;
+
+ virtual int read(char* data, size_t size) = 0;
+ virtual int write(const char* data, size_t size) = 0;
+ virtual int close(bool discard = false) = 0;
+};
+
+class LedgerReader: public LedgerStream {
+public:
+ LedgerReader() :
+ file_(),
+ dataOffs_(0),
+ staged_(false),
+ open_(false) {
+ }
+
+ // Reader instances are not copyable nor movable
+ LedgerReader(const LedgerReader&) = delete;
+
+ ~LedgerReader() {
+ close(true /* discard */);
+ }
+
+ int read(char* data, size_t size) override;
+
+ int write(const char* data, size_t size) override {
+ return SYSTEM_ERROR_INVALID_STATE;
+ }
+
+ int close(bool discard = false) override;
+
+ Ledger* ledger() const {
+ return ledger_.get();
+ }
+
+ const LedgerInfo& info() const {
+ return info_;
+ }
+
+ bool isOpen() const {
+ return open_;
+ }
+
+ LedgerReader& operator=(const LedgerReader&) = delete;
+
+protected:
+ int init(LedgerInfo info, int stagedSeqNum, RefCountPtr ledger); // Called by Ledger
+
+private:
+ RefCountPtr ledger_; // Ledger instance
+ LedgerInfo info_; // Ledger info
+ lfs_file_t file_; // File handle
+ 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
+
+ friend class Ledger;
+};
+
+class LedgerWriter: public LedgerStream {
+public:
+ LedgerWriter() :
+ file_(),
+ src_(),
+ dataSize_(0),
+ tempSeqNum_(0),
+ open_(false) {
+ }
+
+ // Writer instances are not copyable nor movable
+ LedgerWriter(const LedgerWriter&) = delete;
+
+ ~LedgerWriter() {
+ close(true /* discard */);
+ }
+
+ int read(char* data, size_t size) override {
+ return SYSTEM_ERROR_INVALID_STATE;
+ }
+
+ int write(const char* data, size_t size) override;
+ int close(bool discard = false) override;
+
+ void updateInfo(const LedgerInfo& info) {
+ info_.update(info);
+ }
+
+ Ledger* ledger() const {
+ return ledger_.get();
+ }
+
+ bool isOpen() const {
+ return open_;
+ }
+
+ LedgerWriter& operator=(const LedgerWriter&) = delete;
+
+protected:
+ int init(LedgerWriteSource src, int tempSeqNum, RefCountPtr ledger); // Called by Ledger
+
+private:
+ RefCountPtr ledger_; // Ledger instance
+ 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
+
+ friend class Ledger;
+};
+
+inline bool operator==(const LedgerScopeId& id1, const LedgerScopeId& id2) {
+ return id1.size == id2.size && std::memcmp(id1.data, id2.data, id1.size) == 0;
+}
+
+inline bool operator!=(const LedgerScopeId& id1, const LedgerScopeId& id2) {
+ return !(id1 == id2);
+}
+
+} // namespace particle::system
+
+#endif // HAL_PLATFORM_LEDGER
diff --git a/system/src/ledger/ledger_manager.cpp b/system/src/ledger/ledger_manager.cpp
new file mode 100644
index 0000000000..31ca871bc2
--- /dev/null
+++ b/system/src/ledger/ledger_manager.cpp
@@ -0,0 +1,1604 @@
+/*
+ * 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; // Time when the ledger should be synchronized (ticks)
+ uint64_t forcedSyncTime; // The latest time when the ledger should be synchronized (ticks)
+ uint64_t updateTime; // Time the ledger was last updated (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),
+ updateTime(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;
+ updateTime = 0;
+ updateCount = 0;
+ }
+
+ void resetCloudToDeviceState() {
+ lastUpdated = 0;
+ }
+};
+
+} // namespace detail
+
+LedgerManager::LedgerManager() :
+ timer_(timerCallback, this),
+ curCtx_(nullptr),
+ nextSyncTime_(0),
+ retryTime_(0),
+ retryDelay_(0),
+ bytesInBuf_(0),
+ state_(State::NEW),
+ pendingState_(0),
+ reqId_(COAP_INVALID_REQUEST_ID),
+ resubscribe_(false) {
+}
+
+LedgerManager::~LedgerManager() {
+ reset();
+ if (state_ != State::NEW) {
+ coap_remove_request_handler(REQUEST_URI, REQUEST_METHOD, nullptr);
+ coap_remove_connection_handler(connectionCallback, nullptr);
+ }
+}
+
+int LedgerManager::init() {
+ std::lock_guard lock(mutex_);
+ 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();
+ }
+ // Context objects are sorted by ledger name
+ bool found = false;
+ auto it = findContext(contexts, ctx->name, found);
+ assert(!found);
+ it = contexts.insert(it, std::move(ctx));
+ if (it == contexts.end()) {
+ 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);
+ runNext();
+ }
+ }
+ 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;
+}
+
+int LedgerManager::run() {
+ auto now = hal_timer_millis(nullptr);
+ if (state_ == State::FAILED) {
+ if (now >= retryTime_) {
+ LOG(INFO, "Retrying synchronization");
+ startSync();
+ return 1; // Have a task to run
+ }
+ runNext(retryTime_ - now);
+ return 0;
+ }
+ if (state_ != State::READY || (!pendingState_ && !resubscribe_)) {
+ // Some task is in progress, the manager is not in an appropriate state, or there's nothing to do
+ return 0;
+ }
+ if (pendingState_ & PendingState::GET_INFO) {
+ LOG(INFO, "Requesting ledger info");
+ CHECK(sendGetInfoRequest());
+ return 1;
+ }
+ if ((pendingState_ & PendingState::SUBSCRIBE) || resubscribe_) {
+ LOG(INFO, "Subscribing to ledger updates");
+ CHECK(sendSubscribeRequest());
+ return 1;
+ }
+ unsigned nextSyncDelay = 0;
+ if (pendingState_ & PendingState::SYNC_TO_CLOUD) {
+ 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 1;
+ }
+ // Timestamp in nextSyncTime_ was outdated
+ nextSyncTime_ = t;
+ }
+ nextSyncDelay = nextSyncTime_ - now;
+ }
+ 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 1;
+ }
+ }
+ if (nextSyncDelay > 0) {
+ runNext(nextSyncDelay);
+ }
+ 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(CoapMessagePtr& 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.get(), 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(CoapMessagePtr& msg, int /* reqId */) {
+ LOG(TRACE, "Received update notification");
+ pb_istream_t stream = {};
+ CHECK(getStreamForSubmessage(msg.get(), &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(CoapMessagePtr& /* 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(CoapMessagePtr& 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.get(), 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(CoapMessagePtr& /* 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(CoapMessagePtr& 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.get(), &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_ = std::move(msg);
+ // Read the first chunk of the ledger data
+ CHECK(receiveLedgerData());
+ return 0;
+}
+
+int LedgerManager::receiveSubscribeResponse(CoapMessagePtr& 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.get(), &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(CoapMessagePtr& 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.get(), &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.
+ // TODO: Reuse pendingState_ for storing a state not associated with a particular sync context
+ 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* apiMsg = nullptr;
+ int reqId = CHECK(coap_begin_request(&apiMsg, REQUEST_URI, REQUEST_METHOD, 0 /* timeout */, 0 /* flags */, nullptr /* reserved */));
+ CoapMessagePtr msg(apiMsg);
+ // 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.get(), 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_ = std::move(msg);
+ 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* apiMsg = nullptr;
+ int reqId = CHECK(coap_begin_request(&apiMsg, REQUEST_URI, REQUEST_METHOD, 0 /* timeout */, 0 /* flags */, nullptr /* reserved */));
+ CoapMessagePtr msg(apiMsg);
+ pb_ostream_t stream = {};
+ CHECK(pb_ostream_from_coap_message(&stream, msg.get(), nullptr));
+ if (!pb_encode(&stream, &PB_CLOUD(Request_msg), &pbReq)) {
+ return SYSTEM_ERROR_ENCODING_FAILED;
+ }
+ CHECK(coap_end_request(msg.get(), responseCallback, nullptr /* ack_cb */, requestErrorCallback, this, nullptr));
+ msg.release();
+ // 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* apiMsg = nullptr;
+ int reqId = CHECK(coap_begin_request(&apiMsg, REQUEST_URI, REQUEST_METHOD, 0 /* timeout */, 0 /* flags */, nullptr /* reserved */));
+ CoapMessagePtr msg(apiMsg);
+ pb_ostream_t stream = {};
+ CHECK(pb_ostream_from_coap_message(&stream, msg.get(), 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.get(), responseCallback, nullptr /* ack_cb */, requestErrorCallback, this, nullptr));
+ msg.release();
+ // 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* apiMsg = nullptr;
+ int reqId = CHECK(coap_begin_request(&apiMsg, REQUEST_URI, REQUEST_METHOD, 0 /* timeout */, 0 /* flags */, nullptr /* reserved */));
+ CoapMessagePtr msg(apiMsg);
+ pb_ostream_t stream = {};
+ CHECK(pb_ostream_from_coap_message(&stream, msg.get(), 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.get(), responseCallback, nullptr /* ack_cb */, requestErrorCallback, this, nullptr));
+ msg.release();
+ // 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_.get(), 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_.get(), responseCallback, nullptr /* ack_cb */, requestErrorCallback, this, nullptr));
+ msg_.release();
+ }
+ 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_.get(), 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);
+ RefCountPtr ledger(writer->ledger());
+ CHECK(stream_->close());
+ stream_.reset();
+ msg_.reset();
+ 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) {
+ int code = (result == 0) ? COAP_STATUS_CHANGED : COAP_STATUS_BAD_REQUEST;
+ coap_message* apiMsg = nullptr;
+ CHECK(coap_begin_response(&apiMsg, code, reqId, 0 /* flags */, nullptr /* reserved */));
+ CoapMessagePtr msg(apiMsg);
+ 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.get(), nullptr /* ack_cb */, requestErrorCallback, nullptr /* arg */, nullptr /* reserved */));
+ msg.release();
+ 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, bool changed) {
+ // TODO: Revisit the throttling algorithm
+ auto now = hal_timer_millis(nullptr);
+ bool throttle = ctx->updateTime && now - ctx->updateTime < MIN_SYNC_DELAY;
+ if (changed) {
+ ctx->updateTime = now;
+ }
+ if (throttle) {
+ if (!ctx->forcedSyncTime) {
+ ctx->forcedSyncTime = now + MAX_SYNC_DELAY;
+ }
+ ctx->syncTime = std::min(now + MIN_SYNC_DELAY, ctx->forcedSyncTime);
+ } else {
+ // Synchronize the first update in series right away
+ ctx->syncTime = now;
+ ctx->forcedSyncTime = now;
+ }
+ if (!nextSyncTime_ || ctx->syncTime < 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() {
+ timer_.stop();
+ if (reqId_ != COAP_INVALID_REQUEST_ID) {
+ coap_cancel_request(reqId_, nullptr);
+ reqId_ = COAP_INVALID_REQUEST_ID;
+ }
+ msg_.reset();
+ 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;
+ // retryDelay_ is reset when disconnecting from the cloud
+}
+
+void LedgerManager::runNext(unsigned delay) {
+ int r = timer_.start(delay);
+ if (r < 0) {
+ LOG(ERROR, "Failed to start timer: %d", r);
+ }
+}
+
+void LedgerManager::handleError(int error) {
+ assert(error < 0);
+ if (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 LedgerSyncContexts& contexts, const char* name, bool& found) {
+ 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, true /* changed */);
+ runNext();
+ }
+ }
+}
+
+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);
+ }
+ // Make sure to run any asynchronous tasks that might be pending whenever the manager is
+ // notified about some event
+ self->runNext();
+ return 0;
+}
+
+int LedgerManager::requestCallback(coap_message* apiMsg, const char* uri, int method, int reqId, void* arg) {
+ auto self = static_cast(arg);
+ std::lock_guard lock(self->mutex_);
+ clear_system_error_message();
+ CoapMessagePtr msg(apiMsg);
+ 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);
+ }
+ self->runNext();
+ return 0;
+}
+
+int LedgerManager::responseCallback(coap_message* apiMsg, int status, int reqId, void* arg) {
+ auto self = static_cast(arg);
+ std::lock_guard lock(self->mutex_);
+ CoapMessagePtr msg(apiMsg);
+ int r = self->receiveResponse(msg, status);
+ if (r < 0) {
+ LOG(ERROR, "Error while handling response: %d", r);
+ self->handleError(r);
+ }
+ self->runNext();
+ 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_.get() == 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);
+ }
+ self->runNext();
+ return 0;
+}
+
+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);
+ self->runNext();
+}
+
+void LedgerManager::timerCallback(void* arg) {
+ auto self = static_cast(arg);
+ std::lock_guard lock(self->mutex_);
+ int r = self->run();
+ if (r < 0) {
+ LOG(ERROR, "Failed to process ledger task: %d", r);
+ self->handleError(r);
+ }
+ if (r != 0) {
+ // Keep running until there's nothing to do
+ self->runNext();
+ }
+}
+
+LedgerManager* LedgerManager::instance() {
+ static LedgerManager mgr;
+ // XXX: Lazy initialization is used so that ledger instances can be requested in the global
+ // scope by the application. It's dangerous because the application's global constructors are
+ // called before the system is fully initialized but seems to work in this case
+ static volatile bool initCalled = false;
+ if (!initCalled) {
+ int r = mgr.init();
+ initCalled = true;
+ if (r < 0) {
+ LOG(ERROR, "Failed to initialize ledger manager: %d", r);
+ }
+ }
+ 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..3f8691df7d
--- /dev/null
+++ b/system/src/ledger/ledger_manager.h
@@ -0,0 +1,173 @@
+/*
+ * 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 "util/system_timer.h"
+
+#include "coap_api.h"
+#include "coap_util.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 LedgerWriter;
+class LedgerStream;
+
+class LedgerManager {
+public:
+ LedgerManager(const LedgerManager&) = delete;
+
+ ~LedgerManager();
+
+ int getLedger(RefCountPtr& ledger, const char* name, bool create = false);
+
+ int getLedgerNames(Vector& names);
+
+ int removeLedgerData(const char* name);
+ int removeAllData();
+
+ LedgerManager& operator=(const LedgerManager&) = delete;
+
+ static LedgerManager* instance();
+
+protected:
+ using LedgerSyncContext = detail::LedgerSyncContext;
+
+ void notifyLedgerChanged(LedgerSyncContext* ctx); // Called by LedgerWriter
+
+ 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
+ SystemTimer timer_; // Timer used for running asynchronous tasks
+ CoapMessagePtr msg_; // CoAP request or response that is being sent or received
+ LedgerSyncContext* curCtx_; // Context of the ledger being synchronized
+ 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 init();
+
+ int run();
+
+ int notifyConnected();
+ void notifyDisconnected(int error);
+
+ int receiveRequest(CoapMessagePtr& msg, int reqId);
+ int receiveNotifyUpdateRequest(CoapMessagePtr& msg, int reqId);
+ int receiveResetInfoRequest(CoapMessagePtr& msg, int reqId);
+
+ int receiveResponse(CoapMessagePtr& msg, int status);
+ int receiveSetDataResponse(CoapMessagePtr& msg, int result);
+ int receiveGetDataResponse(CoapMessagePtr& msg, int result);
+ int receiveSubscribeResponse(CoapMessagePtr& msg, int result);
+ int receiveGetInfoResponse(CoapMessagePtr& 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, bool changed = false);
+
+ void startSync();
+ void reset();
+
+ void runNext(unsigned delay = 0);
+
+ void handleError(int error);
+
+ LedgerSyncContexts::ConstIterator findContext(const char* name, bool& found) const {
+ return findContext(contexts_, name, found);
+ }
+
+ static LedgerSyncContexts::ConstIterator findContext(const LedgerSyncContexts& contexts, const char* name, bool& found);
+
+ 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);
+ static void timerCallback(void* arg);
+
+ friend class LedgerWriter;
+ 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/communication/src/coap_util.h b/system/src/ledger/ledger_util.h
similarity index 57%
rename from communication/src/coap_util.h
rename to system/src/ledger/ledger_util.h
index a9858d613a..7476b13af9 100644
--- a/communication/src/coap_util.h
+++ b/system/src/ledger/ledger_util.h
@@ -1,5 +1,5 @@
/*
- * Copyright (c) 2022 Particle Industries, Inc. All rights reserved.
+ * 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
@@ -17,19 +17,16 @@
#pragma once
-#include "logging.h"
+#include "hal_platform.h"
-namespace particle::protocol {
+#if HAL_PLATFORM_LEDGER
-/**
- * Log the contents of a CoAP message.
- *
- * @param level Logging level.
- * @param category Logging category.
- * @param data Message data.
- * @param size Message size.
- * @param logPayload Whether to log the payload data of the message.
- */
-void logCoapMessage(LogLevel level, const char* category, const char* data, size_t size, bool logPayload = false);
+#include
+
+namespace particle::system {
+
+int formatLedgerPath(char* buf, size_t size, const char* ledgerName, const char* fmt = nullptr, ...);
+
+} // namespace particle::system
-} // namespace particle::protocol
+#endif // HAL_PLATFORM_LEDGER
diff --git a/system/src/main.cpp b/system/src/main.cpp
index 0503d7a756..bb1f99c3d5 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,8 @@
#include "spark_wiring_wifi.h"
#include "server_config.h"
#include "system_network_manager.h"
+#include "ledger/ledger_manager.h"
+#include "ledger/ledger.h"
// FIXME
#include "system_control_internal.h"
@@ -668,7 +674,17 @@ int resetSettingsToFactoryDefaultsIfNeeded() {
CHECK(dct_write_app_data(devPubKey.get(), DCT_ALT_DEVICE_PUBLIC_KEY_OFFSET, DCT_ALT_DEVICE_PUBLIC_KEY_SIZE));
// Restore default server key and address
ServerConfig::instance()->restoreDefaultSettings();
- particle::system::NetworkManager::instance()->clearStoredConfiguration();
+ system::NetworkManager::instance()->clearStoredConfiguration();
+ // TODO: Discuss if we'd want to clear ledger data on a factory reset
+#if 0
+#if HAL_PLATFORM_LEDGER
+ // Can't use LedgerManager::removeAllData() because the manager is not initialized yet
+ CHECK(rmrf(system::LEDGER_ROOT_DIR));
+#endif // HAL_PLATFORM_LEDGER
+ // XXX: Application global constructors run before this function so an additional system reset
+ // is performed to prevent any inconsistencies that might be caused by the configuration cleanup
+ HAL_Core_System_Reset_Ex(RESET_REASON_CONFIG_UPDATE, 0 /* data */, nullptr /* reserved */);
+#endif // 0
#endif // !defined(SPARK_NO_PLATFORM) && HAL_PLATFORM_DCT
return 0;
}
@@ -784,6 +800,13 @@ void app_setup_and_loop(void)
}
Network_Setup(threaded); // todo - why does this come before system thread initialization?
+#if HAL_PLATFORM_LEDGER
+ if (system_mode() != SAFE_MODE) {
+ // Make sure the ledger manager is initialized
+ system::LedgerManager::instance();
+ }
+#endif // HAL_PLATFORM_LEDGER
+
#if PLATFORM_THREADING
if (threaded)
{
diff --git a/system/src/system_event.cpp b/system/src/system_event.cpp
index a58503987c..a84f5e1338 100644
--- a/system/src/system_event.cpp
+++ b/system/src/system_event.cpp
@@ -30,6 +30,7 @@
namespace {
using namespace particle;
+using namespace particle::system;
struct SystemEventSubscription {
@@ -115,7 +116,7 @@ class SystemEventTask : public ISRTaskQueue::Task {
*/
void notify() {
system_notify_event_async(event_, data_, pointer_, fn_, fndata_);
- system_pool_free(this, nullptr);
+ systemPoolDelete(this);
}
public:
@@ -197,9 +198,8 @@ void system_notify_event(system_event_t event, uint32_t data, void* pointer, voi
if (flags & NOTIFY_SYNCHRONOUSLY) {
system_notify_event_impl(event, data, pointer, fn, fndata);
} else if (hal_interrupt_is_isr()) {
- void* space = (system_pool_alloc(sizeof(SystemEventTask), nullptr));
- if (space) {
- auto task = new (space) SystemEventTask(event, data, pointer, fn, fndata);
+ auto task = systemPoolNew(event, data, pointer, fn, fndata);
+ if (task) {
SystemISRTaskQueue.enqueue(task);
};
} else {
diff --git a/system/src/system_ledger.cpp b/system/src/system_ledger.cpp
new file mode 100644
index 0000000000..80b77bb141
--- /dev/null
+++ b/system/src/system_ledger.cpp
@@ -0,0 +1,171 @@
+/*
+ * 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/ledger.h"
+#include "ledger/ledger_manager.h"
+#include "system_ledger.h"
+#include "system_threading.h"
+
+#include "check.h"
+
+using namespace particle;
+using namespace particle::system;
+
+int ledger_get_instance(ledger_instance** ledger, const char* name, void* reserved) {
+ RefCountPtr lr;
+ CHECK(LedgerManager::instance()->getLedger(lr, name, true /* create */));
+ *ledger = reinterpret_cast(lr.unwrap()); // Transfer ownership
+ return 0;
+}
+
+void ledger_add_ref(ledger_instance* ledger, void* reserved) {
+ auto lr = reinterpret_cast(ledger);
+ lr->addRef();
+}
+
+void ledger_release(ledger_instance* ledger, void* reserved) {
+ if (ledger) {
+ auto lr = reinterpret_cast(ledger);
+ lr->release();
+ }
+}
+
+void ledger_lock(ledger_instance* ledger, void* reserved) {
+ auto lr = reinterpret_cast(ledger);
+ lr->lock();
+}
+
+void ledger_unlock(ledger_instance* ledger, void* reserved) {
+ auto lr = reinterpret_cast(ledger);
+ lr->unlock();
+}
+
+void ledger_set_callbacks(ledger_instance* ledger, const ledger_callbacks* callbacks, void* reserved) {
+ auto lr = reinterpret_cast(ledger);
+ lr->setSyncCallback(callbacks ? callbacks->sync : nullptr);
+}
+
+void ledger_set_app_data(ledger_instance* ledger, void* appData, ledger_destroy_app_data_callback destroy,
+ void* reserved) {
+ auto lr = reinterpret_cast(ledger);
+ lr->setAppData(appData, destroy);
+}
+
+void* ledger_get_app_data(ledger_instance* ledger, void* reserved) {
+ auto lr = reinterpret_cast(ledger);
+ return lr->appData();
+}
+
+int ledger_get_info(ledger_instance* ledger, ledger_info* info, void* reserved) {
+ auto lr = reinterpret_cast(ledger);
+ auto srcInfo = lr->info();
+ info->name = lr->name();
+ info->last_updated = srcInfo.lastUpdated();
+ info->last_synced = srcInfo.lastSynced();
+ info->data_size = srcInfo.dataSize();
+ info->scope = srcInfo.scopeType();
+ info->sync_direction = srcInfo.syncDirection();
+ info->flags = 0;
+ if (srcInfo.syncPending()) {
+ info->flags |= LEDGER_INFO_SYNC_PENDING;
+ }
+ return 0;
+}
+
+int ledger_open(ledger_stream** stream, ledger_instance* ledger, int mode, void* reserved) {
+ auto lr = reinterpret_cast(ledger);
+ if (mode & LEDGER_STREAM_MODE_READ) {
+ // Bidirectional streams are not supported as of now
+ if (mode & LEDGER_STREAM_MODE_WRITE) {
+ return SYSTEM_ERROR_NOT_SUPPORTED;
+ }
+ std::unique_ptr r(new(std::nothrow) LedgerReader());
+ if (!r) {
+ return SYSTEM_ERROR_NO_MEMORY;
+ }
+ CHECK(lr->initReader(*r));
+ *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;
+ }
+ std::unique_ptr w(new(std::nothrow) LedgerWriter());
+ if (!w) {
+ return SYSTEM_ERROR_NO_MEMORY;
+ }
+ CHECK(lr->initWriter(*w, LedgerWriteSource::USER));
+ *stream = reinterpret_cast(w.release());
+ } else {
+ return SYSTEM_ERROR_INVALID_ARGUMENT;
+ }
+ return 0;
+}
+
+int ledger_close(ledger_stream* stream, int flags, void* reserved) {
+ if (!stream) {
+ return 0;
+ }
+ auto s = reinterpret_cast(stream);
+ int r = s->close(flags & LEDGER_STREAM_CLOSE_DISCARD);
+ delete s;
+ return r;
+}
+
+int ledger_read(ledger_stream* stream, char* data, size_t size, void* reserved) {
+ auto s = reinterpret_cast(stream);
+ size_t n = CHECK(s->read(data, size));
+ return n;
+}
+
+int ledger_write(ledger_stream* stream, const char* data, size_t size, void* reserved) {
+ auto s = reinterpret_cast(stream);
+ size_t n = CHECK(s->write(data, size));
+ 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;
+}
+
+int ledger_purge_all(void* reserved) {
+ CHECK(LedgerManager::instance()->removeAllData());
+ return 0;
+}
+
+#endif // HAL_PLATFORM_LEDGER
diff --git a/system/src/system_listening_mode.cpp b/system/src/system_listening_mode.cpp
index 0315b25d2d..7e8a7ddcce 100644
--- a/system/src/system_listening_mode.cpp
+++ b/system/src/system_listening_mode.cpp
@@ -172,7 +172,7 @@ int ListeningModeHandler::command(network_listen_command_t com, void* arg) {
}
int ListeningModeHandler::enqueueCommand(network_listen_command_t com, void* arg) {
- auto task = static_cast(system_pool_alloc(sizeof(Task), nullptr));
+ auto task = systemPoolNew();
if (!task) {
return SYSTEM_ERROR_NO_MEMORY;
}
@@ -198,7 +198,7 @@ void ListeningModeHandler::executeEnqueuedCommand(Task* task) {
auto com = task->command;
auto arg = task->arg;
- system_pool_free(task, nullptr);
+ systemPoolDelete(task);
instance()->command(com, arg);
}
diff --git a/system/src/system_network_manager.cpp b/system/src/system_network_manager.cpp
index 1aad086841..c787e0de74 100644
--- a/system/src/system_network_manager.cpp
+++ b/system/src/system_network_manager.cpp
@@ -1237,7 +1237,7 @@ int NetworkManager::loadStoredConfiguration(spark::Vector&
bool r = conf->append(storedConf);
return r;
};
- const int r = decodeMessageFromFile(&file, PB(NetworkConfig_fields), &pbConf);
+ const int r = decodeProtobufFromFile(&file, PB(NetworkConfig_fields), &pbConf);
if (r < 0) {
LOG(ERROR, "Unable to parse network settings");
LOG(WARN, "Removing file: %s", NETWORK_CONFIG_FILE);
@@ -1320,7 +1320,7 @@ int NetworkManager::saveStoredConfiguration(const spark::Vectoropen();
CloudDiagnostics::instance()->status(CloudDiagnostics::CONNECTED);
system_notify_event(cloud_status, cloud_status_connected);
if (system_mode() == SAFE_MODE) {
@@ -476,7 +479,7 @@ int system_isr_task_queue_free_memory(void *ptrToFree) {
void *arg;
};
- auto task = static_cast(system_pool_alloc(sizeof(FreeTask), nullptr));
+ auto task = systemPoolNew();
if (!task) {
return SYSTEM_ERROR_NO_MEMORY;
}
@@ -485,7 +488,7 @@ int system_isr_task_queue_free_memory(void *ptrToFree) {
task->func = [](ISRTaskQueue::Task * task) {
auto freeTask = reinterpret_cast(task);
free(freeTask->arg);
- system_pool_free(task, nullptr);
+ systemPoolDelete(freeTask);
};
SystemISRTaskQueue.enqueue(task);
diff --git a/system/src/usb_control_request_channel.cpp b/system/src/usb_control_request_channel.cpp
index 85b13b3bf4..a624fc1f00 100644
--- a/system/src/usb_control_request_channel.cpp
+++ b/system/src/usb_control_request_channel.cpp
@@ -40,6 +40,7 @@
namespace {
using namespace particle;
+using namespace particle::system;
// Minimum length of the data stage for high-speed USB devices
const size_t MIN_WLENGTH = 64;
@@ -298,7 +299,7 @@ bool particle::UsbControlRequestChannel::processInitRequest(HAL_USB_SetupRequest
return ServiceReply().status(ServiceReply::BUSY).encode(halReq); // Too many active requests
}
// Allocate a request object from the pool
- const auto req = (Request*)system_pool_alloc(sizeof(Request), nullptr);
+ const auto req = systemPoolNew();
if (!req) {
return ServiceReply().status(ServiceReply::NO_MEMORY).encode(halReq); // Memory allocation error
}
@@ -572,7 +573,7 @@ void particle::UsbControlRequestChannel::finishActiveRequest(Request* req) {
req->request_data = nullptr;
}
if (!req->request_data && !req->reply_data && !req->handler) {
- system_pool_free(req, nullptr);
+ systemPoolDelete(req);
} else {
// Free the request data asynchronously
req->task.func = finishRequest;
@@ -591,7 +592,7 @@ void particle::UsbControlRequestChannel::finishRequest(Request* req) {
if (req->handler) {
req->handler(req->result, req->handlerData);
}
- system_pool_free(req, nullptr);
+ systemPoolDelete(req);
}
// Note: This method is called from an ISR
@@ -619,7 +620,7 @@ void particle::UsbControlRequestChannel::allocRequestData(ISRTaskQueue::Task* is
}
if (req) { // Request has been cancelled
t_free(req->request_data);
- system_pool_free(req, nullptr);
+ systemPoolDelete(req);
}
}
diff --git a/system/src/util/system_timer.cpp b/system/src/util/system_timer.cpp
new file mode 100644
index 0000000000..7e2cc242fa
--- /dev/null
+++ b/system/src/util/system_timer.cpp
@@ -0,0 +1,93 @@
+/*
+ * Copyright (c) 2024 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
+
+#include "system_timer.h"
+
+#include "system_error.h"
+#include "logging.h"
+#include "debug.h"
+
+namespace particle::system {
+
+SystemTimer::~SystemTimer() {
+ stop();
+ if (timer_) {
+ os_timer_destroy(timer_, nullptr);
+ }
+}
+
+int SystemTimer::start(unsigned timeout) {
+ stop();
+ // Period of an RTOS timer must be greater than 0
+ if (timeout > 0) {
+ if (!timer_) {
+ int r = os_timer_create(&timer_, timeout, timerCallback, this /* timer_id */, true /* one_shot */, nullptr /* reserved */);
+ if (r != 0) {
+ return SYSTEM_ERROR_NO_MEMORY;
+ }
+ } else {
+ int r = os_timer_change(timer_, OS_TIMER_CHANGE_PERIOD, false /* fromISR */, timeout, 0xffffffffu /* block */, nullptr /* reserved */);
+ if (r != 0) {
+ return SYSTEM_ERROR_INTERNAL; // Should not happen
+ }
+ }
+ int r = os_timer_change(timer_, OS_TIMER_CHANGE_START, false /* fromISR */, 0 /* period */, 0xffffffffu /* block */, nullptr /* reserved */);
+ if (r != 0) {
+ return SYSTEM_ERROR_INTERNAL; // Should not happen
+ }
+ } else {
+ // Schedule a call in the system thread
+ SystemISRTaskQueue.enqueue(this);
+ }
+ return 0;
+}
+
+void SystemTimer::stop() {
+ if (timer_) {
+ int r = os_timer_change(timer_, OS_TIMER_CHANGE_STOP, false /* fromISR */, 0 /* period */, 0xffffffffu /* block */, nullptr /* reserved */);
+ if (r != 0) {
+ LOG_DEBUG(ERROR, "Failed to stop timer"); // Should not happen
+ }
+ }
+ SystemISRTaskQueue.remove(this);
+}
+
+void SystemTimer::taskCallback(ISRTaskQueue::Task* task) {
+ auto self = static_cast(task);
+ assert(self && self->callback_);
+ self->callback_(self->arg_);
+}
+
+void SystemTimer::timerCallback(os_timer_t timer) {
+ void* id = nullptr;
+ int r = os_timer_get_id(timer, &id);
+ if (r != 0) {
+ LOG_DEBUG(ERROR, "Failed to get timer ID"); // Should not happen
+ return;
+ }
+ auto self = static_cast(id);
+ assert(self);
+ SystemISRTaskQueue.enqueue(self);
+}
+
+} // namespace particle::system
diff --git a/system/src/util/system_timer.h b/system/src/util/system_timer.h
new file mode 100644
index 0000000000..45e065c964
--- /dev/null
+++ b/system/src/util/system_timer.h
@@ -0,0 +1,73 @@
+/*
+ * Copyright (c) 2024 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 "system_threading.h"
+
+#include "concurrent_hal.h"
+
+namespace particle::system {
+
+/**
+ * A one-shot timer executed in the system thread.
+ */
+class SystemTimer: private ISRTaskQueue::Task {
+public:
+ typedef void (*Callback)(void* arg);
+
+ /**
+ * Construct a timer.
+ *
+ * @param callback Callback to invoke when the timer expires.
+ * @param arg Argument to pass to the callback.
+ */
+ explicit SystemTimer(Callback callback, void* arg = nullptr) :
+ Task(taskCallback),
+ timer_(),
+ callback_(callback),
+ arg_(arg) {
+ }
+
+ /**
+ * Destruct the timer.
+ */
+ ~SystemTimer();
+
+ /**
+ * Start the timer.
+ *
+ * @param timeout Timeout in milliseconds.
+ * @return 0 on success, otherwise an error code defined by the `system_error_t` enum.
+ */
+ int start(unsigned timeout);
+
+ /**
+ * Stop the timer.
+ */
+ void stop();
+
+private:
+ os_timer_t timer_;
+ Callback callback_;
+ void* arg_;
+
+ static void taskCallback(ISRTaskQueue::Task* task);
+ static void timerCallback(os_timer_t timer);
+};
+
+} // namespace particle::system
diff --git a/test/unit_tests/cellular/CMakeLists.txt b/test/unit_tests/cellular/CMakeLists.txt
index f91495b77e..26af446db2 100644
--- a/test/unit_tests/cellular/CMakeLists.txt
+++ b/test/unit_tests/cellular/CMakeLists.txt
@@ -14,8 +14,13 @@ include_directories(
add_executable( ${target_name}
${DEVICE_OS_DIR}/wiring/src/spark_wiring_cellular_printable.cpp
${DEVICE_OS_DIR}/wiring/src/spark_wiring_print.cpp
+ ${DEVICE_OS_DIR}/wiring/src/spark_wiring_variant.cpp
+ ${DEVICE_OS_DIR}/wiring/src/spark_wiring_json.cpp
+ ${DEVICE_OS_DIR}/wiring/src/spark_wiring_string.cpp
+ ${DEVICE_OS_DIR}/wiring/src/string_convert.cpp
${DEVICE_OS_DIR}/hal/network/ncp/cellular/network_config_db.cpp
${DEVICE_OS_DIR}/hal/shared/cellular_sig_perc_mapping.cpp
+ ${DEVICE_OS_DIR}/services/src/jsmn.c
cellular.cpp
)
diff --git a/test/unit_tests/communication/CMakeLists.txt b/test/unit_tests/communication/CMakeLists.txt
index 58d8f95628..c07130d44b 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/stub/filesystem.h b/test/unit_tests/stub/filesystem.h
index cb9876ab7e..703673ab5f 100644
--- a/test/unit_tests/stub/filesystem.h
+++ b/test/unit_tests/stub/filesystem.h
@@ -118,7 +118,7 @@ class FsLock {
}
void lock() {
- filesystem_unlock(fs_);
+ filesystem_lock(fs_);
}
void unlock() {
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 05f85b41fb..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
@@ -17,11 +18,13 @@ add_executable( ${target_name}
${DEVICE_OS_DIR}/wiring/src/spark_wiring_fuel.cpp
${DEVICE_OS_DIR}/wiring/src/spark_wiring_i2c.cpp
${DEVICE_OS_DIR}/wiring/src/spark_wiring_ipaddress.cpp
+ ${DEVICE_OS_DIR}/wiring/src/spark_wiring_variant.cpp
${DEVICE_OS_DIR}/wiring_globals/src/wiring_globals_i2c.cpp
${DEVICE_OS_DIR}/hal/src/template/i2c_hal.cpp
${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
@@ -39,6 +42,8 @@ add_executable( ${target_name}
json.cpp
std_functions.cpp
wlan.cpp
+ map.cpp
+ variant.cpp
)
# Set defines specific to target
diff --git a/test/unit_tests/wiring/json.cpp b/test/unit_tests/wiring/json.cpp
index 127822d54b..2330f4d3af 100644
--- a/test/unit_tests/wiring/json.cpp
+++ b/test/unit_tests/wiring/json.cpp
@@ -595,7 +595,18 @@ TEST_CASE("Writing JSON") {
json.value(DBL_MAX);
check(data).equals("1.79769e+308"); // Based on DBL_DIG significant digits for '%g' format specifier
}
-
+ SECTION("NaN") {
+ json.value(NAN);
+ check(data).equals("0");
+ }
+ SECTION("positive infinity") {
+ json.value(INFINITY);
+ check(data).equals("1.79769e+308"); // DBL_MAX formatted with default precision
+ }
+ SECTION("negative infinity") {
+ json.value(-INFINITY);
+ check(data).equals("-1.79769e+308"); // -DBL_MAX formatted with default precision
+ }
// Precision related tests
@@ -987,6 +998,8 @@ TEST_CASE("Writing JSON") {
check(data).equals("{\"a\\tb\\n\":\"a\\tb\\n\"}");
}
}
+
+ CHECK(json.bytesWritten() == data.size());
}
TEST_CASE("JSONValue") {
diff --git a/test/unit_tests/wiring/map.cpp b/test/unit_tests/wiring/map.cpp
new file mode 100644
index 0000000000..f4ccf96063
--- /dev/null
+++ b/test/unit_tests/wiring/map.cpp
@@ -0,0 +1,75 @@
+#include
+#include
+
+#include "spark_wiring_map.h"
+
+#include "util/catch.h"
+
+using namespace particle;
+
+namespace {
+
+template
+void checkMap(const MapT& map, const std::vector& expectedEntries) {
+ auto& entries = map.entries();
+ REQUIRE(entries.size() == expectedEntries.size());
+ for (int i = 0; i < entries.size(); ++i) {
+ CHECK(entries[i] == expectedEntries[i]);
+ CHECK(map.has(entries[i].first));
+ }
+ CHECK(map.size() == entries.size());
+ CHECK(map.capacity() >= map.size());
+ CHECK(((map.isEmpty() && map.size() == 0) || (!map.isEmpty() && map.size() > 0)));
+}
+
+} // namespace
+
+TEST_CASE("Map") {
+ SECTION("Map()") {
+ Map m;
+ checkMap(m, {});
+ }
+
+ SECTION("set()") {
+ Map m;
+ m.set("b", 2);
+ checkMap(m, { { "b", 2 } });
+ m.set("c", 3);
+ checkMap(m, { { "b", 2 }, { "c", 3 } });
+ m.set("a", 1);
+ checkMap(m, { { "a", 1 }, { "b", 2 }, { "c", 3 } });
+ }
+
+ SECTION("get()") {
+ Map m({ { "a", 1 }, { "b", 2 }, { "c", 3 } });
+ CHECK(m.get("a") == 1);
+ CHECK(m.get("b") == 2);
+ CHECK(m.get("c") == 3);
+ CHECK(m.get("d", 4) == 4);
+ }
+
+ SECTION("remove()") {
+ Map m({ { "a", 1 }, { "b", 2 }, { "c", 3 } });
+ CHECK(m.remove("b"));
+ checkMap(m, { { "a", 1 }, { "c", 3 } });
+ CHECK(m.remove("c"));
+ checkMap(m, { { "a", 1 } });
+ CHECK(!m.remove("d"));
+ checkMap(m, { { "a", 1 } });
+ CHECK(m.remove("a"));
+ checkMap(m, {});
+ }
+
+ SECTION("operator[]") {
+ Map m;
+ m["b"] = 2;
+ checkMap(m, { { "b", 2 } });
+ m["c"] = 3;
+ checkMap(m, { { "b", 2 }, { "c", 3 } });
+ m["a"] = 1;
+ checkMap(m, { { "a", 1 }, { "b", 2 }, { "c", 3 } });
+ CHECK(m["a"] == 1);
+ CHECK(m["b"] == 2);
+ CHECK(m["c"] == 3);
+ }
+}
diff --git a/test/unit_tests/wiring/variant.cpp b/test/unit_tests/wiring/variant.cpp
new file mode 100644
index 0000000000..05ac4fc264
--- /dev/null
+++ b/test/unit_tests/wiring/variant.cpp
@@ -0,0 +1,474 @@
+#include
+#include
+
+#include "spark_wiring_variant.h"
+
+#include "util/stream.h"
+#include "util/string.h"
+#include "util/catch.h"
+
+using namespace particle;
+
+namespace {
+
+template
+void checkVariant(const Variant& v, const T& expectedValue = T()) {
+ REQUIRE(v.is());
+ CHECK(v.value() == expectedValue);
+ CHECK(v.to() == expectedValue);
+ bool ok = false;
+ CHECK(v.to(ok) == expectedValue);
+ CHECK(ok);
+ CHECK(v == expectedValue);
+ CHECK(expectedValue == v);
+ CHECK_FALSE(v != expectedValue);
+ CHECK_FALSE(expectedValue != v);
+ if constexpr (std::is_same_v) {
+ CHECK(v.type() == Variant::NULL_);
+ CHECK((v.isNull() && !v.isBool() && !v.isInt() && !v.isUInt() && !v.isInt64() && !v.isUInt64() && !v.isDouble() && !v.isNumber() && !v.isString() && !v.isArray() && !v.isMap()));
+ } else if constexpr (std::is_same_v) {
+ CHECK(v.type() == Variant::BOOL);
+ CHECK((!v.isNull() && v.isBool() && !v.isInt() && !v.isUInt() && !v.isInt64() && !v.isUInt64() && !v.isDouble() && !v.isNumber() && !v.isString() && !v.isArray() && !v.isMap()));
+ CHECK(v.toBool() == expectedValue);
+ bool ok = false;
+ CHECK(v.toBool(ok) == expectedValue);
+ CHECK(ok);
+ } else if constexpr (std::is_same_v) {
+ CHECK(v.type() == Variant::INT);
+ CHECK((!v.isNull() && !v.isBool() && v.isInt() && !v.isUInt() && !v.isInt64() && !v.isUInt64() && !v.isDouble() && v.isNumber() && !v.isString() && !v.isArray() && !v.isMap()));
+ CHECK(v.toInt() == expectedValue);
+ bool ok = false;
+ CHECK(v.toInt(ok) == expectedValue);
+ CHECK(ok);
+ } else if constexpr (std::is_same_v) {
+ CHECK(v.type() == Variant::UINT);
+ CHECK((!v.isNull() && !v.isBool() && !v.isInt() && v.isUInt() && !v.isInt64() && !v.isUInt64() && !v.isDouble() && v.isNumber() && !v.isString() && !v.isArray() && !v.isMap()));
+ CHECK(v.toUInt() == expectedValue);
+ bool ok = false;
+ CHECK(v.toUInt(ok) == expectedValue);
+ CHECK(ok);
+ } else if constexpr (std::is_same_v) {
+ CHECK(v.type() == Variant::INT64);
+ CHECK((!v.isNull() && !v.isBool() && !v.isInt() && !v.isUInt() && v.isInt64() && !v.isUInt64() && !v.isDouble() && v.isNumber() && !v.isString() && !v.isArray() && !v.isMap()));
+ CHECK(v.toInt64() == expectedValue);
+ bool ok = false;
+ CHECK(v.toInt64(ok) == expectedValue);
+ CHECK(ok);
+ } else if constexpr (std::is_same_v) {
+ CHECK(v.type() == Variant::UINT64);
+ CHECK((!v.isNull() && !v.isBool() && !v.isInt() && !v.isUInt() && !v.isInt64() && v.isUInt64() && !v.isDouble() && v.isNumber() && !v.isString() && !v.isArray() && !v.isMap()));
+ CHECK(v.toUInt64() == expectedValue);
+ bool ok = false;
+ CHECK(v.toUInt64(ok) == expectedValue);
+ CHECK(ok);
+ } else if constexpr (std::is_same_v) {
+ CHECK(v.type() == Variant::DOUBLE);
+ CHECK((!v.isNull() && !v.isBool() && !v.isInt() && !v.isUInt() && !v.isInt64() && !v.isUInt64() && v.isDouble() && v.isNumber() && !v.isString() && !v.isArray() && !v.isMap()));
+ CHECK(v.toDouble() == expectedValue);
+ bool ok = false;
+ CHECK(v.toDouble(ok) == expectedValue);
+ CHECK(ok);
+ } else if constexpr (std::is_same_v) {
+ CHECK(v.type() == Variant::STRING);
+ CHECK((!v.isNull() && !v.isBool() && !v.isInt() && !v.isUInt() && !v.isInt64() && !v.isUInt64() && !v.isDouble() && !v.isNumber() && v.isString() && !v.isArray() && !v.isMap()));
+ CHECK(v.toString() == expectedValue);
+ bool ok = false;
+ CHECK(v.toString(ok) == expectedValue);
+ CHECK(ok);
+ } else if constexpr (std::is_same_v) {
+ CHECK(v.type() == Variant::ARRAY);
+ CHECK((!v.isNull() && !v.isBool() && !v.isInt() && !v.isUInt() && !v.isInt64() && !v.isUInt64() && !v.isDouble() && !v.isNumber() && !v.isString() && v.isArray() && !v.isMap()));
+ CHECK(v.toArray() == expectedValue);
+ bool ok = false;
+ CHECK(v.toArray(ok) == expectedValue);
+ CHECK(ok);
+ } else if constexpr (std::is_same_v) {
+ CHECK(v.type() == Variant::MAP);
+ CHECK((!v.isNull() && !v.isBool() && !v.isInt() && !v.isUInt() && !v.isInt64() && !v.isUInt64() && !v.isDouble() && !v.isNumber() && !v.isString() && !v.isArray() && v.isMap()));
+ CHECK(v.toMap() == expectedValue);
+ bool ok = false;
+ CHECK(v.toMap(ok) == expectedValue);
+ CHECK(ok);
+ } else {
+ FAIL("Unexpected type");
+ }
+}
+
+template
+void checkVariant(Variant& v, const T& expectedValue = T()) {
+ // Test const methods
+ checkVariant(static_cast(v), expectedValue);
+ // Test non-const methods
+ CHECK(v.as() == expectedValue);
+ if constexpr (std::is_same_v) {
+ CHECK(v.asBool() == expectedValue);
+ } else if constexpr (std::is_same_v) {
+ CHECK(v.asInt() == expectedValue);
+ } else if constexpr (std::is_same_v) {
+ CHECK(v.asUInt() == expectedValue);
+ } else if constexpr (std::is_same_v) {
+ CHECK(v.asInt64() == expectedValue);
+ } else if constexpr (std::is_same_v) {
+ CHECK(v.asUInt64() == expectedValue);
+ } else if constexpr (std::is_same_v) {
+ CHECK(v.asDouble() == expectedValue);
+ } else if constexpr (std::is_same_v) {
+ CHECK(v.asString() == expectedValue);
+ } else if constexpr (std::is_same_v) {
+ CHECK(v.asArray() == expectedValue);
+ } else if constexpr (std::is_same_v) {
+ CHECK(v.asMap() == expectedValue);
+ } else if constexpr (!std::is_same_v) {
+ FAIL("Unexpected type");
+ }
+}
+
+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") {
+ SECTION("can be constructed from a value of one of the supported types") {
+ {
+ Variant v;
+ checkVariant(v);
+ }
+ {
+ Variant v(std::monostate{});
+ checkVariant(v);
+ }
+ {
+ Variant v(true);
+ checkVariant