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
|
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
|
||||||
|
|
|
||||||
185
api/shack.pb.go
185
api/shack.pb.go
|
|
@ -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,
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -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
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
|
#!/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
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 (
|
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"},
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
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]
|
[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"]
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue