Compare commits
4 commits
f909cbbdd3
...
dc81c21ea9
Author | SHA1 | Date | |
---|---|---|---|
dc81c21ea9 | |||
de396ed05f | |||
e4287bb2ac | |||
e367930505 |
20 changed files with 353 additions and 265 deletions
62
Cargo.lock
generated
62
Cargo.lock
generated
|
@ -984,6 +984,37 @@ dependencies = [
|
|||
"windows-sys 0.48.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "iotishnik-server"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"bstr",
|
||||
"bytes",
|
||||
"chrono",
|
||||
"clap",
|
||||
"derive_more",
|
||||
"dotenvy",
|
||||
"fred",
|
||||
"heapless",
|
||||
"hex",
|
||||
"hifitime",
|
||||
"lazy_static",
|
||||
"nom",
|
||||
"ntex",
|
||||
"phf",
|
||||
"regex",
|
||||
"rust_decimal",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"serde_qs",
|
||||
"serde_with",
|
||||
"smallstr",
|
||||
"thiserror",
|
||||
"tokio",
|
||||
"ufmt",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "is-terminal"
|
||||
version = "0.4.7"
|
||||
|
@ -1156,37 +1187,6 @@ version = "0.7.0"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6a51313c5820b0b02bd422f4b44776fbf47961755c74ce64afc73bfad10226c3"
|
||||
|
||||
[[package]]
|
||||
name = "narodmon-server"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"bstr",
|
||||
"bytes",
|
||||
"chrono",
|
||||
"clap",
|
||||
"derive_more",
|
||||
"dotenvy",
|
||||
"fred",
|
||||
"heapless",
|
||||
"hex",
|
||||
"hifitime",
|
||||
"lazy_static",
|
||||
"nom",
|
||||
"ntex",
|
||||
"phf",
|
||||
"regex",
|
||||
"rust_decimal",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"serde_qs",
|
||||
"serde_with",
|
||||
"smallstr",
|
||||
"thiserror",
|
||||
"tokio",
|
||||
"ufmt",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "nom"
|
||||
version = "7.1.3"
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
[package]
|
||||
name = "narodmon-server"
|
||||
name = "iotishnik-server"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
|
@ -15,7 +15,7 @@ derive_more = "0.99.17"
|
|||
dotenvy = "0.15.7"
|
||||
fred = { version = "6.3.0", features = ["nom"] }
|
||||
heapless = { version = "0.7.16", features = ["ufmt-impl"] }
|
||||
hex = { version = "0.4.3", default-features = false }
|
||||
hex = { version = "0.4.3", default-features = false, features = ["std", "alloc"] }
|
||||
hifitime = "3.8.2"
|
||||
lazy_static = "1.4.0"
|
||||
nom = { version = "7.1.3", default-features = false, features = ["std", "alloc"] }
|
||||
|
@ -30,4 +30,4 @@ serde_with = { version = "3.6.1", features = ["hex"] }
|
|||
smallstr = { version = "0.3.0", features = ["std", "union"] }
|
||||
thiserror = "1.0.40"
|
||||
tokio = { version = "1.28.2", features = ["full"] }
|
||||
ufmt = "0.2.0"
|
||||
ufmt = { version = "0.2.0", features = ["std"] }
|
||||
|
|
22
README.md
Normal file
22
README.md
Normal file
|
@ -0,0 +1,22 @@
|
|||
# IoTishnik
|
||||
Платформа для обработки данных окружающей среды с IoT устройств.
|
||||
|
||||
## Описание идеи
|
||||
> ### Что есть сейчас:
|
||||
>
|
||||
> Есть куча девайсов, которые сейчас со своих сенсоров, для публичного и в некоторых случаях частного использования выкладывают на сервер narodmon.ru данные.
|
||||
>
|
||||
> ### В чём проблема narodmon.ru:
|
||||
>
|
||||
>1. Там всё такоееее легаси, что ты не представляешь. Речь идёт о cgi скриптах написанных на bash.
|
||||
>2. Разрабы ищут помощь в виде лиц, в которых можно плевать без последствий сколько угодно.
|
||||
>3. Отсутствие тех поддержки для обычных пользователей от слову совсем.
|
||||
>4. Ужасное API которое ужасное не только из-за своей кривизны. В нём нужно отправлять телеметрию устройств, админ панель разработчика считай отсутствует. О OAuth2 вообще говорить не стоит. И если ты отправишь неправильные запросы с клиента своего, твой "api ключ" приложения заблокируют, и пофиг на то, что его можно спиздить с чужих приложений.
|
||||
>5. Интерфейс просто ужас. Можешь сам зайти и посмотреть, даже писать про это не буду. Даже сам UX в изоляции от UI ужасный.
|
||||
>6. Документация API не соблюдается. Даже когда мы с Андреем написали, что, мол, у вас поля изменились, им было плевать.
|
||||
>7. Работа с приватными датчиками ужастная и требует денег для того, чтобы оно вообще работало.
|
||||
>8. Некоторые очень важные API для устройств (такие как MQTT, на которых работают большое количество готовых продуктов не заточенные под narodmon.ru) доступны тоже только по подписке разрабам.
|
||||
>
|
||||
> ### Наше решение:
|
||||
>
|
||||
> Сделать с нуля своё решение, которое будет горизонтально масштабируемое, с поддержкой старых API для поддержки устройств сделанных под narodmon.ru . Выдать разрабам которые хотят новое и мощное API это API вместе с SDK в виде либ под ардуинку и т.п. Сделать поддержку OAuth для входа используя чужие сервисы и не только. И многое другое что я мб забыл упомянуть
|
|
@ -8,6 +8,8 @@
|
|||
- Поля
|
||||
- Вся информация о девайсе
|
||||
- `devices_{device_id}_{tai_timestamp}_{sensor_id}`
|
||||
- Только значение
|
||||
- `devices_{device_id}`
|
||||
- Поля
|
||||
- `value`
|
||||
- `unit`
|
||||
- `exists`: bool
|
||||
- `unit`: str
|
||||
|
|
|
@ -1,3 +1,5 @@
|
|||
//! Модуль для парсинга всего, что связанно с данными от устройств.
|
||||
|
||||
pub mod error;
|
||||
mod packet_types;
|
||||
pub mod parser;
|
||||
|
|
|
@ -1,16 +1,34 @@
|
|||
use crate::hashes::SupportedUnit;
|
||||
//! Сборник типов для внутренней обработки данных с датчиков.
|
||||
//!
|
||||
//! Предполагается, что struct-ы будут совместимы с JSON API для передачи данных датчиков.
|
||||
|
||||
// Не забывайте про:
|
||||
// #[serde(rename = "...")]
|
||||
// #[serde(alias = "...")]
|
||||
|
||||
use crate::utils::SupportedUnit;
|
||||
use crate::ingest_protocol::error::Error;
|
||||
use crate::ingest_protocol::parser::parse_mac_address;
|
||||
|
||||
use hifitime::Epoch;
|
||||
use rust_decimal::Decimal;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_with::formats::Separator;
|
||||
use serde_with::serde_as;
|
||||
|
||||
use std::collections::HashSet;
|
||||
use std::hash::{Hash, Hasher};
|
||||
|
||||
/// Данные с одного датчика.
|
||||
///
|
||||
/// Основной идентификатор - поле `mac`. В случае с [crate::web_server::old_device_sensor_api],
|
||||
/// `mac` может быть не полем, а названием ключа в параметрах. Из-за двусмысленности документации
|
||||
/// NarodMon, `mac` может означать EUI-48 совместимый MAC адрес, или же просто
|
||||
/// уникальный идентификатор.
|
||||
///
|
||||
/// Парсинг этих данных отличается в разных транспортных протоколах.
|
||||
/// Для HTTP /post или /get: см. [crate::web_server::old_device_sensor_api]
|
||||
/// Для TCP/UDP: TODO
|
||||
/// Для MQTT: TODO
|
||||
#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub struct SensorValue {
|
||||
pub mac: String,
|
||||
|
@ -26,14 +44,9 @@ impl Hash for SensorValue {
|
|||
}
|
||||
}
|
||||
|
||||
pub struct DashSeparator {}
|
||||
|
||||
impl Separator for DashSeparator {
|
||||
fn separator() -> &'static str {
|
||||
"-"
|
||||
}
|
||||
}
|
||||
|
||||
/// Функция-помощник для [MacAsArray], предназначенный для использования с [serde_with].
|
||||
///
|
||||
/// Преобразует MAC-адрес.
|
||||
fn mac_as_array(value: &str) -> Result<[u8; 6], Error<&str>> {
|
||||
Ok(parse_mac_address(value)?.1)
|
||||
}
|
||||
|
|
|
@ -3,7 +3,6 @@ use nom::bytes::complete::tag;
|
|||
use nom::bytes::complete::take_until1;
|
||||
use nom::bytes::complete::{take, take_while, take_while1};
|
||||
use nom::character::complete::hex_digit1;
|
||||
use nom::{Parser};
|
||||
use std::str::FromStr;
|
||||
|
||||
use crate::ingest_protocol::error::Error;
|
||||
|
|
60
src/main.rs
60
src/main.rs
|
@ -1,69 +1,13 @@
|
|||
/*
|
||||
Три датчика реалтайм:
|
||||
#26-94-1D-75-C2-F8
|
||||
#T1#6.93
|
||||
#H1#21
|
||||
#P1#700.91
|
||||
##
|
||||
#![doc = include_str!("../README.md")]
|
||||
|
||||
Формат пакета данных:
|
||||
#MAC[#NAME]\n
|
||||
#mac1#value1[#time1][#name1]\n
|
||||
...
|
||||
#macN#valueN[#timeN][#nameN]\n
|
||||
##
|
||||
|
||||
Загрузка истории показаний:
|
||||
#26-94-1D-75-C2-F8
|
||||
#T1#6.93#1687006667
|
||||
#T1#10.17#1687006067
|
||||
#T1#27.26#1687005467
|
||||
##
|
||||
|
||||
C названием и координатами:
|
||||
#26-94-1D-75-C2-F8#Метео
|
||||
#OWNER#nm17
|
||||
#T1#6.93#Улица
|
||||
#T2#27.26#Дом
|
||||
#P1#700.91#Барометр
|
||||
#LAT#54.308997
|
||||
#LON#48.395861
|
||||
#ALT#233
|
||||
##
|
||||
*/
|
||||
#![feature(try_blocks)]
|
||||
extern crate core;
|
||||
|
||||
mod hashes;
|
||||
mod utils;
|
||||
mod ingest_protocol;
|
||||
mod web_server;
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
use crate::web_server::server_main;
|
||||
|
||||
|
||||
/*fn parse_sensor_value(input: Vec<&str>) -> MyIError<Vec<&str>, NarodMonValues> {
|
||||
Ok(
|
||||
(input, NarodMonValues {
|
||||
mac: Default::default(),
|
||||
value: Default::default(),
|
||||
time: None,
|
||||
name: None,
|
||||
})
|
||||
)
|
||||
}*/
|
||||
|
||||
struct Params {}
|
||||
|
||||
#[ntex::main]
|
||||
|
|
|
@ -1,11 +1,20 @@
|
|||
//! Глобальный модуль для вспомогательных типов и утилит.
|
||||
//!
|
||||
|
||||
|
||||
use phf::phf_map;
|
||||
use serde::{Deserialize, Deserializer, Serialize, Serializer};
|
||||
use std::borrow::Cow;
|
||||
|
||||
|
||||
/// Поддерживаемые типы.
|
||||
///
|
||||
/// TODO: Решить необходимо ли к данным прикреплять единицы измерения.
|
||||
/// TODO: Сейчас вообще сомнительно оставлять ли это или нет.
|
||||
#[derive(Debug, Clone, Hash, PartialEq, Eq)]
|
||||
#[repr(u64)]
|
||||
pub enum SupportedUnit {
|
||||
Celsius, // Needs verification > 273.15
|
||||
Celsius, // Needs verification > -273.15
|
||||
Percentage, // Needs verification >= 100 && <= 0
|
||||
MillimeterHg, // Needs verification
|
||||
UVIndex, // Needs verification
|
||||
|
@ -56,6 +65,7 @@ impl<'de> Deserialize<'de> for SupportedUnit {
|
|||
}
|
||||
}
|
||||
|
||||
/// Таблица преобразования текстового представления единиц в значения [SupportedUnit].
|
||||
static STR_TO_UNITS: phf::Map<&'static str, SupportedUnit> = phf_map! {
|
||||
"C" => SupportedUnit::Celsius,
|
||||
"%" => SupportedUnit::Percentage,
|
|
@ -1,22 +1,22 @@
|
|||
use derive_more::Display;
|
||||
use fred::prelude::*;
|
||||
|
||||
use ntex::http::{StatusCode};
|
||||
use ntex::http::StatusCode;
|
||||
use ntex::web;
|
||||
|
||||
use ntex::web::{HttpRequest, HttpResponse};
|
||||
|
||||
|
||||
|
||||
use thiserror::Error;
|
||||
|
||||
|
||||
use crate::insert_header;
|
||||
use crate::web_server::old_devices_api::QSParserError;
|
||||
use crate::web_server::old_device_sensor_api::qs_parser::QSParserError;
|
||||
use rust_decimal::Decimal;
|
||||
use serde_json::json;
|
||||
|
||||
|
||||
/// Главный объект ошибки [std::error::Error] для всего Web API.
|
||||
///
|
||||
/// В целом, все Result у Web сервера должны использовать этот Error.
|
||||
#[derive(Debug, Error, Display)]
|
||||
pub enum AppError {
|
||||
#[display(fmt = "IDK")]
|
||||
|
@ -48,6 +48,9 @@ pub enum AppError {
|
|||
json_err: Option<serde_json::Error>,
|
||||
query_error: Option<serde_qs::Error>,
|
||||
},
|
||||
|
||||
#[display(fmt = "IDK")]
|
||||
DeviceNotFound(String)
|
||||
}
|
||||
|
||||
impl web::error::WebResponseError for AppError {
|
||||
|
@ -61,6 +64,7 @@ impl web::error::WebResponseError for AppError {
|
|||
AppError::ApiKeyInvalid { .. } => StatusCode::BAD_REQUEST,
|
||||
AppError::UnknownBody { .. } => StatusCode::BAD_REQUEST,
|
||||
AppError::QSError(..) => StatusCode::BAD_REQUEST,
|
||||
AppError::DeviceNotFound(..) => StatusCode::BAD_REQUEST
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -76,6 +80,7 @@ impl web::error::WebResponseError for AppError {
|
|||
"Can't figure out where and in what encoding the main data is"
|
||||
}
|
||||
AppError::QSError(..) => "UrlEncoded body or query params are incorrect",
|
||||
AppError::DeviceNotFound(..) => "Device not found",
|
||||
};
|
||||
|
||||
let status_code = self.status_code();
|
||||
|
|
|
@ -1,12 +1,19 @@
|
|||
use crate::web_server::old_app_api::old_api_handler;
|
||||
//! Модуль веб сервера.
|
||||
//!
|
||||
//! Все модули отвечают только за Web сторону. Такие вещи как
|
||||
//! [crate::web_server::old_device_sensor_api] отвечают только за веб версию.
|
||||
//!
|
||||
//! TODO: Начать работу над TCP/UDP и MQTT сервером
|
||||
|
||||
use old_app_api::old_api_handler;
|
||||
use fred::bytes_utils::Str;
|
||||
use fred::prelude::*;
|
||||
|
||||
|
||||
|
||||
pub(crate) mod app_error;
|
||||
pub mod app_error;
|
||||
pub mod old_app_api;
|
||||
mod old_devices_api;
|
||||
pub mod old_device_sensor_api;
|
||||
pub mod utils;
|
||||
|
||||
#[derive(Clone)]
|
||||
|
@ -14,7 +21,7 @@ pub struct NMAppState {
|
|||
pub redis_client: RedisClient,
|
||||
}
|
||||
|
||||
use crate::web_server::old_devices_api::device_handler;
|
||||
use crate::web_server::old_device_sensor_api::device_handler;
|
||||
|
||||
use ntex::web;
|
||||
|
||||
|
|
|
@ -1,45 +0,0 @@
|
|||
|
||||
|
||||
use crate::web_server::app_error::AppError;
|
||||
use crate::web_server::old_app_api::handlers::{app_init, version};
|
||||
use crate::web_server::old_app_api::types::{AppInitRequest, MandatoryParams};
|
||||
use crate::web_server::utils::redis::is_api_key_valid;
|
||||
use crate::web_server::NMAppState;
|
||||
use nom::AsBytes;
|
||||
use ntex::util::Bytes;
|
||||
use ntex::web;
|
||||
use ntex::web::types::State;
|
||||
|
||||
|
||||
pub async fn old_api_handler(
|
||||
app_state: State<NMAppState>,
|
||||
body_bytes: Bytes,
|
||||
) -> Result<impl web::Responder, AppError> {
|
||||
if body_bytes.len() > 10 * 1024 {
|
||||
// 10 KiB
|
||||
return Err(AppError::RequestTooLarge);
|
||||
}
|
||||
|
||||
let body_bytes = body_bytes.as_bytes();
|
||||
|
||||
let mandatory_params: MandatoryParams<'_> = serde_json::from_slice(body_bytes)?; // TODO: Simd-JSON
|
||||
|
||||
// Ignore clippy singlematch
|
||||
match mandatory_params.cmd.as_ref() {
|
||||
"version" => return version((), &app_state).await,
|
||||
_ => {}
|
||||
}
|
||||
|
||||
is_api_key_valid(&app_state.redis_client, mandatory_params.api_key.as_ref()).await?;
|
||||
|
||||
match mandatory_params.cmd.as_ref() {
|
||||
"appInit" => {
|
||||
let body: AppInitRequest = serde_json::from_slice(body_bytes)?;
|
||||
|
||||
app_init(body, &app_state).await
|
||||
}
|
||||
_ => Err(AppError::UnknownMethod(mandatory_params.cmd.to_string())),
|
||||
}
|
||||
|
||||
//Ok("fuck")
|
||||
}
|
|
@ -1,36 +0,0 @@
|
|||
use crate::web_server::app_error::AppError;
|
||||
use crate::web_server::old_app_api::types::AppInitRequest;
|
||||
use crate::web_server::NMAppState;
|
||||
|
||||
use serde_json::{json};
|
||||
|
||||
|
||||
use crate::insert_header;
|
||||
use fred::interfaces::KeysInterface;
|
||||
use ntex::http::StatusCode;
|
||||
use ntex::web;
|
||||
|
||||
|
||||
|
||||
pub async fn app_init(
|
||||
_body: AppInitRequest<'_>,
|
||||
app_state: &NMAppState,
|
||||
) -> Result<web::HttpResponse, AppError> {
|
||||
let _: () = app_state
|
||||
.redis_client
|
||||
.set("test", 123, None, None, true)
|
||||
.await?;
|
||||
|
||||
Ok(web::HttpResponse::build(StatusCode::OK).body("Hello world!"))
|
||||
}
|
||||
|
||||
pub async fn version(_body: (), _app_state: &NMAppState) -> Result<web::HttpResponse, AppError> {
|
||||
let mut resp = web::HttpResponse::build(StatusCode::OK).json(&json!({
|
||||
"version": "indev",
|
||||
"iotishnik": true
|
||||
}));
|
||||
|
||||
insert_header!(resp.headers_mut(), "Cache-Control", "no-cache");
|
||||
|
||||
Ok(resp)
|
||||
}
|
|
@ -1,7 +1,36 @@
|
|||
mod methods;
|
||||
use crate::web_server::app_error::AppError;
|
||||
use crate::web_server::old_app_api::types::AppInitRequest;
|
||||
use crate::web_server::NMAppState;
|
||||
|
||||
use serde_json::{json};
|
||||
|
||||
|
||||
pub use methods::*;
|
||||
use crate::insert_header;
|
||||
use fred::interfaces::KeysInterface;
|
||||
use ntex::http::StatusCode;
|
||||
use ntex::web;
|
||||
|
||||
|
||||
|
||||
pub async fn app_init(
|
||||
_body: AppInitRequest<'_>,
|
||||
app_state: &NMAppState,
|
||||
) -> Result<web::HttpResponse, AppError> {
|
||||
let _: () = app_state
|
||||
.redis_client
|
||||
.set("test", 123, None, None, true)
|
||||
.await?;
|
||||
|
||||
Ok(web::HttpResponse::build(StatusCode::OK).body("Hello world!"))
|
||||
}
|
||||
|
||||
pub async fn version(_body: (), _app_state: &NMAppState) -> Result<web::HttpResponse, AppError> {
|
||||
let mut resp = web::HttpResponse::build(StatusCode::OK).json(&json!({
|
||||
"version": "indev",
|
||||
"iotishnik": true
|
||||
}));
|
||||
|
||||
insert_header!(resp.headers_mut(), "Cache-Control", "no-cache");
|
||||
|
||||
Ok(resp)
|
||||
}
|
||||
|
|
|
@ -1,5 +1,53 @@
|
|||
mod config_app;
|
||||
//! Модуль обработки данных для приложений конечных пользователей, которые используют старый API.
|
||||
|
||||
mod handlers;
|
||||
mod types;
|
||||
|
||||
pub use config_app::old_api_handler;
|
||||
use ntex::web::types::State;
|
||||
use ntex::util::Bytes;
|
||||
use ntex::web;
|
||||
use nom::AsBytes;
|
||||
use crate::web_server::app_error::AppError;
|
||||
use crate::web_server::NMAppState;
|
||||
use crate::web_server::old_app_api::handlers::{app_init, version};
|
||||
use crate::web_server::old_app_api::types::{AppInitRequest, MandatoryParams};
|
||||
use crate::web_server::utils::redis::is_api_key_valid;
|
||||
|
||||
|
||||
/// Обработчик запросов от приложений.
|
||||
///
|
||||
/// Отвечает за разделение на функции по `cmd`.
|
||||
///
|
||||
/// Вызывается напрямую из ntex приложения.
|
||||
pub async fn old_api_handler(
|
||||
app_state: State<NMAppState>,
|
||||
body_bytes: Bytes,
|
||||
) -> Result<impl web::Responder, AppError> {
|
||||
if body_bytes.len() > 10 * 1024 {
|
||||
// 10 KiB
|
||||
return Err(AppError::RequestTooLarge);
|
||||
}
|
||||
|
||||
let body_bytes = body_bytes.as_bytes();
|
||||
|
||||
let mandatory_params: MandatoryParams<'_> = serde_json::from_slice(body_bytes)?; // TODO: Simd-JSON
|
||||
|
||||
// Ignore clippy singlematch
|
||||
match mandatory_params.cmd.as_ref() {
|
||||
"version" => return version((), &app_state).await,
|
||||
_ => {}
|
||||
}
|
||||
|
||||
is_api_key_valid(&app_state.redis_client, mandatory_params.api_key.as_ref()).await?;
|
||||
|
||||
match mandatory_params.cmd.as_ref() {
|
||||
"appInit" => {
|
||||
let body: AppInitRequest = serde_json::from_slice(body_bytes)?;
|
||||
|
||||
app_init(body, &app_state).await
|
||||
}
|
||||
_ => Err(AppError::UnknownMethod(mandatory_params.cmd.to_string())),
|
||||
}
|
||||
|
||||
//Ok("fuck")
|
||||
}
|
||||
|
|
|
@ -1,4 +1,3 @@
|
|||
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::borrow::Cow;
|
||||
|
||||
|
@ -24,6 +23,11 @@ pub struct AddLikeRequest {
|
|||
pub version: u64,
|
||||
}
|
||||
|
||||
/// Обязательные параметры у JSON app API.
|
||||
///
|
||||
/// При обработке входящих данных производиться два запроса [serde_json::from_str]. Один вызов пытается
|
||||
/// получить [MandatoryParams], другой в зависимости от `cmd`. Это позволяет не добвалять поля из
|
||||
/// этой структуры в каждом специфичном типе.
|
||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||
pub struct MandatoryParams<'a> {
|
||||
#[serde(borrow)]
|
||||
|
@ -32,6 +36,11 @@ pub struct MandatoryParams<'a> {
|
|||
#[serde(borrow)]
|
||||
pub lang: Cow<'a, str>,
|
||||
|
||||
/// Уникальный ID клиента.
|
||||
///
|
||||
/// Используется на подобии как куки PHPSESSID в php.
|
||||
///
|
||||
/// См. также: <https://www.php.net/manual/en/book.session.php>
|
||||
#[serde(borrow)]
|
||||
pub uuid: Cow<'a, str>,
|
||||
|
||||
|
|
124
src/web_server/old_device_sensor_api/mod.rs
Normal file
124
src/web_server/old_device_sensor_api/mod.rs
Normal file
|
@ -0,0 +1,124 @@
|
|||
//! Модуль обработки данных с устройств, которые используют старый API.
|
||||
|
||||
pub mod qs_parser;
|
||||
|
||||
use crate::ingest_protocol::{NMDeviceDataPacket, NMJsonPacket};
|
||||
use crate::web_server::app_error::AppError;
|
||||
|
||||
|
||||
use fred::types::RedisMap;
|
||||
use ntex::http::{HttpMessage, StatusCode};
|
||||
use ntex::util::{Bytes, HashMap};
|
||||
use ntex::{http, web};
|
||||
use fred::prelude::*;
|
||||
use hifitime::Epoch;
|
||||
use ntex::web::types::State;
|
||||
use thiserror::Error;
|
||||
use ufmt::uwrite;
|
||||
use crate::web_server::NMAppState;
|
||||
use crate::web_server::old_device_sensor_api::qs_parser::QSParserError;
|
||||
|
||||
#[derive(Error, Debug)]
|
||||
pub enum Error {
|
||||
#[error("Device not found")]
|
||||
DeviceNotFound(String),
|
||||
#[error("Time sent with the device is way too behind now")]
|
||||
TimeIsLongBehindNow,
|
||||
#[error("{0}")]
|
||||
QSParserError(#[from] QSParserError),
|
||||
}
|
||||
|
||||
|
||||
/// Обработчик данных датчиков с устройств.
|
||||
///
|
||||
/// Слушает /post и /get.
|
||||
/// Для того чтобы пользователям было легче, на оба пути можно отправлять и POST и GET.
|
||||
///
|
||||
/// На POST можно отправлять JSON или url-encoded тело, на GET - только через Query String.
|
||||
pub async fn device_handler<'a>(
|
||||
request: web::HttpRequest,
|
||||
body: Bytes,
|
||||
app_state: State<NMAppState>,
|
||||
) -> Result<web::HttpResponse, AppError> {
|
||||
let mut real_body: Option<NMJsonPacket> = None;
|
||||
let mut json_error = None;
|
||||
let mut query_error = None;
|
||||
|
||||
dbg!(&request.content_type());
|
||||
dbg!(&request.query_string());
|
||||
|
||||
if request.method() == http::Method::POST {
|
||||
match request.content_type() {
|
||||
"application/json" => match serde_json::from_slice::<NMJsonPacket>(body.as_ref()) {
|
||||
Ok(json_body) => real_body = Some(json_body),
|
||||
Err(error) => json_error = Some(error),
|
||||
},
|
||||
"application/x-www-form-urlencoded" => {
|
||||
match serde_qs::from_bytes::<NMDeviceDataPacket>(body.as_ref()) {
|
||||
Ok(qs_body) => {
|
||||
real_body = Some(NMJsonPacket {
|
||||
devices: Vec::from([qs_body]),
|
||||
})
|
||||
}
|
||||
Err(error) => query_error = Some(error),
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
} else if request.method() == http::Method::GET {
|
||||
match serde_qs::from_str::<NMDeviceDataPacket>(request.query_string()) {
|
||||
Ok(qs_body) => {
|
||||
real_body = Some(NMJsonPacket {
|
||||
devices: Vec::from([qs_body]),
|
||||
})
|
||||
}
|
||||
Err(error) => query_error = Some(error),
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(real_body) = real_body {
|
||||
for device in real_body.devices {
|
||||
let mut device_key_str = String::new();
|
||||
|
||||
let now = Epoch::now().unwrap();
|
||||
let mut device_time = device.time
|
||||
.unwrap_or(now);
|
||||
|
||||
// TODO: Добавить гистерезис
|
||||
// Отчёт совместимости: отсутствует
|
||||
if device_time > now {
|
||||
device_time = now;
|
||||
}
|
||||
|
||||
let device_tai_timestamp = device_time
|
||||
.to_duration_since_j1900()
|
||||
.to_seconds();
|
||||
|
||||
uwrite!(device_key_str, "devices_{}", hex::encode(device.mac));
|
||||
|
||||
let device_exists: bool = app_state.redis_client.hget(device_key_str.as_str(), "exists").await?;
|
||||
|
||||
if !device_exists {
|
||||
return Err(AppError::DeviceNotFound(hex::encode(&device.mac)));
|
||||
}
|
||||
|
||||
// devices_{device_id}_{tai_timestamp}_{sensor_id}
|
||||
for sensor in device.values {
|
||||
let mut device_report_key_str = String::new();
|
||||
uwrite!(&mut device_report_key_str, "devices_{}_{}_{}", hex::encode(device.mac), device_tai_timestamp.to_string(), sensor.mac);
|
||||
|
||||
app_state.redis_client.set(
|
||||
device_key_str.as_str(), sensor.value.to_string(), None, None, false,
|
||||
).await?;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
return Err(AppError::UnknownBody {
|
||||
json_err: json_error,
|
||||
query_error,
|
||||
});
|
||||
}
|
||||
|
||||
Ok(web::HttpResponseBuilder::new(StatusCode::OK)
|
||||
.finish())
|
||||
}
|
|
@ -1,21 +1,14 @@
|
|||
use std::collections::HashSet;
|
||||
use std::num::ParseFloatError;
|
||||
use std::str::FromStr;
|
||||
use crate::ingest_protocol::error::Error;
|
||||
use crate::ingest_protocol::parser::parse_mac_address;
|
||||
use crate::ingest_protocol::{NMDeviceDataPacket, NMJsonPacket, SensorValue};
|
||||
use crate::web_server::app_error::AppError;
|
||||
|
||||
|
||||
use ntex::http::{HttpMessage, StatusCode};
|
||||
use ntex::util::{Bytes, HashMap};
|
||||
use ntex::{http, web};
|
||||
use std::sync::Arc;
|
||||
use fred::bytes_utils::Str;
|
||||
use hifitime::efmt::consts::ISO8601;
|
||||
use hifitime::Epoch;
|
||||
use ntex::util::HashMap;
|
||||
use rust_decimal::Decimal;
|
||||
use thiserror::Error;
|
||||
use crate::ingest_protocol::error::Error;
|
||||
use crate::ingest_protocol::{NMDeviceDataPacket, SensorValue};
|
||||
use crate::ingest_protocol::parser::parse_mac_address;
|
||||
|
||||
/// В иделае было бы хорошо сделать всё как у [serde_json::Error], но это слишком большая морока
|
||||
#[derive(Error, Clone, Debug)]
|
||||
|
@ -47,6 +40,11 @@ impl From<serde_qs::Error> for QSParserError {
|
|||
}
|
||||
}
|
||||
|
||||
/// Преобразование оставшихся параметров в urlencoded теле или query string в данные с датчиков
|
||||
/// [SensorValue].
|
||||
///
|
||||
/// Формат: `<SENSOR_MAC>=<SENSOR_VALUE>`.
|
||||
/// Других данных на подобии названия и времени нет.
|
||||
pub fn qs_rest_to_values(parsed: HashMap<String, String>) -> Result<HashSet<SensorValue>, QSParserError> {
|
||||
let mut hashset = HashSet::new();
|
||||
|
||||
|
@ -118,56 +116,4 @@ pub async fn parse_nm_qs_format(input: &str) -> Result<NMDeviceDataPacket, QSPar
|
|||
};
|
||||
|
||||
Ok(device_data)
|
||||
}
|
||||
|
||||
pub async fn device_handler<'a>(
|
||||
request: web::HttpRequest,
|
||||
body: Bytes,
|
||||
) -> Result<web::HttpResponse, AppError> {
|
||||
let mut real_body: Option<NMJsonPacket> = None;
|
||||
let mut json_error = None;
|
||||
let mut query_error = None;
|
||||
|
||||
dbg!(&request.content_type());
|
||||
dbg!(&request.query_string());
|
||||
|
||||
if request.method() == http::Method::POST {
|
||||
match request.content_type() {
|
||||
"application/json" => match serde_json::from_slice::<NMJsonPacket>(body.as_ref()) {
|
||||
Ok(json_body) => real_body = Some(json_body),
|
||||
Err(error) => json_error = Some(error),
|
||||
},
|
||||
"application/x-www-form-urlencoded" => {
|
||||
match serde_qs::from_bytes::<NMDeviceDataPacket>(body.as_ref()) {
|
||||
Ok(qs_body) => {
|
||||
real_body = Some(NMJsonPacket {
|
||||
devices: Vec::from([qs_body]),
|
||||
})
|
||||
}
|
||||
Err(error) => query_error = Some(error),
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
} else if request.method() == http::Method::GET {
|
||||
match serde_qs::from_str::<NMDeviceDataPacket>(request.query_string()) {
|
||||
Ok(qs_body) => {
|
||||
real_body = Some(NMJsonPacket {
|
||||
devices: Vec::from([qs_body]),
|
||||
})
|
||||
}
|
||||
Err(error) => query_error = Some(error),
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(_body) = real_body {
|
||||
// TODO
|
||||
} else {
|
||||
return Err(AppError::UnknownBody {
|
||||
json_err: json_error,
|
||||
query_error,
|
||||
});
|
||||
}
|
||||
|
||||
Ok(web::HttpResponse::build(StatusCode::OK).finish())
|
||||
}
|
||||
}
|
|
@ -1,3 +1,6 @@
|
|||
//! Сборник полезных функций которые используются в многих местах одновременно или слишком
|
||||
//! неспециализированные.
|
||||
|
||||
pub mod redis;
|
||||
|
||||
#[macro_export]
|
||||
|
|
|
@ -1,3 +1,5 @@
|
|||
//! Сборник утилит для работы с Redis.
|
||||
|
||||
use crate::web_server::app_error::AppError;
|
||||
use fred::prelude::*;
|
||||
use heapless::String as HeaplessString;
|
||||
|
@ -6,13 +8,17 @@ use regex::Regex;
|
|||
use ufmt::uwrite;
|
||||
|
||||
lazy_static! {
|
||||
/// Разрешённые знаки для API ключа.
|
||||
static ref ALLOWED_API_KEY_CHARACTERS: Regex = Regex::new("[a-zA-Z0-9]{13}").unwrap();
|
||||
}
|
||||
|
||||
/// Описание полей в KV DB у `apikey_{}`.
|
||||
pub struct ApiKeyDescription {
|
||||
/// ID владельца API ключа.
|
||||
apikey_owner: i64,
|
||||
}
|
||||
|
||||
/// Проверка API ключа на валидность.
|
||||
pub async fn is_api_key_valid(
|
||||
client: &RedisClient,
|
||||
api_key: &str,
|
||||
|
|
Loading…
Add table
Reference in a new issue