This commit is contained in:
@@ -5,7 +5,7 @@ meta {
|
|||||||
}
|
}
|
||||||
|
|
||||||
put {
|
put {
|
||||||
url: http://localhost:8282/firmware/waterlevel/INA226/1.0.3
|
url: http://localhost:8282/firmware/waterlevel/INA233/1.0.1
|
||||||
body: multipartForm
|
body: multipartForm
|
||||||
auth: none
|
auth: none
|
||||||
}
|
}
|
||||||
|
|||||||
113
src/device_telemetry_api.rs
Normal file
113
src/device_telemetry_api.rs
Normal file
@@ -0,0 +1,113 @@
|
|||||||
|
|
||||||
|
use actix_web::{get, post, web, HttpResponse, Responder};
|
||||||
|
use log::{error, info};
|
||||||
|
use sqlx::types::mac_address::MacAddress;
|
||||||
|
|
||||||
|
use crate::{schemas::{AppState, TelemetryMessageFromDevice, ValueMessageFromDevice}, util::parse_mac_address};
|
||||||
|
|
||||||
|
#[post("/telemetry/{device_id}")]
|
||||||
|
async fn receive_telemetry(
|
||||||
|
device_id: web::Path<String>,
|
||||||
|
data: web::Data<AppState>,
|
||||||
|
telemetry_message: web::Json<TelemetryMessageFromDevice>,
|
||||||
|
) -> impl Responder {
|
||||||
|
info!("POST - telementry - Processing device id {}", device_id);
|
||||||
|
let Ok(mac_converted) = parse_mac_address(&device_id) else {
|
||||||
|
return HttpResponse::InternalServerError();
|
||||||
|
};
|
||||||
|
let mac_converted = MacAddress::from(mac_converted);
|
||||||
|
match data.db.create_device_if_not_exists(&mac_converted).await {
|
||||||
|
Ok(_) => {}
|
||||||
|
Err(e) => {
|
||||||
|
error!("Error creating new device: {}", e);
|
||||||
|
return HttpResponse::InternalServerError();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
match data
|
||||||
|
.db
|
||||||
|
.add_telemetry(&telemetry_message, &mac_converted)
|
||||||
|
.await
|
||||||
|
{
|
||||||
|
Ok(_) => HttpResponse::Created(),
|
||||||
|
Err(e) => {
|
||||||
|
error!("adding Telemetry message to DB failed \n{}", e);
|
||||||
|
HttpResponse::InternalServerError()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[get("/telemetry/{device_id}")]
|
||||||
|
async fn get_telemetry(device_id: web::Path<String>, data: web::Data<AppState>) -> impl Responder {
|
||||||
|
info!("GET - telementry - Processing device id {}", device_id);
|
||||||
|
let Ok(mac_converted) = parse_mac_address(&device_id) else {
|
||||||
|
return HttpResponse::InternalServerError().finish();
|
||||||
|
};
|
||||||
|
let mac_converted = MacAddress::from(mac_converted);
|
||||||
|
let messages = match data.db.get_telemetry_for_id(&mac_converted).await {
|
||||||
|
Ok(msgs) => msgs,
|
||||||
|
Err(e) => {
|
||||||
|
error!("Getting Telemetry Messages from DB failed \n{}", e);
|
||||||
|
return HttpResponse::InternalServerError().finish();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
HttpResponse::Ok().json(messages)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[post("/value/{device_id}")]
|
||||||
|
async fn receive_value(
|
||||||
|
device_id: web::Path<String>,
|
||||||
|
data: web::Data<AppState>,
|
||||||
|
value_message: web::Json<ValueMessageFromDevice>,
|
||||||
|
) -> impl Responder {
|
||||||
|
info!("POST - value - Processing device id {}", device_id);
|
||||||
|
let Ok(mac_converted) = parse_mac_address(&device_id) else {
|
||||||
|
return HttpResponse::InternalServerError();
|
||||||
|
};
|
||||||
|
let mac_converted = MacAddress::from(mac_converted);
|
||||||
|
match data.db.create_device_if_not_exists(&mac_converted).await {
|
||||||
|
Ok(_) => {}
|
||||||
|
Err(e) => {
|
||||||
|
error!("Error creating new device: {}", e);
|
||||||
|
return HttpResponse::InternalServerError();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
match data.db.add_value(&value_message, &mac_converted).await {
|
||||||
|
Ok(_) => HttpResponse::Created(),
|
||||||
|
Err(e) => {
|
||||||
|
error!("adding Telemetry message to DB failed \n{}", e);
|
||||||
|
HttpResponse::InternalServerError()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[get("/value/{device_id}")]
|
||||||
|
async fn get_value(device_id: web::Path<String>, data: web::Data<AppState>) -> impl Responder {
|
||||||
|
info!("GET - value - Processing device id {}", device_id);
|
||||||
|
let Ok(mac_converted) = parse_mac_address(&device_id) else {
|
||||||
|
return HttpResponse::InternalServerError().finish();
|
||||||
|
};
|
||||||
|
let mac_converted = MacAddress::from(mac_converted);
|
||||||
|
let messages = match data.db.get_values_for_id(&mac_converted).await {
|
||||||
|
Ok(msgs) => msgs,
|
||||||
|
Err(e) => {
|
||||||
|
error!("Getting Values from DB failed \n{}", e);
|
||||||
|
return HttpResponse::InternalServerError().finish();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
HttpResponse::Ok().json(messages)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[get("/device")]
|
||||||
|
async fn get_devices(data: web::Data<AppState>) -> impl Responder {
|
||||||
|
info!("GET - devices - Processing");
|
||||||
|
let devices = match data.db.get_devices().await {
|
||||||
|
Ok(devs) => devs,
|
||||||
|
Err(e) => {
|
||||||
|
error!("Getting Devices from DB failed \n{}", e);
|
||||||
|
return HttpResponse::InternalServerError().finish();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
HttpResponse::Ok().json(devices)
|
||||||
|
}
|
||||||
86
src/firmware_api.rs
Normal file
86
src/firmware_api.rs
Normal file
@@ -0,0 +1,86 @@
|
|||||||
|
use std::{fs, path::PathBuf};
|
||||||
|
|
||||||
|
use actix_web::{get, put, web, HttpResponse, Responder};
|
||||||
|
use log::{debug, info, warn};
|
||||||
|
|
||||||
|
use crate::{schemas::{AppState, OTAConfigurationList}, util::get_files};
|
||||||
|
|
||||||
|
// Upload Firmware file
|
||||||
|
#[put("/firmware/{device}/{config}/{version}")]
|
||||||
|
async fn upload_firmware(
|
||||||
|
data: web::Data<AppState>,
|
||||||
|
path: web::Path<(String, String, String)>,
|
||||||
|
body: web::Bytes,
|
||||||
|
) -> impl Responder {
|
||||||
|
let (device, config, version) = path.into_inner();
|
||||||
|
let version = version.replace(".", "-");
|
||||||
|
|
||||||
|
let firmware_root_path = &data.firmwares_path;
|
||||||
|
|
||||||
|
let firmware_folder = firmware_root_path.join(&device);
|
||||||
|
let firmware_path = firmware_folder
|
||||||
|
.join(format!("firmware_{config}_{version}"))
|
||||||
|
.with_extension("bin");
|
||||||
|
|
||||||
|
if firmware_path.is_file() {
|
||||||
|
warn!("Firmware with product: {device}, config: {config} and version: {version} at path {firmware_path:?} already exists, cant upload");
|
||||||
|
return HttpResponse::Conflict().body(format!("{firmware_path:?}"));
|
||||||
|
}
|
||||||
|
|
||||||
|
info!("Uploading firmware with product: {device}, config: {config} and version: {version} to {firmware_path:?}");
|
||||||
|
|
||||||
|
fs::create_dir_all(&firmware_folder).unwrap();
|
||||||
|
let x = tokio::fs::write(&firmware_path, &body).await;
|
||||||
|
debug!("{x:?}");
|
||||||
|
|
||||||
|
HttpResponse::Ok().body(format!(
|
||||||
|
"Firmware version {} uploaded successfully",
|
||||||
|
version
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[get("/firmware/{device}")]
|
||||||
|
async fn get_firmware_json(data: web::Data<AppState>, path: web::Path<String>) -> impl Responder {
|
||||||
|
let device = path.into_inner();
|
||||||
|
let fw_path = &data.firmwares_path.join(device);
|
||||||
|
if fw_path.is_dir() {
|
||||||
|
match get_files(fw_path, &data.hostname) {
|
||||||
|
Ok(cfg) => HttpResponse::Ok().json(OTAConfigurationList {
|
||||||
|
configurations: cfg,
|
||||||
|
}),
|
||||||
|
Err(e) => HttpResponse::InternalServerError().body(e.to_string()),
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
HttpResponse::Ok().json(OTAConfigurationList {
|
||||||
|
configurations: vec![],
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[get("/firmware/{product}/{config}/{version}.bin")]
|
||||||
|
async fn serve_firmware(
|
||||||
|
path: web::Path<(String, String, String)>,
|
||||||
|
data: web::Data<AppState>,
|
||||||
|
) -> impl Responder {
|
||||||
|
let (product, config, version) = path.into_inner();
|
||||||
|
let version = version.replace(".", "-").replace("_", "-");
|
||||||
|
let fw_root_path = &data.firmwares_path;
|
||||||
|
let file_path = PathBuf::from(format!("{product}/firmware_{config}_{version}.bin"));
|
||||||
|
let file_path = fw_root_path.join(&file_path);
|
||||||
|
|
||||||
|
info!("Requested firmware for product: {product}, config: {config} and version: {version}, expected to be stored at {file_path:?}");
|
||||||
|
|
||||||
|
if file_path.exists() {
|
||||||
|
info!("File exists, serving download now");
|
||||||
|
HttpResponse::Ok()
|
||||||
|
.content_type("application/octet-stream")
|
||||||
|
.insert_header((
|
||||||
|
"Content-Disposition",
|
||||||
|
format!("attachment; filename=\"{product}_{config}_{version}.bin\""),
|
||||||
|
))
|
||||||
|
.body(std::fs::read(file_path).unwrap_or_else(|_| Vec::new()))
|
||||||
|
} else {
|
||||||
|
warn!("File does not exist");
|
||||||
|
HttpResponse::NotFound().body("Firmware version not found")
|
||||||
|
}
|
||||||
|
}
|
||||||
221
src/main.rs
221
src/main.rs
@@ -1,210 +1,17 @@
|
|||||||
use std::{env, fs, path::PathBuf, process};
|
use std::{env, path::PathBuf, process};
|
||||||
|
|
||||||
use crate::schemas::{TelemetryMessageFromDevice, ValueMessageFromDevice};
|
use actix_web::{web, App, HttpServer};
|
||||||
use actix_web::{get, post, put, web, App, HttpResponse, HttpServer, Responder};
|
|
||||||
use database::Database;
|
use database::Database;
|
||||||
use dotenvy::dotenv;
|
use dotenvy::dotenv;
|
||||||
use log::{debug, error, info, warn};
|
use log::{error, info};
|
||||||
use schemas::OTAConfigurationList;
|
use schemas::AppState;
|
||||||
use sqlx::types::mac_address::MacAddress;
|
|
||||||
use util::{get_files, parse_mac_address};
|
|
||||||
|
|
||||||
mod database;
|
mod database;
|
||||||
mod schemas;
|
mod schemas;
|
||||||
mod util;
|
mod util;
|
||||||
|
mod firmware_api;
|
||||||
|
mod device_telemetry_api;
|
||||||
|
|
||||||
struct AppState {
|
|
||||||
db: Database,
|
|
||||||
firmwares_path: PathBuf,
|
|
||||||
hostname: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[post("/telemetry/{device_id}")]
|
|
||||||
async fn receive_telemetry(
|
|
||||||
device_id: web::Path<String>,
|
|
||||||
data: web::Data<AppState>,
|
|
||||||
telemetry_message: web::Json<TelemetryMessageFromDevice>,
|
|
||||||
) -> impl Responder {
|
|
||||||
info!("POST - telementry - Processing device id {}", device_id);
|
|
||||||
let Ok(mac_converted) = parse_mac_address(&device_id) else {
|
|
||||||
return HttpResponse::InternalServerError();
|
|
||||||
};
|
|
||||||
let mac_converted = MacAddress::from(mac_converted);
|
|
||||||
match data.db.create_device_if_not_exists(&mac_converted).await {
|
|
||||||
Ok(_) => {}
|
|
||||||
Err(e) => {
|
|
||||||
error!("Error creating new device: {}", e);
|
|
||||||
return HttpResponse::InternalServerError();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
match data
|
|
||||||
.db
|
|
||||||
.add_telemetry(&telemetry_message, &mac_converted)
|
|
||||||
.await
|
|
||||||
{
|
|
||||||
Ok(_) => HttpResponse::Created(),
|
|
||||||
Err(e) => {
|
|
||||||
error!("adding Telemetry message to DB failed \n{}", e);
|
|
||||||
HttpResponse::InternalServerError()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[get("/telemetry/{device_id}")]
|
|
||||||
async fn get_telemetry(device_id: web::Path<String>, data: web::Data<AppState>) -> impl Responder {
|
|
||||||
info!("GET - telementry - Processing device id {}", device_id);
|
|
||||||
let Ok(mac_converted) = parse_mac_address(&device_id) else {
|
|
||||||
return HttpResponse::InternalServerError().finish();
|
|
||||||
};
|
|
||||||
let mac_converted = MacAddress::from(mac_converted);
|
|
||||||
let messages = match data.db.get_telemetry_for_id(&mac_converted).await {
|
|
||||||
Ok(msgs) => msgs,
|
|
||||||
Err(e) => {
|
|
||||||
error!("Getting Telemetry Messages from DB failed \n{}", e);
|
|
||||||
return HttpResponse::InternalServerError().finish();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
HttpResponse::Ok().json(messages)
|
|
||||||
}
|
|
||||||
|
|
||||||
#[post("/value/{device_id}")]
|
|
||||||
async fn receive_value(
|
|
||||||
device_id: web::Path<String>,
|
|
||||||
data: web::Data<AppState>,
|
|
||||||
value_message: web::Json<ValueMessageFromDevice>,
|
|
||||||
) -> impl Responder {
|
|
||||||
info!("POST - value - Processing device id {}", device_id);
|
|
||||||
let Ok(mac_converted) = parse_mac_address(&device_id) else {
|
|
||||||
return HttpResponse::InternalServerError();
|
|
||||||
};
|
|
||||||
let mac_converted = MacAddress::from(mac_converted);
|
|
||||||
match data.db.create_device_if_not_exists(&mac_converted).await {
|
|
||||||
Ok(_) => {}
|
|
||||||
Err(e) => {
|
|
||||||
error!("Error creating new device: {}", e);
|
|
||||||
return HttpResponse::InternalServerError();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
match data.db.add_value(&value_message, &mac_converted).await {
|
|
||||||
Ok(_) => HttpResponse::Created(),
|
|
||||||
Err(e) => {
|
|
||||||
error!("adding Telemetry message to DB failed \n{}", e);
|
|
||||||
HttpResponse::InternalServerError()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[get("/value/{device_id}")]
|
|
||||||
async fn get_value(device_id: web::Path<String>, data: web::Data<AppState>) -> impl Responder {
|
|
||||||
info!("GET - value - Processing device id {}", device_id);
|
|
||||||
let Ok(mac_converted) = parse_mac_address(&device_id) else {
|
|
||||||
return HttpResponse::InternalServerError().finish();
|
|
||||||
};
|
|
||||||
let mac_converted = MacAddress::from(mac_converted);
|
|
||||||
let messages = match data.db.get_values_for_id(&mac_converted).await {
|
|
||||||
Ok(msgs) => msgs,
|
|
||||||
Err(e) => {
|
|
||||||
error!("Getting Values from DB failed \n{}", e);
|
|
||||||
return HttpResponse::InternalServerError().finish();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
HttpResponse::Ok().json(messages)
|
|
||||||
}
|
|
||||||
|
|
||||||
#[get("/device")]
|
|
||||||
async fn get_devices(data: web::Data<AppState>) -> impl Responder {
|
|
||||||
info!("GET - devices - Processing");
|
|
||||||
let devices = match data.db.get_devices().await {
|
|
||||||
Ok(devs) => devs,
|
|
||||||
Err(e) => {
|
|
||||||
error!("Getting Devices from DB failed \n{}", e);
|
|
||||||
return HttpResponse::InternalServerError().finish();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
HttpResponse::Ok().json(devices)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Upload Firmware file
|
|
||||||
#[put("/firmware/{device}/{config}/{version}")]
|
|
||||||
async fn upload_firmware(
|
|
||||||
data: web::Data<AppState>,
|
|
||||||
path: web::Path<(String, String, String)>,
|
|
||||||
body: web::Bytes,
|
|
||||||
) -> impl Responder {
|
|
||||||
let (device, config, version) = path.into_inner();
|
|
||||||
let version = version.replace(".", "-");
|
|
||||||
|
|
||||||
let firmware_root_path = &data.firmwares_path;
|
|
||||||
|
|
||||||
let firmware_folder = firmware_root_path.join(&device);
|
|
||||||
let firmware_path = firmware_folder
|
|
||||||
.join(format!("firmware_{config}_{version}"))
|
|
||||||
.with_extension("bin");
|
|
||||||
|
|
||||||
if firmware_path.is_file() {
|
|
||||||
warn!("Firmware with product: {device}, config: {config} and version: {version} at path {firmware_path:?} already exists, cant upload");
|
|
||||||
return HttpResponse::Conflict().body(format!("{firmware_path:?}"));
|
|
||||||
}
|
|
||||||
|
|
||||||
info!("Uploading firmware with product: {device}, config: {config} and version: {version} to {firmware_path:?}");
|
|
||||||
|
|
||||||
fs::create_dir_all(&firmware_folder).unwrap();
|
|
||||||
let x = tokio::fs::write(&firmware_path, &body).await;
|
|
||||||
debug!("{x:?}");
|
|
||||||
|
|
||||||
HttpResponse::Ok().body(format!(
|
|
||||||
"Firmware version {} uploaded successfully",
|
|
||||||
version
|
|
||||||
))
|
|
||||||
}
|
|
||||||
|
|
||||||
#[get("/firmware/{device}")]
|
|
||||||
async fn get_firmware_json(data: web::Data<AppState>, path: web::Path<String>) -> impl Responder {
|
|
||||||
let device = path.into_inner();
|
|
||||||
let fw_path = &data.firmwares_path.join(device);
|
|
||||||
if fw_path.is_dir() {
|
|
||||||
match get_files(fw_path, &data.hostname) {
|
|
||||||
Ok(cfg) => HttpResponse::Ok().json(OTAConfigurationList {
|
|
||||||
configurations: cfg,
|
|
||||||
}),
|
|
||||||
Err(e) => HttpResponse::InternalServerError().body(e.to_string()),
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
HttpResponse::Ok().json(OTAConfigurationList {
|
|
||||||
configurations: vec![],
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[get("/firmware/{product}/{config}/{version}.bin")]
|
|
||||||
async fn serve_firmware(
|
|
||||||
path: web::Path<(String, String, String)>,
|
|
||||||
data: web::Data<AppState>,
|
|
||||||
) -> impl Responder {
|
|
||||||
let (product, config, version) = path.into_inner();
|
|
||||||
let version = version.replace(".", "-").replace("_", "-");
|
|
||||||
let fw_root_path = &data.firmwares_path;
|
|
||||||
let file_path = PathBuf::from(format!("{product}/firmware_{config}_{version}.bin"));
|
|
||||||
let file_path = fw_root_path.join(&file_path);
|
|
||||||
|
|
||||||
info!("Requested firmware for product: {product}, config: {config} and version: {version}, expected to be stored at {file_path:?}");
|
|
||||||
|
|
||||||
if file_path.exists() {
|
|
||||||
info!("File exists, serving download now");
|
|
||||||
HttpResponse::Ok()
|
|
||||||
.content_type("application/octet-stream")
|
|
||||||
.insert_header((
|
|
||||||
"Content-Disposition",
|
|
||||||
format!("attachment; filename=\"{product}_{config}_{version}.bin\""),
|
|
||||||
))
|
|
||||||
.body(std::fs::read(file_path).unwrap_or_else(|_| Vec::new()))
|
|
||||||
} else {
|
|
||||||
warn!("File does not exist");
|
|
||||||
HttpResponse::NotFound().body("Firmware version not found")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[actix_web::main]
|
#[actix_web::main]
|
||||||
async fn main() -> std::io::Result<()> {
|
async fn main() -> std::io::Result<()> {
|
||||||
@@ -232,14 +39,14 @@ async fn main() -> std::io::Result<()> {
|
|||||||
hostname: "127.0.0.1:8282".to_string(),
|
hostname: "127.0.0.1:8282".to_string(),
|
||||||
}))
|
}))
|
||||||
.app_data(web::PayloadConfig::new(256 * 1024 * 1024)) //256MB
|
.app_data(web::PayloadConfig::new(256 * 1024 * 1024)) //256MB
|
||||||
.service(receive_telemetry)
|
.service(device_telemetry_api::receive_telemetry)
|
||||||
.service(get_telemetry)
|
.service(device_telemetry_api::get_telemetry)
|
||||||
.service(receive_value)
|
.service(device_telemetry_api::receive_value)
|
||||||
.service(get_value)
|
.service(device_telemetry_api::get_value)
|
||||||
.service(get_devices)
|
.service(device_telemetry_api::get_devices)
|
||||||
.service(upload_firmware)
|
.service(firmware_api::upload_firmware)
|
||||||
.service(get_firmware_json)
|
.service(firmware_api::get_firmware_json)
|
||||||
.service(serve_firmware)
|
.service(firmware_api::serve_firmware)
|
||||||
})
|
})
|
||||||
.bind(("0.0.0.0", 8282))?
|
.bind(("0.0.0.0", 8282))?
|
||||||
.run()
|
.run()
|
||||||
|
|||||||
@@ -1,8 +1,12 @@
|
|||||||
|
use std::path::PathBuf;
|
||||||
|
|
||||||
use chrono::NaiveDateTime;
|
use chrono::NaiveDateTime;
|
||||||
use serde::{ser::SerializeStruct, Deserialize, Serialize};
|
use serde::{ser::SerializeStruct, Deserialize, Serialize};
|
||||||
use sqlx::types::mac_address::MacAddress;
|
use sqlx::types::mac_address::MacAddress;
|
||||||
use strum::{Display, EnumString};
|
use strum::{Display, EnumString};
|
||||||
|
|
||||||
|
use crate::database::Database;
|
||||||
|
|
||||||
#[derive(Deserialize, Debug, Serialize)]
|
#[derive(Deserialize, Debug, Serialize)]
|
||||||
pub struct TelemetryMessage {
|
pub struct TelemetryMessage {
|
||||||
pub uptime: i32,
|
pub uptime: i32,
|
||||||
@@ -89,3 +93,9 @@ pub enum BoardConfig {
|
|||||||
INA226,
|
INA226,
|
||||||
INA233,
|
INA233,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub struct AppState {
|
||||||
|
pub db: Database,
|
||||||
|
pub firmwares_path: PathBuf,
|
||||||
|
pub hostname: String,
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user