diff options
| -rw-r--r-- | Cargo.lock | 1 | ||||
| -rw-r--r-- | Cargo.toml | 4 | ||||
| -rw-r--r-- | README.md | 11 | ||||
| -rw-r--r-- | src/bin/fake-radio.rs | 91 | ||||
| -rw-r--r-- | src/main.rs | 35 | ||||
| -rw-r--r-- | src/radio.rs | 18 | ||||
| -rw-r--r-- | src/ui.rs | 58 | ||||
| -rw-r--r-- | static/main.js | 28 | ||||
| -rw-r--r-- | templates/head.html | 2 | ||||
| -rw-r--r-- | templates/send.html | 12 | 
10 files changed, 242 insertions, 18 deletions
@@ -375,6 +375,7 @@ dependencies = [   "askama_axum",   "async-stream 0.2.1",   "axum 0.7.3", + "half",   "ham-cats",   "log",   "rand", @@ -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" @@ -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(()) @@ -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: #}  | 
