390 lines
10 KiB
Go
390 lines
10 KiB
Go
package main
|
|
|
|
import (
|
|
"encoding/json"
|
|
"fmt"
|
|
"log"
|
|
"os"
|
|
"strings"
|
|
"sync"
|
|
"os/signal"
|
|
"syscall"
|
|
|
|
"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"
|
|
"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"`
|
|
SSLCert string `json:"ssl_cert"`
|
|
SSLKey string `json:"ssl_key"`
|
|
I2CBus string `json:"i2c_bus"`
|
|
ADS1115Address1 uint16 `json:"ads1115_address_1"`
|
|
ADS1115Address2 uint16 `json:"ads1115_address_2"`
|
|
NameAdc1Ch0 string `json:"name_adc1_ch0"`
|
|
FactorAdc1Ch0 float64 `json:"factor_adc1_ch0"`
|
|
NameAdc1Ch1 string `json:"name_adc1_ch1"`
|
|
FactorAdc1Ch1 float64 `json:"factor_adc1_ch1"`
|
|
NameAdc1Ch2 string `json:"name_adc1_ch2"`
|
|
FactorAdc1Ch2 float64 `json:"factor_adc1_ch2"`
|
|
NameAdc1Ch3 string `json:"name_adc1_ch3"`
|
|
FactorAdc1Ch3 float64 `json:"factor_adc1_ch3"`
|
|
NameAdc2Ch0 string `json:"name_adc2_ch0"`
|
|
FactorAdc2Ch0 float64 `json:"factor_adc2_ch0"`
|
|
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
|
|
adsDevice1 *ads1x15.Dev
|
|
adsDevice2 *ads1x15.Dev
|
|
relaisMap = make(map[int]gpio.PinIO)
|
|
mutex sync.Mutex
|
|
Version = "Entwicklung"
|
|
BuildDate = "Unbekannt"
|
|
)
|
|
|
|
func main() {
|
|
// 1. Version aus Datei laden, falls vorhanden
|
|
loadVersionInfo()
|
|
|
|
// 2. Konfigurationsdatei einlesen
|
|
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)
|
|
}
|
|
|
|
// 3. Periph Hardware initialisieren
|
|
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)
|
|
}
|
|
|
|
// 4. Ersten ADS1115 initialisieren
|
|
opts1 := ads1x15.DefaultOpts
|
|
opts1.I2cAddress = config.ADS1115Address1
|
|
adsDevice1, err = ads1x15.NewADS1115(bus, &opts1)
|
|
if err != nil {
|
|
log.Fatalf("ADS1115 #1 Init fehlgeschlagen: %v", err)
|
|
}
|
|
|
|
// 5. Zweiten ADS1115 initialisieren
|
|
opts2 := ads1x15.DefaultOpts
|
|
opts2.I2cAddress = config.ADS1115Address2
|
|
adsDevice2, err = ads1x15.NewADS1115(bus, &opts2)
|
|
if err != nil {
|
|
log.Fatalf("ADS1115 #2 Init fehlgeschlagen: %v", err)
|
|
}
|
|
|
|
// 6. Internen Software-Zustand für ALLE Relais VORAB auf AUS (false) setzen
|
|
for i := range config.Relais {
|
|
config.Relais[i].State = false
|
|
}
|
|
|
|
// 7. GPIO-Pins registrieren (ID 1 ist der 230V Hauptschalter am Raspberry Pi)
|
|
for _, r := range config.Relais {
|
|
if r.ID == 1 {
|
|
pin := gpioreg.ByName(r.GPIO)
|
|
if pin == nil {
|
|
log.Fatalf("GPIO-Pin %s existiert nicht", r.GPIO)
|
|
}
|
|
// Hauptschalter hardwareseitig sofort sicher ausschalten
|
|
if err := pin.Out(gpio.Low); err != nil {
|
|
log.Fatalf("Pin-Konfiguration für %s fehlgeschlagen: %v", r.GPIO, err)
|
|
}
|
|
relaisMap[r.ID] = pin
|
|
}
|
|
}
|
|
|
|
// 8. SICHERHEITS-START: Alle USB-Relais initial zwingend ausschalten
|
|
if err := updateUsbRelaysStrict(); err != nil {
|
|
log.Fatalf("[❌] KRITISCHER FEHLER: USB-Relais-Reset beim Start fehlgeschlagen! Programm gestoppt: %v", err)
|
|
}
|
|
|
|
// NEU: Signal-Handling für sauberes Herunterfahren einrichten
|
|
sigChan := make(chan os.Signal, 1)
|
|
signal.Notify(sigChan, os.Interrupt, syscall.SIGTERM)
|
|
|
|
go func() {
|
|
<-sigChan
|
|
fmt.Println("\n[⚠️] Programm-Abbruch erkannt! Schalte Relais 1 (Hauptschalter) sicher aus...")
|
|
mutex.Lock()
|
|
if pin, exists := relaisMap[1]; exists {
|
|
_ = pin.Out(gpio.Low)
|
|
}
|
|
mutex.Unlock()
|
|
os.Exit(0)
|
|
}()
|
|
|
|
// 9. Instanziierung des Webservers mit Übergabe der Callback-Funktionen
|
|
server := web.NewServer(config.WebPort, readVoltageCallback,
|
|
toggleRelaisCallback, getRelaisStateCallback, callbackGetVersion,
|
|
getVoltNamesCallback)
|
|
|
|
fmt.Printf("Verschlüsselter HTTPS-Webserver gestartet auf Port %s...\n", config.WebPort)
|
|
|
|
// ÜBERGABE DER ZERTIFIKATE AUS DER CONFIG AN DIE NEUE START-METHODE
|
|
log.Fatal(server.Start(config.SSLCert, config.SSLKey))
|
|
}
|
|
|
|
// Hilfsfunktion zum Laden der version.json
|
|
func loadVersionInfo() {
|
|
type VersionInfo struct {
|
|
Version string `json:"version"`
|
|
BuildDate string `json:"build_date"`
|
|
}
|
|
|
|
vFile, err := os.ReadFile("version.json")
|
|
if err == nil {
|
|
var vInfo VersionInfo
|
|
if err := json.Unmarshal(vFile, &vInfo); err == nil {
|
|
Version = vInfo.Version
|
|
BuildDate = vInfo.BuildDate
|
|
}
|
|
}
|
|
}
|
|
|
|
// Callback: Wird vom Webserver aufgerufen, um Kanäle zu messen
|
|
func readVoltageCallback(channel int) float64 {
|
|
mutex.Lock()
|
|
defer mutex.Unlock()
|
|
|
|
var targetDevice *ads1x15.Dev
|
|
var adsChannel ads1x15.Channel
|
|
var factor float64
|
|
|
|
switch channel {
|
|
case 0:
|
|
targetDevice = adsDevice1
|
|
adsChannel = ads1x15.Channel0
|
|
factor = config.FactorAdc1Ch0
|
|
case 1:
|
|
targetDevice = adsDevice1
|
|
adsChannel = ads1x15.Channel1
|
|
factor = config.FactorAdc1Ch1
|
|
case 2:
|
|
targetDevice = adsDevice1
|
|
adsChannel = ads1x15.Channel2
|
|
factor = config.FactorAdc1Ch2
|
|
case 3:
|
|
targetDevice = adsDevice1
|
|
adsChannel = ads1x15.Channel3
|
|
factor = config.FactorAdc1Ch3
|
|
case 4:
|
|
targetDevice = adsDevice2
|
|
adsChannel = ads1x15.Channel0
|
|
factor = config.FactorAdc2Ch0
|
|
default:
|
|
fmt.Printf("Ungültiger Kanal abgefragt: %v\n", channel)
|
|
return 0.0
|
|
}
|
|
|
|
pin, err := targetDevice.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
|
|
|
|
//Unterdrückt Rauschen und negative Restspannungen bis einschließlich -0.01 V sowie kleine positive Offsets
|
|
if voltValue >= -0.011 && voltValue <= 0.011 {
|
|
return 0.0
|
|
}
|
|
return voltValue * factor
|
|
}
|
|
|
|
// Callback: Wird vom Webserver aufgerufen, um ein Relais zu schalten
|
|
func toggleRelaisCallback(id int) {
|
|
mutex.Lock()
|
|
defer mutex.Unlock()
|
|
|
|
for i := range config.Relais {
|
|
if config.Relais[i].ID == id {
|
|
|
|
// 1. Sperre: Prüfen auf das Wort "Reserve"
|
|
if strings.Contains(strings.ToLower(config.Relais[i].Name), "reserve") {
|
|
fmt.Printf("[⚠️] Schalten blockiert: '%s' (ID %d) ist als Reserve geschützt!\n", config.Relais[i].Name, id)
|
|
return
|
|
}
|
|
|
|
// Zustand des ausgewählten Relais umkehren
|
|
config.Relais[i].State = !config.Relais[i].State
|
|
|
|
if id == 1 {
|
|
// Hardware-Schaltung Raspberry Pi GPIO (Hauptschalter bleibt unabhängig)
|
|
pin, exists := relaisMap[id]
|
|
if exists {
|
|
if config.Relais[i].State {
|
|
pin.Out(gpio.High)
|
|
} else {
|
|
pin.Out(gpio.Low)
|
|
}
|
|
}
|
|
} else {
|
|
// ERWEITERUNG: Gegenseitige Verriegelung für USB-Relais
|
|
if config.Relais[i].State {
|
|
for j := range config.Relais {
|
|
if config.Relais[j].ID != id && config.Relais[j].ID >= 2 {
|
|
config.Relais[j].State = false
|
|
}
|
|
}
|
|
fmt.Printf("[🔄] Exklusiv-Modus: Relais ID %d aktiviert. Alle anderen USB-Relais wurden ausgeschaltet.\n", id)
|
|
}
|
|
|
|
// Standard-Schaltvorgang (Fehler werfen im UI-Log, blockiert aber nicht den Server)
|
|
if err := updateUsbRelaysStrict(); err != nil {
|
|
fmt.Printf("[❌] Fehler beim Schalten der USB-Relais: %v\n", err)
|
|
}
|
|
}
|
|
break
|
|
}
|
|
}
|
|
}
|
|
|
|
// Optimierte Hilfsfunktion: Schaltet die Abacom-Karte und gibt Fehler an den Aufrufer zurück
|
|
func updateUsbRelaysStrict() error {
|
|
fmt.Println("\n--- [USB ABACOM SCHALTVORGANG] ---")
|
|
ctx := gousb.NewContext()
|
|
defer ctx.Close()
|
|
|
|
dev, err := ctx.OpenDeviceWithVIDPID(0x1a86, 0x5512)
|
|
if err != nil {
|
|
return fmt.Errorf("USB-Fehler beim Öffnen des Geräts: %w", err)
|
|
}
|
|
if dev == nil {
|
|
return fmt.Errorf("Abacom-Karte wurde am USB-Port nicht gefunden")
|
|
}
|
|
defer dev.Close()
|
|
|
|
_ = dev.SetAutoDetach(true)
|
|
|
|
cfg, err := dev.Config(1)
|
|
if err != nil {
|
|
return fmt.Errorf("Konfigurationsfehler: %w", err)
|
|
}
|
|
defer cfg.Close()
|
|
|
|
intf, err := cfg.Interface(0, 0)
|
|
if err != nil {
|
|
return fmt.Errorf("Interface-Fehler: %w", err)
|
|
}
|
|
defer intf.Close()
|
|
|
|
outEP, err := intf.OutEndpoint(2)
|
|
if err != nil {
|
|
return fmt.Errorf("Endpunkt-Fehler: %w", err)
|
|
}
|
|
|
|
// 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
|
|
}
|
|
if r.State && r.ID >= 2 && r.ID <= 9 {
|
|
shiftWidth := r.ID - 2
|
|
bitmask |= (1 << shiftWidth)
|
|
}
|
|
}
|
|
|
|
// Konstanten für den CH341A-Chip
|
|
const (
|
|
ch341aSetOutput = 0xA1
|
|
LATCH = 0x01
|
|
CLK = 0x08
|
|
DATA = 0x20
|
|
)
|
|
|
|
// Anonyme Hilfsfunktion für USB-Anweisungen
|
|
sendOutput := func(dataByte byte) {
|
|
msg := []byte{
|
|
ch341aSetOutput,
|
|
0x6a, 0x1f, 0x00, 0x10,
|
|
dataByte,
|
|
0x3f, 0x00, 0x00, 0x00, 0x00,
|
|
}
|
|
_, _ = outEP.Write(msg)
|
|
}
|
|
|
|
fmt.Printf("[⚙️] Sende Bitmaske an Schieberegister: %08b\n", bitmask)
|
|
|
|
// --- Protokoll-Sequenz ---
|
|
sendOutput(0x00) // Latch initial auf Low halten
|
|
|
|
for i := 0; i < 8; i++ {
|
|
if (bitmask & (1 << (7 - i))) != 0 {
|
|
sendOutput(DATA)
|
|
sendOutput(CLK | DATA)
|
|
sendOutput(DATA)
|
|
} else {
|
|
sendOutput(0x00)
|
|
sendOutput(CLK)
|
|
sendOutput(0x00)
|
|
}
|
|
}
|
|
sendOutput(0x00) // Leitungen leeren
|
|
|
|
sendOutput(LATCH)
|
|
sendOutput(0x00)
|
|
|
|
fmt.Println("[✅] ERFOLG! Signal-Bytes an Schieberegister übergeben.")
|
|
fmt.Println("--- [USB DEBUG ENDE] ---\n")
|
|
return nil
|
|
}
|
|
|
|
// Callback: Wird vom Webserver aufgerufen, um den aktuellen UI-Status abzufragen
|
|
func getRelaisStateCallback() []web.RelaisData {
|
|
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
|
|
}
|
|
|
|
// 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 Version und Build-Datum zu erhalten
|
|
func callbackGetVersion() (string, string) {
|
|
return Version, BuildDate
|
|
}
|