Spannung/main.go

299 lines
8 KiB
Go

package main
import (
"encoding/json"
"fmt"
"log"
"os"
"sync"
"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"`
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() {
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)
}
// 1. 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)
}
// 2. 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)
}
// GPIO-Pins registrieren (ID 1 ist der 230V Hauptschalter am Raspberry Pi)
for i, 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)
}
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
}
// Beim Start alle USB-Relais initial in den definierten AUS-Zustand versetzen
updateUsbRelays()
// 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())
}
// 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
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 {
config.Relais[i].State = !config.Relais[i].State
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 {
// 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 {
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,
}
}
// Versionangaben aus version.json in Webserver übernehmen
func callbackGetVersion() (string, string) {
return Version, BuildDate
}