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.

Installation auf dem Server

Das gesamte Projekt ist auf meinem lokalen git-Server
https://git.lang-dieter.de/dieter/Spannung/releases
hinterlegt und kann wie folgt heruntergeladen werden
(Anpassung an die jeweils verfügbare Version notwendig);

# 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

# Sofware Spannung installieren
cd ~
wget https://git.lang-dieter.de/dieter/Spannung/releases/download/v0.2.8/Spannung-v0.2.8.zip
unzip Spannung-v0.2.8.zip
cd Spannung-v0.2.8/

# jetzt configs/config.json prüfen und ggf. anpassen
# siehe untenstehende Beschreibung

sudo ./install.sh 
sudo systemctl status Spannung.service 

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"}
  ]
}

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

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