2026-06-13 13:02 Anzeige ADS1115 hinzugefügt
This commit is contained in:
parent
17e70ec914
commit
3967c46264
14 changed files with 631 additions and 53 deletions
|
|
@ -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
|
||||
|
|
|
|||
185
api/shack.pb.go
185
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,
|
||||
|
|
|
|||
|
|
@ -65,3 +65,16 @@ message RelaisRequest {
|
|||
message RelaisResponse {
|
||||
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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
}
|
||||
|
|
|
|||
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
app/clients/client-volt
Executable file
BIN
app/clients/client-volt
Executable file
Binary file not shown.
Binary file not shown.
27
build.sh
27
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."
|
||||
|
|
|
|||
210
cmd/client-volt/main.go
Normal file
210
cmd/client-volt/main.go
Normal 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)
|
||||
}
|
||||
}
|
||||
|
|
@ -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"},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
|
|
|
|||
111
internal/server/telemetry.go
Normal file
111
internal/server/telemetry.go
Normal 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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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"]
|
||||
|
|
|
|||
Loading…
Reference in a new issue