This commit is contained in:
3
.env
3
.env
@@ -1,2 +1,3 @@
|
|||||||
DATABASE_URL=postgres://dev:dev@db/iot
|
DATABASE_URL=postgres://dev:dev@db/iot
|
||||||
RUST_LOG=debug
|
RUST_LOG=debug
|
||||||
|
FIRMWARE_FOLDER=/temp/firmware
|
||||||
@@ -9,7 +9,6 @@ edition = "2021"
|
|||||||
actix-web = "4.4.0"
|
actix-web = "4.4.0"
|
||||||
chrono = { version = "0.4.31", features = ["serde"] }
|
chrono = { version = "0.4.31", features = ["serde"] }
|
||||||
dotenvy = "0.15"
|
dotenvy = "0.15"
|
||||||
enum_stringify = "0.6.1"
|
|
||||||
env_logger = "0.11"
|
env_logger = "0.11"
|
||||||
log = "0.4.20"
|
log = "0.4.20"
|
||||||
serde = "1.0.188"
|
serde = "1.0.188"
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ meta {
|
|||||||
}
|
}
|
||||||
|
|
||||||
get {
|
get {
|
||||||
url: http://localhost:8484/firmware/waterlevel
|
url: http://localhost:8282/firmware/waterlevel
|
||||||
body: multipartForm
|
body: multipartForm
|
||||||
auth: none
|
auth: none
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,11 +5,11 @@ meta {
|
|||||||
}
|
}
|
||||||
|
|
||||||
put {
|
put {
|
||||||
url: http://localhost:8484/firmware/waterlevel/INA226/0.0.1
|
url: http://localhost:8282/firmware/waterlevel/INA233/1.0.0
|
||||||
body: multipartForm
|
body: multipartForm
|
||||||
auth: none
|
auth: none
|
||||||
}
|
}
|
||||||
|
|
||||||
body:multipart-form {
|
body:multipart-form {
|
||||||
: @file(/home/tobi/git/iot-cloud-api/target/debug/iot-cloud)
|
: @file(/home/tobi/git/waterlevel-software/.pio/build/ESP32/firmware.bin)
|
||||||
}
|
}
|
||||||
|
|||||||
75
src/main.rs
75
src/main.rs
@@ -1,11 +1,11 @@
|
|||||||
use std::{env, fs, process, str::FromStr};
|
use std::{env, fs, path::PathBuf, process, str::FromStr};
|
||||||
|
|
||||||
use crate::schemas::{TelemetryMessageFromDevice, ValueMessageFromDevice};
|
use crate::schemas::{TelemetryMessageFromDevice, ValueMessageFromDevice};
|
||||||
use actix_web::{get, post, put, web, App, HttpResponse, HttpServer, Responder};
|
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::{error, info};
|
use log::{debug, error, info};
|
||||||
use schemas::{BoardConfig, BoardType, Device, OTAConfiguration};
|
use schemas::{BoardConfig, BoardType, Device, OTAConfiguration, OTAConfigurationList};
|
||||||
use sqlx::types::mac_address::MacAddress;
|
use sqlx::types::mac_address::MacAddress;
|
||||||
use util::parse_mac_address;
|
use util::parse_mac_address;
|
||||||
|
|
||||||
@@ -127,43 +127,49 @@ async fn get_devices(data: web::Data<AppState>) -> impl Responder {
|
|||||||
#[put("/firmware/{product}/{config}/{version}")]
|
#[put("/firmware/{product}/{config}/{version}")]
|
||||||
async fn upload_firmware(path: web::Path<(String, String, String)>, body: web::Bytes) -> impl Responder {
|
async fn upload_firmware(path: web::Path<(String, String, String)>, body: web::Bytes) -> impl Responder {
|
||||||
let (product, config, version) = path.into_inner();
|
let (product, config, version) = path.into_inner();
|
||||||
println!("Uploading firmware version: {}", version);
|
let version = version.replace(".", "_");
|
||||||
|
let firmware_root_path = PathBuf::from(env::var("FIMRWARE_FOLDER").unwrap_or("./firmware".to_string()));
|
||||||
|
|
||||||
let fw_folder = format!("./firmware/{product}");
|
let firmware_folder = firmware_root_path.join(&product).join(&config);
|
||||||
|
let firmware_path = firmware_folder.join(format!("ver_{}", &version)).with_extension("bin");
|
||||||
|
|
||||||
|
info!("Uploading firmware with product: {product}, config: {config} and version: {version} to {firmware_path:?}");
|
||||||
|
|
||||||
fs::create_dir_all(&fw_folder).unwrap();
|
fs::create_dir_all(&firmware_folder).unwrap();
|
||||||
let file_path = format!("{fw_folder}/firmware_{config}_{version}.bin");
|
let x = tokio::fs::write(&firmware_path, &body).await;
|
||||||
info!("Saving to {file_path}");
|
debug!("{x:?}");
|
||||||
tokio::fs::write(&file_path, &body).await.unwrap();
|
|
||||||
|
|
||||||
HttpResponse::Ok().body(format!("Firmware version {} uploaded successfully", version))
|
HttpResponse::Ok().body(format!("Firmware version {} uploaded successfully", version))
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO this is more or less a placeholder. Firmware upload will be more detailed in the future
|
#[get("/firmware/{product}/{config}/{version}")]
|
||||||
#[get("/firmware/{product}")]
|
async fn get_firmware_json(path: web::Path<(String, String, String)>) -> impl Responder {
|
||||||
async fn get_firmware_json(product: web::Path<String>) -> impl Responder {
|
let (product, config, version) = path.into_inner();
|
||||||
let product = product.into_inner();
|
let version = version.replace(".", "_");
|
||||||
let mut configs = Vec::new();
|
|
||||||
if let Ok(entries) = fs::read_dir(format!("./firmware/{product}")) {
|
|
||||||
for entry in entries.flatten() {
|
|
||||||
let path = entry.path();
|
|
||||||
if path.is_file() {
|
|
||||||
println!("File: {:?}", path);
|
|
||||||
let split_name: Vec<_> = path.file_name().unwrap().to_str().unwrap().split("_").collect();
|
|
||||||
let version = split_name[2].strip_suffix(".bin").unwrap();
|
|
||||||
let board_config = BoardConfig::from_str(split_name[1]).unwrap();
|
|
||||||
let board_type = BoardType::from_str(&product).unwrap();
|
|
||||||
let cfg = OTAConfiguration{board: board_type, configuration: board_config, version: version.to_string(), url: path.to_str().to_owned().unwrap().to_owned() };
|
|
||||||
configs.push(cfg);
|
|
||||||
} else if path.is_dir() {
|
|
||||||
println!("Directory: {:?}", path);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
return HttpResponse::InternalServerError().finish()
|
|
||||||
}
|
|
||||||
|
|
||||||
HttpResponse::Ok().json(configs)
|
|
||||||
|
let mut configs = Vec::new();
|
||||||
|
|
||||||
|
|
||||||
|
HttpResponse::Ok().json(OTAConfigurationList{configurations: configs} )
|
||||||
|
}
|
||||||
|
|
||||||
|
#[get("/firmware/waterlevel/firmware_INA233_1.0.0.bin")]
|
||||||
|
async fn serve_firmware() -> impl Responder {
|
||||||
|
let file_path = PathBuf::from("./firmware/waterlevel/firmware_INA233_1.0.0.bin");
|
||||||
|
|
||||||
|
if file_path.exists() {
|
||||||
|
// Serve the file as a download
|
||||||
|
HttpResponse::Ok()
|
||||||
|
.content_type("application/octet-stream") // Binary file MIME type
|
||||||
|
.insert_header((
|
||||||
|
"Content-Disposition",
|
||||||
|
format!("attachment; filename=\"{}\"", "firmware_INA233_1.0.0.bin"),
|
||||||
|
))
|
||||||
|
.body(std::fs::read(file_path).unwrap_or_else(|_| Vec::new()))
|
||||||
|
} else {
|
||||||
|
HttpResponse::NotFound().body("Firmware version not found")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[actix_web::main]
|
#[actix_web::main]
|
||||||
@@ -195,8 +201,9 @@ async fn main() -> std::io::Result<()> {
|
|||||||
.service(get_devices)
|
.service(get_devices)
|
||||||
.service(upload_firmware)
|
.service(upload_firmware)
|
||||||
.service(get_firmware_json)
|
.service(get_firmware_json)
|
||||||
|
.service(serve_firmware)
|
||||||
})
|
})
|
||||||
.bind(("0.0.0.0", 8484))?
|
.bind(("0.0.0.0", 8282))?
|
||||||
.run()
|
.run()
|
||||||
.await
|
.await
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,8 +1,7 @@
|
|||||||
use chrono::NaiveDateTime;
|
use chrono::NaiveDateTime;
|
||||||
use enum_stringify::EnumStringify;
|
|
||||||
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::EnumString;
|
use strum::{Display, EnumString};
|
||||||
|
|
||||||
#[derive(Deserialize, Debug, Serialize)]
|
#[derive(Deserialize, Debug, Serialize)]
|
||||||
pub struct TelemetryMessage {
|
pub struct TelemetryMessage {
|
||||||
@@ -65,21 +64,29 @@ impl Serialize for Device {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[derive(serde::Serialize)]
|
#[derive(serde::Serialize)]
|
||||||
pub struct OTAConfiguration {
|
#[serde(rename_all = "PascalCase")]
|
||||||
pub board: BoardType,
|
pub struct OTAConfigurationList {
|
||||||
pub configuration: BoardConfig,
|
pub configurations: Vec<OTAConfiguration>
|
||||||
pub version: String,
|
|
||||||
pub url: String
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(serde::Serialize, EnumString)]
|
#[derive(serde::Serialize, PartialEq, Debug)]
|
||||||
|
#[serde(rename_all = "PascalCase")]
|
||||||
|
pub struct OTAConfiguration {
|
||||||
|
pub version: String,
|
||||||
|
pub url: String,
|
||||||
|
pub board: Option<BoardType>,
|
||||||
|
pub config: Option<BoardConfig>
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
#[derive(serde::Serialize, EnumString, PartialEq, Debug, Display)]
|
||||||
|
#[strum(serialize_all = "snake_case")]
|
||||||
pub enum BoardType {
|
pub enum BoardType {
|
||||||
Waterlevel
|
Waterlevel
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(serde::Serialize, EnumString)]
|
#[derive(serde::Serialize, EnumString, PartialEq, Debug, Display)]
|
||||||
pub enum BoardConfig {
|
pub enum BoardConfig {
|
||||||
INA226,
|
INA226,
|
||||||
INA233
|
INA233
|
||||||
}
|
}
|
||||||
|
|||||||
63
src/util.rs
63
src/util.rs
@@ -1,4 +1,15 @@
|
|||||||
|
use std::{fs, path::PathBuf};
|
||||||
|
|
||||||
|
use log::info;
|
||||||
|
use strum::EnumString;
|
||||||
use thiserror::Error;
|
use thiserror::Error;
|
||||||
|
use serde;
|
||||||
|
use std::str::FromStr;
|
||||||
|
|
||||||
|
|
||||||
|
use crate::schemas::{BoardConfig, BoardType, OTAConfiguration};
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
pub fn parse_mac_address(mac: &str) -> Result<[u8; 6], MacAddressError> {
|
pub fn parse_mac_address(mac: &str) -> Result<[u8; 6], MacAddressError> {
|
||||||
if mac.len() != 12 {
|
if mac.len() != 12 {
|
||||||
@@ -22,10 +33,62 @@ pub enum MacAddressError {
|
|||||||
Length(usize),
|
Length(usize),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Error, Debug)]
|
||||||
|
pub enum GetFilesError {
|
||||||
|
#[error("IO Error while reading files")]
|
||||||
|
IO(#[from] std::io::Error),
|
||||||
|
#[error("Error getting filename")]
|
||||||
|
Filename,
|
||||||
|
#[error("Error getting extension (.bin)")]
|
||||||
|
Extension,
|
||||||
|
#[error("Strum parse Error")]
|
||||||
|
Conversion(#[from] strum::ParseError)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_files(root_path: &PathBuf, hostname: &str) -> Result<Vec<OTAConfiguration>, GetFilesError>{
|
||||||
|
info!("Getting all files from path {root_path:?}");
|
||||||
|
let mut configs = Vec::new();
|
||||||
|
let product_name = root_path.file_name().ok_or(GetFilesError::Filename)?.to_string_lossy();
|
||||||
|
let entries = fs::read_dir(root_path)?;
|
||||||
|
|
||||||
|
for entry in entries.flatten() {
|
||||||
|
let path = entry.path();
|
||||||
|
if path.is_file() {
|
||||||
|
println!("File: {:?}", path);
|
||||||
|
// Splits the filename at the underscores. This is safe to do as names get sanitized on upload and are only uploaded by the pipeline
|
||||||
|
let split_name: Vec<_> = path.file_name().ok_or(GetFilesError::Filename)?.to_str().ok_or(GetFilesError::Filename)?.split("_").collect();
|
||||||
|
let version = split_name[2].strip_suffix(".bin").ok_or(GetFilesError::Extension)?;
|
||||||
|
let board_config = BoardConfig::from_str(split_name[1])?;
|
||||||
|
let board_type = BoardType::from_str(&product_name).unwrap();
|
||||||
|
let version_replaced = version.replace(".", "_");
|
||||||
|
let fw_url = format!("{hostname}/firmware/{board_type}/{board_config}/{version_replaced}.bin");
|
||||||
|
let cfg = OTAConfiguration{version: version.to_string(), url: fw_url, board: Some(board_type), config: Some(board_config) };
|
||||||
|
configs.push(cfg);
|
||||||
|
} else if path.is_dir() {
|
||||||
|
println!("Directory: {:?}", path);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(configs)
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_file_loading() {
|
||||||
|
let expected_1 = OTAConfiguration{ version: "1.2.3".to_string(), url: "example.com/firmware/waterlevel/INA233/1_2_3.bin".to_string(), board: Some(BoardType::Waterlevel), config: Some(BoardConfig::INA233) };
|
||||||
|
let expected_2 = OTAConfiguration{ version: "4.5.6".to_string(), url: "example.com/firmware/waterlevel/INA226/4_5_6.bin".to_string(), board: Some(BoardType::Waterlevel), config: Some(BoardConfig::INA226) };
|
||||||
|
let loaded_configs = get_files(&PathBuf::from("./test/waterlevel"), "example.com").unwrap();
|
||||||
|
|
||||||
|
assert_eq!(loaded_configs[0], expected_1);
|
||||||
|
assert_eq!(loaded_configs[1], expected_2);
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_valid_mac_address_plain() {
|
fn test_valid_mac_address_plain() {
|
||||||
let mac_str = "001A2B3C4D5E";
|
let mac_str = "001A2B3C4D5E";
|
||||||
|
|||||||
Reference in New Issue
Block a user