diff options
| author | Matthias P. Braendli <matthias.braendli@mpb.li> | 2024-01-16 22:18:32 +0100 | 
|---|---|---|
| committer | Matthias P. Braendli <matthias.braendli@mpb.li> | 2024-01-16 22:18:32 +0100 | 
| commit | 81a61c28619ef9f32e79e5698bc4162ca1ceb82d (patch) | |
| tree | d274a43a7b05a0a48269af1f32f616951b0c4288 | |
| parent | 6db211a70f95c339710ac73ec12d9e7c5664a278 (diff) | |
| download | cats-radio-node-81a61c28619ef9f32e79e5698bc4162ca1ceb82d.tar.gz cats-radio-node-81a61c28619ef9f32e79e5698bc4162ca1ceb82d.tar.bz2 cats-radio-node-81a61c28619ef9f32e79e5698bc4162ca1ceb82d.zip | |
Add Destination to Send page
| -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.dbBinary files differ index 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> | 
