diff --git a/README.md b/README.md index dd44329..cd64dd1 100644 --- a/README.md +++ b/README.md @@ -25,12 +25,14 @@ Component | Content | Comment/Status [eCAL Provider](ecal2val) | Python provider for [eCAL](https://projects.eclipse.org/projects/automotive.ecal) [PS4/PS5 - 2021 Formula Provider](./fone2val) | F1 Telemetrydata source for [KUKSA Databroker](https://github.com/eclipse/kuksa.val/tree/master/kuksa_databroker) [KUKSA GO Client](kuksa_go_client) | Example client written in the [GO](https://go.dev/) programming language for easy interaction with KUKSA Databroker and Server +[ESP32 gRPC provider](gRPC-on-ESP32) | Example for interacting with the [KUKSA Databroker](https://github.com/eclipse/kuksa.val/tree/master/kuksa_databroker) with ESP32-based microcontrollers ## Contribution For contribution guidelines see [CONTRIBUTING.md](CONTRIBUTING.md) ## Pre-commit set up + This repository is set up to use [pre-commit](https://pre-commit.com/) hooks. Use `pip install pre-commit` to install pre-commit. After you clone the project, run `pre-commit install` to install pre-commit into your git hooks. diff --git a/gRPC-on-ESP32/.devcontainer/Dockerfile b/gRPC-on-ESP32/.devcontainer/Dockerfile new file mode 100644 index 0000000..dcf1bac --- /dev/null +++ b/gRPC-on-ESP32/.devcontainer/Dockerfile @@ -0,0 +1,51 @@ +FROM espressif/idf + +ARG DEBIAN_FRONTEND=nointeractive +ARG CONTAINER_USER=esp +ARG USER_UID=1000 +ARG USER_GID=$USER_UID + +RUN apt-get update \ + && apt install -y -q \ + cmake \ + git \ + hwdata \ + libglib2.0-0 \ + libnuma1 \ + libpixman-1-0 \ + linux-tools-virtual \ + && rm -rf /var/lib/apt/lists/* + +RUN update-alternatives --install /usr/local/bin/usbip usbip `ls /usr/lib/linux-tools/*/usbip | tail -n1` 20 + +# QEMU +ENV QEMU_REL=esp-develop-20220919 +ENV QEMU_SHA256=f6565d3f0d1e463a63a7f81aec94cce62df662bd42fc7606de4b4418ed55f870 +ENV QEMU_DIST=qemu-${QEMU_REL}.tar.bz2 +ENV QEMU_URL=https://github.com/espressif/qemu/releases/download/${QEMU_REL}/${QEMU_DIST} + +ENV LC_ALL=C.UTF-8 +ENV LANG=C.UTF-8 + +RUN wget --no-verbose ${QEMU_URL} \ + && echo "${QEMU_SHA256} *${QEMU_DIST}" | sha256sum --check --strict - \ + && tar -xf $QEMU_DIST -C /opt \ + && rm ${QEMU_DIST} + +ENV PATH=/opt/qemu/bin:${PATH} + +RUN groupadd --gid $USER_GID $CONTAINER_USER \ + && adduser --uid $USER_UID --gid $USER_GID --disabled-password --gecos "" ${CONTAINER_USER} \ + && usermod -a -G root $CONTAINER_USER && usermod -a -G dialout $CONTAINER_USER + +RUN chmod -R 775 /opt/esp/python_env/ + +USER ${CONTAINER_USER} +ENV USER=${CONTAINER_USER} +WORKDIR /home/${CONTAINER_USER} + +RUN echo "source /opt/esp/idf/export.sh > /dev/null 2>&1" >> ~/.bashrc + +ENTRYPOINT [ "/opt/esp/entrypoint.sh" ] + +CMD ["/bin/bash", "-c"] diff --git a/gRPC-on-ESP32/.devcontainer/devcontainer.json b/gRPC-on-ESP32/.devcontainer/devcontainer.json new file mode 100644 index 0000000..41e381a --- /dev/null +++ b/gRPC-on-ESP32/.devcontainer/devcontainer.json @@ -0,0 +1,47 @@ +// For format details, see https://aka.ms/devcontainer.json. For config options, see the README at: +// https://github.com/microsoft/vscode-dev-containers/tree/v0.183.0/containers/ubuntu +{ + "name": "ESP-IDF QEMU", + "build": { + "dockerfile": "Dockerfile" + }, + // Add the IDs of extensions you want installed when the container is created + "workspaceMount": "source=${localWorkspaceFolder},target=${localWorkspaceFolder},type=bind", + /* the path of workspace folder to be opened after container is running + */ + "workspaceFolder": "${localWorkspaceFolder}", + "mounts": [ + "source=extensionCache,target=/root/.vscode-server/extensions,type=volume" + ], + "customizations": { + "vscode": { + "settings": { + "terminal.integrated.defaultProfile.linux": "bash", + "idf.espIdfPath": "/opt/esp/idf", + "idf.customExtraPaths": "", + "idf.pythonBinPath": "/opt/esp/python_env/idf5.3_py3.10_env/bin/python", + "idf.toolsPath": "/opt/esp", + "idf.gitPath": "/usr/bin/git" + }, + "extensions": [ + "espressif.esp-idf-extension" + ], + }, + "codespaces": { + "settings": { + "terminal.integrated.defaultProfile.linux": "bash", + "idf.espIdfPath": "/opt/esp/idf", + "idf.customExtraPaths": "", + "idf.pythonBinPath": "/opt/esp/python_env/idf5.3_py3.10_env/bin/python", + "idf.toolsPath": "/opt/esp", + "idf.gitPath": "/usr/bin/git" + }, + "extensions": [ + "espressif.esp-idf-extension" + ], + } + }, + "runArgs": [ + "--privileged" + ] +} diff --git a/gRPC-on-ESP32/.gitignore b/gRPC-on-ESP32/.gitignore new file mode 100644 index 0000000..274ada5 --- /dev/null +++ b/gRPC-on-ESP32/.gitignore @@ -0,0 +1,7 @@ +.idea +.vscode +build/ +managed_components/ +cmake-build-debug/ +sdkconfig +components diff --git a/gRPC-on-ESP32/CMakeLists.txt b/gRPC-on-ESP32/CMakeLists.txt new file mode 100644 index 0000000..0a56c2e --- /dev/null +++ b/gRPC-on-ESP32/CMakeLists.txt @@ -0,0 +1,13 @@ +# The following lines of boilerplate have to be in your project's CMakeLists +# in this exact order for cmake to work correctly +cmake_minimum_required(VERSION 3.16) + +# Add the directory containing Nanopb to the list of extra component directories +# This assumes that `nanopb` is a directory within `components` +set(EXTRA_COMPONENT_DIRS + $ENV{IDF_PATH}/examples/common_components/protocol_examples_common + ${CMAKE_CURRENT_LIST_DIR}/components/nanopb +) + +include($ENV{IDF_PATH}/tools/cmake/project.cmake) +project(gRPC-on-esp32) diff --git a/gRPC-on-ESP32/NOTICE.md b/gRPC-on-ESP32/NOTICE.md new file mode 100644 index 0000000..0b1a4eb --- /dev/null +++ b/gRPC-on-ESP32/NOTICE.md @@ -0,0 +1,33 @@ +# esp-grpc + +The file src/grpc.c and src/grpc.h are derived from [grpc.c](https://github.com/chrisomatic/esp-grpc/blob/main/main/grpc.c) +and [grpc.h](https://github.com/chrisomatic/esp-grpc/blob/main/main/grpc.h). + +Reference: + +The [esp-grpc](https://github.com/chrisomatic/esp-grpc/tree/5395918b5aa38314289b07a386601b7709c6c8e8) project is licensed under the following terms: + +``` +MIT License + +Copyright (c) 2022 Christopher Rose + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + +``` diff --git a/gRPC-on-ESP32/README.md b/gRPC-on-ESP32/README.md new file mode 100644 index 0000000..1bda07f --- /dev/null +++ b/gRPC-on-ESP32/README.md @@ -0,0 +1,74 @@ +# gRPC on ESP32 + +This project offers a example on developing a gRPC client application designed to dispatch unary gRPC calls to a gRPC server, utilizing HTTP/2 as the underlying protocol. + +The project incorporates the following building blocks: + ++ [ESP-IDF](https://github.com/espressif/esp-idf) - ESP-32 Development Framework + ++ [nghttp2](https://nghttp2.org/) - integrated with [esp-idf extra components](https://components.espressif.com/components/espressif/nghttp) + ++ [nanopb](https://github.com/nanopb/nanopb) - compilation of protobufs, encoding/decoding of messages and responses into binary wire format + ++ [kuksa.val Databroker](https://github.com/eclipse/kuksa.val) - protobuf definitions for communicating via gRPC + +Note that this is a simplistic illustration of interacting with the Kuksa Databroker. For more detailed message exchanges, consider starting with `encoder.c` or `decoder.c`. These files contain the protobuf-specific implementations necessary for the exchange. See `grpc.c` for the HTTP/2 gRPC implementation. + +An application scenario would be conceivable in which the ESP32 represents a microcontroller that sends sensor values such as the current speed of the vehicle to the Kuksa Databroker via GRPC. + +## Hardware Required + ++ A development board with ESP32/ESP32-S2/ESP32-C3 SoC (e.g., ESP32-DevKitC, ESP-WROVER-KIT, etc.) ++ A USB cable for power supply and programming + +## Configure the project + +1. **Install the esp-idf toolchain** as shown in the esp-idf [docs](https://docs.espressif.com/projects/esp-idf/en/stable/esp32/get-started/index.html#installation). + +```bash +idf.py menuconfig +``` + +2. **Incorporate nanopb specific header files** + +```bash +chmod +x install_nanopb.sh +./install_nanopb.sh +``` + +3. **Compile Protobufs** + ++ Activate the esp-idf development toolchain and environment with `. ./export.sh` inside your toolchain installation +directory (typically under `~/esp/...`) ++ execute shell script `compile_proto.sh` + +> 📝 Sometimes nanopb will give you a warning, that some dependencies are not installed. Install them with `pip install protobuf grpcio-tools` + +4. **Install expra components** +In the project root, execute the command below to add the nghttp2 lib to the project. + +```bash +idf.py add-dependency "espressif/nghttp^1.58.0" +``` + +5. **Open the project configuration menu** (`idf.py menuconfig`) to configure Wi-Fi or Ethernet. + +## Create Get/Set requests + +Refer to the `main.c` file to modify the message type and set the target URL with a kuksa Databroker running. To send a request for retrieving information, such as the vehicle's current speed, utilize the get section of the code, which is already uncommented. If you wish to set a value in the broker, uncomment the set section and employ the defined constants for _set_. + +## Build and Flash + +Build the project and flash it to the board, then run monitor tool to view serial output: + +```bash +idf.py flash monitor +``` + +(To exit the serial monitor, type ``Ctrl-]`` or ``Ctrl-TX`` on Mac.) + +See the Getting Started Guide for full steps to configure and use ESP-IDF to build projects. + +## Example Output + +![Serial Output](assets/serial-output.png) diff --git a/gRPC-on-ESP32/assets/serial-output.png b/gRPC-on-ESP32/assets/serial-output.png new file mode 100644 index 0000000..3041398 Binary files /dev/null and b/gRPC-on-ESP32/assets/serial-output.png differ diff --git a/gRPC-on-ESP32/compile_proto.sh b/gRPC-on-ESP32/compile_proto.sh new file mode 100755 index 0000000..649324c --- /dev/null +++ b/gRPC-on-ESP32/compile_proto.sh @@ -0,0 +1,19 @@ +################################################################################# +# Copyright (c) 2024 Contributors to the Eclipse Foundation +# +# See the NOTICE file(s) distributed with this work for additional +# information regarding copyright ownership. +# +# This program and the accompanying materials are made available under the +# terms of the Apache License 2.0 which is available at +# http://www.apache.org/licenses/LICENSE-2.0 +# +# SPDX-License-Identifier: Apache-2.0 +################################################################################# + +#!/bin/sh +./components/nanopb/generator-bin/protoc --proto_path=proto/ --nanopb_out=main/generated val.proto types.proto + +./components/nanopb/generator-bin/protoc --proto_path=proto/ --nanopb_out=components/nanopb/google/protobuf/ wrappers.proto empty.proto timestamp.proto + +echo "Done!" diff --git a/gRPC-on-ESP32/install_nanopb.sh b/gRPC-on-ESP32/install_nanopb.sh new file mode 100755 index 0000000..8629859 --- /dev/null +++ b/gRPC-on-ESP32/install_nanopb.sh @@ -0,0 +1,58 @@ +################################################################################# +# Copyright (c) 2024 Contributors to the Eclipse Foundation +# +# See the NOTICE file(s) distributed with this work for additional +# information regarding copyright ownership. +# +# This program and the accompanying materials are made available under the +# terms of the Apache License 2.0 which is available at +# http://www.apache.org/licenses/LICENSE-2.0 +# +# SPDX-License-Identifier: Apache-2.0 +################################################################################# + +#!/bin/bash + +URL="https://jpa.kapsi.fi/nanopb/download/nanopb-0.4.8-linux-x86.tar.gz" +TARGET_DIR="components" +FINAL_NAME="nanopb" +CMAKELISTS="$TARGET_DIR/nanopb/CMakeLists.txt" + +# Download and extract the main package +curl -o "nanopb-0.4.8-linux-x86.tar.gz" $URL +mkdir -p "$TARGET_DIR" +tar -xzf "nanopb-0.4.8-linux-x86.tar.gz" -C "$TARGET_DIR" +mv "$TARGET_DIR/nanopb-0.4.8-linux-x86" "$TARGET_DIR/$FINAL_NAME" +rm "nanopb-0.4.8-linux-x86.tar.gz" + + +echo "Downloading timestamp protobuf definitions." +mkdir -p "$TARGET_DIR/nanopb/google/protobuf/" +PROTO_FILES=( + "https://raw.githubusercontent.com/protocolbuffers/protobuf/main/src/google/protobuf/wrappers.proto" + "https://raw.githubusercontent.com/protocolbuffers/protobuf/main/src/google/protobuf/empty.proto" + "https://raw.githubusercontent.com/protocolbuffers/protobuf/main/src/google/protobuf/timestamp.proto" +) + +for URL in "${PROTO_FILES[@]}"; do + FILE_NAME=$(basename $URL) + curl -o "proto/$FILE_NAME" $URL +done + +# Define CMake configuration and overwrite +CMAKE_CONFIG='idf_component_register(SRCS + "pb_common.c" + "pb_encode.c" + "pb_decode.c" + "google/protobuf/empty.pb.c" + "google/protobuf/timestamp.pb.c" + "google/protobuf/wrappers.pb.c" + INCLUDE_DIRS . + )' + +# Write the configuration to CMakeLists.txt +echo "$CMAKE_CONFIG" > "$CMAKELISTS" + +echo "CMakeLists.txt has been updated." + +echo "Installation of nanopb is complete." diff --git a/gRPC-on-ESP32/main/CMakeLists.txt b/gRPC-on-ESP32/main/CMakeLists.txt new file mode 100644 index 0000000..6a9b6f9 --- /dev/null +++ b/gRPC-on-ESP32/main/CMakeLists.txt @@ -0,0 +1,22 @@ +set ( + srcs + "main.c" + "grpc.c" + "encoder.c" + "decoder.c" + "generated/val.pb.c" + "generated/types.pb.c" +) + +set( + include_dirs + "." + "generated/" +) + +idf_component_register( + SRCS "${srcs}" + INCLUDE_DIRS "${include_dirs}"#EMBED_TXTFILES ../certs/server_cert.pem +) + +target_compile_options(${COMPONENT_LIB} PRIVATE "-Wno-format") diff --git a/gRPC-on-ESP32/main/decoder.c b/gRPC-on-ESP32/main/decoder.c new file mode 100644 index 0000000..5ff4d55 --- /dev/null +++ b/gRPC-on-ESP32/main/decoder.c @@ -0,0 +1,135 @@ +// ################################################################################# +// # Copyright (c) 2024 Contributors to the Eclipse Foundation +// # +// # See the NOTICE file(s) distributed with this work for additional +// # information regarding copyright ownership. +// # +// # This program and the accompanying materials are made available under the +// # terms of the Apache License 2.0 which is available at +// # http://www.apache.org/licenses/LICENSE-2.0 +// # +// # SPDX-License-Identifier: Apache-2.0 +// ################################################################################# + +#include +#include +#include +#include +#include +#include "generated/val.pb.h" +#include "generated/types.pb.h" +#include "esp_log.h" + +static const char *TAG = "DECODER"; + +void print_datapoint(const kuksa_val_v1_Datapoint *datapoint) +{ + + switch (datapoint->which_value) + { + case 11: + printf("String value: %s\n", (char *)(datapoint->value.string.arg)); + break; + case 12: + printf("Boolean value: %s\n", datapoint->value._bool ? "true" : "false"); + break; + case 13: + printf("Int32 value: %d\n", datapoint->value.int32); + break; + case 14: + printf("Int64 value: %lld\n", datapoint->value.int64); + break; + case 15: + printf("Uint32 value: %u\n", datapoint->value.uint32); + break; + case 16: + printf("Uint64 value: %llu\n", datapoint->value.uint64); + break; + case 17: + printf("Float value: %f\n", datapoint->value._float); + break; + case 18: // double + printf("Double value: %f\n", datapoint->value._double); + break; + // TODO: implement functions to print the array data format + default: + printf("Unknown or uninitialized value type.\n"); + break; + } +} + +// Callback for encoding/decoding a dynamically allocated string +bool print_string(pb_istream_t *stream, const pb_field_t *field, void **arg) +{ + uint8_t buffer[1024] = {0}; + + /* We could read block-by-block to avoid the large buffer... */ + if (stream->bytes_left > sizeof(buffer) - 1) + return false; + + if (!pb_read(stream, buffer, stream->bytes_left)) + return false; + + printf((char *)*arg, buffer); + return true; +} + +bool decode_DataEntry(pb_istream_t *stream, const pb_field_t *fields, void **arg) +{ + if (arg == NULL) + { + ESP_LOGE(TAG, "Error: 'arg' pointer is NULL."); + return false; + } + + kuksa_val_v1_DataEntry *entry = *arg; + + if (entry == NULL) + { + ESP_LOGE(TAG, "Error: 'entry' pointer is NULL."); + return false; + } + + ESP_LOGI(TAG, "decoding data entry"); + if (!pb_decode(stream, kuksa_val_v1_DataEntry_fields, entry)) + { + ESP_LOGE(TAG, "Failed to decode entry."); + return false; + } + + if (entry->has_value) + { + print_datapoint(&entry->value); + } + + return true; +} + +bool decode_GetResponse(const uint8_t *buffer, size_t buffer_len) +{ + pb_istream_t stream = pb_istream_from_buffer(buffer, buffer_len); + kuksa_val_v1_GetResponse response = kuksa_val_v1_GetResponse_init_zero; + kuksa_val_v1_DataEntry entry = {}; + + entry.path.funcs.decode = &print_string; + entry.path.arg = "Path: \"%s\" \n"; + + // Set up a decode callback for the repeated DataEntry field + response.entries.funcs.decode = &decode_DataEntry; + response.entries.arg = &entry; + + // Decode the message + if (!pb_decode(&stream, kuksa_val_v1_GetResponse_fields, &response)) + { + ESP_LOGE(TAG, "Failed to decode GetResponse"); + + return false; + } + + if (response.has_error) + { + ESP_LOGE(TAG, "Global Error Code: %u, Reason: %s", response.error.code, response.error.reason); + } + + return true; +} diff --git a/gRPC-on-ESP32/main/decoder.h b/gRPC-on-ESP32/main/decoder.h new file mode 100644 index 0000000..585d3b8 --- /dev/null +++ b/gRPC-on-ESP32/main/decoder.h @@ -0,0 +1,31 @@ +// ################################################################################# +// # Copyright (c) 2024 Contributors to the Eclipse Foundation +// # +// # See the NOTICE file(s) distributed with this work for additional +// # information regarding copyright ownership. +// # +// # This program and the accompanying materials are made available under the +// # terms of the Apache License 2.0 which is available at +// # http://www.apache.org/licenses/LICENSE-2.0 +// # +// # SPDX-License-Identifier: Apache-2.0 +// ################################################################################# + +#pragma once + +#include +#include +#include +#include +#include "generated/val.pb.h" +#include "generated/types.pb.h" + +bool decode_GetResponse(const uint8_t *buffer, size_t buffer_len); + +void decode_DataEntry(pb_istream_t *stream, const pb_field_t fields[], void *const *arg); + +void decode_Datapoint(pb_istream_t *stream, const pb_field_t fields[], void *const *arg); + +void decode_Timestamp(pb_istream_t *stream, const pb_field_t fields[], void *const *arg); + +void handle_GetResponse_Error(pb_istream_t *stream, const pb_field_t fields[], void *const *arg); diff --git a/gRPC-on-ESP32/main/encoder.c b/gRPC-on-ESP32/main/encoder.c new file mode 100644 index 0000000..94bfa7c --- /dev/null +++ b/gRPC-on-ESP32/main/encoder.c @@ -0,0 +1,198 @@ +// ################################################################################# +// # Copyright (c) 2024 Contributors to the Eclipse Foundation +// # +// # See the NOTICE file(s) distributed with this work for additional +// # information regarding copyright ownership. +// # +// # This program and the accompanying materials are made available under the +// # terms of the Apache License 2.0 which is available at +// # http://www.apache.org/licenses/LICENSE-2.0 +// # +// # SPDX-License-Identifier: Apache-2.0 +// ################################################################################# + +#include +#include "esp_log.h" +#include +#include +#include "generated/val.pb.h" +#include "generated/types.pb.h" +#include "encoder.h" + +static const char *TAG = "ENCODER"; + +void log_buffer_content(const uint8_t *buffer, size_t length) +{ + printf("Encoded Buffer: "); + for (size_t i = 0; i < length; i++) + { + printf("%02X ", buffer[i]); + } + printf("\n"); +} + +bool get_server_info(uint8_t *buffer, size_t buffer_size, size_t *message_length) +{ + bool status; + + kuksa_val_v1_GetServerInfoRequest get_server_info_request = kuksa_val_v1_GetServerInfoRequest_init_zero; + + pb_ostream_t stream = pb_ostream_from_buffer(buffer, buffer_size); + + status = pb_encode(&stream, kuksa_val_v1_GetServerInfoRequest_fields, &get_server_info_request); + *message_length = stream.bytes_written; + + if (!status) + { + printf("Encoding failed: %s\n", PB_GET_ERROR(&stream)); + return false; + } + + return status; +} + +bool encode_fields_array(pb_ostream_t *stream, const pb_field_t *field, void *const *arg) +{ + uint32_t *fields = (uint32_t *)*arg; + size_t count = 1; // Change this as necessary to match the number of fields you are encoding + + for (size_t i = 0; i < count; i++) + { + if (!pb_encode_tag_for_field(stream, field)) + { + return false; + } + if (!pb_encode_varint(stream, fields[i])) + { + return false; + } + } + return true; +} + +bool encode_entries_callback(pb_ostream_t *stream, const pb_field_t *field, void *const *arg) +{ + kuksa_val_v1_EntryRequest *request = (kuksa_val_v1_EntryRequest *)*arg; + if (!pb_encode_tag_for_field(stream, field)) + return false; + if (!pb_encode_submessage(stream, kuksa_val_v1_EntryRequest_fields, request)) + return false; + return true; +} + +bool encode_entry_request_callback(pb_ostream_t *stream, const pb_field_t *field, void *const *arg) +{ + kuksa_val_v1_EntryRequest *entry_requests = *arg; + int num_entries = 1; // Example number of entries + + for (int i = 0; i < num_entries; i++) + { + if (!pb_encode_tag_for_field(stream, field)) + return false; + if (!pb_encode_submessage(stream, kuksa_val_v1_EntryRequest_fields, &entry_requests[i])) + return false; + } + return true; +} + +bool callback_string_encoder(pb_ostream_t *stream, const pb_field_t *field, void *const *arg) +{ + const char *str = (char *)*arg; + if (!pb_encode_tag_for_field(stream, field)) + { + return false; + } + return pb_encode_string(stream, (const uint8_t *)str, strlen(str)); +} + +kuksa_val_v1_EntryRequest init_entry_request(EntryRequest *req) +{ + kuksa_val_v1_EntryRequest request = kuksa_val_v1_EntryRequest_init_zero; + + // Properly initialize 'path' + request.path.funcs.encode = callback_string_encoder; + request.path.arg = strdup(req->path); // Allocate and set path + if (!request.path.arg) + { + ESP_LOGE(TAG, "Failed to allocate memory for path"); + abort(); + } + + request.view = req->view; + + // TODO: Set up fields values + + return request; +} + +// Set requests +// ------------------------------------------------------------------------------------------ + +bool encode_updates_array(pb_ostream_t *stream, const pb_field_t *field, void *const *arg) +{ + kuksa_val_v1_EntryUpdate *updates = (kuksa_val_v1_EntryUpdate *)*arg; + for (size_t i = 0; i < 1; i++) + { // Change 1 to the number of updates if dynamic + if (!pb_encode_tag_for_field(stream, field)) + { + return false; + } + if (!pb_encode_submessage(stream, kuksa_val_v1_EntryUpdate_fields, &updates[i])) + { + return false; + } + } + return true; +} + +kuksa_val_v1_Datapoint create_datapoint(float value) +{ + kuksa_val_v1_Datapoint datapoint = kuksa_val_v1_Datapoint_init_zero; + datapoint.which_value = kuksa_val_v1_Datapoint__float_tag; + datapoint.value._float = value; + return datapoint; +} + +kuksa_val_v1_DataEntry create_data_entry(const char *path, kuksa_val_v1_Datapoint datapoint) +{ + kuksa_val_v1_DataEntry data_entry = kuksa_val_v1_DataEntry_init_zero; + data_entry.path.funcs.encode = callback_string_encoder; + data_entry.path.arg = (void *)path; + data_entry.has_value = true; + data_entry.value = datapoint; + return data_entry; +} + +kuksa_val_v1_EntryUpdate create_entry_update(kuksa_val_v1_DataEntry data_entry) +{ + kuksa_val_v1_EntryUpdate entry_update = kuksa_val_v1_EntryUpdate_init_zero; + entry_update.has_entry = true; + entry_update.entry = data_entry; + + static uint32_t fields_array[] = {2}; // Assuming FIELD_VALUE is '2' -> This translates to current_value + entry_update.fields.funcs.encode = encode_fields_array; + entry_update.fields.arg = fields_array; + + return entry_update; +} + +kuksa_val_v1_SetRequest create_set_request(kuksa_val_v1_EntryUpdate *updates) +{ + kuksa_val_v1_SetRequest set_request = kuksa_val_v1_SetRequest_init_zero; + set_request.updates.funcs.encode = encode_updates_array; + set_request.updates.arg = updates; + return set_request; +} + +bool encode_set_request(const kuksa_val_v1_SetRequest *set_request, uint8_t *buffer, size_t buffer_size, size_t *message_length) +{ + pb_ostream_t stream = pb_ostream_from_buffer(buffer, buffer_size); + bool status = pb_encode(&stream, kuksa_val_v1_SetRequest_fields, set_request); + if (!status) + { + printf("Encoding failed: %s\n", PB_GET_ERROR(&stream)); + return false; + } + *message_length = stream.bytes_written; + return true; +} diff --git a/gRPC-on-ESP32/main/encoder.h b/gRPC-on-ESP32/main/encoder.h new file mode 100644 index 0000000..92f1c6f --- /dev/null +++ b/gRPC-on-ESP32/main/encoder.h @@ -0,0 +1,45 @@ +// ################################################################################# +// # Copyright (c) 2024 Contributors to the Eclipse Foundation +// # +// # See the NOTICE file(s) distributed with this work for additional +// # information regarding copyright ownership. +// # +// # This program and the accompanying materials are made available under the +// # terms of the Apache License 2.0 which is available at +// # http://www.apache.org/licenses/LICENSE-2.0 +// # +// # SPDX-License-Identifier: Apache-2.0 +// ################################################################################# + +#pragma once + +#include +#include +#include +#include +#include +#include "generated/val.pb.h" +#include "generated/types.pb.h" + +typedef struct +{ + char *path; + kuksa_val_v1_View view; + // kuksa_val_v1_Field *field; +} EntryRequest; + +void log_buffer_content(const uint8_t *buffer, size_t length); +bool encode_string(pb_ostream_t *stream, const pb_field_t *field, void *const *arg); +bool get_server_info(uint8_t *buffer, size_t buffer_size, size_t *message_length); +bool kuksa_get_request(uint8_t *buffer, size_t buffer_size, size_t *message_length); +bool should_serialize_field(const kuksa_val_v1_EntryRequest *request, kuksa_val_v1_Field field); +bool encode_get_request(kuksa_val_v1_GetRequest *request, uint8_t *buffer, size_t buffer_size, size_t *message_length); +kuksa_val_v1_EntryRequest init_entry_request(EntryRequest *req); +bool callback_string_encoder(pb_ostream_t *stream, const pb_field_t *field, void *const *arg); +bool encode_entries_callback(pb_ostream_t *stream, const pb_field_t *field, void *const *arg); + +kuksa_val_v1_Datapoint create_datapoint(float value); +kuksa_val_v1_DataEntry create_data_entry(const char *path, kuksa_val_v1_Datapoint datapoint); +kuksa_val_v1_EntryUpdate create_entry_update(kuksa_val_v1_DataEntry data_entry); +kuksa_val_v1_SetRequest create_set_request(kuksa_val_v1_EntryUpdate *updates); +bool encode_set_request(const kuksa_val_v1_SetRequest *set_request, uint8_t *buffer, size_t buffer_size, size_t *message_length); diff --git a/gRPC-on-ESP32/main/generated/.gitkeep b/gRPC-on-ESP32/main/generated/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/gRPC-on-ESP32/main/grpc.c b/gRPC-on-ESP32/main/grpc.c new file mode 100644 index 0000000..bd44e24 --- /dev/null +++ b/gRPC-on-ESP32/main/grpc.c @@ -0,0 +1,801 @@ +// ################################################################################# +// # Copyright (c) 2024 Contributors to the Eclipse Foundation +// # +// # See the NOTICE file(s) distributed with this work for additional +// # information regarding copyright ownership. +// # +// # This program and the accompanying materials are made available under the +// # terms of the Apache License 2.0 which is available at +// # http://www.apache.org/licenses/LICENSE-2.0 +// # +// # SPDX-License-Identifier: Apache-2.0 +// ################################################################################# + +#include "grpc.h" +#include +#include "esp_random.h" +#include "esp_tls.h" +#include +#include +#include +#include "esp_log.h" + +#if CONFIG_MBEDTLS_CERTIFICATE_BUNDLE +#include "esp_crt_bundle.h" +#endif + +// ==================================================================== +// DEFINES & MACROS +// ==================================================================== +static const char *TAG = "GRPC"; +#define GRPC_DEBUG 1 + +#define MAKE_NV(NAME, VALUE) \ + { \ + (uint8_t *)NAME, (uint8_t *)VALUE, strlen(NAME), strlen(VALUE), \ + NGHTTP2_NV_FLAG_NONE \ + } + +/** Flag indicating receive stream is reset */ +#define DATA_RECV_RST_STREAM 1 +/** Flag indicating frame is completely received */ +#define DATA_RECV_FRAME_COMPLETE 2 + +#define MESSAGE_PREFIX_SIZE 5 +#define MAX_GRPC_BUFFER_SIZE 1024 + +// ==================================================================== +// TYPES +// ==================================================================== + +typedef int (*_frame_data_recv_cb_t)(const char *data, size_t len, int flags); +typedef int (*_putpost_data_cb_t)(char *data, size_t len, uint32_t *data_flags); + +typedef struct +{ + nghttp2_session *http2_sess; /*!< Pointer to the HTTP2 session handle */ + char *hostname; /*!< The hostname we are connected to */ + struct esp_tls *http2_tls; /*!< Pointer to the TLS session handle */ +} grpc_handle_t; + +typedef struct +{ + uint8_t buf[MAX_GRPC_BUFFER_SIZE]; + size_t len; +} GRPCBuffer; + +// ==================================================================== +// STATIC VARS +// ==================================================================== + +static TaskHandle_t http2_task_handle = NULL; +static TaskHandle_t grpc_task_handle = NULL; + +static grpc_conn_data_t conn_data = {0}; +static grpc_init_t cfg = {0}; +static grpc_handle_t hd = {0}; + +static struct +{ + bool initialized : 1; + bool conn_configured : 1; + + bool trigger_connect : 1; // also SHOULD connect + bool connected : 1; + + bool message_pending : 1; + bool ping_pending : 1; + +} bools; + +static int64_t ping_recv_time = 0; // recv time +static uint8_t ping_data[8] = {0}; + +static GRPCBuffer buffer_recv; +static GRPCBuffer buffer_send; + +// ==================================================================== +// STATIC PROTOS +// ==================================================================== + +static bool grpc_task_start_connection(); +static void grpc_task(); + +static void handle_disconnect(); +static void http2_task(); +static bool http2_execute(); + +static bool _connect(); + +static void free_handle(); +static int send_post_data(char *buf, size_t length, uint32_t *data_flags); +static ssize_t _data_provider_cb(nghttp2_session *session, int32_t stream_id, uint8_t *buf, size_t length, uint32_t *data_flags, nghttp2_data_source *source, void *user_data); + +static ssize_t callback_send(nghttp2_session *session, const uint8_t *data, size_t length, int flags, void *user_data); +static ssize_t callback_recv(nghttp2_session *session, uint8_t *buf, size_t length, int flags, void *user_data); +static int callback_on_frame_send(nghttp2_session *session, const nghttp2_frame *frame, void *user_data); +static int callback_on_frame_recv(nghttp2_session *session, const nghttp2_frame *frame, void *user_data); +static int callback_on_stream_close(nghttp2_session *session, int32_t stream_id, uint32_t error_code, void *user_data); +static int callback_on_data_chunk_recv(nghttp2_session *session, uint8_t flags, int32_t stream_id, const uint8_t *data, size_t len, void *user_data); +static int callback_on_header(nghttp2_session *session, const nghttp2_frame *frame, const uint8_t *name, size_t namelen, const uint8_t *value, size_t valuelen, uint8_t flags, void *user_data); + +static const char *frame_type_to_str(int type); + +// ==================================================================== +// GLOBAL FUNCTIONS +// ==================================================================== + +bool grpc_init(grpc_init_t config) +{ + + int ret = xTaskCreatePinnedToCore(http2_task, "http2_task", config.http2_stack_size, NULL, config.http2_prio, &http2_task_handle, config.http2_core); + ESP_LOGI(TAG, "%d", ret); + if (ret != pdPASS) + { + ESP_LOGE(TAG, "Failed to create http2 task, ret: %d", ret); + return false; + } + + ret = xTaskCreatePinnedToCore(grpc_task, "grpc_task", config.grpc_stack_size, NULL, config.grpc_prio, &grpc_task_handle, config.grpc_core); + if (ret != pdPASS) + { + ESP_LOGE(TAG, "Failed to create GRPC task, ret: %d", ret); + return false; + } + ESP_LOGI(TAG, "%d", ret); + + memcpy(&cfg, &config, sizeof(grpc_init_t)); + vTaskSuspend(http2_task_handle); + bools.initialized = true; + return true; +} + +bool grpc_configure_connection(grpc_conn_data_t connection_data) +{ + if (!bools.initialized) + return false; + if (bools.conn_configured) + return false; + + memcpy(&conn_data, &connection_data, sizeof(grpc_conn_data_t)); + bools.conn_configured = true; + return true; +} + +bool grpc_connect() +{ + if (!bools.initialized) + return false; + if (!bools.conn_configured) + return false; + if (bools.connected) + return true; + + bools.trigger_connect = true; + return true; +} + +bool grpc_connected() +{ + return bools.connected; +} + +bool grpc_wait_for_connection(int timeout_ms) +{ + for (;;) + { + if (bools.connected) + return true; + if (timeout_ms <= 0) + return bools.connected; + timeout_ms -= 10; + vTaskDelay(10); + } +} + +bool gprc_send_message_pending() +{ + return bools.message_pending; +} + +bool grpc_ping(int timeout_ms, int64_t *_ping_time) +{ + if (!bools.conn_configured) + return false; + if (hd.http2_sess == NULL) + return false; + + esp_fill_random(ping_data, 8); + bools.ping_pending = true; + ESP_LOGI(TAG, "Ping data:"); + + int64_t sent_time = esp_timer_get_time(); + int ret = nghttp2_submit_ping(hd.http2_sess, NGHTTP2_FLAG_NONE, (const uint8_t *)ping_data); + if (ret != 0) + { + ESP_LOGE(TAG, "Submit ping failed, ret: %d (%s)", ret, nghttp2_strerror(ret)); + bools.ping_pending = false; + return false; + } + + for (;;) + { + if (!bools.ping_pending) + break; + if (timeout_ms < 0) + break; + timeout_ms -= 10; + vTaskDelay(10); + } + + if (!bools.ping_pending) + { + if (_ping_time != NULL) + { + *_ping_time = ping_recv_time - sent_time; + } + return true; + } + + return false; +} + +bool grpc_call_proc(char *path, char *proc, uint8_t *data, uint32_t len) +{ + if (hd.http2_sess == NULL) + return false; + if (!bools.connected) + return false; + + int max_len = (MAX_GRPC_BUFFER_SIZE - MESSAGE_PREFIX_SIZE); + if (len >= max_len) + { + ESP_LOGE(TAG, "Specified Length of %d exceeds the maximum allowed data size of %d", len, max_len); + return false; + } + + // printf("bools.message_pending is %s\n", BOOLSTR(bools.message_pending)); + for (;;) + { + // wait for prior message to complete + if (!bools.message_pending) + break; + + vTaskDelay(10); + } + // printf("bools.message_pending = true\n"); + bools.message_pending = true; + + memcpy(buffer_send.buf + MESSAGE_PREFIX_SIZE, data, len); + buffer_send.len = len; + + // Necessary for gRPC Message for DATA frame + // https://github.com/grpc/grpc/blob/master/doc/PROTOCOL-HTTP2.md + uint8_t *ml = (uint8_t *)&buffer_send.len; + buffer_send.buf[0] = 0x0; // no compression + buffer_send.buf[1] = *(ml + 3); // big-endian + buffer_send.buf[2] = *(ml + 2); + buffer_send.buf[3] = *(ml + 1); + buffer_send.buf[4] = *(ml + 0); + + char content_length[8 + 1] = {0}; + snprintf(content_length, 8, "%d", len + MESSAGE_PREFIX_SIZE); + + char full_path[128 + 1] = {0}; + snprintf(full_path, 128, "%s/%s", path, proc); + + const nghttp2_nv nva[] = { + MAKE_NV(":method", "POST"), + MAKE_NV(":scheme", "https"), + MAKE_NV(":authority", hd.hostname), + MAKE_NV(":path", full_path), + MAKE_NV("content-type", "application/grpc+proto"), + MAKE_NV("content-length", content_length), + }; + + nghttp2_data_provider dp; + dp.read_callback = _data_provider_cb; + dp.source.ptr = send_post_data; + + int ret = nghttp2_submit_request(hd.http2_sess, NULL, nva, sizeof(nva) / sizeof(nva[0]), &dp, NULL); + + if (ret < 0) + { + ESP_LOGE(TAG, "Submit Request failed: %d", ret); + bools.message_pending = false; + return false; + } + + return true; +} + +const char *grpc_status_code_to_str(GRPCStatusCode sc) +{ + static const char *sc_str[] = {"OK", "CANCELLED", "UNKNOWN", "INVALID_ARGUMENT", "DEADLINE_EXCEEDED", "NOT_FOUND", "ALREADY_EXISTS", "PERMISSION_DENIED", "RESOURCE_EXHAUSTED", "FAILED_PRECONDITION", "ABORTED", "OUT_OF_RANGE", "UNIMPLEMENTED", "INTERNAL", "UNAVAILABLE", "DATA_LOSS", "UNAUTHENTICATED"}; + int num_sc = sizeof(sc_str) / sizeof(sc_str[0]); + if (sc < 0 || sc >= num_sc) + return "UNKNOWN"; + return sc_str[sc]; +} + +// ==================================================================== +// STATIC FUNCTIONS +// ==================================================================== + +static bool grpc_task_start_connection() +{ + if (!bools.conn_configured) + { + return false; + } + + if (bools.connected) + { + ESP_LOGW(TAG, "you shouldn't be here"); + return false; + } + + // if(!wifi_get_internet_status()) + // { + // return false; + // } + + if (bools.trigger_connect) + { + bool success = _connect(); + return success; + } + + return false; +} + +static void grpc_task() +{ + ESP_LOGI(TAG, "Entered GRPC task"); + + for (;;) + { + bool connected = grpc_task_start_connection(); + + if (!connected) + { + vTaskDelay(1000); + continue; + } + + for (;;) + { + + if (!bools.connected) + break; + if (hd.http2_sess == NULL) + break; + + // TODO: process data + + vTaskDelay(100); + } + + vTaskSuspend(http2_task_handle); + } + + vTaskDelete(NULL); +} + +static void handle_disconnect() +{ + + if (bools.connected) + { + ESP_LOGW(TAG, "DISCONNECTED"); + } + + bools.connected = false; + free_handle(); // not safe here +} + +static void http2_task() +{ + ESP_LOGI(TAG, "Entered http2 task"); + + for (;;) + { + if (hd.http2_sess != NULL) + { + if (!http2_execute()) + { + ESP_LOGE(TAG, "Error in send/receive"); + handle_disconnect(); + } + vTaskDelay(10); + } + else + { + vTaskDelay(100); + } + } + + vTaskDelete(NULL); +} + +static bool http2_execute() +{ + if (hd.http2_sess == NULL) + return false; + int ret = nghttp2_session_send(hd.http2_sess); + if (ret != 0) + { + ESP_LOGE(TAG, "[sh2-execute] HTTP2 session send failed, ret: %d (%s)", ret, nghttp2_strerror(ret)); + return false; + } + + if (hd.http2_sess == NULL) + return false; + ret = nghttp2_session_recv(hd.http2_sess); + if (ret != 0) + { + ESP_LOGE(TAG, "[sh2-execute] HTTP2 session recv failed, ret: %d (%s)", ret, nghttp2_strerror(ret)); + return false; + } + + return true; +} + +static bool _connect() +{ + bools.connected = false; + free_handle(); + memset(&hd, 0, sizeof(grpc_handle_t)); + + const char *proto[] = {"h2", NULL}; + esp_tls_cfg_t tls_cfg = {0}; + tls_cfg.alpn_protos = proto; + // we will include the default cert bundle included by esp-tls + tls_cfg.crt_bundle_attach = esp_crt_bundle_attach; + tls_cfg.non_block = true; + tls_cfg.timeout_ms = 10 * 1000; + + if ((hd.http2_tls = esp_tls_conn_http_new(conn_data.uri, &tls_cfg)) == NULL) + { + ESP_LOGE(TAG, "Failed to initialize TLS."); + goto error; + } + + struct http_parser_url u; + http_parser_url_init(&u); + http_parser_parse_url(conn_data.uri, strlen(conn_data.uri), 0, &u); + hd.hostname = strndup(&conn_data.uri[u.field_data[UF_HOST].off], u.field_data[UF_HOST].len); + + ESP_LOGI(TAG, "Setting up HTTP2 connection with uri: %s", conn_data.uri); + nghttp2_session_callbacks *callbacks; + nghttp2_session_callbacks_new(&callbacks); + nghttp2_session_callbacks_set_send_callback(callbacks, callback_send); + nghttp2_session_callbacks_set_recv_callback(callbacks, callback_recv); + nghttp2_session_callbacks_set_on_frame_send_callback(callbacks, callback_on_frame_send); + nghttp2_session_callbacks_set_on_frame_recv_callback(callbacks, callback_on_frame_recv); + nghttp2_session_callbacks_set_on_stream_close_callback(callbacks, callback_on_stream_close); + nghttp2_session_callbacks_set_on_data_chunk_recv_callback(callbacks, callback_on_data_chunk_recv); + nghttp2_session_callbacks_set_on_header_callback(callbacks, callback_on_header); + + int ret = nghttp2_session_client_new(&hd.http2_sess, callbacks, &hd); + if (ret != 0) + { + ESP_LOGE(TAG, "New http2 session failed, ret: %d (%s)", ret, nghttp2_strerror(ret)); + nghttp2_session_callbacks_del(callbacks); + goto error; + } + nghttp2_session_callbacks_del(callbacks); + + /* Create the SETTINGS frame */ + ret = nghttp2_submit_settings(hd.http2_sess, NGHTTP2_FLAG_NONE, NULL, 0); + if (ret != 0) + { + ESP_LOGE(TAG, "New http2 session failed, ret: %d (%s)", ret, nghttp2_strerror(ret)); + goto error; + } + + vTaskResume(http2_task_handle); + + int timeout_ms = 10000; + for (;;) + { + if (bools.connected) + return true; + + // if(!wifi_get_internet_status()) + // { + // ESP_LOGW("Lost internet connection"); + // goto error; + // } + + vTaskDelay(10); + timeout_ms -= 10; + if (timeout_ms <= 0) + { + goto error; + } + } + + return true; + +error: + free_handle(); + vTaskSuspend(http2_task_handle); + return false; +} + +static void free_handle() +{ + ESP_LOGI(TAG, "Freeing handle"); + + if (hd.http2_sess) + { + nghttp2_session_del(hd.http2_sess); + hd.http2_sess = NULL; + } + + if (hd.http2_tls) + { + // esp_tls_conn_delete(hd.http2_tls); + hd.http2_tls = NULL; + } + + if (hd.hostname) + { + free(hd.hostname); + hd.hostname = NULL; + } +} + +static ssize_t _data_provider_cb(nghttp2_session *session, int32_t stream_id, uint8_t *buf, size_t length, uint32_t *data_flags, nghttp2_data_source *source, void *user_data) +{ + _putpost_data_cb_t data_cb = source->ptr; + return (*data_cb)((char *)buf, length, data_flags); +} + +// callback for sending data +static int send_post_data(char *buf, size_t length, uint32_t *data_flags) +{ + int copylen = buffer_send.len + MESSAGE_PREFIX_SIZE; + if (copylen < length) + { + ESP_LOGI(TAG, "[SEND] Sending %d bytes", copylen); + memcpy(buf, buffer_send.buf, buffer_send.len + MESSAGE_PREFIX_SIZE); + } + else + { + copylen = 0; + } + + // printf("bools.message_pending = false\n"); + bools.message_pending = false; + + (*data_flags) |= NGHTTP2_DATA_FLAG_EOF; + return copylen; +} + +static ssize_t callback_send(nghttp2_session *session, const uint8_t *data, size_t length, int flags, void *user_data) +{ + int copy_offset = 0; + int pending_data = length; + int rv = 0; + + /* Send data in 1000 byte chunks */ + while (copy_offset != length) + { + int chunk_len = pending_data > 1000 ? 1000 : pending_data; + + int subrv = esp_tls_conn_write(hd.http2_tls, data + copy_offset, chunk_len); + + if (subrv <= 0) + { + if (subrv == ESP_TLS_ERR_SSL_WANT_READ || subrv == ESP_TLS_ERR_SSL_WANT_WRITE) + { + subrv = NGHTTP2_ERR_WOULDBLOCK; + } + else + { + subrv = NGHTTP2_ERR_CALLBACK_FAILURE; + } + } + + if (subrv <= 0) + { + if (copy_offset == 0) + { + /* If no data is transferred, send the error code */ + rv = subrv; + } + break; + } + copy_offset += subrv; + pending_data -= subrv; + rv += subrv; + } + return rv; +} + +static ssize_t callback_recv(nghttp2_session *session, uint8_t *buf, size_t length, int flags, void *user_data) +{ + int rv = esp_tls_conn_read(hd.http2_tls, (char *)buf, (int)length); + if (rv < 0) + { + if (rv == ESP_TLS_ERR_SSL_WANT_READ || rv == ESP_TLS_ERR_SSL_WANT_WRITE) + { + rv = NGHTTP2_ERR_WOULDBLOCK; + } + else + { + rv = NGHTTP2_ERR_CALLBACK_FAILURE; + } + } + else if (rv == 0) + { + rv = NGHTTP2_ERR_EOF; + } + return rv; +} + +static int callback_on_frame_send(nghttp2_session *session, const nghttp2_frame *frame, void *user_data) +{ + ESP_LOGD(TAG, "[frame-send] frame type %s", frame_type_to_str(frame->hd.type)); + switch (frame->hd.type) + { + case NGHTTP2_HEADERS: + { + if (nghttp2_session_get_stream_user_data(session, frame->hd.stream_id)) + { + ESP_LOGD(TAG, "[frame-send] C ----------------------------> S (HEADERS)"); + ESP_LOGD(TAG, "[frame-send] headers nv-len = %d", frame->headers.nvlen); + const nghttp2_nv *nva = frame->headers.nva; + for (size_t i = 0; i < frame->headers.nvlen; ++i) + { + ESP_LOGD(TAG, "[frame-send] %s : %s", nva[i].name, nva[i].value); + } + } + } + break; + + case NGHTTP2_PING: + { + } + break; + } + return 0; +} + +static int callback_on_frame_recv(nghttp2_session *session, const nghttp2_frame *frame, void *user_data) +{ + int64_t t = esp_timer_get_time(); + + ESP_LOGD(TAG, "[frame-recv][sid: %d] frame type: %d (%s)", frame->hd.stream_id, frame->hd.type, frame_type_to_str(frame->hd.type)); + + switch (frame->hd.type) + { + case NGHTTP2_SETTINGS: + { + // const nghttp2_settings* p = &frame->settings; + if (!bools.connected) + { + ESP_LOGI(TAG, "CONNECTED"); + bools.connected = true; + } + } + break; + + case NGHTTP2_GOAWAY: + { + // const nghttp2_goaway* p = &frame->goaway; + handle_disconnect(); + } + break; + + case NGHTTP2_PING: + { + const nghttp2_ping *p = &frame->ping; + + if (bools.ping_pending) + { + // LOGI_HEX(p->opaque_data, 8); + bool match = memcmp(p->opaque_data, ping_data, 8) == 0; + if (match) + { + bools.ping_pending = false; + ping_recv_time = t; + } + } + } + break; + + case NGHTTP2_WINDOW_UPDATE: + { + const nghttp2_window_update *w = &frame->window_update; + + ESP_LOGI(TAG, "[Window Size] %d", w->window_size_increment); + } + break; + + default: + break; + } + + return 0; +} + +static int callback_on_stream_close(nghttp2_session *session, int32_t stream_id, uint32_t error_code, void *user_data) +{ + ESP_LOGD(TAG, "[stream-close][sid %d]", stream_id); + ESP_LOGI(TAG, "[RECV] Stream Closed"); + return 0; +} + +static int callback_on_data_chunk_recv(nghttp2_session *session, uint8_t flags, int32_t stream_id, const uint8_t *data, size_t len, void *user_data) +{ + ESP_LOGD(TAG, "[data-chunk-recv][sid:%d] %lu bytes", stream_id, (unsigned long int)len); + + if (len) + { + + buffer_recv.len = len - MESSAGE_PREFIX_SIZE; + memcpy(buffer_recv.buf, data + MESSAGE_PREFIX_SIZE, buffer_recv.len); + + ESP_LOGI(TAG, "Received data chunk with following lenght: %d", buffer_recv.len); + ESP_LOGI(TAG, "-----------------------------------------"); + + for (size_t i = 0; i < buffer_recv.len; ++i) + { + printf("%02X ", buffer_recv.buf[i]); + } + printf("\n"); + ESP_LOGI(TAG, "-----------------------------------------"); + } + return 0; +} + +static int callback_on_header(nghttp2_session *session, const nghttp2_frame *frame, const uint8_t *name, size_t namelen, const uint8_t *value, size_t valuelen, uint8_t flags, void *user_data) +{ + ESP_LOGD(TAG, "[hdr-recv][sid:%d] %s : %s", frame->hd.stream_id, name, value); + + if (strcmp((char *)name, "grpc-status") == 0) + { + int sc = atoi((const char *)value); + ESP_LOGI(TAG, "[hdr-recv] GRPC Status: %s (%d)", grpc_status_code_to_str((GRPCStatusCode)sc), sc); + } + + return 0; +} + +static const char *frame_type_to_str(int type) +{ + switch (type) + { + case NGHTTP2_HEADERS: + return "HEADERS"; + case NGHTTP2_RST_STREAM: + return "RST_STREAM"; + case NGHTTP2_GOAWAY: + return "GOAWAY"; + case NGHTTP2_DATA: + return "DATA"; + case NGHTTP2_SETTINGS: + return "SETTINGS"; + case NGHTTP2_PUSH_PROMISE: + return "PUSH_PROMISE"; + case NGHTTP2_PING: + return "PING"; + case NGHTTP2_WINDOW_UPDATE: + return "WINDOW_UPDATE"; + default: + return "other"; + } +} + +uint8_t *grpc_get_buffer_data() +{ + return buffer_recv.buf; +} + +size_t grpc_get_buffer_length() +{ + return buffer_recv.len; +} diff --git a/gRPC-on-ESP32/main/grpc.h b/gRPC-on-ESP32/main/grpc.h new file mode 100644 index 0000000..b0b67e8 --- /dev/null +++ b/gRPC-on-ESP32/main/grpc.h @@ -0,0 +1,70 @@ +// ################################################################################# +// # Copyright (c) 2024 Contributors to the Eclipse Foundation +// # +// # See the NOTICE file(s) distributed with this work for additional +// # information regarding copyright ownership. +// # +// # This program and the accompanying materials are made available under the +// # terms of the Apache License 2.0 which is available at +// # http://www.apache.org/licenses/LICENSE-2.0 +// # +// # SPDX-License-Identifier: Apache-2.0 +// ################################################################################# + +#pragma once +#include +#include +#include + +typedef struct +{ + int grpc_core; + int grpc_stack_size; + int grpc_prio; + + int http2_core; + int http2_stack_size; + int http2_prio; +} grpc_init_t; + +typedef struct +{ + const char *ca; + const char *uri; +} grpc_conn_data_t; + +// https://grpc.github.io/grpc/core/md_doc_statuscodes.html +typedef enum +{ + GRPC_SC_OK, + GRPC_SC_CANCELLED, + GRPC_SC_UNKNOWN, + GRPC_SC_INVALID_ARGUMENT, + GRPC_SC_DEADLINE_EXCEEDED, + GRPC_SC_NOT_FOUND, + GRPC_SC_ALREADY_EXISTS, + GRPC_SC_PERMISSION_DENIED, + GRPC_SC_RESOURCE_EXHAUSTED, + GRPC_SC_FAILED_PRECONDITION, + GRPC_SC_ABORTED, + GRPC_SC_OUT_OF_RANGE, + GRPC_SC_UNIMPLEMENTED, + GRPC_SC_INTERNAL, + GRPC_SC_UNAVAILABLE, + GRPC_SC_DATA_LOSS, + GRPC_SC_UNAUTHENTICATED +} GRPCStatusCode; + +bool grpc_init(grpc_init_t config); +bool grpc_configure_connection(grpc_conn_data_t connection_data); + +bool grpc_connect(); +bool grpc_connected(); +bool grpc_wait_for_connection(int timeout_ms); +bool gprc_send_message_pending(); +bool grpc_call_proc(char *path, char *proc, uint8_t *data, uint32_t len); +bool grpc_ping(int timeout_ms, int64_t *_ping_time); +const char *grpc_status_code_to_str(GRPCStatusCode sc); + +uint8_t *grpc_get_buffer_data(void); +size_t grpc_get_buffer_length(void); diff --git a/gRPC-on-ESP32/main/idf_component.yml b/gRPC-on-ESP32/main/idf_component.yml new file mode 100644 index 0000000..3bc0fbf --- /dev/null +++ b/gRPC-on-ESP32/main/idf_component.yml @@ -0,0 +1,6 @@ +dependencies: + espressif/nghttp: "^1.58.0" + espressif/sh2lib: + version: ^1.0.0 +description: HTTP2 Request Examples +version: 1.0.0 diff --git a/gRPC-on-ESP32/main/main.c b/gRPC-on-ESP32/main/main.c new file mode 100644 index 0000000..a763d44 --- /dev/null +++ b/gRPC-on-ESP32/main/main.c @@ -0,0 +1,196 @@ +// ################################################################################# +// # Copyright (c) 2024 Contributors to the Eclipse Foundation +// # +// # See the NOTICE file(s) distributed with this work for additional +// # information regarding copyright ownership. +// # +// # This program and the accompanying materials are made available under the +// # terms of the Apache License 2.0 which is available at +// # http://www.apache.org/licenses/LICENSE-2.0 +// # +// # SPDX-License-Identifier: Apache-2.0 +// ################################################################################# + +#include "common.h" +#include "grpc.h" +#include "nvs_flash.h" +#include "protocol_examples_common.h" +#include "esp_netif.h" +#include "esp_wifi.h" +#include "esp_event.h" +#include +#include +#include +#include "generated/val.pb.h" +#include "generated/types.pb.h" +#include "esp_log.h" +#include "encoder.h" +#include "decoder.h" + +// ------------------------------------------------------------------------------------------ +static const char *TAG = "MAIN"; +bool session_test = false; +const char *grpc_uri = "https://"; + +// ------------------------------------------------------------------------------------------ + +// Get Reqeust +// ------------------ +#define MESSAGEPB_PATH "/kuksa.val.v1.VAL" +#define MESSAGEPB_REQUEST "Get" +// ------------------ + +// Set Reqeust +// ------------------ +// #define MESSAGEPB_PATH "/kuksa.val.v1.VAL" +// #define MESSAGEPB_REQUEST "Set" +// ------------------ + +void app_main() +{ + + ESP_ERROR_CHECK(nvs_flash_init()); + ESP_ERROR_CHECK(esp_netif_init()); + ESP_ERROR_CHECK(esp_event_loop_create_default()); + + /* This helper function configures Wi-Fi or Ethernet, as selected in menuconfig. + * Read "Establishing Wi-Fi or Ethernet Connection" section in + * examples/protocols/README.md for more information about this function. + */ + ESP_ERROR_CHECK(example_connect()); + + ESP_LOGI(TAG, "Initializing the gRPC connection..."); + + grpc_init_t grpc_cfg = { + .grpc_core = 1, + .grpc_stack_size = 8000, + .grpc_prio = 10, + .http2_core = 1, + .http2_stack_size = 22000, + .http2_prio = 11, + }; + + grpc_init(grpc_cfg); + ESP_LOGI(TAG, "completed the configuration"); + + grpc_conn_data_t grpc_dat = { + .ca = "", + .uri = grpc_uri, + }; + + ESP_LOGD(TAG, "conn data: %s", grpc_dat.uri); + + grpc_configure_connection(grpc_dat); + + grpc_connect(); + + // @TEST: GRPC + for (;;) + { + static bool pinged = false; + static bool conn_prior = false; + + bool conn = grpc_connected(); + + if (conn && !conn_prior) + { + pinged = true; + session_test = false; + } + + if (conn) + { + if (!pinged) + { + int64_t rtt = 0; + bool ret = grpc_ping(1000, &rtt); + if (ret) + { + pinged = true; + int rtt_ms = rtt / 1000; + ESP_LOGI(TAG, "ping time: %d", rtt_ms); + } + } + + if (!session_test) + { + + // ------------------------------------------------------------------------------------------------------- + // See get request example below: + // ------------------------------------------------------------------------------------------------------- + + uint8_t buffer[256]; + size_t message_length; + + EntryRequest message = { + .path = "Vehicle.Speed", + .view = kuksa_val_v1_View_VIEW_CURRENT_VALUE, + }; + + kuksa_val_v1_EntryRequest entry_request = init_entry_request(&message); + + kuksa_val_v1_GetRequest get_request; + get_request.entries.funcs.encode = encode_entries_callback; + get_request.entries.arg = &entry_request; + + pb_ostream_t stream = pb_ostream_from_buffer(buffer, sizeof(buffer)); + + // Encode the GetRequest + if (!pb_encode(&stream, kuksa_val_v1_GetRequest_fields, &get_request)) + { + ESP_LOGE(TAG, "Encoding failed: %s", PB_GET_ERROR(&stream)); + free(entry_request.path.arg); + return; + } + + message_length = stream.bytes_written; + ESP_LOGI(TAG, "Encoded GetRequest with length %zu bytes\n", message_length); + + free(entry_request.path.arg); + + grpc_call_proc(MESSAGEPB_PATH, MESSAGEPB_REQUEST, buffer, message_length); + + vTaskDelay(2000); + + uint8_t *resp_buf = grpc_get_buffer_data(); // Buffer for response + size_t resp_buf_len = grpc_get_buffer_length(); // Populate with actual response length + + if (decode_GetResponse(resp_buf, resp_buf_len)) + { + ESP_LOGI(TAG, "Decoding was successful"); + } + else + { + ESP_LOGE(TAG, "Decoding failed"); + } + + // ------------------------------------------------------------------------------------------------------- + // See set request example below: + // ------------------------------------------------------------------------------------------------------- + + // uint8_t buffer[128]; // Buffer to hold the encoded data + // size_t message_length; // Variable to store the message length after encoding + // kuksa_val_v1_Datapoint datapoint = create_datapoint(62.0); + // kuksa_val_v1_DataEntry data_entry = create_data_entry("Vehicle.Speed", datapoint); + // kuksa_val_v1_EntryUpdate entry_update = create_entry_update(data_entry); + // kuksa_val_v1_EntryUpdate updates[] = {entry_update}; + + // kuksa_val_v1_SetRequest set_request = create_set_request(updates); + + // if (encode_set_request(&set_request, buffer, sizeof(buffer), &message_length)) { + // printf("SetRequest encoded successfully, length = %zu\n", message_length); + // } + + // log_buffer_content(buffer, message_length); + + // grpc_call_proc(MESSAGEPB_PATH,MESSAGEPB_REQUEST, buffer, message_length); + + session_test = true; + conn_prior = conn; + } + } + + conn_prior = conn; + vTaskDelay(100); + } +} diff --git a/gRPC-on-ESP32/proto/types.proto b/gRPC-on-ESP32/proto/types.proto new file mode 100644 index 0000000..26122b5 --- /dev/null +++ b/gRPC-on-ESP32/proto/types.proto @@ -0,0 +1,288 @@ +/******************************************************************************** + * Copyright (c) 2022 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Apache License 2.0 which is available at + * http://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + ********************************************************************************/ + +syntax = "proto3"; + +// I added V1 as in databroker. Is this good practice? +package kuksa.val.v1; +import "google/protobuf/timestamp.proto"; + +option go_package = "kuksa/val/v1"; + +// Describes a VSS entry +// When requesting an entry, the amount of information returned can +// be controlled by specifying either a `View` or a set of `Field`s. +message DataEntry { + // Defines the full VSS path of the entry. + string path = 1; // [field: FIELD_PATH] + + // The value (datapoint) + Datapoint value = 2; // [field: FIELD_VALUE] + + // Actuator target (only used if the entry is an actuator) + Datapoint actuator_target = 3; // [field: FIELD_ACTUATOR_TARGET] + + // Metadata for this entry + Metadata metadata = 10; // [field: FIELD_METADATA] +} + +message Datapoint { + google.protobuf.Timestamp timestamp = 1; + + oneof value { + string string = 11; + bool _bool = 12; + sint32 int32 = 13; + sint64 int64 = 14; + uint32 uint32 = 15; + uint64 uint64 = 16; + float _float = 17; + double _double = 18; + StringArray string_array = 21; + BoolArray bool_array = 22; + Int32Array int32_array = 23; + Int64Array int64_array = 24; + Uint32Array uint32_array = 25; + Uint64Array uint64_array = 26; + FloatArray float_array = 27; + DoubleArray double_array = 28; + } +} + +message Metadata { + // Data type + // The VSS data type of the entry (i.e. the value, min, max etc). + // + // NOTE: protobuf doesn't have int8, int16, uint8 or uint16 which means + // that these values must be serialized as int32 and uint32 respectively. + DataType data_type = 11; // [field: FIELD_METADATA_DATA_TYPE] + + // Entry type + EntryType entry_type = 12; // [field: FIELD_METADATA_ENTRY_TYPE] + + // Description + // Describes the meaning and content of the entry. + optional string description = 13; // [field: FIELD_METADATA_DESCRIPTION] + + // Comment [optional] + // A comment can be used to provide additional informal information + // on a entry. + optional string comment = 14; // [field: FIELD_METADATA_COMMENT] + + // Deprecation [optional] + // Whether this entry is deprecated. Can contain recommendations of what + // to use instead. + optional string deprecation = 15; // [field: FIELD_METADATA_DEPRECATION] + + // Unit [optional] + // The unit of measurement + optional string unit = 16; // [field: FIELD_METADATA_UNIT] + + // Value restrictions [optional] + // Restrict which values are allowed. + // Only restrictions matching the DataType {datatype} above are valid. + ValueRestriction value_restriction = 17; // [field: FIELD_METADATA_VALUE_RESTRICTION] + + // Entry type specific metadata + oneof entry_specific { + Actuator actuator = 20; // [field: FIELD_METADATA_ACTUATOR] + Sensor sensor = 30; // [field: FIELD_METADATA_SENSOR] + Attribute attribute = 40; // [field: FIELD_METADATA_ATTRIBUTE] + } +} + +/////////////////////// +// Actuator specific fields +message Actuator { + // Nothing for now +} + +//////////////////////// +// Sensor specific +message Sensor { + // Nothing for now +} + +//////////////////////// +// Attribute specific +message Attribute { + // Nothing for now. +} + +// Value restriction +// +// One ValueRestriction{type} for each type, since +// they don't make sense unless the types match +// +message ValueRestriction { + oneof type { + ValueRestrictionString string = 21; + // For signed VSS integers + ValueRestrictionInt _signed = 22; + // For unsigned VSS integers + ValueRestrictionUint _unsigned = 23; + // For floating point VSS values (float and double) + ValueRestrictionFloat floating_point = 24; + } +} + +message ValueRestrictionInt { + optional sint64 min = 1; + optional sint64 max = 2; + repeated sint64 allowed_values = 3; +} + +message ValueRestrictionUint { + optional uint64 min = 1; + optional uint64 max = 2; + repeated uint64 allowed_values = 3; +} + +message ValueRestrictionFloat { + optional double min = 1; + optional double max = 2; + + // allowed for doubles/floats not recommended + repeated double allowed_values = 3; +} + +// min, max doesn't make much sense for a string +message ValueRestrictionString { + repeated string allowed_values = 3; +} + +// VSS Data type of a signal +// +// Protobuf doesn't support int8, int16, uint8 or uint16. +// These are mapped to int32 and uint32 respectively. +// +enum DataType { + DATA_TYPE_UNSPECIFIED = 0; + DATA_TYPE_STRING = 1; + DATA_TYPE_BOOLEAN = 2; + DATA_TYPE_INT8 = 3; + DATA_TYPE_INT16 = 4; + DATA_TYPE_INT32 = 5; + DATA_TYPE_INT64 = 6; + DATA_TYPE_UINT8 = 7; + DATA_TYPE_UINT16 = 8; + DATA_TYPE_UINT32 = 9; + DATA_TYPE_UINT64 = 10; + DATA_TYPE_FLOAT = 11; + DATA_TYPE_DOUBLE = 12; + DATA_TYPE_TIMESTAMP = 13; + DATA_TYPE_STRING_ARRAY = 20; + DATA_TYPE_BOOLEAN_ARRAY = 21; + DATA_TYPE_INT8_ARRAY = 22; + DATA_TYPE_INT16_ARRAY = 23; + DATA_TYPE_INT32_ARRAY = 24; + DATA_TYPE_INT64_ARRAY = 25; + DATA_TYPE_UINT8_ARRAY = 26; + DATA_TYPE_UINT16_ARRAY = 27; + DATA_TYPE_UINT32_ARRAY = 28; + DATA_TYPE_UINT64_ARRAY = 29; + DATA_TYPE_FLOAT_ARRAY = 30; + DATA_TYPE_DOUBLE_ARRAY = 31; + DATA_TYPE_TIMESTAMP_ARRAY = 32; +} + +// Entry type +enum EntryType { + ENTRY_TYPE_UNSPECIFIED = 0; + ENTRY_TYPE_ATTRIBUTE = 1; + ENTRY_TYPE_SENSOR = 2; + ENTRY_TYPE_ACTUATOR = 3; +} + +// A `View` specifies a set of fields which should +// be populated in a `DataEntry` (in a response message) +enum View { + VIEW_UNSPECIFIED = 0; // Unspecified. Equivalent to VIEW_CURRENT_VALUE unless `fields` are explicitly set. + VIEW_CURRENT_VALUE = 1; // Populate DataEntry with value. + VIEW_TARGET_VALUE = 2; // Populate DataEntry with actuator target. + VIEW_METADATA = 3; // Populate DataEntry with metadata. + VIEW_FIELDS = 10; // Populate DataEntry only with requested fields. + VIEW_ALL = 20; // Populate DataEntry with everything. +} + +// A `Field` corresponds to a specific field of a `DataEntry`. +// +// It can be used to: +// * populate only specific fields of a `DataEntry` response. +// * specify which fields of a `DataEntry` should be set as +// part of a `Set` request. +// * subscribe to only specific fields of a data entry. +// * convey which fields of an updated `DataEntry` have changed. +enum Field { + FIELD_UNSPECIFIED = 0; // "*" i.e. everything + FIELD_PATH = 1; // path + FIELD_VALUE = 2; // value + FIELD_ACTUATOR_TARGET = 3; // actuator_target + FIELD_METADATA = 10; // metadata.* + FIELD_METADATA_DATA_TYPE = 11; // metadata.data_type + FIELD_METADATA_DESCRIPTION = 12; // metadata.description + FIELD_METADATA_ENTRY_TYPE = 13; // metadata.entry_type + FIELD_METADATA_COMMENT = 14; // metadata.comment + FIELD_METADATA_DEPRECATION = 15; // metadata.deprecation + FIELD_METADATA_UNIT = 16; // metadata.unit + FIELD_METADATA_VALUE_RESTRICTION = 17; // metadata.value_restriction.* + FIELD_METADATA_ACTUATOR = 20; // metadata.actuator.* + FIELD_METADATA_SENSOR = 30; // metadata.sensor.* + FIELD_METADATA_ATTRIBUTE = 40; // metadata.attribute.* +} + +// Error response shall be an HTTP-like code. +// Should follow https://www.w3.org/TR/viss2-transport/#status-codes. +message Error { + uint32 code = 1; + string reason = 2; + string message = 3; +} + +// Used in get/set requests to report errors for specific entries +message DataEntryError { + string path = 1; // vss path + Error error = 2; +} + +message StringArray { + repeated string values = 1; +} + +message BoolArray { + repeated bool values = 1; +} + +message Int32Array { + repeated sint32 values = 1; +} + +message Int64Array { + repeated sint64 values = 1; +} + +message Uint32Array { + repeated uint32 values = 1; +} + +message Uint64Array { + repeated uint64 values = 1; +} + +message FloatArray { + repeated float values = 1; +} + +message DoubleArray { + repeated double values = 1; +} diff --git a/gRPC-on-ESP32/proto/val.proto b/gRPC-on-ESP32/proto/val.proto new file mode 100644 index 0000000..c3818b0 --- /dev/null +++ b/gRPC-on-ESP32/proto/val.proto @@ -0,0 +1,115 @@ +/******************************************************************************** + * Copyright (c) 2022 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Apache License 2.0 which is available at + * http://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + ********************************************************************************/ + +syntax = "proto3"; + +package kuksa.val.v1; + +option go_package = "kuksa/val/v1"; + +import "types.proto"; + +// Note on authorization: +// Tokens (auth-token or auth-uuid) are sent as (GRPC / http2) metadata. +// +// The auth-token is a JWT compliant token as the examples found here: +// https://github.com/eclipse/kuksa.val/tree/master/kuksa_certificates/jwt +// +// See also https://github.com/eclipse/kuksa.val/blob/master/doc/jwt.md +// +// Upon reception of auth-token, server shall generate an auth-uuid in metadata +// that the client can use instead of auth-token in subsequent calls. + +service VAL { + // Get entries + rpc Get(GetRequest) returns (GetResponse); + + // Set entries + rpc Set(SetRequest) returns (SetResponse); + + // Subscribe to a set of entries + // + // Returns a stream of notifications. + // + // InvalidArgument is returned if the request is malformed. + rpc Subscribe(SubscribeRequest) returns (stream SubscribeResponse); + + // Shall return information that allows the client to determine + // what server/server implementation/version it is talking to + // eg. kuksa-databroker 0.5.1 + rpc GetServerInfo(GetServerInfoRequest) returns (GetServerInfoResponse); +} + +// Define which data we want +message EntryRequest { + string path = 1; + View view = 2; + repeated Field fields = 3; +} + +// Request a set of entries. +message GetRequest { + repeated EntryRequest entries = 1; +} + +// Global errors are specified in `error`. +// Errors for individual entries are specified in `errors`. +message GetResponse { + repeated DataEntry entries = 1; + repeated DataEntryError errors = 2; + Error error = 3; +} + +// Define the data we want to set +message EntryUpdate { + DataEntry entry = 1; + repeated Field fields = 2; +} + +// A list of entries to be updated +message SetRequest { + repeated EntryUpdate updates = 1; +} + +// Global errors are specified in `error`. +// Errors for individual entries are specified in `errors`. +message SetResponse { + Error error = 1; + repeated DataEntryError errors = 2; +} + +// Define what to subscribe to +message SubscribeEntry { + string path = 1; + View view = 2; + repeated Field fields = 3; +} + +// Subscribe to changes in datapoints. +message SubscribeRequest { + repeated SubscribeEntry entries = 1; +} + +// A subscription response +message SubscribeResponse { + repeated EntryUpdate updates = 1; +} + +message GetServerInfoRequest { + // Nothing yet +} + +message GetServerInfoResponse { + string name = 1; + string version = 2; +} diff --git a/gRPC-on-ESP32/sdkconfig.ci b/gRPC-on-ESP32/sdkconfig.ci new file mode 100644 index 0000000..81b8c59 --- /dev/null +++ b/gRPC-on-ESP32/sdkconfig.ci @@ -0,0 +1,11 @@ +CONFIG_SPIRAM=y +CONFIG_MBEDTLS_EXTERNAL_MEM_ALLOC=y +CONFIG_EXAMPLE_CONNECT_ETHERNET=y +CONFIG_EXAMPLE_CONNECT_WIFI=n +CONFIG_EXAMPLE_USE_INTERNAL_ETHERNET=y +CONFIG_EXAMPLE_ETH_PHY_IP101=y +CONFIG_EXAMPLE_ETH_MDC_GPIO=23 +CONFIG_EXAMPLE_ETH_MDIO_GPIO=18 +CONFIG_EXAMPLE_ETH_PHY_RST_GPIO=5 +CONFIG_EXAMPLE_ETH_PHY_ADDR=1 +CONFIG_EXAMPLE_CONNECT_IPV6=y diff --git a/gRPC-on-ESP32/sdkconfig.defaults b/gRPC-on-ESP32/sdkconfig.defaults new file mode 100644 index 0000000..998be29 --- /dev/null +++ b/gRPC-on-ESP32/sdkconfig.defaults @@ -0,0 +1 @@ +CONFIG_MBEDTLS_CERTIFICATE_BUNDLE_DEFAULT_CMN=y