2026-06-06 21:02 Version 0.1

This commit is contained in:
Dieter Lang 2026-06-06 21:02:32 +02:00
commit afdbb2e136
13 changed files with 470 additions and 0 deletions

85
build.sh Executable file
View file

@ -0,0 +1,85 @@
#!/bin/bash
set -e
# 1. Aktuellen Linux-Benutzer ermitteln
CURRENT_USER=$(whoami)
USER_HOME=$HOME
# 2. Version & Datum aus version.json auslesen
if [ -f version.json ]; then
VERSION=$(jq -r '.version' version.json)
DATE=$(jq -r '.date' version.json)
else
VERSION="1.0.0"
DATE=$(date +'%Y-%m-%d')
fi
echo "🔧 Baue Spannung-Monitor v$VERSION ($DATE)"
# KORREKTUR: Einfache Anführungszeichen fangen die Leerzeichen für den Go-Linker ab
LDFLAGS="-X 'main.Version=$VERSION' -X 'main.BuildDate=$DATE'"
# Multi-Plattform Builds ausführen
CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -ldflags "$LDFLAGS" -o spannung-amd64 .
echo "✅ Fertig gebaut: ./spannung-amd64"
CGO_ENABLED=0 GOOS=linux GOARCH=arm64 go build -ldflags "$LDFLAGS" -o spannung-arm64 .
echo "✅ Fertig gebaut: ./spannung-arm64"
CGO_ENABLED=0 GOOS=linux GOARCH=arm GOARM=7 go build -ldflags "$LDFLAGS" -o spannung-arm32_7 .
echo "✅ Fertig gebaut: ./spannung-arm32_7"
CGO_ENABLED=0 GOOS=linux GOARCH=arm GOARM=6 go build -ldflags "$LDFLAGS" -o spannung-arm32_6 .
echo "✅ Fertig gebaut: ./spannung-arm32_6"
# Dateiberechtigungen setzen
chmod +x ./spannung-*
# Zielverzeichnis 'app' neu erstellen
rm -rf app
mkdir app
# Binaries, Konfiguration und Web-Ordner kopieren
cp spannung-* ./app/
cp config.json ./app/
cp version.json ./app/
cp web ./app/ -r
# 3. Die Service-Datei universell generiert (nutzt systemd %h/%u Specifiers)
cat << 'EOF' > ./app/spannung.service
# ==============================================================================
# KURZANLEITUNG ZUR INSTALLATION UND VERWALTUNG
# ==============================================================================
# 1. Datei kopieren nach: /etc/systemd/system/spannung.service
# 2. Systemd neu laden: sudo systemctl daemon-reload
# 3. Autostart aktivieren: sudo systemctl enable spannung.service
# 4. Dienst starten: sudo systemctl start spannung.service
# ==============================================================================
[Unit]
Description=Spannung Control Service
After=network-online.target i2c.service
Wants=network-online.target
StartLimitIntervalSec=0
[Service]
Type=simple
ExecStartPre=/bin/bash -c 'for i in {1..10}; do i2cdetect -y 1 | grep -E "^40:" | grep -q -E "\b48\b" && exit 0; echo "warte auf ADS1115..."; sleep 2; done; exit 1'
ExecStart=%h/spannung/app/spannung-arm32_7
WorkingDirectory=%h/spannung/app
User=%u
Restart=on-failure
RestartSec=5s
StartLimitIntervalSec=60s
StartLimitBurst=3
StandardOutput=append:%h/spannung/app/spannung.log
StandardError=append:%h/spannung/app/spannung.log
[Install]
WantedBy=multi-user.target
EOF
echo "📝 Service-Datei universell generiert (nutzt systemd %h/%u Specifiers)"
echo "📂 Inhalt des fertigen app-Verzeichnisses:"
ls -la app

13
config.json Normal file
View file

@ -0,0 +1,13 @@
{
"web_port": ":8080",
"i2c_bus": "/dev/i2c-1",
"ads1115_address": 72,
"voltage_factor_1": 5.0,
"voltage_factor_2": 5.0,
"relais_pins": [
{"id": 1, "name": "Relais 1 (Hauptschalter)", "gpio": "GPIO22"},
{"id": 2, "name": "Relais 2 (unbenutzt)", "gpio": "GPIO23"},
{"id": 3, "name": "Relais 3 (unbenutzt)", "gpio": "GPIO24"},
{"id": 4, "name": "Relais 4 (unbenutzt)", "gpio": "GPIO25"}
]
}

9
go.mod Normal file
View file

@ -0,0 +1,9 @@
module spannung
go 1.25.0
require (
periph.io/x/conn/v3 v3.7.3
periph.io/x/devices/v3 v3.7.4
periph.io/x/host/v3 v3.8.5
)

8
go.sum Normal file
View file

@ -0,0 +1,8 @@
github.com/jonboulle/clockwork v0.5.0 h1:Hyh9A8u51kptdkR+cqRpT1EebBwTn1oK9YfGYbdFz6I=
github.com/jonboulle/clockwork v0.5.0/go.mod h1:3mZlmanh0g2NDKO5TWZVJAfofYk64M7XN3SzBPjZF60=
periph.io/x/conn/v3 v3.7.3 h1:+8UblkC4omTB1M+jZTvTj3qoxQOTJy0ZRQm8DLUuVzc=
periph.io/x/conn/v3 v3.7.3/go.mod h1:tyV9YaYquOJ2Q2yAL0B5zk9ZvHGsbW56M6y92wjyPDQ=
periph.io/x/devices/v3 v3.7.4 h1:g9CGKTtiXS9iyDFDba4sr9pYde4dy+ZCKRPuKpKJdKo=
periph.io/x/devices/v3 v3.7.4/go.mod h1:FqFG9RotW2aCkfIlAes3qxziwgjRTncTMS5cSOcizNg=
periph.io/x/host/v3 v3.8.5 h1:g4g5xE1XZtDiGl1UAJaUur1aT7uNiFLMkyMEiZ7IHII=
periph.io/x/host/v3 v3.8.5/go.mod h1:hPq8dISZIc+UNfWoRj+bPH3XEBQqJPdFdx218W92mdc=

169
main.go Normal file
View file

@ -0,0 +1,169 @@
package main
import (
"encoding/json"
"fmt"
"log"
"os"
"sync"
"spannung/web" // Importiert das neue eigene web-Paket
"periph.io/x/conn/v3/gpio"
"periph.io/x/conn/v3/gpio/gpioreg"
"periph.io/x/conn/v3/i2c/i2creg"
"periph.io/x/conn/v3/physic"
"periph.io/x/devices/v3/ads1x15"
"periph.io/x/host/v3"
)
type Config struct {
WebPort string `json:"web_port"`
I2CBus string `json:"i2c_bus"`
ADS1115Address uint16 `json:"ads1115_address"`
Factor1 float64 `json:"voltage_factor_1"`
Factor2 float64 `json:"voltage_factor_2"`
Relais []RelaisConf `json:"relais_pins"`
}
type RelaisConf struct {
ID int `json:"id"`
Name string `json:"name"`
GPIO string `json:"gpio"`
State bool `json:"-"`
}
var (
config Config
adsDevice *ads1x15.Dev
relaisMap = make(map[int]gpio.PinIO)
mutex sync.Mutex
// Diese Variablen werden durch -ldflags beim Kompilieren befüllt:
Version = "Entwicklung"
BuildDate = "Unbekannt"
)
func main() {
confFile, err := os.ReadFile("config.json")
if err != nil {
log.Fatalf("Fehler beim Lesen der config.json: %v", err)
}
if err := json.Unmarshal(confFile, &config); err != nil {
log.Fatalf("Fehler beim Parsen der config.json: %v", err)
}
if _, err = host.Init(); err != nil {
log.Fatalf("Hardware-Init fehlgeschlagen: %v", err)
}
bus, err := i2creg.Open(config.I2CBus)
if err != nil {
log.Fatalf("I2C Bus Fehler: %v", err)
}
opts := ads1x15.DefaultOpts
opts.I2cAddress = config.ADS1115Address
adsDevice, err = ads1x15.NewADS1115(bus, &opts)
if err != nil {
log.Fatalf("ADS1115 Init fehlgeschlagen: %v", err)
}
// GPIO-Pins registrieren
for i, r := range config.Relais {
pin := gpioreg.ByName(r.GPIO)
if pin == nil {
log.Fatalf("GPIO-Pin %s existiert nicht", r.GPIO)
}
if err := pin.Out(gpio.Low); err != nil {
log.Fatalf("Pin-Konfiguration für %s fehlgeschlagen: %v", r.GPIO, err)
}
relaisMap[r.ID] = pin
config.Relais[i].State = false
}
// Instanziierung des Webservers mit Übergabe der Callback-Funktionen
server := web.NewServer(config.WebPort, readVoltageCallback, toggleRelaisCallback, getRelaisStateCallback, callbackGetVersion)
fmt.Printf("Webserver gestartet auf Port %s\n", config.WebPort)
log.Fatal(server.Start())
}
// Callback: Wird vom Webserver aufgerufen, um Kanäle zu messen
func readVoltageCallback(channel int) float64 {
mutex.Lock()
defer mutex.Unlock()
var adsChannel ads1x15.Channel
var factor float64
if channel == 0 {
adsChannel = ads1x15.Channel0
factor = config.Factor1
} else {
adsChannel = ads1x15.Channel1
factor = config.Factor2
}
pin, err := adsDevice.PinForChannel(adsChannel, physic.Volt*4096/1000, 128*physic.Hertz, 0)
if err != nil {
fmt.Printf("Fehler beim Erzeugen des Pins für Kanal %v: %v\n", channel, err)
return 0.0
}
sample, err := pin.Read()
if err != nil {
fmt.Printf("Fehler beim Lesen von Kanal %v: %v\n", channel, err)
return 0.0
}
voltValue := float64(sample.V) / 1000000000.0
return voltValue * factor
}
// Callback: Wird vom Webserver aufgerufen, um ein Relais zu schalten
func toggleRelaisCallback(id int) {
mutex.Lock()
defer mutex.Unlock()
pin, exists := relaisMap[id]
if !exists {
return
}
for i, rel := range config.Relais {
if rel.ID == id {
newState := !rel.State
config.Relais[i].State = newState
if newState {
pin.Out(gpio.High)
} else {
pin.Out(gpio.Low)
}
break
}
}
}
// Callback: Wird vom Webserver aufgerufen, um den aktuellen UI-Status abzufragen
func getRelaisStateCallback() []web.RelaisData {
mutex.Lock()
defer mutex.Unlock()
data := make([]web.RelaisData, len(config.Relais))
for i, r := range config.Relais {
data[i] = web.RelaisData{
ID: r.ID,
Name: r.Name,
State: r.State,
}
}
return data
}
// Versionangaben aus version.json in Webserver übernehmen
func callbackGetVersion() (string, string) {
return Version, BuildDate
}

BIN
spannung-amd64 Executable file

Binary file not shown.

BIN
spannung-arm32_6 Executable file

Binary file not shown.

BIN
spannung-arm32_7 Executable file

Binary file not shown.

BIN
spannung-arm64 Executable file

Binary file not shown.

44
spannung.service Normal file
View file

@ -0,0 +1,44 @@
# ==============================================================================
# KURZANLEITUNG ZUR INSTALLATION UND VERWALTUNG
# ==============================================================================
# 1. Datei kopieren nach: /etc/systemd/system/spannung.service
# 2. Systemd neu laden: sudo systemctl daemon-reload
# 3. Autostart aktivieren: sudo systemctl enable spannung.service
# 4. Dienst starten: sudo systemctl start spannung.service
#
# Befehle zur Steuerung:
# - Status prüfen: sudo systemctl status spannung.service
# - Live-Logs einsehen: sudo journalctl -u spannung.service -f
# - Dienst stoppen: sudo systemctl stop spannung.service
# - Dienst neu starten: sudo systemctl restart spannung.service
# ==============================================================================
#
# gewohnter rsync-Befehl (Beispiel)
# rsync -avz ./app/ pi@raspirotor:~/spannung/
# 2. Den Dienst auf dem Pi remote aktualisieren und neu starten
# ssh -t afu@raspirotor "sudo cp ~/spannung/app/spannung.service /etc/systemd/system/ && sudo systemctl daemon-reload && sudo systemctl restart spannung.service"
[Unit]
Description=Spannung Control Service
After=network-online.target i2c.service
Wants=network-online.target
StartLimitIntervalSec=0
[Service]
Type=simple
# Die ExecStartPre-Zeile muss bei 32-Bit auskommentiert werden, da kein i2cdetect verfügbar ist
ExecStartPre=/bin/bash -c 'for i in {1..10}; do i2cdetect -y 1 | grep -q "48" && exit 0; echo "warte auf ADS1115..."; sleep 2; done; exit 1'
ExecStart=/home/afu/spannung/spannung
WorkingDirectory=/home/afu/spannung
User=afu
#Restart=always
Restart=on-failure # Startet den Dienst nur bei Fehlern neu
RestartSec=5s # Wartet 5 Sekunden vor dem nächsten Versuch
StartLimitIntervalSec=60s # Innerhalb von 60 Sekunden
StartLimitBurst=3 # Maximal 3 Versuche in diesem Intervall
StandardOutput=append:/home/afu/spannung/spannung.log
StandardError=append:/home/afu/spannung/spannung.log
[Install]
WantedBy=multi-user.target

10
version.json Normal file
View file

@ -0,0 +1,10 @@
{
"version": "0.1",
"date": "06.06.2026 um 20:57",
"description": "Messung von zwei Spannung und Steuerung von Relais",
"features": [
"ADS1115-Abfrage über go-i2c",
"Weboberfläche mit Steuerbuttons für Reilais",
"Definiton der GPIO in der config.json"
]
}

77
web/server.go Normal file
View file

@ -0,0 +1,77 @@
package web
import (
"html/template"
"net/http"
"strconv"
)
type RelaisData struct {
ID int
Name string
State bool
}
// TemplateData erweitern um Version und Datum
type TemplateData struct {
V1 float64
V2 float64
Relais []RelaisData
Version string
BuildDate string
}
type Server struct {
port string
readVoltFunc func(channel int) float64
toggleFunc func(id int)
getRelais func() []RelaisData
versionFunc func() (string, string) // Neu hinzugefügt
}
// NewServer um das version-Callback erweitern
func NewServer(port string, readVolt func(int) float64, toggle func(int), getRelais func() []RelaisData, getVersion func() (string, string)) *Server {
return &Server{
port: port,
readVoltFunc: readVolt,
toggleFunc: toggle,
getRelais: getRelais,
versionFunc: getVersion,
}
}
func (s *Server) Start() error {
http.HandleFunc("/", s.handleIndex)
http.HandleFunc("/toggle", s.handleToggle)
return http.ListenAndServe(s.port, nil)
}
func (s *Server) handleIndex(w http.ResponseWriter, r *http.Request) {
v1 := s.readVoltFunc(0)
v2 := s.readVoltFunc(1)
ver, date := s.versionFunc() // Version abfragen
tmpl, err := template.ParseFiles("web/static/index.html")
if err != nil {
http.Error(w, "Template nicht gefunden: "+err.Error(), http.StatusInternalServerError)
return
}
data := TemplateData{
V1: v1,
V2: v2,
Relais: s.getRelais(),
Version: ver, // Daten an Template übergeben
BuildDate: date, // Daten an Template übergeben
}
tmpl.Execute(w, data)
}
func (s *Server) handleToggle(w http.ResponseWriter, r *http.Request) {
idStr := r.URL.Query().Get("id")
id, err := strconv.Atoi(idStr)
if err == nil {
s.toggleFunc(id)
}
http.Redirect(w, r, "/", http.StatusSeeOther)
}

55
web/static/index.html Normal file
View file

@ -0,0 +1,55 @@
<!DOCTYPE html>
<html>
<head>
<title>Funkstation Steuerung</title>
<meta name="viewport" content="width=device-width, initial-scale=1">
<style>
body { font-family: 'Segoe UI', Arial, sans-serif; text-align: center; background: #0f172a; color: #f8fafc; padding: 20px; }
.container { max-width: 500px; margin: 0 auto; }
.card { background: #1e293b; padding: 20px; margin: 15px 0; border-radius: 12px; box-shadow: 0 4px 6px -1px rgba(0,0,0,0.3); border: 1px solid #334155; }
.val { font-size: 2.5em; color: #38bdf8; font-weight: bold; font-family: monospace; }
.grid { display: grid; grid-template-columns: 1fr; gap: 12px; margin-top: 15px; }
.btn { width: 100%; padding: 14px; font-size: 1.1em; border: none; border-radius: 8px; cursor: pointer; font-weight: bold; transition: background 0.2s; color: white; }
.btn-on { background: #ef4444; }
.btn-on:hover { background: #dc2626; }
.btn-off { background: #10b981; }
.btn-off:hover { background: #059669; }
.footer-nav { margin-top: 25px; font-size: 0.9em; color: #64748b; }
</style>
<meta http-equiv="refresh" content="3">
</head>
<body>
<div class="container">
<h1>U12V Funkstations-Monitor</h1>
<div class="card">
<h3>Spannungsebene 1</h3>
<div class="val">{{printf "%.2f" .V1}} V</div>
</div>
<div class="card">
<h3>Spannungsebene 2</h3>
<div class="val">{{printf "%.2f" .V2}} V</div>
</div>
<div class="card">
<h3>Relaissteuerung</h3>
<div class="grid">
{{range .Relais}}
<a href="/toggle?id={{.ID}}" style="text-decoration: none;">
<button class="btn {{if .State}}btn-on{{else}}btn-off{{end}}">
{{.Name}}: {{if .State}}AKTIV (Ausschalten){{else}}INAKTIV (Einschalten){{end}}
</button>
</a>
{{end}}
</div>
</div>
<div class="footer-nav">
Aktualisiert alle 3 Sekunden automatisch<br>
<span style="font-size: 0.8em; color: #475569; margin-top: 5px; display: block;">
Version v{{.Version}} ({{.BuildDate}})
</span>
</div>
</div>
</body>
</html>