377 lines
12 KiB
Rust
377 lines
12 KiB
Rust
|
|
use std::{net::Ipv4Addr, sync::Arc, time::Duration};
|
|
|
|
use axum::{extract::State, http::StatusCode, response::Html, routing::{get, post}, serve, Form, Router};
|
|
use common::{Name, Request};
|
|
use ping_rs::send_ping_async;
|
|
use postcard::to_allocvec;
|
|
use tokio::{fs, io::{AsyncReadExt, AsyncWriteExt}, join, net::TcpStream, sync::{RwLock, Semaphore}, time::{self, sleep, Instant}};
|
|
use anyhow::{Ok, Result};
|
|
use tracing::{error, info, trace, warn, Level};
|
|
use tracing_subscriber::{filter, layer::{Filter, SubscriberExt}, util::SubscriberInitExt, Layer};
|
|
|
|
const FISH: &'static str = "192.168.0.12:1234";
|
|
const DOOR: &'static str = "192.168.0.10:1234";
|
|
|
|
#[derive(Default, Debug)]
|
|
struct NetStatus {
|
|
fish_reachable: bool,
|
|
fish_ping: Option<u32>,
|
|
door_reachable: bool,
|
|
door_ping: Option<u32>,
|
|
router_reachable: bool,
|
|
router_ping: Option<u32>,
|
|
}
|
|
|
|
#[derive(Clone)]
|
|
struct AppState {
|
|
fish: Arc<Semaphore>,
|
|
last_badge: Arc<RwLock<(u64,Instant)>>,
|
|
net_status: Arc<RwLock<NetStatus>>,
|
|
}
|
|
|
|
#[tokio::main]
|
|
async fn main() -> Result<()>{
|
|
let log = fs::OpenOptions::new().append(true).create(true).open("john.log").await?;
|
|
|
|
let (non_blocking, _guard) = tracing_appender::non_blocking(log.into_std().await);
|
|
|
|
let stdout_filter = filter::Targets::new()
|
|
.with_target("server", Level::TRACE)
|
|
.with_target("mio::poll", Level::WARN);
|
|
|
|
let log_filter = stdout_filter.clone()
|
|
.with_target("server", Level::INFO);
|
|
|
|
let stdout = tracing_subscriber::fmt::layer()
|
|
.compact()
|
|
.with_file(true)
|
|
.with_target(true)
|
|
.with_filter(stdout_filter);
|
|
|
|
let log = tracing_subscriber::fmt::layer()
|
|
.compact()
|
|
.with_file(false)
|
|
.with_target(true)
|
|
.with_writer(non_blocking)
|
|
.with_filter(log_filter);
|
|
|
|
let reg = tracing_subscriber::registry()
|
|
.with(stdout)
|
|
.with(log);
|
|
|
|
reg.try_init()?;
|
|
|
|
let fish = Arc::new(Semaphore::new(1));
|
|
let last_badge = Arc::new(RwLock::new((0u64,Instant::now())));
|
|
let network_status = Arc::new(RwLock::new(NetStatus::default()));
|
|
|
|
info!("starting");
|
|
|
|
|
|
let app: Router = Router::new()
|
|
.route("/names", get(names))
|
|
.route("/", get(root))
|
|
.route("/admin", get(control))
|
|
.route("/phil", get(phil))
|
|
.route("/recent", get(recent_badge))
|
|
.route("/setLast", post(set_last))
|
|
.route("/openDoor", post(open_door))
|
|
.route("/setVolume", post(set_volume))
|
|
//.route("/map", post(set_badge))
|
|
.with_state(AppState {
|
|
fish: fish.clone(),
|
|
last_badge: last_badge.clone(),
|
|
net_status: network_status.clone(),
|
|
});
|
|
|
|
let listener = tokio::net::TcpListener::bind("0.0.0.0:8080").await.unwrap();
|
|
|
|
tokio::spawn(async {serve(listener, app).await.unwrap()});
|
|
|
|
|
|
let fish_ = fish.clone();
|
|
tokio::spawn(async move {
|
|
loop {
|
|
match get_recent_badge(fish_.clone()).await {
|
|
Result::Ok(badge) => {
|
|
*last_badge.write().await = (badge, Instant::now());
|
|
},
|
|
Err(e) => {
|
|
trace!("badge read fail: {e}");
|
|
}
|
|
}
|
|
sleep(Duration::from_secs(1)).await;
|
|
}
|
|
});
|
|
|
|
tokio::spawn(async move {
|
|
loop {
|
|
let fish = Ipv4Addr::new(192,168,0,12).into();
|
|
let door = Ipv4Addr::new(192,168,0,10).into();
|
|
let router = Ipv4Addr::new(192,168,0,50).into();
|
|
let (fish,door,router) = join!(
|
|
send_ping_async(&fish, Duration::from_millis(1500), Arc::new(&[255,127]), None),
|
|
send_ping_async(&door, Duration::from_millis(1500), Arc::new(&[255,127]), None),
|
|
send_ping_async(&router, Duration::from_millis(1500), Arc::new(&[255,127]), None)
|
|
);
|
|
|
|
let status = NetStatus {
|
|
fish_reachable: fish.is_ok(),
|
|
door_reachable: door.is_ok(),
|
|
router_reachable: router.is_ok(),
|
|
fish_ping: fish.ok().map(|p| p.rtt),
|
|
door_ping: door.ok().map(|p| p.rtt),
|
|
router_ping: router.ok().map(|p| p.rtt),
|
|
};
|
|
|
|
let mut last = network_status.write().await;
|
|
|
|
if last.fish_reachable != status.fish_reachable {
|
|
warn!("fish connected: {}", status.fish_reachable);
|
|
}
|
|
if last.door_reachable != status.door_reachable {
|
|
warn!("door connected: {}", status.door_reachable);
|
|
}
|
|
if last.router_reachable != status.router_reachable {
|
|
warn!("router connected: {}", status.router_reachable);
|
|
}
|
|
|
|
*last = status;
|
|
drop(last);
|
|
sleep(Duration::from_secs(1)).await;
|
|
}
|
|
});
|
|
|
|
/*{
|
|
let _permit = fish.acquire().await?;
|
|
let mut conn = TcpStream::connect(FISH).await?;
|
|
|
|
let req = Request::RecentBadge;
|
|
conn.write_all(&to_allocvec(&req)?).await?;
|
|
conn.flush().await?;
|
|
|
|
let badge = conn.read_u64().await?;
|
|
info!("read: {badge}");
|
|
|
|
if badge != 0 {
|
|
let req = Request::SetBadge(common::Name::Evan, badge);
|
|
conn.write_all(&to_allocvec(&req)?).await?;
|
|
conn.flush().await?;
|
|
}
|
|
|
|
let req = Request::SetVolume(30);
|
|
conn.write_all(&to_allocvec(&req)?).await?;
|
|
}*/
|
|
|
|
loop {
|
|
sleep(Duration::from_secs(2)).await;
|
|
}
|
|
}
|
|
|
|
async fn get_recent_badge(fish: Arc<Semaphore>) -> Result<u64> {
|
|
let _permit = fish.acquire().await?;
|
|
let mut conn = TcpStream::connect(FISH).await?;
|
|
|
|
let req = Request::RecentBadge;
|
|
conn.write_all(&to_allocvec(&req)?).await?;
|
|
conn.flush().await?;
|
|
|
|
let badge = conn.read_u64().await?;
|
|
|
|
if badge != 0 {
|
|
info!("new badge: {badge:#0x}");
|
|
Ok(badge)
|
|
} else {
|
|
Err(anyhow::anyhow!("no badge present"))
|
|
}
|
|
}
|
|
|
|
async fn root() -> Html<String> {
|
|
let page = format!(r#"
|
|
<h1>JOHN</h1>
|
|
"#);
|
|
Html::from(page)
|
|
}
|
|
async fn names(state: State<AppState>) -> Html<String> {
|
|
let last_badge_view = last_badge_view(&state.last_badge).await;
|
|
|
|
let mut page = format!(r#"
|
|
{last_badge_view}
|
|
<h1>set last badge</h1>
|
|
"#);
|
|
|
|
for name in [
|
|
"Tess",
|
|
"Amaia",
|
|
"Prueky",
|
|
"David",
|
|
"Nathaniel",
|
|
"Thia",
|
|
"Michael",
|
|
"Zoey",
|
|
] {
|
|
page.push_str(&format!(r#"
|
|
<form target="output" style="display:inline; margin-right:10px; " action="setLast" method="post" id="myForm">
|
|
<button style="font-size:40px;" type="submit" name="name" value="{name}" target="output">{name}</button>
|
|
</form>
|
|
"#));
|
|
}
|
|
|
|
page.push_str(&format!(r#"
|
|
<form target="output" style="margin-top:10px;" action="setLast" method="post" id="myForm">
|
|
<input style="font-size:40px;" name="name" placeholder="other" "type="text"></input>
|
|
<button style="font-size:40px;" type="submit" target="output">other</button>
|
|
</form>
|
|
<iframe name="output"></iframe>
|
|
"#));
|
|
Html::from(page)
|
|
}
|
|
|
|
async fn control(state: State<AppState>) -> Html<String> {
|
|
let last_badge_view = last_badge_view(&state.last_badge).await;
|
|
|
|
let netstat = state.net_status.read().await;
|
|
|
|
let door_color = if netstat.door_reachable {"green"} else {"red"};
|
|
let fish_color = if netstat.fish_reachable {"green"} else {"red"};
|
|
let router_color = if netstat.router_reachable {"green"} else {"red"};
|
|
let fish_ping = if let Some(ping) = netstat.fish_ping {&format!("ping: {ping}ms")} else {"disconnected"};
|
|
let door_ping = if let Some(ping) = netstat.door_ping {&format!("ping: {ping}ms")} else {"disconnected"};
|
|
let router_ping = if let Some(ping) = netstat.router_ping {&format!("ping: {ping}ms")} else {"offline"};
|
|
|
|
|
|
let page = format!(r#"
|
|
<head><title>JOHNADMIN</title></head>
|
|
<h1>JOHN ADMIN PANEL</h1>
|
|
<p>
|
|
<span style="background-color:{router_color};">lovesense</span> {router_ping} <br>
|
|
<span style="background-color:{door_color};">door</span> {door_ping} <br>
|
|
<span style="background-color:{fish_color};">fish</span> {fish_ping}
|
|
</p>
|
|
<form target="output" action="openDoor" method="post" id="myForm">
|
|
<button style="font-size:40px;" type="submit" name="name" value="open" target="output">Open noor</button>
|
|
</form>
|
|
<form target="output" action="setVolume" method="post" id="myForm">
|
|
<input type="range" min="0" max="30" value="0" name="volume" class="slider" id="volume">
|
|
<button style="font-size:30px;" type="submit" target="output">set volume</button>
|
|
</form>
|
|
{last_badge_view}
|
|
<iframe name="output"></iframe>
|
|
"#);
|
|
|
|
Html::from(page)
|
|
}
|
|
|
|
async fn last_badge_view(last_badge: &Arc<RwLock<(u64, Instant)>>) -> String {
|
|
let (last_badge, time) = *last_badge.read().await;
|
|
let time = time.elapsed().as_secs();
|
|
if last_badge == 0 {
|
|
format!(r#"
|
|
<p>
|
|
no badges scanned, up {time}s
|
|
</p>
|
|
"#)
|
|
} else {
|
|
format!(r#"
|
|
<p>
|
|
most recent badge {last_badge:#0x}<br>
|
|
{time}s ago
|
|
</p>
|
|
"#)
|
|
}
|
|
}
|
|
|
|
#[derive(serde::Deserialize)]
|
|
struct Volume {
|
|
volume: u8,
|
|
}
|
|
|
|
async fn open_door() -> Html<&'static str> {
|
|
info!("opening door");
|
|
let mut conn = TcpStream::connect(DOOR).await.unwrap();
|
|
|
|
|
|
conn.write_all(b"B f7c31d1603000000 \r\n").await.unwrap();
|
|
conn.flush().await.unwrap();
|
|
|
|
Html::from("<p>opening the noor</p>")
|
|
}
|
|
|
|
async fn set_volume(
|
|
state: State<AppState>,
|
|
Form(input): Form<Volume>) -> Html<String> {
|
|
|
|
info!("setting volume to {}", input.volume);
|
|
|
|
let _fish = state.fish.acquire().await.unwrap();
|
|
let mut conn = TcpStream::connect(FISH).await.unwrap();
|
|
|
|
let req = Request::SetVolume(input.volume);
|
|
conn.write_all(&to_allocvec(&req).unwrap()).await.unwrap();
|
|
conn.flush().await.unwrap();
|
|
|
|
Html::from(format!("<p>volume is now {}</p></br><a href=\"/\">back</a>", input.volume))
|
|
}
|
|
|
|
#[derive(serde::Deserialize)]
|
|
struct NameForm {
|
|
name: String,
|
|
}
|
|
|
|
async fn set_last(
|
|
state: State<AppState>,
|
|
Form(input): Form<NameForm>) -> Html<String> {
|
|
|
|
let name_e = match input.name.as_str() {
|
|
"Tess" => Name::Tess,
|
|
"Amaia" => Name::Amaia,
|
|
"Prueky" => Name::Prueky,
|
|
"David" => Name::David,
|
|
"Nathaniel" => Name::Nathaniel,
|
|
"Thia" => Name::Thia,
|
|
"Michael" => Name::Michael,
|
|
"Zoey" => Name::Zoey,
|
|
_ => Name::Unknown,
|
|
|
|
};
|
|
|
|
let (last_badge, _) = *state.last_badge.read().await;
|
|
info!("setting {} ({name_e:?}) to {last_badge:#0x}", input.name);
|
|
let _fish = state.fish.acquire().await.unwrap();
|
|
let mut conn = TcpStream::connect(FISH).await.unwrap();
|
|
|
|
if last_badge == 0 {
|
|
return Html::from("<h1>failed, no badge scanned</h1>".to_string());
|
|
}
|
|
|
|
let req = Request::SetBadge(name_e, last_badge);
|
|
conn.write_all(&to_allocvec(&req).unwrap()).await.unwrap();
|
|
conn.flush().await.unwrap();
|
|
|
|
|
|
Html::from(format!("<p>{last_badge:#0x} is now {}</p></br><a href=\"/\">back</a>", input.name))
|
|
}
|
|
|
|
async fn recent_badge(state: State<AppState>) -> String {
|
|
let (last_badge, time) = *state.last_badge.read().await;
|
|
format!("id: {last_badge:#0x}, scanned {:?} ago", time.elapsed())
|
|
}
|
|
|
|
async fn phil(state: State<AppState>) -> String {
|
|
let _fish = state.fish.acquire().await.unwrap();
|
|
let (last_badge, time) = *state.last_badge.read().await;
|
|
info!("id: {last_badge:#0x}, scanned {:?} ago", time.elapsed());
|
|
let mut conn = TcpStream::connect(FISH).await.unwrap();
|
|
|
|
if last_badge != 0 {
|
|
let req = Request::SetBadge(common::Name::Phil, last_badge);
|
|
conn.write_all(&to_allocvec(&req).unwrap()).await.unwrap();
|
|
conn.flush().await.unwrap();
|
|
}
|
|
|
|
|
|
String::from(format!("{last_badge:#0x} is now phil"))
|
|
|
|
}
|
|
|