From 5257d8556f79158dddbadd823d1fb73685e96acb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?K=C3=A9vin=20L=27h=C3=B4pital?= Date: Wed, 17 Dec 2025 09:15:56 -0500 Subject: [PATCH 01/64] Initial empty repository From 692e3a2b7a9d63a2a7f720d12e0ac7ab716b3cea Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?K=C3=A9vin=20L=27h=C3=B4pital?= Date: Thu, 18 Dec 2025 09:40:46 +0100 Subject: [PATCH 02/64] api-mockup: add Apache 2.0 license MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit SFL: #19168 Change-Id: I2b471b8acbbf148dc25f952f93a3c0d2ac7fa3d5 Signed-off-by: Kévin L'hôpital --- .../api/api-mockup/LICENSE | 201 ++++++++++++++++++ 1 file changed, 201 insertions(+) create mode 100644 example-implementations/api/api-mockup/LICENSE diff --git a/example-implementations/api/api-mockup/LICENSE b/example-implementations/api/api-mockup/LICENSE new file mode 100644 index 0000000..261eeb9 --- /dev/null +++ b/example-implementations/api/api-mockup/LICENSE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. From e4633512f1d131c488b707d1aeef041118de9f80 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?K=C3=A9vin=20L=27h=C3=B4pital?= Date: Thu, 18 Dec 2025 10:34:45 +0100 Subject: [PATCH 03/64] api-mockup: add MQTT communication using Mosquitto library MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Import Mosquitto-based MQTT communication developed for the GEISA conformance testing tool into the api-mockup project. This integration enables MQTT messaging capabilities within the api-mockup framework. SFL: #19168 Change-Id: Ia3764f14806cf5f0ed56256ea04eefb5d3268646 Signed-off-by: Kévin L'hôpital --- .../api/api-mockup/gapi_mosquitto.c | 138 ++++++++++++++++++ .../api/api-mockup/gapi_mosquitto.h | 62 ++++++++ 2 files changed, 200 insertions(+) create mode 100644 example-implementations/api/api-mockup/gapi_mosquitto.c create mode 100644 example-implementations/api/api-mockup/gapi_mosquitto.h diff --git a/example-implementations/api/api-mockup/gapi_mosquitto.c b/example-implementations/api/api-mockup/gapi_mosquitto.c new file mode 100644 index 0000000..0baf669 --- /dev/null +++ b/example-implementations/api/api-mockup/gapi_mosquitto.c @@ -0,0 +1,138 @@ +/** + * @file gapi_mosquitto.c + * @brief Definition file for GEISA MQTT communication using the Mosquitto + * library + * @copyright Copyright (C) 2025 Southern California Edison + */ + +#include "gapi_mosquitto.h" + +const int MOSQUITTO_KEEP_ALIVE = 60; + +static int sent_mid; + +static void handle_signal(int signal) +{ + fprintf(stderr, "Caught signal %d, disconnecting...\n", signal); + running = false; +} + +static void on_connect(struct mosquitto *mosq, void *obj, int return_code) +{ + (void)obj; + if (return_code == 0) { + fprintf(stdout, "[connected] OK\n"); + isConnected = true; + } else { + fprintf(stderr, "[connect] failed, return_code=%d\n", + return_code); + running = false; + mosquitto_disconnect(mosq); + } +} + +static void on_disconnect(struct mosquitto *mosq, void *obj, int return_code) +{ + (void)obj; + (void)mosq; + fprintf(stdout, "[disconnected] return_code=%d\n", return_code); + isConnected = false; +} + +static void on_publish(struct mosquitto *mosq, void *obj, int mid) +{ + (void)obj; + (void)mosq; + if (mid == sent_mid) { + running = false; + } else { + fprintf(stderr, "[publish] mid=%d (not expected %d)\n", mid, + sent_mid); + exit(1); + } +} + +static void on_message(struct mosquitto *mosq, void *obj, + const struct mosquitto_message *msg) +{ + (void)obj; + (void)mosq; + fprintf(stdout, "[msg] topic=%s payload=%.*s\n", msg->topic, + msg->payloadlen, (char *)msg->payload); +} + +struct mosquitto *api_communication_init(const char *broker, int port) +{ + struct mosquitto *mosq = NULL; + int return_code = 0; + + mosquitto_lib_init(); + + mosq = mosquitto_new(NULL, true, NULL); + if (!mosq) { + fprintf(stderr, "Error: mosquitto_new() failed\n"); + goto cleanup; + } + + mosquitto_connect_callback_set(mosq, on_connect); + mosquitto_disconnect_callback_set(mosq, on_disconnect); + mosquitto_publish_callback_set(mosq, on_publish); + mosquitto_message_callback_set(mosq, on_message); + + signal(SIGINT, handle_signal); + signal(SIGTERM, handle_signal); + + mosquitto_loop(mosq, -1, 1); + + return_code = + mosquitto_connect(mosq, broker, port, MOSQUITTO_KEEP_ALIVE); + if (return_code != MOSQ_ERR_SUCCESS) { + fprintf(stderr, "Error: could not connect to %s: %s\n", broker, + mosquitto_strerror(return_code)); + goto destroy; + } + + return mosq; + +destroy: + mosquitto_destroy(mosq); +cleanup: + mosquitto_lib_cleanup(); + return NULL; +} + +void api_communication_deinit(struct mosquitto *mosq) +{ + mosquitto_disconnect(mosq); + mosquitto_destroy(mosq); + mosquitto_lib_cleanup(); +} + +int api_subscribe(struct mosquitto *mosq, const char *topic) +{ + int return_code = 0; + + return_code = mosquitto_subscribe(mosq, NULL, topic, 0); + if (return_code != MOSQ_ERR_SUCCESS) { + fprintf(stderr, "Subscribe failed: %s\n", + mosquitto_strerror(return_code)); + return return_code; + } + + return 0; +} + +int api_publish(struct mosquitto *mosq, const char *topic, const char *message) +{ + int return_code = 0; + + return_code = mosquitto_publish( + mosq, &sent_mid, topic, (int)strlen(message), message, 1, false); + if (return_code != MOSQ_ERR_SUCCESS) { + fprintf(stderr, "Publish failed: %s\n", + mosquitto_strerror(return_code)); + return return_code; + } + + return 0; +} diff --git a/example-implementations/api/api-mockup/gapi_mosquitto.h b/example-implementations/api/api-mockup/gapi_mosquitto.h new file mode 100644 index 0000000..4a19d9e --- /dev/null +++ b/example-implementations/api/api-mockup/gapi_mosquitto.h @@ -0,0 +1,62 @@ +/** + * @file gapi_mosquitto.h + * @brief Header file for GEISA MQTT communication using the Mosquitto library + * @copyright Copyright (C) 2025 Southern California Edison + */ + +#ifndef GAPI_MOSQUITTO_H +#define GAPI_MOSQUITTO_H + +#include +#include +#include +#include +#include +#include + +/** + * @brief Global variable to control the running state of the MQTT client + */ +extern volatile bool running; + +/** + * @brief Global variable to indicate the connection state of the MQTT client + */ +extern volatile bool isConnected; + +/** + * @brief Initialize MQTT communication with the specified broker and port + * + * @param broker The MQTT broker address + * @param port The MQTT broker port + * @return A pointer to the initialized mosquitto struct + */ +struct mosquitto *api_communication_init(const char *broker, int port); + +/** + * @brief Deinitialize MQTT communication and clean up resources + * + * @param mosq Pointer to the mosquitto struct to be cleaned up + */ +void api_communication_deinit(struct mosquitto *mosq); + +/** + * @brief Subscribe to a specified MQTT topic + * + * @param mosq Pointer to the mosquitto struct + * @param topic The MQTT topic to subscribe to + * @return 0 on success, non-zero on failure + */ +int api_subscribe(struct mosquitto *mosq, const char *topic); + +/** + * @brief Publish a message to a specified MQTT topic + * + * @param mosq Pointer to the mosquitto struct + * @param topic The MQTT topic to publish to + * @param message The message to publish + * @return 0 on success, non-zero on failure + */ +int api_publish(struct mosquitto *mosq, const char *topic, const char *message); + +#endif // GAPI_MOSQUITTO_H From 0e417d0902ffe15c381d541ae62c627fd433e99c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?K=C3=A9vin=20L=27h=C3=B4pital?= Date: Thu, 18 Dec 2025 15:00:39 +0100 Subject: [PATCH 04/64] api-mockup: add gapi main file MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This main file is for now only connecting to the MQTT broker, waiting for connection and disconnect. SFL: #19168 Change-Id: Ia50f5bddbc1dc26ae2040bdfd92cefccc87791cf Signed-off-by: Kévin L'hôpital --- example-implementations/api/api-mockup/gapi.c | 30 +++++++++++++++++++ 1 file changed, 30 insertions(+) create mode 100644 example-implementations/api/api-mockup/gapi.c diff --git a/example-implementations/api/api-mockup/gapi.c b/example-implementations/api/api-mockup/gapi.c new file mode 100644 index 0000000..87fb1f4 --- /dev/null +++ b/example-implementations/api/api-mockup/gapi.c @@ -0,0 +1,30 @@ +/** + * @file gapi.c + * @brief GEISA API mockup main + * @copyright Copyright (C) 2025 Southern California Edison + */ +#include "gapi_mosquitto.h" + +volatile bool running = true; +volatile bool isConnected = false; +#define PORT 1883 + +int main() +{ + const char *broker = "localhost"; + struct mosquitto *mosq = NULL; + int port = PORT; + + mosq = api_communication_init(broker, port); + if (!mosq) { + return EXIT_FAILURE; + } + + while (running && !isConnected) { + mosquitto_loop(mosq, -1, 1); + sleep(1); + } + + api_communication_deinit(mosq); + return EXIT_SUCCESS; +} From eba41ad4921d1e3bf86d68e7f61770ae50b9345a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?K=C3=A9vin=20L=27h=C3=B4pital?= Date: Thu, 18 Dec 2025 15:03:44 +0100 Subject: [PATCH 05/64] api-mockup: add Makefile for building gapi with mosquitto MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This commit introduces a Makefile to facilitate the building and installation of the gapi program using the mosquitto library. SFL: #19168 Change-Id: Ia9c241abbeda9affa9af51807f8b21f95eae7e44 Signed-off-by: Kévin L'hôpital --- example-implementations/api/api-mockup/Makefile | 13 +++++++++++++ 1 file changed, 13 insertions(+) create mode 100644 example-implementations/api/api-mockup/Makefile diff --git a/example-implementations/api/api-mockup/Makefile b/example-implementations/api/api-mockup/Makefile new file mode 100644 index 0000000..1c96d5d --- /dev/null +++ b/example-implementations/api/api-mockup/Makefile @@ -0,0 +1,13 @@ +# Copyright (C) 2025 Southern California Edison + +all: gapi + +DESTDIR ?= /usr/bin +CFLAGS := -O2 -Wall -std=c11 -Wextra -pedantic -Werror +LDLIBS := -lmosquitto + +gapi: %: %.c gapi_mosquitto.o + +install: gapi + install -D -m 755 gapi $(DESTDIR) + From aef7a23faa4b42c10f0f92a7ca1e4c64a2b22136 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?K=C3=A9vin=20L=27h=C3=B4pital?= Date: Thu, 18 Dec 2025 15:04:47 +0100 Subject: [PATCH 06/64] api-mockup: add clang-format and clang-tidy checks MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add .clang-format and .clang-tidy configuration files to the project, and update the Makefile to include a 'lint' target that runs these tools for code style and static analysis checks. SFL: #19168 Change-Id: I2206903bd71c7d3d864b0773ea08c1aa0fa1eded Signed-off-by: Kévin L'hôpital --- example-implementations/api/api-mockup/.clang-format | 6 ++++++ example-implementations/api/api-mockup/.clang-tidy | 2 ++ example-implementations/api/api-mockup/Makefile | 3 +++ 3 files changed, 11 insertions(+) create mode 100644 example-implementations/api/api-mockup/.clang-format create mode 100644 example-implementations/api/api-mockup/.clang-tidy diff --git a/example-implementations/api/api-mockup/.clang-format b/example-implementations/api/api-mockup/.clang-format new file mode 100644 index 0000000..12c37cc --- /dev/null +++ b/example-implementations/api/api-mockup/.clang-format @@ -0,0 +1,6 @@ +BasedOnStyle: llvm +IndentWidth: 8 +UseTab: Always +BreakBeforeBraces: Linux +AllowShortIfStatementsOnASingleLine: false +IndentCaseLabels: false diff --git a/example-implementations/api/api-mockup/.clang-tidy b/example-implementations/api/api-mockup/.clang-tidy new file mode 100644 index 0000000..c448aea --- /dev/null +++ b/example-implementations/api/api-mockup/.clang-tidy @@ -0,0 +1,2 @@ +Checks: 'readability-*,clang-analyzer-*,cppcoreguidelines-*,modernize-*,-cppcoreguidelines-avoid-non-const-global-variables,-clang-analyzer-security.insecureAPI.DeprecatedOrUnsafeBufferHandling' +WarningsAsErrors: '*' diff --git a/example-implementations/api/api-mockup/Makefile b/example-implementations/api/api-mockup/Makefile index 1c96d5d..52ff94a 100644 --- a/example-implementations/api/api-mockup/Makefile +++ b/example-implementations/api/api-mockup/Makefile @@ -11,3 +11,6 @@ gapi: %: %.c gapi_mosquitto.o install: gapi install -D -m 755 gapi $(DESTDIR) +lint: + clang-format --Werror --dry-run *.c *.h + clang-tidy *.c *.h From 80f27c7e41f599fa92b1c522701ef9978ee1ede4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?K=C3=A9vin=20L=27h=C3=B4pital?= Date: Thu, 18 Dec 2025 15:05:46 +0100 Subject: [PATCH 07/64] api-mockup: add dockerize environment for building and linting MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add a CQFD configuration to build and lint the project inside a Docker container. SFL: #19168 Change-Id: I4ddad6ded6e89c43e16aec9ba69d56bf315ee81c Signed-off-by: Kévin L'hôpital --- .../api/api-mockup/.cqfd/docker/Dockerfile | 14 ++++++++++++++ example-implementations/api/api-mockup/.cqfdrc | 9 +++++++++ 2 files changed, 23 insertions(+) create mode 100644 example-implementations/api/api-mockup/.cqfd/docker/Dockerfile create mode 100644 example-implementations/api/api-mockup/.cqfdrc diff --git a/example-implementations/api/api-mockup/.cqfd/docker/Dockerfile b/example-implementations/api/api-mockup/.cqfd/docker/Dockerfile new file mode 100644 index 0000000..c77b933 --- /dev/null +++ b/example-implementations/api/api-mockup/.cqfd/docker/Dockerfile @@ -0,0 +1,14 @@ +# Copyright (C) 2025 Southern California Edison. + +FROM ubuntu:24.04 + +ENV DEBIAN_FRONTEND=noninteractive + +RUN set -x \ + && apt-get update \ + && apt-get install -y --no-install-recommends \ + build-essential \ + clang-format \ + clang-tidy \ + libmosquitto-dev \ + && rm -rf /var/lib/apt/lists/ diff --git a/example-implementations/api/api-mockup/.cqfdrc b/example-implementations/api/api-mockup/.cqfdrc new file mode 100644 index 0000000..068f1b8 --- /dev/null +++ b/example-implementations/api/api-mockup/.cqfdrc @@ -0,0 +1,9 @@ +[project] +org='geisa' +name='api-mockup' + +[build] +command='make' + +[lint] +command='make lint' From efe5870ad8dab1d45dd5f44639beb82a7e20aca5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?K=C3=A9vin=20L=27h=C3=B4pital?= Date: Thu, 18 Dec 2025 15:16:40 +0100 Subject: [PATCH 08/64] api-mockup: add README MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit SFL: #19168 Change-Id: I2b87fbb0d7f58457e9357f22d0d603e31af52437 Signed-off-by: Kévin L'hôpital --- .../api/api-mockup/README.md | 57 +++++++++++++++++++ 1 file changed, 57 insertions(+) create mode 100644 example-implementations/api/api-mockup/README.md diff --git a/example-implementations/api/api-mockup/README.md b/example-implementations/api/api-mockup/README.md new file mode 100644 index 0000000..c168812 --- /dev/null +++ b/example-implementations/api/api-mockup/README.md @@ -0,0 +1,57 @@ +[![License](https://img.shields.io/badge/License-Apache%202.0-blue.svg)](https://opensource.org/licenses/Apache-2.0) + +# GEISA API Mockup - a GEISA Application Programming Interface mockup implementation + +GEISA API Mockup is designed to be able to run conformance tests. It aims to provide a mockup implementation of the GEISA Application Programming Interface as defined in the +[GEISA Specification](https://github.com/geisa/specification). + +The GEISA Specification is an effort by the Grid Edge Interoperability and Security Alliance to define a consistent, secure, and interoperable computing environment for embedded devices at the very edge of the electric utility grid, like electric meters and distribution automation devices, for the benefit of utilities, platform vendors, and software vendors. If you would like to get involved, please head over to our Wiki page for details on participation (https://lfenergy.org/projects/geisa/). Follow the onboarding link for details about participating in our community process. + +## Building the application + +### Requirements + +To build the GEISA API Mockup application, you need to have the following tools installed on your system: + - make + - libmosquitto-dev + +A docker support is also available to launch the compilation, it requires: + - cqfd (See [requirements](https://github.com/savoirfairelinux/cqfd?tab=readme-ov-file#requirements) and [installation](https://github.com/savoirfairelinux/cqfd?tab=readme-ov-file#installingremoving-cqfd) steps on github) + +### Build using make +To build the GEISA API Mockup application using make, run the following command in the root directory of the project: + +```bash +make +``` + +### Build using cqfd +To build the GEISA API Mockup application using cqfd, run the following command in the root directory of the project: + +```bash +cqfd run +``` + +## Launch the application + +To launch the GEISA API Mockup application, simply run the following command: + +```bash +./gapi +``` + +## Linting and formatting + +Linting is done using `clang-tidy` and formatting using `clang-format`. +To lint and format the GEISA API Mockup application code, run the following +command: + +```bash +make lint +``` + +or with cqfd: + +```bash +cqfd -b lint +``` From 7f4885568433379f1641f9ac5e417e825e295f84 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?K=C3=A9vin=20L=27h=C3=B4pital?= Date: Fri, 19 Dec 2025 17:10:55 +0100 Subject: [PATCH 09/64] gapi_mosquitto: set MQTT username and password MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit As Mosquitto broker now requires authentication, set the username and password when initializing the API MQTT client. SFL: #19168 Change-Id: I4134060c4bb06eb3bda928c206324e7ff0b42ae6 Signed-off-by: Kévin L'hôpital --- example-implementations/api/api-mockup/gapi_mosquitto.c | 1 + 1 file changed, 1 insertion(+) diff --git a/example-implementations/api/api-mockup/gapi_mosquitto.c b/example-implementations/api/api-mockup/gapi_mosquitto.c index 0baf669..36340a0 100644 --- a/example-implementations/api/api-mockup/gapi_mosquitto.c +++ b/example-implementations/api/api-mockup/gapi_mosquitto.c @@ -74,6 +74,7 @@ struct mosquitto *api_communication_init(const char *broker, int port) goto cleanup; } + mosquitto_username_pw_set(mosq, "API", "api"); mosquitto_connect_callback_set(mosq, on_connect); mosquitto_disconnect_callback_set(mosq, on_disconnect); mosquitto_publish_callback_set(mosq, on_publish); From f83b837beae679ba7d5e9abcd315a95e90fc2ef5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?K=C3=A9vin=20L=27h=C3=B4pital?= Date: Fri, 19 Dec 2025 17:12:23 +0100 Subject: [PATCH 10/64] gapi_mosquitto: add QoS parameter to subscribe and publish functions MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Unlike MQTT's application messages, GEISA specification requires different QoS levels for topics. This commit updates the gapi_mosquitto API to allow specifying the QoS level when subscribing or publishing messages. SFL: #19168 Change-Id: I8d9670e346ee558e007e95f34cb21ce0b0f15ae9 Signed-off-by: Kévin L'hôpital --- example-implementations/api/api-mockup/gapi_mosquitto.c | 9 +++++---- example-implementations/api/api-mockup/gapi_mosquitto.h | 7 +++++-- 2 files changed, 10 insertions(+), 6 deletions(-) diff --git a/example-implementations/api/api-mockup/gapi_mosquitto.c b/example-implementations/api/api-mockup/gapi_mosquitto.c index 36340a0..5a11255 100644 --- a/example-implementations/api/api-mockup/gapi_mosquitto.c +++ b/example-implementations/api/api-mockup/gapi_mosquitto.c @@ -109,11 +109,11 @@ void api_communication_deinit(struct mosquitto *mosq) mosquitto_lib_cleanup(); } -int api_subscribe(struct mosquitto *mosq, const char *topic) +int api_subscribe(struct mosquitto *mosq, const char *topic, int qos) { int return_code = 0; - return_code = mosquitto_subscribe(mosq, NULL, topic, 0); + return_code = mosquitto_subscribe(mosq, NULL, topic, qos); if (return_code != MOSQ_ERR_SUCCESS) { fprintf(stderr, "Subscribe failed: %s\n", mosquitto_strerror(return_code)); @@ -123,12 +123,13 @@ int api_subscribe(struct mosquitto *mosq, const char *topic) return 0; } -int api_publish(struct mosquitto *mosq, const char *topic, const char *message) +int api_publish(struct mosquitto *mosq, const char *topic, const char *message, + int qos) { int return_code = 0; return_code = mosquitto_publish( - mosq, &sent_mid, topic, (int)strlen(message), message, 1, false); + mosq, &sent_mid, topic, (int)strlen(message), message, qos, false); if (return_code != MOSQ_ERR_SUCCESS) { fprintf(stderr, "Publish failed: %s\n", mosquitto_strerror(return_code)); diff --git a/example-implementations/api/api-mockup/gapi_mosquitto.h b/example-implementations/api/api-mockup/gapi_mosquitto.h index 4a19d9e..9e05b56 100644 --- a/example-implementations/api/api-mockup/gapi_mosquitto.h +++ b/example-implementations/api/api-mockup/gapi_mosquitto.h @@ -45,9 +45,10 @@ void api_communication_deinit(struct mosquitto *mosq); * * @param mosq Pointer to the mosquitto struct * @param topic The MQTT topic to subscribe to + * @param qos The Quality of Service level for the subscription * @return 0 on success, non-zero on failure */ -int api_subscribe(struct mosquitto *mosq, const char *topic); +int api_subscribe(struct mosquitto *mosq, const char *topic, int qos); /** * @brief Publish a message to a specified MQTT topic @@ -55,8 +56,10 @@ int api_subscribe(struct mosquitto *mosq, const char *topic); * @param mosq Pointer to the mosquitto struct * @param topic The MQTT topic to publish to * @param message The message to publish + * @param qos The Quality of Service level for the publication * @return 0 on success, non-zero on failure */ -int api_publish(struct mosquitto *mosq, const char *topic, const char *message); +int api_publish(struct mosquitto *mosq, const char *topic, const char *message, + int qos); #endif // GAPI_MOSQUITTO_H From d24295cf29f4cef22c78bd545039e6557ae00dee Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?K=C3=A9vin=20L=27h=C3=B4pital?= Date: Tue, 23 Dec 2025 09:41:52 +0100 Subject: [PATCH 11/64] api-mockup: add schemas submodule MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add the schemas submodule to the repository to manage JSON schemas separately. This allows for easier updates and maintenance of the schemas used in the GEISA API Mockup project. SFL: #21154 Change-Id: I601456523813de503c4415b8a78fbc3ffc1a0e58 Signed-off-by: Kévin L'hôpital --- example-implementations/api/api-mockup/.gitmodules | 3 +++ example-implementations/api/api-mockup/README.md | 8 ++++++++ example-implementations/api/api-mockup/schemas | 1 + 3 files changed, 12 insertions(+) create mode 100644 example-implementations/api/api-mockup/.gitmodules create mode 160000 example-implementations/api/api-mockup/schemas diff --git a/example-implementations/api/api-mockup/.gitmodules b/example-implementations/api/api-mockup/.gitmodules new file mode 100644 index 0000000..3384226 --- /dev/null +++ b/example-implementations/api/api-mockup/.gitmodules @@ -0,0 +1,3 @@ +[submodule "schemas"] + path = schemas + url = git@github.com:geisa/schemas.git diff --git a/example-implementations/api/api-mockup/README.md b/example-implementations/api/api-mockup/README.md index c168812..f8e45ac 100644 --- a/example-implementations/api/api-mockup/README.md +++ b/example-implementations/api/api-mockup/README.md @@ -7,6 +7,14 @@ GEISA API Mockup is designed to be able to run conformance tests. It aims to pro The GEISA Specification is an effort by the Grid Edge Interoperability and Security Alliance to define a consistent, secure, and interoperable computing environment for embedded devices at the very edge of the electric utility grid, like electric meters and distribution automation devices, for the benefit of utilities, platform vendors, and software vendors. If you would like to get involved, please head over to our Wiki page for details on participation (https://lfenergy.org/projects/geisa/). Follow the onboarding link for details about participating in our community process. +## Getting started + +To get started with the GEISA API Mockup application, you need to clone the repository and initialize the submodules. + +```bash +git submodule update --init --recursive +``` + ## Building the application ### Requirements diff --git a/example-implementations/api/api-mockup/schemas b/example-implementations/api/api-mockup/schemas new file mode 160000 index 0000000..3653c10 --- /dev/null +++ b/example-implementations/api/api-mockup/schemas @@ -0,0 +1 @@ +Subproject commit 3653c107c3f0cb89ad3c83bea1bde5aea7b0ffaf From 908f3bb8808cbc1fbaae4dc446b5ff4686409cf4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?K=C3=A9vin=20L=27h=C3=B4pital?= Date: Tue, 23 Dec 2025 10:25:53 +0100 Subject: [PATCH 12/64] Makefile: add compilation of protobuf schemas MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Compile the subproject that builds the protobuf schemas as a dependency for the main gapi target. SFL: #21154 Change-Id: Ide232f34a1d98344c171db0236df75c2fbd2b261 Signed-off-by: Kévin L'hôpital --- .../api/api-mockup/.cqfd/docker/Dockerfile | 1 + example-implementations/api/api-mockup/.gitignore | 2 ++ example-implementations/api/api-mockup/Makefile | 11 ++++++++++- example-implementations/api/api-mockup/README.md | 1 + 4 files changed, 14 insertions(+), 1 deletion(-) create mode 100644 example-implementations/api/api-mockup/.gitignore diff --git a/example-implementations/api/api-mockup/.cqfd/docker/Dockerfile b/example-implementations/api/api-mockup/.cqfd/docker/Dockerfile index c77b933..779bf00 100644 --- a/example-implementations/api/api-mockup/.cqfd/docker/Dockerfile +++ b/example-implementations/api/api-mockup/.cqfd/docker/Dockerfile @@ -11,4 +11,5 @@ RUN set -x \ clang-format \ clang-tidy \ libmosquitto-dev \ + protobuf-compiler \ && rm -rf /var/lib/apt/lists/ diff --git a/example-implementations/api/api-mockup/.gitignore b/example-implementations/api/api-mockup/.gitignore new file mode 100644 index 0000000..fc2fb3c --- /dev/null +++ b/example-implementations/api/api-mockup/.gitignore @@ -0,0 +1,2 @@ +gapi +*.o diff --git a/example-implementations/api/api-mockup/Makefile b/example-implementations/api/api-mockup/Makefile index 52ff94a..24e6ff7 100644 --- a/example-implementations/api/api-mockup/Makefile +++ b/example-implementations/api/api-mockup/Makefile @@ -2,11 +2,16 @@ all: gapi +.PHONY: schemas clean + DESTDIR ?= /usr/bin CFLAGS := -O2 -Wall -std=c11 -Wextra -pedantic -Werror LDLIBS := -lmosquitto -gapi: %: %.c gapi_mosquitto.o +gapi: %: %.c gapi_mosquitto.o | schemas + +schemas: + $(MAKE) -C schemas install: gapi install -D -m 755 gapi $(DESTDIR) @@ -14,3 +19,7 @@ install: gapi lint: clang-format --Werror --dry-run *.c *.h clang-tidy *.c *.h + +clean: + $(MAKE) -C schemas clean + rm -f gapi gapi_mosquitto.o diff --git a/example-implementations/api/api-mockup/README.md b/example-implementations/api/api-mockup/README.md index f8e45ac..65ef5f2 100644 --- a/example-implementations/api/api-mockup/README.md +++ b/example-implementations/api/api-mockup/README.md @@ -21,6 +21,7 @@ git submodule update --init --recursive To build the GEISA API Mockup application, you need to have the following tools installed on your system: - make + - protobuf-compiler - libmosquitto-dev A docker support is also available to launch the compilation, it requires: From 2b4fff2d43f2c568b9f8f2b3914d62d6e1ebef96 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?K=C3=A9vin=20L=27h=C3=B4pital?= Date: Tue, 23 Dec 2025 11:28:53 +0100 Subject: [PATCH 13/64] gapi-mockup: add build directory and move source files MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit To ease the build process and include/src files organization, this commit introduces a build directory and moves the gapi_mosquitto.c and gapi_mosquitto.h files into src/ and include/ directories respectively. The Makefile is updated accordingly to reflect these changes. SFL: #21154 Change-Id: Ifae57fa55a8594633ca188dc0ae4933cee7e1fd0 Signed-off-by: Kévin L'hôpital --- .../api/api-mockup/.gitignore | 3 +- .../api/api-mockup/Makefile | 29 ++++++++++++------- .../api-mockup/{ => include}/gapi_mosquitto.h | 0 .../api/api-mockup/{ => src}/gapi_mosquitto.c | 0 4 files changed, 20 insertions(+), 12 deletions(-) rename example-implementations/api/api-mockup/{ => include}/gapi_mosquitto.h (100%) rename example-implementations/api/api-mockup/{ => src}/gapi_mosquitto.c (100%) diff --git a/example-implementations/api/api-mockup/.gitignore b/example-implementations/api/api-mockup/.gitignore index fc2fb3c..378eac2 100644 --- a/example-implementations/api/api-mockup/.gitignore +++ b/example-implementations/api/api-mockup/.gitignore @@ -1,2 +1 @@ -gapi -*.o +build diff --git a/example-implementations/api/api-mockup/Makefile b/example-implementations/api/api-mockup/Makefile index 24e6ff7..f5e2b28 100644 --- a/example-implementations/api/api-mockup/Makefile +++ b/example-implementations/api/api-mockup/Makefile @@ -1,25 +1,34 @@ # Copyright (C) 2025 Southern California Edison -all: gapi - -.PHONY: schemas clean - +BUILDIR ?= build DESTDIR ?= /usr/bin -CFLAGS := -O2 -Wall -std=c11 -Wextra -pedantic -Werror +SRC := $(wildcard src/*.c) +OBJ = $(patsubst src/%.c,$(BUILDIR)/%.o,$(SRC)) +CFLAGS := -O2 -Wall -std=c11 -Wextra -pedantic -Werror -Iinclude LDLIBS := -lmosquitto -gapi: %: %.c gapi_mosquitto.o | schemas +all: $(BUILDIR)/gapi +.PHONY: schemas build_dir clean + +build_dir: + @mkdir -p $(BUILDIR) + +$(BUILDIR)/%.o: src/%.c | build_dir + $(CC) $(CFLAGS) -c $< -o $@ + +$(BUILDIR)/gapi: $(OBJ) | schemas + $(CC) $(CFLAGS) -o $@ gapi.c $(OBJ) $(LDLIBS) schemas: $(MAKE) -C schemas install: gapi - install -D -m 755 gapi $(DESTDIR) + install -D -m 755 $(BUILDIR)/gapi $(DESTDIR) lint: - clang-format --Werror --dry-run *.c *.h - clang-tidy *.c *.h + clang-format --Werror --dry-run *.c src/*.c include/*.h + clang-tidy *.c src/*.c include/*.h -- -Iinclude clean: $(MAKE) -C schemas clean - rm -f gapi gapi_mosquitto.o + rm -rf $(BUILDIR) diff --git a/example-implementations/api/api-mockup/gapi_mosquitto.h b/example-implementations/api/api-mockup/include/gapi_mosquitto.h similarity index 100% rename from example-implementations/api/api-mockup/gapi_mosquitto.h rename to example-implementations/api/api-mockup/include/gapi_mosquitto.h diff --git a/example-implementations/api/api-mockup/gapi_mosquitto.c b/example-implementations/api/api-mockup/src/gapi_mosquitto.c similarity index 100% rename from example-implementations/api/api-mockup/gapi_mosquitto.c rename to example-implementations/api/api-mockup/src/gapi_mosquitto.c From 339798829dad36fb80c14e6bf6b5744a5b062461 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?K=C3=A9vin=20L=27h=C3=B4pital?= Date: Tue, 23 Dec 2025 15:12:46 +0100 Subject: [PATCH 14/64] gapi_mosquitto: add topic handler registration function MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Handlers will be used to process incoming messages for specific topics. It will decrease the complexity of the on_message callback by delegating the processing to dedicated functions in different files. Then add a function to register a handler for a specific topic. SFL: #21154 Change-Id: I9c50794882fafdc6fa041d67a5ab89f382ed754b Signed-off-by: Kévin L'hôpital --- .../api/api-mockup/include/gapi_mosquitto.h | 18 ++++++++ .../api/api-mockup/src/gapi_mosquitto.c | 43 ++++++++++++++++++- 2 files changed, 59 insertions(+), 2 deletions(-) diff --git a/example-implementations/api/api-mockup/include/gapi_mosquitto.h b/example-implementations/api/api-mockup/include/gapi_mosquitto.h index 9e05b56..623cff3 100644 --- a/example-implementations/api/api-mockup/include/gapi_mosquitto.h +++ b/example-implementations/api/api-mockup/include/gapi_mosquitto.h @@ -62,4 +62,22 @@ int api_subscribe(struct mosquitto *mosq, const char *topic, int qos); int api_publish(struct mosquitto *mosq, const char *topic, const char *message, int qos); +/** + * @brief Type definition for a MQTT topic handler function + * + * @param topic The MQTT topic + * @param payloadlen The length of the payload + * @param payload The payload data + */ +typedef void (*topic_handler_t)(const char *topic, const int payloadlen, + const char *payload); + +/** + * @brief Register a handler function for a specific MQTT topic + * + * @param topic The MQTT topic + * @param handler The handler function to be registered + */ +void api_register_handler(const char *topic, topic_handler_t handler); + #endif // GAPI_MOSQUITTO_H diff --git a/example-implementations/api/api-mockup/src/gapi_mosquitto.c b/example-implementations/api/api-mockup/src/gapi_mosquitto.c index 5a11255..67b933c 100644 --- a/example-implementations/api/api-mockup/src/gapi_mosquitto.c +++ b/example-implementations/api/api-mockup/src/gapi_mosquitto.c @@ -8,8 +8,17 @@ #include "gapi_mosquitto.h" const int MOSQUITTO_KEEP_ALIVE = 60; +enum { MAX_TOPIC_HANDLERS = 3 }; static int sent_mid; +static int topic_handler_count = 0; + +typedef struct { + char *topic; + topic_handler_t topic_handler; +} topic_handler_entry_t; + +static topic_handler_entry_t topic_handlers[MAX_TOPIC_HANDLERS]; static void handle_signal(int signal) { @@ -57,8 +66,21 @@ static void on_message(struct mosquitto *mosq, void *obj, { (void)obj; (void)mosq; - fprintf(stdout, "[msg] topic=%s payload=%.*s\n", msg->topic, - msg->payloadlen, (char *)msg->payload); + for (int i = 0; i < topic_handler_count; i++) { + if (strstr(msg->topic, topic_handlers[i].topic) != NULL) { + topic_handlers[i].topic_handler( + msg->topic, msg->payloadlen, (char *)msg->payload); + return; + } + } +} + +static void free_topic_handlers() +{ + for (int i = 0; i < topic_handler_count; i++) { + free(topic_handlers[i].topic); + } + topic_handler_count = 0; } struct mosquitto *api_communication_init(const char *broker, int port) @@ -105,6 +127,7 @@ struct mosquitto *api_communication_init(const char *broker, int port) void api_communication_deinit(struct mosquitto *mosq) { mosquitto_disconnect(mosq); + free_topic_handlers(); mosquitto_destroy(mosq); mosquitto_lib_cleanup(); } @@ -138,3 +161,19 @@ int api_publish(struct mosquitto *mosq, const char *topic, const char *message, return 0; } + +void api_register_handler(const char *topic, topic_handler_t handler) +{ + if (topic_handler_count < MAX_TOPIC_HANDLERS) { + topic_handlers[topic_handler_count].topic = + malloc(strlen(topic) + 1); + + // NOLINTNEXTLINE: strcpy is safe as enough memory allocate here + strcpy(topic_handlers[topic_handler_count].topic, topic); + + topic_handlers[topic_handler_count].topic_handler = handler; + topic_handler_count++; + } else { + fprintf(stderr, "Error: Maximum topic handlers reached\n"); + } +} From 5862f7de1119ee02c14273ed0eb015c187ffd8b8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?K=C3=A9vin=20L=27h=C3=B4pital?= Date: Tue, 23 Dec 2025 15:27:12 +0100 Subject: [PATCH 15/64] gapi: add platform discovery message handlers MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Create a new module to handle API platform discovery messages, including app manifest requests. This module registers the necessary handlers and subscribes to the relevant topics during initialization. SFL: #21154 Change-Id: I2d1e9e6a6a6f05a10ed12f17344dec5981494714 Signed-off-by: Kévin L'hôpital --- example-implementations/api/api-mockup/gapi.c | 8 +++++ .../api/api-mockup/include/gapi_discovery.h | 19 ++++++++++ .../api/api-mockup/src/gapi_discovery.c | 35 +++++++++++++++++++ 3 files changed, 62 insertions(+) create mode 100644 example-implementations/api/api-mockup/include/gapi_discovery.h create mode 100644 example-implementations/api/api-mockup/src/gapi_discovery.c diff --git a/example-implementations/api/api-mockup/gapi.c b/example-implementations/api/api-mockup/gapi.c index 87fb1f4..565e7b8 100644 --- a/example-implementations/api/api-mockup/gapi.c +++ b/example-implementations/api/api-mockup/gapi.c @@ -3,6 +3,7 @@ * @brief GEISA API mockup main * @copyright Copyright (C) 2025 Southern California Edison */ +#include "gapi_discovery.h" #include "gapi_mosquitto.h" volatile bool running = true; @@ -25,6 +26,13 @@ int main() sleep(1); } + api_discovery_init(mosq); + + while (running) { + mosquitto_loop(mosq, -1, 1); + sleep(1); + } + api_communication_deinit(mosq); return EXIT_SUCCESS; } diff --git a/example-implementations/api/api-mockup/include/gapi_discovery.h b/example-implementations/api/api-mockup/include/gapi_discovery.h new file mode 100644 index 0000000..b4ccce6 --- /dev/null +++ b/example-implementations/api/api-mockup/include/gapi_discovery.h @@ -0,0 +1,19 @@ +/** + * @file gapi_discovery.h + * @brief Header file for API platform discovery messages + * @copyright Copyright (C) 2025 Southern California Edison + */ + +#ifndef GAPI_DISCOVERY_H +#define GAPI_DISCOVERY_H + +#include "gapi_mosquitto.h" + +/** + * @brief Initialize API platform discovery messages + * + * @param mosq Pointer to the mosquitto instance + */ +void api_discovery_init(struct mosquitto *mosq); + +#endif // GAPI_DISCOVERY_H diff --git a/example-implementations/api/api-mockup/src/gapi_discovery.c b/example-implementations/api/api-mockup/src/gapi_discovery.c new file mode 100644 index 0000000..208dbac --- /dev/null +++ b/example-implementations/api/api-mockup/src/gapi_discovery.c @@ -0,0 +1,35 @@ +/** + * @file gapi_discovery.c + * @brief Definition file for API platform discovery messages + * @copyright Copyright (C) 2025 Southern California Edison + */ + +#include "gapi_discovery.h" + +static void api_platform_discovery_req_handler(const char *topic, + const int payloadlen, + const char *payload) +{ + fprintf(stdout, "Received platform discovery request\n"); + fprintf(stdout, "[msg] topic=%s payload=%.*s\n", topic, payloadlen, + payload); +} + +static void api_manifest_req_handler(const char *topic, const int payloadlen, + const char *payload) +{ + fprintf(stdout, "Received app manifest request\n"); + fprintf(stdout, "[msg] topic=%s payload=%.*s\n", topic, payloadlen, + payload); +} + +void api_discovery_init(struct mosquitto *mosq) +{ + api_register_handler("geisa/api/app-manifest-req", + api_manifest_req_handler); + api_register_handler("geisa/api/platform-discovery-req", + api_platform_discovery_req_handler); + + api_subscribe(mosq, "geisa/api/platform-discovery-req", 1); + api_subscribe(mosq, "geisa/api/app-manifest-req/#", 1); +} From 194698e76be174209da78c5acc440e998fafe68f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?K=C3=A9vin=20L=27h=C3=B4pital?= Date: Wed, 24 Dec 2025 14:36:34 +0100 Subject: [PATCH 16/64] Makefile: modify compilation of schemas MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit By default, executing the Makefile of schemas directory generates the binpb files only. Since we need the C files, modify the Makefile to generate c and h files from schemas/*.proto files using protoc directly in the main Makefile. Add these files to the compilation process by modifying the OBJ variable and adding the required flags and libraries using pkg-config. The generated C and H files are placed in build/schemas directory as they are the result of a compilation and are not intended to be modified by the user. SFL: #21154 Change-Id: I71574514f1e587b3d89aa79c5f6aa5d5e8f84419 Signed-off-by: Kévin L'hôpital --- .../api/api-mockup/.cqfd/docker/Dockerfile | 3 +++ .../api/api-mockup/Makefile | 22 ++++++++++++------- .../api/api-mockup/README.md | 3 +++ 3 files changed, 20 insertions(+), 8 deletions(-) diff --git a/example-implementations/api/api-mockup/.cqfd/docker/Dockerfile b/example-implementations/api/api-mockup/.cqfd/docker/Dockerfile index 779bf00..f8b6ef2 100644 --- a/example-implementations/api/api-mockup/.cqfd/docker/Dockerfile +++ b/example-implementations/api/api-mockup/.cqfd/docker/Dockerfile @@ -11,5 +11,8 @@ RUN set -x \ clang-format \ clang-tidy \ libmosquitto-dev \ + libprotobuf-c-dev \ + pkg-config \ protobuf-compiler \ + protobuf-c-compiler \ && rm -rf /var/lib/apt/lists/ diff --git a/example-implementations/api/api-mockup/Makefile b/example-implementations/api/api-mockup/Makefile index f5e2b28..10da237 100644 --- a/example-implementations/api/api-mockup/Makefile +++ b/example-implementations/api/api-mockup/Makefile @@ -2,10 +2,14 @@ BUILDIR ?= build DESTDIR ?= /usr/bin +PROTOS := $(wildcard schemas/*.proto) +PB_C := $(PROTOS:schemas/%.proto=$(BUILDIR)/schemas/%.pb-c.c) + SRC := $(wildcard src/*.c) -OBJ = $(patsubst src/%.c,$(BUILDIR)/%.o,$(SRC)) -CFLAGS := -O2 -Wall -std=c11 -Wextra -pedantic -Werror -Iinclude -LDLIBS := -lmosquitto + +OBJ := $(patsubst src/%.c,$(BUILDIR)/%.o,$(SRC)) $(PB_C:.c=.o) +CFLAGS := -O2 -Wall -std=c11 -Wextra -pedantic -Werror -Iinclude -I$(BUILDIR) $(shell pkg-config --cflags 'libprotobuf-c >= 1.0.0') +LDLIBS := -lmosquitto $(shell pkg-config --libs 'libprotobuf-c >= 1.0.0') all: $(BUILDIR)/gapi .PHONY: schemas build_dir clean @@ -13,22 +17,24 @@ all: $(BUILDIR)/gapi build_dir: @mkdir -p $(BUILDIR) -$(BUILDIR)/%.o: src/%.c | build_dir +$(BUILDIR)/schemas/%.pb-c.o: %.c | build_dir schemas + $(CC) $(CFLAGS) -c $< -o $@ + +$(BUILDIR)/%.o: src/%.c | build_dir schemas $(CC) $(CFLAGS) -c $< -o $@ -$(BUILDIR)/gapi: $(OBJ) | schemas +$(BUILDIR)/gapi: $(OBJ) $(CC) $(CFLAGS) -o $@ gapi.c $(OBJ) $(LDLIBS) schemas: - $(MAKE) -C schemas + protoc --c_out=build schemas/*.proto install: gapi install -D -m 755 $(BUILDIR)/gapi $(DESTDIR) lint: clang-format --Werror --dry-run *.c src/*.c include/*.h - clang-tidy *.c src/*.c include/*.h -- -Iinclude + clang-tidy *.c src/*.c include/*.h -- -Iinclude -I$(BUILDIR) clean: - $(MAKE) -C schemas clean rm -rf $(BUILDIR) diff --git a/example-implementations/api/api-mockup/README.md b/example-implementations/api/api-mockup/README.md index 65ef5f2..1fc275d 100644 --- a/example-implementations/api/api-mockup/README.md +++ b/example-implementations/api/api-mockup/README.md @@ -21,8 +21,11 @@ git submodule update --init --recursive To build the GEISA API Mockup application, you need to have the following tools installed on your system: - make + - pkg-config - protobuf-compiler + - protobuf-c-compiler - libmosquitto-dev + - libprotobuf-c-dev A docker support is also available to launch the compilation, it requires: - cqfd (See [requirements](https://github.com/savoirfairelinux/cqfd?tab=readme-ov-file#requirements) and [installation](https://github.com/savoirfairelinux/cqfd?tab=readme-ov-file#installingremoving-cqfd) steps on github) From 48a75a2006d29131ca7eee18c0af3507fb453831 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?K=C3=A9vin=20L=27h=C3=B4pital?= Date: Tue, 30 Dec 2025 10:47:40 +0100 Subject: [PATCH 17/64] clang-format: adjust continuation indent width MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit To avoid mixed indentation between tabs and spaces in some cases, set the ContinuationIndentWidth to 8 (tab width). SFL: #21154 Change-Id: I4423f80fb3760a5c74b60a09def1fc9c8b74c8af Signed-off-by: Kévin L'hôpital --- .../api/api-mockup/.clang-format | 1 + .../api/api-mockup/src/gapi_mosquitto.c | 14 ++++++++------ 2 files changed, 9 insertions(+), 6 deletions(-) diff --git a/example-implementations/api/api-mockup/.clang-format b/example-implementations/api/api-mockup/.clang-format index 12c37cc..c371f58 100644 --- a/example-implementations/api/api-mockup/.clang-format +++ b/example-implementations/api/api-mockup/.clang-format @@ -4,3 +4,4 @@ UseTab: Always BreakBeforeBraces: Linux AllowShortIfStatementsOnASingleLine: false IndentCaseLabels: false +ContinuationIndentWidth: 8 diff --git a/example-implementations/api/api-mockup/src/gapi_mosquitto.c b/example-implementations/api/api-mockup/src/gapi_mosquitto.c index 67b933c..72a520f 100644 --- a/example-implementations/api/api-mockup/src/gapi_mosquitto.c +++ b/example-implementations/api/api-mockup/src/gapi_mosquitto.c @@ -68,8 +68,9 @@ static void on_message(struct mosquitto *mosq, void *obj, (void)mosq; for (int i = 0; i < topic_handler_count; i++) { if (strstr(msg->topic, topic_handlers[i].topic) != NULL) { - topic_handlers[i].topic_handler( - msg->topic, msg->payloadlen, (char *)msg->payload); + topic_handlers[i].topic_handler(msg->topic, + msg->payloadlen, + (char *)msg->payload); return; } } @@ -108,7 +109,7 @@ struct mosquitto *api_communication_init(const char *broker, int port) mosquitto_loop(mosq, -1, 1); return_code = - mosquitto_connect(mosq, broker, port, MOSQUITTO_KEEP_ALIVE); + mosquitto_connect(mosq, broker, port, MOSQUITTO_KEEP_ALIVE); if (return_code != MOSQ_ERR_SUCCESS) { fprintf(stderr, "Error: could not connect to %s: %s\n", broker, mosquitto_strerror(return_code)); @@ -151,8 +152,9 @@ int api_publish(struct mosquitto *mosq, const char *topic, const char *message, { int return_code = 0; - return_code = mosquitto_publish( - mosq, &sent_mid, topic, (int)strlen(message), message, qos, false); + return_code = + mosquitto_publish(mosq, &sent_mid, topic, (int)strlen(message), + message, qos, false); if (return_code != MOSQ_ERR_SUCCESS) { fprintf(stderr, "Publish failed: %s\n", mosquitto_strerror(return_code)); @@ -166,7 +168,7 @@ void api_register_handler(const char *topic, topic_handler_t handler) { if (topic_handler_count < MAX_TOPIC_HANDLERS) { topic_handlers[topic_handler_count].topic = - malloc(strlen(topic) + 1); + malloc(strlen(topic) + 1); // NOLINTNEXTLINE: strcpy is safe as enough memory allocate here strcpy(topic_handlers[topic_handler_count].topic, topic); From 7eed5f8a516de50e091b42cd6cf7325f82d24a13 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?K=C3=A9vin=20L=27h=C3=B4pital?= Date: Wed, 24 Dec 2025 16:09:00 +0100 Subject: [PATCH 18/64] gapi_discovery: add platform discovery response message MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Modify the platform discovery request handler to build and send a platform discovery response message using predefined platform information. As the message size is retrieved with the protobuf-c function, the api_publish function signature is modified to accept a message size parameter. The handler is also updated: * to add the mosquitto pointer parameter to be able to publish the response message. * to change the payload parameter type from char* to uint8_t* as the protbuf-c messages are binary data. SFL: #21154 Change-Id: Id563cd2f8530dcd07c48462250fdd871cc83fedf Signed-off-by: Kévin L'hôpital --- .../api/api-mockup/include/gapi_discovery.h | 1 + .../api/api-mockup/include/gapi_mosquitto.h | 10 +- .../api/api-mockup/src/gapi_discovery.c | 147 +++++++++++++++++- .../api/api-mockup/src/gapi_mosquitto.c | 16 +- 4 files changed, 154 insertions(+), 20 deletions(-) diff --git a/example-implementations/api/api-mockup/include/gapi_discovery.h b/example-implementations/api/api-mockup/include/gapi_discovery.h index b4ccce6..eb36a7a 100644 --- a/example-implementations/api/api-mockup/include/gapi_discovery.h +++ b/example-implementations/api/api-mockup/include/gapi_discovery.h @@ -8,6 +8,7 @@ #define GAPI_DISCOVERY_H #include "gapi_mosquitto.h" +#include "schemas/discovery.pb-c.h" /** * @brief Initialize API platform discovery messages diff --git a/example-implementations/api/api-mockup/include/gapi_mosquitto.h b/example-implementations/api/api-mockup/include/gapi_mosquitto.h index 623cff3..3e3d8ce 100644 --- a/example-implementations/api/api-mockup/include/gapi_mosquitto.h +++ b/example-implementations/api/api-mockup/include/gapi_mosquitto.h @@ -55,22 +55,24 @@ int api_subscribe(struct mosquitto *mosq, const char *topic, int qos); * * @param mosq Pointer to the mosquitto struct * @param topic The MQTT topic to publish to + * @param message_size The size of the message to publish * @param message The message to publish * @param qos The Quality of Service level for the publication * @return 0 on success, non-zero on failure */ -int api_publish(struct mosquitto *mosq, const char *topic, const char *message, - int qos); +int api_publish(struct mosquitto *mosq, const char *topic, size_t message_size, + const uint8_t *message, int qos); /** * @brief Type definition for a MQTT topic handler function * + * @param mosq Pointer to the mosquitto struct * @param topic The MQTT topic * @param payloadlen The length of the payload * @param payload The payload data */ -typedef void (*topic_handler_t)(const char *topic, const int payloadlen, - const char *payload); +typedef void (*topic_handler_t)(struct mosquitto *mosq, const char *topic, + const int payloadlen, const uint8_t *payload); /** * @brief Register a handler function for a specific MQTT topic diff --git a/example-implementations/api/api-mockup/src/gapi_discovery.c b/example-implementations/api/api-mockup/src/gapi_discovery.c index 208dbac..2e76155 100644 --- a/example-implementations/api/api-mockup/src/gapi_discovery.c +++ b/example-implementations/api/api-mockup/src/gapi_discovery.c @@ -6,18 +6,151 @@ #include "gapi_discovery.h" -static void api_platform_discovery_req_handler(const char *topic, +// NOLINTBEGIN(cppcoreguidelines-interfaces-global-init,cppcoreguidelines-avoid-magic-numbers,readability-magic-numbers) +static PlatformDiscoveryGEISA geisa_platform_info = { + .base = PROTOBUF_C_MESSAGE_INIT(&platform_discovery__geisa__descriptor), + .ver_major = 0, + .ver_minor = 1, + .ver_rev = 7, + .piller_adm = true, + .piller_api = true, + .piller_lee = true, + .piller_vee = false, +}; + +static const PlatformDiscoveryType top_type_platform_info = + PLATFORM_DISCOVERY__TYPE__DEVICE_TYPE_ELECTRIC_METER; + +static PlatformDiscoveryModule top_module_platform_info = { + .base = PROTOBUF_C_MESSAGE_INIT( + &platform_discovery__module__descriptor), + .type = top_type_platform_info, + .manufacturer = "SCE", + .model = "GEISA-1", + .serial_number = "GEISA0001", + .hw_revision = "A", + .fw_revision = "1.0.0", +}; + +static const PlatformDiscoveryType sub_type_platform_info = + PLATFORM_DISCOVERY__TYPE__DEVICE_TYPE_METROLOGY_PROCESSOR; + +static PlatformDiscoveryModule sub_module1_platform_info = { + .base = PROTOBUF_C_MESSAGE_INIT( + &platform_discovery__module__descriptor), + .type = sub_type_platform_info, + .manufacturer = "SCE", + .model = "GEISA-2", + .serial_number = "GEISA0002", + .hw_revision = "B", + .fw_revision = "1.1.0", +}; + +static PlatformDiscoveryModule *sub_modules_platform_info[] = { + &sub_module1_platform_info}; + +static PlatformDiscoveryDevice device_platform_info = { + .base = PROTOBUF_C_MESSAGE_INIT( + &platform_discovery__device__descriptor), + .top_module = &top_module_platform_info, + .n_sub_module = 1, + .sub_module = sub_modules_platform_info, +}; + +static PlatformDiscoveryOperator operator_platform_info = + PLATFORM_DISCOVERY__OPERATOR__INIT; +static PlatformDiscoveryMetrology metrology_platform_info = + PLATFORM_DISCOVERY__METROLOGY__INIT; +static PlatformDiscoverySensor sensor_platform_info = + PLATFORM_DISCOVERY__SENSOR__INIT; +static PlatformDiscoveryNetwork network_platform_info = + PLATFORM_DISCOVERY__NETWORK__INIT; + +static const WaveformDatatype waveform_data_type_platform_info = + WAVEFORM__DATATYPE__DATA_INT32; + +static PlatformDiscoveryWaveformInstance waveform_platform_instance1_info = { + .base = PROTOBUF_C_MESSAGE_INIT( + &platform_discovery__waveform__instance__descriptor), + .instance_id = 1, + .data_connection = "/data/waveform/instance1.socket", + .datatype = waveform_data_type_platform_info, + .voltage_multiplier = 0.1, + .current_multiplier = 0.01, + .num_voltage_ch = 1, + .num_current_ch = 1, + .num_other_ch = 0, + .sample_rate_is_cycle_locked = true, + .sample_rate = 7680, + .frame_is_cycle_aligned = true, + .frame_duration_samples = 200, + .voltage_filter_lowpass = 0, + .voltage_filter_highpass = 0, + .current_filter_lowpass = 0, + .current_filter_highpass = 0, +}; + +static PlatformDiscoveryWaveformInstance *waveform_platform_instances[] = { + &waveform_platform_instance1_info, +}; + +static PlatformDiscoveryWaveform waveform_platform_info = { + PROTOBUF_C_MESSAGE_INIT(&platform_discovery__waveform__descriptor), + .n_instances = 1, + .instances = waveform_platform_instances, +}; +// NOLINTEND(cppcoreguidelines-interfaces-global-init,cppcoreguidelines-avoid-magic-numbers,readability-magic-numbers) + +static void +api_platform_discovery_build_response(PlatformDiscoveryRsp *response) +{ + response->geisa = &geisa_platform_info; + response->device = &device_platform_info; + response->operator_ = &operator_platform_info; + response->metrology = &metrology_platform_info; + response->sensor = &sensor_platform_info; + response->network = &network_platform_info; + response->waveform = &waveform_platform_info; +} + +static void api_platform_discovery_req_handler(struct mosquitto *mosq, + const char *topic, const int payloadlen, - const char *payload) + const uint8_t *payload) { - fprintf(stdout, "Received platform discovery request\n"); - fprintf(stdout, "[msg] topic=%s payload=%.*s\n", topic, payloadlen, - payload); + PlatformDiscoveryReq *request = NULL; + PlatformDiscoveryRsp response = PLATFORM_DISCOVERY__RSP__INIT; + uint8_t *message = NULL; + (void)topic; + + fprintf(stdout, "[Discovery] Received platform discovery request\n"); + request = platform_discovery__req__unpack(NULL, payloadlen, payload); + if (request == NULL) { + fprintf(stderr, "[Discovery] Error unpacking platform " + "discovery request\n"); + return; + } + platform_discovery__req__free_unpacked(request, NULL); + + api_platform_discovery_build_response(&response); + message = malloc(platform_discovery__rsp__get_packed_size(&response)); + if (message == NULL) { + fprintf(stderr, "[Discovery] Error allocating memory for " + "platform discovery response\n"); + return; + } + platform_discovery__rsp__pack(&response, message); + api_publish(mosq, "geisa/api/platform-discovery-rsp", + platform_discovery__rsp__get_packed_size(&response), + message, 1); + free(message); } -static void api_manifest_req_handler(const char *topic, const int payloadlen, - const char *payload) +static void api_manifest_req_handler(struct mosquitto *mosq, const char *topic, + const int payloadlen, + const uint8_t *payload) { + (void)mosq; fprintf(stdout, "Received app manifest request\n"); fprintf(stdout, "[msg] topic=%s payload=%.*s\n", topic, payloadlen, payload); diff --git a/example-implementations/api/api-mockup/src/gapi_mosquitto.c b/example-implementations/api/api-mockup/src/gapi_mosquitto.c index 72a520f..1e3c5e3 100644 --- a/example-implementations/api/api-mockup/src/gapi_mosquitto.c +++ b/example-implementations/api/api-mockup/src/gapi_mosquitto.c @@ -65,12 +65,11 @@ static void on_message(struct mosquitto *mosq, void *obj, const struct mosquitto_message *msg) { (void)obj; - (void)mosq; for (int i = 0; i < topic_handler_count; i++) { if (strstr(msg->topic, topic_handlers[i].topic) != NULL) { - topic_handlers[i].topic_handler(msg->topic, - msg->payloadlen, - (char *)msg->payload); + topic_handlers[i].topic_handler( + mosq, msg->topic, msg->payloadlen, + (uint8_t *)msg->payload); return; } } @@ -147,14 +146,13 @@ int api_subscribe(struct mosquitto *mosq, const char *topic, int qos) return 0; } -int api_publish(struct mosquitto *mosq, const char *topic, const char *message, - int qos) +int api_publish(struct mosquitto *mosq, const char *topic, + const size_t message_size, const uint8_t *message, int qos) { int return_code = 0; - return_code = - mosquitto_publish(mosq, &sent_mid, topic, (int)strlen(message), - message, qos, false); + return_code = mosquitto_publish(mosq, &sent_mid, topic, + (int)message_size, message, qos, false); if (return_code != MOSQ_ERR_SUCCESS) { fprintf(stderr, "Publish failed: %s\n", mosquitto_strerror(return_code)); From f79b01ef37794c91c37940b8f530670f6bd02b30 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?K=C3=A9vin=20L=27h=C3=B4pital?= Date: Mon, 29 Dec 2025 16:42:41 +0100 Subject: [PATCH 19/64] gapi_mosquitto: don't exit program after sending message or on error MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit As the API program is a daemon, it should not exit after sending a message or after and error. Instead, it should just continue running. Then remove the exit(1) call in on_publish callback when the mid does not match the expected one and remove the set of running to false that will stop the main loop after sending a message. SFL: #21154 Change-Id: I2f7bfe1ac2a8a5cd1830013cecfd59826ae1609c Signed-off-by: Kévin L'hôpital --- example-implementations/api/api-mockup/src/gapi_mosquitto.c | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/example-implementations/api/api-mockup/src/gapi_mosquitto.c b/example-implementations/api/api-mockup/src/gapi_mosquitto.c index 1e3c5e3..7b3fa14 100644 --- a/example-implementations/api/api-mockup/src/gapi_mosquitto.c +++ b/example-implementations/api/api-mockup/src/gapi_mosquitto.c @@ -52,12 +52,9 @@ static void on_publish(struct mosquitto *mosq, void *obj, int mid) { (void)obj; (void)mosq; - if (mid == sent_mid) { - running = false; - } else { + if (mid != sent_mid) { fprintf(stderr, "[publish] mid=%d (not expected %d)\n", mid, sent_mid); - exit(1); } } From 8af2dc896df061054e69f1a225a1faae7c5c0e9e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?K=C3=A9vin=20L=27h=C3=B4pital?= Date: Mon, 29 Dec 2025 18:03:01 +0100 Subject: [PATCH 20/64] gapi_discovery: add application manifest req MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Update the application manifest handler to respond with a static manifest to the app_id extracted from the topic. SFL: #21154 Change-Id: Ib8fa1081f31a585957a07a843a356f7c05a3c928 Signed-off-by: Kévin L'hôpital --- .../api/api-mockup/include/gapi_discovery.h | 1 + .../api/api-mockup/src/gapi_discovery.c | 57 +++++++++++++++++-- 2 files changed, 54 insertions(+), 4 deletions(-) diff --git a/example-implementations/api/api-mockup/include/gapi_discovery.h b/example-implementations/api/api-mockup/include/gapi_discovery.h index eb36a7a..64d3825 100644 --- a/example-implementations/api/api-mockup/include/gapi_discovery.h +++ b/example-implementations/api/api-mockup/include/gapi_discovery.h @@ -9,6 +9,7 @@ #include "gapi_mosquitto.h" #include "schemas/discovery.pb-c.h" +#include /** * @brief Initialize API platform discovery messages diff --git a/example-implementations/api/api-mockup/src/gapi_discovery.c b/example-implementations/api/api-mockup/src/gapi_discovery.c index 2e76155..bfcf7f7 100644 --- a/example-implementations/api/api-mockup/src/gapi_discovery.c +++ b/example-implementations/api/api-mockup/src/gapi_discovery.c @@ -99,8 +99,15 @@ static PlatformDiscoveryWaveform waveform_platform_info = { .n_instances = 1, .instances = waveform_platform_instances, }; + +uint8_t deployment_manifest_data[] = {0x0A, 0x0B, 0x0C, 0x0D}; // NOLINTEND(cppcoreguidelines-interfaces-global-init,cppcoreguidelines-avoid-magic-numbers,readability-magic-numbers) +static ProtobufCBinaryData deployment_manifest = { + .len = sizeof(deployment_manifest_data), + .data = deployment_manifest_data, +}; + static void api_platform_discovery_build_response(PlatformDiscoveryRsp *response) { @@ -150,10 +157,52 @@ static void api_manifest_req_handler(struct mosquitto *mosq, const char *topic, const int payloadlen, const uint8_t *payload) { - (void)mosq; - fprintf(stdout, "Received app manifest request\n"); - fprintf(stdout, "[msg] topic=%s payload=%.*s\n", topic, payloadlen, - payload); + ApplicationManifestReq *request = NULL; + ApplicationManifestRsp response = APPLICATION_MANIFEST__RSP__INIT; + uint8_t *message = NULL; + char *rsp_topic = NULL; + char *app_id = NULL; + + app_id = basename((char *)topic); + + fprintf(stdout, "[Manifest] Received app manifest request from %s\n", + app_id); + request = application_manifest__req__unpack(NULL, payloadlen, payload); + if (request == NULL) { + fprintf(stderr, + "[Manifest] Error unpacking app manifest request\n"); + return; + } + application_manifest__req__free_unpacked(request, NULL); + + rsp_topic = malloc(strlen("geisa/api/app-manifest-rsp/") + + strlen(app_id) + 1); + if (rsp_topic == NULL) { + fprintf(stderr, + "[Manifest] Error allocating memory for response " + "topic\n"); + return; + } + // NOLINTNEXTLINE: strcpy is safe as enough memory allocate here + strcpy(rsp_topic, "geisa/api/app-manifest-rsp/"); + // NOLINTNEXTLINE: strcat is safe as enough memory allocate here + strcat(rsp_topic, app_id); + + response.manifest = deployment_manifest; + message = malloc(application_manifest__rsp__get_packed_size(&response)); + if (message == NULL) { + fprintf(stderr, + "[Manifest] Error allocating memory for response " + "message\n"); + free(rsp_topic); + return; + } + application_manifest__rsp__pack(&response, message); + api_publish(mosq, rsp_topic, + application_manifest__rsp__get_packed_size(&response), + message, 1); + free(message); + free(rsp_topic); } void api_discovery_init(struct mosquitto *mosq) From a710df9eb1071be0b7464056629ae5e8d8050a60 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?K=C3=A9vin=20L=27h=C3=B4pital?= Date: Tue, 30 Dec 2025 14:32:04 +0100 Subject: [PATCH 21/64] gapi_mosquitto: remove sent_mid tracking MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Sent_mid variable was used to track if the published message ID matched the one received in the on_publish callback. However, since there will be multiple publishers in the future, this tracking is not reliable. As the publish callback is not doing anything else, remove it. SFL: #19175 Change-Id: Ibea7edfd5954bcd29e6f0e5843b04093f5e9390b Signed-off-by: Kévin L'hôpital --- .../api/api-mockup/src/gapi_mosquitto.c | 16 ++-------------- 1 file changed, 2 insertions(+), 14 deletions(-) diff --git a/example-implementations/api/api-mockup/src/gapi_mosquitto.c b/example-implementations/api/api-mockup/src/gapi_mosquitto.c index 7b3fa14..5043095 100644 --- a/example-implementations/api/api-mockup/src/gapi_mosquitto.c +++ b/example-implementations/api/api-mockup/src/gapi_mosquitto.c @@ -10,7 +10,6 @@ const int MOSQUITTO_KEEP_ALIVE = 60; enum { MAX_TOPIC_HANDLERS = 3 }; -static int sent_mid; static int topic_handler_count = 0; typedef struct { @@ -48,16 +47,6 @@ static void on_disconnect(struct mosquitto *mosq, void *obj, int return_code) isConnected = false; } -static void on_publish(struct mosquitto *mosq, void *obj, int mid) -{ - (void)obj; - (void)mosq; - if (mid != sent_mid) { - fprintf(stderr, "[publish] mid=%d (not expected %d)\n", mid, - sent_mid); - } -} - static void on_message(struct mosquitto *mosq, void *obj, const struct mosquitto_message *msg) { @@ -96,7 +85,6 @@ struct mosquitto *api_communication_init(const char *broker, int port) mosquitto_username_pw_set(mosq, "API", "api"); mosquitto_connect_callback_set(mosq, on_connect); mosquitto_disconnect_callback_set(mosq, on_disconnect); - mosquitto_publish_callback_set(mosq, on_publish); mosquitto_message_callback_set(mosq, on_message); signal(SIGINT, handle_signal); @@ -148,8 +136,8 @@ int api_publish(struct mosquitto *mosq, const char *topic, { int return_code = 0; - return_code = mosquitto_publish(mosq, &sent_mid, topic, - (int)message_size, message, qos, false); + return_code = mosquitto_publish(mosq, NULL, topic, (int)message_size, + message, qos, false); if (return_code != MOSQ_ERR_SUCCESS) { fprintf(stderr, "Publish failed: %s\n", mosquitto_strerror(return_code)); From 3b48a844280c67f898786d8f2249b42e1ad5bcac Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?K=C3=A9vin=20L=27h=C3=B4pital?= Date: Tue, 30 Dec 2025 14:39:06 +0100 Subject: [PATCH 22/64] src: add instantaneous data sending thread MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add a thread that will send instantaneous data messages every second. The thread is initialized in main() after the discovery initialization. For now the thread sends a dummy message "instdata" on the topic "geisa/api/instantaneous-data". SFL: #19175 Change-Id: I0b1100b6a5a71ba39661d175c059576d5dd3017b Signed-off-by: Kévin L'hôpital --- example-implementations/api/api-mockup/gapi.c | 12 ++++++- .../api-mockup/include/gapi_instantaneous.h | 24 ++++++++++++++ .../api/api-mockup/src/gapi_instantaneous.c | 33 +++++++++++++++++++ 3 files changed, 68 insertions(+), 1 deletion(-) create mode 100644 example-implementations/api/api-mockup/include/gapi_instantaneous.h create mode 100644 example-implementations/api/api-mockup/src/gapi_instantaneous.c diff --git a/example-implementations/api/api-mockup/gapi.c b/example-implementations/api/api-mockup/gapi.c index 565e7b8..bee9475 100644 --- a/example-implementations/api/api-mockup/gapi.c +++ b/example-implementations/api/api-mockup/gapi.c @@ -4,6 +4,7 @@ * @copyright Copyright (C) 2025 Southern California Edison */ #include "gapi_discovery.h" +#include "gapi_instantaneous.h" #include "gapi_mosquitto.h" volatile bool running = true; @@ -12,8 +13,10 @@ volatile bool isConnected = false; int main() { + pthread_t instantaneous_thread = 0; const char *broker = "localhost"; struct mosquitto *mosq = NULL; + int ret = EXIT_SUCCESS; int port = PORT; mosq = api_communication_init(broker, port); @@ -27,12 +30,19 @@ int main() } api_discovery_init(mosq); + ret = api_instantaneous_init(&instantaneous_thread, mosq); + if (ret != EXIT_SUCCESS) { + goto deinit; + } while (running) { mosquitto_loop(mosq, -1, 1); sleep(1); } + pthread_join(instantaneous_thread, NULL); + +deinit: api_communication_deinit(mosq); - return EXIT_SUCCESS; + return ret; } diff --git a/example-implementations/api/api-mockup/include/gapi_instantaneous.h b/example-implementations/api/api-mockup/include/gapi_instantaneous.h new file mode 100644 index 0000000..b1a70e0 --- /dev/null +++ b/example-implementations/api/api-mockup/include/gapi_instantaneous.h @@ -0,0 +1,24 @@ +/** + * @file gapi_instantaneous.h + * @brief Header file for API instantaneous data sending + * @copyright Copyright (C) 2025 Southern California Edison + */ + +#ifndef GAPI_INSTANTANEOUS_H +#define GAPI_INSTANTANEOUS_H + +#include "gapi_mosquitto.h" +#include "schemas/metered_quantities.pb-c.h" +#include + +/** + * @brief Initialize API instantaneous data sending thread + * + * @param thread Pointer to the pthread_t variable to store the thread ID + * @param mosq Pointer to the mosquitto instance + * + * @return int 0 on success, non-zero on failure + */ +int api_instantaneous_init(pthread_t *thread, struct mosquitto *mosq); + +#endif // GAPI_INSTANTANEOUS_H diff --git a/example-implementations/api/api-mockup/src/gapi_instantaneous.c b/example-implementations/api/api-mockup/src/gapi_instantaneous.c new file mode 100644 index 0000000..26ec33f --- /dev/null +++ b/example-implementations/api/api-mockup/src/gapi_instantaneous.c @@ -0,0 +1,33 @@ +/** + * @file gapi_instantaneous.c + * @brief Definition file for API instantaneous data sending + * @copyright Copyright (C) 2025 Southern California Edison + */ + +#include "gapi_instantaneous.h" + +static void *gapi_instantaneous_thread(void *arg) +{ + struct mosquitto *mosq = (struct mosquitto *)arg; + uint8_t *message = (uint8_t *)"instdata"; + + while (running) { + api_publish(mosq, "geisa/api/instantaneous-data", + sizeof(message), message, 0); + sleep(1); + } + + return NULL; +} + +int api_instantaneous_init(pthread_t *thread, struct mosquitto *mosq) +{ + int ret = 0; + ret = pthread_create(thread, NULL, gapi_instantaneous_thread, mosq); + if (ret != 0) { + fprintf(stderr, "[Instantaneous] Failed to create " + "instantaneous thread\n"); + return ret; + } + return 0; +} From 29ada38d4724d2b8cfec5916e84302f3be3aafa4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?K=C3=A9vin=20L=27h=C3=B4pital?= Date: Tue, 30 Dec 2025 17:56:05 +0100 Subject: [PATCH 23/64] gapi_instantaneous: add mock instantaneous data retrieval and publishing MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit In order to simulate the retrieval of instantaneous data from the system, add a function that populates the data structures with mock values. These values are sent every second on the "geisa/api/instantaneous-data" topic. SFL: #19175 Change-Id: Ieba7371b4b8bd36939476e744d60e629440205cc Signed-off-by: Kévin L'hôpital --- .../api-mockup/include/gapi_instantaneous.h | 3 + .../api/api-mockup/src/gapi_instantaneous.c | 137 +++++++++++++++++- 2 files changed, 137 insertions(+), 3 deletions(-) diff --git a/example-implementations/api/api-mockup/include/gapi_instantaneous.h b/example-implementations/api/api-mockup/include/gapi_instantaneous.h index b1a70e0..2219da8 100644 --- a/example-implementations/api/api-mockup/include/gapi_instantaneous.h +++ b/example-implementations/api/api-mockup/include/gapi_instantaneous.h @@ -10,6 +10,7 @@ #include "gapi_mosquitto.h" #include "schemas/metered_quantities.pb-c.h" #include +#include /** * @brief Initialize API instantaneous data sending thread @@ -21,4 +22,6 @@ */ int api_instantaneous_init(pthread_t *thread, struct mosquitto *mosq); +#define SEC_IN_MS 1000 + #endif // GAPI_INSTANTANEOUS_H diff --git a/example-implementations/api/api-mockup/src/gapi_instantaneous.c b/example-implementations/api/api-mockup/src/gapi_instantaneous.c index 26ec33f..a85dc1a 100644 --- a/example-implementations/api/api-mockup/src/gapi_instantaneous.c +++ b/example-implementations/api/api-mockup/src/gapi_instantaneous.c @@ -6,14 +6,145 @@ #include "gapi_instantaneous.h" +static void +geisa_get_instantaneous_data(InstantaneousQuantities *response, + TypeInstantaneousQuantitiesPerPhase *phase_a, + TypeInstantaneousQuantitiesPerPhase *phase_b, + TypeInstantaneousQuantitiesPerPhase *phase_c, + TypeInstantaneousQuantitiesPerPhase *phase_n, + TypeInstantaneousQuantitiesOther *other) +{ + // This function should interact with the GEISA system to retrieve + // instantaneous data. Here we provide a mock implementation. + instantaneous_quantities__init(response); + type_instantaneous_quantities__per_phase__init(phase_a); + type_instantaneous_quantities__per_phase__init(phase_b); + type_instantaneous_quantities__per_phase__init(phase_c); + type_instantaneous_quantities__per_phase__init(phase_n); + type_instantaneous_quantities__other__init(other); + + time_t timestamp = time(NULL) * SEC_IN_MS; + + // NOLINTBEGIN(readability-magic-numbers,cppcoreguidelines-avoid-magic-numbers) + // Mock data assignment + (*phase_a).message_version = 1; + (*phase_a).phase = TYPE_PHASE__PHASE_A; + (*phase_a).microamps = 10.0F; + (*phase_a).microvolts = 120.0F; + (*phase_a).microw = 1200.0F; + (*phase_a).microvar = 300.0F; + (*phase_a).microva = 1300.0F; + (*phase_a).voltage_angle = 10.0F; + (*phase_a).current_angle = 30.0F; + (*phase_a).pf = 0.95F; + (*phase_a).microamps_fundamental = 9.5F; + (*phase_a).microvolts_fundamental = 115.0F; + (*phase_a).current_percentage_thd = 5.0F; + (*phase_a).voltage_percentage_thd = 4.0F; + (*phase_a).pf_angle = 20.0F; + (*phase_a).current_percentage_tdd = 3.0F; + (*phase_a).harmonic_current = 0.5F; + (*phase_a).phase_voltage_percentage_2nd_harmonic = 1.0F; + + (*phase_b).message_version = 1; + (*phase_b).phase = TYPE_PHASE__PHASE_B; + (*phase_b).microamps = 11.0F; + (*phase_b).microvolts = 121.0F; + (*phase_b).microw = 1210.0F; + (*phase_b).microvar = 310.0F; + (*phase_b).microva = 1310.0F; + (*phase_b).voltage_angle = 11.0F; + (*phase_b).current_angle = 31.0F; + (*phase_b).pf = 0.96F; + (*phase_b).microamps_fundamental = 10.5F; + (*phase_b).microvolts_fundamental = 116.0F; + (*phase_b).current_percentage_thd = 6.0F; + (*phase_b).voltage_percentage_thd = 5.0F; + (*phase_b).pf_angle = 21.0F; + (*phase_b).current_percentage_tdd = 4.0F; + (*phase_b).harmonic_current = 0.6F; + (*phase_b).phase_voltage_percentage_2nd_harmonic = 1.1F; + + (*phase_c).message_version = 1; + (*phase_c).phase = TYPE_PHASE__PHASE_C; + (*phase_c).microamps = 12.0F; + (*phase_c).microvolts = 122.0F; + (*phase_c).microw = 1220.0F; + (*phase_c).microvar = 320.0F; + (*phase_c).microva = 1320.0F; + (*phase_c).voltage_angle = 12.0F; + (*phase_c).current_angle = 32.0F; + (*phase_c).pf = 0.97F; + (*phase_c).microamps_fundamental = 11.5F; + (*phase_c).microvolts_fundamental = 117.0F; + (*phase_c).current_percentage_thd = 7.0F; + (*phase_c).voltage_percentage_thd = 6.0F; + (*phase_c).pf_angle = 22.0F; + (*phase_c).current_percentage_tdd = 5.0F; + (*phase_c).harmonic_current = 0.7F; + (*phase_c).phase_voltage_percentage_2nd_harmonic = 1.2F; + + (*phase_n).message_version = 1; + (*phase_n).microamps = 5.0F; + (*phase_n).microvolts = 60.0F; + (*phase_n).microw = 600.0F; + (*phase_n).microvar = 150.0F; + (*phase_n).microva = 650.0F; + (*phase_n).voltage_angle = 5.0F; + (*phase_n).current_angle = 15.0F; + (*phase_n).pf = 0.98F; + (*phase_n).microamps_fundamental = 4.5F; + (*phase_n).microvolts_fundamental = 55.0F; + (*phase_n).current_percentage_thd = 2.0F; + (*phase_n).voltage_percentage_thd = 1.5F; + (*phase_n).pf_angle = 10.0F; + (*phase_n).current_percentage_tdd = 1.0F; + (*phase_n).harmonic_current = 0.2F; + (*phase_n).phase_voltage_percentage_2nd_harmonic = 0.5F; + + (*other).message_version = 1; + (*other).timestamp = timestamp; + (*other).neutral_imputed_microamps = 5.5F; + (*other).load_side_microvolts = 123.0F; + + // NOLINTEND(readability-magic-numbers,cppcoreguidelines-avoid-magic-numbers) + + response->timestamp = timestamp; + response->phase_a = phase_a; + response->phase_b = phase_b; + response->phase_c = phase_c; + response->phase_n = phase_n; + response->other = other; +} + static void *gapi_instantaneous_thread(void *arg) { struct mosquitto *mosq = (struct mosquitto *)arg; - uint8_t *message = (uint8_t *)"instdata"; + TypeInstantaneousQuantitiesPerPhase phase_a; + TypeInstantaneousQuantitiesPerPhase phase_b; + TypeInstantaneousQuantitiesPerPhase phase_c; + TypeInstantaneousQuantitiesPerPhase phase_n; + TypeInstantaneousQuantitiesOther other; + InstantaneousQuantities response; + uint8_t *message = NULL; while (running) { - api_publish(mosq, "geisa/api/instantaneous-data", - sizeof(message), message, 0); + geisa_get_instantaneous_data(&response, &phase_a, &phase_b, + &phase_c, &phase_n, &other); + message = malloc( + instantaneous_quantities__get_packed_size(&response)); + if (message == NULL) { + fprintf(stderr, "[Instantaneous] Failed to allocate " + "memory for instantaneous data\n"); + sleep(1); + continue; + } + instantaneous_quantities__pack(&response, message); + api_publish( + mosq, "geisa/api/instantaneous-data", + instantaneous_quantities__get_packed_size(&response), + message, 0); + free(message); sleep(1); } From b968dad4f7160074572b03a79245afda837a3eb2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?K=C3=A9vin=20L=27h=C3=B4pital?= Date: Tue, 20 Jan 2026 15:36:23 +0100 Subject: [PATCH 24/64] gapi_instantaneous: add phase_n assignment MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The schemas repository is now defining the TYPE_PHASE__PHASE_N enum value in the latest version. Then update the schemas submodule and assign the phase field for phase_n. SFL: #19175 Change-Id: I850923a66884e013c1d4df5374a1fb48b7bc8551 Signed-off-by: Kévin L'hôpital --- example-implementations/api/api-mockup/schemas | 2 +- example-implementations/api/api-mockup/src/gapi_instantaneous.c | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/example-implementations/api/api-mockup/schemas b/example-implementations/api/api-mockup/schemas index 3653c10..88b36cf 160000 --- a/example-implementations/api/api-mockup/schemas +++ b/example-implementations/api/api-mockup/schemas @@ -1 +1 @@ -Subproject commit 3653c107c3f0cb89ad3c83bea1bde5aea7b0ffaf +Subproject commit 88b36cf065a1b84503a4516af38bbfa509da5074 diff --git a/example-implementations/api/api-mockup/src/gapi_instantaneous.c b/example-implementations/api/api-mockup/src/gapi_instantaneous.c index a85dc1a..60adcfe 100644 --- a/example-implementations/api/api-mockup/src/gapi_instantaneous.c +++ b/example-implementations/api/api-mockup/src/gapi_instantaneous.c @@ -85,6 +85,7 @@ geisa_get_instantaneous_data(InstantaneousQuantities *response, (*phase_c).phase_voltage_percentage_2nd_harmonic = 1.2F; (*phase_n).message_version = 1; + (*phase_n).phase = TYPE_PHASE__PHASE_N; (*phase_n).microamps = 5.0F; (*phase_n).microvolts = 60.0F; (*phase_n).microw = 600.0F; From 28da7ab61fa3f6ca1da4069c6b23e255b7562db4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?K=C3=A9vin=20L=27h=C3=B4pital?= Date: Tue, 20 Jan 2026 16:55:35 +0100 Subject: [PATCH 25/64] gapi: add waveform data message handler MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Create a new module to handle API waveform data messages. This module subscribes to the "geisa/api/waveform-req/#" topic and processes incoming requests. For now if the message is well-formed, it responds with a success status. SFL: #19176 Change-Id: I995a478f44d6a5b2cf1e3cbcb9b8df5bbcf32862 Signed-off-by: Kévin L'hôpital --- example-implementations/api/api-mockup/gapi.c | 2 + .../api/api-mockup/include/gapi_waveform.h | 22 +++++++ .../api/api-mockup/src/gapi_waveform.c | 60 +++++++++++++++++++ 3 files changed, 84 insertions(+) create mode 100644 example-implementations/api/api-mockup/include/gapi_waveform.h create mode 100644 example-implementations/api/api-mockup/src/gapi_waveform.c diff --git a/example-implementations/api/api-mockup/gapi.c b/example-implementations/api/api-mockup/gapi.c index bee9475..12dfada 100644 --- a/example-implementations/api/api-mockup/gapi.c +++ b/example-implementations/api/api-mockup/gapi.c @@ -6,6 +6,7 @@ #include "gapi_discovery.h" #include "gapi_instantaneous.h" #include "gapi_mosquitto.h" +#include "gapi_waveform.h" volatile bool running = true; volatile bool isConnected = false; @@ -34,6 +35,7 @@ int main() if (ret != EXIT_SUCCESS) { goto deinit; } + api_waveform_init(mosq); while (running) { mosquitto_loop(mosq, -1, 1); diff --git a/example-implementations/api/api-mockup/include/gapi_waveform.h b/example-implementations/api/api-mockup/include/gapi_waveform.h new file mode 100644 index 0000000..f4e032f --- /dev/null +++ b/example-implementations/api/api-mockup/include/gapi_waveform.h @@ -0,0 +1,22 @@ +/** + * @file gapi_waveform.h + * @brief Header file for API waveform data messages + * @copyright Copyright (C) 2026 Southern California Edison + */ + +#ifndef GAPI_WAVEFORM_H +#define GAPI_WAVEFORM_H +#define _GNU_SOURCE + +#include "gapi_mosquitto.h" +#include "schemas/waveform.pb-c.h" +#include + +/** + * @brief Initialize API waveform data messages + * + * @param mosq Pointer to the mosquitto instance + */ +void api_waveform_init(struct mosquitto *mosq); + +#endif // GAPI_WAVEFORM_H diff --git a/example-implementations/api/api-mockup/src/gapi_waveform.c b/example-implementations/api/api-mockup/src/gapi_waveform.c new file mode 100644 index 0000000..1557e8b --- /dev/null +++ b/example-implementations/api/api-mockup/src/gapi_waveform.c @@ -0,0 +1,60 @@ +/** + * @file gapi_waveform.c + * @brief Definition file for API waveform data messages + * @copyright Copyright (C) 2026 Southern California Edison + */ + +#include "gapi_waveform.h" + +static void api_waveform_req_handler(struct mosquitto *mosq, const char *topic, + const int payloadlen, + const uint8_t *payload) +{ + WaveformReq *request = NULL; + WaveformRsp response = WAVEFORM__RSP__INIT; + + uint8_t *message = NULL; + char *rsp_topic = NULL; + char *app_id = NULL; + + app_id = basename((char *)topic); + + fprintf(stdout, "[Waveform] Received waveform data request from %s\n", + app_id); + request = waveform__req__unpack(NULL, payloadlen, payload); + if (request == NULL) { + fprintf(stderr, + "[Waveform] Error unpacking app manifest request\n"); + return; + } + + waveform__req__free_unpacked(request, NULL); + + if (asprintf(&rsp_topic, "geisa/api/waveform-rsp/%s", app_id) == -1) { + fprintf(stderr, "[Waveform] Error allocating memory for " + "response topic\n"); + return; + } + + response.status = WAVEFORM__STATUS__WAVEFORM_SUCCESS; + message = malloc(waveform__rsp__get_packed_size(&response)); + if (message == NULL) { + fprintf(stderr, + "[Waveform] Error allocating memory for response " + "message\n"); + free(rsp_topic); + return; + } + waveform__rsp__pack(&response, message); + api_publish(mosq, rsp_topic, waveform__rsp__get_packed_size(&response), + message, 1); + free(message); +} + +void api_waveform_init(struct mosquitto *mosq) +{ + api_register_handler("geisa/api/waveform-req", + api_waveform_req_handler); + + api_subscribe(mosq, "geisa/api/waveform-req/#", 1); +} From 3d90f0b2d4e079c37647ffd8c86a1ff6c20858cc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?K=C3=A9vin=20L=27h=C3=B4pital?= Date: Wed, 21 Jan 2026 15:23:22 +0100 Subject: [PATCH 26/64] gapi_discovery: add function to get waveform info from discovery MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit In order to facilitate access to waveform information for platform discovery, introduce a new function `get_waveform_info()`. This function returns a pointer to the `PlatformDiscoveryWaveform` structure, allowing other components to easily retrieve waveform details without duplicating code. This will be used in the waveform API. SFL: #19176 Change-Id: I94ab2431b1eedc108bdf3630c40b8fdb356580b2 Signed-off-by: Kévin L'hôpital --- .../api/api-mockup/include/gapi_discovery.h | 7 +++++++ .../api/api-mockup/src/gapi_discovery.c | 2 ++ 2 files changed, 9 insertions(+) diff --git a/example-implementations/api/api-mockup/include/gapi_discovery.h b/example-implementations/api/api-mockup/include/gapi_discovery.h index 64d3825..41bb9db 100644 --- a/example-implementations/api/api-mockup/include/gapi_discovery.h +++ b/example-implementations/api/api-mockup/include/gapi_discovery.h @@ -18,4 +18,11 @@ */ void api_discovery_init(struct mosquitto *mosq); +/** + * @brief Get waveform information for platform discovery + * + * @return Pointer to PlatformDiscoveryWaveform structure + */ +PlatformDiscoveryWaveform get_waveform_info(); + #endif // GAPI_DISCOVERY_H diff --git a/example-implementations/api/api-mockup/src/gapi_discovery.c b/example-implementations/api/api-mockup/src/gapi_discovery.c index bfcf7f7..12de279 100644 --- a/example-implementations/api/api-mockup/src/gapi_discovery.c +++ b/example-implementations/api/api-mockup/src/gapi_discovery.c @@ -120,6 +120,8 @@ api_platform_discovery_build_response(PlatformDiscoveryRsp *response) response->waveform = &waveform_platform_info; } +PlatformDiscoveryWaveform get_waveform_info() { return waveform_platform_info; } + static void api_platform_discovery_req_handler(struct mosquitto *mosq, const char *topic, const int payloadlen, From 853fa46889ba0e815f9d589b6761c0eb7f6e9b50 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?K=C3=A9vin=20L=27h=C3=B4pital?= Date: Wed, 21 Jan 2026 15:25:33 +0100 Subject: [PATCH 27/64] gapi_waveform: build the response based on platform info MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit There is multiple status possible when building the response to a waveform enabling, then base on the platform info and the request parameters, set the correct status. SFL: #19176 Change-Id: I87a7d2d5da4d8d6f5a47e6c7d3aeb215ac8caf47 Signed-off-by: Kévin L'hôpital --- .../api/api-mockup/src/gapi_waveform.c | 30 +++++++++++++++++-- 1 file changed, 28 insertions(+), 2 deletions(-) diff --git a/example-implementations/api/api-mockup/src/gapi_waveform.c b/example-implementations/api/api-mockup/src/gapi_waveform.c index 1557e8b..a481750 100644 --- a/example-implementations/api/api-mockup/src/gapi_waveform.c +++ b/example-implementations/api/api-mockup/src/gapi_waveform.c @@ -5,6 +5,29 @@ */ #include "gapi_waveform.h" +#include "gapi_discovery.h" + +static void api_platform_waveform_build_response(WaveformRsp *response, + int32_t instance_id, + bool activate) +{ + PlatformDiscoveryWaveform waveform_platform_info; + (void)activate; + + waveform_platform_info = get_waveform_info(); + if (waveform_platform_info.n_instances == 0) { + response->status = WAVEFORM__STATUS__WAVEFORM_ERR_NO_RESOURCES; + return; + } + + if (instance_id < 0 || + instance_id > (int32_t)waveform_platform_info.n_instances) { + response->status = WAVEFORM__STATUS__WAVEFORM_ERR_INVALID_ID; + return; + } + + response->status = WAVEFORM__STATUS__WAVEFORM_SUCCESS; +} static void api_waveform_req_handler(struct mosquitto *mosq, const char *topic, const int payloadlen, @@ -13,8 +36,10 @@ static void api_waveform_req_handler(struct mosquitto *mosq, const char *topic, WaveformReq *request = NULL; WaveformRsp response = WAVEFORM__RSP__INIT; + int32_t instance_id = 0; uint8_t *message = NULL; char *rsp_topic = NULL; + bool activate = false; char *app_id = NULL; app_id = basename((char *)topic); @@ -27,7 +52,8 @@ static void api_waveform_req_handler(struct mosquitto *mosq, const char *topic, "[Waveform] Error unpacking app manifest request\n"); return; } - + instance_id = request->instance_id; + activate = request->activate; waveform__req__free_unpacked(request, NULL); if (asprintf(&rsp_topic, "geisa/api/waveform-rsp/%s", app_id) == -1) { @@ -36,7 +62,7 @@ static void api_waveform_req_handler(struct mosquitto *mosq, const char *topic, return; } - response.status = WAVEFORM__STATUS__WAVEFORM_SUCCESS; + api_platform_waveform_build_response(&response, instance_id, activate); message = malloc(waveform__rsp__get_packed_size(&response)); if (message == NULL) { fprintf(stderr, From 5418119a1a49719eef140015f0afdca8c549630a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?K=C3=A9vin=20L=27h=C3=B4pital?= Date: Wed, 21 Jan 2026 17:08:19 +0100 Subject: [PATCH 28/64] gapi_discovery: replace malloc+strcat by asprintf MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit To simplify code and avoid potential memory issues, replace the malloc + strcpy + strcat sequence with a single asprintf call. SFL: #19176 Change-Id: Ia43d12882f3d5be3bf91545f38d057e9aef940c2 Signed-off-by: Kévin L'hôpital --- .../api/api-mockup/include/gapi_discovery.h | 1 + .../api/api-mockup/src/gapi_discovery.c | 10 ++-------- 2 files changed, 3 insertions(+), 8 deletions(-) diff --git a/example-implementations/api/api-mockup/include/gapi_discovery.h b/example-implementations/api/api-mockup/include/gapi_discovery.h index 41bb9db..99f4647 100644 --- a/example-implementations/api/api-mockup/include/gapi_discovery.h +++ b/example-implementations/api/api-mockup/include/gapi_discovery.h @@ -6,6 +6,7 @@ #ifndef GAPI_DISCOVERY_H #define GAPI_DISCOVERY_H +#define _GNU_SOURCE #include "gapi_mosquitto.h" #include "schemas/discovery.pb-c.h" diff --git a/example-implementations/api/api-mockup/src/gapi_discovery.c b/example-implementations/api/api-mockup/src/gapi_discovery.c index 12de279..73a8a11 100644 --- a/example-implementations/api/api-mockup/src/gapi_discovery.c +++ b/example-implementations/api/api-mockup/src/gapi_discovery.c @@ -177,18 +177,13 @@ static void api_manifest_req_handler(struct mosquitto *mosq, const char *topic, } application_manifest__req__free_unpacked(request, NULL); - rsp_topic = malloc(strlen("geisa/api/app-manifest-rsp/") + - strlen(app_id) + 1); - if (rsp_topic == NULL) { + if (asprintf(&rsp_topic, "geisa/api/app-manifest-rsp/%s", app_id) == + -1) { fprintf(stderr, "[Manifest] Error allocating memory for response " "topic\n"); return; } - // NOLINTNEXTLINE: strcpy is safe as enough memory allocate here - strcpy(rsp_topic, "geisa/api/app-manifest-rsp/"); - // NOLINTNEXTLINE: strcat is safe as enough memory allocate here - strcat(rsp_topic, app_id); response.manifest = deployment_manifest; message = malloc(application_manifest__rsp__get_packed_size(&response)); @@ -204,7 +199,6 @@ static void api_manifest_req_handler(struct mosquitto *mosq, const char *topic, application_manifest__rsp__get_packed_size(&response), message, 1); free(message); - free(rsp_topic); } void api_discovery_init(struct mosquitto *mosq) From 4eba2c2c5218a6c91eeaef682b59ce43777b86ea Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?K=C3=A9vin=20L=27h=C3=B4pital?= Date: Thu, 29 Jan 2026 09:58:46 +0100 Subject: [PATCH 29/64] .github: add a ci on github to validate the api-mockup MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit In the github CI run the linting and the build of the api-mockup project SFL: #19176 Change-Id: I189470dc38320c59a6a16751bcfd3a04737fb495 Signed-off-by: Kévin L'hôpital --- .../.github/workflows/ci-api-mockup.yml | 43 +++++++++++++++++++ 1 file changed, 43 insertions(+) create mode 100644 example-implementations/api/api-mockup/.github/workflows/ci-api-mockup.yml diff --git a/example-implementations/api/api-mockup/.github/workflows/ci-api-mockup.yml b/example-implementations/api/api-mockup/.github/workflows/ci-api-mockup.yml new file mode 100644 index 0000000..4ab335b --- /dev/null +++ b/example-implementations/api/api-mockup/.github/workflows/ci-api-mockup.yml @@ -0,0 +1,43 @@ +# Copyright (C) 2026 Southern California Edison. +# SPDX-License-Identifier: Apache-2.0 + +name: CI API-mockup + +on: + push: + branches: [main] + pull_request_target: + +jobs: + linting: + runs-on: ubuntu-latest + steps: + - name: Checkout repository + uses: actions/checkout@v4 + with: + ref: ${{ github.event.pull_request.head.sha || github.sha }} + submodules: true + - name: Install CQFD and create docker image + run: | + curl -LO https://github.com/savoirfairelinux/cqfd/releases/download/v5.8.0/cqfd_5.8.0_all.deb + sudo dpkg -i ./cqfd_5.8.0_all.deb + cqfd init + - name: Run linting + run: | + cqfd -b lint + build: + runs-on: ubuntu-latest + steps: + - name: Checkout repository + uses: actions/checkout@v4 + with: + ref: ${{ github.event.pull_request.head.sha || github.sha }} + submodules: true + - name: Install CQFD and create docker image + run: | + curl -LO https://github.com/savoirfairelinux/cqfd/releases/download/v5.8.0/cqfd_5.8.0_all.deb + sudo dpkg -i ./cqfd_5.8.0_all.deb + cqfd init + - name: Build application + run: | + cqfd run From e3d7e08a585222b7e645bd63ad16ce771cbe33bf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?K=C3=A9vin=20L=27h=C3=B4pital?= Date: Thu, 29 Jan 2026 14:44:49 +0100 Subject: [PATCH 30/64] ci-api-mockup: fix indentation MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The identation was incorrect for the linting step in the CI workflow file, causing the linting job to fail. SFL: #19176 Change-Id: I482e97a45a0f90dc1ba773f713c2749b72735c3a Signed-off-by: Kévin L'hôpital --- .../api/api-mockup/.github/workflows/ci-api-mockup.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/example-implementations/api/api-mockup/.github/workflows/ci-api-mockup.yml b/example-implementations/api/api-mockup/.github/workflows/ci-api-mockup.yml index 4ab335b..17f2bbb 100644 --- a/example-implementations/api/api-mockup/.github/workflows/ci-api-mockup.yml +++ b/example-implementations/api/api-mockup/.github/workflows/ci-api-mockup.yml @@ -24,7 +24,7 @@ jobs: cqfd init - name: Run linting run: | - cqfd -b lint + cqfd -b lint build: runs-on: ubuntu-latest steps: From eb21ee37ef3628dee0a7d1267f49de85a215faa1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?K=C3=A9vin=20L=27h=C3=B4pital?= Date: Thu, 29 Jan 2026 14:52:43 +0100 Subject: [PATCH 31/64] ci-api-mockup: build the application before linting MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The linting job is failing because the application is not built before running the linting step. The build is creating schmas source and header files that are required for the linting process. SFL: #19176 Change-Id: I1c24429ee3b2b31f75000598ada60674d70a0d16 Signed-off-by: Kévin L'hôpital --- .../.github/workflows/ci-api-mockup.yml | 23 ++++--------------- 1 file changed, 5 insertions(+), 18 deletions(-) diff --git a/example-implementations/api/api-mockup/.github/workflows/ci-api-mockup.yml b/example-implementations/api/api-mockup/.github/workflows/ci-api-mockup.yml index 17f2bbb..e60d75e 100644 --- a/example-implementations/api/api-mockup/.github/workflows/ci-api-mockup.yml +++ b/example-implementations/api/api-mockup/.github/workflows/ci-api-mockup.yml @@ -9,23 +9,7 @@ on: pull_request_target: jobs: - linting: - runs-on: ubuntu-latest - steps: - - name: Checkout repository - uses: actions/checkout@v4 - with: - ref: ${{ github.event.pull_request.head.sha || github.sha }} - submodules: true - - name: Install CQFD and create docker image - run: | - curl -LO https://github.com/savoirfairelinux/cqfd/releases/download/v5.8.0/cqfd_5.8.0_all.deb - sudo dpkg -i ./cqfd_5.8.0_all.deb - cqfd init - - name: Run linting - run: | - cqfd -b lint - build: + build_and_linting: runs-on: ubuntu-latest steps: - name: Checkout repository @@ -40,4 +24,7 @@ jobs: cqfd init - name: Build application run: | - cqfd run + cqfd run + - name: Run linting + run: + cqfd -b lint From 05c67a72f4b3e11fa9c994e7a4712573fd3089a7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?K=C3=A9vin=20L=27h=C3=B4pital?= Date: Thu, 29 Jan 2026 17:14:28 +0100 Subject: [PATCH 32/64] gitmodules: change submodule URL to use HTTPS MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Yocto is complaining ssh reference in .gitmodules. Then replace it with HTTPS. SFL: #21327 Change-Id: I933cbf23ac465cd9c2b052e1a0517715f6f26923 Signed-off-by: Kévin L'hôpital --- example-implementations/api/api-mockup/.gitmodules | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/example-implementations/api/api-mockup/.gitmodules b/example-implementations/api/api-mockup/.gitmodules index 3384226..4bdad91 100644 --- a/example-implementations/api/api-mockup/.gitmodules +++ b/example-implementations/api/api-mockup/.gitmodules @@ -1,3 +1,3 @@ [submodule "schemas"] path = schemas - url = git@github.com:geisa/schemas.git + url = https://github.com/geisa/schemas.git From edddd76c4bcb7eea724be9ec1b4a290c2891eb56 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?K=C3=A9vin=20L=27h=C3=B4pital?= Date: Mon, 30 Mar 2026 09:49:20 +0200 Subject: [PATCH 33/64] src: add fflush to ensure they are printed immediately MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When launching the application with a systemd service, the output of the application is not printed immediately, but rather buffered until the buffer is full or the application exits. To ensure that the output is printed immediately, add fflush calls after each fprintf call. SFL: #21906 Change-Id: Ie2d0095471c1cf05e759df814ce3b8a4456e17a6 Signed-off-by: Kévin L'hôpital --- example-implementations/api/api-mockup/src/gapi_discovery.c | 2 ++ example-implementations/api/api-mockup/src/gapi_mosquitto.c | 2 ++ example-implementations/api/api-mockup/src/gapi_waveform.c | 1 + 3 files changed, 5 insertions(+) diff --git a/example-implementations/api/api-mockup/src/gapi_discovery.c b/example-implementations/api/api-mockup/src/gapi_discovery.c index 73a8a11..94cf6dc 100644 --- a/example-implementations/api/api-mockup/src/gapi_discovery.c +++ b/example-implementations/api/api-mockup/src/gapi_discovery.c @@ -133,6 +133,7 @@ static void api_platform_discovery_req_handler(struct mosquitto *mosq, (void)topic; fprintf(stdout, "[Discovery] Received platform discovery request\n"); + fflush(stdout); request = platform_discovery__req__unpack(NULL, payloadlen, payload); if (request == NULL) { fprintf(stderr, "[Discovery] Error unpacking platform " @@ -169,6 +170,7 @@ static void api_manifest_req_handler(struct mosquitto *mosq, const char *topic, fprintf(stdout, "[Manifest] Received app manifest request from %s\n", app_id); + fflush(stdout); request = application_manifest__req__unpack(NULL, payloadlen, payload); if (request == NULL) { fprintf(stderr, diff --git a/example-implementations/api/api-mockup/src/gapi_mosquitto.c b/example-implementations/api/api-mockup/src/gapi_mosquitto.c index 5043095..3c81b8b 100644 --- a/example-implementations/api/api-mockup/src/gapi_mosquitto.c +++ b/example-implementations/api/api-mockup/src/gapi_mosquitto.c @@ -30,6 +30,7 @@ static void on_connect(struct mosquitto *mosq, void *obj, int return_code) (void)obj; if (return_code == 0) { fprintf(stdout, "[connected] OK\n"); + fflush(stdout); isConnected = true; } else { fprintf(stderr, "[connect] failed, return_code=%d\n", @@ -44,6 +45,7 @@ static void on_disconnect(struct mosquitto *mosq, void *obj, int return_code) (void)obj; (void)mosq; fprintf(stdout, "[disconnected] return_code=%d\n", return_code); + fflush(stdout); isConnected = false; } diff --git a/example-implementations/api/api-mockup/src/gapi_waveform.c b/example-implementations/api/api-mockup/src/gapi_waveform.c index a481750..8ed460e 100644 --- a/example-implementations/api/api-mockup/src/gapi_waveform.c +++ b/example-implementations/api/api-mockup/src/gapi_waveform.c @@ -46,6 +46,7 @@ static void api_waveform_req_handler(struct mosquitto *mosq, const char *topic, fprintf(stdout, "[Waveform] Received waveform data request from %s\n", app_id); + fflush(stdout); request = waveform__req__unpack(NULL, payloadlen, payload); if (request == NULL) { fprintf(stderr, From d614d65c8d429ef03b182725ca5804ff34868686 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?K=C3=A9vin=20L=27h=C3=B4pital?= Date: Mon, 30 Mar 2026 16:54:13 +0200 Subject: [PATCH 34/64] api-mockup: update schema and MQTT topic structure MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit With the last GEISA schema version, the schemas were updated to include the "geisa" prefix in all message types and enums. Additionally, the MQTT topic structure was updated, replacing - with /, adding appid in discovery topics and rename app status topic This commit updates the code to reflect these changes in the schema and MQTT topics. SFL: #21906 Change-Id: If8a4e6fdc700f0465eeb442fb18d5235b2747224 Signed-off-by: Kévin L'hôpital --- .../api/api-mockup/include/gapi_discovery.h | 2 +- .../api/api-mockup/schemas | 2 +- .../api/api-mockup/src/gapi_discovery.c | 232 +++++++++++++----- .../api/api-mockup/src/gapi_instantaneous.c | 59 ++--- .../api/api-mockup/src/gapi_waveform.c | 35 +-- 5 files changed, 221 insertions(+), 109 deletions(-) diff --git a/example-implementations/api/api-mockup/include/gapi_discovery.h b/example-implementations/api/api-mockup/include/gapi_discovery.h index 99f4647..f006d9b 100644 --- a/example-implementations/api/api-mockup/include/gapi_discovery.h +++ b/example-implementations/api/api-mockup/include/gapi_discovery.h @@ -24,6 +24,6 @@ void api_discovery_init(struct mosquitto *mosq); * * @return Pointer to PlatformDiscoveryWaveform structure */ -PlatformDiscoveryWaveform get_waveform_info(); +GeisaPlatformDiscoveryWaveform get_waveform_info(); #endif // GAPI_DISCOVERY_H diff --git a/example-implementations/api/api-mockup/schemas b/example-implementations/api/api-mockup/schemas index 88b36cf..3b62cb1 160000 --- a/example-implementations/api/api-mockup/schemas +++ b/example-implementations/api/api-mockup/schemas @@ -1 +1 @@ -Subproject commit 88b36cf065a1b84503a4516af38bbfa509da5074 +Subproject commit 3b62cb1d1f1e40438685286cf446a45da1de60f9 diff --git a/example-implementations/api/api-mockup/src/gapi_discovery.c b/example-implementations/api/api-mockup/src/gapi_discovery.c index 94cf6dc..61ff120 100644 --- a/example-implementations/api/api-mockup/src/gapi_discovery.c +++ b/example-implementations/api/api-mockup/src/gapi_discovery.c @@ -7,23 +7,24 @@ #include "gapi_discovery.h" // NOLINTBEGIN(cppcoreguidelines-interfaces-global-init,cppcoreguidelines-avoid-magic-numbers,readability-magic-numbers) -static PlatformDiscoveryGEISA geisa_platform_info = { - .base = PROTOBUF_C_MESSAGE_INIT(&platform_discovery__geisa__descriptor), +static GeisaPlatformDiscoveryGEISA geisa_platform_info = { + .base = PROTOBUF_C_MESSAGE_INIT( + &geisa_platform_discovery__geisa__descriptor), .ver_major = 0, .ver_minor = 1, .ver_rev = 7, - .piller_adm = true, - .piller_api = true, - .piller_lee = true, - .piller_vee = false, + .pillar_adm = true, + .pillar_api = true, + .pillar_lee = true, + .pillar_vee = false, }; -static const PlatformDiscoveryType top_type_platform_info = - PLATFORM_DISCOVERY__TYPE__DEVICE_TYPE_ELECTRIC_METER; +static const GeisaPlatformDiscoveryType top_type_platform_info = + GEISA_PLATFORM_DISCOVERY__TYPE__DEVICE_TYPE_ELECTRIC_METER; -static PlatformDiscoveryModule top_module_platform_info = { +static GeisaPlatformDiscoveryModule top_module_platform_info = { .base = PROTOBUF_C_MESSAGE_INIT( - &platform_discovery__module__descriptor), + &geisa_platform_discovery__module__descriptor), .type = top_type_platform_info, .manufacturer = "SCE", .model = "GEISA-1", @@ -32,12 +33,12 @@ static PlatformDiscoveryModule top_module_platform_info = { .fw_revision = "1.0.0", }; -static const PlatformDiscoveryType sub_type_platform_info = - PLATFORM_DISCOVERY__TYPE__DEVICE_TYPE_METROLOGY_PROCESSOR; +static const GeisaPlatformDiscoveryType sub_type_platform_info = + GEISA_PLATFORM_DISCOVERY__TYPE__DEVICE_TYPE_METROLOGY_PROCESSOR; -static PlatformDiscoveryModule sub_module1_platform_info = { +static GeisaPlatformDiscoveryModule sub_module1_platform_info = { .base = PROTOBUF_C_MESSAGE_INIT( - &platform_discovery__module__descriptor), + &geisa_platform_discovery__module__descriptor), .type = sub_type_platform_info, .manufacturer = "SCE", .model = "GEISA-2", @@ -46,32 +47,37 @@ static PlatformDiscoveryModule sub_module1_platform_info = { .fw_revision = "1.1.0", }; -static PlatformDiscoveryModule *sub_modules_platform_info[] = { +static GeisaPlatformDiscoveryModule *sub_modules_platform_info[] = { &sub_module1_platform_info}; -static PlatformDiscoveryDevice device_platform_info = { +static GeisaPlatformDiscoveryDevice device_platform_info = { .base = PROTOBUF_C_MESSAGE_INIT( - &platform_discovery__device__descriptor), + &geisa_platform_discovery__device__descriptor), .top_module = &top_module_platform_info, .n_sub_module = 1, .sub_module = sub_modules_platform_info, }; -static PlatformDiscoveryOperator operator_platform_info = - PLATFORM_DISCOVERY__OPERATOR__INIT; -static PlatformDiscoveryMetrology metrology_platform_info = - PLATFORM_DISCOVERY__METROLOGY__INIT; -static PlatformDiscoverySensor sensor_platform_info = - PLATFORM_DISCOVERY__SENSOR__INIT; -static PlatformDiscoveryNetwork network_platform_info = - PLATFORM_DISCOVERY__NETWORK__INIT; +static GeisaPlatformDiscoveryOperator operator_platform_info = { + .base = PROTOBUF_C_MESSAGE_INIT( + &geisa_platform_discovery__operator__descriptor), + .operator_name = "SCE", + .operator_identifier = "SCE001", +}; + +static GeisaPlatformDiscoveryMetrology metrology_platform_info = + GEISA_PLATFORM_DISCOVERY__METROLOGY__INIT; +static GeisaPlatformDiscoverySensor sensor_platform_info = + GEISA_PLATFORM_DISCOVERY__SENSOR__INIT; +static GeisaPlatformDiscoveryNetwork network_platform_info = + GEISA_PLATFORM_DISCOVERY__NETWORK__INIT; -static const WaveformDatatype waveform_data_type_platform_info = - WAVEFORM__DATATYPE__DATA_INT32; +static const GeisaWaveformDatatype waveform_data_type_platform_info = + GEISA_WAVEFORM__DATATYPE__DATA_INT32; -static PlatformDiscoveryWaveformInstance waveform_platform_instance1_info = { +static GeisaPlatformDiscoveryWaveformInstance waveform_platform_instance1_info = { .base = PROTOBUF_C_MESSAGE_INIT( - &platform_discovery__waveform__instance__descriptor), + &geisa_platform_discovery__waveform__instance__descriptor), .instance_id = 1, .data_connection = "/data/waveform/instance1.socket", .datatype = waveform_data_type_platform_info, @@ -90,26 +96,99 @@ static PlatformDiscoveryWaveformInstance waveform_platform_instance1_info = { .current_filter_highpass = 0, }; -static PlatformDiscoveryWaveformInstance *waveform_platform_instances[] = { +static GeisaPlatformDiscoveryWaveformInstance *waveform_platform_instances[] = { &waveform_platform_instance1_info, }; -static PlatformDiscoveryWaveform waveform_platform_info = { - PROTOBUF_C_MESSAGE_INIT(&platform_discovery__waveform__descriptor), +static GeisaPlatformDiscoveryWaveform waveform_platform_info = { + PROTOBUF_C_MESSAGE_INIT( + &geisa_platform_discovery__waveform__descriptor), .n_instances = 1, .instances = waveform_platform_instances, }; -uint8_t deployment_manifest_data[] = {0x0A, 0x0B, 0x0C, 0x0D}; -// NOLINTEND(cppcoreguidelines-interfaces-global-init,cppcoreguidelines-avoid-magic-numbers,readability-magic-numbers) +char *deployment_manifest = + "{" + "\"geisa-application-manifest\":{" + "\"manifest\":{" + "\"app-id\":\"com.example.sce.app\"," + "\"author\":\"SCE\"," + "\"name\":\"API-mockup example\"," + "\"description\":\"API-mockup example\"," + "\"app-version\":\"1.2.3-beta\"," + "\"manifest-version\":\"1.0.0\"," -static ProtobufCBinaryData deployment_manifest = { - .len = sizeof(deployment_manifest_data), - .data = deployment_manifest_data, -}; + "\"api-access\":{" + "\"actuator\":true," + "\"messaging\":true," + "\"instantaneous\":false," + "\"sensor\":true," + "\"waveform\":false" + "}," + + "\"artifacts\":[{" + "\"image-name\":\"api_mockup_v1.2.3.img\"," + "\"image-type\":\"appoverlay\"," + "\"image-size\":1048576," + "\"uncompressed-size\":5242880," + "\"signature\":" + "\"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\"" + "}]," + + "\"compatibility\":{" + "\"GEISA-API\":\"1.0.0\"," + "\"GEISA-LEE\":\"1.0.0\"," + "\"GEISA-VEE\":null," + "\"toolchain-id\":\"acme-toolchain-armv7\"," + "\"toolchain-version\":\"2.3.4\"" + "}," + + "\"resources\":{" + "\"app-cpu\":10," + "\"app-ram\":65536," + "\"storage-persistent\":10240," + "\"storage-nonpersistent\":2048," + "\"threads\":2" + "}," + + "\"communication\":{" + "\"FAN\":true," + "\"HAN\":false," + "\"messaging\":{\"daily-messages\":5000}" + "}," + + "\"external-dependencies\":[" + "\"com.example.database\"," + "\"com.example.security\"" + "]," + + "\"default-launch-strategy\":{" + "\"auto-restart\":true," + "\"max-restarts\":5," + "\"restart-period\":60," + "\"start-timeout\":10," + "\"start-background\":true," + "\"start-string\":\"/usr/bin/start-app\"," + "\"stop-string\":\"/usr/bin/stop-app\"," + "\"stop-timeout\":10," + "\"notify-timeout\":30," + "\"watchdog\":true" + "}," + + "\"default-configuration\":{" + "\"mode\":\"production\"," + "\"message\":\"Hello World\"" + "}" + "}," + "\"signature\":" + "\"bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb\"" + "}" + "}"; + +// NOLINTEND(cppcoreguidelines-interfaces-global-init,cppcoreguidelines-avoid-magic-numbers,readability-magic-numbers) static void -api_platform_discovery_build_response(PlatformDiscoveryRsp *response) +api_platform_discovery_build_response(GeisaPlatformDiscoveryRsp *response) { response->geisa = &geisa_platform_info; response->device = &device_platform_info; @@ -120,39 +199,61 @@ api_platform_discovery_build_response(PlatformDiscoveryRsp *response) response->waveform = &waveform_platform_info; } -PlatformDiscoveryWaveform get_waveform_info() { return waveform_platform_info; } +GeisaPlatformDiscoveryWaveform get_waveform_info() +{ + return waveform_platform_info; +} static void api_platform_discovery_req_handler(struct mosquitto *mosq, const char *topic, const int payloadlen, const uint8_t *payload) { - PlatformDiscoveryReq *request = NULL; - PlatformDiscoveryRsp response = PLATFORM_DISCOVERY__RSP__INIT; + GeisaPlatformDiscoveryReq *request = NULL; + GeisaPlatformDiscoveryRsp response = + GEISA_PLATFORM_DISCOVERY__RSP__INIT; uint8_t *message = NULL; - (void)topic; + char *rsp_topic = NULL; + char *app_id = NULL; - fprintf(stdout, "[Discovery] Received platform discovery request\n"); + app_id = basename((char *)topic); + + fprintf(stdout, + "[Discovery] Received platform discovery request from %s\n", + app_id); fflush(stdout); - request = platform_discovery__req__unpack(NULL, payloadlen, payload); + request = geisa_platform_discovery__req__unpack(NULL, payloadlen, + payload); if (request == NULL) { fprintf(stderr, "[Discovery] Error unpacking platform " "discovery request\n"); return; } - platform_discovery__req__free_unpacked(request, NULL); + geisa_platform_discovery__req__free_unpacked(request, NULL); api_platform_discovery_build_response(&response); - message = malloc(platform_discovery__rsp__get_packed_size(&response)); + message = malloc( + geisa_platform_discovery__rsp__get_packed_size(&response)); if (message == NULL) { fprintf(stderr, "[Discovery] Error allocating memory for " "platform discovery response\n"); return; } - platform_discovery__rsp__pack(&response, message); - api_publish(mosq, "geisa/api/platform-discovery-rsp", - platform_discovery__rsp__get_packed_size(&response), + + if (asprintf(&rsp_topic, "geisa/api/platform/discovery/rsp/%s", + app_id) == -1) { + fprintf(stderr, + "[Discovery] Error allocating memory for response " + "topic\n"); + free(message); + return; + } + + geisa_platform_discovery__rsp__pack(&response, message); + api_publish(mosq, rsp_topic, + geisa_platform_discovery__rsp__get_packed_size(&response), message, 1); + free(rsp_topic); free(message); } @@ -160,8 +261,9 @@ static void api_manifest_req_handler(struct mosquitto *mosq, const char *topic, const int payloadlen, const uint8_t *payload) { - ApplicationManifestReq *request = NULL; - ApplicationManifestRsp response = APPLICATION_MANIFEST__RSP__INIT; + GeisaApplicationDeploymentManifestReq *request = NULL; + GeisaApplicationDeploymentManifestRsp response = + GEISA_APPLICATION_DEPLOYMENT_MANIFEST__RSP__INIT; uint8_t *message = NULL; char *rsp_topic = NULL; char *app_id = NULL; @@ -171,15 +273,17 @@ static void api_manifest_req_handler(struct mosquitto *mosq, const char *topic, fprintf(stdout, "[Manifest] Received app manifest request from %s\n", app_id); fflush(stdout); - request = application_manifest__req__unpack(NULL, payloadlen, payload); + request = geisa_application_deployment_manifest__req__unpack( + NULL, payloadlen, payload); if (request == NULL) { fprintf(stderr, "[Manifest] Error unpacking app manifest request\n"); return; } - application_manifest__req__free_unpacked(request, NULL); + geisa_application_deployment_manifest__req__free_unpacked(request, + NULL); - if (asprintf(&rsp_topic, "geisa/api/app-manifest-rsp/%s", app_id) == + if (asprintf(&rsp_topic, "geisa/api/app/manifest/rsp/%s", app_id) == -1) { fprintf(stderr, "[Manifest] Error allocating memory for response " @@ -188,7 +292,9 @@ static void api_manifest_req_handler(struct mosquitto *mosq, const char *topic, } response.manifest = deployment_manifest; - message = malloc(application_manifest__rsp__get_packed_size(&response)); + message = malloc( + geisa_application_deployment_manifest__rsp__get_packed_size( + &response)); if (message == NULL) { fprintf(stderr, "[Manifest] Error allocating memory for response " @@ -196,20 +302,22 @@ static void api_manifest_req_handler(struct mosquitto *mosq, const char *topic, free(rsp_topic); return; } - application_manifest__rsp__pack(&response, message); + geisa_application_deployment_manifest__rsp__pack(&response, message); api_publish(mosq, rsp_topic, - application_manifest__rsp__get_packed_size(&response), + geisa_application_deployment_manifest__rsp__get_packed_size( + &response), message, 1); + free(rsp_topic); free(message); } void api_discovery_init(struct mosquitto *mosq) { - api_register_handler("geisa/api/app-manifest-req", + api_register_handler("geisa/api/app/manifest/req", api_manifest_req_handler); - api_register_handler("geisa/api/platform-discovery-req", + api_register_handler("geisa/api/platform/discovery/req", api_platform_discovery_req_handler); - api_subscribe(mosq, "geisa/api/platform-discovery-req", 1); - api_subscribe(mosq, "geisa/api/app-manifest-req/#", 1); + api_subscribe(mosq, "geisa/api/platform/discovery/req/#", 1); + api_subscribe(mosq, "geisa/api/app/manifest/req/#", 1); } diff --git a/example-implementations/api/api-mockup/src/gapi_instantaneous.c b/example-implementations/api/api-mockup/src/gapi_instantaneous.c index 60adcfe..9a2e878 100644 --- a/example-implementations/api/api-mockup/src/gapi_instantaneous.c +++ b/example-implementations/api/api-mockup/src/gapi_instantaneous.c @@ -7,28 +7,28 @@ #include "gapi_instantaneous.h" static void -geisa_get_instantaneous_data(InstantaneousQuantities *response, - TypeInstantaneousQuantitiesPerPhase *phase_a, - TypeInstantaneousQuantitiesPerPhase *phase_b, - TypeInstantaneousQuantitiesPerPhase *phase_c, - TypeInstantaneousQuantitiesPerPhase *phase_n, - TypeInstantaneousQuantitiesOther *other) +geisa_get_instantaneous_data(GeisaInstantaneousQuantities *response, + GeisaTypeInstantaneousQuantitiesPerPhase *phase_a, + GeisaTypeInstantaneousQuantitiesPerPhase *phase_b, + GeisaTypeInstantaneousQuantitiesPerPhase *phase_c, + GeisaTypeInstantaneousQuantitiesPerPhase *phase_n, + GeisaTypeInstantaneousQuantitiesOther *other) { // This function should interact with the GEISA system to retrieve // instantaneous data. Here we provide a mock implementation. - instantaneous_quantities__init(response); - type_instantaneous_quantities__per_phase__init(phase_a); - type_instantaneous_quantities__per_phase__init(phase_b); - type_instantaneous_quantities__per_phase__init(phase_c); - type_instantaneous_quantities__per_phase__init(phase_n); - type_instantaneous_quantities__other__init(other); + geisa_instantaneous_quantities__init(response); + geisa_type_instantaneous_quantities__per_phase__init(phase_a); + geisa_type_instantaneous_quantities__per_phase__init(phase_b); + geisa_type_instantaneous_quantities__per_phase__init(phase_c); + geisa_type_instantaneous_quantities__per_phase__init(phase_n); + geisa_type_instantaneous_quantities__other__init(other); time_t timestamp = time(NULL) * SEC_IN_MS; // NOLINTBEGIN(readability-magic-numbers,cppcoreguidelines-avoid-magic-numbers) // Mock data assignment (*phase_a).message_version = 1; - (*phase_a).phase = TYPE_PHASE__PHASE_A; + (*phase_a).phase = GEISA_TYPE_PHASE__PHASE_A; (*phase_a).microamps = 10.0F; (*phase_a).microvolts = 120.0F; (*phase_a).microw = 1200.0F; @@ -47,7 +47,7 @@ geisa_get_instantaneous_data(InstantaneousQuantities *response, (*phase_a).phase_voltage_percentage_2nd_harmonic = 1.0F; (*phase_b).message_version = 1; - (*phase_b).phase = TYPE_PHASE__PHASE_B; + (*phase_b).phase = GEISA_TYPE_PHASE__PHASE_B; (*phase_b).microamps = 11.0F; (*phase_b).microvolts = 121.0F; (*phase_b).microw = 1210.0F; @@ -66,7 +66,7 @@ geisa_get_instantaneous_data(InstantaneousQuantities *response, (*phase_b).phase_voltage_percentage_2nd_harmonic = 1.1F; (*phase_c).message_version = 1; - (*phase_c).phase = TYPE_PHASE__PHASE_C; + (*phase_c).phase = GEISA_TYPE_PHASE__PHASE_C; (*phase_c).microamps = 12.0F; (*phase_c).microvolts = 122.0F; (*phase_c).microw = 1220.0F; @@ -85,7 +85,7 @@ geisa_get_instantaneous_data(InstantaneousQuantities *response, (*phase_c).phase_voltage_percentage_2nd_harmonic = 1.2F; (*phase_n).message_version = 1; - (*phase_n).phase = TYPE_PHASE__PHASE_N; + (*phase_n).phase = GEISA_TYPE_PHASE__PHASE_N; (*phase_n).microamps = 5.0F; (*phase_n).microvolts = 60.0F; (*phase_n).microw = 600.0F; @@ -121,30 +121,31 @@ geisa_get_instantaneous_data(InstantaneousQuantities *response, static void *gapi_instantaneous_thread(void *arg) { struct mosquitto *mosq = (struct mosquitto *)arg; - TypeInstantaneousQuantitiesPerPhase phase_a; - TypeInstantaneousQuantitiesPerPhase phase_b; - TypeInstantaneousQuantitiesPerPhase phase_c; - TypeInstantaneousQuantitiesPerPhase phase_n; - TypeInstantaneousQuantitiesOther other; - InstantaneousQuantities response; + GeisaTypeInstantaneousQuantitiesPerPhase phase_a; + GeisaTypeInstantaneousQuantitiesPerPhase phase_b; + GeisaTypeInstantaneousQuantitiesPerPhase phase_c; + GeisaTypeInstantaneousQuantitiesPerPhase phase_n; + GeisaTypeInstantaneousQuantitiesOther other; + GeisaInstantaneousQuantities response; uint8_t *message = NULL; while (running) { geisa_get_instantaneous_data(&response, &phase_a, &phase_b, &phase_c, &phase_n, &other); - message = malloc( - instantaneous_quantities__get_packed_size(&response)); + message = + malloc(geisa_instantaneous_quantities__get_packed_size( + &response)); if (message == NULL) { fprintf(stderr, "[Instantaneous] Failed to allocate " "memory for instantaneous data\n"); sleep(1); continue; } - instantaneous_quantities__pack(&response, message); - api_publish( - mosq, "geisa/api/instantaneous-data", - instantaneous_quantities__get_packed_size(&response), - message, 0); + geisa_instantaneous_quantities__pack(&response, message); + api_publish(mosq, "geisa/api/instantaneous/data", + geisa_instantaneous_quantities__get_packed_size( + &response), + message, 0); free(message); sleep(1); } diff --git a/example-implementations/api/api-mockup/src/gapi_waveform.c b/example-implementations/api/api-mockup/src/gapi_waveform.c index 8ed460e..870b457 100644 --- a/example-implementations/api/api-mockup/src/gapi_waveform.c +++ b/example-implementations/api/api-mockup/src/gapi_waveform.c @@ -7,34 +7,36 @@ #include "gapi_waveform.h" #include "gapi_discovery.h" -static void api_platform_waveform_build_response(WaveformRsp *response, +static void api_platform_waveform_build_response(GeisaWaveformRsp *response, int32_t instance_id, bool activate) { - PlatformDiscoveryWaveform waveform_platform_info; + GeisaPlatformDiscoveryWaveform waveform_platform_info; (void)activate; waveform_platform_info = get_waveform_info(); if (waveform_platform_info.n_instances == 0) { - response->status = WAVEFORM__STATUS__WAVEFORM_ERR_NO_RESOURCES; + response->status = + GEISA_WAVEFORM__STATUS__WAVEFORM_ERR_NO_RESOURCES; return; } if (instance_id < 0 || instance_id > (int32_t)waveform_platform_info.n_instances) { - response->status = WAVEFORM__STATUS__WAVEFORM_ERR_INVALID_ID; + response->status = + GEISA_WAVEFORM__STATUS__WAVEFORM_ERR_INVALID_ID; return; } - response->status = WAVEFORM__STATUS__WAVEFORM_SUCCESS; + response->status = GEISA_WAVEFORM__STATUS__WAVEFORM_SUCCESS; } static void api_waveform_req_handler(struct mosquitto *mosq, const char *topic, const int payloadlen, const uint8_t *payload) { - WaveformReq *request = NULL; - WaveformRsp response = WAVEFORM__RSP__INIT; + GeisaWaveformReq *request = NULL; + GeisaWaveformRsp response = GEISA_WAVEFORM__RSP__INIT; int32_t instance_id = 0; uint8_t *message = NULL; @@ -47,7 +49,7 @@ static void api_waveform_req_handler(struct mosquitto *mosq, const char *topic, fprintf(stdout, "[Waveform] Received waveform data request from %s\n", app_id); fflush(stdout); - request = waveform__req__unpack(NULL, payloadlen, payload); + request = geisa_waveform__req__unpack(NULL, payloadlen, payload); if (request == NULL) { fprintf(stderr, "[Waveform] Error unpacking app manifest request\n"); @@ -55,16 +57,16 @@ static void api_waveform_req_handler(struct mosquitto *mosq, const char *topic, } instance_id = request->instance_id; activate = request->activate; - waveform__req__free_unpacked(request, NULL); + geisa_waveform__req__free_unpacked(request, NULL); - if (asprintf(&rsp_topic, "geisa/api/waveform-rsp/%s", app_id) == -1) { + if (asprintf(&rsp_topic, "geisa/api/waveform/rsp/%s", app_id) == -1) { fprintf(stderr, "[Waveform] Error allocating memory for " "response topic\n"); return; } api_platform_waveform_build_response(&response, instance_id, activate); - message = malloc(waveform__rsp__get_packed_size(&response)); + message = malloc(geisa_waveform__rsp__get_packed_size(&response)); if (message == NULL) { fprintf(stderr, "[Waveform] Error allocating memory for response " @@ -72,16 +74,17 @@ static void api_waveform_req_handler(struct mosquitto *mosq, const char *topic, free(rsp_topic); return; } - waveform__rsp__pack(&response, message); - api_publish(mosq, rsp_topic, waveform__rsp__get_packed_size(&response), - message, 1); + geisa_waveform__rsp__pack(&response, message); + api_publish(mosq, rsp_topic, + geisa_waveform__rsp__get_packed_size(&response), message, + 1); free(message); } void api_waveform_init(struct mosquitto *mosq) { - api_register_handler("geisa/api/waveform-req", + api_register_handler("geisa/api/waveform/req", api_waveform_req_handler); - api_subscribe(mosq, "geisa/api/waveform-req/#", 1); + api_subscribe(mosq, "geisa/api/waveform/req/#", 1); } From 9f612b49c64dfdc8d9f76bd5698168fec6fbe139 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?K=C3=A9vin=20L=27h=C3=B4pital?= Date: Fri, 17 Apr 2026 10:28:07 +0200 Subject: [PATCH 35/64] .cqfd/Dockerfile: replace the default mirror to avoid timeouts As the default mirror for Ubuntu is currently experiencing timeouts, this commit replaces it with the French mirror to ensure that the Docker image can be built successfully. This change is temporary and will be removed once the issue with the default mirror is resolved. SFL: #21905 Change-Id: If0193b90cf21c501ddd36aa0b039d19c293a573e --- .../api/api-mockup/.cqfd/docker/Dockerfile | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/example-implementations/api/api-mockup/.cqfd/docker/Dockerfile b/example-implementations/api/api-mockup/.cqfd/docker/Dockerfile index f8b6ef2..1565b62 100644 --- a/example-implementations/api/api-mockup/.cqfd/docker/Dockerfile +++ b/example-implementations/api/api-mockup/.cqfd/docker/Dockerfile @@ -4,6 +4,12 @@ FROM ubuntu:24.04 ENV DEBIAN_FRONTEND=noninteractive +#To be removed when the issue with the default mirror is resolved. +RUN sed -i \ + -e 's|http://archive.ubuntu.com/ubuntu|http://fr.archive.ubuntu.com/ubuntu|g' \ + -e 's|http://security.ubuntu.com/ubuntu|http://fr.archive.ubuntu.com/ubuntu|g' \ + /etc/apt/sources.list.d/ubuntu.sources + RUN set -x \ && apt-get update \ && apt-get install -y --no-install-recommends \ From 1a29d462a5055d1a00e0d00b2b74da5253ae88a7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?K=C3=A9vin=20L=27h=C3=B4pital?= Date: Thu, 16 Apr 2026 16:39:32 +0200 Subject: [PATCH 36/64] gapi: handle disconnection and reconnection to broker It could happen that the connection to the broker is lost, for example if the broker is restarted. In this case, the client should try to reconnect to the broker. This commit adds a check for disconnection and tries to reconnect to the broker if the connection is lost. If the reconnection fails, the client will exit with an error message. SFL: #21905 Change-Id: Id4e0b420dbed47c5b1b59a9047d606cd4cac4896 --- example-implementations/api/api-mockup/gapi.c | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/example-implementations/api/api-mockup/gapi.c b/example-implementations/api/api-mockup/gapi.c index 12dfada..b1f2fa2 100644 --- a/example-implementations/api/api-mockup/gapi.c +++ b/example-implementations/api/api-mockup/gapi.c @@ -40,6 +40,16 @@ int main() while (running) { mosquitto_loop(mosq, -1, 1); sleep(1); + if (!isConnected) { + printf("Disconnected from broker, try reconnecing\n"); + ret = mosquitto_reconnect(mosq); + sleep(2); + if (ret != MOSQ_ERR_SUCCESS) { + printf("Failed to reconnect, exiting\n"); + running = false; + ret = EXIT_FAILURE; + } + } } pthread_join(instantaneous_thread, NULL); From c0c11c8f0c947e9763df491567da0e81b929da5b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?K=C3=A9vin=20L=27h=C3=B4pital?= Date: Fri, 17 Apr 2026 11:43:09 +0200 Subject: [PATCH 37/64] gapi_mosquitto: do not use clean session In order to reconnect properly to the MQTT broker without losing the subscriptions, set the clean session flag to false when creating the mosquitto client instance. The id argument is required when clean session is false. SFL: #21905 Change-Id: Ibf031b665f737816c1878d57db5dce5e45c9cd9c --- example-implementations/api/api-mockup/src/gapi_mosquitto.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/example-implementations/api/api-mockup/src/gapi_mosquitto.c b/example-implementations/api/api-mockup/src/gapi_mosquitto.c index 3c81b8b..d633fd3 100644 --- a/example-implementations/api/api-mockup/src/gapi_mosquitto.c +++ b/example-implementations/api/api-mockup/src/gapi_mosquitto.c @@ -78,7 +78,7 @@ struct mosquitto *api_communication_init(const char *broker, int port) mosquitto_lib_init(); - mosq = mosquitto_new(NULL, true, NULL); + mosq = mosquitto_new("gapi", false, NULL); if (!mosq) { fprintf(stderr, "Error: mosquitto_new() failed\n"); goto cleanup; From 8663ebc6e1d7c4da27008fe5c30caca5671178ed Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?K=C3=A9vin=20L=27h=C3=B4pital?= Date: Thu, 28 May 2026 14:57:55 +0200 Subject: [PATCH 38/64] Dockerfile: update to ubuntu:26.04 Update to the latest LTS to get the latest package version. This bump was encouraged by clang-format returning a strange error of formatting in the following commits. With the latest clang-format, the error is gone. SFL: #22399 Change-Id: I34fa31eec1c9b6d4bbc635d1c6662c770843fb8d --- example-implementations/api/api-mockup/.cqfd/docker/Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/example-implementations/api/api-mockup/.cqfd/docker/Dockerfile b/example-implementations/api/api-mockup/.cqfd/docker/Dockerfile index 1565b62..6e9ca2c 100644 --- a/example-implementations/api/api-mockup/.cqfd/docker/Dockerfile +++ b/example-implementations/api/api-mockup/.cqfd/docker/Dockerfile @@ -1,6 +1,6 @@ # Copyright (C) 2025 Southern California Edison. -FROM ubuntu:24.04 +FROM ubuntu:26.04 ENV DEBIAN_FRONTEND=noninteractive From 6179ddf054d50a42b5d73da73175bce94f53a168 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?K=C3=A9vin=20L=27h=C3=B4pital?= Date: Tue, 26 May 2026 17:57:27 +0200 Subject: [PATCH 39/64] gapi: update GEISA schemas version Update GEISA schemas to the latest version. As the latest version is now using optional fields, and protobuf-c does not support optional fields for now, we need to change the C files generation to use nanopb. Then this commit update the code to use nanopb instead of protobuf-c for: * Protobuf schemas C files generation * Encoding and decoding of messages in the API handlers This commit also update the code to follow the changes in the schemas. For now, only the required changes to make the code compile and behave as before has been done. The schemas new fields will be added in further commits. SFL: #22399 Change-Id: If346849e2eed48d27c0cbbc2c81b726efc9d6c9b --- .../api/api-mockup/.cqfd/docker/Dockerfile | 7 +- .../api/api-mockup/.gitmodules | 3 + .../api/api-mockup/Makefile | 18 +- .../api/api-mockup/include/gapi_discovery.h | 8 +- .../api-mockup/include/gapi_instantaneous.h | 4 +- .../api/api-mockup/include/gapi_waveform.h | 5 +- example-implementations/api/api-mockup/nanopb | 1 + .../nanopb_options/discovery.options | 4 + .../nanopb_options/manifest.options | 1 + .../nanopb_options/waveform.options | 2 + .../api/api-mockup/schemas | 2 +- .../api/api-mockup/src/gapi_discovery.c | 221 ++++++++++------- .../api/api-mockup/src/gapi_instantaneous.c | 225 +++++++++--------- .../api/api-mockup/src/gapi_waveform.c | 94 +++++--- 14 files changed, 353 insertions(+), 242 deletions(-) create mode 160000 example-implementations/api/api-mockup/nanopb create mode 100644 example-implementations/api/api-mockup/nanopb_options/discovery.options create mode 100644 example-implementations/api/api-mockup/nanopb_options/manifest.options create mode 100644 example-implementations/api/api-mockup/nanopb_options/waveform.options diff --git a/example-implementations/api/api-mockup/.cqfd/docker/Dockerfile b/example-implementations/api/api-mockup/.cqfd/docker/Dockerfile index 6e9ca2c..a3ac8c3 100644 --- a/example-implementations/api/api-mockup/.cqfd/docker/Dockerfile +++ b/example-implementations/api/api-mockup/.cqfd/docker/Dockerfile @@ -17,8 +17,13 @@ RUN set -x \ clang-format \ clang-tidy \ libmosquitto-dev \ + libnanopb-dev \ libprotobuf-c-dev \ pkg-config \ protobuf-compiler \ - protobuf-c-compiler \ + python3-pip \ && rm -rf /var/lib/apt/lists/ + +RUN set -x \ + && pip3 install --no-cache-dir --break-system-packages \ + nanopb diff --git a/example-implementations/api/api-mockup/.gitmodules b/example-implementations/api/api-mockup/.gitmodules index 4bdad91..50ae739 100644 --- a/example-implementations/api/api-mockup/.gitmodules +++ b/example-implementations/api/api-mockup/.gitmodules @@ -1,3 +1,6 @@ [submodule "schemas"] path = schemas url = https://github.com/geisa/schemas.git +[submodule "nanopb"] + path = nanopb + url = https://github.com/nanopb/nanopb.git diff --git a/example-implementations/api/api-mockup/Makefile b/example-implementations/api/api-mockup/Makefile index 10da237..0fc2d11 100644 --- a/example-implementations/api/api-mockup/Makefile +++ b/example-implementations/api/api-mockup/Makefile @@ -1,15 +1,17 @@ # Copyright (C) 2025 Southern California Edison +include nanopb/extra/nanopb.mk + BUILDIR ?= build DESTDIR ?= /usr/bin PROTOS := $(wildcard schemas/*.proto) -PB_C := $(PROTOS:schemas/%.proto=$(BUILDIR)/schemas/%.pb-c.c) +PB_C := $(PROTOS:schemas/%.proto=$(BUILDIR)/schemas/%.pb.c) SRC := $(wildcard src/*.c) -OBJ := $(patsubst src/%.c,$(BUILDIR)/%.o,$(SRC)) $(PB_C:.c=.o) -CFLAGS := -O2 -Wall -std=c11 -Wextra -pedantic -Werror -Iinclude -I$(BUILDIR) $(shell pkg-config --cflags 'libprotobuf-c >= 1.0.0') -LDLIBS := -lmosquitto $(shell pkg-config --libs 'libprotobuf-c >= 1.0.0') +OBJ := $(patsubst src/%.c,$(BUILDIR)/%.o,$(SRC)) $(PB_C:.c=.o) $(NANOPB_CORE:.c=.o) +CFLAGS := -O2 -Wall -std=c11 -Wextra -pedantic -Werror -Iinclude -I$(BUILDIR) -I/usr/include/nanopb -DPB_ENABLE_MALLOC +LDLIBS := -lmosquitto all: $(BUILDIR)/gapi .PHONY: schemas build_dir clean @@ -17,7 +19,7 @@ all: $(BUILDIR)/gapi build_dir: @mkdir -p $(BUILDIR) -$(BUILDIR)/schemas/%.pb-c.o: %.c | build_dir schemas +$(BUILDIR)/schemas/%.pb.o: %.c | build_dir schemas $(CC) $(CFLAGS) -c $< -o $@ $(BUILDIR)/%.o: src/%.c | build_dir schemas @@ -27,14 +29,16 @@ $(BUILDIR)/gapi: $(OBJ) $(CC) $(CFLAGS) -o $@ gapi.c $(OBJ) $(LDLIBS) schemas: - protoc --c_out=build schemas/*.proto + @mkdir -p $(BUILDIR)/schemas + protoc --nanopb_out=$(BUILDIR)/schemas schemas/*.proto -I schemas --nanopb_opt='-Inanopb_options' install: gapi install -D -m 755 $(BUILDIR)/gapi $(DESTDIR) lint: clang-format --Werror --dry-run *.c src/*.c include/*.h - clang-tidy *.c src/*.c include/*.h -- -Iinclude -I$(BUILDIR) + clang-tidy *.c src/*.c include/*.h -- -Iinclude -I$(BUILDIR) -I nanopb clean: rm -rf $(BUILDIR) + rm nanopb/*.o diff --git a/example-implementations/api/api-mockup/include/gapi_discovery.h b/example-implementations/api/api-mockup/include/gapi_discovery.h index f006d9b..b9abc65 100644 --- a/example-implementations/api/api-mockup/include/gapi_discovery.h +++ b/example-implementations/api/api-mockup/include/gapi_discovery.h @@ -9,7 +9,11 @@ #define _GNU_SOURCE #include "gapi_mosquitto.h" -#include "schemas/discovery.pb-c.h" +#include "pb.h" +#include "pb_decode.h" +#include "pb_encode.h" +#include "schemas/discovery.pb.h" +#include "schemas/manifest.pb.h" #include /** @@ -24,6 +28,6 @@ void api_discovery_init(struct mosquitto *mosq); * * @return Pointer to PlatformDiscoveryWaveform structure */ -GeisaPlatformDiscoveryWaveform get_waveform_info(); +GeisaPlatformDiscovery_Waveform get_waveform_info(); #endif // GAPI_DISCOVERY_H diff --git a/example-implementations/api/api-mockup/include/gapi_instantaneous.h b/example-implementations/api/api-mockup/include/gapi_instantaneous.h index 2219da8..e03086d 100644 --- a/example-implementations/api/api-mockup/include/gapi_instantaneous.h +++ b/example-implementations/api/api-mockup/include/gapi_instantaneous.h @@ -8,7 +8,9 @@ #define GAPI_INSTANTANEOUS_H #include "gapi_mosquitto.h" -#include "schemas/metered_quantities.pb-c.h" +#include "pb.h" +#include "pb_encode.h" +#include "schemas/metered_quantities.pb.h" #include #include diff --git a/example-implementations/api/api-mockup/include/gapi_waveform.h b/example-implementations/api/api-mockup/include/gapi_waveform.h index f4e032f..4c4faae 100644 --- a/example-implementations/api/api-mockup/include/gapi_waveform.h +++ b/example-implementations/api/api-mockup/include/gapi_waveform.h @@ -9,7 +9,10 @@ #define _GNU_SOURCE #include "gapi_mosquitto.h" -#include "schemas/waveform.pb-c.h" +#include "pb.h" +#include "pb_decode.h" +#include "pb_encode.h" +#include "schemas/waveform.pb.h" #include /** diff --git a/example-implementations/api/api-mockup/nanopb b/example-implementations/api/api-mockup/nanopb new file mode 160000 index 0000000..cad3c18 --- /dev/null +++ b/example-implementations/api/api-mockup/nanopb @@ -0,0 +1 @@ +Subproject commit cad3c18ef15a663e30e3e43e3a752b66378adec1 diff --git a/example-implementations/api/api-mockup/nanopb_options/discovery.options b/example-implementations/api/api-mockup/nanopb_options/discovery.options new file mode 100644 index 0000000..e243e4c --- /dev/null +++ b/example-implementations/api/api-mockup/nanopb_options/discovery.options @@ -0,0 +1,4 @@ +*.* max_size:32 +GeisaPlatformDiscovery_Device.sub_module type:FT_POINTER +GeisaPlatformDiscovery_Waveform_Instance.description max_size:64 +GeisaPlatformDiscovery_Waveform.streams type:FT_POINTER diff --git a/example-implementations/api/api-mockup/nanopb_options/manifest.options b/example-implementations/api/api-mockup/nanopb_options/manifest.options new file mode 100644 index 0000000..e2f8ccc --- /dev/null +++ b/example-implementations/api/api-mockup/nanopb_options/manifest.options @@ -0,0 +1 @@ +GeisaApplicationDeploymentManifest_Rsp.manifest type:FT_POINTER diff --git a/example-implementations/api/api-mockup/nanopb_options/waveform.options b/example-implementations/api/api-mockup/nanopb_options/waveform.options new file mode 100644 index 0000000..0758eac --- /dev/null +++ b/example-implementations/api/api-mockup/nanopb_options/waveform.options @@ -0,0 +1,2 @@ +GeisaWaveform_Req.stream_id type:FT_POINTER +GeisaWaveform_Rsp.stream_id type:FT_POINTER diff --git a/example-implementations/api/api-mockup/schemas b/example-implementations/api/api-mockup/schemas index 3b62cb1..e67fa99 160000 --- a/example-implementations/api/api-mockup/schemas +++ b/example-implementations/api/api-mockup/schemas @@ -1 +1 @@ -Subproject commit 3b62cb1d1f1e40438685286cf446a45da1de60f9 +Subproject commit e67fa9904f7c000c5b8727a146c3ce4f50a819d9 diff --git a/example-implementations/api/api-mockup/src/gapi_discovery.c b/example-implementations/api/api-mockup/src/gapi_discovery.c index 61ff120..7b153bc 100644 --- a/example-implementations/api/api-mockup/src/gapi_discovery.c +++ b/example-implementations/api/api-mockup/src/gapi_discovery.c @@ -7,9 +7,7 @@ #include "gapi_discovery.h" // NOLINTBEGIN(cppcoreguidelines-interfaces-global-init,cppcoreguidelines-avoid-magic-numbers,readability-magic-numbers) -static GeisaPlatformDiscoveryGEISA geisa_platform_info = { - .base = PROTOBUF_C_MESSAGE_INIT( - &geisa_platform_discovery__geisa__descriptor), +static GeisaPlatformDiscovery_GEISA geisa_platform_info = { .ver_major = 0, .ver_minor = 1, .ver_rev = 7, @@ -19,12 +17,10 @@ static GeisaPlatformDiscoveryGEISA geisa_platform_info = { .pillar_vee = false, }; -static const GeisaPlatformDiscoveryType top_type_platform_info = - GEISA_PLATFORM_DISCOVERY__TYPE__DEVICE_TYPE_ELECTRIC_METER; +static const GeisaPlatformDiscovery_DeviceType top_type_platform_info = + GeisaPlatformDiscovery_DeviceType_TYPE_ELECTRIC_METER; -static GeisaPlatformDiscoveryModule top_module_platform_info = { - .base = PROTOBUF_C_MESSAGE_INIT( - &geisa_platform_discovery__module__descriptor), +static const GeisaPlatformDiscovery_Module top_module_platform_info = { .type = top_type_platform_info, .manufacturer = "SCE", .model = "GEISA-1", @@ -33,12 +29,10 @@ static GeisaPlatformDiscoveryModule top_module_platform_info = { .fw_revision = "1.0.0", }; -static const GeisaPlatformDiscoveryType sub_type_platform_info = - GEISA_PLATFORM_DISCOVERY__TYPE__DEVICE_TYPE_METROLOGY_PROCESSOR; +static const GeisaPlatformDiscovery_DeviceType sub_type_platform_info = + GeisaPlatformDiscovery_DeviceType_TYPE_METROLOGY_PROCESSOR; -static GeisaPlatformDiscoveryModule sub_module1_platform_info = { - .base = PROTOBUF_C_MESSAGE_INIT( - &geisa_platform_discovery__module__descriptor), +static GeisaPlatformDiscovery_Module sub_module_platform_info = { .type = sub_type_platform_info, .manufacturer = "SCE", .model = "GEISA-2", @@ -47,64 +41,54 @@ static GeisaPlatformDiscoveryModule sub_module1_platform_info = { .fw_revision = "1.1.0", }; -static GeisaPlatformDiscoveryModule *sub_modules_platform_info[] = { - &sub_module1_platform_info}; - -static GeisaPlatformDiscoveryDevice device_platform_info = { - .base = PROTOBUF_C_MESSAGE_INIT( - &geisa_platform_discovery__device__descriptor), - .top_module = &top_module_platform_info, - .n_sub_module = 1, - .sub_module = sub_modules_platform_info, +static GeisaPlatformDiscovery_Device device_platform_info = { + .top_module = top_module_platform_info, + .has_top_module = true, + .sub_module_count = 1, + .sub_module = &sub_module_platform_info, }; -static GeisaPlatformDiscoveryOperator operator_platform_info = { - .base = PROTOBUF_C_MESSAGE_INIT( - &geisa_platform_discovery__operator__descriptor), +static GeisaPlatformDiscovery_Operator operator_platform_info = { .operator_name = "SCE", .operator_identifier = "SCE001", }; -static GeisaPlatformDiscoveryMetrology metrology_platform_info = - GEISA_PLATFORM_DISCOVERY__METROLOGY__INIT; -static GeisaPlatformDiscoverySensor sensor_platform_info = - GEISA_PLATFORM_DISCOVERY__SENSOR__INIT; -static GeisaPlatformDiscoveryNetwork network_platform_info = - GEISA_PLATFORM_DISCOVERY__NETWORK__INIT; - -static const GeisaWaveformDatatype waveform_data_type_platform_info = - GEISA_WAVEFORM__DATATYPE__DATA_INT32; - -static GeisaPlatformDiscoveryWaveformInstance waveform_platform_instance1_info = { - .base = PROTOBUF_C_MESSAGE_INIT( - &geisa_platform_discovery__waveform__instance__descriptor), - .instance_id = 1, - .data_connection = "/data/waveform/instance1.socket", +static GeisaPlatformDiscovery_Metrology metrology_platform_info = + GeisaPlatformDiscovery_Metrology_init_default; +static GeisaPlatformDiscovery_Sensor sensor_platform_info = + GeisaPlatformDiscovery_Sensor_init_default; +static GeisaPlatformDiscovery_Network network_platform_info = + GeisaPlatformDiscovery_Network_init_default; + +static const GeisaWaveform_Datatype waveform_data_type_platform_info = + GeisaWaveform_Datatype_DATA_INT32; + +static GeisaPlatformDiscovery_Waveform_Instance waveform_platform_instances = { + .stream_id = "api-mockup-waveform", + .name = "API Mockup Waveform", + .description = "Example waveform instance for API mockup application", .datatype = waveform_data_type_platform_info, .voltage_multiplier = 0.1, .current_multiplier = 0.01, .num_voltage_ch = 1, .num_current_ch = 1, .num_other_ch = 0, - .sample_rate_is_cycle_locked = true, + .total_channel_count = 2, + .cycle_aligned = true, + .zero_crossing_aligned = true, .sample_rate = 7680, - .frame_is_cycle_aligned = true, - .frame_duration_samples = 200, + .samples_per_cycle = 1280, + .nominal_frequency_hz = 60, + .expected_frame_period_ms = 1000, .voltage_filter_lowpass = 0, .voltage_filter_highpass = 0, .current_filter_lowpass = 0, .current_filter_highpass = 0, }; -static GeisaPlatformDiscoveryWaveformInstance *waveform_platform_instances[] = { - &waveform_platform_instance1_info, -}; - -static GeisaPlatformDiscoveryWaveform waveform_platform_info = { - PROTOBUF_C_MESSAGE_INIT( - &geisa_platform_discovery__waveform__descriptor), - .n_instances = 1, - .instances = waveform_platform_instances, +static GeisaPlatformDiscovery_Waveform waveform_platform_info = { + .streams_count = 1, + .streams = &waveform_platform_instances, }; char *deployment_manifest = @@ -188,18 +172,25 @@ char *deployment_manifest = // NOLINTEND(cppcoreguidelines-interfaces-global-init,cppcoreguidelines-avoid-magic-numbers,readability-magic-numbers) static void -api_platform_discovery_build_response(GeisaPlatformDiscoveryRsp *response) +api_platform_discovery_build_response(GeisaPlatformDiscovery_Rsp *response) { - response->geisa = &geisa_platform_info; - response->device = &device_platform_info; - response->operator_ = &operator_platform_info; - response->metrology = &metrology_platform_info; - response->sensor = &sensor_platform_info; - response->network = &network_platform_info; - response->waveform = &waveform_platform_info; + response->geisa = geisa_platform_info; + response->has_geisa = true; + response->device = device_platform_info; + response->has_device = true; + response->operator = operator_platform_info; + response->has_operator = true; + response->metrology = metrology_platform_info; + response->has_metrology = true; + response->sensor = sensor_platform_info; + response->has_sensor = true; + response->network = network_platform_info; + response->has_network = true; + response->waveform = waveform_platform_info; + response->has_waveform = true; } -GeisaPlatformDiscoveryWaveform get_waveform_info() +GeisaPlatformDiscovery_Waveform get_waveform_info() { return waveform_platform_info; } @@ -209,12 +200,17 @@ static void api_platform_discovery_req_handler(struct mosquitto *mosq, const int payloadlen, const uint8_t *payload) { - GeisaPlatformDiscoveryReq *request = NULL; - GeisaPlatformDiscoveryRsp response = - GEISA_PLATFORM_DISCOVERY__RSP__INIT; + GeisaPlatformDiscovery_Req request = + GeisaPlatformDiscovery_Req_init_default; + GeisaPlatformDiscovery_Rsp response = + GeisaPlatformDiscovery_Rsp_init_default; uint8_t *message = NULL; char *rsp_topic = NULL; char *app_id = NULL; + pb_istream_t istream; + pb_ostream_t ostream; + bool status = false; + size_t encoded_size = 0; app_id = basename((char *)topic); @@ -222,18 +218,27 @@ static void api_platform_discovery_req_handler(struct mosquitto *mosq, "[Discovery] Received platform discovery request from %s\n", app_id); fflush(stdout); - request = geisa_platform_discovery__req__unpack(NULL, payloadlen, - payload); - if (request == NULL) { - fprintf(stderr, "[Discovery] Error unpacking platform " + + istream = pb_istream_from_buffer(payload, payloadlen); + status = pb_decode(&istream, GeisaPlatformDiscovery_Req_fields, + &request); + if (!status) { + fprintf(stderr, "[Discovery] Error decoding platform " "discovery request\n"); return; } - geisa_platform_discovery__req__free_unpacked(request, NULL); + pb_release(GeisaPlatformDiscovery_Req_fields, &request); api_platform_discovery_build_response(&response); - message = malloc( - geisa_platform_discovery__rsp__get_packed_size(&response)); + status = pb_get_encoded_size( + &encoded_size, GeisaPlatformDiscovery_Rsp_fields, &response); + if (!status) { + fprintf(stderr, "[Discovery] Error calculating size of " + "platform discovery response\n"); + return; + } + + message = malloc(encoded_size); if (message == NULL) { fprintf(stderr, "[Discovery] Error allocating memory for " "platform discovery response\n"); @@ -249,10 +254,19 @@ static void api_platform_discovery_req_handler(struct mosquitto *mosq, return; } - geisa_platform_discovery__rsp__pack(&response, message); - api_publish(mosq, rsp_topic, - geisa_platform_discovery__rsp__get_packed_size(&response), - message, 1); + ostream = pb_ostream_from_buffer(message, encoded_size); + status = pb_encode(&ostream, GeisaPlatformDiscovery_Rsp_fields, + &response); + if (!status) { + fprintf(stderr, "[Discovery] Error encoding platform " + "discovery response\n"); + free(rsp_topic); + free(message); + return; + } + + api_publish(mosq, rsp_topic, encoded_size, message, 1); + free(rsp_topic); free(message); } @@ -261,27 +275,34 @@ static void api_manifest_req_handler(struct mosquitto *mosq, const char *topic, const int payloadlen, const uint8_t *payload) { - GeisaApplicationDeploymentManifestReq *request = NULL; - GeisaApplicationDeploymentManifestRsp response = - GEISA_APPLICATION_DEPLOYMENT_MANIFEST__RSP__INIT; + GeisaApplicationDeploymentManifest_Req request = + GeisaApplicationDeploymentManifest_Req_init_default; + GeisaApplicationDeploymentManifest_Rsp response = + GeisaApplicationDeploymentManifest_Rsp_init_default; + size_t encoded_size = 0; uint8_t *message = NULL; char *rsp_topic = NULL; char *app_id = NULL; + pb_istream_t istream; + pb_ostream_t ostream; + bool status = false; app_id = basename((char *)topic); fprintf(stdout, "[Manifest] Received app manifest request from %s\n", app_id); fflush(stdout); - request = geisa_application_deployment_manifest__req__unpack( - NULL, payloadlen, payload); - if (request == NULL) { + + istream = pb_istream_from_buffer(payload, payloadlen); + status = pb_decode(&istream, + GeisaApplicationDeploymentManifest_Req_fields, + &request); + if (!status) { fprintf(stderr, - "[Manifest] Error unpacking app manifest request\n"); + "[Manifest] Error decoding app manifest request\n"); return; } - geisa_application_deployment_manifest__req__free_unpacked(request, - NULL); + pb_release(GeisaApplicationDeploymentManifest_Req_fields, &request); if (asprintf(&rsp_topic, "geisa/api/app/manifest/rsp/%s", app_id) == -1) { @@ -292,9 +313,16 @@ static void api_manifest_req_handler(struct mosquitto *mosq, const char *topic, } response.manifest = deployment_manifest; - message = malloc( - geisa_application_deployment_manifest__rsp__get_packed_size( - &response)); + status = pb_get_encoded_size( + &encoded_size, GeisaApplicationDeploymentManifest_Rsp_fields, + &response); + if (!status) { + fprintf(stderr, "[Manifest] Error calculating size of " + "app manifest response\n"); + return; + } + + message = malloc(encoded_size); if (message == NULL) { fprintf(stderr, "[Manifest] Error allocating memory for response " @@ -302,11 +330,20 @@ static void api_manifest_req_handler(struct mosquitto *mosq, const char *topic, free(rsp_topic); return; } - geisa_application_deployment_manifest__rsp__pack(&response, message); - api_publish(mosq, rsp_topic, - geisa_application_deployment_manifest__rsp__get_packed_size( - &response), - message, 1); + + ostream = pb_ostream_from_buffer(message, encoded_size); + status = pb_encode(&ostream, + GeisaApplicationDeploymentManifest_Rsp_fields, + &response); + if (!status) { + fprintf(stderr, "[Manifest] Error encoding app manifest " + "response\n"); + free(rsp_topic); + free(message); + return; + } + + api_publish(mosq, rsp_topic, encoded_size, message, 1); free(rsp_topic); free(message); } diff --git a/example-implementations/api/api-mockup/src/gapi_instantaneous.c b/example-implementations/api/api-mockup/src/gapi_instantaneous.c index 9a2e878..ec7ca4d 100644 --- a/example-implementations/api/api-mockup/src/gapi_instantaneous.c +++ b/example-implementations/api/api-mockup/src/gapi_instantaneous.c @@ -5,146 +5,157 @@ */ #include "gapi_instantaneous.h" +#include "pb_decode.h" -static void -geisa_get_instantaneous_data(GeisaInstantaneousQuantities *response, - GeisaTypeInstantaneousQuantitiesPerPhase *phase_a, - GeisaTypeInstantaneousQuantitiesPerPhase *phase_b, - GeisaTypeInstantaneousQuantitiesPerPhase *phase_c, - GeisaTypeInstantaneousQuantitiesPerPhase *phase_n, - GeisaTypeInstantaneousQuantitiesOther *other) +static void geisa_get_instantaneous_data(GeisaInstantaneousQuantities *response) { // This function should interact with the GEISA system to retrieve // instantaneous data. Here we provide a mock implementation. - geisa_instantaneous_quantities__init(response); - geisa_type_instantaneous_quantities__per_phase__init(phase_a); - geisa_type_instantaneous_quantities__per_phase__init(phase_b); - geisa_type_instantaneous_quantities__per_phase__init(phase_c); - geisa_type_instantaneous_quantities__per_phase__init(phase_n); - geisa_type_instantaneous_quantities__other__init(other); + GeisaTypeInstantaneousQuantities_PerPhase phase_a; + GeisaTypeInstantaneousQuantities_PerPhase phase_b; + GeisaTypeInstantaneousQuantities_PerPhase phase_c; + GeisaTypeInstantaneousQuantities_PerPhase phase_n; + GeisaTypeInstantaneousQuantities_Other other; time_t timestamp = time(NULL) * SEC_IN_MS; // NOLINTBEGIN(readability-magic-numbers,cppcoreguidelines-avoid-magic-numbers) // Mock data assignment - (*phase_a).message_version = 1; - (*phase_a).phase = GEISA_TYPE_PHASE__PHASE_A; - (*phase_a).microamps = 10.0F; - (*phase_a).microvolts = 120.0F; - (*phase_a).microw = 1200.0F; - (*phase_a).microvar = 300.0F; - (*phase_a).microva = 1300.0F; - (*phase_a).voltage_angle = 10.0F; - (*phase_a).current_angle = 30.0F; - (*phase_a).pf = 0.95F; - (*phase_a).microamps_fundamental = 9.5F; - (*phase_a).microvolts_fundamental = 115.0F; - (*phase_a).current_percentage_thd = 5.0F; - (*phase_a).voltage_percentage_thd = 4.0F; - (*phase_a).pf_angle = 20.0F; - (*phase_a).current_percentage_tdd = 3.0F; - (*phase_a).harmonic_current = 0.5F; - (*phase_a).phase_voltage_percentage_2nd_harmonic = 1.0F; + phase_a.message_version = 1; + phase_a.phase = GeisaTypePhase_PHASE_A; + phase_a.microAmps = 10.0F; + phase_a.microVolts = 120.0F; + phase_a.microW = 1200.0F; + phase_a.microVAR = 300.0F; + phase_a.microVA = 1300.0F; + phase_a.Voltage_Angle = 10.0F; + phase_a.Current_Angle = 30.0F; + phase_a.PF = 0.95F; + phase_a.microAmps_fundamental = 9.5F; + phase_a.microVolts_fundamental = 115.0F; + phase_a.Current_percentage_THD = 5.0F; + phase_a.Voltage_percentage_THD = 4.0F; + phase_a.PF_Angle = 20.0F; + phase_a.Current_percentage_TDD = 3.0F; + phase_a.Harmonic_Current = 0.5F; + phase_a.Phase_Voltage_percentage_2nd_Harmonic = 1.0F; - (*phase_b).message_version = 1; - (*phase_b).phase = GEISA_TYPE_PHASE__PHASE_B; - (*phase_b).microamps = 11.0F; - (*phase_b).microvolts = 121.0F; - (*phase_b).microw = 1210.0F; - (*phase_b).microvar = 310.0F; - (*phase_b).microva = 1310.0F; - (*phase_b).voltage_angle = 11.0F; - (*phase_b).current_angle = 31.0F; - (*phase_b).pf = 0.96F; - (*phase_b).microamps_fundamental = 10.5F; - (*phase_b).microvolts_fundamental = 116.0F; - (*phase_b).current_percentage_thd = 6.0F; - (*phase_b).voltage_percentage_thd = 5.0F; - (*phase_b).pf_angle = 21.0F; - (*phase_b).current_percentage_tdd = 4.0F; - (*phase_b).harmonic_current = 0.6F; - (*phase_b).phase_voltage_percentage_2nd_harmonic = 1.1F; + phase_b.message_version = 1; + phase_b.phase = GeisaTypePhase_PHASE_B; + phase_b.microAmps = 11.0F; + phase_b.microVolts = 121.0F; + phase_b.microW = 1210.0F; + phase_b.microVAR = 310.0F; + phase_b.microVA = 1310.0F; + phase_b.Voltage_Angle = 11.0F; + phase_b.Current_Angle = 31.0F; + phase_b.PF = 0.96F; + phase_b.microAmps_fundamental = 10.5F; + phase_b.microVolts_fundamental = 116.0F; + phase_b.Current_percentage_THD = 6.0F; + phase_b.Voltage_percentage_THD = 5.0F; + phase_b.PF_Angle = 21.0F; + phase_b.Current_percentage_TDD = 4.0F; + phase_b.Harmonic_Current = 0.6F; + phase_b.Phase_Voltage_percentage_2nd_Harmonic = 1.1F; - (*phase_c).message_version = 1; - (*phase_c).phase = GEISA_TYPE_PHASE__PHASE_C; - (*phase_c).microamps = 12.0F; - (*phase_c).microvolts = 122.0F; - (*phase_c).microw = 1220.0F; - (*phase_c).microvar = 320.0F; - (*phase_c).microva = 1320.0F; - (*phase_c).voltage_angle = 12.0F; - (*phase_c).current_angle = 32.0F; - (*phase_c).pf = 0.97F; - (*phase_c).microamps_fundamental = 11.5F; - (*phase_c).microvolts_fundamental = 117.0F; - (*phase_c).current_percentage_thd = 7.0F; - (*phase_c).voltage_percentage_thd = 6.0F; - (*phase_c).pf_angle = 22.0F; - (*phase_c).current_percentage_tdd = 5.0F; - (*phase_c).harmonic_current = 0.7F; - (*phase_c).phase_voltage_percentage_2nd_harmonic = 1.2F; + phase_c.message_version = 1; + phase_c.phase = GeisaTypePhase_PHASE_C; + phase_c.microAmps = 12.0F; + phase_c.microVolts = 122.0F; + phase_c.microW = 1220.0F; + phase_c.microVAR = 320.0F; + phase_c.microVA = 1320.0F; + phase_c.Voltage_Angle = 12.0F; + phase_c.Current_Angle = 32.0F; + phase_c.PF = 0.97F; + phase_c.microAmps_fundamental = 11.5F; + phase_c.microVolts_fundamental = 117.0F; + phase_c.Current_percentage_THD = 7.0F; + phase_c.Voltage_percentage_THD = 6.0F; + phase_c.PF_Angle = 22.0F; + phase_c.Current_percentage_TDD = 5.0F; + phase_c.Harmonic_Current = 0.7F; + phase_c.Phase_Voltage_percentage_2nd_Harmonic = 1.2F; - (*phase_n).message_version = 1; - (*phase_n).phase = GEISA_TYPE_PHASE__PHASE_N; - (*phase_n).microamps = 5.0F; - (*phase_n).microvolts = 60.0F; - (*phase_n).microw = 600.0F; - (*phase_n).microvar = 150.0F; - (*phase_n).microva = 650.0F; - (*phase_n).voltage_angle = 5.0F; - (*phase_n).current_angle = 15.0F; - (*phase_n).pf = 0.98F; - (*phase_n).microamps_fundamental = 4.5F; - (*phase_n).microvolts_fundamental = 55.0F; - (*phase_n).current_percentage_thd = 2.0F; - (*phase_n).voltage_percentage_thd = 1.5F; - (*phase_n).pf_angle = 10.0F; - (*phase_n).current_percentage_tdd = 1.0F; - (*phase_n).harmonic_current = 0.2F; - (*phase_n).phase_voltage_percentage_2nd_harmonic = 0.5F; + phase_n.message_version = 1; + phase_n.phase = GeisaTypePhase_PHASE_N; + phase_n.microAmps = 5.0F; + phase_n.microVolts = 60.0F; + phase_n.microW = 600.0F; + phase_n.microVAR = 150.0F; + phase_n.microVA = 650.0F; + phase_n.Voltage_Angle = 5.0F; + phase_n.Current_Angle = 15.0F; + phase_n.PF = 0.98F; + phase_n.microAmps_fundamental = 4.5F; + phase_n.microVolts_fundamental = 55.0F; + phase_n.Current_percentage_THD = 2.0F; + phase_n.Voltage_percentage_THD = 1.5F; + phase_n.PF_Angle = 10.0F; + phase_n.Current_percentage_TDD = 1.0F; + phase_n.Harmonic_Current = 0.2F; + phase_n.Phase_Voltage_percentage_2nd_Harmonic = 0.5F; - (*other).message_version = 1; - (*other).timestamp = timestamp; - (*other).neutral_imputed_microamps = 5.5F; - (*other).load_side_microvolts = 123.0F; + other.message_version = 1; + other.timestamp = timestamp; + other.Neutral_Imputed_microAmps = 5.5F; + other.Load_Side_microVolts = 123.0F; // NOLINTEND(readability-magic-numbers,cppcoreguidelines-avoid-magic-numbers) response->timestamp = timestamp; - response->phase_a = phase_a; - response->phase_b = phase_b; - response->phase_c = phase_c; - response->phase_n = phase_n; + response->phase_A = phase_a; + response->has_phase_A = true; + response->phase_B = phase_b; + response->has_phase_B = true; + response->phase_C = phase_c; + response->has_phase_C = true; + response->phase_N = phase_n; + response->has_phase_N = true; response->other = other; + response->has_other = true; } static void *gapi_instantaneous_thread(void *arg) { struct mosquitto *mosq = (struct mosquitto *)arg; - GeisaTypeInstantaneousQuantitiesPerPhase phase_a; - GeisaTypeInstantaneousQuantitiesPerPhase phase_b; - GeisaTypeInstantaneousQuantitiesPerPhase phase_c; - GeisaTypeInstantaneousQuantitiesPerPhase phase_n; - GeisaTypeInstantaneousQuantitiesOther other; GeisaInstantaneousQuantities response; + size_t encoded_size = 0; uint8_t *message = NULL; + pb_ostream_t ostream; + bool status = true; while (running) { - geisa_get_instantaneous_data(&response, &phase_a, &phase_b, - &phase_c, &phase_n, &other); - message = - malloc(geisa_instantaneous_quantities__get_packed_size( - &response)); + geisa_get_instantaneous_data(&response); + status = pb_get_encoded_size( + &encoded_size, GeisaInstantaneousQuantities_fields, + &response); + if (!status) { + fprintf(stderr, "[Instantaneous] Error calculating " + "size of instantaneous data\n"); + sleep(1); + continue; + } + message = malloc(encoded_size); if (message == NULL) { fprintf(stderr, "[Instantaneous] Failed to allocate " "memory for instantaneous data\n"); sleep(1); continue; } - geisa_instantaneous_quantities__pack(&response, message); - api_publish(mosq, "geisa/api/instantaneous/data", - geisa_instantaneous_quantities__get_packed_size( - &response), + ostream = pb_ostream_from_buffer(message, encoded_size); + status = + pb_encode(&ostream, GeisaInstantaneousQuantities_fields, + &response); + if (!status) { + fprintf(stderr, "[Instantaneous] Error encoding " + "instantaneous data\n"); + free(message); + sleep(1); + continue; + } + api_publish(mosq, "geisa/api/instantaneous/data", encoded_size, message, 0); free(message); sleep(1); diff --git a/example-implementations/api/api-mockup/src/gapi_waveform.c b/example-implementations/api/api-mockup/src/gapi_waveform.c index 870b457..ce47b7f 100644 --- a/example-implementations/api/api-mockup/src/gapi_waveform.c +++ b/example-implementations/api/api-mockup/src/gapi_waveform.c @@ -7,57 +7,72 @@ #include "gapi_waveform.h" #include "gapi_discovery.h" -static void api_platform_waveform_build_response(GeisaWaveformRsp *response, - int32_t instance_id, - bool activate) +static void +api_platform_waveform_build_response(GeisaWaveform_Rsp *response, + char *stream_id, + GeisaWaveform_RequestType request_type) { - GeisaPlatformDiscoveryWaveform waveform_platform_info; - (void)activate; + GeisaPlatformDiscovery_Waveform waveform_platform_info; + (void)request_type; waveform_platform_info = get_waveform_info(); - if (waveform_platform_info.n_instances == 0) { - response->status = - GEISA_WAVEFORM__STATUS__WAVEFORM_ERR_NO_RESOURCES; + + if (waveform_platform_info.streams_count == 0) { + response->waveform_status = + GeisaWaveform_Status_WAVEFORM_ERR_NO_RESOURCES; return; } - if (instance_id < 0 || - instance_id > (int32_t)waveform_platform_info.n_instances) { - response->status = - GEISA_WAVEFORM__STATUS__WAVEFORM_ERR_INVALID_ID; + if (stream_id == NULL) { + response->waveform_status = + GeisaWaveform_Status_WAVEFORM_ERR_INVALID_STREAM_ID; return; } - response->status = GEISA_WAVEFORM__STATUS__WAVEFORM_SUCCESS; + for (size_t i = 0; i < waveform_platform_info.streams_count; i++) { + if (strcmp(waveform_platform_info.streams[i].stream_id, + stream_id) == 0) { + response->stream_id = stream_id; + response->waveform_status = + GeisaWaveform_Status_WAVEFORM_SUCCESS; + break; + } + } + + if (response->waveform_status != + GeisaWaveform_Status_WAVEFORM_SUCCESS) { + response->waveform_status = + GeisaWaveform_Status_WAVEFORM_ERR_UNAVAILABLE; + } } static void api_waveform_req_handler(struct mosquitto *mosq, const char *topic, const int payloadlen, const uint8_t *payload) { - GeisaWaveformReq *request = NULL; - GeisaWaveformRsp response = GEISA_WAVEFORM__RSP__INIT; - - int32_t instance_id = 0; + GeisaWaveform_Req request = GeisaWaveform_Req_init_default; + GeisaWaveform_Rsp response = GeisaWaveform_Rsp_init_default; + size_t encoded_size = 0; uint8_t *message = NULL; char *rsp_topic = NULL; - bool activate = false; char *app_id = NULL; + pb_istream_t istream; + pb_ostream_t ostream; + bool status = false; app_id = basename((char *)topic); fprintf(stdout, "[Waveform] Received waveform data request from %s\n", app_id); fflush(stdout); - request = geisa_waveform__req__unpack(NULL, payloadlen, payload); - if (request == NULL) { + + istream = pb_istream_from_buffer(payload, payloadlen); + status = pb_decode(&istream, GeisaWaveform_Req_fields, &request); + if (!status) { fprintf(stderr, - "[Waveform] Error unpacking app manifest request\n"); + "[Waveform] Error decoding waveform app request\n"); return; } - instance_id = request->instance_id; - activate = request->activate; - geisa_waveform__req__free_unpacked(request, NULL); if (asprintf(&rsp_topic, "geisa/api/waveform/rsp/%s", app_id) == -1) { fprintf(stderr, "[Waveform] Error allocating memory for " @@ -65,8 +80,20 @@ static void api_waveform_req_handler(struct mosquitto *mosq, const char *topic, return; } - api_platform_waveform_build_response(&response, instance_id, activate); - message = malloc(geisa_waveform__rsp__get_packed_size(&response)); + api_platform_waveform_build_response(&response, request.stream_id, + request.request_type); + pb_release(GeisaWaveform_Req_fields, &request); + + status = pb_get_encoded_size(&encoded_size, GeisaWaveform_Rsp_fields, + &response); + if (!status) { + fprintf(stderr, "[Waveform] Error calculating size of response " + "message\n"); + free(rsp_topic); + return; + } + + message = malloc(encoded_size); if (message == NULL) { fprintf(stderr, "[Waveform] Error allocating memory for response " @@ -74,10 +101,17 @@ static void api_waveform_req_handler(struct mosquitto *mosq, const char *topic, free(rsp_topic); return; } - geisa_waveform__rsp__pack(&response, message); - api_publish(mosq, rsp_topic, - geisa_waveform__rsp__get_packed_size(&response), message, - 1); + + ostream = pb_ostream_from_buffer(message, encoded_size); + status = pb_encode(&ostream, GeisaWaveform_Rsp_fields, &response); + if (!status) { + fprintf(stderr, "[Waveform] Error encoding response message\n"); + free(message); + free(rsp_topic); + return; + } + + api_publish(mosq, rsp_topic, encoded_size, message, 1); free(message); } From 67d212e83c3e5c8e93beb5cf1cf18d2b7df879fd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?K=C3=A9vin=20L=27h=C3=B4pital?= Date: Thu, 28 May 2026 15:34:06 +0200 Subject: [PATCH 40/64] gapi: add a test application to validate the API calls This test application will only print some elements of the messages received to validate the API calls. Document the testing procedure in the README.md file. SFL: #22399 Change-Id: I6cbf38b103808ad959bb9009a7e8647bc1f45162 --- .../api/api-mockup/Makefile | 5 +- .../api/api-mockup/README.md | 31 ++++ .../api/api-mockup/test/test_app.c | 175 ++++++++++++++++++ 3 files changed, 210 insertions(+), 1 deletion(-) create mode 100644 example-implementations/api/api-mockup/test/test_app.c diff --git a/example-implementations/api/api-mockup/Makefile b/example-implementations/api/api-mockup/Makefile index 0fc2d11..64d0ab4 100644 --- a/example-implementations/api/api-mockup/Makefile +++ b/example-implementations/api/api-mockup/Makefile @@ -13,7 +13,7 @@ OBJ := $(patsubst src/%.c,$(BUILDIR)/%.o,$(SRC)) $(PB_C:.c=.o) $(NANOPB_CORE:.c= CFLAGS := -O2 -Wall -std=c11 -Wextra -pedantic -Werror -Iinclude -I$(BUILDIR) -I/usr/include/nanopb -DPB_ENABLE_MALLOC LDLIBS := -lmosquitto -all: $(BUILDIR)/gapi +all: $(BUILDIR)/gapi $(BUILDIR)/test_app .PHONY: schemas build_dir clean build_dir: @@ -42,3 +42,6 @@ lint: clean: rm -rf $(BUILDIR) rm nanopb/*.o + +$(BUILDIR)/test_app: $(OBJ) + $(CC) $(CFLAGS) -o $@ test/test_app.c $(PB_C:.c=.o) $(NANOPB_CORE:.c=.o) $(LDLIBS) diff --git a/example-implementations/api/api-mockup/README.md b/example-implementations/api/api-mockup/README.md index 1fc275d..2acc9be 100644 --- a/example-implementations/api/api-mockup/README.md +++ b/example-implementations/api/api-mockup/README.md @@ -67,3 +67,34 @@ or with cqfd: ```bash cqfd -b lint ``` + +## Testing + +To validate the GEISA API Mockup application easily, a test script has been done to print some of the informations received. +The test script is compiled at the same time as the application and is available in the build directory. +After launching the test application, you can run the following command to trigger API calls: + +* To test the discovery API: + +```bash +mosquitto_pub -u testapp -P testapp -h localhost -t geisa/api/platform/discovery/req/testapp -f /tmp/discovery.binpb +``` + +* To test the manifest API: + +```bash +mosquitto_pub -u testapp -P testapp -h localhost -t geisa/api/app/manifest/req/testapp -f /tmp/manifest.binpb +``` + +* To test the waveform API: + * First, you need to create the binpb file with the information you want, for example: + +```bash +printf 'stream_id: "api-mockup-waveform"\nrequest_type: 1\n' | protoc --encode=GeisaWaveform_Req waveform.proto > waveform.binpb +``` + + * Then, you can trigger the API call with the following command: + +```bash +mosquitto_pub -u testapp -P testapp -h localhost -t geisa/api/waveform/req/testapp -f /tmp/waveform.binpb +``` diff --git a/example-implementations/api/api-mockup/test/test_app.c b/example-implementations/api/api-mockup/test/test_app.c new file mode 100644 index 0000000..f3a0f62 --- /dev/null +++ b/example-implementations/api/api-mockup/test/test_app.c @@ -0,0 +1,175 @@ +#include "pb.h" +#include "pb_decode.h" +#include "pb_encode.h" +#include "schemas/discovery.pb.h" +#include "schemas/manifest.pb.h" +#include "schemas/metered_quantities.pb.h" +#include "schemas/waveform.pb.h" +#include +#include +#include +#include + +#define BROKER "localhost" +#define PORT 1883 + +void on_connect(struct mosquitto *mosq, void *obj, int rc) +{ + (void)obj; + if (rc == 0) { + printf("Connected to broker\n"); + mosquitto_subscribe(mosq, NULL, "geisa/api/instantaneous/data", + 0); + mosquitto_subscribe(mosq, NULL, + "geisa/api/platform/discovery/rsp/testapp", + 0); + mosquitto_subscribe(mosq, NULL, + "geisa/api/app/manifest/rsp/testapp", 0); + mosquitto_subscribe(mosq, NULL, + "geisa/api/waveform/rsp/testapp", 0); + } else { + printf("Connection failed: %d\n", rc); + } +} + +void handle_instantaneous(const void *payload, size_t len) +{ + bool status; + pb_istream_t istream; + GeisaInstantaneousQuantities msg = + GeisaInstantaneousQuantities_init_default; + + istream = pb_istream_from_buffer(payload, len); + + status = pb_decode(&istream, GeisaInstantaneousQuantities_fields, &msg); + if (!status) { + printf("Failed to decode manifest request: %s\n", + PB_GET_ERROR(&istream)); + return; + } + printf("Decoded instantaneous quantities successfully\n"); + + GeisaTypeInstantaneousQuantities_PerPhase a = msg.phase_A; + GeisaTypeInstantaneousQuantities_PerPhase b = msg.phase_B; + GeisaTypeInstantaneousQuantities_PerPhase c = msg.phase_C; + GeisaTypeInstantaneousQuantities_PerPhase n = msg.phase_N; + printf("Timestamp: %ld\n", msg.timestamp); + printf("Phase A: microamps:%f microvolts:%f\n", a.microAmps, + a.microVolts); + printf("Phase B: microamps: %f microvolts:%f\n", b.microAmps, + b.microVolts); + printf("Phase C: microamps: %f microvolts:%f\n", c.microAmps, + c.microVolts); + printf("Phase N: microamps: %f microvolts:%f\n", n.microAmps, + n.microVolts); + + pb_release(GeisaInstantaneousQuantities_fields, &msg); +} + +void handle_discovery(const void *payload, size_t len) +{ + bool status; + pb_istream_t istream; + GeisaPlatformDiscovery_Rsp msg = + GeisaPlatformDiscovery_Rsp_init_default; + + istream = pb_istream_from_buffer(payload, len); + status = pb_decode(&istream, GeisaPlatformDiscovery_Rsp_fields, &msg); + if (!status) { + printf("Failed to decode GeisaPlatformDiscoveryRsp: %s\n", + PB_GET_ERROR(&istream)); + return; + } + GeisaPlatformDiscovery_Module *sub_module = msg.device.sub_module; + if (!msg.has_geisa) { + printf("GEISA field is missing in the response\n"); + } + printf("GEISA: ver_major=%d ver_minor=%d\n", msg.geisa.ver_major, + msg.geisa.ver_minor); + printf("Device: top_model=%s sub_model=%s\n", + msg.device.top_module.model, sub_module[0].model); + printf("waveform: data_connection=%s, sample_rate=%d\n", + msg.waveform.streams[0].description, + msg.waveform.streams[0].sample_rate); + + pb_release(GeisaPlatformDiscovery_Rsp_fields, &msg); +} + +void handle_manifest(const void *payload, size_t len) +{ + bool status; + pb_istream_t istream; + GeisaApplicationDeploymentManifest_Rsp msg = + GeisaApplicationDeploymentManifest_Rsp_init_default; + + istream = pb_istream_from_buffer(payload, len); + status = pb_decode(&istream, + GeisaApplicationDeploymentManifest_Rsp_fields, &msg); + if (!status) { + printf("Failed to decode ApplicationManifestRsps\n"); + return; + } + printf("manifest:%s\n", msg.manifest); + + pb_release(GeisaApplicationDeploymentManifest_Rsp_fields, &msg); +} + +void handle_waveform(const void *payload, size_t len) +{ + bool status; + pb_istream_t istream; + GeisaWaveform_Rsp msg = GeisaWaveform_Rsp_init_default; + + istream = pb_istream_from_buffer(payload, len); + status = pb_decode(&istream, GeisaWaveform_Rsp_fields, &msg); + if (!status) { + printf("Failed to decode WaveformRsp\n"); + return; + } + printf("waveform status: %d\n", msg.waveform_status); + + pb_release(GeisaWaveform_Rsp_fields, &msg); +} + +void on_message(struct mosquitto *mosq, void *obj, + const struct mosquitto_message *msg) +{ + (void)mosq; + (void)obj; + printf("Received message on topic: %s\n", msg->topic); + if (strcmp(msg->topic, "geisa/api/instantaneous/data") == 0) { + handle_instantaneous(msg->payload, msg->payloadlen); + } else if (strcmp(msg->topic, + "geisa/api/platform/discovery/rsp/testapp") == 0) { + handle_discovery(msg->payload, msg->payloadlen); + } else if (strcmp(msg->topic, "geisa/api/app/manifest/rsp/testapp") == + 0) { + handle_manifest(msg->payload, msg->payloadlen); + } else if (strcmp(msg->topic, "geisa/api/waveform/rsp/testapp") == 0) { + handle_waveform(msg->payload, msg->payloadlen); + } else { + printf("Unknown topic\n"); + } +} + +int main() +{ + struct mosquitto *mosq; + mosquitto_lib_init(); + mosq = mosquitto_new(NULL, true, NULL); + if (!mosq) { + perror("mosquitto_new"); + return 1; + } + mosquitto_connect_callback_set(mosq, on_connect); + mosquitto_message_callback_set(mosq, on_message); + mosquitto_username_pw_set(mosq, "testapp", "testapp"); + if (mosquitto_connect(mosq, BROKER, PORT, 60) != MOSQ_ERR_SUCCESS) { + fprintf(stderr, "Unable to connect\n"); + return 1; + } + mosquitto_loop_forever(mosq, -1, 1); + mosquitto_destroy(mosq); + mosquitto_lib_cleanup(); + return 0; +} From 8adfbdb2dec4d347e0412e0c54cad6841d2b5a8e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?K=C3=A9vin=20L=27h=C3=B4pital?= Date: Thu, 28 May 2026 18:03:37 +0200 Subject: [PATCH 41/64] gapi_discovery: update discovery response following schemas updates MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Geisa schemas for platform discovery and manifest have added new fields, so update the mockup implementation to fill those fields in the responses. Update the test app to print the new fields in the discovery response. SFL: #22399 Change-Id: Iddabb3f1be890083107aba6e01c5307e24cd034b Signed-off-by: Kévin L'hôpital --- .../nanopb_options/discovery.options | 2 + .../nanopb_options/geisa-status.options | 2 + .../api-mockup/nanopb_options/sensor.options | 1 + .../api/api-mockup/src/gapi_discovery.c | 134 +++++++++++++++--- .../api/api-mockup/test/test_app.c | 12 ++ 5 files changed, 133 insertions(+), 18 deletions(-) create mode 100644 example-implementations/api/api-mockup/nanopb_options/geisa-status.options create mode 100644 example-implementations/api/api-mockup/nanopb_options/sensor.options diff --git a/example-implementations/api/api-mockup/nanopb_options/discovery.options b/example-implementations/api/api-mockup/nanopb_options/discovery.options index e243e4c..90c20e4 100644 --- a/example-implementations/api/api-mockup/nanopb_options/discovery.options +++ b/example-implementations/api/api-mockup/nanopb_options/discovery.options @@ -2,3 +2,5 @@ GeisaPlatformDiscovery_Device.sub_module type:FT_POINTER GeisaPlatformDiscovery_Waveform_Instance.description max_size:64 GeisaPlatformDiscovery_Waveform.streams type:FT_POINTER +GeisaPlatformDiscovery_Sensor.sensors type:FT_POINTER +GeisaPlatformDiscovery_Network.interfaces type:FT_POINTER diff --git a/example-implementations/api/api-mockup/nanopb_options/geisa-status.options b/example-implementations/api/api-mockup/nanopb_options/geisa-status.options new file mode 100644 index 0000000..b82cf1f --- /dev/null +++ b/example-implementations/api/api-mockup/nanopb_options/geisa-status.options @@ -0,0 +1,2 @@ +GeisaStatus.message type:FT_POINTER +GeisaStatus.details type:FT_POINTER diff --git a/example-implementations/api/api-mockup/nanopb_options/sensor.options b/example-implementations/api/api-mockup/nanopb_options/sensor.options new file mode 100644 index 0000000..66b662d --- /dev/null +++ b/example-implementations/api/api-mockup/nanopb_options/sensor.options @@ -0,0 +1 @@ +*.* max_size:32 diff --git a/example-implementations/api/api-mockup/src/gapi_discovery.c b/example-implementations/api/api-mockup/src/gapi_discovery.c index 7b153bc..90bebbb 100644 --- a/example-implementations/api/api-mockup/src/gapi_discovery.c +++ b/example-implementations/api/api-mockup/src/gapi_discovery.c @@ -53,12 +53,69 @@ static GeisaPlatformDiscovery_Operator operator_platform_info = { .operator_identifier = "SCE001", }; -static GeisaPlatformDiscovery_Metrology metrology_platform_info = - GeisaPlatformDiscovery_Metrology_init_default; -static GeisaPlatformDiscovery_Sensor sensor_platform_info = - GeisaPlatformDiscovery_Sensor_init_default; -static GeisaPlatformDiscovery_Network network_platform_info = - GeisaPlatformDiscovery_Network_init_default; +static GeisaPlatformDiscovery_Metrology metrology_platform_info = { + .meter_rating_class = "A", + .meter_form = "1S", + .phase_count = 3, + .neutral_connected = true, + .nominal_phase_angle_deg = 120, + .nominal_frequency_hz = 60, + .nominal_phase_to_phase_voltage_v = 208, + .nominal_phase_to_neutral_voltage_v = 120, +}; + +static GeisaSensorDescriptor sensors_platform_info_descriptors[] = { + { + .sensor_type = GeisaSensorType_GEISA_SENSOR_TYPE_TEMPERATURE, + .name = "Ambient Temperature", + .has_name = true, + .unit = "Celsius", + .supports_read = true, + .supports_publish = false, + }, + { + .sensor_type = GeisaSensorType_GEISA_SENSOR_TYPE_HUMIDITY, + .manufacturer = "SCE", + .has_manufacturer = true, + .model = "HUMID-1000", + .has_model = true, + .geolocation = + { + .latitude = 34.0522, + .longitude = -118.2437, + }, + .has_geolocation = true, + .unit = "Percent", + .supports_read = true, + .supports_publish = false, + }, +}; + +static GeisaPlatformDiscovery_Sensor sensor_platform_info = { + .sensors_count = 2, + .sensors = sensors_platform_info_descriptors, +}; + +static GeisaPlatformDiscovery_Network_Instance network_interfaces_platform_info[] = { + { + .interface_id = "eth0", + .network_class = + GeisaPlatformDiscovery_NetworkClass_NETWORK_CLASS_INTERNET, + .owner = + GeisaPlatformDiscovery_NetworkOwner_NETWORK_OWNER_OPERATOR, + .technology = + GeisaPlatformDiscovery_NetworkTechnology_NETWORK_TECHNOLOGY_ETHERNET, + .supports_ipv4 = true, + .supports_ipv6 = false, + .has_name = true, + .name = "Ethernet 0", + }, +}; + +static GeisaPlatformDiscovery_Network network_platform_info = { + .interfaces_count = 1, + .interfaces = network_interfaces_platform_info, +}; static const GeisaWaveform_Datatype waveform_data_type_platform_info = GeisaWaveform_Datatype_DATA_INT32; @@ -91,17 +148,30 @@ static GeisaPlatformDiscovery_Waveform waveform_platform_info = { .streams = &waveform_platform_instances, }; +static GeisaStatus geisa_discovery_success_status = { + .code = GeisaStatusCode_GEISA_STATUS_SUCCESS, + .message = "Platform discovery successful", +}; + +static GeisaStatus geisa_discovery_payload_status = { + .code = GeisaStatusCode_GEISA_STATUS_CODE_REQUEST_MALFORMED_PAYLOAD, + .message = "Malformed payload in platform discovery request", +}; + +static GeisaStatus geisa_manifest_success_status = { + .code = GeisaStatusCode_GEISA_STATUS_SUCCESS, + .message = "App manifest retrieval successful", +}; + +static GeisaStatus geisa_manifest_payload_status = { + .code = GeisaStatusCode_GEISA_STATUS_CODE_REQUEST_MALFORMED_PAYLOAD, + .message = "Malformed payload in app manifest request", +}; + char *deployment_manifest = "{" "\"geisa-application-manifest\":{" "\"manifest\":{" - "\"app-id\":\"com.example.sce.app\"," - "\"author\":\"SCE\"," - "\"name\":\"API-mockup example\"," - "\"description\":\"API-mockup example\"," - "\"app-version\":\"1.2.3-beta\"," - "\"manifest-version\":\"1.0.0\"," - "\"api-access\":{" "\"actuator\":true," "\"messaging\":true," @@ -110,6 +180,13 @@ char *deployment_manifest = "\"waveform\":false" "}," + "\"app-id\":\"com.example.sce.app\"," + "\"author\":\"SCE\"," + "\"name\":\"API-mockup example\"," + "\"description\":\"API-mockup example\"," + "\"app-version\":\"1.2.3-beta\"," + "\"manifest-version\":\"1.0.0\"," + "\"artifacts\":[{" "\"image-name\":\"api_mockup_v1.2.3.img\"," "\"image-type\":\"appoverlay\"," @@ -138,7 +215,20 @@ char *deployment_manifest = "\"communication\":{" "\"FAN\":true," "\"HAN\":false," - "\"messaging\":{\"daily-messages\":5000}" + "\"messaging\":{\"daily-messages\":5000}," + "\"operator\":{" + "\"daily-volume\": 5000000," + "\"inbound\": [\"TCP:10.1.5.22:8080\"]," + "\"outbound\": [\"TCP:10.1.5.1:443\"]" + "}," + "\"internet\": {" + "\"daily-volume\": 1000000," + "\"outbound\": [\"TCP:8.8.8.8:53\"]" + "}," + "\"local\": {" + "\"daily-volume\": 500000," + "\"inbound\": [\"TCP:127.0.0.1:6500\"]" + "}" "}," "\"external-dependencies\":[" @@ -174,6 +264,8 @@ char *deployment_manifest = static void api_platform_discovery_build_response(GeisaPlatformDiscovery_Rsp *response) { + response->status = geisa_discovery_success_status; + response->has_status = true; response->geisa = geisa_platform_info; response->has_geisa = true; response->device = device_platform_info; @@ -225,11 +317,13 @@ static void api_platform_discovery_req_handler(struct mosquitto *mosq, if (!status) { fprintf(stderr, "[Discovery] Error decoding platform " "discovery request\n"); - return; + response.status = geisa_discovery_payload_status; + response.has_status = true; + } else { + api_platform_discovery_build_response(&response); } pb_release(GeisaPlatformDiscovery_Req_fields, &request); - api_platform_discovery_build_response(&response); status = pb_get_encoded_size( &encoded_size, GeisaPlatformDiscovery_Rsp_fields, &response); if (!status) { @@ -300,7 +394,12 @@ static void api_manifest_req_handler(struct mosquitto *mosq, const char *topic, if (!status) { fprintf(stderr, "[Manifest] Error decoding app manifest request\n"); - return; + response.status = geisa_manifest_payload_status; + response.has_status = true; + } else { + response.status = geisa_manifest_success_status; + response.has_status = true; + response.manifest = deployment_manifest; } pb_release(GeisaApplicationDeploymentManifest_Req_fields, &request); @@ -312,7 +411,6 @@ static void api_manifest_req_handler(struct mosquitto *mosq, const char *topic, return; } - response.manifest = deployment_manifest; status = pb_get_encoded_size( &encoded_size, GeisaApplicationDeploymentManifest_Rsp_fields, &response); diff --git a/example-implementations/api/api-mockup/test/test_app.c b/example-implementations/api/api-mockup/test/test_app.c index f3a0f62..790a34f 100644 --- a/example-implementations/api/api-mockup/test/test_app.c +++ b/example-implementations/api/api-mockup/test/test_app.c @@ -84,6 +84,7 @@ void handle_discovery(const void *payload, size_t len) if (!msg.has_geisa) { printf("GEISA field is missing in the response\n"); } + printf("Status: %s\n", msg.status.message); printf("GEISA: ver_major=%d ver_minor=%d\n", msg.geisa.ver_major, msg.geisa.ver_minor); printf("Device: top_model=%s sub_model=%s\n", @@ -91,6 +92,16 @@ void handle_discovery(const void *payload, size_t len) printf("waveform: data_connection=%s, sample_rate=%d\n", msg.waveform.streams[0].description, msg.waveform.streams[0].sample_rate); + printf("Metrology: meter_rating_class:%s, neutral_connected:%d\n", + msg.metrology.meter_rating_class, + msg.metrology.neutral_connected); + printf("Sensors: sensor1_name:%s sensor1_type:%d\n", + msg.sensor.sensors[0].name, msg.sensor.sensors[0].sensor_type); + printf("Sensors: sensor2_model:%s sensor2_latitude:%f\n", + msg.sensor.sensors[1].model, + msg.sensor.sensors[1].geolocation.latitude); + printf("Network: interface1_id:%s\n", + msg.network.interfaces[0].interface_id); pb_release(GeisaPlatformDiscovery_Rsp_fields, &msg); } @@ -109,6 +120,7 @@ void handle_manifest(const void *payload, size_t len) printf("Failed to decode ApplicationManifestRsps\n"); return; } + printf("Status: %s\n", msg.status.message); printf("manifest:%s\n", msg.manifest); pb_release(GeisaApplicationDeploymentManifest_Rsp_fields, &msg); From 897b8a51a209698f72ffc586ff1eb20b7c432146 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?K=C3=A9vin=20L=27h=C3=B4pital?= Date: Fri, 29 May 2026 10:44:19 +0200 Subject: [PATCH 42/64] README: update required dependencies following schemas update MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit SFL: #22399 Change-Id: I8ab4031766526219894fcca29baf7fdda7678347 Signed-off-by: Kévin L'hôpital --- example-implementations/api/api-mockup/README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/example-implementations/api/api-mockup/README.md b/example-implementations/api/api-mockup/README.md index 2acc9be..359f40f 100644 --- a/example-implementations/api/api-mockup/README.md +++ b/example-implementations/api/api-mockup/README.md @@ -23,9 +23,9 @@ To build the GEISA API Mockup application, you need to have the following tools - make - pkg-config - protobuf-compiler - - protobuf-c-compiler - libmosquitto-dev - - libprotobuf-c-dev + - libnanopb-dev + - nanopb (python package: pip3 install --break-system-packages nanopb) A docker support is also available to launch the compilation, it requires: - cqfd (See [requirements](https://github.com/savoirfairelinux/cqfd?tab=readme-ov-file#requirements) and [installation](https://github.com/savoirfairelinux/cqfd?tab=readme-ov-file#installingremoving-cqfd) steps on github) From e5c383dba4ec5a86b18e1eec0eadfb87f3f63e43 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?K=C3=A9vin=20L=27h=C3=B4pital?= Date: Fri, 29 May 2026 10:45:06 +0200 Subject: [PATCH 43/64] cqfd: add possibility to compile for arm64 architecture MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit As GEISA platform are arm64 architecture, provide an easy way to build the application for this architecture using cqfd. SFL: #22399 Change-Id: I3b9e160ef0b5ef9c7a84cc3c835af2601d4b231a Signed-off-by: Kévin L'hôpital --- example-implementations/api/api-mockup/.cqfdrc | 5 +++++ example-implementations/api/api-mockup/README.md | 8 ++++++++ 2 files changed, 13 insertions(+) diff --git a/example-implementations/api/api-mockup/.cqfdrc b/example-implementations/api/api-mockup/.cqfdrc index 068f1b8..b4ef289 100644 --- a/example-implementations/api/api-mockup/.cqfdrc +++ b/example-implementations/api/api-mockup/.cqfdrc @@ -7,3 +7,8 @@ command='make' [lint] command='make lint' + +[build_arm64] +command='make' +docker_run_args='--platform linux/arm64' +docker_build_args='--platform linux/arm64' diff --git a/example-implementations/api/api-mockup/README.md b/example-implementations/api/api-mockup/README.md index 359f40f..6409e06 100644 --- a/example-implementations/api/api-mockup/README.md +++ b/example-implementations/api/api-mockup/README.md @@ -44,6 +44,14 @@ To build the GEISA API Mockup application using cqfd, run the following command cqfd run ``` +## Build the application for arm64 +To be able to build the application for arm64 architecture, so that you can test directly on an arm64 device, you can use the following commands: + +```bash +cqfd -b build_arm64 init +cqfd -b build_arm64 +``` + ## Launch the application To launch the GEISA API Mockup application, simply run the following command: From 2ff2245b0e4549312838aaadaa643309f6daa92a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?K=C3=A9vin=20L=27h=C3=B4pital?= Date: Fri, 29 May 2026 11:02:02 +0200 Subject: [PATCH 44/64] gapi: split discovery and manifest handlers into separate files MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit As the new schemas is now defining the manifest outside the discovery proto, split the discovery and manifest handlers into separate files to better reflect the new structure and improve code organization. SFL: #22399 Change-Id: Iaebf290173a1642eb27dfac1c284e46c2c745791 Signed-off-by: Kévin L'hôpital --- example-implementations/api/api-mockup/gapi.c | 2 + .../api/api-mockup/include/gapi_discovery.h | 1 - .../api/api-mockup/include/gapi_manifest.h | 25 +++ .../api/api-mockup/src/gapi_discovery.c | 185 ---------------- .../api/api-mockup/src/gapi_manifest.c | 197 ++++++++++++++++++ 5 files changed, 224 insertions(+), 186 deletions(-) create mode 100644 example-implementations/api/api-mockup/include/gapi_manifest.h create mode 100644 example-implementations/api/api-mockup/src/gapi_manifest.c diff --git a/example-implementations/api/api-mockup/gapi.c b/example-implementations/api/api-mockup/gapi.c index b1f2fa2..f44d2d7 100644 --- a/example-implementations/api/api-mockup/gapi.c +++ b/example-implementations/api/api-mockup/gapi.c @@ -5,6 +5,7 @@ */ #include "gapi_discovery.h" #include "gapi_instantaneous.h" +#include "gapi_manifest.h" #include "gapi_mosquitto.h" #include "gapi_waveform.h" @@ -31,6 +32,7 @@ int main() } api_discovery_init(mosq); + api_manifest_init(mosq); ret = api_instantaneous_init(&instantaneous_thread, mosq); if (ret != EXIT_SUCCESS) { goto deinit; diff --git a/example-implementations/api/api-mockup/include/gapi_discovery.h b/example-implementations/api/api-mockup/include/gapi_discovery.h index b9abc65..890a1c8 100644 --- a/example-implementations/api/api-mockup/include/gapi_discovery.h +++ b/example-implementations/api/api-mockup/include/gapi_discovery.h @@ -13,7 +13,6 @@ #include "pb_decode.h" #include "pb_encode.h" #include "schemas/discovery.pb.h" -#include "schemas/manifest.pb.h" #include /** diff --git a/example-implementations/api/api-mockup/include/gapi_manifest.h b/example-implementations/api/api-mockup/include/gapi_manifest.h new file mode 100644 index 0000000..e5d8647 --- /dev/null +++ b/example-implementations/api/api-mockup/include/gapi_manifest.h @@ -0,0 +1,25 @@ +/** + * @file gapi_manifest.h + * @brief Header file for API application manifest messages + * @copyright Copyright (C) 2026 Southern California Edison + */ + +#ifndef GAPI_MANIFEST_H +#define GAPI_MANIFEST_H +#define _GNU_SOURCE + +#include "gapi_mosquitto.h" +#include "pb.h" +#include "pb_decode.h" +#include "pb_encode.h" +#include "schemas/manifest.pb.h" +#include + +/** + * @brief Initialize API application manifest messages + * + * @param mosq Pointer to the mosquitto instance + */ +void api_manifest_init(struct mosquitto *mosq); + +#endif // GAPI_MANIFEST_H diff --git a/example-implementations/api/api-mockup/src/gapi_discovery.c b/example-implementations/api/api-mockup/src/gapi_discovery.c index 90bebbb..a63694f 100644 --- a/example-implementations/api/api-mockup/src/gapi_discovery.c +++ b/example-implementations/api/api-mockup/src/gapi_discovery.c @@ -158,107 +158,6 @@ static GeisaStatus geisa_discovery_payload_status = { .message = "Malformed payload in platform discovery request", }; -static GeisaStatus geisa_manifest_success_status = { - .code = GeisaStatusCode_GEISA_STATUS_SUCCESS, - .message = "App manifest retrieval successful", -}; - -static GeisaStatus geisa_manifest_payload_status = { - .code = GeisaStatusCode_GEISA_STATUS_CODE_REQUEST_MALFORMED_PAYLOAD, - .message = "Malformed payload in app manifest request", -}; - -char *deployment_manifest = - "{" - "\"geisa-application-manifest\":{" - "\"manifest\":{" - "\"api-access\":{" - "\"actuator\":true," - "\"messaging\":true," - "\"instantaneous\":false," - "\"sensor\":true," - "\"waveform\":false" - "}," - - "\"app-id\":\"com.example.sce.app\"," - "\"author\":\"SCE\"," - "\"name\":\"API-mockup example\"," - "\"description\":\"API-mockup example\"," - "\"app-version\":\"1.2.3-beta\"," - "\"manifest-version\":\"1.0.0\"," - - "\"artifacts\":[{" - "\"image-name\":\"api_mockup_v1.2.3.img\"," - "\"image-type\":\"appoverlay\"," - "\"image-size\":1048576," - "\"uncompressed-size\":5242880," - "\"signature\":" - "\"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\"" - "}]," - - "\"compatibility\":{" - "\"GEISA-API\":\"1.0.0\"," - "\"GEISA-LEE\":\"1.0.0\"," - "\"GEISA-VEE\":null," - "\"toolchain-id\":\"acme-toolchain-armv7\"," - "\"toolchain-version\":\"2.3.4\"" - "}," - - "\"resources\":{" - "\"app-cpu\":10," - "\"app-ram\":65536," - "\"storage-persistent\":10240," - "\"storage-nonpersistent\":2048," - "\"threads\":2" - "}," - - "\"communication\":{" - "\"FAN\":true," - "\"HAN\":false," - "\"messaging\":{\"daily-messages\":5000}," - "\"operator\":{" - "\"daily-volume\": 5000000," - "\"inbound\": [\"TCP:10.1.5.22:8080\"]," - "\"outbound\": [\"TCP:10.1.5.1:443\"]" - "}," - "\"internet\": {" - "\"daily-volume\": 1000000," - "\"outbound\": [\"TCP:8.8.8.8:53\"]" - "}," - "\"local\": {" - "\"daily-volume\": 500000," - "\"inbound\": [\"TCP:127.0.0.1:6500\"]" - "}" - "}," - - "\"external-dependencies\":[" - "\"com.example.database\"," - "\"com.example.security\"" - "]," - - "\"default-launch-strategy\":{" - "\"auto-restart\":true," - "\"max-restarts\":5," - "\"restart-period\":60," - "\"start-timeout\":10," - "\"start-background\":true," - "\"start-string\":\"/usr/bin/start-app\"," - "\"stop-string\":\"/usr/bin/stop-app\"," - "\"stop-timeout\":10," - "\"notify-timeout\":30," - "\"watchdog\":true" - "}," - - "\"default-configuration\":{" - "\"mode\":\"production\"," - "\"message\":\"Hello World\"" - "}" - "}," - "\"signature\":" - "\"bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb\"" - "}" - "}"; - // NOLINTEND(cppcoreguidelines-interfaces-global-init,cppcoreguidelines-avoid-magic-numbers,readability-magic-numbers) static void @@ -365,94 +264,10 @@ static void api_platform_discovery_req_handler(struct mosquitto *mosq, free(message); } -static void api_manifest_req_handler(struct mosquitto *mosq, const char *topic, - const int payloadlen, - const uint8_t *payload) -{ - GeisaApplicationDeploymentManifest_Req request = - GeisaApplicationDeploymentManifest_Req_init_default; - GeisaApplicationDeploymentManifest_Rsp response = - GeisaApplicationDeploymentManifest_Rsp_init_default; - size_t encoded_size = 0; - uint8_t *message = NULL; - char *rsp_topic = NULL; - char *app_id = NULL; - pb_istream_t istream; - pb_ostream_t ostream; - bool status = false; - - app_id = basename((char *)topic); - - fprintf(stdout, "[Manifest] Received app manifest request from %s\n", - app_id); - fflush(stdout); - - istream = pb_istream_from_buffer(payload, payloadlen); - status = pb_decode(&istream, - GeisaApplicationDeploymentManifest_Req_fields, - &request); - if (!status) { - fprintf(stderr, - "[Manifest] Error decoding app manifest request\n"); - response.status = geisa_manifest_payload_status; - response.has_status = true; - } else { - response.status = geisa_manifest_success_status; - response.has_status = true; - response.manifest = deployment_manifest; - } - pb_release(GeisaApplicationDeploymentManifest_Req_fields, &request); - - if (asprintf(&rsp_topic, "geisa/api/app/manifest/rsp/%s", app_id) == - -1) { - fprintf(stderr, - "[Manifest] Error allocating memory for response " - "topic\n"); - return; - } - - status = pb_get_encoded_size( - &encoded_size, GeisaApplicationDeploymentManifest_Rsp_fields, - &response); - if (!status) { - fprintf(stderr, "[Manifest] Error calculating size of " - "app manifest response\n"); - return; - } - - message = malloc(encoded_size); - if (message == NULL) { - fprintf(stderr, - "[Manifest] Error allocating memory for response " - "message\n"); - free(rsp_topic); - return; - } - - ostream = pb_ostream_from_buffer(message, encoded_size); - status = pb_encode(&ostream, - GeisaApplicationDeploymentManifest_Rsp_fields, - &response); - if (!status) { - fprintf(stderr, "[Manifest] Error encoding app manifest " - "response\n"); - free(rsp_topic); - free(message); - return; - } - - api_publish(mosq, rsp_topic, encoded_size, message, 1); - free(rsp_topic); - free(message); -} - void api_discovery_init(struct mosquitto *mosq) { - api_register_handler("geisa/api/app/manifest/req", - api_manifest_req_handler); api_register_handler("geisa/api/platform/discovery/req", api_platform_discovery_req_handler); api_subscribe(mosq, "geisa/api/platform/discovery/req/#", 1); - api_subscribe(mosq, "geisa/api/app/manifest/req/#", 1); } diff --git a/example-implementations/api/api-mockup/src/gapi_manifest.c b/example-implementations/api/api-mockup/src/gapi_manifest.c new file mode 100644 index 0000000..7e95273 --- /dev/null +++ b/example-implementations/api/api-mockup/src/gapi_manifest.c @@ -0,0 +1,197 @@ +/** + * @file gapi_manifest.c + * @brief Definition file for API application manifest messages + * @copyright Copyright (C) 2026 Southern California Edison + */ + +#include "gapi_manifest.h" + +static GeisaStatus geisa_manifest_success_status = { + .code = GeisaStatusCode_GEISA_STATUS_SUCCESS, + .message = "App manifest retrieval successful", +}; + +static GeisaStatus geisa_manifest_payload_status = { + .code = GeisaStatusCode_GEISA_STATUS_CODE_REQUEST_MALFORMED_PAYLOAD, + .message = "Malformed payload in app manifest request", +}; + +char *deployment_manifest = + "{" + "\"geisa-application-manifest\":{" + "\"manifest\":{" + "\"api-access\":{" + "\"actuator\":true," + "\"messaging\":true," + "\"instantaneous\":false," + "\"sensor\":true," + "\"waveform\":false" + "}," + + "\"app-id\":\"com.example.sce.app\"," + "\"author\":\"SCE\"," + "\"name\":\"API-mockup example\"," + "\"description\":\"API-mockup example\"," + "\"app-version\":\"1.2.3-beta\"," + "\"manifest-version\":\"1.0.0\"," + + "\"artifacts\":[{" + "\"image-name\":\"api_mockup_v1.2.3.img\"," + "\"image-type\":\"appoverlay\"," + "\"image-size\":1048576," + "\"uncompressed-size\":5242880," + "\"signature\":" + "\"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\"" + "}]," + + "\"compatibility\":{" + "\"GEISA-API\":\"1.0.0\"," + "\"GEISA-LEE\":\"1.0.0\"," + "\"GEISA-VEE\":null," + "\"toolchain-id\":\"acme-toolchain-armv7\"," + "\"toolchain-version\":\"2.3.4\"" + "}," + + "\"resources\":{" + "\"app-cpu\":10," + "\"app-ram\":65536," + "\"storage-persistent\":10240," + "\"storage-nonpersistent\":2048," + "\"threads\":2" + "}," + + "\"communication\":{" + "\"FAN\":true," + "\"HAN\":false," + "\"messaging\":{\"daily-messages\":5000}," + "\"operator\":{" + "\"daily-volume\": 5000000," + "\"inbound\": [\"TCP:10.1.5.22:8080\"]," + "\"outbound\": [\"TCP:10.1.5.1:443\"]" + "}," + "\"internet\": {" + "\"daily-volume\": 1000000," + "\"outbound\": [\"TCP:8.8.8.8:53\"]" + "}," + "\"local\": {" + "\"daily-volume\": 500000," + "\"inbound\": [\"TCP:127.0.0.1:6500\"]" + "}" + "}," + + "\"external-dependencies\":[" + "\"com.example.database\"," + "\"com.example.security\"" + "]," + + "\"default-launch-strategy\":{" + "\"auto-restart\":true," + "\"max-restarts\":5," + "\"restart-period\":60," + "\"start-timeout\":10," + "\"start-background\":true," + "\"start-string\":\"/usr/bin/start-app\"," + "\"stop-string\":\"/usr/bin/stop-app\"," + "\"stop-timeout\":10," + "\"notify-timeout\":30," + "\"watchdog\":true" + "}," + + "\"default-configuration\":{" + "\"mode\":\"production\"," + "\"message\":\"Hello World\"" + "}" + "}," + "\"signature\":" + "\"bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb\"" + "}" + "}"; + +static void api_manifest_req_handler(struct mosquitto *mosq, const char *topic, + const int payloadlen, + const uint8_t *payload) +{ + GeisaApplicationDeploymentManifest_Req request = + GeisaApplicationDeploymentManifest_Req_init_default; + GeisaApplicationDeploymentManifest_Rsp response = + GeisaApplicationDeploymentManifest_Rsp_init_default; + size_t encoded_size = 0; + uint8_t *message = NULL; + char *rsp_topic = NULL; + char *app_id = NULL; + pb_istream_t istream; + pb_ostream_t ostream; + bool status = false; + + app_id = basename((char *)topic); + + fprintf(stdout, "[Manifest] Received app manifest request from %s\n", + app_id); + fflush(stdout); + + istream = pb_istream_from_buffer(payload, payloadlen); + status = pb_decode(&istream, + GeisaApplicationDeploymentManifest_Req_fields, + &request); + if (!status) { + fprintf(stderr, + "[Manifest] Error decoding app manifest request\n"); + response.status = geisa_manifest_payload_status; + response.has_status = true; + } else { + response.status = geisa_manifest_success_status; + response.has_status = true; + response.manifest = deployment_manifest; + } + pb_release(GeisaApplicationDeploymentManifest_Req_fields, &request); + + if (asprintf(&rsp_topic, "geisa/api/app/manifest/rsp/%s", app_id) == + -1) { + fprintf(stderr, + "[Manifest] Error allocating memory for response " + "topic\n"); + return; + } + + status = pb_get_encoded_size( + &encoded_size, GeisaApplicationDeploymentManifest_Rsp_fields, + &response); + if (!status) { + fprintf(stderr, "[Manifest] Error calculating size of " + "app manifest response\n"); + return; + } + + message = malloc(encoded_size); + if (message == NULL) { + fprintf(stderr, + "[Manifest] Error allocating memory for response " + "message\n"); + free(rsp_topic); + return; + } + + ostream = pb_ostream_from_buffer(message, encoded_size); + status = pb_encode(&ostream, + GeisaApplicationDeploymentManifest_Rsp_fields, + &response); + if (!status) { + fprintf(stderr, "[Manifest] Error encoding app manifest " + "response\n"); + free(rsp_topic); + free(message); + return; + } + + api_publish(mosq, rsp_topic, encoded_size, message, 1); + free(rsp_topic); + free(message); +} + +void api_manifest_init(struct mosquitto *mosq) +{ + api_register_handler("geisa/api/app/manifest/req", + api_manifest_req_handler); + + api_subscribe(mosq, "geisa/api/app/manifest/req/#", 1); +} From 70df14bfcba0bd5818142dbcbf507e3e7f398830 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?K=C3=A9vin=20L=27h=C3=B4pital?= Date: Fri, 29 May 2026 11:13:07 +0200 Subject: [PATCH 45/64] gapi_instantaneous: update instantaneous data with latest schemas MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Latest instantaneous data schema includes frequency and temperature. This commit updates the instantaneous data with these new fields. SFL: #22399 Change-Id: I97fb466952394a68e6559a3243e7100d2d9eb4a2 Signed-off-by: Kévin L'hôpital --- .../api/api-mockup/src/gapi_instantaneous.c | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/example-implementations/api/api-mockup/src/gapi_instantaneous.c b/example-implementations/api/api-mockup/src/gapi_instantaneous.c index ec7ca4d..04fa0ee 100644 --- a/example-implementations/api/api-mockup/src/gapi_instantaneous.c +++ b/example-implementations/api/api-mockup/src/gapi_instantaneous.c @@ -102,8 +102,6 @@ static void geisa_get_instantaneous_data(GeisaInstantaneousQuantities *response) other.Neutral_Imputed_microAmps = 5.5F; other.Load_Side_microVolts = 123.0F; - // NOLINTEND(readability-magic-numbers,cppcoreguidelines-avoid-magic-numbers) - response->timestamp = timestamp; response->phase_A = phase_a; response->has_phase_A = true; @@ -115,6 +113,10 @@ static void geisa_get_instantaneous_data(GeisaInstantaneousQuantities *response) response->has_phase_N = true; response->other = other; response->has_other = true; + response->frequency_hz = 60.0F; + response->temperature_celsius = 25.0F; + + // NOLINTEND(readability-magic-numbers,cppcoreguidelines-avoid-magic-numbers) } static void *gapi_instantaneous_thread(void *arg) From b9f8020078106d71d1b9a1f65a5b45188f2b6f49 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?K=C3=A9vin=20L=27h=C3=B4pital?= Date: Fri, 29 May 2026 16:10:22 +0200 Subject: [PATCH 46/64] gapi_waveform: update waveform with latest schemas MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Latest waveform schema now includes request and expect waveform status depending on the result of the request, as well as a socket path to be used by the app to receive waveform data. The API implementation is updated to reflect these changes, and to create and manage Unix domain sockets for each app subscribing to a waveform stream. A deinit function is also added to clean up the created sockets on shutdown. The test app is also updated to print the new fields in the response message. SFL: #22399 Change-Id: I0124a1caf455f960840364886792979b787413e1 Signed-off-by: Kévin L'hôpital --- .../api/api-mockup/.clang-format | 1 + example-implementations/api/api-mockup/gapi.c | 1 + .../api/api-mockup/include/gapi_waveform.h | 9 + .../nanopb_options/waveform.options | 1 + .../api/api-mockup/src/gapi_waveform.c | 371 +++++++++++++++++- .../api/api-mockup/test/test_app.c | 3 +- 6 files changed, 369 insertions(+), 17 deletions(-) diff --git a/example-implementations/api/api-mockup/.clang-format b/example-implementations/api/api-mockup/.clang-format index c371f58..a14da4f 100644 --- a/example-implementations/api/api-mockup/.clang-format +++ b/example-implementations/api/api-mockup/.clang-format @@ -5,3 +5,4 @@ BreakBeforeBraces: Linux AllowShortIfStatementsOnASingleLine: false IndentCaseLabels: false ContinuationIndentWidth: 8 +BreakStringLiterals: false diff --git a/example-implementations/api/api-mockup/gapi.c b/example-implementations/api/api-mockup/gapi.c index f44d2d7..513c705 100644 --- a/example-implementations/api/api-mockup/gapi.c +++ b/example-implementations/api/api-mockup/gapi.c @@ -58,5 +58,6 @@ int main() deinit: api_communication_deinit(mosq); + api_waveform_deinit(); return ret; } diff --git a/example-implementations/api/api-mockup/include/gapi_waveform.h b/example-implementations/api/api-mockup/include/gapi_waveform.h index 4c4faae..c846afe 100644 --- a/example-implementations/api/api-mockup/include/gapi_waveform.h +++ b/example-implementations/api/api-mockup/include/gapi_waveform.h @@ -13,7 +13,11 @@ #include "pb_decode.h" #include "pb_encode.h" #include "schemas/waveform.pb.h" +#include #include +#include +#include +#include /** * @brief Initialize API waveform data messages @@ -22,4 +26,9 @@ */ void api_waveform_init(struct mosquitto *mosq); +/** + * @brief Deinitialize API waveform data messages + */ +void api_waveform_deinit(); + #endif // GAPI_WAVEFORM_H diff --git a/example-implementations/api/api-mockup/nanopb_options/waveform.options b/example-implementations/api/api-mockup/nanopb_options/waveform.options index 0758eac..db046c1 100644 --- a/example-implementations/api/api-mockup/nanopb_options/waveform.options +++ b/example-implementations/api/api-mockup/nanopb_options/waveform.options @@ -1,2 +1,3 @@ GeisaWaveform_Req.stream_id type:FT_POINTER GeisaWaveform_Rsp.stream_id type:FT_POINTER +GeisaWaveform_Rsp.socket_path type:FT_POINTER diff --git a/example-implementations/api/api-mockup/src/gapi_waveform.c b/example-implementations/api/api-mockup/src/gapi_waveform.c index ce47b7f..e65414f 100644 --- a/example-implementations/api/api-mockup/src/gapi_waveform.c +++ b/example-implementations/api/api-mockup/src/gapi_waveform.c @@ -7,13 +7,315 @@ #include "gapi_waveform.h" #include "gapi_discovery.h" -static void -api_platform_waveform_build_response(GeisaWaveform_Rsp *response, - char *stream_id, - GeisaWaveform_RequestType request_type) +enum { MAX_WAVEFORM_SOCKETS = 16 }; + +typedef struct { + char *app_id; + char *stream_id; + int fd; +} SocketRecord; + +static SocketRecord w_sockets[MAX_WAVEFORM_SOCKETS]; +static int w_socket_count = 0; + +static int add_socket(const char *app_id, const char *stream_id, + int file_descriptor) +{ + if (w_socket_count >= MAX_WAVEFORM_SOCKETS) { + return -1; + } + for (int i = 0; i < w_socket_count; i++) { + if (strcmp(w_sockets[i].app_id, app_id) == 0 && + strcmp(w_sockets[i].stream_id, stream_id) == 0) { + return -2; + } + } + w_sockets[w_socket_count].app_id = strdup(app_id); + w_sockets[w_socket_count].stream_id = strdup(stream_id); + w_sockets[w_socket_count].fd = file_descriptor; + w_socket_count++; + return 0; +} + +static int find_socket(const char *app_id, const char *stream_id) +{ + for (int i = 0; i < w_socket_count; i++) { + if (strcmp(w_sockets[i].app_id, app_id) == 0 && + strcmp(w_sockets[i].stream_id, stream_id) == 0) { + return i; + } + } + return -1; +} + +static int remove_socket(const char *app_id, const char *stream_id) +{ + int idx = find_socket(app_id, stream_id); + if (idx == -1) { + return -1; + } + free(w_sockets[idx].app_id); + free(w_sockets[idx].stream_id); + close(w_sockets[idx].fd); + + for (int i = idx; i < w_socket_count - 1; i++) { + w_sockets[i] = w_sockets[i + 1]; + } + w_socket_count--; + return 0; +} + +char *get_dirname(const char *path) +{ + char *path_copy = strdup(path); + char *dir = dirname(path_copy); + char *result = strdup(dir); + + free(path_copy); + + return result; +} + +static int handle_socket_creation(char *app_id, char *stream_id, + char *socket_path) +{ + int permissions = S_IRUSR | S_IWUSR | S_IRGRP | S_IROTH; + struct sockaddr_un addr; + char *dir_path = NULL; + struct stat statbuf = {0}; + int file_descriptor = 0; + + if (find_socket(app_id, stream_id) != -1) { + fprintf(stderr, + "[Waveform] App %s is already subscribed to stream " + "%s\n", + app_id, stream_id); + return 1; + } + + file_descriptor = socket(AF_UNIX, SOCK_SEQPACKET, 0); + if (file_descriptor == -1) { + fprintf(stderr, + "[Waveform] Error creating socket for app %s and stream" + "%s\n", + app_id, stream_id); + return 2; + } + + dir_path = get_dirname(socket_path); + if (stat(dir_path, &statbuf) == -1) { + if (mkdir(dir_path, permissions) == -1) { + fprintf(stderr, + "[Waveform] Error creating directory for socket for app %s and stream %s\n", + app_id, stream_id); + close(file_descriptor); + free(dir_path); + return 2; + } + } + free(dir_path); + + memset(&addr, 0, sizeof(addr)); + addr.sun_family = AF_UNIX; + strncpy(addr.sun_path, socket_path, sizeof(addr.sun_path) - 1); + + unlink(socket_path); + + if (bind(file_descriptor, (struct sockaddr *)&addr, sizeof(addr)) == + -1) { + fprintf(stderr, + "[Waveform] Error binding socket for app %s and stream " + "%s (error: %s)\n", + app_id, stream_id, strerror(errno)); + close(file_descriptor); + return 2; + } + + if (add_socket(app_id, stream_id, file_descriptor) != 0) { + fprintf(stderr, + "[Waveform] Error tracking socket for app %s stream " + "%s\n", + app_id, stream_id); + close(file_descriptor); + unlink(socket_path); + return 2; + } + + fprintf(stdout, + "[Waveform] %s successfully subscribed on stream " + "%s\n", + app_id, stream_id); + + return 0; +} + +static int handle_socket_removal(char *app_id, char *stream_id, + char *socket_path) +{ + int ret = 0; + + ret = remove_socket(app_id, stream_id); + if (ret == -1) { + fprintf(stderr, + "[Waveform] App %s is not subscribed to stream " + "%s\n", + app_id, stream_id); + return 1; + } + + if (unlink(socket_path) == -1) { + fprintf(stderr, + "[Waveform] Error removing socket for app %s and stream" + "%s\n", + app_id, stream_id); + return 2; + } + + fprintf(stdout, + "[Waveform] %s successfully unsubscribed on stream " + "%s\n", + app_id, stream_id); + + return 0; +} + +static GeisaWaveform_SampleType +get_sample_type(GeisaWaveform_Datatype sample_type) +{ + switch (sample_type) { + case GeisaWaveform_Datatype_DATA_INT16: + return GeisaWaveform_SampleType_WAVEFORM_SAMPLE_TYPE_INT16; + case GeisaWaveform_Datatype_DATA_INT32: + return GeisaWaveform_SampleType_WAVEFORM_SAMPLE_TYPE_INT32; + case GeisaWaveform_Datatype_DATA_FLOAT32: + return GeisaWaveform_SampleType_WAVEFORM_SAMPLE_TYPE_FLOAT32; + case GeisaWaveform_Datatype_DATA_FLOAT64: + return GeisaWaveform_SampleType_WAVEFORM_SAMPLE_TYPE_FLOAT64; + default: + return GeisaWaveform_SampleType_WAVEFORM_SAMPLE_TYPE_UNSPECIFIED; + } +} + +static void waveform_handle_stream_request( + GeisaWaveform_Rsp *response, GeisaWaveform_RequestType request_type, + int stream_number, char *app_id, + GeisaPlatformDiscovery_Waveform waveform_platform_info) +{ + char *socket_path = NULL; + int ret = 0; + + if (asprintf(&socket_path, "/run/geisa/waveform/%s/%s.sock", app_id, + waveform_platform_info.streams[stream_number].stream_id) == + -1) { + fprintf(stderr, + "[Waveform] Error allocating memory for socket path\n"); + response->waveform_status = + GeisaWaveform_Status_WAVEFORM_ERR_OTHER; + return; + } + + switch (request_type) { + case GeisaWaveform_RequestType_WAVEFORM_SUBSCRIBE: + response->stream_id = + waveform_platform_info.streams[stream_number].stream_id; + + ret = handle_socket_creation(app_id, response->stream_id, + socket_path); + if (ret == 1) { + response->waveform_status = + GeisaWaveform_Status_WAVEFORM_ERR_ALREADY_SUBSCRIBED; + break; + } + if (ret == 2) { + response->waveform_status = + GeisaWaveform_Status_WAVEFORM_ERR_OTHER; + break; + } + + response->subscribed = true; + response->socket_path = socket_path; + response->sample_type = get_sample_type( + waveform_platform_info.streams[stream_number].datatype); + response->voltage_channel_count = + waveform_platform_info.streams[stream_number] + .num_voltage_ch; + response->current_channel_count = + waveform_platform_info.streams[stream_number] + .num_current_ch; + response->total_channel_count = + waveform_platform_info.streams[stream_number] + .total_channel_count; + response->sample_rate_hz = + waveform_platform_info.streams[stream_number] + .sample_rate; + response->samples_per_cycle = + waveform_platform_info.streams[stream_number] + .samples_per_cycle; + response->nominal_frequency_hz = + waveform_platform_info.streams[stream_number] + .nominal_frequency_hz; + response->cycle_aligned = + waveform_platform_info.streams[stream_number] + .cycle_aligned; + response->zero_crossing_aligned = + waveform_platform_info.streams[stream_number] + .zero_crossing_aligned; + response->voltage_scale = + waveform_platform_info.streams[stream_number] + .voltage_multiplier; + response->current_scale = + waveform_platform_info.streams[stream_number] + .current_multiplier; + response->expected_frame_period_ms = + waveform_platform_info.streams[stream_number] + .expected_frame_period_ms; + response->waveform_status = + GeisaWaveform_Status_WAVEFORM_SUCCESS; + break; + case GeisaWaveform_RequestType_WAVEFORM_UNSUBSCRIBE: + response->stream_id = + waveform_platform_info.streams[stream_number].stream_id; + + ret = handle_socket_removal(app_id, response->stream_id, + socket_path); + if (ret == 1) { + response->waveform_status = + GeisaWaveform_Status_WAVEFORM_ERR_NOT_SUBSCRIBED; + break; + } + if (ret == 2) { + response->waveform_status = + GeisaWaveform_Status_WAVEFORM_ERR_OTHER; + break; + } + response->subscribed = false; + response->waveform_status = + GeisaWaveform_Status_WAVEFORM_SUCCESS; + break; + default: + response->waveform_status = + GeisaWaveform_Status_WAVEFORM_ERR_NOT_SUPPORTED; + break; + } + + free(socket_path); +} + +static GeisaStatus geisa_waveform_success_status = { + .code = GeisaStatusCode_GEISA_STATUS_SUCCESS, + .message = "Waveform request successful", +}; + +static GeisaStatus geisa_waveform_payload_status = { + .code = GeisaStatusCode_GEISA_STATUS_CODE_REQUEST_MALFORMED_PAYLOAD, + .message = "Malformed payload in waveform request", +}; + +static void api_platform_waveform_build_response( + GeisaWaveform_Rsp *response, char *stream_id, + GeisaWaveform_RequestType request_type, char *app_id) { GeisaPlatformDiscovery_Waveform waveform_platform_info; - (void)request_type; waveform_platform_info = get_waveform_info(); @@ -29,18 +331,18 @@ api_platform_waveform_build_response(GeisaWaveform_Rsp *response, return; } - for (size_t i = 0; i < waveform_platform_info.streams_count; i++) { + for (int i = 0; i < waveform_platform_info.streams_count; i++) { if (strcmp(waveform_platform_info.streams[i].stream_id, stream_id) == 0) { - response->stream_id = stream_id; - response->waveform_status = - GeisaWaveform_Status_WAVEFORM_SUCCESS; + waveform_handle_stream_request(response, request_type, + i, app_id, + waveform_platform_info); break; } } - if (response->waveform_status != - GeisaWaveform_Status_WAVEFORM_SUCCESS) { + if (response->waveform_status == + GeisaWaveform_Status_WAVEFORM_STATUS_UNSPECIFIED) { response->waveform_status = GeisaWaveform_Status_WAVEFORM_ERR_UNAVAILABLE; } @@ -71,19 +373,24 @@ static void api_waveform_req_handler(struct mosquitto *mosq, const char *topic, if (!status) { fprintf(stderr, "[Waveform] Error decoding waveform app request\n"); - return; + response.status = geisa_waveform_payload_status; + response.has_status = true; + } else { + api_platform_waveform_build_response( + &response, request.stream_id, request.request_type, + app_id); + response.status = geisa_waveform_success_status; + response.has_status = true; } + pb_release(GeisaWaveform_Req_fields, &request); + if (asprintf(&rsp_topic, "geisa/api/waveform/rsp/%s", app_id) == -1) { fprintf(stderr, "[Waveform] Error allocating memory for " "response topic\n"); return; } - api_platform_waveform_build_response(&response, request.stream_id, - request.request_type); - pb_release(GeisaWaveform_Req_fields, &request); - status = pb_get_encoded_size(&encoded_size, GeisaWaveform_Rsp_fields, &response); if (!status) { @@ -117,8 +424,40 @@ static void api_waveform_req_handler(struct mosquitto *mosq, const char *topic, void api_waveform_init(struct mosquitto *mosq) { + int permissions = S_IRUSR | S_IWUSR | S_IRGRP | S_IROTH; + struct stat statbuf = {0}; + api_register_handler("geisa/api/waveform/req", api_waveform_req_handler); api_subscribe(mosq, "geisa/api/waveform/req/#", 1); + + if (stat("/run/geisa/", &statbuf) == -1) { + mkdir("/run/geisa", permissions); + } + if (stat("/run/geisa/waveform/", &statbuf) == -1) { + mkdir("/run/geisa/waveform", permissions); + } +} + +void api_waveform_deinit() +{ + int socket_cleanup_count = w_socket_count; + char *socket_path = NULL; + + fprintf(stdout, "[Waveform] Cleaning up waveform sockets\n"); + + for (int i = 0; i < socket_cleanup_count; i++) { + if (asprintf(&socket_path, "/run/geisa/waveform/%s/%s.sock", + w_sockets[i].app_id, + w_sockets[i].stream_id) == -1) { + fprintf(stderr, + "[Waveform] Error allocating memory for socket path during cleanup\n"); + continue; + } + + remove_socket(w_sockets[i].app_id, w_sockets[i].stream_id); + unlink(socket_path); + free(socket_path); + } } diff --git a/example-implementations/api/api-mockup/test/test_app.c b/example-implementations/api/api-mockup/test/test_app.c index 790a34f..f989574 100644 --- a/example-implementations/api/api-mockup/test/test_app.c +++ b/example-implementations/api/api-mockup/test/test_app.c @@ -138,7 +138,8 @@ void handle_waveform(const void *payload, size_t len) printf("Failed to decode WaveformRsp\n"); return; } - printf("waveform status: %d\n", msg.waveform_status); + printf("waveform status: %d, subscribed:%d, expected_frame_period_ms: %d\n", msg.waveform_status, + msg.subscribed, msg.expected_frame_period_ms); pb_release(GeisaWaveform_Rsp_fields, &msg); } From af92c61cd21d4a5ab75d29f2ae36913f18f722d6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?K=C3=A9vin=20L=27h=C3=B4pital?= Date: Wed, 3 Jun 2026 11:42:16 +0200 Subject: [PATCH 47/64] Makefile: avoid failing if nanopb/*.o doesn't exist MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Clean target should not fail if nanopb/*.o doesn't exist. This can happen if the user has never built the project before. SFL: #22439 Change-Id: Ia91a703638a13e5c620105a0af7a3f2ef333f055 Signed-off-by: Kévin L'hôpital --- example-implementations/api/api-mockup/Makefile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/example-implementations/api/api-mockup/Makefile b/example-implementations/api/api-mockup/Makefile index 64d0ab4..d661f78 100644 --- a/example-implementations/api/api-mockup/Makefile +++ b/example-implementations/api/api-mockup/Makefile @@ -41,7 +41,7 @@ lint: clean: rm -rf $(BUILDIR) - rm nanopb/*.o + rm -f nanopb/*.o $(BUILDIR)/test_app: $(OBJ) $(CC) $(CFLAGS) -o $@ test/test_app.c $(PB_C:.c=.o) $(NANOPB_CORE:.c=.o) $(LDLIBS) From 3e42a053350084d106c16072744cd38c2bec8323 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?K=C3=A9vin=20L=27h=C3=B4pital?= Date: Wed, 3 Jun 2026 11:43:20 +0200 Subject: [PATCH 48/64] gapi_discovery: add required sensor_id field to sensor descriptors MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit SFL: #22439 Change-Id: Ia5079b2809092b55e90e0038ea7a891503359c96 Signed-off-by: Kévin L'hôpital --- example-implementations/api/api-mockup/src/gapi_discovery.c | 2 ++ 1 file changed, 2 insertions(+) diff --git a/example-implementations/api/api-mockup/src/gapi_discovery.c b/example-implementations/api/api-mockup/src/gapi_discovery.c index a63694f..df01654 100644 --- a/example-implementations/api/api-mockup/src/gapi_discovery.c +++ b/example-implementations/api/api-mockup/src/gapi_discovery.c @@ -66,6 +66,7 @@ static GeisaPlatformDiscovery_Metrology metrology_platform_info = { static GeisaSensorDescriptor sensors_platform_info_descriptors[] = { { + .sensor_id = "ambient-temp-1", .sensor_type = GeisaSensorType_GEISA_SENSOR_TYPE_TEMPERATURE, .name = "Ambient Temperature", .has_name = true, @@ -74,6 +75,7 @@ static GeisaSensorDescriptor sensors_platform_info_descriptors[] = { .supports_publish = false, }, { + .sensor_id = "ambient-humidity-1", .sensor_type = GeisaSensorType_GEISA_SENSOR_TYPE_HUMIDITY, .manufacturer = "SCE", .has_manufacturer = true, From 9ae4e7d34339538bc051fef300274e0b15437d76 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?K=C3=A9vin=20L=27h=C3=B4pital?= Date: Wed, 3 Jun 2026 16:46:25 +0200 Subject: [PATCH 49/64] gapi_instantaneous: initialize response with default values MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit To avoid uninitialized values in the response, we initialize it with default values using the `GeisaInstantaneousQuantities_init_default` macro provided by nanopb. This ensures that all fields in the response are set to their default values before being populated with actual data. SFL: #21956 Change-Id: I40cae925a9959d3eb11be6b3d835d7e86253e467 Signed-off-by: Kévin L'hôpital --- .../api/api-mockup/src/gapi_instantaneous.c | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/example-implementations/api/api-mockup/src/gapi_instantaneous.c b/example-implementations/api/api-mockup/src/gapi_instantaneous.c index 04fa0ee..01d2286 100644 --- a/example-implementations/api/api-mockup/src/gapi_instantaneous.c +++ b/example-implementations/api/api-mockup/src/gapi_instantaneous.c @@ -122,7 +122,8 @@ static void geisa_get_instantaneous_data(GeisaInstantaneousQuantities *response) static void *gapi_instantaneous_thread(void *arg) { struct mosquitto *mosq = (struct mosquitto *)arg; - GeisaInstantaneousQuantities response; + GeisaInstantaneousQuantities response = + GeisaInstantaneousQuantities_init_default; size_t encoded_size = 0; uint8_t *message = NULL; pb_ostream_t ostream; From 8dfa12e67a2e6278661f3872e75d241551f13239 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?K=C3=A9vin=20L=27h=C3=B4pital?= Date: Mon, 22 Jun 2026 09:23:44 +0200 Subject: [PATCH 50/64] gapi_waveform: do not free socket_path before message encoding MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit If socket_path is freed before the message is encoded, the response will contain a NULL value for socket_path, which is not the expected behavior. The socket_path should only be freed after the message has been successfully encoded and published. SFL: #21908 Change-Id: Ia0bffe38ba12a4b973ffd7c02d0e02657f4acb2e Signed-off-by: Kévin L'hôpital --- example-implementations/api/api-mockup/src/gapi_waveform.c | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/example-implementations/api/api-mockup/src/gapi_waveform.c b/example-implementations/api/api-mockup/src/gapi_waveform.c index e65414f..43f552a 100644 --- a/example-implementations/api/api-mockup/src/gapi_waveform.c +++ b/example-implementations/api/api-mockup/src/gapi_waveform.c @@ -297,8 +297,6 @@ static void waveform_handle_stream_request( GeisaWaveform_Status_WAVEFORM_ERR_NOT_SUPPORTED; break; } - - free(socket_path); } static GeisaStatus geisa_waveform_success_status = { @@ -388,6 +386,7 @@ static void api_waveform_req_handler(struct mosquitto *mosq, const char *topic, if (asprintf(&rsp_topic, "geisa/api/waveform/rsp/%s", app_id) == -1) { fprintf(stderr, "[Waveform] Error allocating memory for " "response topic\n"); + free(response.socket_path); return; } @@ -396,6 +395,7 @@ static void api_waveform_req_handler(struct mosquitto *mosq, const char *topic, if (!status) { fprintf(stderr, "[Waveform] Error calculating size of response " "message\n"); + free(response.socket_path); free(rsp_topic); return; } @@ -405,6 +405,7 @@ static void api_waveform_req_handler(struct mosquitto *mosq, const char *topic, fprintf(stderr, "[Waveform] Error allocating memory for response " "message\n"); + free(response.socket_path); free(rsp_topic); return; } @@ -413,12 +414,14 @@ static void api_waveform_req_handler(struct mosquitto *mosq, const char *topic, status = pb_encode(&ostream, GeisaWaveform_Rsp_fields, &response); if (!status) { fprintf(stderr, "[Waveform] Error encoding response message\n"); + free(response.socket_path); free(message); free(rsp_topic); return; } api_publish(mosq, rsp_topic, encoded_size, message, 1); + free(response.socket_path); free(message); } From ff33a663166562187c79fcdad813032185956d88 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?K=C3=A9vin=20L=27h=C3=B4pital?= Date: Mon, 22 Jun 2026 14:34:48 +0200 Subject: [PATCH 51/64] gapi_waveform: add waveform thread to handle incoming connections MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit To fully test the waveform feature, we need to implement a dedicated thread that will handle incoming connections on the waveform sockets. This thread will use epoll to efficiently monitor multiple sockets for incoming connection requests and accept them as they arrive. SFL: #21908 Change-Id: I51626e4ca6a03a552bb5ef2c34601fc7efcea4ef Signed-off-by: Kévin L'hôpital --- .../api/api-mockup/include/gapi_waveform.h | 4 + .../api/api-mockup/src/gapi_waveform.c | 160 +++++++++++++++++- 2 files changed, 162 insertions(+), 2 deletions(-) diff --git a/example-implementations/api/api-mockup/include/gapi_waveform.h b/example-implementations/api/api-mockup/include/gapi_waveform.h index c846afe..aeeaeca 100644 --- a/example-implementations/api/api-mockup/include/gapi_waveform.h +++ b/example-implementations/api/api-mockup/include/gapi_waveform.h @@ -14,7 +14,11 @@ #include "pb_encode.h" #include "schemas/waveform.pb.h" #include +#include #include +#include +#include +#include #include #include #include diff --git a/example-implementations/api/api-mockup/src/gapi_waveform.c b/example-implementations/api/api-mockup/src/gapi_waveform.c index 43f552a..58eeb8d 100644 --- a/example-implementations/api/api-mockup/src/gapi_waveform.c +++ b/example-implementations/api/api-mockup/src/gapi_waveform.c @@ -8,6 +8,8 @@ #include "gapi_discovery.h" enum { MAX_WAVEFORM_SOCKETS = 16 }; +enum { MAX_CONNECTION_SOCKFD = 10 }; +enum { EPOLL_TIMEOUT_MS = 1000 }; typedef struct { char *app_id; @@ -17,6 +19,81 @@ typedef struct { static SocketRecord w_sockets[MAX_WAVEFORM_SOCKETS]; static int w_socket_count = 0; +static int waveform_epoll_fd = -1; +static pthread_t waveform_thread; +static bool waveform_thread_started = false; +static volatile bool waveform_running = false; +static pthread_mutex_t waveform_socket_lock = PTHREAD_MUTEX_INITIALIZER; + +static SocketRecord *find_socket_by_fd(int file_descriptor) +{ + for (int i = 0; i < w_socket_count; i++) { + if (w_sockets[i].fd == file_descriptor) { + return &w_sockets[i]; + } + } + return NULL; +} + +static void accept_pending_connections(SocketRecord *record) +{ + for (;;) { + int conn_fd = accept4(record->fd, NULL, NULL, + SOCK_NONBLOCK | SOCK_CLOEXEC); + + if (conn_fd >= 0) { + fprintf(stdout, + "[Waveform] Connection accepted for app %s stream %s\n", + record->app_id, record->stream_id); + close(conn_fd); + continue; + } + + if (errno == EAGAIN || errno == EWOULDBLOCK) { + break; + } + + fprintf(stderr, "[Waveform] Error accepting connection: %s\n", + strerror(errno)); + break; + } +} + +static void *waveform_event_loop(void *arg) +{ + (void)arg; + + while (waveform_running) { + struct epoll_event events[MAX_WAVEFORM_SOCKETS]; + int nfds = epoll_wait(waveform_epoll_fd, events, + MAX_WAVEFORM_SOCKETS, EPOLL_TIMEOUT_MS); + + if (nfds < 0) { + if (errno == EINTR) { + continue; + } + fprintf(stderr, "[Waveform] epoll_wait failed: %s\n", + strerror(errno)); + break; + } + + for (int i = 0; i < nfds; i++) { + if (!(events[i].events & EPOLLIN)) { + continue; + } + + pthread_mutex_lock(&waveform_socket_lock); + SocketRecord *record = + find_socket_by_fd(events[i].data.fd); + if (record != NULL) { + accept_pending_connections(record); + } + pthread_mutex_unlock(&waveform_socket_lock); + } + } + + return NULL; +} static int add_socket(const char *app_id, const char *stream_id, int file_descriptor) @@ -93,7 +170,12 @@ static int handle_socket_creation(char *app_id, char *stream_id, return 1; } - file_descriptor = socket(AF_UNIX, SOCK_SEQPACKET, 0); + if (waveform_epoll_fd == -1) { + fprintf(stderr, "[Waveform] epoll not initialized\n"); + return 2; + } + + file_descriptor = socket(AF_UNIX, SOCK_SEQPACKET | SOCK_NONBLOCK, 0); if (file_descriptor == -1) { fprintf(stderr, "[Waveform] Error creating socket for app %s and stream" @@ -102,6 +184,14 @@ static int handle_socket_creation(char *app_id, char *stream_id, return 2; } + if (fcntl(file_descriptor, F_SETFL, O_NONBLOCK) == -1) { + fprintf(stderr, + "[Waveform] Error configuring socket for app %s and stream %s\n", + app_id, stream_id); + close(file_descriptor); + return 2; + } + dir_path = get_dirname(socket_path); if (stat(dir_path, &statbuf) == -1) { if (mkdir(dir_path, permissions) == -1) { @@ -131,7 +221,18 @@ static int handle_socket_creation(char *app_id, char *stream_id, return 2; } + if (listen(file_descriptor, MAX_CONNECTION_SOCKFD) < 0) { + fprintf(stderr, + "[Waveform] Error listening on socket for app %s and stream " + "%s\n", + app_id, stream_id); + close(file_descriptor); + return 2; + } + + pthread_mutex_lock(&waveform_socket_lock); if (add_socket(app_id, stream_id, file_descriptor) != 0) { + pthread_mutex_unlock(&waveform_socket_lock); fprintf(stderr, "[Waveform] Error tracking socket for app %s stream " "%s\n", @@ -141,6 +242,20 @@ static int handle_socket_creation(char *app_id, char *stream_id, return 2; } + struct epoll_event event = {0}; + event.events = EPOLLIN; + event.data.fd = file_descriptor; + if (epoll_ctl(waveform_epoll_fd, EPOLL_CTL_ADD, file_descriptor, + &event) == -1) { + remove_socket(app_id, stream_id); + pthread_mutex_unlock(&waveform_socket_lock); + fprintf(stderr, + "[Waveform] Error registering waveform socket with epoll\n"); + unlink(socket_path); + return 2; + } + pthread_mutex_unlock(&waveform_socket_lock); + fprintf(stdout, "[Waveform] %s successfully subscribed on stream " "%s\n", @@ -153,8 +268,18 @@ static int handle_socket_removal(char *app_id, char *stream_id, char *socket_path) { int ret = 0; - + int idx = -1; + int file_descriptor = -1; + + pthread_mutex_lock(&waveform_socket_lock); + idx = find_socket(app_id, stream_id); + if (idx != -1) { + file_descriptor = w_sockets[idx].fd; + epoll_ctl(waveform_epoll_fd, EPOLL_CTL_DEL, file_descriptor, + NULL); + } ret = remove_socket(app_id, stream_id); + pthread_mutex_unlock(&waveform_socket_lock); if (ret == -1) { fprintf(stderr, "[Waveform] App %s is not subscribed to stream " @@ -441,6 +566,26 @@ void api_waveform_init(struct mosquitto *mosq) if (stat("/run/geisa/waveform/", &statbuf) == -1) { mkdir("/run/geisa/waveform", permissions); } + + waveform_epoll_fd = epoll_create1(EPOLL_CLOEXEC); + if (waveform_epoll_fd == -1) { + fprintf(stderr, + "[Waveform] Failed to create epoll instance: %s\n", + strerror(errno)); + return; + } + waveform_running = true; + + if (pthread_create(&waveform_thread, NULL, waveform_event_loop, NULL) == + 0) { + waveform_thread_started = true; + } else { + fprintf(stderr, + "[Waveform] Failed to create waveform event thread\n"); + waveform_running = false; + close(waveform_epoll_fd); + waveform_epoll_fd = -1; + } } void api_waveform_deinit() @@ -450,6 +595,12 @@ void api_waveform_deinit() fprintf(stdout, "[Waveform] Cleaning up waveform sockets\n"); + if (waveform_thread_started) { + waveform_running = false; + pthread_join(waveform_thread, NULL); + waveform_thread_started = false; + } + for (int i = 0; i < socket_cleanup_count; i++) { if (asprintf(&socket_path, "/run/geisa/waveform/%s/%s.sock", w_sockets[i].app_id, @@ -463,4 +614,9 @@ void api_waveform_deinit() unlink(socket_path); free(socket_path); } + + if (waveform_epoll_fd != -1) { + close(waveform_epoll_fd); + waveform_epoll_fd = -1; + } } From b374db37cba919741787c96af771d9a734fffac5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?K=C3=A9vin=20L=27h=C3=B4pital?= Date: Mon, 22 Jun 2026 14:36:58 +0200 Subject: [PATCH 52/64] gapi_waveform: fix waveform socket cleanup in api_waveform_deinit() MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit As remove_socket is shifting the array of sockets, we need to iterate backwards to avoid skipping sockets during cleanup. SFL: #21908 Change-Id: Ic29157309722069911a0543c1a5fc0dfc427df62 Signed-off-by: Kévin L'hôpital --- example-implementations/api/api-mockup/src/gapi_waveform.c | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/example-implementations/api/api-mockup/src/gapi_waveform.c b/example-implementations/api/api-mockup/src/gapi_waveform.c index 58eeb8d..e6db60e 100644 --- a/example-implementations/api/api-mockup/src/gapi_waveform.c +++ b/example-implementations/api/api-mockup/src/gapi_waveform.c @@ -590,7 +590,6 @@ void api_waveform_init(struct mosquitto *mosq) void api_waveform_deinit() { - int socket_cleanup_count = w_socket_count; char *socket_path = NULL; fprintf(stdout, "[Waveform] Cleaning up waveform sockets\n"); @@ -601,7 +600,7 @@ void api_waveform_deinit() waveform_thread_started = false; } - for (int i = 0; i < socket_cleanup_count; i++) { + for (int i = w_socket_count - 1; i >= 0; i--) { if (asprintf(&socket_path, "/run/geisa/waveform/%s/%s.sock", w_sockets[i].app_id, w_sockets[i].stream_id) == -1) { From c9c62f593c7a9cfd3b36730f5ef2153dae8d3cba Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?K=C3=A9vin=20L=27h=C3=B4pital?= Date: Mon, 22 Jun 2026 14:38:22 +0200 Subject: [PATCH 53/64] gapi_discovery: add a second waveform for API mockup application MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit To take the tests a step further, add a second waveform instance with reduced sample rate to the API mockup application. This will allow for testing of different waveform configurations and ensure that the system can handle multiple waveform instances simultaneously. SFL: #21908 Change-Id: I9cb5b7c56eeff5fc6d9ee831cc8a831e3951d638 Signed-off-by: Kévin L'hôpital --- .../api/api-mockup/src/gapi_discovery.c | 72 +++++++++++++------ 1 file changed, 49 insertions(+), 23 deletions(-) diff --git a/example-implementations/api/api-mockup/src/gapi_discovery.c b/example-implementations/api/api-mockup/src/gapi_discovery.c index df01654..82cbe42 100644 --- a/example-implementations/api/api-mockup/src/gapi_discovery.c +++ b/example-implementations/api/api-mockup/src/gapi_discovery.c @@ -122,32 +122,58 @@ static GeisaPlatformDiscovery_Network network_platform_info = { static const GeisaWaveform_Datatype waveform_data_type_platform_info = GeisaWaveform_Datatype_DATA_INT32; -static GeisaPlatformDiscovery_Waveform_Instance waveform_platform_instances = { - .stream_id = "api-mockup-waveform", - .name = "API Mockup Waveform", - .description = "Example waveform instance for API mockup application", - .datatype = waveform_data_type_platform_info, - .voltage_multiplier = 0.1, - .current_multiplier = 0.01, - .num_voltage_ch = 1, - .num_current_ch = 1, - .num_other_ch = 0, - .total_channel_count = 2, - .cycle_aligned = true, - .zero_crossing_aligned = true, - .sample_rate = 7680, - .samples_per_cycle = 1280, - .nominal_frequency_hz = 60, - .expected_frame_period_ms = 1000, - .voltage_filter_lowpass = 0, - .voltage_filter_highpass = 0, - .current_filter_lowpass = 0, - .current_filter_highpass = 0, +static GeisaPlatformDiscovery_Waveform_Instance waveform_platform_instances[] = { + { + .stream_id = "api-mockup-waveform", + .name = "API Mockup Waveform", + .description = + "Example waveform instance for API mockup application", + .datatype = waveform_data_type_platform_info, + .voltage_multiplier = 0.1, + .current_multiplier = 0.01, + .num_voltage_ch = 1, + .num_current_ch = 1, + .num_other_ch = 0, + .total_channel_count = 2, + .cycle_aligned = true, + .zero_crossing_aligned = true, + .sample_rate = 7680, + .samples_per_cycle = 1280, + .nominal_frequency_hz = 60, + .expected_frame_period_ms = 1000, + .voltage_filter_lowpass = 0, + .voltage_filter_highpass = 0, + .current_filter_lowpass = 0, + .current_filter_highpass = 0, + }, + { + .stream_id = "api-mockup-waveform-reduced", + .name = "API Mockup Waveform Reduced", + .description = + "Example waveform instance with reduced sample rate", + .datatype = waveform_data_type_platform_info, + .voltage_multiplier = 0.1, + .current_multiplier = 0.01, + .num_voltage_ch = 1, + .num_current_ch = 1, + .num_other_ch = 0, + .total_channel_count = 2, + .cycle_aligned = true, + .zero_crossing_aligned = true, + .sample_rate = 1920, + .samples_per_cycle = 320, + .nominal_frequency_hz = 60, + .expected_frame_period_ms = 1000, + .voltage_filter_lowpass = 0, + .voltage_filter_highpass = 0, + .current_filter_lowpass = 0, + .current_filter_highpass = 0, + }, }; static GeisaPlatformDiscovery_Waveform waveform_platform_info = { - .streams_count = 1, - .streams = &waveform_platform_instances, + .streams_count = 2, + .streams = waveform_platform_instances, }; static GeisaStatus geisa_discovery_success_status = { From 45b9d6f141d575f7f84066bc871f53ce221216d9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?K=C3=A9vin=20L=27h=C3=B4pital?= Date: Wed, 24 Jun 2026 15:31:52 +0200 Subject: [PATCH 54/64] schemas: update schemas to the latest version MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit In order to have the latest changes, update the schemas submodule to the latest commit. SFL: #22704 Change-Id: I7d9c21299aaa61a599c68aae647a7abf0aafd10b Signed-off-by: Kévin L'hôpital --- example-implementations/api/api-mockup/schemas | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/example-implementations/api/api-mockup/schemas b/example-implementations/api/api-mockup/schemas index e67fa99..df75ff5 160000 --- a/example-implementations/api/api-mockup/schemas +++ b/example-implementations/api/api-mockup/schemas @@ -1 +1 @@ -Subproject commit e67fa9904f7c000c5b8727a146c3ce4f50a819d9 +Subproject commit df75ff51332c2aca0541fa60c2e740bcc276c3ce From 0339a9a5c9f117c4d3d8742aceca709c5928256c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?K=C3=A9vin=20L=27h=C3=B4pital?= Date: Wed, 24 Jun 2026 15:33:16 +0200 Subject: [PATCH 55/64] gapi_waveform: add error messages to stderr when waveform request fails MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit To inform the user about the reason of the failure, add error messages to stderr when the waveform request fails. SFL: #21908 Change-Id: I5a18a2bf6c757b62eaeb58b3e62fdb43267342be Signed-off-by: Kévin L'hôpital --- example-implementations/api/api-mockup/src/gapi_waveform.c | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/example-implementations/api/api-mockup/src/gapi_waveform.c b/example-implementations/api/api-mockup/src/gapi_waveform.c index e6db60e..60f239b 100644 --- a/example-implementations/api/api-mockup/src/gapi_waveform.c +++ b/example-implementations/api/api-mockup/src/gapi_waveform.c @@ -445,12 +445,16 @@ static void api_platform_waveform_build_response( if (waveform_platform_info.streams_count == 0) { response->waveform_status = GeisaWaveform_Status_WAVEFORM_ERR_NO_RESOURCES; + fprintf(stderr, + "[Waveform] No waveform streams available in platform waveform info\n"); return; } if (stream_id == NULL) { response->waveform_status = GeisaWaveform_Status_WAVEFORM_ERR_INVALID_STREAM_ID; + fprintf(stderr, + "[Waveform] Stream ID is NULL in waveform request\n"); return; } @@ -468,6 +472,9 @@ static void api_platform_waveform_build_response( GeisaWaveform_Status_WAVEFORM_STATUS_UNSPECIFIED) { response->waveform_status = GeisaWaveform_Status_WAVEFORM_ERR_UNAVAILABLE; + fprintf(stderr, + "[Waveform] Stream ID %s not found in platform waveform info\n", + stream_id); } } From 2cfc28ee74eee6befaaa46f0db8ae7aa0ac94f05 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?K=C3=A9vin=20L=27h=C3=B4pital?= Date: Wed, 24 Jun 2026 16:54:20 +0200 Subject: [PATCH 56/64] src: add gapi_actuator skeleton MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This skeleton code provides the basic structure for handling actuator messages in the GEISA API. It includes the necessary header and source files, as well as the implementation of request handlers for actuator set and get requests. The code also includes test cases to verify the functionality of the actuator message handling. SFL: #22704 Change-Id: I9da946565139f34479591a5ed2286e7dccdf0104 Signed-off-by: Kévin L'hôpital --- example-implementations/api/api-mockup/gapi.c | 2 + .../api/api-mockup/include/gapi_actuator.h | 24 +++ .../api/api-mockup/src/gapi_actuator.c | 178 ++++++++++++++++++ .../api/api-mockup/src/gapi_mosquitto.c | 2 +- .../api/api-mockup/test/test_app.c | 47 +++++ 5 files changed, 252 insertions(+), 1 deletion(-) create mode 100644 example-implementations/api/api-mockup/include/gapi_actuator.h create mode 100644 example-implementations/api/api-mockup/src/gapi_actuator.c diff --git a/example-implementations/api/api-mockup/gapi.c b/example-implementations/api/api-mockup/gapi.c index 513c705..08c652d 100644 --- a/example-implementations/api/api-mockup/gapi.c +++ b/example-implementations/api/api-mockup/gapi.c @@ -3,6 +3,7 @@ * @brief GEISA API mockup main * @copyright Copyright (C) 2025 Southern California Edison */ +#include "gapi_actuator.h" #include "gapi_discovery.h" #include "gapi_instantaneous.h" #include "gapi_manifest.h" @@ -38,6 +39,7 @@ int main() goto deinit; } api_waveform_init(mosq); + api_actuator_init(mosq); while (running) { mosquitto_loop(mosq, -1, 1); diff --git a/example-implementations/api/api-mockup/include/gapi_actuator.h b/example-implementations/api/api-mockup/include/gapi_actuator.h new file mode 100644 index 0000000..8f170c2 --- /dev/null +++ b/example-implementations/api/api-mockup/include/gapi_actuator.h @@ -0,0 +1,24 @@ +/** + * @file gapi_actuator.h + * @brief Header file for API actuator messages + * @copyright Copyright (C) 2026 Southern California Edison + */ + +#ifndef GAPI_ACTUATOR_H +#define GAPI_ACTUATOR_H +#define _GNU_SOURCE + +#include "gapi_mosquitto.h" +#include "pb.h" +#include "pb_decode.h" +#include "pb_encode.h" +#include "schemas/actuator.pb.h" + +/** + * @brief Initialize API actuator messages + * + * @param mosq Pointer to the mosquitto instance + */ +void api_actuator_init(struct mosquitto *mosq); + +#endif // GAPI_ACTUATOR_H diff --git a/example-implementations/api/api-mockup/src/gapi_actuator.c b/example-implementations/api/api-mockup/src/gapi_actuator.c new file mode 100644 index 0000000..3342116 --- /dev/null +++ b/example-implementations/api/api-mockup/src/gapi_actuator.c @@ -0,0 +1,178 @@ +/** + * @file gapi_actuator.c + * @brief Definition file for API actuator messages + * @copyright Copyright (C) 2026 Southern California Edison + */ + +#include "gapi_actuator.h" + +static GeisaStatus geisa_actuator_success_status = { + .code = GeisaStatusCode_GEISA_STATUS_SUCCESS, + .message = "Actuator request successful", +}; + +static GeisaStatus geisa_actuator_payload_status = { + .code = GeisaStatusCode_GEISA_STATUS_CODE_REQUEST_MALFORMED_PAYLOAD, + .message = "Malformed payload in actuator request", +}; + +static void api_actuator_set_req_handler(struct mosquitto *mosq, + const char *topic, + const int payloadlen, + const uint8_t *payload) +{ + GeisaActuatorSet_Req request = GeisaActuatorSet_Req_init_default; + GeisaActuatorSet_Rsp response = GeisaActuatorSet_Rsp_init_default; + uint8_t *message = NULL; + char *rsp_topic = NULL; + char *app_id = NULL; + pb_istream_t istream; + pb_ostream_t ostream; + bool status = false; + size_t encoded_size = 0; + + app_id = basename((char *)topic); + + fprintf(stdout, "[Actuator] Received actuator set request from %s\n", + app_id); + fflush(stdout); + + istream = pb_istream_from_buffer(payload, payloadlen); + status = pb_decode(&istream, GeisaActuatorSet_Req_fields, &request); + if (!status) { + fprintf(stderr, + "[Actuator] Error decoding actuator set request\n"); + response.status = geisa_actuator_payload_status; + response.has_status = true; + } else { + fprintf(stdout, "[Actuator] Received actuator set request\n"); + fflush(stdout); + response.status = geisa_actuator_success_status; + response.has_status = true; + } + pb_release(GeisaActuatorSet_Req_fields, &request); + + status = pb_get_encoded_size(&encoded_size, GeisaActuatorSet_Rsp_fields, + &response); + if (!status) { + fprintf(stderr, + "[Actuator] Error calculating size of actuator set response\n"); + return; + } + + message = malloc(encoded_size); + if (message == NULL) { + fprintf(stderr, + "[Actuator] Error allocating memory for actuator set response\n"); + return; + } + + if (asprintf(&rsp_topic, "geisa/api/actuator/set/rsp/%s", app_id) == + -1) { + fprintf(stderr, + "[Actuator] Error allocating memory for response topic\n"); + free(message); + return; + } + + ostream = pb_ostream_from_buffer(message, encoded_size); + status = pb_encode(&ostream, GeisaActuatorSet_Rsp_fields, &response); + if (!status) { + fprintf(stderr, + "[Actuator] Error encoding platform actuator set response\n"); + free(rsp_topic); + free(message); + return; + } + + api_publish(mosq, rsp_topic, encoded_size, message, 1); + + free(rsp_topic); + free(message); +} + +static void api_actuator_get_req_handler(struct mosquitto *mosq, + const char *topic, + const int payloadlen, + const uint8_t *payload) +{ + GeisaActuatorGet_Req request = GeisaActuatorGet_Req_init_default; + GeisaActuatorGet_Rsp response = GeisaActuatorGet_Rsp_init_default; + uint8_t *message = NULL; + char *rsp_topic = NULL; + char *app_id = NULL; + pb_istream_t istream; + pb_ostream_t ostream; + bool status = false; + size_t encoded_size = 0; + + app_id = basename((char *)topic); + + fprintf(stdout, "[Actuator] Received actuator get request from %s\n", + app_id); + fflush(stdout); + + istream = pb_istream_from_buffer(payload, payloadlen); + status = pb_decode(&istream, GeisaActuatorGet_Req_fields, &request); + if (!status) { + fprintf(stderr, + "[Actuator] Error decoding actuator get request\n"); + response.status = geisa_actuator_payload_status; + response.has_status = true; + } else { + fprintf(stdout, "[Actuator] Received actuator get request\n"); + fflush(stdout); + response.status = geisa_actuator_success_status; + response.has_status = true; + } + pb_release(GeisaActuatorGet_Req_fields, &request); + + status = pb_get_encoded_size(&encoded_size, GeisaActuatorGet_Rsp_fields, + &response); + if (!status) { + fprintf(stderr, + "[Actuator] Error calculating size of actuator get response\n"); + return; + } + + message = malloc(encoded_size); + if (message == NULL) { + fprintf(stderr, + "[Actuator] Error allocating memory for actuator get response\n"); + return; + } + + if (asprintf(&rsp_topic, "geisa/api/actuator/get/rsp/%s", app_id) == + -1) { + fprintf(stderr, + "[Actuator] Error allocating memory for response topic\n"); + free(message); + return; + } + + ostream = pb_ostream_from_buffer(message, encoded_size); + status = pb_encode(&ostream, GeisaActuatorGet_Rsp_fields, &response); + if (!status) { + fprintf(stderr, + "[Actuator] Error encoding platform actuator get response\n"); + free(rsp_topic); + free(message); + return; + } + + api_publish(mosq, rsp_topic, encoded_size, message, 1); + + free(rsp_topic); + free(message); +} + +void api_actuator_init(struct mosquitto *mosq) +{ + api_register_handler("geisa/api/actuator/get/req", + api_actuator_get_req_handler); + api_register_handler("geisa/api/actuator/set/req", + api_actuator_set_req_handler); + + api_subscribe(mosq, "geisa/api/actuator/get/req/#", 1); + api_subscribe(mosq, "geisa/api/actuator/set/req/#", 1); +} diff --git a/example-implementations/api/api-mockup/src/gapi_mosquitto.c b/example-implementations/api/api-mockup/src/gapi_mosquitto.c index d633fd3..7b021da 100644 --- a/example-implementations/api/api-mockup/src/gapi_mosquitto.c +++ b/example-implementations/api/api-mockup/src/gapi_mosquitto.c @@ -8,7 +8,7 @@ #include "gapi_mosquitto.h" const int MOSQUITTO_KEEP_ALIVE = 60; -enum { MAX_TOPIC_HANDLERS = 3 }; +enum { MAX_TOPIC_HANDLERS = 5 }; static int topic_handler_count = 0; diff --git a/example-implementations/api/api-mockup/test/test_app.c b/example-implementations/api/api-mockup/test/test_app.c index f989574..e70714f 100644 --- a/example-implementations/api/api-mockup/test/test_app.c +++ b/example-implementations/api/api-mockup/test/test_app.c @@ -1,6 +1,7 @@ #include "pb.h" #include "pb_decode.h" #include "pb_encode.h" +#include "schemas/actuator.pb.h" #include "schemas/discovery.pb.h" #include "schemas/manifest.pb.h" #include "schemas/metered_quantities.pb.h" @@ -27,6 +28,10 @@ void on_connect(struct mosquitto *mosq, void *obj, int rc) "geisa/api/app/manifest/rsp/testapp", 0); mosquitto_subscribe(mosq, NULL, "geisa/api/waveform/rsp/testapp", 0); + mosquitto_subscribe(mosq, NULL, + "geisa/api/actuator/get/rsp/testapp", 0); + mosquitto_subscribe(mosq, NULL, + "geisa/api/actuator/set/rsp/testapp", 0); } else { printf("Connection failed: %d\n", rc); } @@ -144,6 +149,44 @@ void handle_waveform(const void *payload, size_t len) pb_release(GeisaWaveform_Rsp_fields, &msg); } +void handle_actuator_get_response(const void *payload, size_t len) +{ + bool status; + pb_istream_t istream; + GeisaActuatorGet_Rsp msg = GeisaActuatorGet_Rsp_init_default; + + istream = pb_istream_from_buffer(payload, len); + status = pb_decode(&istream, GeisaActuatorGet_Rsp_fields, &msg); + if (!status) { + printf("Failed to decode ActuatorGetRsp\n"); + return; + } + printf("actuator get response status: %d, message: %s\n", + msg.status.code, + msg.status.message); + + pb_release(GeisaActuatorGet_Rsp_fields, &msg); +} + +void handle_actuator_set_response(const void *payload, size_t len) +{ + bool status; + pb_istream_t istream; + GeisaActuatorSet_Rsp msg = GeisaActuatorSet_Rsp_init_default; + + istream = pb_istream_from_buffer(payload, len); + status = pb_decode(&istream, GeisaActuatorSet_Rsp_fields, &msg); + if (!status) { + printf("Failed to decode ActuatorSetRsp\n"); + return; + } + printf("actuator set response status: %d, message: %s\n", + msg.status.code, + msg.status.message); + + pb_release(GeisaActuatorSet_Rsp_fields, &msg); +} + void on_message(struct mosquitto *mosq, void *obj, const struct mosquitto_message *msg) { @@ -160,6 +203,10 @@ void on_message(struct mosquitto *mosq, void *obj, handle_manifest(msg->payload, msg->payloadlen); } else if (strcmp(msg->topic, "geisa/api/waveform/rsp/testapp") == 0) { handle_waveform(msg->payload, msg->payloadlen); + } else if (strcmp(msg->topic, "geisa/api/actuator/get/rsp/testapp") == 0) { + handle_actuator_get_response(msg->payload, msg->payloadlen); + } else if (strcmp(msg->topic, "geisa/api/actuator/set/rsp/testapp") == 0) { + handle_actuator_set_response(msg->payload, msg->payloadlen); } else { printf("Unknown topic\n"); } From 0be958b97aa19d7db8acfa6505bfb366a9f42258 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?K=C3=A9vin=20L=27h=C3=B4pital?= Date: Wed, 24 Jun 2026 17:21:03 +0200 Subject: [PATCH 57/64] gapi_actuator: implement actuator get/set requests MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Initialize actuator statuses and implement get/set requests to manage them. SFL: #22704 Change-Id: I10f1e60a75a83740b04d2ffe12988df646d36660 Signed-off-by: Kévin L'hôpital --- .../nanopb_options/actuator.options | 1 + .../api/api-mockup/src/gapi_actuator.c | 105 +++++++++++++++++- 2 files changed, 100 insertions(+), 6 deletions(-) create mode 100644 example-implementations/api/api-mockup/nanopb_options/actuator.options diff --git a/example-implementations/api/api-mockup/nanopb_options/actuator.options b/example-implementations/api/api-mockup/nanopb_options/actuator.options new file mode 100644 index 0000000..ebdccea --- /dev/null +++ b/example-implementations/api/api-mockup/nanopb_options/actuator.options @@ -0,0 +1 @@ +GeisaActuatorSet_Req.new_settings type:FT_POINTER diff --git a/example-implementations/api/api-mockup/src/gapi_actuator.c b/example-implementations/api/api-mockup/src/gapi_actuator.c index 3342116..941856b 100644 --- a/example-implementations/api/api-mockup/src/gapi_actuator.c +++ b/example-implementations/api/api-mockup/src/gapi_actuator.c @@ -16,6 +16,90 @@ static GeisaStatus geisa_actuator_payload_status = { .message = "Malformed payload in actuator request", }; +static GeisaStatus geisa_actuator_invalid_status = { + .code = GeisaStatusCode_GEISA_STATUS_CODE_REQUEST_INVALID_ARGUMENT, + .message = "Invalid actuator target", +}; + +static GeisaActuatorStatus actuator_statuses[_GeisaTypeActuator_ARRAYSIZE] = { + [GeisaTypeActuator_GEISA_TYPE_ACTUATOR_UNSPECIFIED] = + { + .actuator = + GeisaTypeActuator_GEISA_TYPE_ACTUATOR_UNSPECIFIED, + .on = _GeisaTypeOnOff_MIN, + }, + [GeisaTypeActuator_GEISA_TYPE_ACTUATOR_SERVICE_SWITCH] = + { + .actuator = + GeisaTypeActuator_GEISA_TYPE_ACTUATOR_SERVICE_SWITCH, + .on = _GeisaTypeOnOff_MIN, + }, + [GeisaTypeActuator_GEISA_TYPE_ACTUATOR_DER_SWITCH] = + { + .actuator = + GeisaTypeActuator_GEISA_TYPE_ACTUATOR_DER_SWITCH, + .on = _GeisaTypeOnOff_MIN, + }, + [GeisaTypeActuator_GEISA_TYPE_ACTUATOR_LC_RELAY_0] = + { + .actuator = + GeisaTypeActuator_GEISA_TYPE_ACTUATOR_LC_RELAY_0, + .on = _GeisaTypeOnOff_MIN, + }, + [GeisaTypeActuator_GEISA_TYPE_ACTUATOR_LC_RELAY_1] = + { + .actuator = + GeisaTypeActuator_GEISA_TYPE_ACTUATOR_LC_RELAY_1, + .on = _GeisaTypeOnOff_MIN, + }, + [GeisaTypeActuator_GEISA_TYPE_ACTUATOR_LC_RELAY_2] = + { + .actuator = + GeisaTypeActuator_GEISA_TYPE_ACTUATOR_LC_RELAY_2, + .on = _GeisaTypeOnOff_MIN, + }, + [GeisaTypeActuator_GEISA_TYPE_ACTUATOR_LC_RELAY_3] = + { + .actuator = + GeisaTypeActuator_GEISA_TYPE_ACTUATOR_LC_RELAY_3, + .on = _GeisaTypeOnOff_MIN, + }, +}; + +static bool actuator_is_valid(GeisaTypeActuator actuator) +{ + switch (actuator) { + case GeisaTypeActuator_GEISA_TYPE_ACTUATOR_UNSPECIFIED: + case GeisaTypeActuator_GEISA_TYPE_ACTUATOR_SERVICE_SWITCH: + case GeisaTypeActuator_GEISA_TYPE_ACTUATOR_DER_SWITCH: + case GeisaTypeActuator_GEISA_TYPE_ACTUATOR_LC_RELAY_0: + case GeisaTypeActuator_GEISA_TYPE_ACTUATOR_LC_RELAY_1: + case GeisaTypeActuator_GEISA_TYPE_ACTUATOR_LC_RELAY_2: + case GeisaTypeActuator_GEISA_TYPE_ACTUATOR_LC_RELAY_3: + return true; + default: + return false; + } +} + +static void actuator_status_apply(const GeisaActuatorStatus *status) +{ + if (status == NULL || !actuator_is_valid(status->actuator)) { + return; + } + + actuator_statuses[status->actuator] = *status; +} + +static int actuator_status_find(GeisaTypeActuator actuator) +{ + if (!actuator_is_valid(actuator)) { + return -1; + } + + return (int)actuator; +} + static void api_actuator_set_req_handler(struct mosquitto *mosq, const char *topic, const int payloadlen, @@ -45,8 +129,9 @@ static void api_actuator_set_req_handler(struct mosquitto *mosq, response.status = geisa_actuator_payload_status; response.has_status = true; } else { - fprintf(stdout, "[Actuator] Received actuator set request\n"); - fflush(stdout); + for (pb_size_t i = 0; i < request.new_settings_count; i++) { + actuator_status_apply(&request.new_settings[i]); + } response.status = geisa_actuator_success_status; response.has_status = true; } @@ -105,6 +190,7 @@ static void api_actuator_get_req_handler(struct mosquitto *mosq, pb_ostream_t ostream; bool status = false; size_t encoded_size = 0; + int actuator_idx = -1; app_id = basename((char *)topic); @@ -120,10 +206,17 @@ static void api_actuator_get_req_handler(struct mosquitto *mosq, response.status = geisa_actuator_payload_status; response.has_status = true; } else { - fprintf(stdout, "[Actuator] Received actuator get request\n"); - fflush(stdout); - response.status = geisa_actuator_success_status; - response.has_status = true; + actuator_idx = actuator_status_find(request.actuator); + if (actuator_idx < 0) { + response.status = geisa_actuator_invalid_status; + response.has_status = true; + } else { + response.status = geisa_actuator_success_status; + response.has_status = true; + response.actuator_status = + actuator_statuses[actuator_idx]; + response.has_actuator_status = true; + } } pb_release(GeisaActuatorGet_Req_fields, &request); From f5807d1560993b4ed3869636d3afebb1110b7816 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?K=C3=A9vin=20L=27h=C3=B4pital?= Date: Thu, 25 Jun 2026 10:52:24 +0200 Subject: [PATCH 58/64] test: add a test application to send actuator get/set requests MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit To ease the testing of the actuator API, this commit adds a simple test application that can send get and set requests to the actuator API. SFL: #22704 Change-Id: I3bcfbd7dcd74e58a33d3d6715bb35e5c33228364 Signed-off-by: Kévin L'hôpital --- .../api/api-mockup/Makefile | 5 +- .../api/api-mockup/README.md | 8 + .../api/api-mockup/test/test_app.c | 10 + .../api/api-mockup/test/test_app_actuator.c | 171 ++++++++++++++++++ 4 files changed, 193 insertions(+), 1 deletion(-) create mode 100644 example-implementations/api/api-mockup/test/test_app_actuator.c diff --git a/example-implementations/api/api-mockup/Makefile b/example-implementations/api/api-mockup/Makefile index d661f78..13744dc 100644 --- a/example-implementations/api/api-mockup/Makefile +++ b/example-implementations/api/api-mockup/Makefile @@ -13,7 +13,7 @@ OBJ := $(patsubst src/%.c,$(BUILDIR)/%.o,$(SRC)) $(PB_C:.c=.o) $(NANOPB_CORE:.c= CFLAGS := -O2 -Wall -std=c11 -Wextra -pedantic -Werror -Iinclude -I$(BUILDIR) -I/usr/include/nanopb -DPB_ENABLE_MALLOC LDLIBS := -lmosquitto -all: $(BUILDIR)/gapi $(BUILDIR)/test_app +all: $(BUILDIR)/gapi $(BUILDIR)/test_app $(BUILDIR)/test_app_actuator .PHONY: schemas build_dir clean build_dir: @@ -45,3 +45,6 @@ clean: $(BUILDIR)/test_app: $(OBJ) $(CC) $(CFLAGS) -o $@ test/test_app.c $(PB_C:.c=.o) $(NANOPB_CORE:.c=.o) $(LDLIBS) + +$(BUILDIR)/test_app_actuator: $(OBJ) + $(CC) $(CFLAGS) -o $@ test/test_app_actuator.c $(PB_C:.c=.o) $(NANOPB_CORE:.c=.o) $(LDLIBS) diff --git a/example-implementations/api/api-mockup/README.md b/example-implementations/api/api-mockup/README.md index 6409e06..74cefd7 100644 --- a/example-implementations/api/api-mockup/README.md +++ b/example-implementations/api/api-mockup/README.md @@ -106,3 +106,11 @@ printf 'stream_id: "api-mockup-waveform"\nrequest_type: 1\n' | protoc --encode ```bash mosquitto_pub -u testapp -P testapp -h localhost -t geisa/api/waveform/req/testapp -f /tmp/waveform.binpb ``` + +* To test the actuator API, there is a dedicated test application that can sends API requests: + +```bash +usage: /tmp/test_app_actuator --get | --set [position] +``` + +The response will be printed by the test_app application diff --git a/example-implementations/api/api-mockup/test/test_app.c b/example-implementations/api/api-mockup/test/test_app.c index e70714f..fe2cca8 100644 --- a/example-implementations/api/api-mockup/test/test_app.c +++ b/example-implementations/api/api-mockup/test/test_app.c @@ -164,6 +164,16 @@ void handle_actuator_get_response(const void *payload, size_t len) printf("actuator get response status: %d, message: %s\n", msg.status.code, msg.status.message); + if (msg.has_actuator_status) { + printf("actuator get response actuator: %d, on: %d, position_present: %d", + msg.actuator_status.actuator, + msg.actuator_status.on, + msg.actuator_status.position_present); + if (msg.actuator_status.position_present) { + printf(", position: %d", msg.actuator_status.position); + } + printf("\n"); + } pb_release(GeisaActuatorGet_Rsp_fields, &msg); } diff --git a/example-implementations/api/api-mockup/test/test_app_actuator.c b/example-implementations/api/api-mockup/test/test_app_actuator.c new file mode 100644 index 0000000..b82f017 --- /dev/null +++ b/example-implementations/api/api-mockup/test/test_app_actuator.c @@ -0,0 +1,171 @@ +#include "pb.h" +#include "pb_encode.h" +#include "schemas/actuator.pb.h" +#include +#include +#include +#include + +#define BROKER "localhost" +#define PORT 1883 + +static bool parse_actuator(const char *value, GeisaTypeActuator *actuator) +{ + if (strcmp(value, "service-switch") == 0) { + *actuator = GeisaTypeActuator_GEISA_TYPE_ACTUATOR_SERVICE_SWITCH; + return true; + } + if (strcmp(value, "der-switch") == 0) { + *actuator = GeisaTypeActuator_GEISA_TYPE_ACTUATOR_DER_SWITCH; + return true; + } + if (strcmp(value, "lc-relay-0") == 0) { + *actuator = GeisaTypeActuator_GEISA_TYPE_ACTUATOR_LC_RELAY_0; + return true; + } + if (strcmp(value, "lc-relay-1") == 0) { + *actuator = GeisaTypeActuator_GEISA_TYPE_ACTUATOR_LC_RELAY_1; + return true; + } + if (strcmp(value, "lc-relay-2") == 0) { + *actuator = GeisaTypeActuator_GEISA_TYPE_ACTUATOR_LC_RELAY_2; + return true; + } + if (strcmp(value, "lc-relay-3") == 0) { + *actuator = GeisaTypeActuator_GEISA_TYPE_ACTUATOR_LC_RELAY_3; + return true; + } + return false; +} + +static bool parse_on_off(const char *value, GeisaTypeOnOff *state) +{ + if (strcmp(value, "on") == 0) { + *state = GeisaTypeOnOff_GEISA_TYPE_ON_OFF_ON; + return true; + } + if (strcmp(value, "off") == 0) { + *state = GeisaTypeOnOff_GEISA_TYPE_ON_OFF_OFF; + return true; + } + return false; +} + +static int publish_encoded(struct mosquitto *mosq, const char *topic, + const void *msg, const pb_msgdesc_t *fields) +{ + uint8_t buffer[128]; + pb_ostream_t ostream = pb_ostream_from_buffer(buffer, sizeof(buffer)); + if (!pb_encode(&ostream, fields, msg)) { + fprintf(stderr, "Encode failed for %s: %s\n", topic, + PB_GET_ERROR(&ostream)); + return 1; + } + if (mosquitto_publish(mosq, NULL, topic, (int)ostream.bytes_written, + buffer, 1, false) != MOSQ_ERR_SUCCESS) { + fprintf(stderr, "Publish failed for %s\n", topic); + return 1; + } + return 0; +} + +int main(int argc, char **argv) +{ + struct mosquitto *mosq; + GeisaActuatorSet_Req set_req = GeisaActuatorSet_Req_init_default; + GeisaActuatorGet_Req get_req = GeisaActuatorGet_Req_init_default; + GeisaActuatorStatus status = GeisaActuatorStatus_init_default; + GeisaTypeActuator actuator; + GeisaTypeOnOff on_off; + int rc = 0; + + mosquitto_lib_init(); + + if (argc != 3 && argc != 4 && argc != 5) { + fprintf(stderr, + "usage: %s --get | --set [position]\n", + argv[0]); + mosquitto_lib_cleanup(); + return 1; + } + + if (strcmp(argv[1], "--get") == 0) { + if (argc != 3) { + fprintf(stderr, + "usage: %s --get | --set [position]\n", + argv[0]); + mosquitto_lib_cleanup(); + return 1; + } + if (!parse_actuator(argv[2], &actuator)) { + fprintf(stderr, "invalid actuator: %s\n", argv[2]); + mosquitto_lib_cleanup(); + return 1; + } + get_req.actuator = actuator; + } else if (strcmp(argv[1], "--set") == 0) { + if (argc != 4 && argc != 5) { + fprintf(stderr, + "usage: %s --set [position]\n", + argv[0]); + mosquitto_lib_cleanup(); + return 1; + } + if (!parse_actuator(argv[2], &actuator)) { + fprintf(stderr, "invalid actuator: %s\n", argv[2]); + mosquitto_lib_cleanup(); + return 1; + } + if (!parse_on_off(argv[3], &on_off)) { + fprintf(stderr, "invalid state: %s\n", argv[3]); + mosquitto_lib_cleanup(); + return 1; + } + status.actuator = actuator; + status.on = on_off; + status.position_present = (argc == 5); + if (status.position_present) { + status.position = (int32_t)strtol(argv[4], NULL, 10); + } + set_req.new_settings_count = 1; + set_req.new_settings = &status; + } else { + fprintf(stderr, + "usage: %s --get | --set [position]\n", + argv[0]); + mosquitto_lib_cleanup(); + return 1; + } + + mosq = mosquitto_new(NULL, true, NULL); + if (!mosq) { + perror("mosquitto_new"); + mosquitto_lib_cleanup(); + return 1; + } + + mosquitto_username_pw_set(mosq, "testapp", "testapp"); + if (mosquitto_connect(mosq, BROKER, PORT, 60) != MOSQ_ERR_SUCCESS) { + fprintf(stderr, "Unable to connect\n"); + mosquitto_destroy(mosq); + mosquitto_lib_cleanup(); + return 1; + } + + if (strcmp(argv[1], "--get") == 0) { + rc = publish_encoded(mosq, "geisa/api/actuator/get/req/testapp", + &get_req, GeisaActuatorGet_Req_fields); + } else { + rc = publish_encoded(mosq, "geisa/api/actuator/set/req/testapp", + &set_req, GeisaActuatorSet_Req_fields); + } + + if (rc == 0) { + mosquitto_loop(mosq, 100, 1); + } + + mosquitto_disconnect(mosq); + mosquitto_destroy(mosq); + mosquitto_lib_cleanup(); + return rc; +} From 7b2153b86f903fecbbb0047e366a3d09828c6cb9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?K=C3=A9vin=20L=27h=C3=B4pital?= Date: Fri, 24 Jul 2026 14:42:27 +0200 Subject: [PATCH 59/64] src: add sensor API support MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This change adds support for the sensor API, allowing clients to request sensor readings from the platform. It will handle requests for specific sensors or all available sensors, and respond with the corresponding readings or error messages if the request is malformed or the requested sensor is not found or not supported. SFL: #21958 Change-Id: I0b07f7f616307dced9fc9d1a1abd909286efe8b8 Signed-off-by: Kévin L'hôpital --- example-implementations/api/api-mockup/gapi.c | 2 + .../api/api-mockup/include/gapi_discovery.h | 7 + .../api/api-mockup/include/gapi_sensor.h | 18 ++ .../api-mockup/nanopb_options/sensor.options | 3 + .../api/api-mockup/src/gapi_discovery.c | 5 + .../api/api-mockup/src/gapi_mosquitto.c | 2 +- .../api/api-mockup/src/gapi_sensor.c | 266 ++++++++++++++++++ 7 files changed, 302 insertions(+), 1 deletion(-) create mode 100644 example-implementations/api/api-mockup/include/gapi_sensor.h create mode 100644 example-implementations/api/api-mockup/src/gapi_sensor.c diff --git a/example-implementations/api/api-mockup/gapi.c b/example-implementations/api/api-mockup/gapi.c index 08c652d..0abf2d0 100644 --- a/example-implementations/api/api-mockup/gapi.c +++ b/example-implementations/api/api-mockup/gapi.c @@ -8,6 +8,7 @@ #include "gapi_instantaneous.h" #include "gapi_manifest.h" #include "gapi_mosquitto.h" +#include "gapi_sensor.h" #include "gapi_waveform.h" volatile bool running = true; @@ -40,6 +41,7 @@ int main() } api_waveform_init(mosq); api_actuator_init(mosq); + api_sensor_init(mosq); while (running) { mosquitto_loop(mosq, -1, 1); diff --git a/example-implementations/api/api-mockup/include/gapi_discovery.h b/example-implementations/api/api-mockup/include/gapi_discovery.h index 890a1c8..42cbe42 100644 --- a/example-implementations/api/api-mockup/include/gapi_discovery.h +++ b/example-implementations/api/api-mockup/include/gapi_discovery.h @@ -29,4 +29,11 @@ void api_discovery_init(struct mosquitto *mosq); */ GeisaPlatformDiscovery_Waveform get_waveform_info(); +/** + * @brief Get sensors information for platform discovery + * + * @return Pointer to PlatformDiscoverySensor structure + */ +GeisaPlatformDiscovery_Sensor get_sensors_info(); + #endif // GAPI_DISCOVERY_H diff --git a/example-implementations/api/api-mockup/include/gapi_sensor.h b/example-implementations/api/api-mockup/include/gapi_sensor.h new file mode 100644 index 0000000..04434f4 --- /dev/null +++ b/example-implementations/api/api-mockup/include/gapi_sensor.h @@ -0,0 +1,18 @@ +/** + * @file gapi_sensor.h + * @brief Header file for API sensors data messages + * @copyright Copyright (C) 2026 Southern California Edison + */ +#ifndef GAPI_SENSOR_H +#define GAPI_SENSOR_H + +#include "gapi_mosquitto.h" + +/** + * @brief Initialize API sensor handlers + * + * @param mosq Pointer to mosquitto instance + */ +void api_sensor_init(struct mosquitto *mosq); + +#endif // GAPI_SENSOR_H diff --git a/example-implementations/api/api-mockup/nanopb_options/sensor.options b/example-implementations/api/api-mockup/nanopb_options/sensor.options index 66b662d..b69619a 100644 --- a/example-implementations/api/api-mockup/nanopb_options/sensor.options +++ b/example-implementations/api/api-mockup/nanopb_options/sensor.options @@ -1 +1,4 @@ *.* max_size:32 +GeisaSensorReadings_Req.sensor_id type:FT_POINTER +GeisaSensorReadings_Rsp.readings type:FT_POINTER +GeisaSensorReading.values type:FT_POINTER diff --git a/example-implementations/api/api-mockup/src/gapi_discovery.c b/example-implementations/api/api-mockup/src/gapi_discovery.c index 82cbe42..2ae823b 100644 --- a/example-implementations/api/api-mockup/src/gapi_discovery.c +++ b/example-implementations/api/api-mockup/src/gapi_discovery.c @@ -214,6 +214,11 @@ GeisaPlatformDiscovery_Waveform get_waveform_info() return waveform_platform_info; } +GeisaPlatformDiscovery_Sensor get_sensors_info() +{ + return sensor_platform_info; +} + static void api_platform_discovery_req_handler(struct mosquitto *mosq, const char *topic, const int payloadlen, diff --git a/example-implementations/api/api-mockup/src/gapi_mosquitto.c b/example-implementations/api/api-mockup/src/gapi_mosquitto.c index 7b021da..8cb3be7 100644 --- a/example-implementations/api/api-mockup/src/gapi_mosquitto.c +++ b/example-implementations/api/api-mockup/src/gapi_mosquitto.c @@ -8,7 +8,7 @@ #include "gapi_mosquitto.h" const int MOSQUITTO_KEEP_ALIVE = 60; -enum { MAX_TOPIC_HANDLERS = 5 }; +enum { MAX_TOPIC_HANDLERS = 6 }; static int topic_handler_count = 0; diff --git a/example-implementations/api/api-mockup/src/gapi_sensor.c b/example-implementations/api/api-mockup/src/gapi_sensor.c new file mode 100644 index 0000000..e147b81 --- /dev/null +++ b/example-implementations/api/api-mockup/src/gapi_sensor.c @@ -0,0 +1,266 @@ +/** + * @file gapi_sensor.c + * @brief Definition file for API sensor messages + * @copyright Copyright (C) 2026 Southern California Edison + */ + +#define _GNU_SOURCE +#include "gapi_sensor.h" +#include "gapi_discovery.h" +#include "pb.h" +#include "pb_decode.h" +#include "pb_encode.h" +#include "schemas/sensor.pb.h" +#include +#include +#include +#include +#include + +enum { SEC_IN_MS = 1000 }; +enum { MAX_SENSORS_READINGS = 16 }; + +typedef struct { + const char *sensor_id; + double value; +} sensor_t; + +// NOLINTBEGIN(cppcoreguidelines-avoid-magic-numbers,readability-magic-numbers) +static sensor_t sensors[] = { + {"ambient-temp-1", 25.0}, + {"ambient-humidity-1", 45.0}, +}; +// NOLINTEND(cppcoreguidelines-avoid-magic-numbers,readability-magic-numbers) + +static const size_t sensor_count = sizeof(sensors) / sizeof(sensors[0]); + +static GeisaStatus geisa_sensor_success_status = { + .code = GeisaStatusCode_GEISA_STATUS_SUCCESS, + .message = "Sensor request successful", +}; + +static GeisaStatus geisa_sensor_malformed_payload_status = { + .code = GeisaStatusCode_GEISA_STATUS_CODE_REQUEST_MALFORMED_PAYLOAD, + .message = "Malformed payload in sensor request", +}; + +static GeisaStatus geisa_sensor_not_found_status = { + .code = GeisaStatusCode_GEISA_STATUS_CODE_RESOURCE_NOT_FOUND, + .message = "Requested sensor not found", +}; + +static GeisaStatus geisa_sensor_not_supported_status = { + .code = GeisaStatusCode_GEISA_STATUS_CODE_EXEC_NOT_SUPPORTED, + .message = "Requested read is not supported by the sensor", +}; + +static int sensor_find(const char *sensor_id) +{ + if (sensor_id == NULL) { + return -1; + } + + for (size_t i = 0; i < sensor_count; i++) { + if (strcmp(sensor_id, sensors[i].sensor_id) == 0) { + return (int)i; + } + } + return -1; +} + +static void get_all_sensors_info(GeisaSensorReadings_Rsp *response, + GeisaPlatformDiscovery_Sensor sensors_desc, + GeisaSensorDescriptor sensor_desc, + size_t readings_idx, uint64_t now_ms, + GeisaSensorReading *readings, + GeisaSensorValue *values) +{ + for (size_t i = 0; + i < sensor_count && readings_idx < MAX_SENSORS_READINGS; i++) { + sensor_desc = sensors_desc.sensors[i]; + if (sensor_desc.supports_read) { + const sensor_t *sensor = &sensors[i]; + GeisaSensorReading *read = &readings[readings_idx]; + GeisaSensorValue *value = &values[readings_idx]; + + memset(read, 0, sizeof(*read)); + strncpy(read->sensor_id, sensor->sensor_id, + sizeof(read->sensor_id) - 1); + read->timestamp_ms = now_ms; + + memset(value, 0, sizeof(*value)); + value->which_value = GeisaSensorValue_double_value_tag; + value->value.double_value = sensor->value; + read->values_count = 1; + read->values = value; + read->has_unit = true; + strncpy(read->unit, sensor_desc.unit, + sizeof(read->unit)); + + readings_idx++; + } + } + response->status = geisa_sensor_success_status; + response->has_status = true; + response->readings_count = (pb_size_t)readings_idx; + response->readings = readings; +} + +static void get_requested_sensors_info( + GeisaSensorReadings_Rsp *response, + GeisaPlatformDiscovery_Sensor sensors_desc, + GeisaSensorDescriptor sensor_desc, size_t readings_idx, uint64_t now_ms, + GeisaSensorReading *readings, GeisaSensorValue *values, + const GeisaSensorReadings_Req *request) +{ + for (pb_size_t i = 0; i < request->sensor_id_count && + readings_idx < MAX_SENSORS_READINGS; + i++) { + + const char *req_id = request->sensor_id[i]; + int idx = sensor_find(req_id); + // NOLINTNEXTLINE: Out of bound check is done above + const sensor_t *sensor = &sensors[idx]; + sensor_desc = sensors_desc.sensors[idx]; + + GeisaSensorReading *read = &readings[readings_idx]; + GeisaSensorValue *value = &values[readings_idx]; + + memset(read, 0, sizeof(*read)); + strncpy(read->sensor_id, sensor->sensor_id, + sizeof(read->sensor_id) - 1); + read->timestamp_ms = now_ms; + + memset(value, 0, sizeof(*value)); + value->which_value = GeisaSensorValue_double_value_tag; + value->value.double_value = sensor->value; + read->values_count = 1; + read->values = value; + read->has_unit = true; + strncpy(read->unit, sensor_desc.unit, sizeof(read->unit)); + + readings_idx++; + } + + response->status = geisa_sensor_success_status; + response->has_status = true; + response->readings_count = (pb_size_t)readings_idx; + response->readings = readings; +} + +static bool geisa_sensor_build_response(const GeisaSensorReadings_Req *request, + GeisaSensorReadings_Rsp *response) +{ + static GeisaSensorReading readings[MAX_SENSORS_READINGS]; + static GeisaSensorValue values[MAX_SENSORS_READINGS]; + GeisaPlatformDiscovery_Sensor sensors_desc = + GeisaPlatformDiscovery_Sensor_init_default; + GeisaSensorDescriptor sensor_desc = GeisaSensorDescriptor_init_default; + + size_t readings_idx = 0; + uint64_t now_ms = (uint64_t)time(NULL) * SEC_IN_MS; + sensors_desc = get_sensors_info(); + + if (request->sensor_id_count == 0) { + get_all_sensors_info(response, sensors_desc, sensor_desc, + readings_idx, now_ms, readings, values); + return true; + } + + for (pb_size_t i = 0; i < request->sensor_id_count; i++) { + const char *req_id = request->sensor_id[i]; + int idx = sensor_find(req_id); + if (idx < 0) { + response->status = geisa_sensor_not_found_status; + response->has_status = true; + response->readings_count = 0; + response->readings = NULL; + return false; + } + if (!sensors_desc.sensors[idx].supports_read) { + response->status = geisa_sensor_not_supported_status; + response->has_status = true; + response->readings_count = 0; + response->readings = NULL; + return false; + } + } + + get_requested_sensors_info(response, sensors_desc, sensor_desc, + readings_idx, now_ms, readings, values, + request); + return true; +} + +static void api_sensor_req_handler(struct mosquitto *mosq, const char *topic, + const int payloadlen, const uint8_t *payload) +{ + GeisaSensorReadings_Req request = GeisaSensorReadings_Req_init_default; + GeisaSensorReadings_Rsp response = GeisaSensorReadings_Rsp_init_default; + size_t encoded_size = 0; + uint8_t *message = NULL; + char *rsp_topic = NULL; + char *app_id = NULL; + pb_istream_t istream; + pb_ostream_t ostream; + bool status = false; + + app_id = basename((char *)topic); + + fprintf(stdout, "[Sensor] Received sensor read request from %s\n", + app_id); + fflush(stdout); + + istream = pb_istream_from_buffer(payload, payloadlen); + status = pb_decode(&istream, GeisaSensorReadings_Req_fields, &request); + if (!status) { + fprintf(stderr, "[Sensor] Error decoding sensor request\n"); + response.status = geisa_sensor_malformed_payload_status; + response.has_status = true; + } else { + geisa_sensor_build_response(&request, &response); + } + pb_release(GeisaSensorReadings_Req_fields, &request); + + if (asprintf(&rsp_topic, "geisa/api/sensor-rsp/%s", app_id) == -1) { + fprintf(stderr, + "[Sensor] Error allocating memory for response topic\n"); + return; + } + + status = pb_get_encoded_size(&encoded_size, + GeisaSensorReadings_Rsp_fields, &response); + if (!status) { + fprintf(stderr, + "[Sensor] Error calculating size of response\n"); + goto rsp_topic_cleanup; + } + + message = malloc(encoded_size); + if (message == NULL) { + fprintf(stderr, + "[Sensor] Error allocating memory for response message\n"); + goto rsp_topic_cleanup; + } + + ostream = pb_ostream_from_buffer(message, encoded_size); + status = pb_encode(&ostream, GeisaSensorReadings_Rsp_fields, &response); + if (!status) { + fprintf(stderr, "[Sensor] Error encoding response message\n"); + goto message_cleanup; + } + + api_publish(mosq, rsp_topic, encoded_size, message, 1); + +message_cleanup: + free(message); +rsp_topic_cleanup: + free(rsp_topic); +} + +void api_sensor_init(struct mosquitto *mosq) +{ + api_register_handler("geisa/api/sensor-req", api_sensor_req_handler); + + api_subscribe(mosq, "geisa/api/sensor-req/#", 0); +} From fc5ca3eb89cc7fd2c0d1b0186f5754f51e849d6f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?K=C3=A9vin=20L=27h=C3=B4pital?= Date: Fri, 24 Jul 2026 15:46:32 +0200 Subject: [PATCH 60/64] test: add test_app_sensor to test the sensor API MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit To ease sending requests to the sensor API, a dedicated test application has been added. This application can send requests to get all sensors or specific sensors by their IDs. The test_app application has also been updated to handle the sensor response messages. SFL: #21958 Change-Id: I49be6768e5f062b279d8049ac8cf222b32ff5988 Signed-off-by: Kévin L'hôpital --- .../api/api-mockup/Makefile | 5 +- .../api/api-mockup/README.md | 7 + .../api/api-mockup/test/test_app.c | 48 +++++++ .../api/api-mockup/test/test_app_sensor.c | 122 ++++++++++++++++++ 4 files changed, 181 insertions(+), 1 deletion(-) create mode 100644 example-implementations/api/api-mockup/test/test_app_sensor.c diff --git a/example-implementations/api/api-mockup/Makefile b/example-implementations/api/api-mockup/Makefile index 13744dc..b16841e 100644 --- a/example-implementations/api/api-mockup/Makefile +++ b/example-implementations/api/api-mockup/Makefile @@ -13,7 +13,7 @@ OBJ := $(patsubst src/%.c,$(BUILDIR)/%.o,$(SRC)) $(PB_C:.c=.o) $(NANOPB_CORE:.c= CFLAGS := -O2 -Wall -std=c11 -Wextra -pedantic -Werror -Iinclude -I$(BUILDIR) -I/usr/include/nanopb -DPB_ENABLE_MALLOC LDLIBS := -lmosquitto -all: $(BUILDIR)/gapi $(BUILDIR)/test_app $(BUILDIR)/test_app_actuator +all: $(BUILDIR)/gapi $(BUILDIR)/test_app $(BUILDIR)/test_app_actuator $(BUILDIR)/test_app_sensor .PHONY: schemas build_dir clean build_dir: @@ -48,3 +48,6 @@ $(BUILDIR)/test_app: $(OBJ) $(BUILDIR)/test_app_actuator: $(OBJ) $(CC) $(CFLAGS) -o $@ test/test_app_actuator.c $(PB_C:.c=.o) $(NANOPB_CORE:.c=.o) $(LDLIBS) + +$(BUILDIR)/test_app_sensor: $(OBJ) + $(CC) $(CFLAGS) -o $@ test/test_app_sensor.c $(PB_C:.c=.o) $(NANOPB_CORE:.c=.o) $(LDLIBS) diff --git a/example-implementations/api/api-mockup/README.md b/example-implementations/api/api-mockup/README.md index 74cefd7..10e33c5 100644 --- a/example-implementations/api/api-mockup/README.md +++ b/example-implementations/api/api-mockup/README.md @@ -113,4 +113,11 @@ mosquitto_pub -u testapp -P testapp -h localhost -t geisa/api/waveform/req/testa usage: /tmp/test_app_actuator --get | --set [position] ``` +* To test the sensor API, there is a dedicated test application that can sends API requests: + +```bash +usage: /tmp/test_app_sensor [sensor_id ...] +``` +When not providing any sensor_id, the application will send a request to get all the sensors available. Otherwise, it will send a request to get the specified sensors. + The response will be printed by the test_app application diff --git a/example-implementations/api/api-mockup/test/test_app.c b/example-implementations/api/api-mockup/test/test_app.c index fe2cca8..dc7fc2d 100644 --- a/example-implementations/api/api-mockup/test/test_app.c +++ b/example-implementations/api/api-mockup/test/test_app.c @@ -6,6 +6,7 @@ #include "schemas/manifest.pb.h" #include "schemas/metered_quantities.pb.h" #include "schemas/waveform.pb.h" +#include "schemas/sensor.pb.h" #include #include #include @@ -32,6 +33,8 @@ void on_connect(struct mosquitto *mosq, void *obj, int rc) "geisa/api/actuator/get/rsp/testapp", 0); mosquitto_subscribe(mosq, NULL, "geisa/api/actuator/set/rsp/testapp", 0); + mosquitto_subscribe(mosq, NULL, + "geisa/api/sensor-rsp/testapp", 0); } else { printf("Connection failed: %d\n", rc); } @@ -149,6 +152,49 @@ void handle_waveform(const void *payload, size_t len) pb_release(GeisaWaveform_Rsp_fields, &msg); } +void handle_sensor_response(const void *payload, size_t len) +{ + bool status; + pb_istream_t istream; + GeisaSensorReadings_Rsp msg = GeisaSensorReadings_Rsp_init_default; + + istream = pb_istream_from_buffer(payload, len); + status = pb_decode(&istream, GeisaSensorReadings_Rsp_fields, &msg); + if (!status) { + printf("Failed to decode SensorReadings_Rsp: %s\n", PB_GET_ERROR(&istream)); + return; + } + + printf("Sensor response status: %d, message: %s\n", msg.status.code, msg.status.message); + for (pb_size_t i = 0; i < msg.readings_count; i++) { + GeisaSensorReading *r = &msg.readings[i]; + printf("Reading[%d]: id=%s timestamp_ms=%llu unit=%s\n", + (int)i, r->sensor_id, (unsigned long long)r->timestamp_ms, r->unit); + for (pb_size_t j = 0; j < r->values_count; j++) { + GeisaSensorValue *v = &r->values[j]; + switch (v->which_value) { + case GeisaSensorValue_double_value_tag: + printf(" value: double=%f\n", v->value.double_value); + break; + case GeisaSensorValue_int64_value_tag: + printf(" value: int64=%lld\n", (long long)v->value.int64_value); + break; + case GeisaSensorValue_bool_value_tag: + printf(" value: bool=%d\n", v->value.bool_value); + break; + case GeisaSensorValue_string_value_tag: + printf(" value: string=%s\n", v->value.string_value); + break; + default: + printf(" value: unknown\n"); + break; + } + } + } + + pb_release(GeisaSensorReadings_Rsp_fields, &msg); +} + void handle_actuator_get_response(const void *payload, size_t len) { bool status; @@ -217,6 +263,8 @@ void on_message(struct mosquitto *mosq, void *obj, handle_actuator_get_response(msg->payload, msg->payloadlen); } else if (strcmp(msg->topic, "geisa/api/actuator/set/rsp/testapp") == 0) { handle_actuator_set_response(msg->payload, msg->payloadlen); + } else if (strcmp(msg->topic, "geisa/api/sensor-rsp/testapp") == 0) { + handle_sensor_response(msg->payload, msg->payloadlen); } else { printf("Unknown topic\n"); } diff --git a/example-implementations/api/api-mockup/test/test_app_sensor.c b/example-implementations/api/api-mockup/test/test_app_sensor.c new file mode 100644 index 0000000..a4a08c2 --- /dev/null +++ b/example-implementations/api/api-mockup/test/test_app_sensor.c @@ -0,0 +1,122 @@ +#include "pb.h" +#include "pb_encode.h" +#include "pb_decode.h" +#include "schemas/sensor.pb.h" +#include +#include +#include +#include + +#define BROKER "localhost" +#define PORT 1883 + +static int publish_encoded(struct mosquitto *mosq, const char *topic, + const void *msg, const pb_msgdesc_t *fields) +{ + uint8_t *buffer = NULL; + size_t encoded_size = 0; + pb_ostream_t ostream; + bool status; + int rc = 0; + + status = pb_get_encoded_size(&encoded_size, fields, msg); + if (!status) { + fprintf(stderr, "Encode size calc failed for %s\n", topic); + return 1; + } + + buffer = malloc(encoded_size); + if (!buffer) { + fprintf(stderr, "Allocation failed for encode buffer\n"); + return 1; + } + + ostream = pb_ostream_from_buffer(buffer, encoded_size); + if (!pb_encode(&ostream, fields, msg)) { + fprintf(stderr, "Encode failed for %s: %s\n", topic, + PB_GET_ERROR(&ostream)); + rc = 1; + goto cleanup; + } + + if (mosquitto_publish(mosq, NULL, topic, (int)ostream.bytes_written, + buffer, 1, false) != MOSQ_ERR_SUCCESS) { + fprintf(stderr, "Publish failed for %s\n", topic); + rc = 1; + goto cleanup; + } + +cleanup: + free(buffer); + return rc; +} + +int main(int argc, char **argv) +{ + struct mosquitto *mosq; + GeisaSensorReadings_Req req = GeisaSensorReadings_Req_init_default; + int rc = 0; + + if (argc > 1) { + if (strcmp(argv[1], "--help") == 0 || strcmp(argv[1], "-h") == 0) { + fprintf(stderr, + "usage: %s [sensor_id ...]\n", + argv[0]); + return 0; + } + + int count = argc - 1; + req.sensor_id_count = (pb_size_t)count; + req.sensor_id = malloc((size_t)count * sizeof(char *)); + if (!req.sensor_id) { + fprintf(stderr, + "Allocation failed for sensor_id pointers\n"); + return 1; + } + for (int i = 0; i < count; i++) { + req.sensor_id[i] = malloc(strlen(argv[i + 1]) + 1); + if (!req.sensor_id[i]) { + fprintf(stderr, + "Allocation failed for sensor_id string\n"); + rc = 1; + goto cleanup_req; + } + strcpy(req.sensor_id[i], argv[i + 1]); + } + } else { + req.sensor_id_count = 0; + req.sensor_id = NULL; + } + + mosquitto_lib_init(); + + mosq = mosquitto_new(NULL, true, NULL); + if (!mosq) { + perror("mosquitto_new"); + rc = 1; + goto lib_cleanup_mosq; + } + + mosquitto_username_pw_set(mosq, "testapp", "testapp"); + if (mosquitto_connect(mosq, BROKER, PORT, 60) != MOSQ_ERR_SUCCESS) { + fprintf(stderr, "Unable to connect\n"); + rc = 1; + goto destroy_mosq; + } + + rc = publish_encoded(mosq, "geisa/api/sensor-req/testapp", &req, + GeisaSensorReadings_Req_fields); + + if (rc == 0) { + mosquitto_loop(mosq, 100, 1); + } + + mosquitto_disconnect(mosq); +destroy_mosq: + mosquitto_destroy(mosq); +lib_cleanup_mosq: + mosquitto_lib_cleanup(); +cleanup_req: + pb_release(GeisaSensorReadings_Req_fields, &req); + return rc; +} From 2f2d8c85d9f173d9edbe4aa56896add37313dd60 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?K=C3=A9vin=20L=27h=C3=B4pital?= Date: Fri, 24 Jul 2026 16:55:48 +0200 Subject: [PATCH 61/64] sensor: add a not readable sensor MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit For test purpose, add a sensor that is not readable, to test the behavior of the system when a sensor cannot be read. SFL: #21958 Change-Id: I21f7c4a662b15e536a415e3c4821d4fe3a11c04a Signed-off-by: Kévin L'hôpital --- .../api/api-mockup/src/gapi_discovery.c | 9 +++++++++ example-implementations/api/api-mockup/src/gapi_sensor.c | 1 + 2 files changed, 10 insertions(+) diff --git a/example-implementations/api/api-mockup/src/gapi_discovery.c b/example-implementations/api/api-mockup/src/gapi_discovery.c index 2ae823b..3fb675c 100644 --- a/example-implementations/api/api-mockup/src/gapi_discovery.c +++ b/example-implementations/api/api-mockup/src/gapi_discovery.c @@ -91,6 +91,15 @@ static GeisaSensorDescriptor sensors_platform_info_descriptors[] = { .supports_read = true, .supports_publish = false, }, + { + .sensor_id = "contact-closure-1", + .sensor_type = GeisaSensorType_GEISA_SENSOR_TYPE_CONTACT_STATE, + .name = "Contact Closure", + .has_name = true, + .unit = "State", + .supports_read = false, + .supports_publish = false, + }, }; static GeisaPlatformDiscovery_Sensor sensor_platform_info = { diff --git a/example-implementations/api/api-mockup/src/gapi_sensor.c b/example-implementations/api/api-mockup/src/gapi_sensor.c index e147b81..ec5a193 100644 --- a/example-implementations/api/api-mockup/src/gapi_sensor.c +++ b/example-implementations/api/api-mockup/src/gapi_sensor.c @@ -29,6 +29,7 @@ typedef struct { static sensor_t sensors[] = { {"ambient-temp-1", 25.0}, {"ambient-humidity-1", 45.0}, + {"contact-closure-1", 0.0}, }; // NOLINTEND(cppcoreguidelines-avoid-magic-numbers,readability-magic-numbers) From 3c50bb9a820bfed6aa8c50bb78c32d7ab16221c6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?K=C3=A9vin=20L=27h=C3=B4pital?= Date: Mon, 27 Jul 2026 17:31:19 +0200 Subject: [PATCH 62/64] gapi_sensor: remove useless function arguments MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Some arguments in get_all_sensors_info and get_requested_sensors_info were passed but could be defined inside the function, so we removed them to simplify the code. SFL: #22707 Change-Id: I758bcc9457295500c19960ca1523d1fe29289637 Signed-off-by: Kévin L'hôpital --- .../api/api-mockup/src/gapi_sensor.c | 33 ++++++++++--------- 1 file changed, 17 insertions(+), 16 deletions(-) diff --git a/example-implementations/api/api-mockup/src/gapi_sensor.c b/example-implementations/api/api-mockup/src/gapi_sensor.c index ec5a193..19bb4e3 100644 --- a/example-implementations/api/api-mockup/src/gapi_sensor.c +++ b/example-implementations/api/api-mockup/src/gapi_sensor.c @@ -71,11 +71,12 @@ static int sensor_find(const char *sensor_id) static void get_all_sensors_info(GeisaSensorReadings_Rsp *response, GeisaPlatformDiscovery_Sensor sensors_desc, - GeisaSensorDescriptor sensor_desc, - size_t readings_idx, uint64_t now_ms, - GeisaSensorReading *readings, + uint64_t now_ms, GeisaSensorReading *readings, GeisaSensorValue *values) { + GeisaSensorDescriptor sensor_desc = GeisaSensorDescriptor_init_default; + size_t readings_idx = 0; + for (size_t i = 0; i < sensor_count && readings_idx < MAX_SENSORS_READINGS; i++) { sensor_desc = sensors_desc.sensors[i]; @@ -107,13 +108,16 @@ static void get_all_sensors_info(GeisaSensorReadings_Rsp *response, response->readings = readings; } -static void get_requested_sensors_info( - GeisaSensorReadings_Rsp *response, - GeisaPlatformDiscovery_Sensor sensors_desc, - GeisaSensorDescriptor sensor_desc, size_t readings_idx, uint64_t now_ms, - GeisaSensorReading *readings, GeisaSensorValue *values, - const GeisaSensorReadings_Req *request) +static void +get_requested_sensors_info(GeisaSensorReadings_Rsp *response, + GeisaPlatformDiscovery_Sensor sensors_desc, + uint64_t now_ms, GeisaSensorReading *readings, + GeisaSensorValue *values, + const GeisaSensorReadings_Req *request) { + GeisaSensorDescriptor sensor_desc = GeisaSensorDescriptor_init_default; + size_t readings_idx = 0; + for (pb_size_t i = 0; i < request->sensor_id_count && readings_idx < MAX_SENSORS_READINGS; i++) { @@ -156,15 +160,13 @@ static bool geisa_sensor_build_response(const GeisaSensorReadings_Req *request, static GeisaSensorValue values[MAX_SENSORS_READINGS]; GeisaPlatformDiscovery_Sensor sensors_desc = GeisaPlatformDiscovery_Sensor_init_default; - GeisaSensorDescriptor sensor_desc = GeisaSensorDescriptor_init_default; - size_t readings_idx = 0; uint64_t now_ms = (uint64_t)time(NULL) * SEC_IN_MS; sensors_desc = get_sensors_info(); if (request->sensor_id_count == 0) { - get_all_sensors_info(response, sensors_desc, sensor_desc, - readings_idx, now_ms, readings, values); + get_all_sensors_info(response, sensors_desc, now_ms, readings, + values); return true; } @@ -187,9 +189,8 @@ static bool geisa_sensor_build_response(const GeisaSensorReadings_Req *request, } } - get_requested_sensors_info(response, sensors_desc, sensor_desc, - readings_idx, now_ms, readings, values, - request); + get_requested_sensors_info(response, sensors_desc, now_ms, readings, + values, request); return true; } From 3c3d18ed8c2c5f6e02de03720b5811f61af50f50 Mon Sep 17 00:00:00 2001 From: Scott Wegener Date: Thu, 30 Jul 2026 21:40:13 -0400 Subject: [PATCH 63/64] Integrate GEISA API mockup into Community Signed-off-by: Scott Wegener --- .github/CODEOWNERS | 2 +- .github/workflows/api-mockup.yml | 52 ++++++ .gitmodules | 6 + .../.github/workflows/ci-api-mockup.yml | 30 ---- .../api/api-mockup/.gitmodules | 6 - .../api/api-mockup/README.md | 148 ++++++------------ example-implementations/api/api-mockup/gapi.c | 2 +- .../api/api-mockup/include/gapi_discovery.h | 4 +- .../api/api-mockup/include/gapi_waveform.h | 2 +- .../api/api-mockup/src/gapi_actuator.c | 1 + .../api/api-mockup/src/gapi_discovery.c | 5 +- .../api/api-mockup/src/gapi_manifest.c | 1 + .../api/api-mockup/src/gapi_mosquitto.c | 2 +- .../api/api-mockup/src/gapi_waveform.c | 3 +- .../api/api-mockup/test/test_app.c | 2 +- 15 files changed, 117 insertions(+), 149 deletions(-) create mode 100644 .github/workflows/api-mockup.yml create mode 100644 .gitmodules delete mode 100644 example-implementations/api/api-mockup/.github/workflows/ci-api-mockup.yml delete mode 100644 example-implementations/api/api-mockup/.gitmodules diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 130e5e2..ebefc65 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -17,4 +17,4 @@ /demonstrations/application-isolation-security-systemd-demo/ @blthayer @swegener-rtp # Wakaama ADM client mockup -/example-implementations/adm/lwm2m-client/wakaama/ @ndam-sfl @kevlhop @swegener-rtp +/example-implementations/adm/lwm2m-client/wakaama/ @ndam-sfl @kevlhop @swegener-rtp\n\n# GEISA API mockup\n/example-implementations/api/api-mockup/ @kevlhop @swegener-rtp\n \ No newline at end of file diff --git a/.github/workflows/api-mockup.yml b/.github/workflows/api-mockup.yml new file mode 100644 index 0000000..afb0e13 --- /dev/null +++ b/.github/workflows/api-mockup.yml @@ -0,0 +1,52 @@ +name: API mockup + +on: + pull_request: + paths: + - "example-implementations/api/api-mockup/**" + - ".gitmodules" + - ".github/workflows/api-mockup.yml" + push: + branches: + - main + paths: + - "example-implementations/api/api-mockup/**" + - ".gitmodules" + - ".github/workflows/api-mockup.yml" + +permissions: + contents: read + +jobs: + build: + runs-on: ubuntu-latest + + defaults: + run: + working-directory: example-implementations/api/api-mockup + + steps: + - name: Check out Community + uses: actions/checkout@v4 + with: + submodules: recursive + + - name: Install build dependencies + run: | + sudo apt-get update + sudo apt-get install --yes \ + build-essential \ + clang-format \ + clang-tidy \ + libmosquitto-dev \ + libnanopb-dev \ + pkg-config \ + protobuf-compiler \ + python3-pip + python3 -m pip install --break-system-packages nanopb + + - name: Build + run: make + + - name: Check formatting and lint + run: make lint diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 0000000..0d207bc --- /dev/null +++ b/.gitmodules @@ -0,0 +1,6 @@ +[submodule "api-mockup-schemas"] + path = example-implementations/api/api-mockup/schemas + url = https://github.com/geisa/schemas.git +[submodule "api-mockup-nanopb"] + path = example-implementations/api/api-mockup/nanopb + url = https://github.com/nanopb/nanopb.git diff --git a/example-implementations/api/api-mockup/.github/workflows/ci-api-mockup.yml b/example-implementations/api/api-mockup/.github/workflows/ci-api-mockup.yml deleted file mode 100644 index e60d75e..0000000 --- a/example-implementations/api/api-mockup/.github/workflows/ci-api-mockup.yml +++ /dev/null @@ -1,30 +0,0 @@ -# Copyright (C) 2026 Southern California Edison. -# SPDX-License-Identifier: Apache-2.0 - -name: CI API-mockup - -on: - push: - branches: [main] - pull_request_target: - -jobs: - build_and_linting: - runs-on: ubuntu-latest - steps: - - name: Checkout repository - uses: actions/checkout@v4 - with: - ref: ${{ github.event.pull_request.head.sha || github.sha }} - submodules: true - - name: Install CQFD and create docker image - run: | - curl -LO https://github.com/savoirfairelinux/cqfd/releases/download/v5.8.0/cqfd_5.8.0_all.deb - sudo dpkg -i ./cqfd_5.8.0_all.deb - cqfd init - - name: Build application - run: | - cqfd run - - name: Run linting - run: - cqfd -b lint diff --git a/example-implementations/api/api-mockup/.gitmodules b/example-implementations/api/api-mockup/.gitmodules deleted file mode 100644 index 50ae739..0000000 --- a/example-implementations/api/api-mockup/.gitmodules +++ /dev/null @@ -1,6 +0,0 @@ -[submodule "schemas"] - path = schemas - url = https://github.com/geisa/schemas.git -[submodule "nanopb"] - path = nanopb - url = https://github.com/nanopb/nanopb.git diff --git a/example-implementations/api/api-mockup/README.md b/example-implementations/api/api-mockup/README.md index 10e33c5..091723d 100644 --- a/example-implementations/api/api-mockup/README.md +++ b/example-implementations/api/api-mockup/README.md @@ -1,123 +1,65 @@ -[![License](https://img.shields.io/badge/License-Apache%202.0-blue.svg)](https://opensource.org/licenses/Apache-2.0) +# GEISA API Mockup -# GEISA API Mockup - a GEISA Application Programming Interface mockup implementation +## Relationship to GEISA -GEISA API Mockup is designed to be able to run conformance tests. It aims to provide a mockup implementation of the GEISA Application Programming Interface as defined in the -[GEISA Specification](https://github.com/geisa/specification). +This project is a mock implementation of portions of the GEISA Application +Programming Interface. It was developed primarily to support API conformance +testing and integration work. -The GEISA Specification is an effort by the Grid Edge Interoperability and Security Alliance to define a consistent, secure, and interoperable computing environment for embedded devices at the very edge of the electric utility grid, like electric meters and distribution automation devices, for the benefit of utilities, platform vendors, and software vendors. If you would like to get involved, please head over to our Wiki page for details on participation (https://lfenergy.org/projects/geisa/). Follow the onboarding link for details about participating in our community process. +It is a Community example implementation, not the GEISA specification or a +complete production platform. The published GEISA specification remains the +authoritative source for API requirements and behavior. -## Getting started +## Project Information -To get started with the GEISA API Mockup application, you need to clone the repository and initialize the submodules. +- **Status:** Maintained +- **Maintainer:** Kévin L'hôpital (`@kevlhop`) +- **License:** Apache License 2.0 +- **Original repository:** `geisa/api-mockup` +- **Imported source revision:** `423ac34815099fa7bda455099c6ab654b24b3d7d` -```bash -git submodule update --init --recursive -``` +The project retains its original Git history and pinned dependencies. -## Building the application +## Building and Using -### Requirements +The project uses two Git submodules: -To build the GEISA API Mockup application, you need to have the following tools installed on your system: - - make - - pkg-config - - protobuf-compiler - - libmosquitto-dev - - libnanopb-dev - - nanopb (python package: pip3 install --break-system-packages nanopb) +- nanopb, pinned to the version used by the original implementation +- the GEISA schemas revision used by the original implementation -A docker support is also available to launch the compilation, it requires: - - cqfd (See [requirements](https://github.com/savoirfairelinux/cqfd?tab=readme-ov-file#requirements) and [installation](https://github.com/savoirfairelinux/cqfd?tab=readme-ov-file#installingremoving-cqfd) steps on github) +From the root of the Community repository, initialize them with: -### Build using make -To build the GEISA API Mockup application using make, run the following command in the root directory of the project: + git submodule update --init --recursive \ + example-implementations/api/api-mockup/nanopb \ + example-implementations/api/api-mockup/schemas -```bash -make -``` +Build from the project directory: -### Build using cqfd -To build the GEISA API Mockup application using cqfd, run the following command in the root directory of the project: + cd example-implementations/api/api-mockup + make -```bash -cqfd run -``` +The build requires a C compiler, make, pkg-config, protoc, Mosquitto development +files, nanopb runtime development files, and the nanopb Python generator. -## Build the application for arm64 -To be able to build the application for arm64 architecture, so that you can test directly on an arm64 device, you can use the following commands: +The original CQFD build environment remains under `.cqfd/` and can also be used +from the project directory: -```bash -cqfd -b build_arm64 init -cqfd -b build_arm64 -``` + cqfd run -## Launch the application +After building, run the API mockup with: -To launch the GEISA API Mockup application, simply run the following command: + ./gapi -```bash -./gapi -``` +## Known Limitations -## Linting and formatting - -Linting is done using `clang-tidy` and formatting using `clang-format`. -To lint and format the GEISA API Mockup application code, run the following -command: - -```bash -make lint -``` - -or with cqfd: - -```bash -cqfd -b lint -``` - -## Testing - -To validate the GEISA API Mockup application easily, a test script has been done to print some of the informations received. -The test script is compiled at the same time as the application and is available in the build directory. -After launching the test application, you can run the following command to trigger API calls: - -* To test the discovery API: - -```bash -mosquitto_pub -u testapp -P testapp -h localhost -t geisa/api/platform/discovery/req/testapp -f /tmp/discovery.binpb -``` - -* To test the manifest API: - -```bash -mosquitto_pub -u testapp -P testapp -h localhost -t geisa/api/app/manifest/req/testapp -f /tmp/manifest.binpb -``` - -* To test the waveform API: - * First, you need to create the binpb file with the information you want, for example: - -```bash -printf 'stream_id: "api-mockup-waveform"\nrequest_type: 1\n' | protoc --encode=GeisaWaveform_Req waveform.proto > waveform.binpb -``` - - * Then, you can trigger the API call with the following command: - -```bash -mosquitto_pub -u testapp -P testapp -h localhost -t geisa/api/waveform/req/testapp -f /tmp/waveform.binpb -``` - -* To test the actuator API, there is a dedicated test application that can sends API requests: - -```bash -usage: /tmp/test_app_actuator --get | --set [position] -``` - -* To test the sensor API, there is a dedicated test application that can sends API requests: - -```bash -usage: /tmp/test_app_sensor [sensor_id ...] -``` -When not providing any sensor_id, the application will send a request to get all the sensors available. Otherwise, it will send a request to get the specified sensors. - -The response will be printed by the test_app application +- The implementation covers the API behavior needed by its existing + conformance and integration tests; it is not a complete GEISA platform. +- It assumes a Linux-style environment with Mosquitto and Unix-domain socket + support. +- MQTT users, permissions, topics, and supporting platform services must be + configured separately. +- The schema submodule points to the historical standalone `geisa/schemas` + repository so the imported build remains reproducible. +- Updating the project to schemas maintained with the GEISA specification + should be handled as a separate functional change. +- The original pinned nanopb version is retained during migration. diff --git a/example-implementations/api/api-mockup/gapi.c b/example-implementations/api/api-mockup/gapi.c index 0abf2d0..af2751b 100644 --- a/example-implementations/api/api-mockup/gapi.c +++ b/example-implementations/api/api-mockup/gapi.c @@ -15,7 +15,7 @@ volatile bool running = true; volatile bool isConnected = false; #define PORT 1883 -int main() +int main(void) { pthread_t instantaneous_thread = 0; const char *broker = "localhost"; diff --git a/example-implementations/api/api-mockup/include/gapi_discovery.h b/example-implementations/api/api-mockup/include/gapi_discovery.h index 42cbe42..55a20d4 100644 --- a/example-implementations/api/api-mockup/include/gapi_discovery.h +++ b/example-implementations/api/api-mockup/include/gapi_discovery.h @@ -27,13 +27,13 @@ void api_discovery_init(struct mosquitto *mosq); * * @return Pointer to PlatformDiscoveryWaveform structure */ -GeisaPlatformDiscovery_Waveform get_waveform_info(); +GeisaPlatformDiscovery_Waveform get_waveform_info(void); /** * @brief Get sensors information for platform discovery * * @return Pointer to PlatformDiscoverySensor structure */ -GeisaPlatformDiscovery_Sensor get_sensors_info(); +GeisaPlatformDiscovery_Sensor get_sensors_info(void); #endif // GAPI_DISCOVERY_H diff --git a/example-implementations/api/api-mockup/include/gapi_waveform.h b/example-implementations/api/api-mockup/include/gapi_waveform.h index aeeaeca..14c0598 100644 --- a/example-implementations/api/api-mockup/include/gapi_waveform.h +++ b/example-implementations/api/api-mockup/include/gapi_waveform.h @@ -33,6 +33,6 @@ void api_waveform_init(struct mosquitto *mosq); /** * @brief Deinitialize API waveform data messages */ -void api_waveform_deinit(); +void api_waveform_deinit(void); #endif // GAPI_WAVEFORM_H diff --git a/example-implementations/api/api-mockup/src/gapi_actuator.c b/example-implementations/api/api-mockup/src/gapi_actuator.c index 941856b..615fd66 100644 --- a/example-implementations/api/api-mockup/src/gapi_actuator.c +++ b/example-implementations/api/api-mockup/src/gapi_actuator.c @@ -5,6 +5,7 @@ */ #include "gapi_actuator.h" +#include static GeisaStatus geisa_actuator_success_status = { .code = GeisaStatusCode_GEISA_STATUS_SUCCESS, diff --git a/example-implementations/api/api-mockup/src/gapi_discovery.c b/example-implementations/api/api-mockup/src/gapi_discovery.c index 3fb675c..89c7967 100644 --- a/example-implementations/api/api-mockup/src/gapi_discovery.c +++ b/example-implementations/api/api-mockup/src/gapi_discovery.c @@ -5,6 +5,7 @@ */ #include "gapi_discovery.h" +#include // NOLINTBEGIN(cppcoreguidelines-interfaces-global-init,cppcoreguidelines-avoid-magic-numbers,readability-magic-numbers) static GeisaPlatformDiscovery_GEISA geisa_platform_info = { @@ -218,12 +219,12 @@ api_platform_discovery_build_response(GeisaPlatformDiscovery_Rsp *response) response->has_waveform = true; } -GeisaPlatformDiscovery_Waveform get_waveform_info() +GeisaPlatformDiscovery_Waveform get_waveform_info(void) { return waveform_platform_info; } -GeisaPlatformDiscovery_Sensor get_sensors_info() +GeisaPlatformDiscovery_Sensor get_sensors_info(void) { return sensor_platform_info; } diff --git a/example-implementations/api/api-mockup/src/gapi_manifest.c b/example-implementations/api/api-mockup/src/gapi_manifest.c index 7e95273..d5e92fa 100644 --- a/example-implementations/api/api-mockup/src/gapi_manifest.c +++ b/example-implementations/api/api-mockup/src/gapi_manifest.c @@ -5,6 +5,7 @@ */ #include "gapi_manifest.h" +#include static GeisaStatus geisa_manifest_success_status = { .code = GeisaStatusCode_GEISA_STATUS_SUCCESS, diff --git a/example-implementations/api/api-mockup/src/gapi_mosquitto.c b/example-implementations/api/api-mockup/src/gapi_mosquitto.c index 8cb3be7..b7e19fe 100644 --- a/example-implementations/api/api-mockup/src/gapi_mosquitto.c +++ b/example-implementations/api/api-mockup/src/gapi_mosquitto.c @@ -63,7 +63,7 @@ static void on_message(struct mosquitto *mosq, void *obj, } } -static void free_topic_handlers() +static void free_topic_handlers(void) { for (int i = 0; i < topic_handler_count; i++) { free(topic_handlers[i].topic); diff --git a/example-implementations/api/api-mockup/src/gapi_waveform.c b/example-implementations/api/api-mockup/src/gapi_waveform.c index 60f239b..e28ab9f 100644 --- a/example-implementations/api/api-mockup/src/gapi_waveform.c +++ b/example-implementations/api/api-mockup/src/gapi_waveform.c @@ -6,6 +6,7 @@ #include "gapi_waveform.h" #include "gapi_discovery.h" +#include enum { MAX_WAVEFORM_SOCKETS = 16 }; enum { MAX_CONNECTION_SOCKFD = 10 }; @@ -595,7 +596,7 @@ void api_waveform_init(struct mosquitto *mosq) } } -void api_waveform_deinit() +void api_waveform_deinit(void) { char *socket_path = NULL; diff --git a/example-implementations/api/api-mockup/test/test_app.c b/example-implementations/api/api-mockup/test/test_app.c index dc7fc2d..8ab1958 100644 --- a/example-implementations/api/api-mockup/test/test_app.c +++ b/example-implementations/api/api-mockup/test/test_app.c @@ -270,7 +270,7 @@ void on_message(struct mosquitto *mosq, void *obj, } } -int main() +int main(void) { struct mosquitto *mosq; mosquitto_lib_init(); From ae70b4ec0bc53926dbdab58abeb0ef78bfee6242 Mon Sep 17 00:00:00 2001 From: Scott Wegener Date: Fri, 31 Jul 2026 19:28:21 -0400 Subject: [PATCH 64/64] Finalize API mockup Community migration Signed-off-by: Scott Wegener --- .github/CODEOWNERS | 5 +- .github/workflows/api-mockup.yml | 12 ++- .../api/api-mockup/.cqfd/docker/Dockerfile | 6 +- .../api/api-mockup/.cqfdrc | 6 +- .../api/api-mockup/.gitignore | 3 +- .../api/api-mockup/Makefile | 92 ++++++++++++++----- .../api/api-mockup/README.md | 13 ++- .../api/api-mockup/src/gapi_discovery.c | 8 +- .../api/api-mockup/src/gapi_sensor.c | 23 +++-- 9 files changed, 116 insertions(+), 52 deletions(-) diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index ebefc65..348329c 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -17,4 +17,7 @@ /demonstrations/application-isolation-security-systemd-demo/ @blthayer @swegener-rtp # Wakaama ADM client mockup -/example-implementations/adm/lwm2m-client/wakaama/ @ndam-sfl @kevlhop @swegener-rtp\n\n# GEISA API mockup\n/example-implementations/api/api-mockup/ @kevlhop @swegener-rtp\n \ No newline at end of file +/example-implementations/adm/lwm2m-client/wakaama/ @ndam-sfl @kevlhop @swegener-rtp + +# GEISA API mockup +/example-implementations/api/api-mockup/ @kevlhop @swegener-rtp diff --git a/.github/workflows/api-mockup.yml b/.github/workflows/api-mockup.yml index afb0e13..930f1f9 100644 --- a/.github/workflows/api-mockup.yml +++ b/.github/workflows/api-mockup.yml @@ -39,14 +39,18 @@ jobs: clang-format \ clang-tidy \ libmosquitto-dev \ - libnanopb-dev \ pkg-config \ protobuf-compiler \ - python3-pip - python3 -m pip install --break-system-packages nanopb + python3-pip \ + python3-venv + + - name: Set up development environment + run: make setup-dev - name: Build - run: make + run: | + make clean + make all - name: Check formatting and lint run: make lint diff --git a/example-implementations/api/api-mockup/.cqfd/docker/Dockerfile b/example-implementations/api/api-mockup/.cqfd/docker/Dockerfile index a3ac8c3..ae50ee7 100644 --- a/example-implementations/api/api-mockup/.cqfd/docker/Dockerfile +++ b/example-implementations/api/api-mockup/.cqfd/docker/Dockerfile @@ -17,13 +17,9 @@ RUN set -x \ clang-format \ clang-tidy \ libmosquitto-dev \ - libnanopb-dev \ libprotobuf-c-dev \ pkg-config \ protobuf-compiler \ python3-pip \ + python3-venv \ && rm -rf /var/lib/apt/lists/ - -RUN set -x \ - && pip3 install --no-cache-dir --break-system-packages \ - nanopb diff --git a/example-implementations/api/api-mockup/.cqfdrc b/example-implementations/api/api-mockup/.cqfdrc index b4ef289..a9325a9 100644 --- a/example-implementations/api/api-mockup/.cqfdrc +++ b/example-implementations/api/api-mockup/.cqfdrc @@ -3,12 +3,12 @@ org='geisa' name='api-mockup' [build] -command='make' +command='make setup-dev all' [lint] -command='make lint' +command='make setup-dev lint' [build_arm64] -command='make' +command='make setup-dev all' docker_run_args='--platform linux/arm64' docker_build_args='--platform linux/arm64' diff --git a/example-implementations/api/api-mockup/.gitignore b/example-implementations/api/api-mockup/.gitignore index 378eac2..e7f9d25 100644 --- a/example-implementations/api/api-mockup/.gitignore +++ b/example-implementations/api/api-mockup/.gitignore @@ -1 +1,2 @@ -build +build/ +.venv/ diff --git a/example-implementations/api/api-mockup/Makefile b/example-implementations/api/api-mockup/Makefile index b16841e..655d065 100644 --- a/example-implementations/api/api-mockup/Makefile +++ b/example-implementations/api/api-mockup/Makefile @@ -4,50 +4,96 @@ include nanopb/extra/nanopb.mk BUILDIR ?= build DESTDIR ?= /usr/bin +VENV ?= .venv +PYTHON ?= python3 +PROTOC_CMD ?= protoc + +VENV_BIN := $(CURDIR)/$(VENV)/bin +NANOPB_PLUGIN := $(CURDIR)/nanopb/generator/protoc-gen-nanopb +PROTOC_FLAGS := \ + --experimental_allow_proto3_optional \ + --plugin=protoc-gen-nanopb=$(NANOPB_PLUGIN) + +MOSQUITTO_CFLAGS := $(shell pkg-config --cflags libmosquitto) +MOSQUITTO_LIBS := $(shell pkg-config --libs libmosquitto) + PROTOS := $(wildcard schemas/*.proto) PB_C := $(PROTOS:schemas/%.proto=$(BUILDIR)/schemas/%.pb.c) - +SCHEMA_STAMP := $(BUILDIR)/schemas/.generated SRC := $(wildcard src/*.c) -OBJ := $(patsubst src/%.c,$(BUILDIR)/%.o,$(SRC)) $(PB_C:.c=.o) $(NANOPB_CORE:.c=.o) -CFLAGS := -O2 -Wall -std=c11 -Wextra -pedantic -Werror -Iinclude -I$(BUILDIR) -I/usr/include/nanopb -DPB_ENABLE_MALLOC -LDLIBS := -lmosquitto +OBJ := $(patsubst src/%.c,$(BUILDIR)/%.o,$(SRC)) \ + $(PB_C:.c=.o) \ + $(NANOPB_CORE:.c=.o) + +CFLAGS ?= -O2 -Wall -std=c11 -Wextra -pedantic -Werror +CFLAGS += -Iinclude -I$(BUILDIR) -Inanopb $(MOSQUITTO_CFLAGS) \ + -DPB_ENABLE_MALLOC +LDLIBS += $(MOSQUITTO_LIBS) + +export PATH := $(VENV_BIN):$(PATH) -all: $(BUILDIR)/gapi $(BUILDIR)/test_app $(BUILDIR)/test_app_actuator $(BUILDIR)/test_app_sensor -.PHONY: schemas build_dir clean +.PHONY: all build_dir check-dev clean install lint schemas setup-dev + +all: check-dev $(BUILDIR)/gapi $(BUILDIR)/test_app \ + $(BUILDIR)/test_app_actuator $(BUILDIR)/test_app_sensor + +setup-dev: + $(PYTHON) -m venv $(VENV) + $(VENV_BIN)/python -m pip install --upgrade pip + $(VENV_BIN)/python -m pip install -r nanopb/requirements.txt + +check-dev: + @test -x "$(VENV_BIN)/python" || { \ + echo "Development environment missing. Run 'make setup-dev' first."; \ + exit 1; \ + } build_dir: @mkdir -p $(BUILDIR) -$(BUILDIR)/schemas/%.pb.o: %.c | build_dir schemas +$(PB_C): $(SCHEMA_STAMP) + +$(BUILDIR)/schemas/%.pb.o: $(BUILDIR)/schemas/%.pb.c $(CC) $(CFLAGS) -c $< -o $@ -$(BUILDIR)/%.o: src/%.c | build_dir schemas +$(BUILDIR)/%.o: src/%.c | build_dir $(SCHEMA_STAMP) $(CC) $(CFLAGS) -c $< -o $@ $(BUILDIR)/gapi: $(OBJ) $(CC) $(CFLAGS) -o $@ gapi.c $(OBJ) $(LDLIBS) -schemas: +$(BUILDIR)/test_app: $(OBJ) + $(CC) $(CFLAGS) -o $@ test/test_app.c $(PB_C:.c=.o) \ + $(NANOPB_CORE:.c=.o) $(LDLIBS) + +$(BUILDIR)/test_app_actuator: $(OBJ) + $(CC) $(CFLAGS) -o $@ test/test_app_actuator.c $(PB_C:.c=.o) \ + $(NANOPB_CORE:.c=.o) $(LDLIBS) + +$(BUILDIR)/test_app_sensor: $(OBJ) + $(CC) $(CFLAGS) -o $@ test/test_app_sensor.c $(PB_C:.c=.o) \ + $(NANOPB_CORE:.c=.o) $(LDLIBS) + +schemas: $(SCHEMA_STAMP) + +$(SCHEMA_STAMP): $(PROTOS) $(wildcard nanopb_options/*.options) | check-dev @mkdir -p $(BUILDIR)/schemas - protoc --nanopb_out=$(BUILDIR)/schemas schemas/*.proto -I schemas --nanopb_opt='-Inanopb_options' + $(PROTOC_CMD) $(PROTOC_FLAGS) \ + --nanopb_out=$(BUILDIR)/schemas \ + -I schemas \ + --nanopb_opt='-Inanopb_options' \ + $(PROTOS) + @touch $@ -install: gapi - install -D -m 755 $(BUILDIR)/gapi $(DESTDIR) +install: $(BUILDIR)/gapi + install -D -m 755 $(BUILDIR)/gapi $(DESTDIR)/gapi -lint: +lint: schemas clang-format --Werror --dry-run *.c src/*.c include/*.h - clang-tidy *.c src/*.c include/*.h -- -Iinclude -I$(BUILDIR) -I nanopb + clang-tidy *.c src/*.c include/*.h -- \ + -Iinclude -I$(BUILDIR) -Inanopb $(MOSQUITTO_CFLAGS) clean: rm -rf $(BUILDIR) rm -f nanopb/*.o - -$(BUILDIR)/test_app: $(OBJ) - $(CC) $(CFLAGS) -o $@ test/test_app.c $(PB_C:.c=.o) $(NANOPB_CORE:.c=.o) $(LDLIBS) - -$(BUILDIR)/test_app_actuator: $(OBJ) - $(CC) $(CFLAGS) -o $@ test/test_app_actuator.c $(PB_C:.c=.o) $(NANOPB_CORE:.c=.o) $(LDLIBS) - -$(BUILDIR)/test_app_sensor: $(OBJ) - $(CC) $(CFLAGS) -o $@ test/test_app_sensor.c $(PB_C:.c=.o) $(NANOPB_CORE:.c=.o) $(LDLIBS) diff --git a/example-implementations/api/api-mockup/README.md b/example-implementations/api/api-mockup/README.md index 091723d..bf0dda8 100644 --- a/example-implementations/api/api-mockup/README.md +++ b/example-implementations/api/api-mockup/README.md @@ -36,10 +36,17 @@ From the root of the Community repository, initialize them with: Build from the project directory: cd example-implementations/api/api-mockup + make setup-dev make -The build requires a C compiler, make, pkg-config, protoc, Mosquitto development -files, nanopb runtime development files, and the nanopb Python generator. +`make setup-dev` creates a project-local Python virtual environment and installs +the generator requirements for the pinned nanopb submodule. The host also needs +a C compiler, make, pkg-config, protoc, Python virtual-environment support, +Mosquitto development files, clang-format, and clang-tidy. + +Run the formatting and static-analysis checks with: + + make lint The original CQFD build environment remains under `.cqfd/` and can also be used from the project directory: @@ -48,7 +55,7 @@ from the project directory: After building, run the API mockup with: - ./gapi + ./build/gapi ## Known Limitations diff --git a/example-implementations/api/api-mockup/src/gapi_discovery.c b/example-implementations/api/api-mockup/src/gapi_discovery.c index 89c7967..3af7185 100644 --- a/example-implementations/api/api-mockup/src/gapi_discovery.c +++ b/example-implementations/api/api-mockup/src/gapi_discovery.c @@ -43,8 +43,6 @@ static GeisaPlatformDiscovery_Module sub_module_platform_info = { }; static GeisaPlatformDiscovery_Device device_platform_info = { - .top_module = top_module_platform_info, - .has_top_module = true, .sub_module_count = 1, .sub_module = &sub_module_platform_info, }; @@ -206,8 +204,12 @@ api_platform_discovery_build_response(GeisaPlatformDiscovery_Rsp *response) response->geisa = geisa_platform_info; response->has_geisa = true; response->device = device_platform_info; + response->device.top_module = top_module_platform_info; + response->device.has_top_module = true; response->has_device = true; - response->operator = operator_platform_info; + + GeisaPlatformDiscovery_Operator *operator_info = &response->operator; + *operator_info = operator_platform_info; response->has_operator = true; response->metrology = metrology_platform_info; response->has_metrology = true; diff --git a/example-implementations/api/api-mockup/src/gapi_sensor.c b/example-implementations/api/api-mockup/src/gapi_sensor.c index 19bb4e3..0819fec 100644 --- a/example-implementations/api/api-mockup/src/gapi_sensor.c +++ b/example-implementations/api/api-mockup/src/gapi_sensor.c @@ -74,13 +74,14 @@ static void get_all_sensors_info(GeisaSensorReadings_Rsp *response, uint64_t now_ms, GeisaSensorReading *readings, GeisaSensorValue *values) { - GeisaSensorDescriptor sensor_desc = GeisaSensorDescriptor_init_default; size_t readings_idx = 0; for (size_t i = 0; i < sensor_count && readings_idx < MAX_SENSORS_READINGS; i++) { - sensor_desc = sensors_desc.sensors[i]; - if (sensor_desc.supports_read) { + const GeisaSensorDescriptor *sensor_desc = + &sensors_desc.sensors[i]; + + if (sensor_desc->supports_read) { const sensor_t *sensor = &sensors[i]; GeisaSensorReading *read = &readings[readings_idx]; GeisaSensorValue *value = &values[readings_idx]; @@ -96,8 +97,10 @@ static void get_all_sensors_info(GeisaSensorReadings_Rsp *response, read->values_count = 1; read->values = value; read->has_unit = true; - strncpy(read->unit, sensor_desc.unit, - sizeof(read->unit)); + size_t unit_len = strnlen(sensor_desc->unit, + sizeof(read->unit) - 1); + memcpy(read->unit, sensor_desc->unit, unit_len); + read->unit[unit_len] = '\0'; readings_idx++; } @@ -115,7 +118,6 @@ get_requested_sensors_info(GeisaSensorReadings_Rsp *response, GeisaSensorValue *values, const GeisaSensorReadings_Req *request) { - GeisaSensorDescriptor sensor_desc = GeisaSensorDescriptor_init_default; size_t readings_idx = 0; for (pb_size_t i = 0; i < request->sensor_id_count && @@ -124,9 +126,9 @@ get_requested_sensors_info(GeisaSensorReadings_Rsp *response, const char *req_id = request->sensor_id[i]; int idx = sensor_find(req_id); - // NOLINTNEXTLINE: Out of bound check is done above const sensor_t *sensor = &sensors[idx]; - sensor_desc = sensors_desc.sensors[idx]; + const GeisaSensorDescriptor *sensor_desc = + &sensors_desc.sensors[idx]; GeisaSensorReading *read = &readings[readings_idx]; GeisaSensorValue *value = &values[readings_idx]; @@ -142,7 +144,10 @@ get_requested_sensors_info(GeisaSensorReadings_Rsp *response, read->values_count = 1; read->values = value; read->has_unit = true; - strncpy(read->unit, sensor_desc.unit, sizeof(read->unit)); + size_t unit_len = + strnlen(sensor_desc->unit, sizeof(read->unit) - 1); + memcpy(read->unit, sensor_desc->unit, unit_len); + read->unit[unit_len] = '\0'; readings_idx++; }