Compare commits

...

29 commits

Author SHA1 Message Date
MikaPikaDerZerstoerer 8c32cf1a3d README.md aktualisiert 2024-12-17 11:23:24 +01:00
Mika Bomm 9c38779205 display uptime in seconds 2024-10-15 10:53:05 +02:00
mikailkilli 9010b5d81e added timed refresh 2024-10-15 10:52:06 +02:00
mikailkilli c355bc8f7d updated files for correct data getting 2024-10-15 09:56:15 +02:00
Mika Bomm fd98afafb6 Update docs/SETUP.md 2024-10-14 23:24:55 +02:00
Mika Bomm 6c7bf0223b Update docs/SETUP.md 2024-10-14 23:24:04 +02:00
Mikail Killi b2f6f216f6 Added axios requests to show the nodes from the database, added calculator for battery level and input field for la and lo 2024-10-14 20:20:49 +02:00
Mika Bomm fe65349b54 update endpoint 2024-10-14 18:08:38 +02:00
Mikail Killi abdc45a799 Updated web interface with axios requests, additonal functions and styling 2024-10-14 17:23:30 +02:00
Mika Bomm e7436622f8 add documentation for embadet 2024-10-14 09:20:17 +02:00
Conner ef3e253a12 ApfelStein lebt 2024-10-14 01:19:50 +02:00
Conner 55102027c1 frankensteins monster 2024-10-14 00:31:43 +02:00
Mika Bomm e3e1c4a5b9 fix warnings 2024-10-14 00:16:29 +02:00
Mika Bomm fcf449ec8b fix node 2024-10-14 00:10:32 +02:00
Mika Bomm bc8a300305 automatically save data to database 2024-10-14 00:08:02 +02:00
Conner aab15384d3 tcp working 2024-10-13 23:35:17 +02:00
Mika Bomm de4d5448de small fix for README 2024-10-13 22:54:44 +02:00
Mika Bomm 87416e5af9 added loop to listener 2024-10-13 22:44:19 +02:00
Mika Bomm 38a0f4d189 change database structure ... again... 2024-10-13 22:44:19 +02:00
Mika Bomm a696a05595 implement mac address stuff 2024-10-13 22:44:19 +02:00
Mika Bomm ec8859b59c forgot to cd into a directory before generating entities 2024-10-13 22:44:19 +02:00
Mika Bomm a65657aea1 restructure backend and database 2024-10-13 22:44:19 +02:00
Conner c8a70b5a32 fix gitignore 2024-10-13 22:21:14 +02:00
Conner f3c8d8d9a3 typedef 2024-10-13 21:06:34 +02:00
mikailkilli 07f37b7426 added miro board link for readability purposes 2024-10-13 18:43:40 +02:00
mikailkilli 6dc9fc1af4 removed unnecessary backslashes (I suffer from the stoopid) 2024-10-13 18:39:24 +02:00
mikailkilli b10b02f117 adjusted markdown styling in the readme file 2024-10-13 18:28:10 +02:00
mikailkilli 6761423411 adjusted the readme.md file with additional information 2024-10-13 18:21:02 +02:00
Mika Bomm 2e39d9fb56 added miro sections to readme 2024-10-13 12:40:05 +02:00
38 changed files with 1381 additions and 305 deletions

4
.gitignore vendored
View file

@ -1,2 +1,4 @@
/target /target
/build /embedded/build
/.idea
.vscode

View file

@ -0,0 +1,11 @@
meta {
name: get data from last hour
type: http
seq: 1
}
get {
url: http://localhost:8080/api/v1/data
body: none
auth: none
}

View file

@ -1,7 +1,7 @@
meta { meta {
name: Create node group name: Create node group
type: http type: http
seq: 3 seq: 1
} }
post { post {

View file

@ -12,9 +12,11 @@ post {
body:json { body:json {
{ {
"name":"some mac address", "id":"04-7c-16-06-b3-53",
"coord_la":1.123123, "coord_la":1,
"coord_lo":5.3123123, "coord_lo":2,
"group":"efbd70a9-dc89-4c8d-9e6c-e7607c823df3" "battery_minimum":3,
"battery_maximum":4,
"group":"54eccfb5-1d5a-4cad-a1a2-468eca68ffd6"
} }
} }

View file

@ -1,7 +1,7 @@
meta { meta {
name: delete node name: delete node
type: http type: http
seq: 4 seq: 3
} }
delete { delete {

View file

@ -0,0 +1,25 @@
meta {
name: update node
type: http
seq: 4
}
put {
url: http://localhost:8080/api/v1/nodes/:id
body: json
auth: none
}
params:path {
id: 04-7c-16-06-b3-53
}
body:json {
{
"coord_la":1,
"coord_lo":2,
"battery_minimum":99,
"battery_maximum":9,
"group":"32e0f9af-867b-4888-8261-cb99dfff5675"
}
}

63
Cargo.lock generated
View file

@ -543,12 +543,16 @@ dependencies = [
"actix-cors", "actix-cors",
"actix-web", "actix-web",
"argon2", "argon2",
"chrono",
"deku",
"dotenvy", "dotenvy",
"entity", "entity",
"eui48",
"futures", "futures",
"jsonwebtoken", "jsonwebtoken",
"sea-orm", "sea-orm",
"serde", "serde",
"tokio",
"uuid", "uuid",
] ]
@ -773,8 +777,10 @@ checksum = "a21f936df1771bf62b77f047b726c4625ff2e8aa607c01ec06e5a05bd8463401"
dependencies = [ dependencies = [
"android-tzdata", "android-tzdata",
"iana-time-zone", "iana-time-zone",
"js-sys",
"num-traits", "num-traits",
"serde", "serde",
"wasm-bindgen",
"windows-targets 0.52.6", "windows-targets 0.52.6",
] ]
@ -950,6 +956,7 @@ dependencies = [
"ident_case", "ident_case",
"proc-macro2", "proc-macro2",
"quote", "quote",
"strsim",
"syn 2.0.79", "syn 2.0.79",
] ]
@ -964,6 +971,31 @@ dependencies = [
"syn 2.0.79", "syn 2.0.79",
] ]
[[package]]
name = "deku"
version = "0.18.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a9711031e209dc1306d66985363b4397d4c7b911597580340b93c9729b55f6eb"
dependencies = [
"bitvec",
"deku_derive",
"no_std_io2",
"rustversion",
]
[[package]]
name = "deku_derive"
version = "0.18.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "58cb0719583cbe4e81fb40434ace2f0d22ccc3e39a74bb3796c22b451b4f139d"
dependencies = [
"darling",
"proc-macro-crate",
"proc-macro2",
"quote",
"syn 2.0.79",
]
[[package]] [[package]]
name = "der" name = "der"
version = "0.7.9" version = "0.7.9"
@ -1108,6 +1140,16 @@ dependencies = [
"windows-sys 0.48.0", "windows-sys 0.48.0",
] ]
[[package]]
name = "eui48"
version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "887418ac5e8d57c2e66e04bdc2fe15f9a5407be20b54a82c86bd0e368b709701"
dependencies = [
"regex",
"rustc-serialize",
]
[[package]] [[package]]
name = "event-listener" name = "event-listener"
version = "2.5.3" version = "2.5.3"
@ -1758,6 +1800,15 @@ dependencies = [
"libc", "libc",
] ]
[[package]]
name = "no_std_io2"
version = "0.8.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8a3564ce7035b1e4778d8cb6cacebb5d766b5e8fe5a75b9e441e33fb61a872c6"
dependencies = [
"memchr",
]
[[package]] [[package]]
name = "nom" name = "nom"
version = "7.1.3" version = "7.1.3"
@ -2313,6 +2364,12 @@ version = "0.1.24"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "719b953e2095829ee67db738b3bfa9fa368c94900df327b3f07fe6e794d2fe1f" checksum = "719b953e2095829ee67db738b3bfa9fa368c94900df327b3f07fe6e794d2fe1f"
[[package]]
name = "rustc-serialize"
version = "0.3.25"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fe834bc780604f4674073badbad26d7219cadfb4a2275802db12cbae17498401"
[[package]] [[package]]
name = "rustc_version" name = "rustc_version"
version = "0.4.1" version = "0.4.1"
@ -2365,6 +2422,12 @@ dependencies = [
"untrusted", "untrusted",
] ]
[[package]]
name = "rustversion"
version = "1.0.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "955d28af4278de8121b7ebeb796b6a45735dc01436d898801014aced2773a3d6"
[[package]] [[package]]
name = "ryu" name = "ryu"
version = "1.0.18" version = "1.0.18"

View file

@ -1,4 +1,90 @@
# ApfelNetzwerk # ApfelNetzwerk
[Considerations](docs/considerations.md) \ 1. [ApfelNetzwerk](#apfelnetzwerk) \
[Setup](docs/SETUP.md) 1.1 [Individual learning goal & reflection](#individual-learning-goal--reflection) \
1.2. [DIY Problem Definition](#diy-problem-definition) \
1.3. [Empathy Map](#empathy-map) \
1.4. [Walt Disney Method](#walt-disney-method) \
1.5. [Business Canvas Model](#business-canvas-model) \
1.6. [Go-Viral Post](#go-viral-post) \
1.7. [Use Case Diagram](#use-case-diagram)
2. [Additional infos about the project](#additional-infos-about-the-project) \
2.1. [Considerations](#approachesconsiderations)\
2.2. [Setup](#setup)\
2.3. [Notes](#notes)
# Apfelnetzwerk
This document contains the entire portfolio for our project, "ApfelNetzwerk." It presents a solution to the challenges faced by the "Altes Land" region in recent years.
Below, you will find individual learning goals, reflections, and important details about the project, including the methods used throughout our work.
## Individual learning goal & reflection
**Mikail Killi** \
**Goal:** To improve my documentation and communication skills while expanding my technical knowledge regarding the hardware we are documenting. \
**Reflection:** Throughout the project, I significantly improved my ability to document technical details clearly and concisely. This process helped me develop stronger communication skills, and I now feel more confident in handling technical documentation tasks.
**Niklas Wollenberg** \
**Goal:** To learn how to work with REST APIs. \
**Reflection:** By working on the frontend of our project, I gained a solid understanding of effectively using API calls and handling responses.
**Mika Bomm** \
**Goal:** To program a REST API. \
**Reflection:** Through extensive research and trial-and-error, I successfully built a fully functional REST API capable of managing all supported API calls.
**Conner Bogen** \
**Goal:** To develop and implement a functional Ad-Hoc wireless network using multiple microcontrollers, enabling efficient communication between devices without the need for a central controller. \
**Reflection:** Through hands-on experimentaion and testing, I successfully built a reliable Ad-Hoc wireless network that allowed multiple microcontrollers to communicate seamlessly. This experience greatly enhanced my understanding of network protocols and microcontroller programming.
## DIY Problem Definition
The DIY Problem Definition provides a structured overview of the issues we aim to address in our project.
![DIY Problem Definition](docs/diy-problem-definition.jpg)
## Empathy Map
The Empathy Map helps us understand the needs, thoughts, and emotions of our user, guiding is in designing solutions that truly address their concerns.
![Empathy Map](docs/empathy-map.jpg)
## Walt Disney Method
We utilized the Walt Disney method to brainstorm and evaluate ideas. This process allowed us to explore creative solutions from different perspectives aswell as improving our problem-solving approach.
https://miro.com/welcomeonboard/ZHFHc29qcksreUZOQlNmMnpVTVpmRXZ5cldXL3FOcGV3NDd1cU40MmFJempXdUVRTDlCT2EyK2FGTlRZcXpDVTdkS3JDZXV6OGR5NmEzcmczMk43ZlFrTmlxQXZsVmtTL0dERUowTGxHL2Jkb2xnWllTVWplbnB4aXJpY0FPTVghZQ==?share_link_id=164862170873
![Walt Disney](docs/walt-disney.jpg)
## Business Canvas Model
The Business Canvas Model outlines key business aspects of the project, such as key partners, key propositions, customer segments and many more.
![Business Canvas Model](docs/HACKERSOHN%20-%20Frame%204.jpg)
## Go-Viral Post
Our project is featured in a Go-Viral post, which not only provides an overview of the project but also helps spread awareness about our initiative. You can read the full post here:
[Go-Viral Post](https://www.designentrepreneurshipworkshop.org/2024/10/10/team-16-supporting-the-farmers-on-the-altes-land/)
## Use Case Diagram
The Use Case Diagram visually represents the interactions within our system. It was generated using Graphviz, with a Python library that translates code into the DOT language used by Graphviz.
![Use Case Diagram](docs/use_case_diagram.svg)
# Additional infos about the project
## Approaches/Considerations
In the following [document](docs/considerations.md), we have documented various approaches, challenges and lessons learned during the project.
## Setup
We have created a comprehensive [setup document](docs/SETUP.md) to guide you through setting up the project. This document primarily focuses on using Docker to ensure a smooth configuration process.
## Notes
You may also view our [Miro board](https://miro.com/app/board/uXjVLalWhTw=/?share_link_id=740126325996) for better readiblity.

View file

@ -18,3 +18,7 @@ sea-orm = { version = "1", features = [
dotenvy = "*" dotenvy = "*"
jsonwebtoken = "*" jsonwebtoken = "*"
futures = "*" futures = "*"
chrono = "*"
eui48 = "*"
tokio = { version = "1", features = ["full"] }
deku = "*"

View file

@ -1,45 +1,147 @@
use crate::AppState; use crate::AppState;
use actix_web::{error::ErrorInternalServerError, web, HttpResponse, Responder}; use actix_web::web::Path;
use entity::node_group; use actix_web::{
use sea_orm::{ActiveModelTrait, ActiveValue, EntityTrait}; error::{ErrorBadRequest, ErrorInternalServerError},
web, Responder,
};
use chrono::Utc;
use entity::{node, node_group, sensor_data};
use eui48::ParseError;
use sea_orm::{entity::*, query::*, ActiveModelTrait, ActiveValue, EntityTrait};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use uuid::Uuid;
#[derive(Serialize)]
struct NodeWithSensorData {
node: NodeWithMac,
sensor_data: Vec<sensor_data::Model>,
}
#[derive(Deserialize)] #[derive(Deserialize)]
pub struct CreateGroupWithoutId { pub struct CreateGroupWithoutId {
name: String, name: String,
} }
#[derive(Deserialize)] #[derive(Deserialize, Serialize)]
pub struct CreateLicense { pub struct NodeWithMac {
name: String, id: String,
coord_la: f64, coord_la: f64,
coord_lo: f64, coord_lo: f64,
battery_minimum: f64,
battery_maximum: f64,
group: uuid::Uuid, group: uuid::Uuid,
} }
#[derive(Deserialize, Serialize)]
pub struct UpdateNode {
coord_la: f64,
coord_lo: f64,
battery_minimum: f64,
battery_maximum: f64,
group: uuid::Uuid,
}
impl From<node::Model> for NodeWithMac {
fn from(value: node::Model) -> Self {
let mac = convert_id_to_mac(&value.id);
Self {
id: mac,
coord_la: value.coord_la,
coord_lo: value.coord_lo,
battery_minimum: value.battery_minimum,
battery_maximum: value.battery_maximum,
group: value.group,
}
}
}
impl TryInto<node::Model> for NodeWithMac {
type Error = eui48::ParseError;
fn try_into(self) -> Result<node::Model, Self::Error> {
let mac = convert_mac_to_id(&self.id)?;
Ok(node::Model {
id: mac,
coord_la: self.coord_la,
coord_lo: self.coord_lo,
battery_minimum: self.battery_minimum,
battery_maximum: self.battery_maximum,
group: self.group,
})
}
}
pub fn convert_mac_to_id(mac: &str) -> Result<i64, ParseError> {
let mac = eui48::MacAddress::parse_str(mac)?;
let mac_bytes = mac.to_array();
let mut mac_id_bytes: [u8; 8] = [0; 8];
mac_id_bytes[2..].copy_from_slice(&mac_bytes);
Ok(i64::from_be_bytes(mac_id_bytes))
}
pub fn convert_id_to_mac(id: &i64) -> String {
let mac_id_bytes = id.to_be_bytes();
let mut mac_bytes: [u8; 6] = [0; 6];
mac_bytes.copy_from_slice(&mac_id_bytes[2..]);
eui48::MacAddress::new(mac_bytes).to_string(eui48::MacAddressFormat::Canonical)
}
#[derive(Serialize)] #[derive(Serialize)]
struct GroupWithNode { struct GroupWithNode {
#[serde(flatten)] #[serde(flatten)]
group: entity::node_group::Model, group: node_group::Model,
node: Vec<entity::node::Model>, node: Vec<NodeWithMac>,
} }
pub async fn get_nodes(state: web::Data<AppState>) -> actix_web::Result<impl Responder> { pub async fn get_nodes(state: web::Data<AppState>) -> actix_web::Result<impl Responder> {
let db = &state.db; let db = &state.db;
let result = node_group::Entity::find() let result: Vec<GroupWithNode> = node_group::Entity::find()
.find_with_related(entity::prelude::Node) .find_with_related(entity::prelude::Node)
.all(db) .all(db)
.await .await
.map_err(ErrorInternalServerError)? .map_err(ErrorInternalServerError)?
.into_iter() .into_iter()
.map(|(group, node)| GroupWithNode { group, node }) .map(|(group, nodes)| {
let nodes = nodes
.into_iter()
.map(|n| n.into())
.collect::<Vec<NodeWithMac>>();
GroupWithNode { group, node: nodes }
})
.collect::<Vec<_>>(); .collect::<Vec<_>>();
Ok(web::Json(result)) Ok(web::Json(result))
} }
pub async fn get_data(state: web::Data<AppState>) -> actix_web::Result<impl Responder> {
let db = &state.db;
let nodes = node::Entity::find()
.all(db)
.await
.map_err(ErrorInternalServerError)?;
let now = Utc::now();
let one_hour_ago = now - chrono::Duration::hours(1);
let mut result: Vec<NodeWithSensorData> = Vec::new();
for node in nodes {
let sensor_data = sensor_data::Entity::find()
.filter(sensor_data::Column::Id.eq(node.id))
.filter(sensor_data::Column::Timestamp.gt(one_hour_ago))
.all(db)
.await
.map_err(ErrorInternalServerError)?;
result.push(NodeWithSensorData {
node: node.into(),
sensor_data,
});
}
Ok(web::Json(result))
}
pub async fn create_group( pub async fn create_group(
state: web::Data<AppState>, state: web::Data<AppState>,
group: web::Json<CreateGroupWithoutId>, group: web::Json<CreateGroupWithoutId>,
@ -60,11 +162,11 @@ pub async fn create_group(
pub async fn create_node( pub async fn create_node(
state: web::Data<AppState>, state: web::Data<AppState>,
node: web::Json<CreateLicense>, node_request: web::Json<NodeWithMac>,
) -> actix_web::Result<impl Responder> { ) -> actix_web::Result<impl Responder> {
let db = &state.db; let db = &state.db;
let node = node.into_inner(); let node: NodeWithMac = node_request.into_inner();
println!("Checking group ID: {:?}", node.group); println!("Checking group ID: {:?}", node.group);
@ -78,26 +180,41 @@ pub async fn create_node(
return Err(ErrorInternalServerError("Group ID does not exist")); return Err(ErrorInternalServerError("Group ID does not exist"));
} }
let node = entity::node::ActiveModel { let node: node::Model = node
id: ActiveValue::NotSet, .try_into()
name: ActiveValue::Set(node.name), .map_err(|_| ErrorBadRequest("Invalid Mac Address"))?;
status: ActiveValue::NotSet, let node = node.into_active_model();
coord_la: ActiveValue::Set(node.coord_la),
coord_lo: ActiveValue::Set(node.coord_lo),
temperature: ActiveValue::NotSet,
battery_minimum: ActiveValue::NotSet,
battery_current: ActiveValue::NotSet,
battery_maximum: ActiveValue::NotSet,
voltage: ActiveValue::NotSet,
uptime: ActiveValue::NotSet,
group: ActiveValue::Set(node.group),
};
let result = node.insert(db).await.map_err(ErrorInternalServerError)?; let result = node.insert(db).await.map_err(ErrorInternalServerError)?;
Ok(web::Json(result)) Ok(web::Json(result))
} }
pub async fn update_node(
state: web::Data<AppState>,
node: web::Json<UpdateNode>,
path: Path<String>,
) -> actix_web::Result<impl Responder> {
let db = &state.db;
let node = node.into_inner();
let id = path.into_inner();
let mac_id = convert_mac_to_id(&id).map_err(ErrorBadRequest)?;
let node = entity::node::ActiveModel {
id: ActiveValue::Unchanged(mac_id),
battery_minimum: ActiveValue::Set(node.battery_minimum),
battery_maximum: ActiveValue::Set(node.battery_maximum),
coord_la: ActiveValue::Set(node.coord_la),
coord_lo: ActiveValue::Set(node.coord_lo),
group: ActiveValue::Set(node.group),
};
let result = node.update(db).await.map_err(ErrorInternalServerError)?;
Ok(web::Json(result))
}
/*
pub async fn delete_node( pub async fn delete_node(
state: web::Data<AppState>, state: web::Data<AppState>,
path: web::Path<Uuid>, path: web::Path<Uuid>,
@ -113,3 +230,4 @@ pub async fn delete_node(
Ok(HttpResponse::Ok().finish()) Ok(HttpResponse::Ok().finish())
} }
*/

View file

@ -1,6 +1,8 @@
use actix_web::{web, App, HttpServer}; use actix_web::{web, App, HttpServer};
use sea_orm::{Database, DatabaseConnection}; use deku::prelude::*;
use sea_orm::{ActiveModelTrait, ActiveValue, Database, DatabaseConnection};
use std::env; use std::env;
use tokio::{io::AsyncReadExt, net::TcpListener};
mod controller; mod controller;
@ -10,7 +12,16 @@ use routes::config;
#[derive(Clone)] #[derive(Clone)]
struct AppState { struct AppState {
db: DatabaseConnection, db: DatabaseConnection,
secret: String, }
#[derive(DekuRead, DekuWrite, Debug)]
#[deku(endian = "little")]
struct Data {
#[deku(bytes = 8)]
mac: [u8; 8],
temp: f32,
battery_voltage: f32,
up_time: u64,
} }
#[actix_web::main] #[actix_web::main]
@ -21,7 +32,6 @@ async fn main() -> std::io::Result<()> {
dotenvy::dotenv().ok(); dotenvy::dotenv().ok();
let db_url = env::var("DATABASE_URL").expect("DATABASE_URL must be set"); let db_url = env::var("DATABASE_URL").expect("DATABASE_URL must be set");
let jwt_secret = env::var("TOKEN_SECRET").expect("TOKEN_SECRET must be set");
let conn = Database::connect(&db_url) let conn = Database::connect(&db_url)
.await .await
@ -29,10 +39,55 @@ async fn main() -> std::io::Result<()> {
println!("Finished running migrations"); println!("Finished running migrations");
let state = AppState { let state = AppState { db: conn.clone() };
db: conn,
secret: jwt_secret, tokio::spawn(async {
}; let db = conn;
let listener = TcpListener::bind("0.0.0.0:7999")
.await
.expect("Couldnt bind to port 7999");
loop {
if let Ok((mut stream, _)) = listener.accept().await {
println!("ESP CONNECTED");
let mut buffer = vec![0; 24];
loop {
if let Ok(_) = stream.read(&mut buffer).await {
println!("{:#x?}", &buffer);
if let Ok((_, value)) = Data::from_bytes((&buffer, 0)) {
println!("Received: {:#?}", value);
let mut mac = value.mac;
mac.rotate_right(2);
let mac = i64::from_be_bytes(mac);
println!("MAC AS INT: {}", mac);
let sensor_data = entity::sensor_data::ActiveModel {
id: ActiveValue::Set(mac),
timestamp: ActiveValue::Set(chrono::Utc::now().naive_utc()),
temperature: ActiveValue::Set(value.temp),
voltage: ActiveValue::Set(value.battery_voltage),
uptime: ActiveValue::Set(value.up_time as i64),
};
let result = sensor_data.insert(&db).await;
match result {
Err(_) => println!(
"Failed to insert data (You probably didnt add the node)"
),
_ => (),
}
} else {
println!("Failed to parse data");
}
}
}
}
}
});
println!("Listening for connections..."); println!("Listening for connections...");
HttpServer::new(move || { HttpServer::new(move || {

View file

@ -19,7 +19,9 @@ pub fn config(cfg: &mut web::ServiceConfig) {
.get(node::get_nodes) .get(node::get_nodes)
.post(node::create_node), .post(node::create_node),
) )
.service(web::resource("/nodes/{id}").delete(node::delete_node)) .service(web::resource("/nodes/{id}").put(node::update_node))
.service(web::resource("/data").get(node::get_data))
//.service(web::resource("/nodes/{id}").delete(node::delete_node))
.service(web::resource("/groups").post(node::create_group)), .service(web::resource("/groups").post(node::create_group)),
); );
} }

View file

@ -4,4 +4,5 @@ pub mod prelude;
pub mod node; pub mod node;
pub mod node_group; pub mod node_group;
pub mod sensor_data;
pub mod user; pub mod user;

View file

@ -7,24 +7,15 @@ use serde::{Deserialize, Serialize};
#[sea_orm(table_name = "node")] #[sea_orm(table_name = "node")]
pub struct Model { pub struct Model {
#[sea_orm(primary_key, auto_increment = false)] #[sea_orm(primary_key, auto_increment = false)]
pub id: Uuid, pub id: i64,
pub name: String,
pub status: bool,
#[sea_orm(column_type = "Double")] #[sea_orm(column_type = "Double")]
pub coord_la: f64, pub coord_la: f64,
#[sea_orm(column_type = "Double")] #[sea_orm(column_type = "Double")]
pub coord_lo: f64, pub coord_lo: f64,
#[sea_orm(column_type = "Float")]
pub temperature: f32,
#[sea_orm(column_type = "Double")] #[sea_orm(column_type = "Double")]
pub battery_minimum: f64, pub battery_minimum: f64,
#[sea_orm(column_type = "Double")] #[sea_orm(column_type = "Double")]
pub battery_current: f64,
#[sea_orm(column_type = "Double")]
pub battery_maximum: f64, pub battery_maximum: f64,
#[sea_orm(column_type = "Double")]
pub voltage: f64,
pub uptime: i64,
pub group: Uuid, pub group: Uuid,
} }
@ -38,6 +29,8 @@ pub enum Relation {
on_delete = "Cascade" on_delete = "Cascade"
)] )]
NodeGroup, NodeGroup,
#[sea_orm(has_many = "super::sensor_data::Entity")]
SensorData,
} }
impl Related<super::node_group::Entity> for Entity { impl Related<super::node_group::Entity> for Entity {
@ -46,4 +39,10 @@ impl Related<super::node_group::Entity> for Entity {
} }
} }
impl Related<super::sensor_data::Entity> for Entity {
fn to() -> RelationDef {
Relation::SensorData.def()
}
}
impl ActiveModelBehavior for ActiveModel {} impl ActiveModelBehavior for ActiveModel {}

View file

@ -2,4 +2,5 @@
pub use super::node::Entity as Node; pub use super::node::Entity as Node;
pub use super::node_group::Entity as NodeGroup; pub use super::node_group::Entity as NodeGroup;
pub use super::sensor_data::Entity as SensorData;
pub use super::user::Entity as User; pub use super::user::Entity as User;

View file

@ -0,0 +1,38 @@
//! `SeaORM` Entity, @generated by sea-orm-codegen 1.0.1
use sea_orm::entity::prelude::*;
use serde::{Deserialize, Serialize};
#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Serialize, Deserialize)]
#[sea_orm(table_name = "sensor_data")]
pub struct Model {
#[sea_orm(primary_key, auto_increment = false)]
pub id: i64,
#[sea_orm(primary_key, auto_increment = false)]
pub timestamp: DateTime,
#[sea_orm(column_type = "Float")]
pub temperature: f32,
#[sea_orm(column_type = "Float")]
pub voltage: f32,
pub uptime: i64,
}
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
pub enum Relation {
#[sea_orm(
belongs_to = "super::node::Entity",
from = "Column::Id",
to = "super::node::Column::Id",
on_update = "Cascade",
on_delete = "Cascade"
)]
Node,
}
impl Related<super::node::Entity> for Entity {
fn to() -> RelationDef {
Relation::Node.def()
}
}
impl ActiveModelBehavior for ActiveModel {}

View file

@ -21,12 +21,10 @@
#![warn(rust_2018_idioms)] #![warn(rust_2018_idioms)]
use tokio::io::{AsyncReadExt, AsyncWriteExt}; use tokio::io::AsyncReadExt;
use tokio::net::TcpListener; use tokio::net::TcpListener;
use core::str; use core::str;
use std::env;
use std::error::Error;
#[tokio::main] #[tokio::main]
async fn main() -> std::io::Result<()> { async fn main() -> std::io::Result<()> {

View file

@ -2,6 +2,7 @@ pub use sea_orm_migration::prelude::*;
mod m20241008_091626_create_table_user; mod m20241008_091626_create_table_user;
mod m20241008_095058_create_table_node; mod m20241008_095058_create_table_node;
mod m20241013_134422_create_table_sensor_data;
pub struct Migrator; pub struct Migrator;
@ -11,6 +12,7 @@ impl MigratorTrait for Migrator {
vec![ vec![
Box::new(m20241008_091626_create_table_user::Migration), Box::new(m20241008_091626_create_table_user::Migration),
Box::new(m20241008_095058_create_table_node::Migration), Box::new(m20241008_095058_create_table_node::Migration),
Box::new(m20241013_134422_create_table_sensor_data::Migration),
] ]
} }
} }

View file

@ -26,21 +26,11 @@ impl MigrationTrait for Migration {
Table::create() Table::create()
.table(Node::Table) .table(Node::Table)
.if_not_exists() .if_not_exists()
.col( .col(big_unsigned(Node::Id).primary_key())
uuid(Node::Id)
.extra("DEFAULT gen_random_uuid()")
.primary_key(),
)
.col(string(Node::Name))
.col(boolean(Node::Status).default(false))
.col(double(Node::CoordLa)) .col(double(Node::CoordLa))
.col(double(Node::CoordLo)) .col(double(Node::CoordLo))
.col(float(Node::Temperature).default(-127))
.col(double(Node::BatteryMinimum).default(-127)) .col(double(Node::BatteryMinimum).default(-127))
.col(double(Node::BatteryCurrent).default(-127))
.col(double(Node::BatteryMaximum).default(-127)) .col(double(Node::BatteryMaximum).default(-127))
.col(double(Node::Voltage).default(-127))
.col(big_unsigned(Node::Uptime).default(0))
.col(uuid(Node::Group)) .col(uuid(Node::Group))
.foreign_key( .foreign_key(
ForeignKey::create() ForeignKey::create()
@ -71,25 +61,19 @@ impl MigrationTrait for Migration {
} }
#[derive(DeriveIden)] #[derive(DeriveIden)]
enum Node { pub enum Node {
Table, Table,
Id, Id, // Mac address
Name,
Status,
CoordLa, CoordLa,
CoordLo, CoordLo,
Temperature, // def: -127
BatteryMinimum, // def: -127 BatteryMinimum, // def: -127
BatteryCurrent, // def: -127
BatteryMaximum, // def: -127 BatteryMaximum, // def: -127
Voltage, // def: -127
Uptime, // def: 0
Group, Group,
} }
#[derive(DeriveIden)] #[derive(DeriveIden)]
enum NodeGroup { enum NodeGroup {
Table, Table,
Id, Id, // Uuid
Name, Name, // Groupname
} }

View file

@ -0,0 +1,55 @@
use crate::m20241008_095058_create_table_node::Node;
use sea_orm_migration::{prelude::*, schema::*};
#[derive(DeriveMigrationName)]
pub struct Migration;
#[async_trait::async_trait]
impl MigrationTrait for Migration {
async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> {
manager
.create_table(
Table::create()
.table(SensorData::Table)
.if_not_exists()
.col(big_unsigned(Node::Id))
.col(timestamp(SensorData::Timestamp))
.primary_key(
Index::create()
.col(SensorData::Id)
.col(SensorData::Timestamp),
)
.col(float(SensorData::Temperature).default(-127))
.col(float(SensorData::Voltage).default(-127))
.col(big_unsigned(SensorData::Uptime).default(0))
.foreign_key(
ForeignKey::create()
.name("fk-data-node_id")
.from(SensorData::Table, SensorData::Id)
.to(Node::Table, Node::Id)
.on_update(ForeignKeyAction::Cascade)
.on_delete(ForeignKeyAction::Cascade),
)
.to_owned(),
)
.await
}
async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> {
// Replace the sample below with your own migration scripts
manager
.drop_table(Table::drop().table(SensorData::Table).to_owned())
.await
}
}
#[derive(DeriveIden)]
enum SensorData {
Table,
Id, // Mac address
Timestamp,
Temperature, // def: -127
Voltage, // def: -127
Uptime, // def: 0
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 121 KiB

View file

@ -31,24 +31,24 @@ docker compose -f dev-compose.yml down
# Applying Migrations # Applying Migrations
If migrations haven't been applied yet you can apply them with the following commands: If migrations haven't been applied yet you can apply them with the following commands:
*Note: Plase ensure the database is running* *Note: Plase ensure the database and backend is running*
1. remove old docker image to rebuild remove old docker image if you have updated the backend
```bash ```bash
docker image remove apfelnetzwerk-backend docker image remove apfelnetzwerk-backend
``` ```
2. check which dockers are running 1. check which dockers are running (both the database and backend should run otherwise start them as stated above)
```bash ```bash
docker ps docker ps
``` ```
3. attach a shell to the running postgres docker container 2. attach a shell to the running backend docker container
```bash ```bash
docker exec -it <it/name> /bin/bash docker exec -it <id/name> /bin/bash
``` ```
4. apply migrations 3. apply migrations
```bash ```bash
#(go into the crates folder) #(go into the crates folder)
cd crates cd crates

Binary file not shown.

After

Width:  |  Height:  |  Size: 240 KiB

BIN
docs/empathy-map.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 207 KiB

BIN
docs/walt-disney.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 106 KiB

23
embedded/README.md Normal file
View file

@ -0,0 +1,23 @@
# Info
## Important Commands
Command to configure build options of the project:
`make menuconfig`
Command to flash the firmware:
`make flash`
Command to show the serial monitor:
`make monitor`
## Connection Error
If you encounter the error `[Errno 2]`, it likely means the microcontroller is not connected or the wrong tty port is selected.
To select the correct tty port, use the command `make menuconfig`, then navigate to **Serial Flasher Config -> Default Serial Port** and choose the correct port.
## Sensor Issues
If the sensor output is around **80°C**, there is likely a power connection issue.
If the output is around **-127°C**, the sensor's data wire is probably not connected.
## Compilation Errors
If the temperature input is `none`, it might be due to a compilation error caused by an incorrect SDK configuration.
Use the command `make menuconfig` and change the following settings:
**Component config -> newlib -> Disable nano formatting**

View file

@ -0,0 +1,31 @@
# DS18B20 Component
Simple DS18B20 temperature sensor library for [ESP8266 RTOS SDK](https://github.com/espressif/ESP8266_RTOS_SDK) for reading Celsius temperature with different resolutions from singular device.
## Usage
```
// Create variable for handler
ds18b20_handler_t sensor;
// Check for any initialization failures
if (!ds18b20_init(&sensor, GPIO_NUM_12, TEMP_RES_12_BIT))
{
ESP_LOGE("TAG", "Failed to initalize DS18B20!");
return 0; // Exit
}
float temp = 0;
// Initalize conversion
ds18b20_convert_temp(&sensor);
// If you doesn't convert temperature you may read 85.0 Celsius,
// as it is default temperature set by DS18B20 if convert command wasn't issued.
temp = ds18b20_read_temp(&sensor); // Read temperature
// Print temperature with 4 decimal places
// (12 bit resolution measurement accuracy is 0.0625 Celsius)
ESP_LOGI("TAG", "Temperature = %.4f", temp);
```
> **_NOTE:_** If last statement doesn't print temperature you may have to disable Newlib nano in `menuconfig` of RTOS SDK.

View file

@ -0,0 +1,5 @@
#
# Component Makefile
#
COMPONENT_ADD_INCLUDEDIRS := .

View file

@ -0,0 +1,120 @@
#include "ds18b20.h"
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "esp_log.h"
static const char *TAG_DS18B20 = "DS18B20";
static const uint16_t ds18b20_temp_conv_time[] = {94, 188, 375, 750}; // ms
static const uint16_t ds18b20_resolution_val[] = {0x1F, 0x3F, 0x5F, 0x7F};
uint8_t ds18b20_init(ds18b20_handler_t *device, gpio_num_t pin, ds18b20_temp_res_t resolution)
{
if (!device)
{
ESP_LOGW(TAG_DS18B20, "device is null!");
return 0;
}
if (!onewire_init(&device->bus, pin, NULL))
{
ESP_LOGW(TAG_DS18B20, "Failed to initialize onewire bus");
return 0;
}
device->res = resolution;
// Configure resolution
ds18b20_write_scratchpad(device);
ds18b20_read_scratchpad(device);
return 1;
}
void ds18b20_send_command(ds18b20_handler_t *device, ds18b20_commands_t command)
{
uint8_t payload = 0x0 ^ command;
onewire_write_byte(&device->bus, payload);
}
void ds18b20_convert_temp(ds18b20_handler_t *device)
{
onewire_reset(&device->bus);
onewire_send_command(&device->bus, _ROM_SKIP);
ds18b20_send_command(device, _CONVERT_T);
vTaskDelay(pdMS_TO_TICKS(ds18b20_temp_conv_time[device->res]));
}
void ds18b20_write_scratchpad(ds18b20_handler_t *device)
{
onewire_reset(&device->bus);
onewire_send_command(&device->bus, _ROM_SKIP);
ds18b20_send_command(device, _SCRATCH_WRITE);
// Th and Tl registers
onewire_write_byte(&device->bus, 0);
onewire_write_byte(&device->bus, 0);
// Resolution value
onewire_write_byte(&device->bus, ds18b20_resolution_val[device->res]);
}
void ds18b20_copy_scratchpad(ds18b20_handler_t *device)
{
onewire_reset(&device->bus);
onewire_send_command(&device->bus, _ROM_SKIP);
ds18b20_send_command(device, _SCRATCH_COPY);
}
void ds18b20_read_scratchpad(ds18b20_handler_t *device)
{
onewire_reset(&device->bus);
onewire_send_command(&device->bus, _ROM_SKIP);
ds18b20_send_command(device, _SCRATCH_READ);
uint8_t i;
for (i = 0; i < 9; i++)
{
device->scratchpad[i] = onewire_read_byte(&device->bus);
}
}
void ds18b20_print_scratchpad(ds18b20_handler_t *device)
{
uint8_t i;
for (i = 0; i < 9; i++)
{
printf("%x ", device->scratchpad[i]);
}
printf("\n");
}
float ds18b20_read_temp(ds18b20_handler_t *device)
{
ds18b20_read_scratchpad(device);
uint8_t sign = 0x0;
uint8_t lsb = device->scratchpad[0];
uint8_t mask = 0xFF << (TEMP_RES_12_BIT - device->res);
lsb &= mask; // Mask out last 3 bits accordingly
uint8_t msb = device->scratchpad[1];
sign = msb & 0x80;
int16_t temp = 0x0;
temp = lsb + (msb << 8);
if (sign)
{
temp = ~(-temp) + 1; // Convert signed two complement's
}
return temp / 16.0;
}

View file

@ -0,0 +1,100 @@
#ifndef DS18B20_H
#define DS18B20_H
#include "onewire.h"
typedef enum
{
TEMP_RES_9_BIT = 0,
TEMP_RES_10_BIT = 1,
TEMP_RES_11_BIT = 2,
TEMP_RES_12_BIT = 3
} ds18b20_temp_res_t;
typedef enum
{
_SCRATCH_WRITE = 0x4E,
_SCRATCH_READ = 0xBE,
_SCRATCH_COPY = 0x48,
_CONVERT_T = 0x44
} ds18b20_commands_t;
typedef uint8_t ds18b20_scratchpad_t[9];
typedef struct
{
onewire_bus_handle_t bus;
ds18b20_temp_res_t res;
ds18b20_scratchpad_t scratchpad;
} ds18b20_handler_t;
/**
* @brief Initialize DS18B20
*
* @param device DS18B20 handler
* @param pin Data pin
* @param resolution Temperature resolution
*
* @retval 1: Success
* @retval 0: Incorrect pin or gpio configuration failed (Logs tells which happened)
*/
uint8_t ds18b20_init(ds18b20_handler_t *device, gpio_num_t pin, ds18b20_temp_res_t resolution);
/**
* @brief Send command to DS18B20
*
* @param device DS18B20 handler
* @param command Function command
*/
void ds18b20_send_command(ds18b20_handler_t *device, ds18b20_commands_t command);
/**
* @brief Write to scratchpad
*
* @param device DS18B20 handler
*/
void ds18b20_write_scratchpad(ds18b20_handler_t *device);
/**
* @brief Read from scratchpad
*
* @param device DS18B20 handler
*/
void ds18b20_read_scratchpad(ds18b20_handler_t *device);
/**
* @brief Copy to scratchpad
*
* @param device DS18B20 handler
*/
void ds18b20_copy_scratchpad(ds18b20_handler_t *device);
/**
* @brief Print scratchpad bytes
*
* @param device DS18B20 handler
*/
void ds18b20_print_scratchpad(ds18b20_handler_t *device);
/**
* @brief Initialize temperature conversion and wait for conversion
*
* Function sends CONV_T command and waits for X ms according to `ds18b20_temp_conv_time` static array
*
* @warning Should be called before `ds18b20_convert_temp()` function
*
* @param device DS18B20 handler
*/
void ds18b20_convert_temp(ds18b20_handler_t *device);
/**
* @brief Read temperature from scratchpad
*
* Function reads temperature from scratchpad and converts it to Celsius.
* @warning `ds18b20_convert_temp()` have to be called before for updated temperature.
*
* @param device DS18B20 handler
*/
float ds18b20_read_temp(ds18b20_handler_t *device);
#endif

View file

@ -0,0 +1,5 @@
#
# Component Makefile
#
COMPONENT_ADD_INCLUDEDIRS := .

View file

@ -0,0 +1,175 @@
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "esp_log.h"
#include "rom/ets_sys.h"
#include "onewire.h"
uint8_t onewire_configure_gpio(gpio_num_t pin, gpio_config_t *custom_config)
{
if (!GPIO_IS_VALID_GPIO(pin))
{
ESP_LOGE(TAG_ONEWIRE, "Provided pin is incorrect!");
return 0;
}
gpio_config_t config = {};
if (!custom_config)
{
config.intr_type = GPIO_INTR_DISABLE;
config.mode = GPIO_MODE_OUTPUT_OD;
config.pin_bit_mask = ((uint32_t)1 << pin);
config.pull_down_en = 0;
config.pull_up_en = 0;
}
else
{
config = *custom_config;
}
if (gpio_config(&config) != ESP_OK)
{
return 0;
}
return 1;
}
uint8_t onewire_init(onewire_bus_handle_t *bus, gpio_num_t bus_pin, gpio_config_t *custom_config)
{
if (!bus)
{
ESP_LOGW(TAG_ONEWIRE, "bus is null! (onewire_init)");
return 0;
}
bus->pin = bus_pin;
bus->mutex = xSemaphoreCreateMutex();
// configure GPIO
if (!onewire_configure_gpio(bus_pin, custom_config))
{
return 0;
}
return 1;
}
uint8_t onewire_reset(onewire_bus_handle_t *bus)
{
uint8_t presence;
if (xSemaphoreTake(bus->mutex, _BLOCK_TIME))
{
gpio_set_level(bus->pin, 0); // Send reset pulse
ets_delay_us(_ONEWIRE_RESET_WAIT);
gpio_set_level(bus->pin, 1); // Leave floating
ets_delay_us(_ONEWIRE_PRESENCE_WAIT);
presence = !gpio_get_level(bus->pin);
xSemaphoreGive(bus->mutex);
}
else
{
ESP_LOGE(TAG_ONEWIRE, _SEMFAIL_MSG, "onewire_reset");
return -1;
}
ets_delay_us(_ONEWIRE_RESET_RECOVERY);
return presence;
}
void onewire_write_bit(onewire_bus_handle_t *bus, uint8_t bit)
{
if (xSemaphoreTake(bus->mutex, _BLOCK_TIME))
{
if (bit)
{
// Write 1
gpio_set_level(bus->pin, 0);
ets_delay_us(_ONEWIRE_WRITE1_LOW);
gpio_set_level(bus->pin, 1);
ets_delay_us(_ONEWIRE_WRITE1_WAIT);
}
else
{
// Write 0
gpio_set_level(bus->pin, 0);
ets_delay_us(_ONEWIRE_WRITE0_LOW);
gpio_set_level(bus->pin, 1);
ets_delay_us(_ONEWIRE_WRITE0_WAIT);
}
xSemaphoreGive(bus->mutex);
}
else
{
ESP_LOGE(TAG_ONEWIRE, _SEMFAIL_MSG, "onewire_write_bit");
}
}
uint8_t onewire_read_bit(onewire_bus_handle_t *bus)
{
uint8_t bit;
if (xSemaphoreTake(bus->mutex, _BLOCK_TIME))
{
gpio_set_level(bus->pin, 0);
ets_delay_us(_ONEWIRE_WRITE1_LOW);
gpio_set_level(bus->pin, 1);
ets_delay_us(_ONEWIRE_READ_WAIT);
bit = !gpio_get_level(bus->pin);
xSemaphoreGive(bus->mutex);
ets_delay_us(_ONEWIRE_READ_RECOVERY);
}
else
{
ESP_LOGE(TAG_ONEWIRE, _SEMFAIL_MSG, "onewire_read_bit");
return -1;
}
return bit;
}
void onewire_write_byte(onewire_bus_handle_t *bus, uint8_t byte)
{
uint8_t i;
for (i = 0; i < 8; i++)
{
onewire_write_bit(bus, (byte >> i) & 0x01);
}
}
uint8_t onewire_read_byte(onewire_bus_handle_t *bus)
{
uint8_t i;
uint8_t byte = 0x0;
for (i = 0; i < 8; i++)
{
byte |= (!onewire_read_bit(bus) << i);
}
return byte;
}
void onewire_send_command(onewire_bus_handle_t *bus, onewire_rom_commands_t command)
{
uint8_t payload = 0x0 ^ command;
onewire_write_byte(bus, payload);
}

View file

@ -0,0 +1,123 @@
#ifndef ONEWIRE_H
#define ONEWIRE_H
#include "freertos/FreeRTOS.h"
#include "freertos/semphr.h"
#include "driver/gpio.h"
#include "esp_types.h"
#include "esp_err.h"
#define _ONEWIRE_WRITE1_LOW 6
#define _ONEWIRE_WRITE1_WAIT 64
#define _ONEWIRE_WRITE0_LOW 60
#define _ONEWIRE_WRITE0_WAIT 10
#define _ONEWIRE_READ_WAIT 9
#define _ONEWIRE_READ_RECOVERY 55
#define _ONEWIRE_RESET_WAIT 480
#define _ONEWIRE_PRESENCE_WAIT 70
#define _ONEWIRE_RESET_RECOVERY 410
#define _BLOCK_TIME pdMS_TO_TICKS(1000)
#define _SEMFAIL_MSG "Failed to obtain semaphore. (%s)"
static const char *TAG_ONEWIRE = "ONEWIRE";
typedef enum
{
_ROM_READ = 0x33,
_ROM_SEARCH = 0xF0,
_ROM_MATCH = 0x55,
_ROM_SKIP = 0xCC
} onewire_rom_commands_t;
typedef struct
{
gpio_num_t pin;
SemaphoreHandle_t mutex;
} onewire_bus_handle_t;
/**
* @brief Configure gpio pins for onewire communication
*
* Set `custom_config` to NULL for default config.
*
* @param pin Bus pin
* @param custom_config Custom gpio config
*
* @retval 1: Success
* @retval 0: Incorrect pin or gpio configuration failed (Logs tells which happened)
*/
uint8_t onewire_configure_gpio(gpio_num_t pin, gpio_config_t *custom_config);
/**
* @brief Initalize onewire bus
*
* Set `custom_config` to NULL for default config.
* @warning MUST be called before any other library function!
*
* @param bus Bus handle
* @param pin Bus pin
* @param custom_config Custom gpio config
*
* @retval 1: Success
* @retval 0: `bus` is NULL or gpio configuration failed (Logs tells which happened)
*/
uint8_t onewire_init(onewire_bus_handle_t *bus, gpio_num_t bus_pin, gpio_config_t *custom_config);
/**
* @brief Send reset pulse
*
* @param bus Bus handle
*
* @retval 1: Success (device sent presence pulse)
* @retval -1: Failed to obtain semaphore for gpio handling
* @retval 0: Device failed to return presence pulse
*/
uint8_t onewire_reset(onewire_bus_handle_t *bus);
/**
* @brief Write bit
*
* @param bus Bus handle
* @param bit Bit to send
*/
void onewire_write_bit(onewire_bus_handle_t *bus, uint8_t bit);
/**
* @brief Write byte
*
* @param bus Bus handle
* @param bit Byte to send
*/
void onewire_write_byte(onewire_bus_handle_t *bus, uint8_t byte);
/**
* @brief Read bit
*
* @param bus Bus handle
*
* @retval 1: Device returned 1
* @retval 0: Device returned 0
* @retval -1: Failed to obtain semaphore for gpio handling
*/
uint8_t onewire_read_bit(onewire_bus_handle_t *bus);
/**
* @brief Read bit
*
* @param bus Bus handle
*
* @return Byte returned by device
*/
uint8_t onewire_read_byte(onewire_bus_handle_t *bus);
/**
* @brief Send command to device
*
* @param bus Bus handle
* @param command Onewire rom command
*
*/
void onewire_send_command(onewire_bus_handle_t *bus, onewire_rom_commands_t command);
#endif

View file

@ -3,51 +3,84 @@
#include "zh_network.h" #include "zh_network.h"
#include "driver/gpio.h" #include "driver/gpio.h"
#include "lwip/sockets.h" #include "lwip/sockets.h"
#include "ds18b20.h"
#include "onewire.h"
#include <strings.h> #include <strings.h>
static const char *payload = "Nüttchen"; // #define EXIT_NODE
#define GPIO_OUTPUT_IO_0 2
#define GPIO_OUTPUT_IO_1 16
#define GPIO_OUTPUT_PIN_SEL ((1ULL << GPIO_OUTPUT_IO_0) | (1ULL << GPIO_OUTPUT_IO_1))
#ifdef EXIT_NODE
#define TAG "TCP" #define TAG "TCP"
#define PORT 7999 #define PORT 7999
#define HOST_IP_ADDR #define HOST_IP_ADDR
#define ESP_WIFI_SSID
#define ESP_WIFI_PASS
#endif
#define EXAMPLE_ESP_WIFI_SSID #define ESP_CHANNEL 7
#define EXAMPLE_ESP_WIFI_PASS
#define MAC2STR(a) (a)[0], (a)[1], (a)[2], (a)[3], (a)[4], (a)[5] #define MAC2STR(a) (a)[0], (a)[1], (a)[2], (a)[3], (a)[4], (a)[5]
void zh_network_event_handler(void *arg, esp_event_base_t event_base, int32_t event_id, void *event_data); void zh_network_event_handler(void *arg, esp_event_base_t event_base, int32_t event_id, void *event_data);
// bc:dd:c2:82:82:9e
uint8_t target[6] = {0xBC, 0xDD, 0xC2, 0x82, 0x82, 0x9E}; uint8_t broadcast[6] = {0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF};
uint8_t test[6] = {0x09, 0xF2, 0x69, 0x42, 0x11, 0xA9};
uint8_t _self_mac[6] = {0};
typedef struct typedef struct
{ {
char char_value[30]; float temperature;
int int_value; float battery_voltage;
float float_value; unsigned long up_time;
bool bool_value; } sensor_message_t;
} example_message_t;
typedef struct
{
uint8_t mac[6];
float temperature;
float battery_voltage;
unsigned long up_time;
} tcp_message_t;
int sock;
int getUpTime()
{
// Get system uptime in milliseconds
int uptime = (xTaskGetTickCount() * (1000 / configTICK_RATE_HZ));
return uptime;
}
float getTemp()
{
float temp = 0.0;
ds18b20_handler_t sensor;
// Initialize DS18B20 sensor
if (!ds18b20_init(&sensor, GPIO_NUM_2, TEMP_RES_12_BIT))
{
ESP_LOGE("DS18B20", "Failed to initialize DS18B20 sensor!");
return -1.0; // Indicate an error with a negative value
}
// Convert temperature
ds18b20_convert_temp(&sensor);
// Read the temperature
temp = ds18b20_read_temp(&sensor);
// Check if the temperature is within a reasonable range for DS18B20
if (temp < -55.0 || temp > 125.0)
{
ESP_LOGE("DS18B20", "Temperature reading out of range: %.2f", temp);
return -1.0; // Indicate invalid reading
}
return temp;
}
void app_main(void) void app_main(void)
{ {
gpio_config_t io_conf;
// disable interrupt
io_conf.intr_type = GPIO_INTR_DISABLE;
// set as output mode
io_conf.mode = GPIO_MODE_OUTPUT;
// bit mask of the pins that you want to set,e.g.GPIO15/16
io_conf.pin_bit_mask = GPIO_OUTPUT_PIN_SEL;
// disable pull-down mode
io_conf.pull_down_en = 0;
// disable pull-up mode
io_conf.pull_up_en = 0;
// configure GPIO with the given settings
gpio_config(&io_conf);
esp_log_level_set("zh_vector", ESP_LOG_NONE); esp_log_level_set("zh_vector", ESP_LOG_NONE);
esp_log_level_set("zh_network", ESP_LOG_NONE); esp_log_level_set("zh_network", ESP_LOG_NONE);
nvs_flash_init(); nvs_flash_init();
@ -55,21 +88,22 @@ void app_main(void)
esp_event_loop_create_default(); esp_event_loop_create_default();
wifi_init_config_t wifi_init_config = WIFI_INIT_CONFIG_DEFAULT(); wifi_init_config_t wifi_init_config = WIFI_INIT_CONFIG_DEFAULT();
esp_wifi_init(&wifi_init_config); esp_wifi_init(&wifi_init_config);
esp_wifi_set_mode(WIFI_MODE_STA);
#ifdef EXIT_NODE
wifi_config_t wifi_config = { wifi_config_t wifi_config = {
.sta = { .sta = {
.ssid = EXAMPLE_ESP_WIFI_SSID, .ssid = ESP_WIFI_SSID,
.password = EXAMPLE_ESP_WIFI_PASS, .password = ESP_WIFI_PASS,
.channel = 7}, .channel = ESP_CHANNEL},
}; };
esp_wifi_set_config(ESP_IF_WIFI_STA, &wifi_config); esp_wifi_set_config(ESP_IF_WIFI_STA, &wifi_config);
esp_wifi_set_mode(WIFI_MODE_STA); esp_wifi_set_channel(ESP_CHANNEL, 1);
esp_wifi_start();
esp_wifi_set_channel(7, 1);
esp_wifi_connect(); esp_wifi_connect();
#endif
esp_wifi_start();
// esp_wifi_set_max_tx_power(8); // Power reduction is for example and testing purposes only. Do not use in your own programs! // esp_wifi_set_max_tx_power(8); // Power reduction is for example and testing purposes only. Do not use in your own programs!
zh_network_init_config_t network_init_config = ZH_NETWORK_INIT_CONFIG_DEFAULT(); zh_network_init_config_t network_init_config = ZH_NETWORK_INIT_CONFIG_DEFAULT();
network_init_config.max_waiting_time = 1000; network_init_config.max_waiting_time = 1000;
@ -81,10 +115,8 @@ void app_main(void)
esp_event_handler_instance_register(ZH_NETWORK, ESP_EVENT_ANY_ID, &zh_network_event_handler, NULL, NULL); esp_event_handler_instance_register(ZH_NETWORK, ESP_EVENT_ANY_ID, &zh_network_event_handler, NULL, NULL);
#endif #endif
char rx_buffer[128]; #ifdef EXIT_NODE
char addr_str[128]; uint8_t data = 0xFF;
int addr_family;
int ip_protocol;
for (;;) for (;;)
{ {
@ -94,7 +126,7 @@ void app_main(void)
destAddr.sin_port = htons(7999); destAddr.sin_port = htons(7999);
inet_pton(AF_INET, HOST_IP_ADDR, &destAddr.sin_addr); inet_pton(AF_INET, HOST_IP_ADDR, &destAddr.sin_addr);
int sock = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP); sock = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
if (sock < 0) if (sock < 0)
{ {
ESP_LOGE(TAG, "Unable to create socket: errno %d", errno); ESP_LOGE(TAG, "Unable to create socket: errno %d", errno);
@ -106,13 +138,20 @@ void app_main(void)
{ {
ESP_LOGE(TAG, "Socket unable to connect: errno %d", errno); ESP_LOGE(TAG, "Socket unable to connect: errno %d", errno);
close(sock); close(sock);
vTaskDelay(500 / portTICK_PERIOD_MS);
continue; continue;
} }
ESP_LOGI(TAG, "Successfully connected"); ESP_LOGI(TAG, "Successfully connected");
for (;;) for (;;)
{ {
vTaskDelay(5000 / portTICK_PERIOD_MS); tcp_message_t packet;
int err = send(sock, payload, strlen(payload), 0); esp_read_mac(packet.mac, 0);
packet.battery_voltage = 3.3f;
packet.temperature = getTemp();
packet.up_time = getUpTime();
vTaskDelay(10000 / portTICK_PERIOD_MS);
zh_network_send(broadcast, (uint8_t *)&data, sizeof(data));
int err = send(sock, (uint8_t *)&packet, sizeof(tcp_message_t), 0);
if (err < 0) if (err < 0)
{ {
ESP_LOGE(TAG, "Error ocured during sending: errno %d", errno); ESP_LOGE(TAG, "Error ocured during sending: errno %d", errno);
@ -121,20 +160,7 @@ void app_main(void)
} }
close(sock); close(sock);
} }
#endif
example_message_t send_message = {0};
strcpy(send_message.char_value, "Test Message");
send_message.float_value = 1.234;
send_message.bool_value = false;
for (;;)
{
printf("Sending Message");
zh_network_send(target, (uint8_t *)&send_message, sizeof(send_message));
vTaskDelay(5000 / portTICK_PERIOD_MS);
gpio_set_level(GPIO_OUTPUT_IO_0, 0);
vTaskDelay(100 / portTICK_PERIOD_MS);
gpio_set_level(GPIO_OUTPUT_IO_0, 1);
}
} }
void zh_network_event_handler(void *arg, esp_event_base_t event_base, int32_t event_id, void *event_data) void zh_network_event_handler(void *arg, esp_event_base_t event_base, int32_t event_id, void *event_data)
@ -143,17 +169,28 @@ void zh_network_event_handler(void *arg, esp_event_base_t event_base, int32_t ev
{ {
case ZH_NETWORK_ON_RECV_EVENT:; case ZH_NETWORK_ON_RECV_EVENT:;
zh_network_event_on_recv_t *recv_data = event_data; zh_network_event_on_recv_t *recv_data = event_data;
printf("Message from MAC %02X:%02X:%02X:%02X:%02X:%02X is received. Data lenght %d bytes.\n", MAC2STR(recv_data->mac_addr), recv_data->data_len); #ifdef EXIT_NODE
example_message_t *recv_message = (example_message_t *)recv_data->data; sensor_message_t *recv_message = (sensor_message_t *)recv_data->data;
printf("Char %s\n", recv_message->char_value); tcp_message_t packet;
printf("Int %d\n", recv_message->int_value); memcpy(packet.mac, recv_data->mac_addr, 6);
printf("Float %f\n", recv_message->float_value); packet.temperature = recv_message->temperature;
printf("Bool %d\n", recv_message->bool_value); packet.battery_voltage = recv_message->battery_voltage;
heap_caps_free(recv_data->data); // Do not delete to avoid memory leaks! packet.up_time = recv_message->up_time;
gpio_set_level(GPIO_OUTPUT_IO_0, 0); heap_caps_free(recv_data->data);
vTaskDelay(100 / portTICK_PERIOD_MS); int err = send(sock, (uint8_t *)&packet, sizeof(packet), 0);
gpio_set_level(GPIO_OUTPUT_IO_0, 1); if (err < 0)
break; {
ESP_LOGE(TAG, "Error sending TCP data");
break;
}
#else
heap_caps_free(recv_data->data);
sensor_message_t message = {0};
message.temperature = getTemp();
message.battery_voltage = 3.3f;
message.up_time = getUpTime();
zh_network_send(recv_data->mac_addr, (uint8_t *)&message, sizeof(message));
#endif
case ZH_NETWORK_ON_SEND_EVENT:; case ZH_NETWORK_ON_SEND_EVENT:;
zh_network_event_on_send_t *send_data = event_data; zh_network_event_on_send_t *send_data = event_data;
if (send_data->status == ZH_NETWORK_SEND_SUCCESS) if (send_data->status == ZH_NETWORK_SEND_SUCCESS)
@ -168,4 +205,6 @@ void zh_network_event_handler(void *arg, esp_event_base_t event_base, int32_t ev
default: default:
break; break;
} }
} }
// BoskoopBase

View file

@ -3,29 +3,9 @@
<v-toolbar color="main" dark prominent> <v-toolbar color="main" dark prominent>
<img src="../assets/turbologo.svg" alt="logo" class="logo" width="75" /> <img src="../assets/turbologo.svg" alt="logo" class="logo" width="75" />
<v-spacer></v-spacer> <v-spacer></v-spacer>
<!-- Users, Groups, Licenses -->
<div>
<!-- USER MANAGEMENT -->
<template v-if="!user" />
<UsersDialog v-else-if="user.admin" />
<UsersEditActions v-else :admin-menu="false" :user="user!" />
</div>
<!-- Search -->
<v-text-field
class="compact-form mr-2"
label="Search"
variant="solo"
density="compact"
prepend-inner-icon="mdi-magnify"
hide-details
single-line
clearable
v-model="search"
rounded="pill"
></v-text-field>
<!-- Logout --> <!-- Logout -->
<v-btn icon variant="outlined" @click="handlelogout"> <v-btn @click="handlelogout">
<LogOut /> <LogOut />
</v-btn> </v-btn>
</v-toolbar> </v-toolbar>
@ -39,20 +19,6 @@
import { store } from "@/store"; import { store } from "@/store";
import { ref } from "vue"; import { ref } from "vue";
import { LogOut } from "lucide-vue-next"; import { LogOut } from "lucide-vue-next";
import { useQuery } from "@tanstack/vue-query";
import UsersDialog from "./NavBarIcons/UsersDialog.vue";
import UsersEditActions from "./NavBarIcons/UsersEditActions.vue";
import { axiosInstance } from "@/client";
import { User } from "@/types";
import { search } from "@/store";
const { data: user } = useQuery({
queryKey: ["user"],
queryFn: async () => {
const res = await axiosInstance.get<User>("/users/me");
return res.data;
},
});
function handlelogout() { function handlelogout() {
store.setToken(null); store.setToken(null);

View file

@ -5,11 +5,11 @@
<v-expansion-panel> <v-expansion-panel>
<v-expansion-panel-title> <v-expansion-panel-title>
<div class="d-flex justify-content-between align-items-center"> <div class="d-flex justify-content-between align-items-center">
<div class="mr-3" v-if="node.status === 200"> <div class="mr-3" v-if="latestSensorData?.voltage">
Name: {{ node.name }} (Status: online) Name: {{ node.id }} (Status: online)
</div> </div>
<div class="mr-3" v-else> <div class="mr-3" v-else>
Name: {{ node.name }} (Status: offline) Name: {{ node.id }} (Status: offline)
</div> </div>
</div> </div>
</v-expansion-panel-title> </v-expansion-panel-title>
@ -18,21 +18,21 @@
<div class="d-flex flex-column"> <div class="d-flex flex-column">
<div class="d-flex align-items-center mb-2"> <div class="d-flex align-items-center mb-2">
<span class="mr-2">Coordinates:</span> <span class="mr-2">Coordinates:</span>
<span>La: {{ node.coordla }}, Long: {{ node.coordlong }}</span> <span>La: {{ node.coord_la }}, Long: {{ node.coord_lo }}</span>
</div>
</div> </div>
<div class="d-flex align-items-center mb-2"> <div class="d-flex align-items-center mb-2">
<span class="mr-2">Temperature:</span> <span class="mr-2">Temperature:</span>
<span>{{ node.temperature }}°C</span> <span>{{ latestSensorData?.temperature !== undefined ? latestSensorData.temperature : 'NaN' }}°C</span>
</div> </div>
<div class="d-flex align-items-center mb-2"> <div class="d-flex align-items-center mb-2">
<span class="mr-2">Battery:</span> <span class="mr-2">Battery Voltage:</span>
<span>{{ node.battery }}%</span> <span>{{ latestSensorData?.voltage || 'N/A' }}V</span>
</div> </div>
<div class="d-flex align-items-center mb-2"> <div class="d-flex align-items-center mb-2">
<span class="mr-2">Runtime:</span> <span class="mr-2">Runtime:</span>
<span>{{ node.runtime }} hours</span> <span>{{ latestSensorData?.uptime ? (latestSensorData.uptime / 3600).toFixed(2) + ' hours' : 'N/A' }}</span>
</div> </div>
</div>
</v-expansion-panel-text> </v-expansion-panel-text>
</v-expansion-panel> </v-expansion-panel>
</v-expansion-panels> </v-expansion-panels>
@ -41,31 +41,46 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { Node } from "@/types"; import { Node, SensorData } from "@/types";
import { ComputedRef, inject, ref, watch } from "vue"; import { ComputedRef, inject, ref, watch, onMounted, computed, onUnmounted } from "vue";
import { key } from "@/store"; import { key } from "@/store";
import axios from "axios";
const props = defineProps<{ node: Node }>(); const props = defineProps<{ node: Node }>();
const visible = ref(true); const visible = ref(true);
const sensorDataArray = ref<SensorData[]>([]);
const { searching, visibleIds } = inject(key) as { const { searching, visibleIds } = inject(key) as {
searching: ComputedRef<boolean>; searching: ComputedRef<boolean>;
visibleIds: ComputedRef<string[]>; visibleIds: ComputedRef<string[]>;
}; };
watch([searching, visibleIds], ([searching, visibleIds]) => { watch([searching, visibleIds], ([searching, visibleIds]) => {
visible.value = searching ? visibleIds.includes(props.node.uuid) : true; visible.value = searching ? visibleIds.includes(props.node.id) : true;
}); });
const fetchSensorData = async () => {
try {
const { data } = await axios.get<SensorData[]>(
`http://localhost:8080/api/v1/data?id=${props.node.id}`
);
sensorDataArray.value = data;
} catch (error) {
console.error("Error fetching sensor data:", error);
}
}
onMounted(async () => {
fetchSensorData()
const interval = setInterval(fetchSensorData, 10000)
onUnmounted(() => clearInterval(interval))
});
const latestSensorData = computed(() => {
return sensorDataArray.value.length > 0
? sensorDataArray.value[sensorDataArray.value.length - 1]
: null;
});
</script> </script>
<style scoped>
.values {
display: flex;
align-items: center;
}
.v-text-field {
max-width: 200px;
width: 100%;
}
</style>

View file

@ -5,119 +5,140 @@
<tr> <tr>
<th>Node/Name</th> <th>Node/Name</th>
<th>Status</th> <th>Status</th>
<th>Latitude</th> <!-- Separate column for Latitude --> <th>Latitude</th>
<th>Longitude</th> <!-- Separate column for Longitude --> <th>Longitude</th>
<th>Battery</th> <th>Battery</th>
<th>Gemessene - Temperatur</th> <th>Temperature</th>
<th>Laufzeit</th> <th>Runtime</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
<tr v-for="(node, index) in tableData" :key="index"> <tr v-for="(node, index) in tableData" :key="index">
<td>{{ node.name }}</td> <td>{{ node.id }}</td>
<td> <td>
<span :class="node.status === 'ONLINE' ? 'status-online' : 'status-offline'"> <span :class="getLastSensorData(node)?.voltage !== 'N/A' ? 'status-online' : 'status-offline'">
{{ node.status }} {{ getLastSensorData(node)?.voltage !== 'N/A' ? 'ONLINE' : 'OFFLINE' }}
</span> </span>
</td> </td>
<td>{{ node.position.lat }}</td> <td
<td>{{ node.position.lng }}</td> contenteditable="true"
<td>{{ node.battery }}%</td> @blur="validateAndUpdateLatLng(node, 'coord_la', $event)"
<td>{{ node.temperature }}°C</td> >{{ node.coord_la }}</td>
<td>{{ node.runtime }}</td> <td
contenteditable="true"
@blur="validateAndUpdateLatLng(node, 'coord_lo', $event)"
>{{ node.coord_lo }}</td>
<td>{{ calculateBatteryPercentage(getLastSensorData(node)?.voltage, node.battery_minimum, node.battery_maximum) }}%</td>
<td>{{ getLastSensorData(node)?.temperature }}°C</td>
<td>{{ getLastSensorData(node)?.uptime/1000 }}</td>
</tr> </tr>
</tbody> </tbody>
</table> </table>
</div> </div>
</template> </template>
<script> <script>
import axios from 'axios';
export default { export default {
data() { data() {
return { return {
tableData: [ tableData: [],
{ updateInterval: null,
name: "XXXX-XXXX-XXXX-XXXX",
status: "ONLINE",
position: { lat: 40.7128, lng: 74.0012 },
battery: 98,
temperature: 100,
runtime: "12h 30m 12s",
},
{
name: "XXXX-XXXX-XXXX-XXXX",
status: "OFFLINE",
position: { lat: 40.7128, lng: 74.0012 },
battery: 19,
temperature: 100,
runtime: "30m",
},
{
name: "Localnode-3",
status: "ONLINE",
position: { lat: 40.7128, lng: 74.0012 },
battery: 66,
temperature: 100,
runtime: "12h 30m 12s",
},
{
name: "XXXX-XXXX-XXXX-XXXX",
status: "ONLINE",
position: { lat: 40.7128, lng: 74.0012 },
battery: 79,
temperature: 100,
runtime: "12h 30m 12s",
},
{
name: "XXXX-XXXX-XXXX-XXXX",
status: "ONLINE",
position: { lat: 40.7128, lng: 74.0012 },
battery: 10,
temperature: 100,
runtime: "12h 30m 12s",
},
{
name: "XXXX-XXXX-XXXX-XXXX",
status: "OFFLINE",
position: { lat: 40.7128, lng: 74.0012 },
battery: 0,
temperature: 100,
runtime: "12h 30m 12s",
},
{
name: "XXXX-XXXX-XXXX-XXXX",
status: "ONLINE",
position: { lat: 40.7128, lng: 74.0012 },
battery: 56,
temperature: 100,
runtime: "12h 30m 12s",
},
],
}; };
}, },
mounted() {
this.fetchNodesAndData();
this.updateInterval = setInterval(this.fetchNodesAndData, 10000)
},
methods: {
async fetchNodesAndData() {
try {
const nodeGroupsResponse = await axios.get('http://localhost:8080/api/v1/nodes');
const nodeGroups = nodeGroupsResponse.data;
const nodes = nodeGroups.flatMap(group => group.node);
const sensorDataResponse = await axios.get('http://localhost:8080/api/v1/data');
const sensorDataArray = sensorDataResponse.data;
this.tableData = nodes.map(node => {
const nodeSensorData = sensorDataArray.find(data => data.node.id === node.id);
return {
...node,
sensorData: nodeSensorData ? nodeSensorData.sensor_data : {
temperature: 'N/A',
voltage: 'N/A',
uptime: 'N/A',
},
};
});
} catch (error) {
console.error('Error fetching node or sensor data:', error);
}
},
getLastSensorData(node) {
return node.sensorData && node.sensorData.length > 0
? node.sensorData[node.sensorData.length - 1]
: { temperature: 'N/A', voltage: 'N/A', uptime: 'N/A'};
},
calculateBatteryPercentage(voltage, batteryMinimum, batteryMaximum) {
if (voltage <= batteryMinimum) {
return 0;
} else if (voltage >= batteryMaximum) {
return 100;
} else {
return ((voltage - batteryMinimum) / (batteryMaximum - batteryMinimum) * 100).toFixed(2);
}
},
formatRuntime(uptime) {
const hours = Math.floor(uptime / 3600);
const minutes = Math.floor((uptime % 3600) / 60);
const seconds = uptime % 60;
return `${hours}h ${minutes}m ${seconds}s`;
},
validateAndUpdateLatLng(node, field, event) {
const originalValue = node[field];
let newValue = event.target.innerText;
newValue = newValue.replace(',', '.');
const validNumberRegex = /^-?\d+(\.\d+)?$/;
if (validNumberRegex.test(newValue)) {
const parsedValue = parseFloat(newValue);
node[field] = parsedValue;
console.log(`Updated ${field} of ${node.id}: ${parsedValue}`);
} else {
event.target.innerText = originalValue;
console.log(`Failed to set ${field} of ${node.id}: Invalid input "${newValue}"`);
}
},
},
}; };
</script> </script>
<style scoped> <style scoped>
.table-container { .table-container {
background-color: white; background-color: white;
padding: 0.5rem 1rem; /* Reduce top padding to 0.5rem */ padding: 0.5rem 1rem;
margin: 0 auto; /* Remove margin at the top, keep it centered horizontally */ margin: 0 auto;
width: 100%; /* Full width of parent */ width: 100%;
overflow-x: auto; /* Allow horizontal scroll if necessary */ overflow-x: auto;
} }
.responsive-table { .responsive-table {
width: 100%; /* Full width */ width: 100%;
border-collapse: collapse; /* Ensure tight borders */ border-collapse: collapse;
margin: 0; /* Remove margin at the top of the table */ margin: 0;
table-layout: auto; /* Auto layout for slimmer columns */ table-layout: auto;
} }
.responsive-table th, .responsive-table th,
.responsive-table td { .responsive-table td {
padding: 0.5rem; /* Maintain slim padding */ padding: 0.5rem;
text-align: center; /* Center text */ text-align: center;
border-bottom: 1px solid #ddd; border-bottom: 1px solid #ddd;
} }
@ -126,7 +147,7 @@ export default {
} }
.responsive-table tr:nth-child(even) { .responsive-table tr:nth-child(even) {
background-color: #f9f9f9; /* Striped rows */ background-color: #f9f9f9;
} }
.status-online { .status-online {

View file

@ -1,25 +1,32 @@
export interface Node { export interface Node {
uuid: string; id: string;
name: string; coord_la: number;
status: number; coord_lo: number;
coordla: number; battery_minimum: number;
coordlong: number; battery_maximum: number;
temperature: number; group: string;
battery: number; sensorData: SensorData[];
runtime: number;
} }
export interface NodeGroup { export interface NodeGroup {
groupId: string; id: string;
name: string; name: string;
nodes: Node[]; node: Node[];
}
export interface SensorData {
id: string;
timestamp: number;
temperature: number;
voltage: number;
uptime: number;
} }
export interface User { export interface User {
uuid: string; id: string;
name: string; name: string;
email: string; email: string;
admin: boolean; hash: string;
} }
export interface CreateUserDto { export interface CreateUserDto {