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 }