Replace gRPC Backend (#10)

**Rationale:**

Having two separate servers and communication methods resulted in additional maintenance & the need to convert often between backend & frontend data types.
By moving the backend communication off of gRPC and to just use websockets it both gives more control & allows for simplification of the implementation.

#8

**Changes:**

- Replaces gRPC backend.
  - New implementation automatically handles reconnect logic
- Implements an api layer
- Migrates examples to the api layer
- Implements a proc macro to make command handling easier
- Implements unit tests for the api layer (90+% coverage)
- Implements integration tests for the proc macro (90+% coverage)

Reviewed-on: #10
Co-authored-by: Sergey Savelyev <sergeysav.nn@gmail.com>
Co-committed-by: Sergey Savelyev <sergeysav.nn@gmail.com>
This commit was merged in pull request #10.
This commit is contained in:
2026-01-01 10:11:53 -08:00
committed by sergeysav
parent f658b55586
commit 788dd10a91
68 changed files with 3934 additions and 1504 deletions

View File

@@ -1,36 +1,40 @@
use crate::command::definition::CommandDefinition;
use crate::command::command_handle::CommandHandle;
use crate::command::error::Error as CmdError;
use crate::command::error::Error::{
CommandFailure, CommandNotFound, FailedToReceiveResponse, FailedToSend,
IncorrectParameterCount, MisingParameter, NoCommandReceiver, WrongParameterType,
};
use crate::core::telemetry_value::Value;
use crate::core::{
Command, CommandDefinitionRequest, CommandResponse, TelemetryDataType, TelemetryValue,
Timestamp, Uuid,
};
use chrono::{DateTime, Utc};
use anyhow::bail;
use api::data_type::DataType;
use api::data_value::DataValue;
use api::messages::command::{Command, CommandDefinition, CommandHeader, CommandResponse};
use api::messages::ResponseMessage;
use chrono::Utc;
use log::error;
use papaya::HashMap;
use std::collections::HashMap as StdHashMap;
use tokio::sync::mpsc;
use tokio::sync::oneshot;
use tokio::sync::{mpsc, RwLock};
use uuid::Uuid;
#[derive(Clone)]
pub(super) struct RegisteredCommand {
pub(super) name: String,
pub(super) definition: CommandDefinitionRequest,
tx: mpsc::Sender<Option<(Command, oneshot::Sender<CommandResponse>)>>,
pub(super) definition: CommandDefinition,
response_uuid: Uuid,
tx: mpsc::Sender<ResponseMessage>,
}
pub struct CommandManagementService {
registered_commands: HashMap<String, RegisteredCommand>,
outstanding_responses: RwLock<StdHashMap<Uuid, oneshot::Sender<CommandResponse>>>,
}
impl CommandManagementService {
pub fn new() -> Self {
Self {
registered_commands: HashMap::new(),
outstanding_responses: RwLock::new(StdHashMap::new()),
}
}
@@ -52,26 +56,26 @@ impl CommandManagementService {
.map(|registration| registration.clone().into())
}
pub async fn register_command(
pub fn register_command(
&self,
command: CommandDefinitionRequest,
) -> anyhow::Result<mpsc::Receiver<Option<(Command, oneshot::Sender<CommandResponse>)>>> {
let (tx, rx) = mpsc::channel(1);
let registered_commands = self.registered_commands.pin_owned();
if let Some(previous) = registered_commands.insert(
command.name.clone(),
uuid: Uuid,
command: CommandDefinition,
tx: mpsc::Sender<ResponseMessage>,
) -> anyhow::Result<CommandHandle> {
let registered_commands = self.registered_commands.pin();
// We don't care about the previously registered command
let name = command.name.clone();
let _ = registered_commands.insert(
name.clone(),
RegisteredCommand {
name: command.name.clone(),
response_uuid: uuid,
name: name.clone(),
definition: command,
tx,
},
) {
// If the receiver was already closed, we don't care (ignore error)
let _ = previous.tx.send(None).await;
}
);
Ok(rx)
Ok(CommandHandle::new(name, uuid))
}
pub async fn send_command(
@@ -80,8 +84,6 @@ impl CommandManagementService {
parameters: serde_json::Map<String, serde_json::Value>,
) -> Result<String, CmdError> {
let timestamp = Utc::now();
let offset_from_unix_epoch =
timestamp - DateTime::from_timestamp(0, 0).expect("Could not get Unix epoch");
let name = name.into();
let registered_commands = self.registered_commands.pin();
@@ -100,27 +102,21 @@ impl CommandManagementService {
let Some(param_value) = parameters.get(&parameter.name) else {
return Err(MisingParameter(parameter.name.clone()));
};
let Some(param_value) = (match parameter.data_type() {
TelemetryDataType::Float32 => {
param_value.as_f64().map(|v| Value::Float32(v as f32))
}
TelemetryDataType::Float64 => param_value.as_f64().map(Value::Float64),
TelemetryDataType::Boolean => param_value.as_bool().map(Value::Boolean),
let Some(param_value) = (match parameter.data_type {
DataType::Float32 => param_value.as_f64().map(|v| DataValue::Float32(v as f32)),
DataType::Float64 => param_value.as_f64().map(DataValue::Float64),
DataType::Boolean => param_value.as_bool().map(DataValue::Boolean),
}) else {
return Err(WrongParameterType {
name: parameter.name.clone(),
expected_type: parameter.data_type(),
expected_type: parameter.data_type,
});
};
result_parameters.insert(
parameter.name.clone(),
TelemetryValue {
value: Some(param_value),
},
);
result_parameters.insert(parameter.name.clone(), param_value);
}
// Clone & Drop lets us use a standard pin instead of an owned pin
let response_uuid = registration.response_uuid;
let tx = registration.tx.clone();
drop(registered_commands);
@@ -128,23 +124,27 @@ impl CommandManagementService {
return Err(NoCommandReceiver);
}
let uuid = Uuid::random();
let uuid = Uuid::new_v4();
let (response_tx, response_rx) = oneshot::channel();
{
let mut outstanding_responses = self.outstanding_responses.write().await;
outstanding_responses.insert(uuid, response_tx);
}
if let Err(e) = tx
.send(Some((
Command {
uuid: Some(uuid),
timestamp: Some(Timestamp {
secs: offset_from_unix_epoch.num_seconds(),
nanos: offset_from_unix_epoch.subsec_nanos(),
}),
.send(ResponseMessage {
uuid,
response: Some(response_uuid),
payload: Command {
header: CommandHeader { timestamp },
parameters: result_parameters,
},
response_tx,
)))
}
.into(),
})
.await
{
error!("Failed to Send Command: {e}");
error!("Failed to Send Command {e}");
return Err(FailedToSend);
}
@@ -162,4 +162,33 @@ impl CommandManagementService {
}
})
}
pub async fn handle_command_response(
&self,
uuid: Uuid,
response: CommandResponse,
) -> anyhow::Result<()> {
let responder = {
let mut outstanding_responses = self.outstanding_responses.write().await;
outstanding_responses.remove(&uuid)
};
match responder {
None => bail!("Unexpected Command Response for Command {uuid}"),
Some(response_tx) => {
if let Err(e) = response_tx.send(response) {
bail!("Failed to send Command Response {e:?}");
}
}
};
Ok(())
}
pub fn unregister(&self, command_handle: CommandHandle) {
let registered_commands = self.registered_commands.pin();
// We don't care if this succeeded
let _ = registered_commands.remove_if(command_handle.name(), |_, registration| {
registration.response_uuid == *command_handle.uuid()
});
}
}