aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--Cargo.lock122
-rw-r--r--Cargo.toml1
-rw-r--r--cats-radio-node.dbbin0 -> 16384 bytes
-rw-r--r--src/db.rs16
-rw-r--r--src/main.rs4
-rw-r--r--src/ui.rs36
-rw-r--r--static/main.js26
-rw-r--r--templates/dashboard.html5
-rw-r--r--templates/send.html20
9 files changed, 219 insertions, 11 deletions
diff --git a/Cargo.lock b/Cargo.lock
index 1ae0ba4..dff1ed8 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -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"
diff --git a/Cargo.toml b/Cargo.toml
index 23ccf35..419374f 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -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
index e69de29..5cf7cb0 100644
--- a/cats-radio-node.db
+++ b/cats-radio-node.db
Binary files differ
diff --git a/src/db.rs b/src/db.rs
index 26f0c32..5d0f627 100644
--- a/src/db.rs
+++ b/src/db.rs
@@ -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 {
diff --git a/src/ui.rs b/src/ui.rs
index 2226a43..82c8b5c 100644
--- a/src/ui.rs
+++ b/src/ui.rs
@@ -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>