diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 130e5e2..348329c 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -18,3 +18,6 @@ # Wakaama ADM client mockup /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 new file mode 100644 index 0000000..930f1f9 --- /dev/null +++ b/.github/workflows/api-mockup.yml @@ -0,0 +1,56 @@ +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 \ + pkg-config \ + protobuf-compiler \ + python3-pip \ + python3-venv + + - name: Set up development environment + run: make setup-dev + + - name: Build + run: | + make clean + make all + + - name: Check formatting and lint + run: make lint diff --git a/.gitmodules b/.gitmodules index 2b31032..13089cc 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,3 +1,9 @@ +[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 [submodule "platforms/nxp-imx93-yocto/sources/poky"] path = platforms/nxp-imx93-yocto/sources/poky url = https://git.yoctoproject.org/poky diff --git a/example-implementations/api/api-mockup/.clang-format b/example-implementations/api/api-mockup/.clang-format new file mode 100644 index 0000000..a14da4f --- /dev/null +++ b/example-implementations/api/api-mockup/.clang-format @@ -0,0 +1,8 @@ +BasedOnStyle: llvm +IndentWidth: 8 +UseTab: Always +BreakBeforeBraces: Linux +AllowShortIfStatementsOnASingleLine: false +IndentCaseLabels: false +ContinuationIndentWidth: 8 +BreakStringLiterals: 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/.cqfd/docker/Dockerfile b/example-implementations/api/api-mockup/.cqfd/docker/Dockerfile new file mode 100644 index 0000000..ae50ee7 --- /dev/null +++ b/example-implementations/api/api-mockup/.cqfd/docker/Dockerfile @@ -0,0 +1,25 @@ +# Copyright (C) 2025 Southern California Edison. + +FROM ubuntu:26.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 \ + build-essential \ + clang-format \ + clang-tidy \ + libmosquitto-dev \ + libprotobuf-c-dev \ + pkg-config \ + protobuf-compiler \ + python3-pip \ + python3-venv \ + && 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..a9325a9 --- /dev/null +++ b/example-implementations/api/api-mockup/.cqfdrc @@ -0,0 +1,14 @@ +[project] +org='geisa' +name='api-mockup' + +[build] +command='make setup-dev all' + +[lint] +command='make setup-dev lint' + +[build_arm64] +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 new file mode 100644 index 0000000..e7f9d25 --- /dev/null +++ b/example-implementations/api/api-mockup/.gitignore @@ -0,0 +1,2 @@ +build/ +.venv/ 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. diff --git a/example-implementations/api/api-mockup/Makefile b/example-implementations/api/api-mockup/Makefile new file mode 100644 index 0000000..655d065 --- /dev/null +++ b/example-implementations/api/api-mockup/Makefile @@ -0,0 +1,99 @@ +# Copyright (C) 2025 Southern California Edison + +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 +CFLAGS += -Iinclude -I$(BUILDIR) -Inanopb $(MOSQUITTO_CFLAGS) \ + -DPB_ENABLE_MALLOC +LDLIBS += $(MOSQUITTO_LIBS) + +export PATH := $(VENV_BIN):$(PATH) + +.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) + +$(PB_C): $(SCHEMA_STAMP) + +$(BUILDIR)/schemas/%.pb.o: $(BUILDIR)/schemas/%.pb.c + $(CC) $(CFLAGS) -c $< -o $@ + +$(BUILDIR)/%.o: src/%.c | build_dir $(SCHEMA_STAMP) + $(CC) $(CFLAGS) -c $< -o $@ + +$(BUILDIR)/gapi: $(OBJ) + $(CC) $(CFLAGS) -o $@ gapi.c $(OBJ) $(LDLIBS) + +$(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_CMD) $(PROTOC_FLAGS) \ + --nanopb_out=$(BUILDIR)/schemas \ + -I schemas \ + --nanopb_opt='-Inanopb_options' \ + $(PROTOS) + @touch $@ + +install: $(BUILDIR)/gapi + install -D -m 755 $(BUILDIR)/gapi $(DESTDIR)/gapi + +lint: schemas + clang-format --Werror --dry-run *.c src/*.c include/*.h + clang-tidy *.c src/*.c include/*.h -- \ + -Iinclude -I$(BUILDIR) -Inanopb $(MOSQUITTO_CFLAGS) + +clean: + rm -rf $(BUILDIR) + rm -f nanopb/*.o diff --git a/example-implementations/api/api-mockup/README.md b/example-implementations/api/api-mockup/README.md new file mode 100644 index 0000000..bf0dda8 --- /dev/null +++ b/example-implementations/api/api-mockup/README.md @@ -0,0 +1,72 @@ +# GEISA API Mockup + +## Relationship to GEISA + +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. + +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. + +## Project Information + +- **Status:** Maintained +- **Maintainer:** Kévin L'hôpital (`@kevlhop`) +- **License:** Apache License 2.0 +- **Original repository:** `geisa/api-mockup` +- **Imported source revision:** `423ac34815099fa7bda455099c6ab654b24b3d7d` + +The project retains its original Git history and pinned dependencies. + +## Building and Using + +The project uses two Git submodules: + +- nanopb, pinned to the version used by the original implementation +- the GEISA schemas revision used by the original implementation + +From the root of the Community repository, initialize them with: + + git submodule update --init --recursive \ + example-implementations/api/api-mockup/nanopb \ + example-implementations/api/api-mockup/schemas + +Build from the project directory: + + cd example-implementations/api/api-mockup + make setup-dev + make + +`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: + + cqfd run + +After building, run the API mockup with: + + ./build/gapi + +## Known Limitations + +- 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 new file mode 100644 index 0000000..af2751b --- /dev/null +++ b/example-implementations/api/api-mockup/gapi.c @@ -0,0 +1,67 @@ +/** + * @file gapi.c + * @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" +#include "gapi_mosquitto.h" +#include "gapi_sensor.h" +#include "gapi_waveform.h" + +volatile bool running = true; +volatile bool isConnected = false; +#define PORT 1883 + +int main(void) +{ + 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); + if (!mosq) { + return EXIT_FAILURE; + } + + while (running && !isConnected) { + mosquitto_loop(mosq, -1, 1); + sleep(1); + } + + api_discovery_init(mosq); + api_manifest_init(mosq); + ret = api_instantaneous_init(&instantaneous_thread, mosq); + if (ret != EXIT_SUCCESS) { + goto deinit; + } + api_waveform_init(mosq); + api_actuator_init(mosq); + api_sensor_init(mosq); + + 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); + +deinit: + api_communication_deinit(mosq); + api_waveform_deinit(); + return ret; +} 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/include/gapi_discovery.h b/example-implementations/api/api-mockup/include/gapi_discovery.h new file mode 100644 index 0000000..55a20d4 --- /dev/null +++ b/example-implementations/api/api-mockup/include/gapi_discovery.h @@ -0,0 +1,39 @@ +/** + * @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 +#define _GNU_SOURCE + +#include "gapi_mosquitto.h" +#include "pb.h" +#include "pb_decode.h" +#include "pb_encode.h" +#include "schemas/discovery.pb.h" +#include + +/** + * @brief Initialize API platform discovery messages + * + * @param mosq Pointer to the mosquitto instance + */ +void api_discovery_init(struct mosquitto *mosq); + +/** + * @brief Get waveform information for platform discovery + * + * @return Pointer to PlatformDiscoveryWaveform structure + */ +GeisaPlatformDiscovery_Waveform get_waveform_info(void); + +/** + * @brief Get sensors information for platform discovery + * + * @return Pointer to PlatformDiscoverySensor structure + */ +GeisaPlatformDiscovery_Sensor get_sensors_info(void); + +#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 new file mode 100644 index 0000000..e03086d --- /dev/null +++ b/example-implementations/api/api-mockup/include/gapi_instantaneous.h @@ -0,0 +1,29 @@ +/** + * @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 "pb.h" +#include "pb_encode.h" +#include "schemas/metered_quantities.pb.h" +#include +#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); + +#define SEC_IN_MS 1000 + +#endif // GAPI_INSTANTANEOUS_H 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/include/gapi_mosquitto.h b/example-implementations/api/api-mockup/include/gapi_mosquitto.h new file mode 100644 index 0000000..3e3d8ce --- /dev/null +++ b/example-implementations/api/api-mockup/include/gapi_mosquitto.h @@ -0,0 +1,85 @@ +/** + * @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 + * @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 qos); + +/** + * @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_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, 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)(struct mosquitto *mosq, const char *topic, + const int payloadlen, const uint8_t *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/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/include/gapi_waveform.h b/example-implementations/api/api-mockup/include/gapi_waveform.h new file mode 100644 index 0000000..14c0598 --- /dev/null +++ b/example-implementations/api/api-mockup/include/gapi_waveform.h @@ -0,0 +1,38 @@ +/** + * @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 "pb.h" +#include "pb_decode.h" +#include "pb_encode.h" +#include "schemas/waveform.pb.h" +#include +#include +#include +#include +#include +#include +#include +#include +#include + +/** + * @brief Initialize API waveform data messages + * + * @param mosq Pointer to the mosquitto instance + */ +void api_waveform_init(struct mosquitto *mosq); + +/** + * @brief Deinitialize API waveform data messages + */ +void api_waveform_deinit(void); + +#endif // GAPI_WAVEFORM_H 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/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/nanopb_options/discovery.options b/example-implementations/api/api-mockup/nanopb_options/discovery.options new file mode 100644 index 0000000..90c20e4 --- /dev/null +++ b/example-implementations/api/api-mockup/nanopb_options/discovery.options @@ -0,0 +1,6 @@ +*.* max_size:32 +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/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/sensor.options b/example-implementations/api/api-mockup/nanopb_options/sensor.options new file mode 100644 index 0000000..b69619a --- /dev/null +++ b/example-implementations/api/api-mockup/nanopb_options/sensor.options @@ -0,0 +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/nanopb_options/waveform.options b/example-implementations/api/api-mockup/nanopb_options/waveform.options new file mode 100644 index 0000000..db046c1 --- /dev/null +++ b/example-implementations/api/api-mockup/nanopb_options/waveform.options @@ -0,0 +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/schemas b/example-implementations/api/api-mockup/schemas new file mode 160000 index 0000000..df75ff5 --- /dev/null +++ b/example-implementations/api/api-mockup/schemas @@ -0,0 +1 @@ +Subproject commit df75ff51332c2aca0541fa60c2e740bcc276c3ce 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..615fd66 --- /dev/null +++ b/example-implementations/api/api-mockup/src/gapi_actuator.c @@ -0,0 +1,272 @@ +/** + * @file gapi_actuator.c + * @brief Definition file for API actuator messages + * @copyright Copyright (C) 2026 Southern California Edison + */ + +#include "gapi_actuator.h" +#include + +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 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, + 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 { + 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; + } + 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; + int actuator_idx = -1; + + 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 { + 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); + + 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_discovery.c b/example-implementations/api/api-mockup/src/gapi_discovery.c new file mode 100644 index 0000000..3af7185 --- /dev/null +++ b/example-implementations/api/api-mockup/src/gapi_discovery.c @@ -0,0 +1,318 @@ +/** + * @file gapi_discovery.c + * @brief Definition file for API platform discovery messages + * @copyright Copyright (C) 2025 Southern California Edison + */ + +#include "gapi_discovery.h" +#include + +// NOLINTBEGIN(cppcoreguidelines-interfaces-global-init,cppcoreguidelines-avoid-magic-numbers,readability-magic-numbers) +static GeisaPlatformDiscovery_GEISA geisa_platform_info = { + .ver_major = 0, + .ver_minor = 1, + .ver_rev = 7, + .pillar_adm = true, + .pillar_api = true, + .pillar_lee = true, + .pillar_vee = false, +}; + +static const GeisaPlatformDiscovery_DeviceType top_type_platform_info = + GeisaPlatformDiscovery_DeviceType_TYPE_ELECTRIC_METER; + +static const GeisaPlatformDiscovery_Module top_module_platform_info = { + .type = top_type_platform_info, + .manufacturer = "SCE", + .model = "GEISA-1", + .serial_number = "GEISA0001", + .hw_revision = "A", + .fw_revision = "1.0.0", +}; + +static const GeisaPlatformDiscovery_DeviceType sub_type_platform_info = + GeisaPlatformDiscovery_DeviceType_TYPE_METROLOGY_PROCESSOR; + +static GeisaPlatformDiscovery_Module sub_module_platform_info = { + .type = sub_type_platform_info, + .manufacturer = "SCE", + .model = "GEISA-2", + .serial_number = "GEISA0002", + .hw_revision = "B", + .fw_revision = "1.1.0", +}; + +static GeisaPlatformDiscovery_Device device_platform_info = { + .sub_module_count = 1, + .sub_module = &sub_module_platform_info, +}; + +static GeisaPlatformDiscovery_Operator operator_platform_info = { + .operator_name = "SCE", + .operator_identifier = "SCE001", +}; + +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_id = "ambient-temp-1", + .sensor_type = GeisaSensorType_GEISA_SENSOR_TYPE_TEMPERATURE, + .name = "Ambient Temperature", + .has_name = true, + .unit = "Celsius", + .supports_read = true, + .supports_publish = false, + }, + { + .sensor_id = "ambient-humidity-1", + .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, + }, + { + .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 = { + .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; + +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 = 2, + .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", +}; + +// NOLINTEND(cppcoreguidelines-interfaces-global-init,cppcoreguidelines-avoid-magic-numbers,readability-magic-numbers) + +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; + response->device.top_module = top_module_platform_info; + response->device.has_top_module = true; + response->has_device = true; + + GeisaPlatformDiscovery_Operator *operator_info = &response->operator; + *operator_info = 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; +} + +GeisaPlatformDiscovery_Waveform get_waveform_info(void) +{ + return waveform_platform_info; +} + +GeisaPlatformDiscovery_Sensor get_sensors_info(void) +{ + return sensor_platform_info; +} + +static void api_platform_discovery_req_handler(struct mosquitto *mosq, + const char *topic, + const int payloadlen, + const uint8_t *payload) +{ + 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); + + fprintf(stdout, + "[Discovery] Received platform discovery request from %s\n", + app_id); + fflush(stdout); + + 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"); + response.status = geisa_discovery_payload_status; + response.has_status = true; + } else { + api_platform_discovery_build_response(&response); + } + pb_release(GeisaPlatformDiscovery_Req_fields, &request); + + 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"); + return; + } + + 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; + } + + 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); +} + +void api_discovery_init(struct mosquitto *mosq) +{ + api_register_handler("geisa/api/platform/discovery/req", + api_platform_discovery_req_handler); + + api_subscribe(mosq, "geisa/api/platform/discovery/req/#", 1); +} 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..01d2286 --- /dev/null +++ b/example-implementations/api/api-mockup/src/gapi_instantaneous.c @@ -0,0 +1,180 @@ +/** + * @file gapi_instantaneous.c + * @brief Definition file for API instantaneous data sending + * @copyright Copyright (C) 2025 Southern California Edison + */ + +#include "gapi_instantaneous.h" +#include "pb_decode.h" + +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. + 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 = 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 = 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 = 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 = 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; + + response->timestamp = timestamp; + 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; + 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) +{ + struct mosquitto *mosq = (struct mosquitto *)arg; + GeisaInstantaneousQuantities response = + GeisaInstantaneousQuantities_init_default; + size_t encoded_size = 0; + uint8_t *message = NULL; + pb_ostream_t ostream; + bool status = true; + + while (running) { + 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; + } + 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); + } + + 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; +} 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..d5e92fa --- /dev/null +++ b/example-implementations/api/api-mockup/src/gapi_manifest.c @@ -0,0 +1,198 @@ +/** + * @file gapi_manifest.c + * @brief Definition file for API application manifest messages + * @copyright Copyright (C) 2026 Southern California Edison + */ + +#include "gapi_manifest.h" +#include + +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); +} diff --git a/example-implementations/api/api-mockup/src/gapi_mosquitto.c b/example-implementations/api/api-mockup/src/gapi_mosquitto.c new file mode 100644 index 0000000..b7e19fe --- /dev/null +++ b/example-implementations/api/api-mockup/src/gapi_mosquitto.c @@ -0,0 +1,166 @@ +/** + * @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; +enum { MAX_TOPIC_HANDLERS = 6 }; + +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) +{ + 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"); + fflush(stdout); + 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); + fflush(stdout); + isConnected = false; +} + +static void on_message(struct mosquitto *mosq, void *obj, + const struct mosquitto_message *msg) +{ + (void)obj; + for (int i = 0; i < topic_handler_count; i++) { + if (strstr(msg->topic, topic_handlers[i].topic) != NULL) { + topic_handlers[i].topic_handler( + mosq, msg->topic, msg->payloadlen, + (uint8_t *)msg->payload); + return; + } + } +} + +static void free_topic_handlers(void) +{ + 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) +{ + struct mosquitto *mosq = NULL; + int return_code = 0; + + mosquitto_lib_init(); + + mosq = mosquitto_new("gapi", false, NULL); + if (!mosq) { + fprintf(stderr, "Error: mosquitto_new() failed\n"); + goto cleanup; + } + + mosquitto_username_pw_set(mosq, "API", "api"); + mosquitto_connect_callback_set(mosq, on_connect); + mosquitto_disconnect_callback_set(mosq, on_disconnect); + 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); + free_topic_handlers(); + mosquitto_destroy(mosq); + mosquitto_lib_cleanup(); +} + +int api_subscribe(struct mosquitto *mosq, const char *topic, int qos) +{ + int return_code = 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)); + return return_code; + } + + return 0; +} + +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, NULL, topic, (int)message_size, + message, qos, false); + if (return_code != MOSQ_ERR_SUCCESS) { + fprintf(stderr, "Publish failed: %s\n", + mosquitto_strerror(return_code)); + return return_code; + } + + 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"); + } +} 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..0819fec --- /dev/null +++ b/example-implementations/api/api-mockup/src/gapi_sensor.c @@ -0,0 +1,273 @@ +/** + * @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}, + {"contact-closure-1", 0.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, + uint64_t now_ms, GeisaSensorReading *readings, + GeisaSensorValue *values) +{ + size_t readings_idx = 0; + + for (size_t i = 0; + i < sensor_count && readings_idx < MAX_SENSORS_READINGS; i++) { + 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]; + + 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; + 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++; + } + } + 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, + uint64_t now_ms, GeisaSensorReading *readings, + GeisaSensorValue *values, + const GeisaSensorReadings_Req *request) +{ + size_t readings_idx = 0; + + 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); + const sensor_t *sensor = &sensors[idx]; + const GeisaSensorDescriptor *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; + 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++; + } + + 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; + + 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, 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, 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); +} 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..e28ab9f --- /dev/null +++ b/example-implementations/api/api-mockup/src/gapi_waveform.c @@ -0,0 +1,629 @@ +/** + * @file gapi_waveform.c + * @brief Definition file for API waveform data messages + * @copyright Copyright (C) 2026 Southern California Edison + */ + +#include "gapi_waveform.h" +#include "gapi_discovery.h" +#include + +enum { MAX_WAVEFORM_SOCKETS = 16 }; +enum { MAX_CONNECTION_SOCKFD = 10 }; +enum { EPOLL_TIMEOUT_MS = 1000 }; + +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 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) +{ + 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; + } + + 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" + "%s\n", + app_id, 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) { + 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 (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", + app_id, stream_id); + close(file_descriptor); + unlink(socket_path); + 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", + app_id, stream_id); + + return 0; +} + +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 " + "%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; + } +} + +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; + + waveform_platform_info = get_waveform_info(); + + 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; + } + + for (int i = 0; i < waveform_platform_info.streams_count; i++) { + if (strcmp(waveform_platform_info.streams[i].stream_id, + stream_id) == 0) { + waveform_handle_stream_request(response, request_type, + i, app_id, + waveform_platform_info); + break; + } + } + + if (response->waveform_status == + 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); + } +} + +static void api_waveform_req_handler(struct mosquitto *mosq, const char *topic, + const int payloadlen, + const uint8_t *payload) +{ + 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; + 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); + + istream = pb_istream_from_buffer(payload, payloadlen); + status = pb_decode(&istream, GeisaWaveform_Req_fields, &request); + if (!status) { + fprintf(stderr, + "[Waveform] Error decoding waveform app request\n"); + 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"); + free(response.socket_path); + return; + } + + status = pb_get_encoded_size(&encoded_size, GeisaWaveform_Rsp_fields, + &response); + if (!status) { + fprintf(stderr, "[Waveform] Error calculating size of response " + "message\n"); + free(response.socket_path); + free(rsp_topic); + return; + } + + message = malloc(encoded_size); + if (message == NULL) { + fprintf(stderr, + "[Waveform] Error allocating memory for response " + "message\n"); + free(response.socket_path); + free(rsp_topic); + return; + } + + 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(response.socket_path); + free(message); + free(rsp_topic); + return; + } + + api_publish(mosq, rsp_topic, encoded_size, message, 1); + free(response.socket_path); + free(message); +} + +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); + } + + 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(void) +{ + char *socket_path = NULL; + + 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 = 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) { + 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); + } + + if (waveform_epoll_fd != -1) { + close(waveform_epoll_fd); + waveform_epoll_fd = -1; + } +} 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..8ab1958 --- /dev/null +++ b/example-implementations/api/api-mockup/test/test_app.c @@ -0,0 +1,293 @@ +#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" +#include "schemas/waveform.pb.h" +#include "schemas/sensor.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); + mosquitto_subscribe(mosq, NULL, + "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); + } +} + +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("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", + 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); + 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); +} + +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("Status: %s\n", msg.status.message); + 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, subscribed:%d, expected_frame_period_ms: %d\n", msg.waveform_status, + msg.subscribed, msg.expected_frame_period_ms); + + 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; + 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); + 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); +} + +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) +{ + (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 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 if (strcmp(msg->topic, "geisa/api/sensor-rsp/testapp") == 0) { + handle_sensor_response(msg->payload, msg->payloadlen); + } else { + printf("Unknown topic\n"); + } +} + +int main(void) +{ + 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; +} 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; +} 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; +}