From cbee09be85756d14226f5c16356809c4d396dd9f Mon Sep 17 00:00:00 2001
From: Michael Sippel <micha@fragmental.art>
Date: Mon, 17 Mar 2025 10:14:41 +0100
Subject: [PATCH] initial virtual sensor with protobuf

---
 database.proto                           |  21 ++++
 random-sensor-protobuf/meson.build       |  26 ++++
 random-sensor-protobuf/random-sensor.cpp | 154 +++++++++++++++++++++++
 sensor-query-cli/main.cpp                |  30 +++++
 sensor-query-cli/meson.build             |  26 ++++
 sensor.lt                                |  25 ++++
 sensor.proto                             |  44 +++++++
 7 files changed, 326 insertions(+)
 create mode 100644 database.proto
 create mode 100644 random-sensor-protobuf/meson.build
 create mode 100644 random-sensor-protobuf/random-sensor.cpp
 create mode 100644 sensor-query-cli/main.cpp
 create mode 100644 sensor-query-cli/meson.build
 create mode 100644 sensor.lt
 create mode 100644 sensor.proto

diff --git a/database.proto b/database.proto
new file mode 100644
index 0000000..a7a46d3
--- /dev/null
+++ b/database.proto
@@ -0,0 +1,21 @@
+syntax = "proto3";
+
+
+message DatabaseStatus {
+  
+}
+
+message TimeInterval {
+  
+}
+
+message DataSlice {
+  
+}
+
+service Database {
+  rpc get_status () returns (DatabaseStatus) {}
+  rpc fetch_data (TimeInterval) returns (DataSlice) {}
+}
+
+
diff --git a/random-sensor-protobuf/meson.build b/random-sensor-protobuf/meson.build
new file mode 100644
index 0000000..0dc1f78
--- /dev/null
+++ b/random-sensor-protobuf/meson.build
@@ -0,0 +1,26 @@
+project('random-sensor-protobuf', 'cpp',
+default_options: ['cpp_std=c++20'])
+
+protobuf_dep = dependency('protobuf')
+grpc_dep = dependency('grpc++', required: true)
+
+proto_files = files('../sensor.proto')
+
+protobuf_gen = custom_target(
+    'generate_protobuf',
+    input : proto_files,
+    output : ['sensor.pb.cc', 'sensor.pb.h', 'sensor.grpc.pb.cc', 'sensor.grpc.pb.h'],
+    command : [
+        'protoc',
+        '--proto_path=../../',
+        '--cpp_out', '.',
+        '--grpc_out', '.',
+        '--plugin=protoc-gen-grpc=/usr/bin/grpc_cpp_plugin',
+        '@INPUT@'
+    ],
+)
+
+executable('random-sensor-protobuf',
+    ['random-sensor.cpp', protobuf_gen],
+    dependencies: [protobuf_dep, grpc_dep]
+)
diff --git a/random-sensor-protobuf/random-sensor.cpp b/random-sensor-protobuf/random-sensor.cpp
new file mode 100644
index 0000000..90cbea2
--- /dev/null
+++ b/random-sensor-protobuf/random-sensor.cpp
@@ -0,0 +1,154 @@
+#include <iostream>
+#include <chrono>
+#include <cmath>
+#include <random>
+#include <semaphore>
+#include <google/protobuf/arena.h>
+#include <thread>
+
+#include <grpc/grpc.h>
+#include <grpcpp/security/server_credentials.h>
+#include <grpcpp/server.h>
+#include <grpcpp/server_builder.h>
+#include <grpcpp/server_context.h>
+
+#include "sensor.pb.h"
+#include "sensor.grpc.pb.h"
+
+template < size_t T_Capacity >
+struct RandomSensor
+    : public Sensor::Service
+{
+private:
+    google::protobuf::Arena arena;
+
+    std::chrono::time_point<std::chrono::high_resolution_clock> online_since;
+    std::chrono::duration<uint64_t, std::milli> period_duration;
+    uint32_t chunk_size;
+    uint32_t next_free;
+    uint32_t next_full;
+
+    float bat_charge;
+
+    std::counting_semaphore<> sem_full;
+    std::counting_semaphore<> sem_free;
+
+    float data[ T_Capacity ];
+    std::thread sensor_thread;
+
+    void generate_value()
+    {
+        sem_free.acquire();
+
+        data[ next_free ] = (float) next_free / 100.0;
+        next_free = (next_free + 1) % T_Capacity;
+
+        sem_full.release();
+    }
+
+public:
+    RandomSensor()
+        : online_since(std::chrono::high_resolution_clock::now())
+        , period_duration( 10 )
+        , chunk_size( 8192 )
+        , next_free( 0 )
+        , next_full( 0 )
+        , bat_charge( 8200.0 )
+        , sem_full( 0 )
+        , sem_free( T_Capacity )
+        , sensor_thread([&] {
+            while(bat_charge > 0) {
+                generate_value();
+                bat_charge -= 0.01;
+                std::this_thread::sleep_for( period_duration );
+            }
+        })
+    {}
+
+    ~RandomSensor() {
+        sensor_thread.join();
+    }
+
+    uint32_t n_chunks_capacity() const {
+        return T_Capacity / chunk_size;
+    }
+
+    uint32_t n_full_chunks() const {
+        return (( next_free - next_full ) % T_Capacity) / chunk_size;
+    }
+
+    uint32_t n_empty_chunks() const {
+        return n_chunks_capacity() - n_full_chunks();
+    }
+
+    grpc::Status set_sampling_period(
+        grpc::ServerContext * context,
+        Duration const * new_sampling_period,
+        SetSamplingPeriodResult * result
+    ) override {
+        // todo
+        return grpc::Status::OK;
+    }
+
+    grpc::Status pop_data_chunk(
+        grpc::ServerContext * context,
+        DataChunkRequest const * request,
+        DataChunk * chunk
+    ) override {
+        Duration * rate = google::protobuf::Arena::Create<Duration>(&arena);
+        rate->set_millis(period_duration.count());
+
+        chunk->set_allocated_period(rate);
+
+        for( uint32_t i = 0; i < chunk_size; ++i ) {
+            Temperature * temp = chunk->add_data();
+
+            sem_full.acquire();
+
+            temp->set_celsius( data[next_full] );
+            next_full = (next_full + 1) % T_Capacity;
+
+            sem_free.release();
+        }
+
+        return grpc::Status::OK;
+    }
+
+    grpc::Status get_status(
+        grpc::ServerContext * context,
+        StatusRequest const * request,
+        SensorStatus * status
+    ) override
+    {
+        status->set_name("random sensor (protobuf)");
+        status->mutable_online_since()->set_millis_since_epoch(
+            std::chrono::duration_cast<std::chrono::milliseconds>(online_since.time_since_epoch()).count()
+        );
+        status->mutable_battery_capacity()->set_mah(8200);
+        status->mutable_battery_charge()->set_mah(bat_charge);
+        status->mutable_cur_sampling_period()->set_millis(period_duration.count());
+        status->mutable_max_sampling_period()->set_millis(10);
+        status->set_max_chunk_size(T_Capacity);
+        status->set_cur_chunk_size(chunk_size);
+        status->set_n_chunk_capacity( n_chunks_capacity() );
+        status->set_n_full_data_chunks( n_full_chunks() );
+        status->set_n_empty_data_chunks( n_empty_chunks() );
+
+        return grpc::Status::OK;
+    }
+};
+
+int main() {
+    RandomSensor< 1048576 > sensor;
+
+    std::string server_address("0.0.0.0:50051");
+
+    grpc::ServerBuilder builder;
+    builder.AddListeningPort(server_address, grpc::InsecureServerCredentials());
+    builder.RegisterService(&sensor);
+    std::unique_ptr<grpc::Server> server(builder.BuildAndStart());
+    std::cout << "Server listening on " << server_address << std::endl;
+    server->Wait();
+
+    return 0;
+}
diff --git a/sensor-query-cli/main.cpp b/sensor-query-cli/main.cpp
new file mode 100644
index 0000000..b0c71b5
--- /dev/null
+++ b/sensor-query-cli/main.cpp
@@ -0,0 +1,30 @@
+#include <iostream>
+#include <memory>
+#include <grpc/grpc.h>
+#include <grpcpp/channel.h>
+#include <grpcpp/client_context.h>
+#include <grpcpp/create_channel.h>
+#include <grpcpp/security/credentials.h>
+
+#include "sensor.pb.h"
+#include "sensor.grpc.pb.h"
+
+int main() {
+    auto channel = std::shared_ptr(grpc::CreateChannel("localhost:50051", grpc::InsecureChannelCredentials()));
+    auto stub = Sensor::NewStub(channel);
+
+    grpc::ClientContext context;
+
+    StatusRequest request;
+    SensorStatus status;
+    grpc::Status rpc_status = stub->get_status(&context, request, &status);
+
+    if( rpc_status.ok() ) {
+        std::cout << "Sensor Status:" << std::endl;
+        std::cout << status.DebugString() << std::endl;
+    } else {
+        std::cout << "get_status() failed" << std::endl;
+    }
+
+    return 0;
+}
diff --git a/sensor-query-cli/meson.build b/sensor-query-cli/meson.build
new file mode 100644
index 0000000..859e30d
--- /dev/null
+++ b/sensor-query-cli/meson.build
@@ -0,0 +1,26 @@
+project('sensor-query-cli', 'cpp',
+default_options: ['cpp_std=c++20'])
+
+protobuf_dep = dependency('protobuf')
+grpc_dep = dependency('grpc++', required: true)
+
+proto_files = files('../sensor.proto')
+
+protobuf_gen = custom_target(
+    'generate_protobuf',
+    input : proto_files,
+    output : ['sensor.pb.cc', 'sensor.pb.h', 'sensor.grpc.pb.cc', 'sensor.grpc.pb.h'],
+    command : [
+        'protoc',
+        '--proto_path=../../',
+        '--cpp_out', '.',
+        '--grpc_out', '.',
+        '--plugin=protoc-gen-grpc=/usr/bin/grpc_cpp_plugin',
+        '@INPUT@'
+    ],
+)
+
+executable('sensor-query-cli',
+    ['main.cpp', protobuf_gen],
+    dependencies: [protobuf_dep, grpc_dep]
+)
diff --git a/sensor.lt b/sensor.lt
new file mode 100644
index 0000000..7172b65
--- /dev/null
+++ b/sensor.lt
@@ -0,0 +1,25 @@
+
+trait SensorStatus = {
+    name : [ Char ] ;
+    online_since : TimePoint ;
+    battery_charge : Energy ;
+    battery_capacity : Energy ;
+    min_sampling_period : Duration ;
+    cur_sampling_period : Duration ;
+    max_chunk_size : ℤ ;
+    cur_chunk_size : ℤ ;
+    n_chunk_capacity : ℤ ;
+    n_full_data_chunks : ℤ ;
+    n_empty_data_chunks : ℤ ;
+}
+
+trait DataChunk = {
+    begin : TimePoint ;
+    data : [ Temperature ] ;
+}
+
+trait Sensor = {
+    get_status : {} -> SensorStatus ;
+    set_sampling_period : Duration -> (Ok | OutOfRange);
+    pop_data_chunk : {} -> DataChunk ;
+}
diff --git a/sensor.proto b/sensor.proto
new file mode 100644
index 0000000..e3cbbc7
--- /dev/null
+++ b/sensor.proto
@@ -0,0 +1,44 @@
+syntax = "proto3";
+
+message Energy {
+    uint32 mAh = 1;
+}
+message TimePoint {
+    uint64 millis_since_epoch = 1;
+}
+message Duration {
+    uint32 millis = 1;
+}
+message Temperature {
+    double celsius = 1;
+}
+
+message StatusRequest {}
+message SensorStatus {
+    string name = 1;
+    TimePoint online_since = 2;
+    Energy battery_charge = 3;
+    Energy battery_capacity = 4;
+    Duration max_sampling_period = 5;
+    Duration cur_sampling_period = 6;
+    uint32 max_chunk_size = 7;
+    uint32 cur_chunk_size = 8;
+    uint32 n_chunk_capacity = 9;
+    uint32 n_full_data_chunks = 10;
+    uint32 n_empty_data_chunks = 11;
+}
+
+message DataChunkRequest {}
+message DataChunk {
+    TimePoint begin = 1;
+    Duration period = 2;
+    repeated Temperature data = 3;
+}
+
+message SetSamplingPeriodResult {}
+
+service Sensor {
+    rpc get_status (StatusRequest) returns (SensorStatus) {}
+    rpc set_sampling_period (Duration) returns (SetSamplingPeriodResult) {}
+    rpc pop_data_chunk (DataChunkRequest) returns (DataChunk) {}
+}