Compare commits

..

3 commits

41 changed files with 319 additions and 1389 deletions

4
.gitignore vendored
View file

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

View file

@ -1,11 +0,0 @@
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 {
name: Create node group
type: http
seq: 1
seq: 3
}
post {

View file

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

View file

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

View file

@ -1,25 +0,0 @@
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,16 +543,12 @@ dependencies = [
"actix-cors",
"actix-web",
"argon2",
"chrono",
"deku",
"dotenvy",
"entity",
"eui48",
"futures",
"jsonwebtoken",
"sea-orm",
"serde",
"tokio",
"uuid",
]
@ -777,10 +773,8 @@ checksum = "a21f936df1771bf62b77f047b726c4625ff2e8aa607c01ec06e5a05bd8463401"
dependencies = [
"android-tzdata",
"iana-time-zone",
"js-sys",
"num-traits",
"serde",
"wasm-bindgen",
"windows-targets 0.52.6",
]
@ -956,7 +950,6 @@ dependencies = [
"ident_case",
"proc-macro2",
"quote",
"strsim",
"syn 2.0.79",
]
@ -971,31 +964,6 @@ dependencies = [
"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]]
name = "der"
version = "0.7.9"
@ -1140,16 +1108,6 @@ dependencies = [
"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]]
name = "event-listener"
version = "2.5.3"
@ -1800,15 +1758,6 @@ dependencies = [
"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]]
name = "nom"
version = "7.1.3"
@ -2364,12 +2313,6 @@ version = "0.1.24"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "719b953e2095829ee67db738b3bfa9fa368c94900df327b3f07fe6e794d2fe1f"
[[package]]
name = "rustc-serialize"
version = "0.3.25"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fe834bc780604f4674073badbad26d7219cadfb4a2275802db12cbae17498401"
[[package]]
name = "rustc_version"
version = "0.4.1"
@ -2422,12 +2365,6 @@ dependencies = [
"untrusted",
]
[[package]]
name = "rustversion"
version = "1.0.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "955d28af4278de8121b7ebeb796b6a45735dc01436d898801014aced2773a3d6"
[[package]]
name = "ryu"
version = "1.0.18"

View file

@ -1,90 +1,4 @@
# ApfelNetzwerk
1. [ApfelNetzwerk](#apfelnetzwerk) \
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.
[Considerations](docs/considerations.md) \
[Setup](docs/SETUP.md)

View file

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

View file

@ -1,147 +1,45 @@
use crate::AppState;
use actix_web::web::Path;
use actix_web::{
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 actix_web::{error::ErrorInternalServerError, web, HttpResponse, Responder};
use entity::node_group;
use sea_orm::{ActiveModelTrait, ActiveValue, EntityTrait};
use serde::{Deserialize, Serialize};
#[derive(Serialize)]
struct NodeWithSensorData {
node: NodeWithMac,
sensor_data: Vec<sensor_data::Model>,
}
use uuid::Uuid;
#[derive(Deserialize)]
pub struct CreateGroupWithoutId {
name: String,
}
#[derive(Deserialize, Serialize)]
pub struct NodeWithMac {
id: String,
#[derive(Deserialize)]
pub struct CreateLicense {
name: String,
coord_la: f64,
coord_lo: f64,
battery_minimum: f64,
battery_maximum: f64,
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)]
struct GroupWithNode {
#[serde(flatten)]
group: node_group::Model,
node: Vec<NodeWithMac>,
group: entity::node_group::Model,
node: Vec<entity::node::Model>,
}
pub async fn get_nodes(state: web::Data<AppState>) -> actix_web::Result<impl Responder> {
let db = &state.db;
let result: Vec<GroupWithNode> = node_group::Entity::find()
let result = node_group::Entity::find()
.find_with_related(entity::prelude::Node)
.all(db)
.await
.map_err(ErrorInternalServerError)?
.into_iter()
.map(|(group, nodes)| {
let nodes = nodes
.into_iter()
.map(|n| n.into())
.collect::<Vec<NodeWithMac>>();
GroupWithNode { group, node: nodes }
})
.map(|(group, node)| GroupWithNode { group, node })
.collect::<Vec<_>>();
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(
state: web::Data<AppState>,
group: web::Json<CreateGroupWithoutId>,
@ -162,11 +60,11 @@ pub async fn create_group(
pub async fn create_node(
state: web::Data<AppState>,
node_request: web::Json<NodeWithMac>,
node: web::Json<CreateLicense>,
) -> actix_web::Result<impl Responder> {
let db = &state.db;
let node: NodeWithMac = node_request.into_inner();
let node = node.into_inner();
println!("Checking group ID: {:?}", node.group);
@ -180,41 +78,26 @@ pub async fn create_node(
return Err(ErrorInternalServerError("Group ID does not exist"));
}
let node: node::Model = node
.try_into()
.map_err(|_| ErrorBadRequest("Invalid Mac Address"))?;
let node = node.into_active_model();
let node = entity::node::ActiveModel {
id: ActiveValue::NotSet,
name: ActiveValue::Set(node.name),
status: ActiveValue::NotSet,
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)?;
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(
state: web::Data<AppState>,
path: web::Path<Uuid>,
@ -230,4 +113,3 @@ pub async fn delete_node(
Ok(HttpResponse::Ok().finish())
}
*/

View file

@ -1,8 +1,6 @@
use actix_web::{web, App, HttpServer};
use deku::prelude::*;
use sea_orm::{ActiveModelTrait, ActiveValue, Database, DatabaseConnection};
use sea_orm::{Database, DatabaseConnection};
use std::env;
use tokio::{io::AsyncReadExt, net::TcpListener};
mod controller;
@ -12,16 +10,7 @@ use routes::config;
#[derive(Clone)]
struct AppState {
db: DatabaseConnection,
}
#[derive(DekuRead, DekuWrite, Debug)]
#[deku(endian = "little")]
struct Data {
#[deku(bytes = 8)]
mac: [u8; 8],
temp: f32,
battery_voltage: f32,
up_time: u64,
secret: String,
}
#[actix_web::main]
@ -32,6 +21,7 @@ async fn main() -> std::io::Result<()> {
dotenvy::dotenv().ok();
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)
.await
@ -39,62 +29,17 @@ async fn main() -> std::io::Result<()> {
println!("Finished running migrations");
let state = AppState { db: conn.clone() };
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");
}
}
}
}
}
});
let state = AppState {
db: conn,
secret: jwt_secret,
};
println!("Listening for connections...");
HttpServer::new(move || {
let cors = if cfg!(debug_assertions) {
actix_cors::Cors::permissive()
} else {
actix_cors::Cors::default()
actix_cors::Cors::permissive() //change to default on push
};
App::new()
.wrap(cors)

View file

@ -19,9 +19,7 @@ pub fn config(cfg: &mut web::ServiceConfig) {
.get(node::get_nodes)
.post(node::create_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("/nodes/{id}").delete(node::delete_node))
.service(web::resource("/groups").post(node::create_group)),
);
}

View file

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

View file

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

View file

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

View file

@ -1,38 +0,0 @@
//! `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,10 +21,12 @@
#![warn(rust_2018_idioms)]
use tokio::io::AsyncReadExt;
use tokio::io::{AsyncReadExt, AsyncWriteExt};
use tokio::net::TcpListener;
use core::str;
use std::env;
use std::error::Error;
#[tokio::main]
async fn main() -> std::io::Result<()> {

View file

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

View file

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

View file

@ -1,55 +0,0 @@
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.

Before

Width:  |  Height:  |  Size: 121 KiB

View file

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

Binary file not shown.

Before

Width:  |  Height:  |  Size: 240 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 207 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 106 KiB

View file

@ -1,23 +0,0 @@
# 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

@ -1,31 +0,0 @@
# 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

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

View file

@ -1,120 +0,0 @@
#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

@ -1,100 +0,0 @@
#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

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

View file

@ -1,175 +0,0 @@
#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

@ -1,123 +0,0 @@
#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,84 +3,51 @@
#include "zh_network.h"
#include "driver/gpio.h"
#include "lwip/sockets.h"
#include "ds18b20.h"
#include "onewire.h"
#include <strings.h>
// #define EXIT_NODE
static const char *payload = "Nüttchen";
#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 PORT 7999
#define HOST_IP_ADDR
#define ESP_WIFI_SSID
#define ESP_WIFI_PASS
#endif
#define ESP_CHANNEL 7
#define EXAMPLE_ESP_WIFI_SSID
#define EXAMPLE_ESP_WIFI_PASS
#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);
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};
// bc:dd:c2:82:82:9e
uint8_t target[6] = {0xBC, 0xDD, 0xC2, 0x82, 0x82, 0x9E};
typedef struct
{
float temperature;
float battery_voltage;
unsigned long up_time;
} sensor_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;
}
char char_value[30];
int int_value;
float float_value;
bool bool_value;
} example_message_t;
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_network", ESP_LOG_NONE);
nvs_flash_init();
@ -88,22 +55,21 @@ void app_main(void)
esp_event_loop_create_default();
wifi_init_config_t wifi_init_config = WIFI_INIT_CONFIG_DEFAULT();
esp_wifi_init(&wifi_init_config);
esp_wifi_set_mode(WIFI_MODE_STA);
#ifdef EXIT_NODE
wifi_config_t wifi_config = {
.sta = {
.ssid = ESP_WIFI_SSID,
.password = ESP_WIFI_PASS,
.channel = ESP_CHANNEL},
.ssid = EXAMPLE_ESP_WIFI_SSID,
.password = EXAMPLE_ESP_WIFI_PASS,
.channel = 7},
};
esp_wifi_set_config(ESP_IF_WIFI_STA, &wifi_config);
esp_wifi_set_channel(ESP_CHANNEL, 1);
esp_wifi_connect();
#endif
esp_wifi_set_mode(WIFI_MODE_STA);
esp_wifi_start();
esp_wifi_set_channel(7, 1);
esp_wifi_connect();
// 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();
network_init_config.max_waiting_time = 1000;
@ -115,8 +81,10 @@ void app_main(void)
esp_event_handler_instance_register(ZH_NETWORK, ESP_EVENT_ANY_ID, &zh_network_event_handler, NULL, NULL);
#endif
#ifdef EXIT_NODE
uint8_t data = 0xFF;
char rx_buffer[128];
char addr_str[128];
int addr_family;
int ip_protocol;
for (;;)
{
@ -126,7 +94,7 @@ void app_main(void)
destAddr.sin_port = htons(7999);
inet_pton(AF_INET, HOST_IP_ADDR, &destAddr.sin_addr);
sock = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
int sock = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
if (sock < 0)
{
ESP_LOGE(TAG, "Unable to create socket: errno %d", errno);
@ -138,20 +106,13 @@ void app_main(void)
{
ESP_LOGE(TAG, "Socket unable to connect: errno %d", errno);
close(sock);
vTaskDelay(500 / portTICK_PERIOD_MS);
continue;
}
ESP_LOGI(TAG, "Successfully connected");
for (;;)
{
tcp_message_t packet;
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);
vTaskDelay(5000 / portTICK_PERIOD_MS);
int err = send(sock, payload, strlen(payload), 0);
if (err < 0)
{
ESP_LOGE(TAG, "Error ocured during sending: errno %d", errno);
@ -160,7 +121,20 @@ void app_main(void)
}
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)
@ -169,28 +143,17 @@ void zh_network_event_handler(void *arg, esp_event_base_t event_base, int32_t ev
{
case ZH_NETWORK_ON_RECV_EVENT:;
zh_network_event_on_recv_t *recv_data = event_data;
#ifdef EXIT_NODE
sensor_message_t *recv_message = (sensor_message_t *)recv_data->data;
tcp_message_t packet;
memcpy(packet.mac, recv_data->mac_addr, 6);
packet.temperature = recv_message->temperature;
packet.battery_voltage = recv_message->battery_voltage;
packet.up_time = recv_message->up_time;
heap_caps_free(recv_data->data);
int err = send(sock, (uint8_t *)&packet, sizeof(packet), 0);
if (err < 0)
{
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
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);
example_message_t *recv_message = (example_message_t *)recv_data->data;
printf("Char %s\n", recv_message->char_value);
printf("Int %d\n", recv_message->int_value);
printf("Float %f\n", recv_message->float_value);
printf("Bool %d\n", recv_message->bool_value);
heap_caps_free(recv_data->data); // Do not delete to avoid memory leaks!
gpio_set_level(GPIO_OUTPUT_IO_0, 0);
vTaskDelay(100 / portTICK_PERIOD_MS);
gpio_set_level(GPIO_OUTPUT_IO_0, 1);
break;
case ZH_NETWORK_ON_SEND_EVENT:;
zh_network_event_on_send_t *send_data = event_data;
if (send_data->status == ZH_NETWORK_SEND_SUCCESS)
@ -205,6 +168,4 @@ void zh_network_event_handler(void *arg, esp_event_base_t event_base, int32_t ev
default:
break;
}
}
// BoskoopBase
}

View file

@ -1,11 +1,9 @@
import axios from "axios";
import { store } from "@/store";
let axiosInstance = axios.create({
baseURL: import.meta.env.VITE_BACKEND_URL,
});
axiosInstance.interceptors.request.use((config) => {
config.headers.Authorization = `Bearer ${store.token}`;
return config;
});
export { axiosInstance };
export { axiosInstance };

View file

@ -19,6 +19,14 @@
import { store } from "@/store";
import { ref } from "vue";
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";
function handlelogout() {
store.setToken(null);

View file

@ -5,11 +5,11 @@
<v-expansion-panel>
<v-expansion-panel-title>
<div class="d-flex justify-content-between align-items-center">
<div class="mr-3" v-if="latestSensorData?.voltage">
Name: {{ node.id }} (Status: online)
<div class="mr-3" v-if="node.status === 200">
Name: {{ node.name }} (Status: online)
</div>
<div class="mr-3" v-else>
Name: {{ node.id }} (Status: offline)
Name: {{ node.name }} (Status: offline)
</div>
</div>
</v-expansion-panel-title>
@ -18,21 +18,21 @@
<div class="d-flex flex-column">
<div class="d-flex align-items-center mb-2">
<span class="mr-2">Coordinates:</span>
<span>La: {{ node.coord_la }}, Long: {{ node.coord_lo }}</span>
<span>La: {{ node.coordla }}, Long: {{ node.coordlong }}</span>
</div>
</div>
<div class="d-flex align-items-center mb-2">
<span class="mr-2">Temperature:</span>
<span>{{ latestSensorData?.temperature !== undefined ? latestSensorData.temperature : 'NaN' }}°C</span>
<span>{{ node.temperature }}°C</span>
</div>
<div class="d-flex align-items-center mb-2">
<span class="mr-2">Battery Voltage:</span>
<span>{{ latestSensorData?.voltage || 'N/A' }}V</span>
<span class="mr-2">Battery:</span>
<span>{{ node.batteryCurrent }}%</span>
</div>
<div class="d-flex align-items-center mb-2">
<span class="mr-2">Runtime:</span>
<span>{{ latestSensorData?.uptime ? (latestSensorData.uptime / 3600).toFixed(2) + ' hours' : 'N/A' }}</span>
<span>{{ node.uptime }} hours</span>
</div>
</div>
</v-expansion-panel-text>
</v-expansion-panel>
</v-expansion-panels>
@ -41,46 +41,31 @@
</template>
<script setup lang="ts">
import { Node, SensorData } from "@/types";
import { ComputedRef, inject, ref, watch, onMounted, computed, onUnmounted } from "vue";
import { Node } from "@/types";
import { ComputedRef, inject, ref, watch } from "vue";
import { key } from "@/store";
import axios from "axios";
const props = defineProps<{ node: Node }>();
const visible = ref(true);
const sensorDataArray = ref<SensorData[]>([]);
const { searching, visibleIds } = inject(key) as {
searching: ComputedRef<boolean>;
visibleIds: ComputedRef<string[]>;
};
watch([searching, visibleIds], ([searching, visibleIds]) => {
visible.value = searching ? visibleIds.includes(props.node.id) : true;
visible.value = searching ? visibleIds.includes(props.node.uuid) : 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);
}
</script>
<style scoped>
.values {
display: flex;
align-items: center;
}
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>
.v-text-field {
max-width: 200px;
width: 100%;
}
</style>

View file

@ -9,54 +9,28 @@
<script setup lang="ts">
import HeaderBar from "./HeaderBar.vue";
import CategoryContainer from "./CategoryContainer.vue";
import { useQuery } from "@tanstack/vue-query";
import { axiosInstance } from "@/client";
import { LicenseGroup, License } from "@/types";
import { search, key } from "@/store";
import MiniSearch from "minisearch";
import { computed, provide } from "vue";
import { NodeGroup } from "@/types";
import TableCategory from "./TableCategory.vue";
const { isPending, isError, data, error } = useQuery({
queryKey: ["licenses"],
const { data } = useQuery({
queryKey: ["nodes"],
queryFn: async () => {
const res = await axiosInstance.get<LicenseGroup[]>("/licenses");
console.log(res.data);
const res = await axiosInstance.get<NodeGroup[]>("/nodes");
return res.data;
},
refetchInterval: 60 * 1000,
select: (data) => {
return data.map((group) => {
return {
title: group.name,
value: group.nodes,
};
});
},
});
const searchEngine = computed(() => {
let minisearch = new MiniSearch({
fields: ["name", "description", "id"],
searchOptions: {
boost: { name: 2 },
prefix: true,
},
});
let licenses: License[] = [];
data.value?.forEach((group) => {
group.licenses.forEach((license) => licenses.push(license));
});
console.log(licenses);
minisearch.addAll(licenses);
return minisearch;
});
const searching = computed(() => search.value !== "");
const visibleIds = computed(() => {
return searchEngine.value.search(search.value).map((searchResult) => {
return searchResult.id;
});
});
provide(key, {
visibleIds,
searching,
});
</script>
<style scoped>

View file

@ -8,114 +8,149 @@
<th>Latitude</th>
<th>Longitude</th>
<th>Battery</th>
<th>Temperature</th>
<th>Runtime</th>
<th>Gemessene - Temperatur</th>
<th>Laufzeit</th>
</tr>
</thead>
<tbody>
<tr v-for="(node, index) in tableData" :key="index">
<td>{{ node.id }}</td>
<td>{{ node.name }}</td>
<td>
<span :class="getLastSensorData(node)?.voltage !== 'N/A' ? 'status-online' : 'status-offline'">
{{ getLastSensorData(node)?.voltage !== 'N/A' ? 'ONLINE' : 'OFFLINE' }}
<span :class="node.status === 'ONLINE' ? 'status-online' : 'status-offline'">
{{ node.status }}
</span>
</td>
<td
<td
contenteditable="true"
@blur="validateAndUpdateLatLng(node, 'coord_la', $event)"
>{{ node.coord_la }}</td>
<td
@blur="validateAndUpdateLatLng(node, 'lat', $event)"
>{{ node.position.lat }}</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>
@blur="validateAndUpdateLatLng(node, 'lng', $event)"
>{{ node.position.lng }}</td>
<td>{{ calculateBatteryPercentage(node.batteryVoltage, node.minVoltage, node.maxVoltage) }}%</td>
<td>{{ node.temperature }}°C</td>
<td>{{ node.runtime }}</td>
</tr>
</tbody>
</tbody>
</table>
</div>
</template>
<script>
import axios from 'axios';
//folgende Mockdaten rausnehmen und mit axios requests abfragen
export default {
data() {
return {
tableData: [],
updateInterval: null,
tableData: [
{
name: "XXXX-XXXX-XXXX-XXXX",
status: "ONLINE",
position: { lat: 40.7128, lng: 74.0012 },
batteryVoltage: 3.9,
minVoltage: 3.0,
maxVoltage: 4.2,
temperature: 100,
runtime: "12h 30m 12s",
},
{
name: "XXXX-XXXX-XXXX-XXXX",
status: "OFFLINE",
position: { lat: 40.7128, lng: 74.0012 },
batteryVoltage: 3.2,
minVoltage: 3.0,
maxVoltage: 4.2,
temperature: 100,
runtime: "30m",
},
{
name: "Localnode-3",
status: "ONLINE",
position: { lat: 40.7128, lng: 74.0012 },
batteryVoltage: 3.7,
minVoltage: 3.0,
maxVoltage: 4.2,
temperature: 100,
runtime: "12h 30m 12s",
},
{
name: "XXXX-XXXX-XXXX-XXXX",
status: "ONLINE",
position: { lat: 40.7128, lng: 74.0012 },
batteryVoltage: 3.8,
minVoltage: 3.0,
maxVoltage: 4.2,
temperature: 100,
runtime: "12h 30m 12s",
},
{
name: "XXXX-XXXX-XXXX-XXXX",
status: "ONLINE",
position: { lat: 40.7128, lng: 74.0012 },
batteryVoltage: 3.1,
minVoltage: 3.0,
maxVoltage: 4.2,
temperature: 100,
runtime: "12h 30m 12s",
},
{
name: "XXXX-XXXX-XXXX-XXXX",
status: "OFFLINE",
position: { lat: 40.7128, lng: 74.0012 },
batteryVoltage: 3.0,
minVoltage: 3.0,
maxVoltage: 4.2,
temperature: 100,
runtime: "12h 30m 12s",
},
{
name: "XXXX-XXXX-XXXX-XXXX",
status: "ONLINE",
position: { lat: 40.7128, lng: 74.0012 },
batteryVoltage: 3.6,
minVoltage: 3.0,
maxVoltage: 4.2,
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) {
calculateBatteryPercentage(currentVoltage, minVoltage, maxVoltage) {
if (currentVoltage <= minVoltage) {
return 0;
} else if (voltage >= batteryMaximum) {
} else if (currentVoltage >= maxVoltage) {
return 100;
} else {
return ((voltage - batteryMinimum) / (batteryMaximum - batteryMinimum) * 100).toFixed(2);
return ((currentVoltage - minVoltage) / (maxVoltage - minVoltage) * 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];
const originalValue = node.position[field];
let newValue = event.target.innerText;
// normalize seperated values
newValue = newValue.replace(',', '.');
// check if float value
const validNumberRegex = /^-?\d+(\.\d+)?$/;
if (validNumberRegex.test(newValue)) {
const parsedValue = parseFloat(newValue);
node[field] = parsedValue;
console.log(`Updated ${field} of ${node.id}: ${parsedValue}`);
// Update if valid
node.position[field] = parsedValue;
console.log(`Updated ${field} of ${node.name}: ${parsedValue}`);
} else {
// Reset to original value if invalid
event.target.innerText = originalValue;
console.log(`Failed to set ${field} of ${node.id}: Invalid input "${newValue}"`);
console.log(`Failed to set ${field} of ${node.name}: Invalid input "${newValue}"`);
}
},
},
}
}
};
</script>

View file

@ -19,7 +19,7 @@ export default createVuetify({
light: {
dark: false,
colors: {
main: "#024950",
main: "#353A43",
darker: "#003135",
contrast: "#964734",
accent: "#0FA4AF",

View file

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