2026-06-06 21:02 Version 0.1
This commit is contained in:
commit
afdbb2e136
13 changed files with 470 additions and 0 deletions
85
build.sh
Executable file
85
build.sh
Executable 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
13
config.json
Normal 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
9
go.mod
Normal 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
8
go.sum
Normal 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
169
main.go
Normal 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
BIN
spannung-amd64
Executable file
Binary file not shown.
BIN
spannung-arm32_6
Executable file
BIN
spannung-arm32_6
Executable file
Binary file not shown.
BIN
spannung-arm32_7
Executable file
BIN
spannung-arm32_7
Executable file
Binary file not shown.
BIN
spannung-arm64
Executable file
BIN
spannung-arm64
Executable file
Binary file not shown.
44
spannung.service
Normal file
44
spannung.service
Normal 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
10
version.json
Normal 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
77
web/server.go
Normal 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
55
web/static/index.html
Normal 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>
|
||||
Loading…
Reference in a new issue