diff options
-rw-r--r-- | Cargo.lock | 122 | ||||
-rw-r--r-- | Cargo.toml | 1 | ||||
-rw-r--r-- | cats-radio-node.db | bin | 0 -> 16384 bytes | |||
-rw-r--r-- | src/db.rs | 16 | ||||
-rw-r--r-- | src/main.rs | 4 | ||||
-rw-r--r-- | src/ui.rs | 36 | ||||
-rw-r--r-- | static/main.js | 26 | ||||
-rw-r--r-- | templates/dashboard.html | 5 | ||||
-rw-r--r-- | templates/send.html | 20 |
9 files changed, 219 insertions, 11 deletions
@@ -37,6 +37,21 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0942ffc6dcaadf03badf6e6a2d0228460359d5e34b57ccdc720b7382dfbd5ec5" [[package]] +name = "android-tzdata" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0" + +[[package]] +name = "android_system_properties" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" +dependencies = [ + "libc", +] + +[[package]] name = "anyhow" version = "1.0.79" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -334,6 +349,12 @@ dependencies = [ ] [[package]] +name = "bumpalo" +version = "3.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f30e7476521f6f8af1a1c4c0b8cc94f0bee37d91763d0ca2665f299b6cd8aec" + +[[package]] name = "byteorder" version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -374,6 +395,7 @@ dependencies = [ "askama_axum", "async-stream", "axum 0.7.4", + "chrono", "futures", "futures-core", "half", @@ -408,6 +430,20 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" [[package]] +name = "chrono" +version = "0.4.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f2c685bad3eb3d45a01354cedb7d5faa66194d1d58ba6e267a8de788f79db38" +dependencies = [ + "android-tzdata", + "iana-time-zone", + "js-sys", + "num-traits", + "wasm-bindgen", + "windows-targets 0.48.5", +] + +[[package]] name = "colored" version = "2.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -1079,6 +1115,29 @@ dependencies = [ ] [[package]] +name = "iana-time-zone" +version = "0.1.59" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6a67363e2aa4443928ce15e57ebae94fd8949958fd1223c4cfc0cd473ad7539" +dependencies = [ + "android_system_properties", + "core-foundation-sys", + "iana-time-zone-haiku", + "js-sys", + "wasm-bindgen", + "windows-core", +] + +[[package]] +name = "iana-time-zone-haiku" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" +dependencies = [ + "cc", +] + +[[package]] name = "idna" version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -1130,6 +1189,15 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b1a46d1a171d865aa5f83f92695765caa047a9b4cbae2cbf37dbd613a793fd4c" [[package]] +name = "js-sys" +version = "0.3.67" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a1d36f1235bc969acba30b7f5990b864423a6068a10f7c90ae8f0112e3a59d1" +dependencies = [ + "wasm-bindgen", +] + +[[package]] name = "labrador-ldpc" version = "1.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -2647,6 +2715,60 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" [[package]] +name = "wasm-bindgen" +version = "0.2.90" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1223296a201415c7fad14792dbefaace9bd52b62d33453ade1c5b5f07555406" +dependencies = [ + "cfg-if", + "wasm-bindgen-macro", +] + +[[package]] +name = "wasm-bindgen-backend" +version = "0.2.90" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fcdc935b63408d58a32f8cc9738a0bffd8f05cc7c002086c6ef20b7312ad9dcd" +dependencies = [ + "bumpalo", + "log", + "once_cell", + "proc-macro2", + "quote", + "syn 2.0.48", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.90" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3e4c238561b2d428924c49815533a8b9121c664599558a5d9ec51f8a1740a999" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.90" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bae1abb6806dc1ad9e560ed242107c0f6c84335f1749dd4e8ddb012ebd5e25a7" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.48", + "wasm-bindgen-backend", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.90" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4d91413b1c31d7539ba5ef2451af3f0b833a005eb27a631cec32bc0635a8602b" + +[[package]] name = "webpki-roots" version = "0.25.3" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -10,6 +10,7 @@ anyhow = "1.0" askama = { version = "0.12", features = ["with-axum"] } askama_axum = "0.4" axum = "0.7" +chrono = "0.4" simple_logger = "4.3" log = "0.4" serde = { version = "1.0", features = ["derive"] } diff --git a/cats-radio-node.db b/cats-radio-node.db Binary files differindex e69de29..5cf7cb0 100644 --- a/cats-radio-node.db +++ b/cats-radio-node.db @@ -5,7 +5,8 @@ use sqlx::SqlitePool; #[derive(Clone)] pub struct Database { - pool : SqlitePool + pool : SqlitePool, + num_frames_received : u64, } #[derive(sqlx::FromRow, Debug)] @@ -25,7 +26,16 @@ impl Database { .await .expect("could not run SQLx migrations"); - Self { pool } + let num_frames_received : i64 = sqlx::query_scalar(r#"SELECT COUNT(id) FROM frames_received"#) + .fetch_one(&pool) + .await + .expect("could not count frames"); + + Self { pool, num_frames_received: num_frames_received.try_into().unwrap() } + } + + pub fn get_num_received_frames(&self) -> u64 { + self.num_frames_received } pub async fn store_packet(&mut self, packet: &[u8]) -> anyhow::Result<()> { @@ -41,6 +51,8 @@ impl Database { .await? .last_insert_rowid(); + self.num_frames_received += 1; + debug!("INSERTed row {id}"); Ok(()) } diff --git a/src/main.rs b/src/main.rs index fd4cc03..6b79581 100644 --- a/src/main.rs +++ b/src/main.rs @@ -14,6 +14,7 @@ struct AppState { conf : config::Config, db : db::Database, transmit_queue : mpsc::Sender<Vec<u8>>, + start_time : chrono::DateTime<chrono::Utc>, } type SharedState = Arc<Mutex<AppState>>; @@ -59,7 +60,8 @@ async fn main() -> std::io::Result<()> { let shared_state = Arc::new(Mutex::new(AppState { conf : conf.clone(), db : db::Database::new().await, - transmit_queue: packet_send.clone(), + transmit_queue : packet_send.clone(), + start_time : chrono::Utc::now(), })); if conf.freq == 0 { @@ -16,7 +16,7 @@ use tower_http::services::ServeDir; use ham_cats::{ buffer::Buffer, - whisker::Identification, + whisker::{Identification, Destination}, }; use crate::{config, radio::MAX_PACKET_LEN}; @@ -51,6 +51,8 @@ struct DashboardTemplate<'a> { title: &'a str, page: ActivePage, conf: config::Config, + node_startup_time: String, + num_received_frames: u64, packets: Vec<UIPacket>, } @@ -64,7 +66,10 @@ struct UIPacket { } async fn dashboard(State(state): State<SharedState>) -> DashboardTemplate<'static> { - let mut db = state.lock().unwrap().db.clone(); + let (conf, mut db, node_startup_time) = { + let st = state.lock().unwrap(); + (st.conf.clone(), st.db.clone(), st.start_time.clone()) + }; let packets = match db.get_most_recent_packets(10).await { Ok(v) => v, @@ -106,9 +111,11 @@ async fn dashboard(State(state): State<SharedState>) -> DashboardTemplate<'stati DashboardTemplate { title: "Dashboard", - conf: state.lock().unwrap().conf.clone(), + conf, page: ActivePage::Dashboard, - packets + num_received_frames : db.get_num_received_frames(), + node_startup_time : node_startup_time.format("%Y-%m-%d %H:%M:%S").to_string(), + packets, } } @@ -145,11 +152,18 @@ async fn send(State(state): State<SharedState>) -> SendTemplate<'static> { } #[derive(Deserialize, Debug)] +struct ApiSendPacketDestination { + callsign : String, + ssid : u8, +} + +#[derive(Deserialize, Debug)] struct ApiSendPacket { + destinations : Vec<ApiSendPacketDestination>, comment : Option<String>, } -fn build_packet(config: config::Config, comment: Option<String>) -> anyhow::Result<Vec<u8>> { +fn build_packet(config: config::Config, payload: ApiSendPacket) -> anyhow::Result<Vec<u8>> { let mut buf = [0; crate::radio::MAX_PACKET_LEN]; let mut pkt = ham_cats::packet::Packet::new(&mut buf); pkt.add_identification( @@ -158,11 +172,19 @@ fn build_packet(config: config::Config, comment: Option<String>) -> anyhow::Resu ) .map_err(|e| anyhow!("Could not add identification to packet: {e}"))?; - if let Some(c) = comment { + if let Some(c) = payload.comment { pkt.add_comment(&c) .map_err(|e| anyhow!("Could not add comment to packet: {e}"))?; } + for dest in payload.destinations { + let dest = Destination::new(false, 0, &dest.callsign, dest.ssid) + .ok_or(anyhow!("Cound not create destination"))?; + + pkt.add_destination(dest) + .map_err(|e| anyhow!("Could not add destination to packet: {e}"))?; + } + let mut buf2 = [0; crate::radio::MAX_PACKET_LEN]; let mut data = Buffer::new_empty(&mut buf2); pkt.fully_encode(&mut data) @@ -179,7 +201,7 @@ async fn post_packet(State(state): State<SharedState>, Json(payload): Json<ApiSe info!("send_packet {:?}", payload); - match build_packet(config, payload.comment) { + match build_packet(config, payload) { Ok(p) => { info!("Built packet of {} bytes", p.len()); match transmit_queue.send(p).await { diff --git a/static/main.js b/static/main.js index 8687dc1..ad50976 100644 --- a/static/main.js +++ b/static/main.js @@ -1,13 +1,37 @@ -async function btn_send_packet() { +async function btn_add_destination() { + const template = document.getElementById('destination_template'); + + let clon = template.content.cloneNode(true); + document.getElementById('destinations').appendChild(clon); +} + +async function btn_remove_destination(element_clicked) { + element_clicked.parentElement.remove() +} +async function btn_send_packet() { let data = { 'comment': null, + 'destinations': [], }; if (document.getElementById('with_comment').checked) { data.comment = document.getElementById('whisker_comment').value; } + const destinations = document.getElementById('destinations'); + const destList = destinations.querySelectorAll("p.destination"); + for (let i = 0; i < destList.length; i++) { + const dest_callsign = destList[i].querySelector("input.dest_callsign").value; + const dest_ssid_str = destList[i].querySelector("input.dest_ssid").value; + const dest_ssid = parseInt(dest_ssid_str, 10); + if (dest_ssid < 0 || dest_ssid > 255) { + alert("SSID must be between 0 and 255"); + return; + } + data.destinations.push({'callsign': dest_callsign, 'ssid': dest_ssid}); + } + await post('/api/send_packet', data); } diff --git a/templates/dashboard.html b/templates/dashboard.html index be176c4..fa899ba 100644 --- a/templates/dashboard.html +++ b/templates/dashboard.html @@ -2,6 +2,11 @@ <div class=""> <h1>Dashboard</h1> <div> + <h2>Statistics</h2> + <p>This node is up since {{ node_startup_time }}</p> + <p>Database contains {{ num_received_frames }} received frames</p> + </div> + <div> <h2>Ten most recent packets</h2> <ul> {% for packet in packets %} diff --git a/templates/send.html b/templates/send.html index e287b18..2a669e4 100644 --- a/templates/send.html +++ b/templates/send.html @@ -1,8 +1,28 @@ {% include "head.html" %} <div class=""> <h1>Send a frame</h1> + <p> + One main feature of CATS is that packets are constructed from Whiskers. + Each Whisker represents one possible attribute of data.</p> + <p>On this page you can select which whiskers to include in your packet.</p> <div> + <h2>Identification</h2> + <p>{{ conf.callsign }}-{{ conf.ssid }}</p> + <h2>Destination Whisker</h2> + <p>CATS packets can optionally have one or more destinations. + This can be useful for e.g. sending a message to another amateur radio operator, + or for communicating with a service. + The destination consists of a UTF-8 callsign and an SSID byte.</p> + <template id="destination_template"> + <p class="destination"> + <input class="textinput dest_callsign" type="text" placeholder="Type callsign here"> + <input class="textinput dest_ssid" type="text" placeholder="Type SSID here"> + <button class="btn" type="button" onclick="btn_remove_destination(this)">Remove</button> + </p> + </template> + <div id="destinations"></div> + <button class="btn" type="button" onclick="btn_add_destination()">Add destination</button> <h2>Comment Whisker</h2> <div> <input type="checkbox" id="with_comment" value="Include Comment" checked> |