Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add : Device manager integrations (RestAPI and Websocket) #4

Merged
merged 4 commits into from
Jul 29, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,19 @@ version = "0.0.0"
edition = "2021"

[dependencies]
actix = "0.13.3"
actix-web = "4.6.0"
bluerobotics-ping = "0.2.3"
actix-web-actors = "4.3.0"
chrono = "0.4.38"
clap = {version = "4.5.4", features = ["derive"] }
lazy_static = "1.4.0"
mime_guess = "2.0.4"
paperclip = { version = "0.8.2" , features = ["actix4", "swagger-ui", "uuid"] }
serde = { version = "1.0.197", features = ["derive"] }
regex = "1.10.4"
rust-embed = "8.4.0"
serde_json = "1.0.117"
tokio = { version = "1.37.0", features = ["full"] }
tokio-serial = "5.4.4"
tracing = { version = "0.1.40", features = ["log", "async-await"] }
Expand All @@ -20,6 +26,8 @@ tracing-appender = { git = "https://github.com/joaoantoniocardoso/tracing", bran
tracing-tracy = "0.11.0"
udp-stream = "0.0.12"
uuid = { version = "1.8", features = ["serde"] }
validator = "0.18.1"
thiserror = "1.0.61"

[build-dependencies]
vergen-gix = { version = "1.0.0-beta.2", default-features = false, features = ["build", "cargo"] }
52 changes: 42 additions & 10 deletions src/device/manager.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
use paperclip::actix::Apiv2Schema;
use serde::{Deserialize, Serialize};
use std::{
collections::{hash_map::DefaultHasher, HashMap},
Expand Down Expand Up @@ -59,13 +60,13 @@ enum SourceType {
Serial(SerialStream),
}

#[derive(Clone, Debug, Deserialize, Serialize, Hash)]
#[derive(Clone, Debug, Deserialize, Serialize, Hash, Apiv2Schema)]
pub struct SourceUdpStruct {
pub ip: Ipv4Addr,
pub port: u16,
}

#[derive(Clone, Debug, Deserialize, Serialize, Hash)]
#[derive(Clone, Debug, Deserialize, Serialize, Hash, Apiv2Schema)]
pub struct SourceSerialStruct {
pub path: String,
pub baudrate: u32,
Expand All @@ -92,9 +93,9 @@ pub struct ManagerActorHandler {
pub sender: mpsc::Sender<ManagerActorRequest>,
}

#[derive(Debug, Serialize, Deserialize, Clone)]
#[derive(Debug, Serialize, Deserialize, Clone, Apiv2Schema)]
pub enum Answer {
Ping(PingAnswer),
DeviceMessage(DeviceAnswer),
#[serde(skip)]
InnerDeviceHandler(DeviceActorHandler),
DeviceInfo(Vec<DeviceInfo>),
Expand All @@ -113,17 +114,18 @@ pub enum ManagerError {
}

#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct PingAnswer {
pub answer: Result<crate::device::devices::PingAnswer, crate::device::devices::DeviceError>,
pub struct DeviceAnswer {
#[serde(flatten)]
pub answer: crate::device::devices::PingAnswer,
pub device_id: Uuid,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
#[derive(Debug, Clone, Serialize, Deserialize, Apiv2Schema)]
pub enum Request {
Create(CreateStruct),
Delete(Uuid),
List,
Status,
Info(Uuid),
Search,
Ping(DeviceRequestStruct),
GetDeviceHandler(Uuid),
Expand Down Expand Up @@ -163,6 +165,12 @@ impl DeviceManager {
error!("DeviceManager: Failed to return List response: {e:?}");
}
}
Request::Info(device_id) => {
let result = self.info(device_id).await;
if let Err(e) = actor_request.respond_to.send(result) {
error!("DeviceManager: Failed to return Info response: {:?}", e);
}
}
Request::GetDeviceHandler(id) => {
let answer = self.get_device_handler(id).await;
if let Err(e) = actor_request.respond_to.send(answer) {
Expand Down Expand Up @@ -343,6 +351,30 @@ impl DeviceManager {
Ok(Answer::DeviceInfo(list))
}

pub async fn info(&self, device_id: Uuid) -> Result<Answer, ManagerError> {
self.check_device_uuid(device_id)?;
Ok(Answer::DeviceInfo(vec![self.get_device(device_id)?.info()]))
}

fn check_device_uuid(&self, device_id: Uuid) -> Result<(), ManagerError> {
if self.device.contains_key(&device_id) {
return Ok(());
}
error!(
"Getting device handler for device: {:?} : Error, device doesn't exist",
device_id
);
Err(ManagerError::DeviceNotExist(device_id))
}

fn get_device(&self, device_id: Uuid) -> Result<&Device, ManagerError> {
let device = self
.device
.get(&device_id)
.ok_or(ManagerError::DeviceNotExist(device_id))?;
Ok(device)
}

pub async fn delete(&mut self, device_id: Uuid) -> Result<Answer, ManagerError> {
match self.device.remove(&device_id) {
Some(device) => {
Expand Down Expand Up @@ -410,8 +442,8 @@ impl ManagerActorHandler {
match result {
Ok(result) => {
info!("Handling Ping request: {request:?}: Success");
Ok(Answer::Ping(PingAnswer {
answer: Ok(result),
Ok(Answer::DeviceMessage(DeviceAnswer {
answer: result,
device_id: request.target,
}))
}
Expand Down
7 changes: 2 additions & 5 deletions src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -21,13 +21,10 @@ async fn main() {
//Todo: Load previous devices
info!(
"DeviceManager initialized with following devices: {:?}",
handler
.send(crate::device::manager::Request::List {})
.await
.unwrap()
handler.send(crate::device::manager::Request::List).await
);

server::manager::run(&cli::manager::server_address())
server::manager::run(&cli::manager::server_address(), handler)
.await
.unwrap();
}
30 changes: 25 additions & 5 deletions src/server/manager.rs
Original file line number Diff line number Diff line change
@@ -1,19 +1,39 @@
use actix_web::{middleware, App, HttpServer};
use paperclip::actix::OpenApiExt;
use crate::device::manager::ManagerActorHandler;

use super::protocols;
use actix_web::{middleware, web::Data, App, HttpServer};
use tracing::info;

pub async fn run(server_address: &str) -> std::io::Result<()> {
use paperclip::actix::{
web::{self, Scope},
OpenApiExt,
};

fn add_v1_paths(scope: Scope) -> Scope {
scope.configure(protocols::v1::rest::register_services)
}

pub async fn run(server_address: &str, handler: ManagerActorHandler) -> std::io::Result<()> {
let server_address = server_address.to_string();
info!("starting HTTP server at http://{server_address}");
info!("ServerManager: Service starting");

let server = HttpServer::new(move || {
let v1 = add_v1_paths(web::scope("/v1"));
let default = add_v1_paths(web::scope(""));

let server = HttpServer::new(|| {
App::new()
.app_data(Data::new(handler.clone()))
.wrap(middleware::Logger::default())
.wrap_api()
.with_json_spec_at("/api/spec")
.with_swagger_ui_at("/docs")
.service(v1)
.service(protocols::v1::rest::server_metadata)
.service(protocols::v1::websocket::websocket)
.service(default)
.build()
});

info!("ServerManager: HTTP server running at http://{server_address}");
server.bind(server_address)?.run().await
}
24 changes: 24 additions & 0 deletions src/server/mod.rs
Original file line number Diff line number Diff line change
@@ -1 +1,25 @@
pub mod manager;
pub mod protocols;

// The Server module consists of a manager and all available layers that provide access to internal services.
//
// Manager:
// The Manager module requires a DeviceManagerHandler, which will be used to forward all incoming requests.
// This allows the Manager to receive and process requests from RestAPI and WebSocket methods.
// The requests are forwarded to the DeviceManager using the server's AppData, which holds a clone of the DeviceManager's Handler and will provide the responses.
//
// Front-end:
// The frontend provides access to REST API documentation through {address}/docs with a Swagger interface and the API specifications.
//
// RestAPI:
// The REST API will have a default route and versioned routes.
// To keep the application stable through updates, users can use {address}/v{x}/route.
//
// WebSocket:
// WebSocket is provided via the {address}/ws route.
// Users can use the following queries:
// ?filter="some_desired_string_to_use_regex"
// ?device-number="00000000-0000-0000-b9c0-f5752d453eb3" // The UUID provided by the source of the device created
// Otherwise, if they are not defined, the WebSocket channel will receive all available messages.
// All operations made through REST API and WebSocket routes will be broadcast to all clients subscribed to device-number=null (default),
// except for errors, which are forwarded directly to the requester.
1 change: 1 addition & 0 deletions src/server/protocols/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
pub mod v1;
40 changes: 40 additions & 0 deletions src/server/protocols/v1/errors.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
use actix_web::{http::StatusCode, ResponseError};

use paperclip::actix::api_v2_errors;
use validator::ValidationErrors;

#[allow(dead_code)]
#[api_v2_errors(
code = 400,
description = "Bad Request: The client's request contains invalid or malformed data.",
code = 500,
description = "Internal Server Error: An unexpected server error has occurred."
)]
#[derive(Debug, thiserror::Error)]
pub enum Error {
#[error("Bad Request: {0}")]
BadRequest(String),
#[error("Internal Server Error: {0}")]
Internal(String),
}

impl ResponseError for Error {
fn status_code(&self) -> StatusCode {
match self {
Self::BadRequest(_) => StatusCode::BAD_REQUEST,
Self::Internal(_) => StatusCode::INTERNAL_SERVER_ERROR,
}
}
}

impl From<ValidationErrors> for Error {
fn from(error: ValidationErrors) -> Self {
Self::BadRequest(error.to_string())
}
}

impl From<crate::device::manager::ManagerError> for Error {
fn from(error: crate::device::manager::ManagerError) -> Self {
Self::Internal(serde_json::to_string_pretty(&error).unwrap_or_default())
}
}
61 changes: 61 additions & 0 deletions src/server/protocols/v1/frontend/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Ping Viewer Next</title>
<script>
function redirectToDocs() {
window.location.href = '/docs/';
}

let socket;

function connectWebSocket() {
socket = new WebSocket('ws://' + window.location.host + '/ws');

socket.onopen = function(event) {
document.getElementById('status').innerText = 'Connected';
};

socket.onmessage = function(event) {
const messagesDiv = document.getElementById('messages');
const messageElement = document.createElement('div');
messageElement.textContent = event.data;
messagesDiv.appendChild(messageElement);
messagesDiv.scrollTop = messagesDiv.scrollHeight; // Auto-scroll to the bottom
};

socket.onclose = function(event) {
document.getElementById('status').innerText = 'Disconnected';
};

socket.onerror = function(event) {
document.getElementById('status').innerText = 'Error';
};
}

function sendMessage() {
const messageInput = document.getElementById('messageInput');
const message = messageInput.value;
if (message && socket.readyState === WebSocket.OPEN) {
socket.send(message);
messageInput.value = '';
}
}

window.onload = function() {
connectWebSocket();
};
</script>
</head>
<body>
<h1>Ping Viewer Next</h1>
<button onclick="redirectToDocs()">Check API specifications</button>
<h2>Websocket Client</h2>
<div id="status">Connecting...</div>
<div id="messages" style="border: 1px solid #ccc; height: 600px; overflow-y: scroll; padding: 5px; white-space: pre-wrap;"></div>
<input type="text" id="messageInput" placeholder="Type your message here" />
<button onclick="sendMessage()">Send</button>
</body>
</html>
3 changes: 3 additions & 0 deletions src/server/protocols/v1/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
pub mod errors;
pub mod rest;
pub mod websocket;
Loading