2026-06-13 13:02 Anzeige ADS1115 hinzugefügt

This commit is contained in:
Dieter Lang 2026-06-13 13:02:54 +02:00
parent 17e70ec914
commit 3967c46264
14 changed files with 631 additions and 53 deletions

View file

@ -66,9 +66,14 @@ go install google.golang.org/grpc/cmd/protoc-gen-go-grpc@latest
export PATH=$PATH:$(go env GOPATH)/bin export PATH=$PATH:$(go env GOPATH)/bin
#Go-Code aus der Proto-Datei erzeugen #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 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 # Zum Zeichnen der GUI wird fyne verwendet
go get fyne.io/fyne/v2 go get fyne.io/fyne/v2

View file

@ -481,6 +481,103 @@ func (x *RelaisResponse) GetPins() map[int32]bool {
return nil 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 var File_api_shack_proto protoreflect.FileDescriptor
const file_api_shack_proto_rawDesc = "" + 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" + "\x04pins\x18\x01 \x03(\v2\x1d.api.RelaisResponse.PinsEntryR\x04pins\x1a7\n" +
"\tPinsEntry\x12\x10\n" + "\tPinsEntry\x12\x10\n" +
"\x03key\x18\x01 \x01(\x05R\x03key\x12\x14\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" + "\vAuthService\x12.\n" +
"\x05Login\x12\x11.api.LoginRequest\x1a\x12.api.LoginResponse2t\n" + "\x05Login\x12\x11.api.LoginRequest\x1a\x12.api.LoginResponse2t\n" +
"\n" + "\n" +
@ -533,7 +638,10 @@ const file_api_shack_proto_rawDesc = "" +
"\rRelaisService\x124\n" + "\rRelaisService\x124\n" +
"\tSetRelais\x12\x12.api.RelaisRequest\x1a\x13.api.RelaisResponse\x121\n" + "\tSetRelais\x12\x12.api.RelaisRequest\x1a\x13.api.RelaisResponse\x121\n" +
"\x0eGetRelaisState\x12\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 ( var (
file_api_shack_proto_rawDescOnce sync.Once file_api_shack_proto_rawDescOnce sync.Once
@ -547,40 +655,47 @@ func file_api_shack_proto_rawDescGZIP() []byte {
return file_api_shack_proto_rawDescData 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{ var file_api_shack_proto_goTypes = []any{
(*Empty)(nil), // 0: api.Empty (*Empty)(nil), // 0: api.Empty
(*LoginRequest)(nil), // 1: api.LoginRequest (*LoginRequest)(nil), // 1: api.LoginRequest
(*LoginResponse)(nil), // 2: api.LoginResponse (*LoginResponse)(nil), // 2: api.LoginResponse
(*RigCommand)(nil), // 3: api.RigCommand (*RigCommand)(nil), // 3: api.RigCommand
(*RigStatus)(nil), // 4: api.RigStatus (*RigStatus)(nil), // 4: api.RigStatus
(*RotorTarget)(nil), // 5: api.RotorTarget (*RotorTarget)(nil), // 5: api.RotorTarget
(*RotorStatus)(nil), // 6: api.RotorStatus (*RotorStatus)(nil), // 6: api.RotorStatus
(*RelaisRequest)(nil), // 7: api.RelaisRequest (*RelaisRequest)(nil), // 7: api.RelaisRequest
(*RelaisResponse)(nil), // 8: api.RelaisResponse (*RelaisResponse)(nil), // 8: api.RelaisResponse
nil, // 9: api.RelaisResponse.PinsEntry (*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{ var file_api_shack_proto_depIdxs = []int32{
9, // 0: api.RelaisResponse.pins:type_name -> api.RelaisResponse.PinsEntry 11, // 0: api.RelaisResponse.pins:type_name -> api.RelaisResponse.PinsEntry
1, // 1: api.AuthService.Login:input_type -> api.LoginRequest 12, // 1: api.VoltageResponse.channels:type_name -> api.VoltageResponse.ChannelsEntry
3, // 2: api.RigService.OperatorSession:input_type -> api.RigCommand 9, // 2: api.VoltageResponse.ChannelsEntry.value:type_name -> api.ChannelReading
0, // 3: api.RigService.MonitorSession:input_type -> api.Empty 1, // 3: api.AuthService.Login:input_type -> api.LoginRequest
5, // 4: api.RotorService.TargetPosition:input_type -> api.RotorTarget 3, // 4: api.RigService.OperatorSession:input_type -> api.RigCommand
0, // 5: api.RotorService.LivePosition:input_type -> api.Empty 0, // 5: api.RigService.MonitorSession:input_type -> api.Empty
7, // 6: api.RelaisService.SetRelais:input_type -> api.RelaisRequest 5, // 6: api.RotorService.TargetPosition:input_type -> api.RotorTarget
0, // 7: api.RelaisService.GetRelaisState:input_type -> api.Empty 0, // 7: api.RotorService.LivePosition:input_type -> api.Empty
2, // 8: api.AuthService.Login:output_type -> api.LoginResponse 7, // 8: api.RelaisService.SetRelais:input_type -> api.RelaisRequest
4, // 9: api.RigService.OperatorSession:output_type -> api.RigStatus 0, // 9: api.RelaisService.GetRelaisState:input_type -> api.Empty
4, // 10: api.RigService.MonitorSession:output_type -> api.RigStatus 0, // 10: api.TelemetryService.StreamVoltages:input_type -> api.Empty
0, // 11: api.RotorService.TargetPosition:output_type -> api.Empty 2, // 11: api.AuthService.Login:output_type -> api.LoginResponse
6, // 12: api.RotorService.LivePosition:output_type -> api.RotorStatus 4, // 12: api.RigService.OperatorSession:output_type -> api.RigStatus
8, // 13: api.RelaisService.SetRelais:output_type -> api.RelaisResponse 4, // 13: api.RigService.MonitorSession:output_type -> api.RigStatus
8, // 14: api.RelaisService.GetRelaisState:output_type -> api.RelaisResponse 0, // 14: api.RotorService.TargetPosition:output_type -> api.Empty
8, // [8:15] is the sub-list for method output_type 6, // 15: api.RotorService.LivePosition:output_type -> api.RotorStatus
1, // [1:8] is the sub-list for method input_type 8, // 16: api.RelaisService.SetRelais:output_type -> api.RelaisResponse
1, // [1:1] is the sub-list for extension type_name 8, // 17: api.RelaisService.GetRelaisState:output_type -> api.RelaisResponse
1, // [1:1] is the sub-list for extension extendee 10, // 18: api.TelemetryService.StreamVoltages:output_type -> api.VoltageResponse
0, // [0:1] is the sub-list for field type_name 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() } func init() { file_api_shack_proto_init() }
@ -594,9 +709,9 @@ func file_api_shack_proto_init() {
GoPackagePath: reflect.TypeOf(x{}).PkgPath(), GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
RawDescriptor: unsafe.Slice(unsafe.StringData(file_api_shack_proto_rawDesc), len(file_api_shack_proto_rawDesc)), RawDescriptor: unsafe.Slice(unsafe.StringData(file_api_shack_proto_rawDesc), len(file_api_shack_proto_rawDesc)),
NumEnums: 0, NumEnums: 0,
NumMessages: 10, NumMessages: 13,
NumExtensions: 0, NumExtensions: 0,
NumServices: 4, NumServices: 5,
}, },
GoTypes: file_api_shack_proto_goTypes, GoTypes: file_api_shack_proto_goTypes,
DependencyIndexes: file_api_shack_proto_depIdxs, DependencyIndexes: file_api_shack_proto_depIdxs,

View file

@ -65,3 +65,16 @@ message RelaisRequest {
message RelaisResponse { message RelaisResponse {
map<int32, bool> pins = 1; map<int32, bool> 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<int32, ChannelReading> channels = 1;
}

View file

@ -540,3 +540,108 @@ var RelaisService_ServiceDesc = grpc.ServiceDesc{
Streams: []grpc.StreamDesc{}, Streams: []grpc.StreamDesc{},
Metadata: "api/shack.proto", 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",
}

Binary file not shown.

Binary file not shown.

Binary file not shown.

BIN
app/clients/client-volt Executable file

Binary file not shown.

Binary file not shown.

View file

@ -1,32 +1,31 @@
#!/bin/bash #!/bin/bash
set -e set -e
# Versionierung (optional, falls jq installiert ist) echo "🔧 Baue Remote-Shack (Reiner Kompilierungs-Sync)..."
VERSION="2.5.0"
DATE=$(date +'%Y-%m-%d')
LDFLAGS="-X 'main.Version=$VERSION' -X 'main.BuildDate=$DATE'"
echo "🔧 Baue Remote-Shack (Struktur-Sync)..."
# Clean & Prep # Clean & Prep
rm -rf app rm -rf app
rm -f .shack_session
mkdir -p app/server app/clients mkdir -p app/server app/clients
# 1. Server bauen # 1. Server bauen
echo " -> [Server] Kompiliere Haupt-Server..." 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..." 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..." 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)..." 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 echo " -> [Client] Volt-Monitor (ADS1115 Telemetrie)..."
chmod +x ./app/server/* ./app/clients/* 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."

210
cmd/client-volt/main.go Normal file
View file

@ -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)
}
}

View file

@ -4,13 +4,19 @@ package config
import ( import (
"os" "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. // ServerSettings definiert die Netzwerkparameter des Server-Daemons.
type ServerSettings struct { 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. // ServerConfig bündelt alle Konfigurationseinstellungen des Servers.
@ -35,6 +41,12 @@ func LoadServerConfig(path string) (*ServerConfig, error) {
cfg := &ServerConfig{ cfg := &ServerConfig{
Server: ServerSettings{ Server: ServerSettings{
ListenAddress: "0.0.0.0:50051", ListenAddress: "0.0.0.0:50051",
Modules: []ADS1115Module{
{
Address: "0x48",
Labels: []string{"Kanal 1", "Kanal 2", "Kanal 3", "Kanal 4"},
},
},
}, },
} }

View file

@ -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
}
}
}
}

View file

@ -1,2 +1,10 @@
[server] [server]
listen_address = "0.0.0.0:50051" 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"]