Amateurfunkstation aus der Ferne steuern

Dieses Projekt befindet sich noch in der Entwicklungsphase. Die Angaben hier stellen nur den aktuellen Zwischenstand dar und können sich deshalb fortlaufend ändern.

Aufgabenstellung

Zu einer Amateuerfunkanlage soll eine sichere Verbindung zur Steuerung des Antennenrotors und des Funkgerätes aufgebaut werden. Die serielle RS232-Schnittstelle des Antennenrotors soll dabei remote über TCP angesprochen werden. Hierzu soll ein VPN-Netzwerk aufgebaut werden. Es können sich also nur in diesem Netzwerk angemeldete Geräte gegenseitig sehen.

Softwarekomponenten

Mittels der Software Tailscale kann eine Verbindung zwischen zwei und mehr Geräten vermittelt werden. Damit kein externer Cloud-Server von Tailscale verwendet werden muss, läuft auf einem Raspberry Pi, der mit einer festen öffentlichen IP-Adresse ausgestattet ist, die Software headscale in Form eines Docker-Containers. Diese Software dient also als „Vermittlungsstelle“ für die Tailscale-Software auf den beteiligten Clients. Nachdem diese von Headscale die IP-Adressen der Gegenstelle erhalten haben, kommunizieren die Geräte direkt miteinander. Der Headscale-Server ist an dieser Kommunikation nicht mehr beteiligt.

Die serielle Schnittstelle wird durch die Software ser2net mit dem TCP-Netwerk verbunden und mit socat werden die Daten zwischen zwei IP-Adressen transportiert.

Übersicht der Topologie

[Internet]

▼ (91.137.72.107)
[Haupt-Raspberry Pi]

├── [Traefik (Netzwerk: proxy)] ────┬──── [Headscale-Container] (Port 8080, Domain: headscale.lang-dieter.de)
│ │
│ ▼ (Tailscale-VPN)
│ [Externer Raspberry Pi]
│ │
│ ▼
│ [socat/ser2net] ──── [Funkstation (RS232)]

└── [Andere Container]

[Anwender mit Client-Rechner Windows]

├── [SPID-Softwar Rotorsteuerung]
│ │
│ ▼ (Tailscale-VPN)

headscale am bestehenden Traefik-Server mit öffentlicher IP ergänzen

headscale als Container einrichten

Headscale wird auf dem bestehenden Rasperry Pi mit Traefik im Hintergrund aus zusätzlicher Container hinzugefügt.

Ich verwende folgende Verzeichnisstruktur:
/srv/containers/
├── traefik/ # Bestehende Traefik-Instanz (Netzwerk: proxy)
│ └── docker-compose.yml

├── headscale/ # Headscale-Instanz
│ ├── docker-compose.yml
│ ├── config/
│ │ ├── config.yaml
│ │ └── acls.yaml
│ └── data/

└── … # Andere Container

Inhalt der Datei /src/containers/headscale/docker-compose.yml:

services:
  headscale:
    image: headscale/headscale:latest
    container_name: headscale
    restart: unless-stopped
    read_only: true
    tmpfs:
      - /var/run/headscale
    ports:
      - target: 3478
        published: 3478
        protocol: udp
        mode: host
    volumes:
      - ./config:/etc/headscale:ro
      - ./data:/var/lib/headscale
    command: serve
    healthcheck:
      test: ["CMD", "headscale", "health"]
    networks:
      - proxy
    labels:
      - traefik.enable=true

      # HTTP-Router
      - traefik.http.routers.headscale-http.rule=Host(`headscale.lang-dieter.de`)
      - traefik.http.routers.headscale-http.entrypoints=web

      # HTTPS-Router
      - traefik.http.routers.headscale-https.rule=Host(`headscale.lang-dieter.de`)
      - traefik.http.routers.headscale-https.entrypoints=websecure
      - traefik.http.routers.headscale-https.tls=true
      - traefik.http.routers.headscale-https.tls.certresolver=tls_resolver
      - traefik.http.routers.headscale-https.tls.domains[0].main=headscale.lang-dieter.de

      # Middleware für DERP Websockets
      - traefik.http.middlewares.headscale-headers.headers.customrequestheaders.Connection=Upgrade
      - traefik.http.middlewares.headscale-headers.headers.customrequestheaders.Upgrade=websocket

      # Service-Konfiguration
      - traefik.http.services.headscale.loadbalancer.server.scheme=http
      - traefik.http.services.headscale.loadbalancer.server.port=8087

networks:
  proxy:
    external: true


Die Konfiguration wird in der Datei /srv/containers/headscale/config/config.yaml hinterlegt:

# =============================================================================
# HEADSCALE KONFIGURATION
# Angepasst für Traefik Reverse Proxy & Subdomain headscale.lang-dieter.de
# =============================================================================

# Die externe URL unter der Traefik Ihr Headscale erreichbar macht
server_url: https://headscale.lang-dieter.de

# Adressen, auf denen Headscale INNERHALB des Docker-Containers lauscht
listen_addr: 0.0.0.0:8087
metrics_listen_addr: 0.0.0.0:9097

# =============================================================================
# DATENBANK (SQLite)
# =============================================================================
database:
  type: sqlite
  sqlite:
    path: /var/lib/headscale/db.sqlite

# =============================================================================
# NETZWERK-PRÄFIXE (Interne VPN-IPs für Ihre Geräte)
# =============================================================================
prefixes:
  v4: 100.64.0.0/10
  v6: fd7a:115c:a1e0::/48

# =============================================================================
# DNS EINSTELLUNGEN
# =============================================================================
dns:
  magic_dns: false
  base_domain: net.lang-dieter.de
  override_local_dns: true
  nameservers:
    global:
      - 1.1.1.1
      - 8.8.8.8
  search_domains: []
  extra_records: []

# =============================================================================
# KRYPTOGRAFIE SCHLÜSSEL
# =============================================================================
private_key_path: /var/lib/headscale/private.key
noise:
  private_key_path: /var/lib/headscale/noise_private.key

# =============================================================================
# DERP (RELAY SERVER) EINSTELLUNGEN
# =============================================================================
derp:
  # Keine externen, öffentlichen Tailscale-Server abfragen
  urls: []  
  paths: []
  auto_update_enabled: false

  # Integrierten DERP-Server aktivieren
  server:
    enabled: true
    region_id: 901
    region_code: "raspi-relay"
    region_name: "Mein privater Raspi DERP Server"
    
    # WICHTIG: Auf 8088 geändert, um Konflikt mit dem Hauptdienst (8087) zu vermeiden!
    listen_addr: "0.0.0.0:8088"  
    
    # STUN läuft über UDP und wird in der Compose direkt nach außen gereicht
    stun_listen_addr: "0.0.0.0:3478"
    private_key_path: /var/lib/headscale/derp_server_private.key

Nun starten wir den Container mit:

docker compose up -d &

Firewall-Regeln am Host ergänzen

sudo ufw allow 3478/udp   # STUN für DERP
sudo ufw enable

Headscale-ACLs (Zugangskontrolle einrichten)

sudo nano /srv/containers/headscale/config/acls.yaml
acls:
  - action: accept
    src: ["100.64.0.0/10"]  # Erlaube alle Headscale-IPs
    dst: ["100.64.0.0/10:5000"]  # Erlaube Zugriff auf Port 5000

Headscale-Benutzer für den Bereich Schlüsselverwaltung anlegen

docker exec -it headscale headscale user create funkstation-admin

Liste der Headscale-Benutzer
anzeigen lassen, da im nächsten Befehl die User-ID benötigt wird (in unserem Fall die Nummer 1)

docker exec -it headscale headscale user list

Auth-Key für den externen Raspberry Pi (an der Funkstation) erzeugen:

docker exec -it headscale headscale preauthkey create --user 1 --expiration=24h --reusable

Den damit erzeugten Key kopieren wir uns, damit er auf dem externen Raspi eingefügt werden kann.

Eine Liste der API-Keys kann man sich wie folgt anzeigen lassen:

docker exec -it headscale headscale apikeys list

Arbeiten auf externen Rasperry Pi an der Funkstation

Verbindung mit headscale einrichten

Tailscale installieren und den Geräte-Key hinterlegen (ersetze <DEIN_AUTH_KEY> durch den generierten Key):

curl -fsSL https://tailscale.com/install.sh | sh
sudo tailscale up --login-server=https://headscale.lang-dieter.de --auth-key=<DEIN_AUTH_KEY> --hostname=funkstation-pi

Wenn alles funktioniert, sollte folgender Befehl eine IP-Adresse im Bereich 100.64.x.y anzeigen:

tailscale status

TCP-to-Serial-Bridge am Funkgeräte-Pi auf Port 5000 zur seriellen Schnittstelle einrichten:

socat TCP-LISTEN:5000,fork /dev/ttyUSB0,raw,echo=0,baud=9600

Für Dauerbetrieb richten wir es als systemd-Dienst ein:

sudo nano /etc/systemd/system/tcp-serial-bridge.service

mit folgendem Inhalt:

[Unit]
Description=TCP to Serial Bridge for Funkstation
After=network.target

[Service]
ExecStart=/usr/bin/socat TCP-LISTEN:5000,fork /dev/ttyUSB0,raw,echo=0,baud=9600
Restart=always
User=root

[Install]
WantedBy=multi-user.target

Liste der Dienste aktualisieren und den neuen Dienst starten:

sudo systemctl daemon-reload
sudo systemctl enable tcp-serial-bridge
sudo systemctl start tcp-serial-bridge

Firewall-Regeln am Funkgeräte-Rechner ergänzen

sudo ufw allow 5000/tcp   # TCP-Port für Funkstation
sudo ufw enable

Software zur Spannungsanzeige und Ansteuerung Relaisplatine

Besonderheit:
Von den zu schaltenden Relais war nur eines mit dem GPIO des Raspberry Pi verbunden. Die anderen Relais befanden sich auf einer mittels USB-Anschluß verbundenen Relaisplatine von Abacom.
https://www.electronic-software-shop.com/hardware/usb-relaiskarte-lrb-8-fach.html

Von den dort vorhanden 8 Relais werden in diesem Projekt nur 6 verwendet. Die restlichen wurden in der Config.json als Reserve beschriftet. Dadurch werden sie von unserer Software automatisch als inaktiv behandelt.

Die Software zur Ansteuerung der Relaisplatine und Messung der 12-V Versorgungsspanne wurde in der Programmiersprache Golang entwickelt. Das ADS-1115 Modul kann maximal 3 V messen, wenn es mit einer Betriebsspannung von 3,3V betrieben wird.. Deshalb muss die zu überwachende Spannung durch einen Spannungsteiler reduziert werden. Zur Sicherheit wurde mit einer zu überwachenden Spannung von 15 V gerechnet.
Für das Verhältnis wählen wir Standardwiderstände aus der E12-Reihe (R1 gegen 12V, R2 gegen Masse)):

R1 = 12 kOhm, R2 = 3 kOhm
Das ergibt ein Teileverhältnis von 5 (R1 + R2 / R2)
Bei maximal 15 V liegen 3,0 V am Pin des ADS1115 an. Im Programm wird dann der gemessene Wert einfach mit 5 multipliziert.

Das gesamte Projekt ist auf meinem privaten git-Server hinterlegt und kann wie folgt heruntergeladen werden:

cd ~
git clone https://git.lang-dieter.de/dieter/Spannung.git
cd Spannung

und wir erhalten folgende Verzeichnisstruktur:
.
└── spannung
└── web
└── statik

Die verwendeten GPIO-Ports und andere Angaben, sollen flexibel in einer Konfigurationsdatei im json-Format verwaltet werden.

In der Konfigurationsdatei hinterlegen wir diverse Werte zur Steuerung der Anwendung

  • web_port
    Portadresse, unter der wir die Anzeige der Messwerte im Browser aufrufen können
  • ssl_cert
    vollständiger Dateiname Zertifikat für Webserver
  • ssl_key
    vollständiger Dateiname für Zertifikatsschlüssel-Datei
  • i2c_Bus
    Bezeichnung des i2c-Bus, an dem der Baustein ADS1115 angeschlossen ist
  • ads1115_address_1
    Die am ADS1115 eingestellte Bus-Adresse des ersten Bausteins
  • ads1115_address_2
    Die am zweiten ADS1115 eingestellte Bus-Adresse
  • name_adc1_ch0
    Bezeichnung für Anzeige des Spannugnswertes des ersten Kanals vom ersten Baustein
  • factor_adc1_ch0
    Spannungsfactor für den ersten Kanal des ersten Bausteines.
    Berücksichtigung des vorgeschalteten Spannungsteilers um die tatsächlich gemessenen Spannungen am Chip-Eingang in den tatsächliche überwachten Spannungswert umzurechnen
  • relais-pins
    Definition der GPIO-Ports, an denen die Relais-Karte mit ihren Eingängen verdrahtet ist
Verwendete Pin-Belegung am Raspi
https://www.elektronik-kompendium.de/sites/raspberry-pi/1907101.htm
# Config-Datei bearbeiten
geany spannung/config.json
{
  "web_port": ":8080",
  "ssl_cert": "cert.pem",
  "ssl_key": "key.pem",
   "i2c_bus": "1",
  "ads1115_address_1": 72,
  "ads1115_address_2": 73,
  "name_adc1_ch0": "23cm",
  "factor_adc1_ch0": 2.0,
  "name_adc1_ch1": "13 cm",
  "factor_adc1_ch1": 2.0,
  "name_adc1_ch2": "9 cm",
  "factor_adc1_ch2": 2.0,
  "name_adc1_ch3": "6 cm",
  "factor_adc1_ch3": 2.0,
  "name_adc2_ch0": "3 cm",
  "factor_adc2_ch0": 2.0,
  "relais_pins": [
    {"id": 1, "name": "Relais 1 (230V Hauptschalter)", "gpio": "GPIO12"},
    {"id": 2, "name": "USB-Relais 1 (Reserve)", "gpio": "USB_CH1"},
    {"id": 3, "name": "USB-Relais 2 23 cm", "gpio": "USB_CH2"},
    {"id": 4, "name": "USB-Relais 3 13 cm", "gpio": "USB_CH3"},
    {"id": 5, "name": "USB-Relais 4  9 cm", "gpio": "USB_CH4"},
    {"id": 6, "name": "USB-Relais 5  6 cm", "gpio": "USB_CH5"},
    {"id": 7, "name": "USB-Relais 6 3 cm", "gpio": "USB_CH6"},
    {"id": 8, "name": "USB-Relais 7 (Reserve)", "gpio": "USB_CH7"},
    {"id": 9, "name": "USB-Relais 8 (Reserve)", "gpio": "USB_CH8"}
  ]
}

Im Unterverzeichns web liegt der mit go programmierte Webserver. Er stellt die Verbindung zwischen der Hardware und den Elementen auf der angezeigten Webseite dar.

Im weiteren Unterverzeichnis /web liegen die HTML-Seiten für den Webserver.

Erster Test des Programmcodes

# In das Hauptverzeichnis des Projektes gehen
cd Spannung

# Interne Go-Strukturen aktualisieren
go get periph.io/x/devices/v3/ads1x15
go mod tidy

# Hauptprogramm starten
go run main.go

Als Ergebnis erhalten Sie die Ausgabe
Webserver gestartet auf Port :8080

Binärdateien aus Quellcode erstellen

Als nächstes können wir unser Programm kompilieren lassen. Wir verwenden hierzu die Datei build.sh. Dabei wird auch eine Datei namens spannung.service erzeugt. Diese Datei kann zur Einrichtung der Anwendung als automatischen Systemdienst verwendet werden. Diese Datei ist für den Build-Prozess auf einem Ubuntu-Rechner. Dafür müssen die C-Bibliotheken für ARM-Prozessoren bereitgesstellt werden, damit di benötigt libusb compiliert werden kann. Wenn das Script auf einem Raspberry läuft, ist dies nicht notwendig.

# Batchdatei zum Bauen der Anwendung erzeugen
geany build.sh
# Die Batchdatei ausführbar machen
sudo chmod +x build.sh

#Den Buildvorgang starten
./build.sh

Nun steht die fertige Anwendung im Unterverzeichnis app zur Verfügung:

Zum automatischen Ablauf er Installation vom Entwicklungs-PC aus, hier das passend Script.
Aufruf mit ./deploy.sh User Rechnername

Die Datei install.sh richtet den Dienst auf dem Zielrechner ein und wird von deploy.sh aufgerufen.

Aufruf der Weboberfläche im Tailscale-Netzwerk

Auf einem Client, der ebenfalls mit dem oben eingerichteten Tailscale-Netzwerk verbunden ist, können wir damit die Weboberfläche unseres Raspi aufrufen. Angenommen es wurde ihm die Adresse 100.64.0.1. zugewiesen:

http://100.64.0.1:8080

Quickstart

Spannungsüberwachung einrichten

# I2C-Schnittstelle freischalten
sudo raspi-config
# Menüfolge 3 -> I5 -> Schnittstelle aktivieren

#Eigenschaften anzeigen lassen
i2cdetect -y 1

#Falls i2cdetect fehlt -nachinstallieren
sudo apt install i2c-tools

# Verzeichnis anlegen und dorthin wechseln
mkdir spannung
cd spannung

# app herunterladen
wget https://www.lang-dieter.de/wp/wp-content/uploads/2026/08/spannung_app.zip

# Entpacken des ZIP-Archives
unzip spannung_app.zip

# In das app-Verzeichnis wechseln
cd app

# Installation durchführen
./install.sh

#Status des Service prüfen
sudo systemctl status spannung.service

#IP-Adresse des Rechners in tailscale anzeigen lassen
tailscale status

Remote-RS232-Verbindung (Serielle Datenverbindung)

Das ist die Kernaufgabe in diesem Projekt. Auf der Clientseite (HomePC) läuft diverse Software zur Steuerung von Hardware wie Funkgeräte, Rotor usw. Die Geräte sind standardmäßig per RS232-Schnittstelle verbunden.

Die zu steuernde Hardware befindet sich an der entfernten Funkstation.

Es soll nun auf dem Heim-PC eine Clientsoftware eine RS-232-Schnittstelle vortäuschen und die darüber laufenden Daten über das Internet an einem Server an der Funkstation übertragen.

Der Server ist physikalisch über RS232 mit der Hardware verbunden und leitet die empfangenen Daten an die Geräte weiter. Umgekehrt empfängt er die von den Geräten erhalten Daten und sendet diese über das Internet an unseren Client um die Steuersoftware damit zu versorgen.

Das Projekt befindet sich gerade in Arbeit. Der aktuelle Stand kann auf meinem GIT-Server verfolgt werden

https://git.lang-dieter.de/dieter/rs2322tcp.git