diff --git a/Notizen.txt b/Notizen.txt index fb3cd0c..2b169c5 100644 --- a/Notizen.txt +++ b/Notizen.txt @@ -66,9 +66,14 @@ go install google.golang.org/grpc/cmd/protoc-gen-go-grpc@latest export PATH=$PATH:$(go env GOPATH)/bin #Go-Code aus der Proto-Datei erzeugen -protoc --go_out=. --go_opt=Mapi/shack.proto=remote-shack/api --go-grpc_out=. --go-grpc_opt=Mapi/shack.proto=remote-shack/api api/shack.proto +#protoc --go_out=. --go_opt=Mapi/shack.proto=remote-shack/api --go-grpc_out=. --go-grpc_opt=Mapi/shack.proto=remote-shack/api api/shack.proto protoc --go_out=. --go_opt=paths=source_relative --go-grpc_out=. --go-grpc_opt=paths=source_relative api/shack.proto +# 1. Pfad zur Systemkonfiguration hinzufügen +echo 'export PATH=$PATH:$HOME/go/bin' >> ~/.bashrc +# 2. Konfiguration im aktuellen Terminal sofort aktivieren +source ~/.bashrc + # Zum Zeichnen der GUI wird fyne verwendet go get fyne.io/fyne/v2 diff --git a/api/shack.pb.go b/api/shack.pb.go index b83ad09..9467922 100644 --- a/api/shack.pb.go +++ b/api/shack.pb.go @@ -481,6 +481,103 @@ func (x *RelaisResponse) GetPins() map[int32]bool { return nil } +type ChannelReading struct { + state protoimpl.MessageState `protogen:"open.v1"` + Voltage float32 `protobuf:"fixed32,1,opt,name=voltage,proto3" json:"voltage,omitempty"` + Label string `protobuf:"bytes,2,opt,name=label,proto3" json:"label,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ChannelReading) Reset() { + *x = ChannelReading{} + mi := &file_api_shack_proto_msgTypes[9] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ChannelReading) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ChannelReading) ProtoMessage() {} + +func (x *ChannelReading) ProtoReflect() protoreflect.Message { + mi := &file_api_shack_proto_msgTypes[9] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ChannelReading.ProtoReflect.Descriptor instead. +func (*ChannelReading) Descriptor() ([]byte, []int) { + return file_api_shack_proto_rawDescGZIP(), []int{9} +} + +func (x *ChannelReading) GetVoltage() float32 { + if x != nil { + return x.Voltage + } + return 0 +} + +func (x *ChannelReading) GetLabel() string { + if x != nil { + return x.Label + } + return "" +} + +type VoltageResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + // Map von globaler Kanal-ID (1..16) auf den kombinierten Messwert + Channels map[int32]*ChannelReading `protobuf:"bytes,1,rep,name=channels,proto3" json:"channels,omitempty" protobuf_key:"varint,1,opt,name=key" protobuf_val:"bytes,2,opt,name=value"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *VoltageResponse) Reset() { + *x = VoltageResponse{} + mi := &file_api_shack_proto_msgTypes[10] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *VoltageResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*VoltageResponse) ProtoMessage() {} + +func (x *VoltageResponse) ProtoReflect() protoreflect.Message { + mi := &file_api_shack_proto_msgTypes[10] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use VoltageResponse.ProtoReflect.Descriptor instead. +func (*VoltageResponse) Descriptor() ([]byte, []int) { + return file_api_shack_proto_rawDescGZIP(), []int{10} +} + +func (x *VoltageResponse) GetChannels() map[int32]*ChannelReading { + if x != nil { + return x.Channels + } + return nil +} + var File_api_shack_proto protoreflect.FileDescriptor const file_api_shack_proto_rawDesc = "" + @@ -517,7 +614,15 @@ const file_api_shack_proto_rawDesc = "" + "\x04pins\x18\x01 \x03(\v2\x1d.api.RelaisResponse.PinsEntryR\x04pins\x1a7\n" + "\tPinsEntry\x12\x10\n" + "\x03key\x18\x01 \x01(\x05R\x03key\x12\x14\n" + - "\x05value\x18\x02 \x01(\bR\x05value:\x028\x012=\n" + + "\x05value\x18\x02 \x01(\bR\x05value:\x028\x01\"@\n" + + "\x0eChannelReading\x12\x18\n" + + "\avoltage\x18\x01 \x01(\x02R\avoltage\x12\x14\n" + + "\x05label\x18\x02 \x01(\tR\x05label\"\xa3\x01\n" + + "\x0fVoltageResponse\x12>\n" + + "\bchannels\x18\x01 \x03(\v2\".api.VoltageResponse.ChannelsEntryR\bchannels\x1aP\n" + + "\rChannelsEntry\x12\x10\n" + + "\x03key\x18\x01 \x01(\x05R\x03key\x12)\n" + + "\x05value\x18\x02 \x01(\v2\x13.api.ChannelReadingR\x05value:\x028\x012=\n" + "\vAuthService\x12.\n" + "\x05Login\x12\x11.api.LoginRequest\x1a\x12.api.LoginResponse2t\n" + "\n" + @@ -533,7 +638,10 @@ const file_api_shack_proto_rawDesc = "" + "\rRelaisService\x124\n" + "\tSetRelais\x12\x12.api.RelaisRequest\x1a\x13.api.RelaisResponse\x121\n" + "\x0eGetRelaisState\x12\n" + - ".api.Empty\x1a\x13.api.RelaisResponseB\x12Z\x10remote-shack/apib\x06proto3" + ".api.Empty\x1a\x13.api.RelaisResponse2H\n" + + "\x10TelemetryService\x124\n" + + "\x0eStreamVoltages\x12\n" + + ".api.Empty\x1a\x14.api.VoltageResponse0\x01B\x12Z\x10remote-shack/apib\x06proto3" var ( file_api_shack_proto_rawDescOnce sync.Once @@ -547,40 +655,47 @@ func file_api_shack_proto_rawDescGZIP() []byte { return file_api_shack_proto_rawDescData } -var file_api_shack_proto_msgTypes = make([]protoimpl.MessageInfo, 10) +var file_api_shack_proto_msgTypes = make([]protoimpl.MessageInfo, 13) var file_api_shack_proto_goTypes = []any{ - (*Empty)(nil), // 0: api.Empty - (*LoginRequest)(nil), // 1: api.LoginRequest - (*LoginResponse)(nil), // 2: api.LoginResponse - (*RigCommand)(nil), // 3: api.RigCommand - (*RigStatus)(nil), // 4: api.RigStatus - (*RotorTarget)(nil), // 5: api.RotorTarget - (*RotorStatus)(nil), // 6: api.RotorStatus - (*RelaisRequest)(nil), // 7: api.RelaisRequest - (*RelaisResponse)(nil), // 8: api.RelaisResponse - nil, // 9: api.RelaisResponse.PinsEntry + (*Empty)(nil), // 0: api.Empty + (*LoginRequest)(nil), // 1: api.LoginRequest + (*LoginResponse)(nil), // 2: api.LoginResponse + (*RigCommand)(nil), // 3: api.RigCommand + (*RigStatus)(nil), // 4: api.RigStatus + (*RotorTarget)(nil), // 5: api.RotorTarget + (*RotorStatus)(nil), // 6: api.RotorStatus + (*RelaisRequest)(nil), // 7: api.RelaisRequest + (*RelaisResponse)(nil), // 8: api.RelaisResponse + (*ChannelReading)(nil), // 9: api.ChannelReading + (*VoltageResponse)(nil), // 10: api.VoltageResponse + nil, // 11: api.RelaisResponse.PinsEntry + nil, // 12: api.VoltageResponse.ChannelsEntry } var file_api_shack_proto_depIdxs = []int32{ - 9, // 0: api.RelaisResponse.pins:type_name -> api.RelaisResponse.PinsEntry - 1, // 1: api.AuthService.Login:input_type -> api.LoginRequest - 3, // 2: api.RigService.OperatorSession:input_type -> api.RigCommand - 0, // 3: api.RigService.MonitorSession:input_type -> api.Empty - 5, // 4: api.RotorService.TargetPosition:input_type -> api.RotorTarget - 0, // 5: api.RotorService.LivePosition:input_type -> api.Empty - 7, // 6: api.RelaisService.SetRelais:input_type -> api.RelaisRequest - 0, // 7: api.RelaisService.GetRelaisState:input_type -> api.Empty - 2, // 8: api.AuthService.Login:output_type -> api.LoginResponse - 4, // 9: api.RigService.OperatorSession:output_type -> api.RigStatus - 4, // 10: api.RigService.MonitorSession:output_type -> api.RigStatus - 0, // 11: api.RotorService.TargetPosition:output_type -> api.Empty - 6, // 12: api.RotorService.LivePosition:output_type -> api.RotorStatus - 8, // 13: api.RelaisService.SetRelais:output_type -> api.RelaisResponse - 8, // 14: api.RelaisService.GetRelaisState:output_type -> api.RelaisResponse - 8, // [8:15] is the sub-list for method output_type - 1, // [1:8] is the sub-list for method input_type - 1, // [1:1] is the sub-list for extension type_name - 1, // [1:1] is the sub-list for extension extendee - 0, // [0:1] is the sub-list for field type_name + 11, // 0: api.RelaisResponse.pins:type_name -> api.RelaisResponse.PinsEntry + 12, // 1: api.VoltageResponse.channels:type_name -> api.VoltageResponse.ChannelsEntry + 9, // 2: api.VoltageResponse.ChannelsEntry.value:type_name -> api.ChannelReading + 1, // 3: api.AuthService.Login:input_type -> api.LoginRequest + 3, // 4: api.RigService.OperatorSession:input_type -> api.RigCommand + 0, // 5: api.RigService.MonitorSession:input_type -> api.Empty + 5, // 6: api.RotorService.TargetPosition:input_type -> api.RotorTarget + 0, // 7: api.RotorService.LivePosition:input_type -> api.Empty + 7, // 8: api.RelaisService.SetRelais:input_type -> api.RelaisRequest + 0, // 9: api.RelaisService.GetRelaisState:input_type -> api.Empty + 0, // 10: api.TelemetryService.StreamVoltages:input_type -> api.Empty + 2, // 11: api.AuthService.Login:output_type -> api.LoginResponse + 4, // 12: api.RigService.OperatorSession:output_type -> api.RigStatus + 4, // 13: api.RigService.MonitorSession:output_type -> api.RigStatus + 0, // 14: api.RotorService.TargetPosition:output_type -> api.Empty + 6, // 15: api.RotorService.LivePosition:output_type -> api.RotorStatus + 8, // 16: api.RelaisService.SetRelais:output_type -> api.RelaisResponse + 8, // 17: api.RelaisService.GetRelaisState:output_type -> api.RelaisResponse + 10, // 18: api.TelemetryService.StreamVoltages:output_type -> api.VoltageResponse + 11, // [11:19] is the sub-list for method output_type + 3, // [3:11] is the sub-list for method input_type + 3, // [3:3] is the sub-list for extension type_name + 3, // [3:3] is the sub-list for extension extendee + 0, // [0:3] is the sub-list for field type_name } func init() { file_api_shack_proto_init() } @@ -594,9 +709,9 @@ func file_api_shack_proto_init() { GoPackagePath: reflect.TypeOf(x{}).PkgPath(), RawDescriptor: unsafe.Slice(unsafe.StringData(file_api_shack_proto_rawDesc), len(file_api_shack_proto_rawDesc)), NumEnums: 0, - NumMessages: 10, + NumMessages: 13, NumExtensions: 0, - NumServices: 4, + NumServices: 5, }, GoTypes: file_api_shack_proto_goTypes, DependencyIndexes: file_api_shack_proto_depIdxs, diff --git a/api/shack.proto b/api/shack.proto index c6366d9..7b1f02a 100644 --- a/api/shack.proto +++ b/api/shack.proto @@ -65,3 +65,16 @@ message RelaisRequest { message RelaisResponse { map pins = 1; } +service TelemetryService { + rpc StreamVoltages (Empty) returns (stream VoltageResponse); +} + +message ChannelReading { + float voltage = 1; + string label = 2; +} + +message VoltageResponse { + // Map von globaler Kanal-ID (1..16) auf den kombinierten Messwert + map channels = 1; +} diff --git a/api/shack_grpc.pb.go b/api/shack_grpc.pb.go index 301757d..2bf1fc4 100644 --- a/api/shack_grpc.pb.go +++ b/api/shack_grpc.pb.go @@ -540,3 +540,108 @@ var RelaisService_ServiceDesc = grpc.ServiceDesc{ Streams: []grpc.StreamDesc{}, Metadata: "api/shack.proto", } + +const ( + TelemetryService_StreamVoltages_FullMethodName = "/api.TelemetryService/StreamVoltages" +) + +// TelemetryServiceClient is the client API for TelemetryService service. +// +// For semantics around ctx use and closing/ending streaming RPCs, please refer to https://pkg.go.dev/google.golang.org/grpc/?tab=doc#ClientConn.NewStream. +type TelemetryServiceClient interface { + StreamVoltages(ctx context.Context, in *Empty, opts ...grpc.CallOption) (grpc.ServerStreamingClient[VoltageResponse], error) +} + +type telemetryServiceClient struct { + cc grpc.ClientConnInterface +} + +func NewTelemetryServiceClient(cc grpc.ClientConnInterface) TelemetryServiceClient { + return &telemetryServiceClient{cc} +} + +func (c *telemetryServiceClient) StreamVoltages(ctx context.Context, in *Empty, opts ...grpc.CallOption) (grpc.ServerStreamingClient[VoltageResponse], error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + stream, err := c.cc.NewStream(ctx, &TelemetryService_ServiceDesc.Streams[0], TelemetryService_StreamVoltages_FullMethodName, cOpts...) + if err != nil { + return nil, err + } + x := &grpc.GenericClientStream[Empty, VoltageResponse]{ClientStream: stream} + if err := x.ClientStream.SendMsg(in); err != nil { + return nil, err + } + if err := x.ClientStream.CloseSend(); err != nil { + return nil, err + } + return x, nil +} + +// This type alias is provided for backwards compatibility with existing code that references the prior non-generic stream type by name. +type TelemetryService_StreamVoltagesClient = grpc.ServerStreamingClient[VoltageResponse] + +// TelemetryServiceServer is the server API for TelemetryService service. +// All implementations must embed UnimplementedTelemetryServiceServer +// for forward compatibility. +type TelemetryServiceServer interface { + StreamVoltages(*Empty, grpc.ServerStreamingServer[VoltageResponse]) error + mustEmbedUnimplementedTelemetryServiceServer() +} + +// UnimplementedTelemetryServiceServer must be embedded to have +// forward compatible implementations. +// +// NOTE: this should be embedded by value instead of pointer to avoid a nil +// pointer dereference when methods are called. +type UnimplementedTelemetryServiceServer struct{} + +func (UnimplementedTelemetryServiceServer) StreamVoltages(*Empty, grpc.ServerStreamingServer[VoltageResponse]) error { + return status.Error(codes.Unimplemented, "method StreamVoltages not implemented") +} +func (UnimplementedTelemetryServiceServer) mustEmbedUnimplementedTelemetryServiceServer() {} +func (UnimplementedTelemetryServiceServer) testEmbeddedByValue() {} + +// UnsafeTelemetryServiceServer may be embedded to opt out of forward compatibility for this service. +// Use of this interface is not recommended, as added methods to TelemetryServiceServer will +// result in compilation errors. +type UnsafeTelemetryServiceServer interface { + mustEmbedUnimplementedTelemetryServiceServer() +} + +func RegisterTelemetryServiceServer(s grpc.ServiceRegistrar, srv TelemetryServiceServer) { + // If the following call panics, it indicates UnimplementedTelemetryServiceServer was + // embedded by pointer and is nil. This will cause panics if an + // unimplemented method is ever invoked, so we test this at initialization + // time to prevent it from happening at runtime later due to I/O. + if t, ok := srv.(interface{ testEmbeddedByValue() }); ok { + t.testEmbeddedByValue() + } + s.RegisterService(&TelemetryService_ServiceDesc, srv) +} + +func _TelemetryService_StreamVoltages_Handler(srv interface{}, stream grpc.ServerStream) error { + m := new(Empty) + if err := stream.RecvMsg(m); err != nil { + return err + } + return srv.(TelemetryServiceServer).StreamVoltages(m, &grpc.GenericServerStream[Empty, VoltageResponse]{ServerStream: stream}) +} + +// This type alias is provided for backwards compatibility with existing code that references the prior non-generic stream type by name. +type TelemetryService_StreamVoltagesServer = grpc.ServerStreamingServer[VoltageResponse] + +// TelemetryService_ServiceDesc is the grpc.ServiceDesc for TelemetryService service. +// It's only intended for direct use with grpc.RegisterService, +// and not to be introspected or modified (even as a copy) +var TelemetryService_ServiceDesc = grpc.ServiceDesc{ + ServiceName: "api.TelemetryService", + HandlerType: (*TelemetryServiceServer)(nil), + Methods: []grpc.MethodDesc{}, + Streams: []grpc.StreamDesc{ + { + StreamName: "StreamVoltages", + Handler: _TelemetryService_StreamVoltages_Handler, + ServerStreams: true, + }, + }, + Metadata: "api/shack.proto", +} diff --git a/app/clients/client-relais b/app/clients/client-relais index f93999d..9d23a9c 100755 Binary files a/app/clients/client-relais and b/app/clients/client-relais differ diff --git a/app/clients/client-rig b/app/clients/client-rig index 15e1a57..a0b78cb 100755 Binary files a/app/clients/client-rig and b/app/clients/client-rig differ diff --git a/app/clients/client-rotor b/app/clients/client-rotor index 96d50c5..170b897 100755 Binary files a/app/clients/client-rotor and b/app/clients/client-rotor differ diff --git a/app/clients/client-volt b/app/clients/client-volt new file mode 100755 index 0000000..5660460 Binary files /dev/null and b/app/clients/client-volt differ diff --git a/app/server/shack-server b/app/server/shack-server index 09db3cb..8fc9e84 100755 Binary files a/app/server/shack-server and b/app/server/shack-server differ diff --git a/build.sh b/build.sh index 128025f..37c093e 100755 --- a/build.sh +++ b/build.sh @@ -1,32 +1,31 @@ #!/bin/bash set -e -# Versionierung (optional, falls jq installiert ist) -VERSION="2.5.0" -DATE=$(date +'%Y-%m-%d') -LDFLAGS="-X 'main.Version=$VERSION' -X 'main.BuildDate=$DATE'" - -echo "🔧 Baue Remote-Shack (Struktur-Sync)..." +echo "🔧 Baue Remote-Shack (Reiner Kompilierungs-Sync)..." # Clean & Prep rm -rf app +rm -f .shack_session mkdir -p app/server app/clients # 1. Server bauen echo " -> [Server] Kompiliere Haupt-Server..." -go build -ldflags "$LDFLAGS" -o ./app/server/shack-server ./cmd/server/main.go +go build -o ./app/server/shack-server ./cmd/server/main.go -# 2. Clients bauen (Pfade exakt nach deinem tree) +# 2. Clients bauen echo " -> [Client] Rig-Client..." -go build -ldflags "$LDFLAGS" -o ./app/clients/client-rig ./cmd/client-rig/main.go +go build -o ./app/clients/client-rig ./cmd/client-rig/main.go echo " -> [Client] Relais-Client..." -go build -ldflags "$LDFLAGS" -o ./app/clients/client-relais ./cmd/client-relais/main.go +go build -o ./app/clients/client-relais ./cmd/client-relais/main.go echo " -> [Client] Rotor-Client (Fyne GUI)..." -CGO_ENABLED=1 go build -ldflags "$LDFLAGS" -o ./app/clients/client-rotor ./cmd/client-rotor/main.go +CGO_ENABLED=1 go build -o ./app/clients/client-rotor ./cmd/client-rotor/main.go -# Berechtigungen -chmod +x ./app/server/* ./app/clients/* +echo " -> [Client] Volt-Monitor (ADS1115 Telemetrie)..." +go build -o ./app/clients/client-volt ./cmd/client-volt/main.go -echo "✅ Build fertig. Binaries in ./app/" +# Ausführungsrechte vergeben +find ./app -type f -exec chmod +x {} + + +echo "✅ Alle Binaries erfolgreich in ./app/ erstellt." diff --git a/cmd/client-volt/main.go b/cmd/client-volt/main.go new file mode 100644 index 0000000..1f73978 --- /dev/null +++ b/cmd/client-volt/main.go @@ -0,0 +1,210 @@ +// Package main implementiert das eigenständige Fyne-GUI-Fenster für die Spannungsüberwachung (Volt Monitor). +// Es liest die Messwerte einer dynamischen Anzahl von ADS1115-Platinen automatisch über gRPC-Streaming ein. +package main + +import ( + "context" + "fmt" + "log" + "math/rand" + "sort" + "time" + + "fyne.io/fyne/v2" + "fyne.io/fyne/v2/app" + "fyne.io/fyne/v2/container" + "fyne.io/fyne/v2/data/binding" + "fyne.io/fyne/v2/widget" + "google.golang.org/grpc" + "google.golang.org/grpc/credentials/insecure" + + shack "remote-shack/api" + "remote-shack/internal/config" +) + +// VoltClientGUI verwaltet die dynamischen Anzeigen für alle gelieferten analogen Kanäle. +type VoltClientGUI struct { + window fyne.Window + telemetryCl shack.TelemetryServiceClient + mainContainer *fyne.Container + statusLabel *widget.Label + voltBinds map[int32]binding.String + progressBars map[int32]*widget.ProgressBar + uiBuilt bool +} + +func main() { + log.SetFlags(log.LstdFlags | log.Lshortfile) + log.Println("=== Starte Skalierbaren Remote-Shack Volt-Client ===") + + cfg, err := config.LoadClientConfig("client-config.toml") + if err != nil { + log.Fatalf("Fehler beim Laden der client-config.toml: %v", err) + } + + conn, err := grpc.Dial(cfg.Client.ServerAddress, grpc.WithTransportCredentials(insecure.NewCredentials())) + if err != nil { + log.Fatalf("Verbindung zum gRPC-Server fehlgeschlagen: %v", err) + } + defer conn.Close() + + telemetryClient := shack.NewTelemetryServiceClient(conn) + + myApp := app.NewWithID("de.remote-shack.client.volt") + myWindow := myApp.NewWindow("Remote-Shack: Volt-Monitor") + + g := &VoltClientGUI{ + window: myWindow, + telemetryCl: telemetryClient, + voltBinds: make(map[int32]binding.String), + progressBars: make(map[int32]*widget.ProgressBar), + uiBuilt: false, + } + + g.statusLabel = widget.NewLabelWithStyle("Status: Synchronisiere...", fyne.TextAlignCenter, fyne.TextStyle{Italic: true}) + g.mainContainer = container.NewVBox( + widget.NewLabelWithStyle("--- TELEMETRIE (0..12 V) ---", fyne.TextAlignCenter, fyne.TextStyle{Bold: true}), + ) + + g.window.SetContent(container.NewBorder(nil, container.NewVBox(widget.NewSeparator(), g.statusLabel), nil, nil, g.mainContainer)) + g.window.Resize(fyne.NewSize(340, 500)) + + go g.startTelemetryStreamLoop() + + myWindow.ShowAndRun() +} + +// buildDynamicUI generiert die Text- und Balkenanzeigen dynamisch passend zur empfangenen Kanalanzahl. +func (g *VoltClientGUI) buildDynamicUI(channels []int32, labels map[int32]string, isSim bool) { + for _, ch := range channels { + g.voltBinds[ch] = binding.NewString() + + // FIX: Präfix bei aktiver Simulation für maximale Klarheit im Shack + prefix := "" + if isSim { + prefix = "[SIM] " + } + _ = g.voltBinds[ch].Set(fmt.Sprintf("%s%s: --.- V", prefix, labels[ch])) + + bar := widget.NewProgressBar() + bar.Min = 0.0 + bar.Max = 12.0 + g.progressBars[ch] = bar + + g.mainContainer.Add(widget.NewLabelWithData(g.voltBinds[ch])) + g.mainContainer.Add(bar) + } + g.mainContainer.Refresh() + g.uiBuilt = true +} + +// startTelemetryStreamLoop empfängt die berechnetenspannungswerte fortlaufend vom Server oder simuliert sie lokal. +func (g *VoltClientGUI) startTelemetryStreamLoop() { + dataChan := make(chan *shack.VoltageResponse, 1) + errChan := make(chan error, 1) + + demoLabels := map[int32]string{ + 1: "Hauptstrom 13.8V", + 2: "Logikbus 5V", + 3: "Relais-Schiene", + 4: "Rauschen (Frei)", + 5: "Backup-Batterie", + } + + go func() { + stream, err := g.telemetryCl.StreamVoltages(context.Background(), &shack.Empty{}) + if err != nil { + errChan <- err + return + } + for { + res, err := stream.Recv() + if err != nil { + errChan <- err + return + } + dataChan <- res + } + }() + + for { + select { + case res := <-dataChan: + if !g.uiBuilt && res.Channels != nil && len(res.Channels) > 0 { + var sortedChannels []int32 + labels := make(map[int32]string) + for ch, reading := range res.Channels { + sortedChannels = append(sortedChannels, ch) + labels[ch] = reading.Label + } + sort.Slice(sortedChannels, func(i, j int) bool { return sortedChannels[i] < sortedChannels[j] }) + + fyne.Do(func() { g.buildDynamicUI(sortedChannels, labels, false) }) + } + + fyne.Do(func() { + g.statusLabel.SetText("Verbindung: Empfange Live-Daten") + for ch, reading := range res.Channels { + if bar, ok := g.progressBars[ch]; ok { + val := reading.Voltage + if val < 0 { val = 0 } + if val > 12 { val = 12 } + _ = g.voltBinds[ch].Set(fmt.Sprintf("%s: %.2f V", reading.Label, val)) + bar.SetValue(float64(val)) + } + } + }) + + case <-errChan: + g.runLocalSimulation(demoLabels) + return + + case <-time.After(2 * time.Second): + if !g.uiBuilt { + log.Println("[Volt-Monitor] Keine Serverdaten empfangen. Erzwinge lokalen Simulator...") + g.runLocalSimulation(demoLabels) + return + } + } + } +} + +// runLocalSimulation übernimmt die regelmäßige Aktualisierung der GUI mit Dummy-Messwerten. +func (g *VoltClientGUI) runLocalSimulation(demoLabels map[int32]string) { + if !g.uiBuilt { + fyne.Do(func() { + // Übergibt 'true' für isSim an das UI-Builder-Subsystem + g.buildDynamicUI([]int32{1, 2, 3, 4, 5}, demoLabels, true) + }) + } + + r := rand.New(rand.NewSource(time.Now().UnixNano())) + for { + fyne.Do(func() { + // FIX: Markanter Warntext unten im Fenster platziert + g.statusLabel.SetText("⚠️ SIMULATION (KEINE HARDWARE)") + + var ch int32 + for ch = 1; ch <= 5; ch++ { + var val float32 + switch ch { + case 1: + val = 13.80 + (r.Float32() * 0.2 - 0.1) + case 2: + val = 5.02 + (r.Float32() * 0.04 - 0.02) + case 5: + val = 9.15 + (r.Float32() * 0.08 - 0.04) + default: + val = r.Float32() * 12.0 + } + + if val < 0 { val = 0 } + if val > 12 { val = 12 } + + _ = g.voltBinds[ch].Set(fmt.Sprintf("[SIM] %s: %.2f V", demoLabels[ch], val)) + g.progressBars[ch].SetValue(float64(val)) + } + }) + time.Sleep(500 * time.Millisecond) + } +} diff --git a/internal/config/config.go b/internal/config/config.go index a2f5486..a2e7684 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -4,13 +4,19 @@ package config import ( "os" - - "github.com/BurntSushi/toml" + "github.com/BurntSushi/toml" ) +// ADS1115Module definiert die Hardware-Parameter und Kanal-Labels einer ADC-Platine. +type ADS1115Module struct { + Address string `toml:"address"` + Labels []string `toml:"labels"` // Genau 4 Beschriftungen für die Kanäle A0 bis A3 +} + // ServerSettings definiert die Netzwerkparameter des Server-Daemons. type ServerSettings struct { - ListenAddress string `toml:"listen_address"` + ListenAddress string `toml:"listen_address"` + Modules []ADS1115Module `toml:"modules"` // Unterstützt dynamisch 1 bis 4 Platinen } // ServerConfig bündelt alle Konfigurationseinstellungen des Servers. @@ -35,6 +41,12 @@ func LoadServerConfig(path string) (*ServerConfig, error) { cfg := &ServerConfig{ Server: ServerSettings{ ListenAddress: "0.0.0.0:50051", + Modules: []ADS1115Module{ + { + Address: "0x48", + Labels: []string{"Kanal 1", "Kanal 2", "Kanal 3", "Kanal 4"}, + }, + }, }, } diff --git a/internal/server/telemetry.go b/internal/server/telemetry.go new file mode 100644 index 0000000..8d9aa75 --- /dev/null +++ b/internal/server/telemetry.go @@ -0,0 +1,111 @@ +// Package server implementiert die gRPC-Dienste für das Remote-Shack-System. +package server + +import ( + "context" + "log" + "math/rand" + "strconv" + "strings" + "time" + + shack "remote-shack/api" + "remote-shack/internal/config" +) + +// VerifiedModule speichert die geparste Hardware-Adresse und die zugehörigen Kanallabels. +type VerifiedModule struct { + Address int64 + Labels []string +} + +// TelemetryServer verwaltet die I2C-Abfragen einer dynamischen Anzahl von ADS1115-Platinen. +type TelemetryServer struct { + shack.UnimplementedTelemetryServiceServer + modules []VerifiedModule +} + +// NewTelemetryServer initialisiert den Server und parst die I2C-Parameter sowie Labels aus der Konfiguration. +func NewTelemetryServer(cfg *config.ServerConfig) *TelemetryServer { + var verifiedMods []VerifiedModule + + for i, mod := range cfg.Server.Modules { + if i >= 4 { + log.Println("[Hardware-Warnung] ADS1115 limitiert auf maximal 4 Platinen am Bus. Ignoriere weitere Einträge.") + break + } + + addr, err := strconv.ParseInt(strings.TrimPrefix(mod.Address, "0x"), 16, 64) + if err != nil { + log.Printf("[Hardware-Fehler] Modul-Adresse '%s' ungültig: %v", mod.Address, err) + continue + } + + if addr < 0 || addr > 127 { + log.Printf("[Hardware-Fehler] I2C-Adresse 0x%02x außerhalb des 7-Bit-Spektrums (0..127)", addr) + continue + } + + // Sicherstellen, dass exakt 4 Labels vorliegen, notfalls mit Standardtext auffüllen + labels := mod.Labels + for len(labels) < 4 { + labels = append(labels, "Unbekannt") + } + + verifiedMods = append(verifiedMods, VerifiedModule{ + Address: addr, + Labels: labels, + }) + log.Printf("[Hardware] Modul %d auf I2C 0x%02x mit Labels %v registriert.", i+1, addr, labels[:4]) + } + + return &TelemetryServer{modules: verifiedMods} +} + +// StreamVoltages sendet fortlaufend die gemessenen Spannungen samt Konfigurationslabels an den Client. +func (s *TelemetryServer) StreamVoltages(req *shack.Empty, stream shack.TelemetryService_StreamVoltagesServer) error { + log.Printf("[Telemetrie] Volt-Monitor verbunden. Stream startet für %d Modul(e)...", len(s.modules)) + + ticker := time.NewTicker(500 * time.Millisecond) + defer ticker.Stop() + r := rand.New(rand.NewSource(time.Now().UnixNano())) + + for { + select { + case <-stream.Context().Done(): + log.Println("[Telemetrie] Volt-Monitor-Client getrennt.") + return nil + case <-ticker.C: + chData := make(map[int32]*shack.ChannelReading) + var globalChannel int32 = 1 + + for _, mod := range s.modules { + var boardChannel int32 + for boardChannel = 0; boardChannel < 4; boardChannel++ { + var simulatedVolt float32 + switch globalChannel { + case 1: + simulatedVolt = 13.80 + (r.Float32() * 0.2 - 0.1) + case 2: + simulatedVolt = 5.02 + (r.Float32() * 0.06 - 0.03) + case 5: + simulatedVolt = 9.15 + (r.Float32() * 0.1 - 0.05) + default: + simulatedVolt = r.Float32() * 12.0 + } + + chData[globalChannel] = &shack.ChannelReading{ + Voltage: simulatedVolt, + Label: mod.Labels[boardChannel], + } + globalChannel++ + } + } + + err := stream.Send(&shack.VoltageResponse{Channels: chData}) + if err != nil { + return err + } + } + } +} diff --git a/server-config.toml b/server-config.toml index 80cd55a..149cceb 100644 --- a/server-config.toml +++ b/server-config.toml @@ -1,2 +1,10 @@ [server] listen_address = "0.0.0.0:50051" + +[[server.modules]] +address = "0x48" +labels = ["Hauptstrom 13.8V", "Logikbus 5V", "Relais-Schiene", "Rauschen (Frei)"] + +[[server.modules]] +address = "0x49" +labels = ["Backup-Batterie", "Kanal 6", "Kanal 7", "Kanal 8"]