aboutsummaryrefslogtreecommitdiffstats
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-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
4 files changed, 186 insertions, 16 deletions
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> {