diff --git a/config.example.toml b/config.example.toml
index 019f721483af154424157e191162389ad6354552..76ce98d821857eaaf68568e60293ccabcbeaf66f 100644
--- a/config.example.toml
+++ b/config.example.toml
@@ -5,10 +5,11 @@ ssid = 0
 
 [felinet.beacon]
 period_seconds = 120
+icon = 0
 max_hops = 3
 latitude = 45.0
 longitude = -66.0
 altitude = 30 # meters
-comment = "Check out my cool CATS I-gate!"
+comment = "Check out my cool CATS SDR I-gate!"
 antenna_height = 10 # meters above ground
 antenna_gain = 2 # dBi
diff --git a/src/config.rs b/src/config.rs
index e0c3f0bd7caa40bab7e9f5dc164be1cc9cec0dcd..3bceba17d6a93dbe037f5d2655ef855c52dcf2fc 100644
--- a/src/config.rs
+++ b/src/config.rs
@@ -1,13 +1,31 @@
-use std::fs;
+use std::{fs, time::Duration};
 
 use anyhow::{bail, Context};
 use serde::Deserialize;
+use serde_with::{serde_as, DurationSeconds};
+
+#[serde_as]
+#[derive(Deserialize, Clone)]
+pub struct BeaconConfig {
+    #[serde_as(as = "DurationSeconds<u64>")]
+    pub period_seconds: Duration,
+    pub icon: u16,
+    #[serde(default)]
+    pub max_hops: u8,
+    pub latitude: Option<f64>,
+    pub longitude: Option<f64>,
+    pub altitude: Option<f64>,
+    pub comment: Option<String>,
+    pub antenna_height: Option<u8>,
+    pub antenna_gain: Option<f32>,
+}
 
 #[derive(Deserialize, Clone)]
 pub struct FelinetConfig {
     pub address: String,
     pub callsign: String,
     pub ssid: u8,
+    pub beacon: BeaconConfig,
 }
 
 #[derive(Deserialize, Clone)]
diff --git a/src/gate.rs b/src/gate.rs
index 5d224a05ea725740aed56d23548a9bcc5bbcd6d7..78d7afc72ef88f4b9d5c4ea14f56e6a5ae8b1d79 100644
--- a/src/gate.rs
+++ b/src/gate.rs
@@ -1,8 +1,129 @@
-use crate::felinet::{handler_client::HandlerClient, PacketIn as SemiPacketIn};
+use crate::{
+    config::FelinetConfig,
+    felinet::{handler_client::HandlerClient, PacketIn as SemiPacketIn},
+    MAX_PACKET_LEN,
+};
+use anyhow::{anyhow, Context};
 use async_stream::stream;
-use std::time::Duration;
+use half::f16;
+use ham_cats::{
+    buffer::Buffer,
+    packet::Packet,
+    whisker::{Gps, Identification, NodeInfoBuilder, Route},
+};
+use std::time::{Duration, Instant};
 use tokio::sync::broadcast;
 use tonic::{transport::Channel, Request};
+use uuid::Uuid;
+
+const HARDWARE_ID: u16 = 0x7c86;
+const SOFTWARE_ID: u8 = 0x00;
+
+pub fn beacon_forever(
+    c: &FelinetConfig,
+    felinet_send: broadcast::Sender<SemiPacketIn>,
+    uuid: Uuid,
+) -> anyhow::Result<()> {
+    let start_time = Instant::now();
+
+    let mut buf = [0; MAX_PACKET_LEN];
+    let mut packet = Packet::new(&mut buf);
+    packet
+        .add_identification(
+            Identification::new(&c.callsign, c.ssid, c.beacon.icon).context(
+                "Config does not describe a valid identification (callsign, ssid, icon)",
+            )?,
+        )
+        .map_err(|e| anyhow!("Could not add identification to beacon packet: {e}"))?;
+
+    let period = c.beacon.period_seconds;
+
+    if let (Some(latitude), Some(longitude), Some(altitude)) =
+        (c.beacon.latitude, c.beacon.longitude, c.beacon.altitude)
+    {
+        packet
+            .add_gps(Gps::new(
+                latitude,
+                longitude,
+                f16::from_f64(altitude),
+                0,
+                0.0,
+                f16::ZERO,
+            ))
+            .map_err(|e| anyhow!("Could not add gps to beacon packet: {e}"))?;
+    }
+
+    if let Some(c) = &c.beacon.comment {
+        packet
+            .add_comment(c)
+            .map_err(|e| anyhow!("Could not add gps to beacon packet: {e}"))?;
+    }
+
+    let mut info_builder = NodeInfoBuilder::default()
+        .hardware_id(HARDWARE_ID)
+        .software_id(SOFTWARE_ID);
+
+    if let Some(height) = c.beacon.antenna_height {
+        info_builder = info_builder.antenna_height(height);
+    }
+
+    if let Some(gain) = c.beacon.antenna_gain {
+        info_builder = info_builder.antenna_gain(gain as f64);
+    }
+
+    // This is going to be overwritten.
+    // We add it now to ensure there are no errors when we unwrap it later
+    packet
+        .add_node_info(info_builder.build())
+        .map_err(|e| anyhow!("Could not add info to beacon packet: {e}"))?;
+
+    let mut r = Route::new(c.beacon.max_hops);
+    r.push_internet().context("Could not create beacon route")?;
+    packet
+        .add_route(r)
+        .map_err(|e| anyhow!("Could not add route to beacon packet: {e}"))?;
+
+    let internet_packet_bytes = packet.encode().to_vec();
+    tokio::task::spawn(async move {
+        let mut internet_packet_buf = [0; MAX_PACKET_LEN];
+        let mut internet_packet_buf = Buffer::new_empty(&mut internet_packet_buf);
+        internet_packet_buf.extend(&internet_packet_bytes);
+        let mut packet =
+            Packet::decode(internet_packet_buf).expect("Could not re-create internet packet");
+
+        let mut tmp = [0; MAX_PACKET_LEN];
+
+        loop {
+            tokio::time::sleep(period).await;
+
+            let uptime = (Instant::now() - start_time)
+                .as_secs()
+                .try_into()
+                .expect("Could not calculate uptime");
+
+            let info = info_builder.uptime(uptime).build();
+
+            packet.clear_node_info();
+            packet
+                .add_node_info(info)
+                .expect("Could not add node info to rf packet");
+
+            let semi = packet
+                .clone_backing(&mut tmp)
+                .semi_encode()
+                .expect("Could not encode beacon packet")
+                .to_vec();
+            let semi = SemiPacketIn {
+                raw: semi,
+                uuid: uuid.into(),
+            };
+
+            let _ = felinet_send.send(semi.clone());
+        }
+    });
+
+    Ok(())
+}
 
 pub async fn felinet_send_forever(
     mut client: HandlerClient<Channel>,
diff --git a/src/main.rs b/src/main.rs
index f2f0b0c4149134d85ae3ca284c21ded91d222154..8cbed7ad90d8b543436ec93d02e85e337931e98d 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -2,7 +2,7 @@ use crate::felinet::handler_client::HandlerClient;
 
 use config::Config;
 use felinet::PingRequest;
-use gate::felinet_send_forever;
+use gate::{beacon_forever, felinet_send_forever};
 use std::{str::FromStr, time::Duration};
 use tokio::sync::broadcast;
 use tonic::{transport::Endpoint, Request};
@@ -33,29 +33,25 @@ async fn gate_forever(config: &Config) -> anyhow::Result<()> {
 
     let tx = broadcast::Sender::new(16);
 
-    let client = if let Some(felinet_config) = &config.felinet {
+    if let Some(felinet_config) = &config.felinet {
         let endpoint = Endpoint::from_str(&felinet_config.address)?
             .keep_alive_while_idle(true)
             .keep_alive_timeout(Duration::from_secs(5))
             .http2_keep_alive_interval(Duration::from_secs(5))
             .tcp_keepalive(Some(Duration::from_secs(5)));
 
-        Some(HandlerClient::connect(endpoint).await?)
-    } else {
-        eprintln!(
-            "No FELINET section specified in configuration. Received packets will not be uploaded!"
-        );
-        None
-    };
+        let client = HandlerClient::connect(endpoint).await?;
 
-    if let Some(client) = client {
         {
             let client = client.clone();
+            let tx = tx.clone();
             tokio::task::spawn(async {
                 felinet_send_forever(client, tx).await;
             });
         }
 
+        beacon_forever(felinet_config, tx.clone(), uuid)?;
+
         {
             let mut client = client.clone();
             tokio::task::spawn(async move {
@@ -69,6 +65,10 @@ async fn gate_forever(config: &Config) -> anyhow::Result<()> {
                 }
             });
         }
+    } else {
+        eprintln!(
+            "No FELINET section specified in configuration. Received packets will not be uploaded!"
+        );
     }
 
     decoder::decode_forever();