aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--Cargo.lock1
-rw-r--r--Cargo.toml4
-rw-r--r--README.md11
-rw-r--r--src/bin/fake-radio.rs91
-rw-r--r--src/main.rs35
-rw-r--r--src/radio.rs18
-rw-r--r--src/ui.rs58
-rw-r--r--static/main.js28
-rw-r--r--templates/head.html2
-rw-r--r--templates/send.html12
10 files changed, 242 insertions, 18 deletions
diff --git a/Cargo.lock b/Cargo.lock
index 6fd1f8d..bd58099 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -375,6 +375,7 @@ dependencies = [
"askama_axum",
"async-stream 0.2.1",
"axum 0.7.3",
+ "half",
"ham-cats",
"log",
"rand",
diff --git a/Cargo.toml b/Cargo.toml
index 452c41e..048d13d 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -19,6 +19,7 @@ tokio = { version = "1", features = ["full"] }
tower-http = { version = "0.5.0", features = ["fs"] }
ham-cats = { git = "https://gitlab.scd31.com/cats/ham-cats", commit = "d22f541c9a7e1c3a6c6e9449d87212b060f5edfb" }
+half = { version = "2" }
rf4463 = { git = "https://gitlab.scd31.com/stephen/rf4463-lib", commit = "79c8def87540f8ab2663bfa3c9fb13db344ef84e" }
rppal = { version = "0.14", features = ["hal"] }
tonic = { version = "0.10", features = ["tls", "tls-roots"] }
@@ -27,3 +28,6 @@ rand = "0.8"
# Websockets example in https://github.com/tokio-rs/axum/tree/main/examples/websockets
# tokio-tungstenite = "0.21"
+
+[[bin]]
+name = "fake-radio"
diff --git a/README.md b/README.md
index eddcfed..9414e9a 100644
--- a/README.md
+++ b/README.md
@@ -15,3 +15,14 @@ This project contains a web user interface for controlling a
Configuration read/write through UI is done.
Pending: RF4463 integration, message decoding and presentation, igate integration, UI to send messages
+
+## Additional tools
+
+### fake-radio
+
+If no radio is available, frames can be sent and received over UDP for debugging.
+cats-radio-node receives on 127.0.0.1:9073, and transmits to 127.0.0.1:9074.
+
+The `fake-radio` binary can be used to inject frames for that, and decodes those sent by cats-radio-node.
+
+Build with `cargo build --bin fake-radio`
diff --git a/src/bin/fake-radio.rs b/src/bin/fake-radio.rs
new file mode 100644
index 0000000..fd38149
--- /dev/null
+++ b/src/bin/fake-radio.rs
@@ -0,0 +1,91 @@
+use anyhow::{anyhow, Context};
+use tokio::net::UdpSocket;
+use ham_cats::{
+ buffer::Buffer,
+ whisker::{Arbitrary, Identification, Gps},
+};
+
+const MAX_PACKET_LEN : usize = 8191;
+
+#[tokio::main]
+async fn main() -> std::io::Result<()> {
+ eprintln!("Sending example packet");
+
+ let packet = build_example_packet().await.unwrap();
+ let sock = UdpSocket::bind("127.0.0.1:9074").await.unwrap();
+ sock.send_to(&packet, "127.0.0.1:9073").await.unwrap();
+
+ eprintln!("Receiving packets. Ctrl-C to stop");
+
+ let mut data = [0; MAX_PACKET_LEN];
+ while let Ok((len, _addr)) = sock.recv_from(&mut data).await {
+ let mut buf = [0; MAX_PACKET_LEN];
+ match ham_cats::packet::Packet::fully_decode(&data[2..len], &mut buf) {
+ Ok(packet) => {
+ if let Some(ident) = packet.identification() {
+ eprintln!(" Ident {}-{}", ident.callsign, ident.ssid);
+ }
+
+ if let Some(gps) = packet.gps() {
+ eprintln!(" GPS {} {}", gps.latitude(), gps.longitude());
+ }
+
+ let mut comment = [0; 1024];
+ if let Ok(c) = packet.comment(&mut comment) {
+ eprintln!(" Comment {}", c);
+ }
+
+ eprintln!(" With {} Arbitrary whiskers", packet.arbitrary_iter().count());
+ },
+ Err(e) => {
+ eprintln!(" Cannot decode packet of length {} {:?}", len, e);
+ }
+ }
+ }
+
+ Ok(())
+}
+
+async fn build_example_packet() -> anyhow::Result<Vec<u8>> {
+ let callsign = "EX4MPLE";
+ let ssid = 0;
+ let icon = 0;
+
+ let mut buf = [0; MAX_PACKET_LEN];
+ let mut pkt = ham_cats::packet::Packet::new(&mut buf);
+ pkt.add_identification(
+ Identification::new(&callsign, ssid, icon)
+ .context("Invalid identification")?,
+ )
+ .map_err(|e| anyhow!("Could not add identification to packet: {e}"))?;
+
+ pkt.add_comment("Debugging packet")
+ .map_err(|e| anyhow!("Could not add comment to packet: {e}"))?;
+
+ let latitude = 46.5;
+ let longitude = -6.2;
+ let altitude = 200u8.into();
+ let max_error = 1;
+ let heading = 120.0;
+ let speed = 1u8.into();
+
+ pkt.add_gps(Gps::new(
+ latitude,
+ longitude,
+ altitude,
+ max_error,
+ heading,
+ speed)
+ )
+ .map_err(|e| anyhow!("Could not add GPS to packet: {e}"))?;
+
+ pkt.add_arbitrary(Arbitrary::new(&[0xA5; 8]).unwrap())
+ .map_err(|e| anyhow!("Could not add arbitrary to packet: {e}"))?;
+
+ let mut buf2 = [0; MAX_PACKET_LEN];
+ let mut data = Buffer::new_empty(&mut buf2);
+ pkt.fully_encode(&mut data)
+ .map_err(|e| anyhow!("Could not encode packet: {e}"))?;
+
+ Ok(data.to_vec())
+}
diff --git a/src/main.rs b/src/main.rs
index 9a4ae2b..0a427e9 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -1,6 +1,7 @@
use anyhow::Context;
use log::{debug, info, warn, error};
use std::sync::{Arc, Mutex};
+use tokio::net::UdpSocket;
use tokio::sync::mpsc;
use sqlx::{Connection, SqliteConnection};
use radio::{RadioManager, MAX_PACKET_LEN};
@@ -11,7 +12,8 @@ mod ui;
struct AppState {
conf : config::Config,
- db : Mutex<SqliteConnection>
+ db : Mutex<SqliteConnection>,
+ transmit_queue : mpsc::Sender<Vec<u8>>,
}
type SharedState = Arc<Mutex<AppState>>;
@@ -28,17 +30,37 @@ async fn main() -> std::io::Result<()> {
let conf = config::Config::load().expect("Could not load config");
+ let (radio_rx_queue, mut packet_receive) = mpsc::channel(16);
+ let (packet_send, mut radio_tx_queue) = mpsc::channel::<Vec<u8>>(16);
+
if conf.freq == 0 {
- warn!("Frequency {0} is zero, disabling radio", conf.freq);
+ warn!("Frequency {0} is zero, disabling radio. Fake receiver udp 127.0.0.1:9073, sending to 9074", conf.freq);
+ let sock_r = Arc::new(UdpSocket::bind("127.0.0.1:9073").await?);
+ let sock_s = sock_r.clone();
+
+ // These two tasks behave like the radio, but use UDP instead of the RF channel.
+ tokio::spawn(async move {
+ let mut buf = [0; 1024];
+ while let Ok((len, addr)) = sock_r.recv_from(&mut buf).await {
+ println!("{:?} bytes received from {:?}", len, addr);
+ let packet = buf[..len].to_vec();
+ let rssi = 0f64;
+ radio_rx_queue.send((packet, rssi)).await.expect("Inject frame");
+ }
+ });
+
+ tokio::spawn(async move {
+ while let Some(p) = radio_tx_queue.recv().await {
+ sock_s.send_to(&p, "127.0.0.1:9074").await.unwrap();
+ }
+ });
}
else if !(430000..=436380).contains(&conf.freq) {
error!("Frequency {} kHz out of range (430MHz - 436.375MHz), skipping radio setup", conf.freq);
}
else {
info!("Setting up radio");
- let (packet_tx, mut packet_receive) = mpsc::channel(16);
- let (packet_send, packet_rx) = mpsc::channel(16);
- let mut radio = RadioManager::new(packet_tx, packet_rx).expect("Could not initialize radio");
+ let mut radio = RadioManager::new(radio_rx_queue, radio_tx_queue).expect("Could not initialize radio");
let channel = ((conf.freq - 430000) / 25) as u8;
radio.set_channel(channel);
@@ -82,7 +104,8 @@ async fn main() -> std::io::Result<()> {
let shared_state = Arc::new(Mutex::new(AppState {
conf,
- db: Mutex::new(conn)
+ db: Mutex::new(conn),
+ transmit_queue: packet_send.clone(),
}));
let port = 3000;
diff --git a/src/radio.rs b/src/radio.rs
index 77f7c0f..239685f 100644
--- a/src/radio.rs
+++ b/src/radio.rs
@@ -20,14 +20,14 @@ pub const MAX_PACKET_LEN: usize = 8191;
pub struct RadioManager {
radio: Rf4463<Spi, OutputPin, OutputPin, Delay>,
- tx: Sender<(Vec<u8>, f64)>,
- rx: Receiver<Vec<u8>>,
+ receive_queue: Sender<(Vec<u8>, f64)>,
+ transmit_queue: Receiver<Vec<u8>>,
rx_buf: [u8; MAX_PACKET_LEN],
temperature: Arc<Mutex<f32>>,
}
impl RadioManager {
- pub fn new(tx: Sender<(Vec<u8>, f64)>, rx: Receiver<Vec<u8>>) -> anyhow::Result<Self> {
+ pub fn new(receive_queue: Sender<(Vec<u8>, f64)>, transmit_queue: Receiver<Vec<u8>>) -> anyhow::Result<Self> {
let spi = Spi::new(Bus::Spi0, SlaveSelect::Ss0, 1_000_000, Mode::Mode0)?;
let gpio = Gpio::new()?;
let sdn = gpio.get(22)?.into_output();
@@ -44,8 +44,8 @@ impl RadioManager {
Ok(Self {
radio,
- tx,
- rx,
+ receive_queue,
+ transmit_queue,
rx_buf,
temperature,
})
@@ -65,13 +65,13 @@ impl RadioManager {
*self.temperature.lock().await = self.radio.get_temp()?;
- match self.rx.try_recv() {
+ match self.transmit_queue.try_recv() {
Ok(pkt) => {
self.tx(&pkt).await?;
}
Err(TryRecvError::Empty) => {}
Err(TryRecvError::Disconnected) => {
- bail!("RX channel disconnected")
+ bail!("TX channel disconnected")
}
}
@@ -101,11 +101,11 @@ impl RadioManager {
.start_rx(None, false)
.map_err(|e| anyhow!("{e}"))?;
- self.tx
+ self.receive_queue
.send((data.data().to_vec(), data.rssi()))
.await
.ok()
- .context("TX channel died")?;
+ .context("RX channel died")?;
}
Ok(())
diff --git a/src/ui.rs b/src/ui.rs
index 5a13bc0..aedf56c 100644
--- a/src/ui.rs
+++ b/src/ui.rs
@@ -1,9 +1,12 @@
+use anyhow::{anyhow, Context};
use std::str::FromStr;
+use axum::Json;
+use log::info;
use serde::Deserialize;
use askama::Template;
use axum::{
extract::State,
- routing::get,
+ routing::{get, post},
Router,
response::Html,
Form,
@@ -11,6 +14,11 @@ use axum::{
};
use tower_http::services::ServeDir;
+use ham_cats::{
+ buffer::Buffer,
+ whisker::Identification,
+};
+
use crate::config;
use crate::SharedState;
@@ -19,6 +27,7 @@ pub async fn serve(port: u16, shared_state: SharedState) {
.route("/", get(dashboard))
.route("/incoming", get(incoming))
.route("/send", get(send))
+ .route("/api/send_packet", post(post_packet))
.route("/settings", get(show_settings).post(post_settings))
.nest_service("/static", ServeDir::new("static"))
/* For an example for timeouts and tracing, have a look at the git history */
@@ -84,6 +93,53 @@ async fn send(State(state): State<SharedState>) -> SendTemplate<'static> {
}
}
+#[derive(Deserialize, Debug)]
+struct ApiSendPacket {
+ comment : Option<String>,
+}
+
+fn build_packet(config: config::Config, comment: Option<String>) -> 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(
+ Identification::new(&config.callsign, config.ssid, config.icon)
+ .context("Invalid identification")?,
+ )
+ .map_err(|e| anyhow!("Could not add identification to packet: {e}"))?;
+
+ if let Some(c) = comment {
+ pkt.add_comment(&c)
+ .map_err(|e| anyhow!("Could not add comment 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)
+ .map_err(|e| anyhow!("Could not encode packet: {e}"))?;
+
+ Ok(data.to_vec())
+}
+
+async fn post_packet(State(state): State<SharedState>, Json(payload): Json<ApiSendPacket>) -> StatusCode {
+ let (config, transmit_queue) = {
+ let s = state.lock().unwrap();
+ (s.conf.clone(), s.transmit_queue.clone())
+ };
+
+ info!("send_packet {:?}", payload);
+
+ match build_packet(config, payload.comment) {
+ Ok(p) => {
+ info!("Built packet of {} bytes", p.len());
+ match transmit_queue.send(p).await {
+ Ok(()) => StatusCode::OK,
+ Err(_) => StatusCode::BAD_REQUEST,
+ }
+ },
+ Err(_) =>StatusCode::INTERNAL_SERVER_ERROR,
+ }
+}
+
#[derive(Template)]
#[template(path = "settings.html")]
struct SettingsTemplate<'a> {
diff --git a/static/main.js b/static/main.js
new file mode 100644
index 0000000..8687dc1
--- /dev/null
+++ b/static/main.js
@@ -0,0 +1,28 @@
+async function btn_send_packet() {
+
+ let data = {
+ 'comment': null,
+ };
+
+ if (document.getElementById('with_comment').checked) {
+ data.comment = document.getElementById('whisker_comment').value;
+ }
+
+ await post('/api/send_packet', data);
+}
+
+async function post(url, data) {
+ const params = {
+ method: "POST",
+ headers: {
+ 'Content-Type': 'application/json'
+ },
+ body: JSON.stringify(data),
+ };
+
+ let response = await fetch(url, params);
+ if (!response.ok) {
+ const text = await response.text();
+ alert(`Error Sending: ${response.statusText} ${text}`);
+ }
+}
diff --git a/templates/head.html b/templates/head.html
index 67eb7bd..10d2b13 100644
--- a/templates/head.html
+++ b/templates/head.html
@@ -6,7 +6,7 @@
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link rel="stylesheet" href="/static/style.css" type="text/css">
<link rel="stylesheet" href="/static/font-awesome/css/font-awesome.min.css">
- <!-- <script src="/static/index.js" defer></script> -->
+ <script src="/static/main.js" defer></script>
</head>
<body>
<div class="flex">
diff --git a/templates/send.html b/templates/send.html
index b47f6b0..e287b18 100644
--- a/templates/send.html
+++ b/templates/send.html
@@ -1,6 +1,16 @@
{% include "head.html" %}
<div class="">
- Send!
+ <h1>Send a frame</h1>
+
+ <div>
+ <h2>Comment Whisker</h2>
+ <div>
+ <input type="checkbox" id="with_comment" value="Include Comment" checked>
+ <input class="textinput" type="text" id="whisker_comment" placeholder="Type comment here">
+ </div>
+
+ <button class="btn" type="button" onclick="btn_send_packet()">Send</button>
+ </div>
</div>
{% include "foot.html" %}
{# vi:set et sw=2 ts=2: #}