[WIP] A simple wake-on-lan service

add ping route, utils module, and misc html tweaks

vielle.dev 9d5a4967 4b3aee7c

verified
+105 -36
+2 -2
src/config.rs
··· 35 35 #[derive(Deserialize, Serialize, Debug, Clone)] 36 36 pub struct Target { 37 37 #[serde( 38 - deserialize_with = "crate::mac::deserialize_mac", 39 - serialize_with = "crate::mac::serialize_mac" 38 + deserialize_with = "crate::utils::deserialize_mac", 39 + serialize_with = "crate::utils::serialize_mac" 40 40 )] 41 41 pub mac: crate::mac::MacAddress, 42 42 pub ip: Option<String>,
-15
src/mac.rs
··· 1 1 use std::{fmt::Display, net::UdpSocket, num::ParseIntError, str::FromStr}; 2 2 3 - use serde::{Deserialize, Deserializer, Serializer, de::Error}; 4 3 use thiserror::Error; 5 4 6 5 #[derive(Clone, Debug)] ··· 71 70 ])) 72 71 } 73 72 } 74 - 75 - pub fn deserialize_mac<'de, D>(de: D) -> Result<MacAddress, D::Error> 76 - where 77 - D: Deserializer<'de>, 78 - { 79 - MacAddress::from_str(&String::deserialize(de)?).map_err(Error::custom) 80 - } 81 - 82 - pub fn serialize_mac<S>(mac: &MacAddress, se: S) -> Result<S::Ok, S::Error> 83 - where 84 - S: Serializer, 85 - { 86 - se.serialize_str(&mac.to_string()) 87 - }
+1
src/main.rs
··· 3 3 mod config; 4 4 mod mac; 5 5 mod server; 6 + mod utils; 6 7 7 8 #[derive(thiserror::Error, Debug)] 8 9 enum Error {
+18 -3
src/server.rs
··· 1 - use std::{sync::Arc, time::Instant}; 1 + use std::{net::IpAddr, pin::Pin, sync::Arc, time::Instant}; 2 2 3 3 use axum::{ 4 4 Json, Router, 5 - extract::{Request, State}, 5 + body::Body, 6 + extract::{Query, Request, State}, 7 + handler::Handler, 6 8 http::Response, 7 9 middleware::{self}, 8 10 routing::{get, post}, ··· 15 17 16 18 #[derive(Deserialize)] 17 19 struct WakeRequest { 18 - #[serde(deserialize_with = "crate::mac::deserialize_mac")] 20 + #[serde(deserialize_with = "crate::utils::deserialize_mac")] 19 21 mac: MacAddress, 20 22 } 21 23 ··· 30 32 }) 31 33 } 32 34 35 + #[derive(Deserialize)] 36 + struct PingRequest { 37 + #[serde(deserialize_with = "crate::utils::deserialize_ip")] 38 + ip: IpAddr, 39 + } 40 + 41 + async fn ping(Query(req): Query<PingRequest>) -> Result<Json<bool>, Response<String>> { 42 + println!("Pinging {}", req.ip); 43 + 44 + Ok(Json(false)) 45 + } 46 + 33 47 async fn config(State(conf): State<Arc<Config>>) -> Json<Config> { 34 48 Json((*conf).clone()) 35 49 } ··· 48 62 Router::new() 49 63 .route("/wake", post(wake)) 50 64 .route("/config", get(config)) 65 + .route("/ping", get(ping)) 51 66 .with_state(Arc::new(conf)) 52 67 .merge(dist::main()) 53 68 .layer(middleware::from_fn(log))
+33
src/utils.rs
··· 1 + use std::{net::IpAddr, str::FromStr}; 2 + 3 + use serde::{Deserialize, Deserializer, Serializer, de::Error}; 4 + 5 + use crate::mac::MacAddress; 6 + 7 + pub fn deserialize_ip<'de, D>(de: D) -> Result<IpAddr, D::Error> 8 + where 9 + D: Deserializer<'de>, 10 + { 11 + IpAddr::from_str(&String::deserialize(de)?).map_err(Error::custom) 12 + } 13 + 14 + pub fn serialize_ip<S>(ip: &IpAddr, se: S) -> Result<S::Ok, S::Error> 15 + where 16 + S: Serializer, 17 + { 18 + se.serialize_str(ip.to_string().as_str()) 19 + } 20 + 21 + pub fn deserialize_mac<'de, D>(de: D) -> Result<MacAddress, D::Error> 22 + where 23 + D: Deserializer<'de>, 24 + { 25 + MacAddress::from_str(&String::deserialize(de)?).map_err(Error::custom) 26 + } 27 + 28 + pub fn serialize_mac<S>(mac: &MacAddress, se: S) -> Result<S::Ok, S::Error> 29 + where 30 + S: Serializer, 31 + { 32 + se.serialize_str(&mac.to_string()) 33 + }
+10 -4
web/src/App.svelte
··· 19 19 </script> 20 20 21 21 <ThemeProvider /> 22 - <h1>Wake on Lan</h1> 23 - {#each targets as [name, { mac, ip, url }]} 24 - <Power {name} {mac} {ip} {url} /> 25 - {/each} 22 + <main> 23 + <h1>Wake on Lan</h1> 24 + <ul> 25 + {#each targets as [name, { mac, ip, url }]} 26 + <li> 27 + <Power {name} {mac} {ip} {url} /> 28 + </li> 29 + {/each} 30 + </ul> 31 + </main>
+31 -11
web/src/lib/Power.svelte
··· 12 12 ip: string | null; 13 13 url: string | null; 14 14 } = $props(); 15 + 16 + let online: boolean | undefined = $state(undefined); 17 + const checkStatus: () => Promise<void> = async () => { 18 + if (!ip) return; 19 + const active = await Api.ping({ ip }); 20 + online = active; 21 + // check every 30s if online, or every 5 seconds if offline 22 + setTimeout(checkStatus, (active ? 30 : 5) * 1000); 23 + }; 24 + checkStatus(); 15 25 </script> 16 26 17 27 <section> ··· 22 32 Power On 23 33 </button> 24 34 25 - {#if url} 26 - <a href={url}>{url}</a> 27 - &bull; 28 - {/if} 29 - {name} 30 - &bull; 31 - {mac} 32 - {#if ip} 33 - &bull; 34 - <a href={`http://${ip}/`}>{ip}</a> 35 - {/if} 35 + <div class="name"> 36 + {name} <span class="status" data-status={online}></span> 37 + </div> 38 + <div class="mac">{mac}</div> 39 + {#if ip}<div class="ip"><a href={`http://${ip}/`}>{ip}</a></div>{/if} 40 + {#if url}<div class="url"><a href={url}>{url}</a></div>{/if} 36 41 </section> 42 + 43 + <style> 44 + .status { 45 + display: inline-block; 46 + width: 0.5em; 47 + height: 0.5em; 48 + 49 + &[data-status="true"] { 50 + background: green; 51 + } 52 + &[data-status="false"] { 53 + background: red; 54 + } 55 + } 56 + </style>
+10 -1
web/src/lib/api.ts
··· 94 94 wake: route({ 95 95 route: "/wake", 96 96 method: "POST", 97 + input: z.object({ 98 + mac: z.string(), 99 + }), 97 100 output: z.boolean(), 101 + }), 102 + 103 + ping: route({ 104 + route: "/ping", 105 + method: "GET", 98 106 input: z.object({ 99 - mac: z.string(), 107 + ip: z.string(), 100 108 }), 109 + output: z.boolean(), 101 110 }), 102 111 } as const; 103 112