2026-08-02 05:30 USB-Relais schalten noch nicht

This commit is contained in:
Dieter Lang 2026-08-02 08:25:00 +02:00
parent 2c63235b54
commit e39598103d
19 changed files with 241 additions and 239 deletions

View file

@ -1,13 +0,0 @@
{
"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": "GPIO12"},
{"id": 2, "name": "Relais 2 (unbenutzt)", "gpio": "GPIO16"},
{"id": 3, "name": "Relais 3 (unbenutzt)", "gpio": "GPIO20"},
{"id": 4, "name": "Relais 4 (unbenutzt)", "gpio": "GPIO21"}
]
}

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View file

@ -1,31 +0,0 @@
# ==============================================================================
# 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

View file

@ -1,10 +0,0 @@
{
"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"
]
}

View file

@ -1,77 +0,0 @@
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)
}

View file

@ -1,55 +0,0 @@
<!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>

View file

@ -14,9 +14,14 @@
"name_adc2_ch0": "Zusatzakku Funk",
"factor_adc2_ch0": 1.0,
"relais_pins": [
{"id": 1, "name": "Relais 1 (Hauptschalter)", "gpio": "GPIO12"},
{"id": 2, "name": "Relais 2 (unbenutzt)", "gpio": "GPIO16"},
{"id": 3, "name": "Relais 3 (unbenutzt)", "gpio": "GPIO20"},
{"id": 4, "name": "Relais 4 (unbenutzt)", "gpio": "GPIO21"}
{"id": 1, "name": "Relais 1 (230V Hauptschalter)", "gpio": "GPIO12"},
{"id": 2, "name": "USB-Relais 1 (Funkgerät 1)", "gpio": "USB_CH1"},
{"id": 3, "name": "USB-Relais 2 (Funkgerät 2)", "gpio": "USB_CH2"},
{"id": 4, "name": "USB-Relais 3 (Verstärker)", "gpio": "USB_CH3"},
{"id": 5, "name": "USB-Relais 4 (Lüfter)", "gpio": "USB_CH4"},
{"id": 6, "name": "USB-Relais 5 (Beleuchtung)", "gpio": "USB_CH5"},
{"id": 7, "name": "USB-Relais 6 (Zubehör 1)", "gpio": "USB_CH6"},
{"id": 8, "name": "USB-Relais 7 (Zubehör 2)", "gpio": "USB_CH7"},
{"id": 9, "name": "USB-Relais 8 (Reserve)", "gpio": "USB_CH8"}
]
}

5
go.mod
View file

@ -7,3 +7,8 @@ require (
periph.io/x/devices/v3 v3.7.4
periph.io/x/host/v3 v3.8.5
)
require (
github.com/adriankumpf/abacom-relay-board v0.7.1 // indirect
github.com/google/gousb v1.1.3 // indirect
)

4
go.sum
View file

@ -1,3 +1,7 @@
github.com/adriankumpf/abacom-relay-board v0.7.1 h1:BEq26Z+mstFFO12Anla7QXmi/C5tkZT/8dCqB/vyfwY=
github.com/adriankumpf/abacom-relay-board v0.7.1/go.mod h1:j1JZRj26BIUMuxdtiBP1G5dF9piCbX/xQATZB5+CPs4=
github.com/google/gousb v1.1.3 h1:xt6M5TDsGSZ+rlomz5Si5Hmd/Fvbmo2YCJHN+yGaK4o=
github.com/google/gousb v1.1.3/go.mod h1:GGWUkK0gAXDzxhwrzetW592aOmkkqSGcj5KLEgmCVUg=
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=

168
main.go
View file

@ -7,8 +7,9 @@ import (
"os"
"sync"
"spannung/web" // Importiert das eigene web-Paket
"spannung/web" // Importiert Ihr eigenes web-Paket
"github.com/google/gousb"
"periph.io/x/conn/v3/gpio"
"periph.io/x/conn/v3/gpio/gpioreg"
"periph.io/x/conn/v3/i2c/i2creg"
@ -70,7 +71,7 @@ func main() {
log.Fatalf("I2C Bus Fehler: %v", err)
}
// 1. Ersten ADS1115 initialisieren (z.B. Adresse 72 / 0x48)
// 1. Ersten ADS1115 initialisieren
opts1 := ads1x15.DefaultOpts
opts1.I2cAddress = config.ADS1115Address1
adsDevice1, err = ads1x15.NewADS1115(bus, &opts1)
@ -78,7 +79,7 @@ func main() {
log.Fatalf("ADS1115 #1 Init fehlgeschlagen: %v", err)
}
// 2. Zweiten ADS1115 initialisieren (z.B. Adresse 73 / 0x49)
// 2. Zweiten ADS1115 initialisieren
opts2 := ads1x15.DefaultOpts
opts2.I2cAddress = config.ADS1115Address2
adsDevice2, err = ads1x15.NewADS1115(bus, &opts2)
@ -86,48 +87,32 @@ func main() {
log.Fatalf("ADS1115 #2 Init fehlgeschlagen: %v", err)
}
// GPIO-Pins registrieren
// GPIO-Pins registrieren (ID 1 ist der 230V Hauptschalter am Raspberry Pi)
for i, r := range config.Relais {
pin := gpioreg.ByName(r.GPIO)
if pin == nil {
log.Fatalf("GPIO-Pin %s existiert nicht", r.GPIO)
if r.ID == 1 {
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
}
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
// Beim Start alle USB-Relais initial in den definierten AUS-Zustand versetzen
updateUsbRelays()
server := web.NewServer(
config.WebPort,
readVoltageCallback,
toggleRelaisCallback,
getRelaisStateCallback,
callbackGetVersion,
getVoltNamesCallback,
)
// Instanziierung des Webservers mit Übergabe der Callback-Funktionen
server := web.NewServer(config.WebPort, readVoltageCallback, toggleRelaisCallback, getRelaisStateCallback, callbackGetVersion, getVoltNamesCallback)
fmt.Printf("Webserver gestartet auf Port %s\n", config.WebPort)
log.Fatal(server.Start())
}
// NEUES CALLBACK: Liefert die Namen der Kanäle an das Web-Paket
func getVoltNamesCallback() []string {
return []string{
config.NameAdc1Ch0,
config.NameAdc1Ch1,
config.NameAdc1Ch2,
config.NameAdc1Ch3,
config.NameAdc2Ch0,
}
}
// Callback: Wird vom Webserver aufgerufen, um Kanäle zu messen
// Kanäle 0-3 = ADS1115 Nr. 1 (A0-A3)
// Kanal 4 = ADS1115 Nr. 2 (A0)
func readVoltageCallback(channel int) float64 {
mutex.Lock()
defer mutex.Unlock()
@ -155,9 +140,8 @@ func readVoltageCallback(channel int) float64 {
factor = config.FactorAdc1Ch3
case 4:
targetDevice = adsDevice2
adsChannel = ads1x15.Channel0 // Erster Eingang vom zweiten Chip
adsChannel = ads1x15.Channel0
factor = config.FactorAdc2Ch0
default:
fmt.Printf("Ungültiger Kanal abgefragt: %v\n", channel)
return 0.0
@ -184,34 +168,109 @@ func toggleRelaisCallback(id int) {
mutex.Lock()
defer mutex.Unlock()
pin, exists := relaisMap[id]
if !exists {
return
}
// Direkt über den Index das globale Slice manipulieren
for i := range config.Relais {
if config.Relais[i].ID == id {
// Status sauber invertieren
config.Relais[i].State = !config.Relais[i].State
// Hardware basierend auf dem neuen, echten Zustand schalten
if config.Relais[i].State {
pin.Out(gpio.High) // Wenn True -> Pin High
if id == 1 {
// Hardware-Schaltung Raspberry Pi GPIO
pin, exists := relaisMap[id]
if exists {
if config.Relais[i].State {
pin.Out(gpio.High)
} else {
pin.Out(gpio.Low)
}
}
} else {
pin.Out(gpio.Low) // Wenn False -> Pin Low
// Hardware-Schaltung über Abacom USB-Karte via Bulk-Streaming
updateUsbRelays()
}
break
}
}
}
// Hilfsfunktion: Schaltet die Abacom-Karte nach dem Python-Skript-Muster via Bulk-Stream
func updateUsbRelays() {
fmt.Println("\n--- [USB DEBUG START] ---")
ctx := gousb.NewContext()
defer ctx.Close()
// USB-Gerät anhand der VID:PID öffnen (1a86:5512)
dev, err := ctx.OpenDeviceWithVIDPID(0x1a86, 0x5512)
if err != nil {
fmt.Printf("[❌] USB-Fehler beim Öffnen des Geräts: %v\n", err)
return
}
if dev == nil {
fmt.Println("[❌] Hardware-Fehler: Abacom-Karte wurde am USB-Port nicht gefunden!")
return
}
defer dev.Close()
fmt.Println("[✅] USB-Hardware erfolgreich geöffnet.")
// Kernel-Treiber trennen (erzwingt exklusiven Zugriff unter Linux)
_ = dev.SetAutoDetach(true)
// Bitmaske berechnen (ID 2 -> Bit 0 bis ID 9 -> Bit 7)
var bitmask byte = 0x00
for _, r := range config.Relais {
if r.ID == 1 {
continue // ID 1 überspringen (ist Ihr 230V GPIO-Hauptschalter)
}
if r.State && r.ID >= 2 && r.ID <= 9 {
shiftWidth := r.ID - 2
bitmask |= (1 << shiftWidth)
}
}
// Abacom-LRB nutzt Low-Active-Pegel (0V = Ein, 5V = Aus)
dataByte := ^bitmask
fmt.Printf("[⚙️] Gewünschter UI-Status (Binär): %08b\n", bitmask)
fmt.Printf("[⚙️] Zu sendendes Daten-Byte : 0x%02X\n", dataByte)
cfg, err := dev.Config(1)
if err != nil {
fmt.Printf("[❌] Konfigurationsfehler: %v\n", err)
return
}
defer cfg.Close()
intf, err := cfg.Interface(0, 0)
if err != nil {
fmt.Printf("[❌] Interface-Fehler: %v\n", err)
return
}
defer intf.Close()
// Endpunkt 2 (Out) für das native CH341A-Parallel-Streaming öffnen
outEP, err := intf.OutEndpoint(2)
if err != nil {
fmt.Printf("[❌] Endpunkt-Fehler: %v\n", err)
return
}
// Erst 4 leere Bytes senden, um das Hardware-Sperrlatch des CH341A aufzuwecken
initTrigger := []byte{0x00, 0x00, 0x00, 0x00}
_, _ = outEP.Write(initTrigger)
// Start-Kennung 0x5A, gefolgt von 3x Null und dem Relais-Datenbyte am Ende
payload := []byte{0x5A, 0x00, 0x00, 0x00, dataByte}
// Daten über die Bulk-Pipeline an das Latch-Register übergeben
bytesWritten, err := outEP.Write(payload)
if err != nil {
fmt.Printf("[❌] BULK-SCHALTFEHLER: %v\n", err)
} else {
fmt.Printf("[✅] ERFOLG! Abacom-Stream exakt wie in Python übertragen (%d Bytes geschrieben).\n", bytesWritten)
}
fmt.Println("--- [USB DEBUG ENDE] ---\n")
}
// 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{
@ -223,6 +282,17 @@ func getRelaisStateCallback() []web.RelaisData {
return data
}
// Callback: Liefert die Namen der Kanäle an das Web-Paket
func getVoltNamesCallback() []string {
return []string{
config.NameAdc1Ch0,
config.NameAdc1Ch1,
config.NameAdc1Ch2,
config.NameAdc1Ch3,
config.NameAdc2Ch0,
}
}
// Versionangaben aus version.json in Webserver übernehmen
func callbackGetVersion() (string, string) {
return Version, BuildDate

48
relais.py Normal file
View file

@ -0,0 +1,48 @@
#!/usr/bin/env python3
import sys
import usb.core
import usb.util
# Abacom CH341A IDs
VENDOR_ID = 0x1a86
PRODUCT_ID = 0x5512
def main():
if len(sys.argv) < 2:
print("Nutzung: python3 relais.py <Wert_0_bis_255>")
sys.exit(1)
try:
data_value = int(sys.argv[1])
if not (0 <= data_value <= 255):
raise ValueError
except ValueError:
print("Fehler: Bitte eine Zahl zwischen 0 und 255 eingeben.")
sys.exit(1)
# Gerät suchen
dev = usb.core.find(idVendor=VENDOR_ID, idProduct=PRODUCT_ID)
if dev is None:
print("Relaiskarte nicht gefunden! USB-Verbindung prüfen.")
sys.exit(1)
# Kernel-Treiber lösen, falls aktiv
if dev.is_kernel_driver_active(0):
dev.detach_kernel_driver(0)
# Konfiguration setzen
dev.set_configuration()
# CH341 Befehl zum Schreiben von IO-Pins (Abacom-Protokoll)
# Befehl 0xA6 setzt die Ausgänge beim CH341
# Das Format ist oft: [0xA6, Schalter-Byte, 0x00] oder direkte Control-Transfers
try:
# Steuerbefehl direkt an den Chip senden (CH341 spezifisch)
# 0x40 = Vendor-Write, 0x9a = CH341_REQ_WRITE_REG
dev.ctrl_transfer(0x40, 0x9a, 0x2525, data_value)
print(f"Erfolgreich geschaltet. Wert: {data_value} (Binär: {data_value:08b})")
except Exception as e:
print(f"Fehler beim Senden: {e}")
if __name__ == "__main__":
main()

56
relais2.py Normal file
View file

@ -0,0 +1,56 @@
#!/usr/bin/env python3
import sys
import usb.core
import usb.util
VENDOR_ID = 0x1a86
PRODUCT_ID = 0x5512
def main():
if len(sys.argv) < 2:
print("Nutzung: sudo python3 relais2.py <Wert_0_bis_255>")
sys.exit(1)
try:
data_value = int(sys.argv[1])
if not (0 <= data_value <= 255):
raise ValueError
except ValueError:
print("Fehler: Bitte eine Zahl zwischen 0 und 255 eingeben.")
sys.exit(1)
dev = usb.core.find(idVendor=VENDOR_ID, idProduct=PRODUCT_ID)
if dev is None:
print("Relaiskarte nicht gefunden! USB-Verbindung prüfen.")
sys.exit(1)
if dev.is_kernel_driver_active(0):
try:
dev.detach_kernel_driver(0)
except Exception:
pass
dev.set_configuration()
try:
# CH341A GPIO-Befehl (0x5a)
# Aufbau des Pakets:
# Byte 0: 0x5a (Befehl für GPIO-Steuerung)
# Byte 1: 0x51 (Sub-Befehl für I/O-Wechsel)
# Byte 2: Richtungsmaske/Enable für die Pins (0xff = Alle Pins D0-D7 auf Ausgang)
# Byte 3: Datenwert für Pins D0-D7 (deine Relais 0-255)
# Byte 4: Richtung/Enable für obere Pins (0x00 = nicht genutzt)
# Byte 5: Datenwert für obere Pins (0x00)
cmd_packet = bytes([0x5a, 0x51, 0xff, data_value, 0x00, 0x00])
# Senden an den Standard-Bulk-Ausgang (Endpoint 0x02)
dev.write(0x02, cmd_packet, 1000)
print(f"Befehl gesendet. Wert: {data_value} (Binär: {data_value:08b})")
except Exception as e:
print(f"Fehler beim Senden: {e}")
if __name__ == "__main__":
main()

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.