Anzeige
Anzeige
Anzeige
Anzeige
Anzeige
Anzeige
Lesedauer 8 Min.

Commands in SignalRC

Durch das Zusammenspiel verschiedener Entwicklungsmuster lassen sich interessante Verhaltensweisen für die Steuerung von RC-Modellen aufbauen.
© Sofija De Mitri, Patrizio De Mitri, Event Wave

Dieser Artikel ist Teil der Reihe vertiefender Beiträge zur Steuerung unseres DDC-Trucks, den wir im Artikel „SignalRC – in Echtzeit ans Steuer“ [1] ausführlich vorgestellt haben. In dieser Folge möchte ich darauf eingehen, wie die Commands für die Steuerung vom Client zum Fahrzeug gelangen und warum das alles am Ende durchaus einer übergreifenden Strategie folgt. Entsprechend fokussiert sich dieser Beitrag darauf, welche Muster zum Einsatz kommen.

Definition: Was ist ein Command?

Ein Command bezeichnet in diesem Kontext ein strukturiertes Steuerobjekt. Es beschreibt, welcher Kanal eines Fahrzeugs auf welchen Wert gesetzt werden soll. Commands sind die eigentliche Abstraktion zwischen User Interface und dem Onboard-Client. 

Wie verläuft der Datenfluss?

Wir starten im UI, denn hier wird das Command erstellt. Folgende Bestandteile finden wir in dem Command:

  • FahrzeugId: Referenz zum Ausführer
  • SessionId: Authentifizierungscookie-like als string
  • ChannelId: Referenz auf den Channel
  • Value: Der neue Wert, der geschrieben wird
  • Sender: SignalR-Session

 

All das wird an den Hub geschickt. Dieser löst damit den Empfänger auf. Danach sendet er den gesamten Request an das entsprechende Fahrzeug. Dieses nimmt die Daten auf und prüft zunächst gegen die aktuelle Session, ob die ID übereinstimmt. Falls nein, wird das Paket direkt verworfen.

Danach wird aus dem Command die ChannelId geholt, die vorher bereits mit dem Server abgeglichen wurde, damit alle Kanäle eine ID als int haben. So muss nicht für jeden Befehl der Name übertragen werden. Das spart ein paar Daten.

Aus den abgeglichenen ChannelIds wird der entsprechende Channel Name. Dieser dient zum Auflösen der ControlTypes, der in der ChannelMap.json definiert ist.

Die Channels wurden beim Start einmalig initialisiert und werden entsprechend in einem Dictionary vorgehalten. Danach wird die Channel-Instanz via OnControlRecived(float value) mit dem aktuellen Wert geupdatet.

Welche Entwurfsmuster sind enthalten? 

Die Entwurfsmuster, die wir innerhalb dieses Ablaufs finden, möchte ich nun gerne in der Reihenfolge durchgehen, wie der Programmfluss sie vorgibt. 

Factory Pattern: Initialisierung der Kontrollfunktionen (Start des Onboard-Clients)

Zuallererst, beim Start, werden die Funktionen des Fahrzeugs definiert. Dabei werden sie sowohl mit den nötigen Einstellungen als auch mit den Services versorgt, die sie zum Funktionieren brauchen.

Aus der ChannelMap.json werden alle Channels als Instanzen einmalig zusammengebaut. Dazu wird das Factory Pattern verwendet: Config in die Factory rein, Instanz des Elements raus.

Innerhalb des ControlExecutionService wird beim Initialisieren für jeden Channel eine Instanz gebaut.

 

{
   "pinManagers": { ... },
   "controlChannels": {
     "leftTurnLight": {
       "controlType": "PwmBlinker",
       "pinManager": "Pca9685",
       "address": 9,
       "options": {
         "blinkCycleMs": 500
       }
     }
   }
 }

 

Dabei wird der Property Name im JSON als Key für die Instanz benutzt. Das macht das JSON selbst lesbarer. Einige Properties werden von der Factory selbst genutzt, während andere als Teil des Produktionsprozesses in die Instanz Properties gemappt werden.

Als Erstes wird entschieden, welcher Type produziert werden soll: ControlType. Die eigentliche Factory ist der IServiceProvider der Anwendung, an dieser Stelle nur mit nachträglicher Konfiguration.

Entsprechend der Konfiguration wird die neue Instanz mit unterschiedlichen Constructor-Parametern und Properties versorgt. Danach gibt die Factory die fertige Instanz zurück.

 

foreach (var channel in _channelMap.ControlChannels)
 {
     var controlType = GetControlType(channel.Value.ControlType); // Ermitteln des Typs (gleich mehr dazu)
     var control = ServiceProvider.GetService(controlType) as ControlTypeBase; // Instanzieren via Service Provider
     if (control == null)
         throw new Exception($"ControlType '{channel.Value.ControlType}' can not be instantiated");
     
     // Hardware Manager konfigurieren
     var pinManagerName = channel.Value.PinManager;
     if (string.IsNullOrWhiteSpace(pinManagerName))
         pinManagerName = "default"; // Default pin manager if not specified
     var pinManager = ServiceProvider.GetRequiredService<IModuleManagerFactory>().Create(pinManagerName); // auch eine weitere Factory
     if (pinManager == null)
         throw new Exception($"Pin manager '{pinManagerName}' not found in ChannelMap.");
 
     // Instanz konfigurieren
     control.PinManager = pinManager;
     control.Name = channel.Key;
     control.Options = channel.Value.Options;
     control.TestDisabled = channel.Value.TestDisabled;
     control.Address = channel.Value.Address;
     if (channel.Value.MaxResendInterval is not null)
     {
         // Decorate (gleich mehr)
         control = new ResendRequiredContolDecorator(control, TimeSpan.FromMilliseconds(channel.Value.MaxResendInterval), TimeSpan.FromMilliseconds(channel.Value.MaxResendInterval) / 3);
     }
     control.Initialize();
     // Instanz zurückgeben
     _controls.Add(channel.Key, control);
 }

 

Ich weiß: Diese Initialisierung könnte noch in eine eigene Klasse für jede einzelne Instanz ausgelagert werden. Das schien mir dennoch etwas Overkill zu sein – zumindest in diesem Fall.

Decorator Pattern: Dynamische Instanzen mit Standardverhalten erweitern

In der Factory haben wir gesehen, dass die Instanzen bei bestimmten Konfigurationen durch andere Instanzen ersetzt werden. Das bedeutet, dass an dieser Stelle das Decorator Pattern im Einsatz ist. Man erkennt es meist deutlich daran, dass sich ein Aufruf wie dieser im Code findet:

 

// Verwendung:
 IInterface objekt = new EineKlasse();
 objekt = new AndereKlasse(objekt);
 objekt.Execute();
 
 public interface IInterface { 
     void Execute(); 
 }
 
 public class EineKlasse : IInterface
 {
     public void Execute() => Console.WriteLine("Grundverhalten");
 }
 
 public class AndereKlasse : IInterface
 {
     private readonly IInterface _inner;
     public AndereKlasse(IInterface inner) => _inner = inner;
     public void Execute()
     {
         Console.WriteLine("Vorher: Zusatzverhalten");
         _inner.Execute();
         Console.WriteLine("Nachher: Zusatzverhalten");
     }
 }

 

Das Decorator Pattern macht es sehr einfach, ein Objekt mit weiteren Funktionen zu versehen, ohne dass der eigentliche Code des Grundverhaltens angepasst werden muss.

Das ist bei unserer Anforderung praktisch, denn wir wollen beispielsweise das Verhalten des Resend-Features nicht für alle Steuerungen haben. So können wir gezielt Verhaltensweisen hinzufügen, ohne überhaupt wissen zu müssen, zu welchen Funktionen wir diese hinzufügen.  

Service Locator: Magic Strings zu Typen auflösen

GetControlType gibt uns den Typ zurück, der das Verhalten des Kanals repräsentieren soll. Je nachdem, welcher Typ hierbei herauskommt, ändert sich das Verhalten, das die Steuerung nach außen hin zeigt.

So lässt sich entweder ein Servomotor steuern, ein Licht zum Leuchten bringen oder sogar ein eigener Bash-Befehl absetzen. Alle abhängig von dem Typ, den wir ausgewählt haben. Da wir aber nicht einfach den Typnamen in die Konfiguration schreiben, sondern nur bestimmte Klassen zur Auswahl stellen wollen, müssen wir eine Zuordnung vornehmen. Dafür nutzen wir einen Service Locator. Dieser erzeugt aus unseren Magic Strings eine Zuordnung zu der korrekten Klasse.

Zuallererst müssen wir die Namen der Typen in der ChannelMap.json definieren. Dazu gibt es das ControlTypeAttribute, das nur einen TypeName besitzt.

 

[ControlType("Steering")]
 public class SteeringControl : ServoControlBase 
 { ... }

 

Das ist der Key für den Locator, der in unserem Fall eine Klasse identifiziert, um sie zu instanzieren. Das Ganze ist mit Reflection implementiert, sodass einfach neue Typen hinzukommen können, ohne anderen Code anzufassen.

Service Locator: Der Hub

Der Hub, der die Aufrufe vom Client bekommt, entscheidet, an wen die Nachrichten gesendet werden. Es geht also an dieser Stelle nicht darum, welche Typen gebraucht werden, sondern an welches Fahrzeug eine Information geht. 

Hier ändert sich lediglich, wie das Heraussuchen des Service abläuft. Zuvor war die Verbindung zwischen den Namen und den Types über das Attribut gegeben. Jetzt wird die Verbindung über die FahrzeugId hergestellt. Es ändert sich auch nicht mehr der Typ, sondern diesmal der Empfänger, der sich zunächst registriert hat. Das Prinzip an sich bleibt aber gleich.

Strategy: Führt am Ende alles aus

Kommt nun ein Update für einen Channel am Fahrzeug an, so liegt bereits alles bereit, um das eigentliche Ändern des Fahrzeugzustands vorzunehmen und damit alles in Bewegung zu bringen.

Die von der Factory erzeugten, über den Locator gefundenen und gegebenenfalls gemäß der Config dekorierten Instanzen finden sich alle in einem Dictionary wieder, das sowohl den Namen der Strategy als auch die Instanz zu ihrer Ausführung enthält. So kann der ChannelName nun als Strategy Selector genutzt werden, um die richtige Strategie herauszusuchen und diese eben mit ihren Daten auszuführen. Die Daten sind an der Stelle zwar recht unspektakulär, aber ihre Auswirkung am Ende eben nicht. Denn über die zuvor stattgefundene Anreicherung der Verhaltensweisen passiert schon deutlich mehr, als man von einem float als Datum erwarten würde.

Command Pattern

Ich habe die ganze Zeit über Commands geschrieben. Wo ist also das Command Pattern?

Das kommt in dem Sinne nicht vor, denn es gibt in dem Konstrukt keine wirklichen Commands, die sich ausführen lassen. In Ermangelung eines besseren Begriffs bin ich aber bei Commands geblieben, denn es ist am Ende eine Fernbedienung, die die Kommandos des Benutzers weiterleitet und etwas damit zur Ausführung bringt.

Warum erzähle ich euch das alles?

Durch das Zusammenspiel von verschiedenen Entwicklungsmustern, die hier nur an ein paar Beispielen aufgezeigt wurden, lassen sich interessante Verhaltensweisen aufbauen. Man findet diese Patterns in der Softwareentwicklung überall und ständig. Manchmal verwenden wir sie sogar intuitiv, auch ohne sie im Zweifel benennen zu können.

Auch wenn nicht immer alle Bestandteile eines Patterns enthalten sind, bleiben diese meist doch erkennbar. Werden Entwicklungsmuster mit Bedacht eingesetzt, können sie unseren Code besser strukturieren und ihn dynamischer, erweiterbarer und leichter verständlich machen.

 

 

[1] Georg Poweleit, SignalRC – in Echtzeit ans Steuer, dotnetpro 10-11/2025, Seite 46 ff.

Neueste Beiträge

Die ganze Welt der Softwareentwicklung
Ein riesiges Angebot an Wissen, das von Expert:innen lebendig vermittelt wird, gewürzt mit Kontakt zu Gleichdenkenden – das ist der Kern der DWX.
6 Minuten
19. Feb 2026
Nest.js: Warum sich der Blick über den Tellerrand für .NET-Entwickler lohnt - Nest.js für .NET-Entwickler, Teil 1
In modernen Softwareprojekten steht das C#-Backend längst neben einem Frontend, das in TypeScript lebt. Zwei Sprachen, zwei Ökosysteme, zwei Denkweisen. Was, wenn das Backend dieselbe Sprache sprechen könnte – ohne auf Enterprise-Patterns zu verzichten? Nest.js zeigt, dass genau das geht.
6 Minuten
18. Feb 2026
Die Suche nach Texten - Acht Kostbarkeiten in T-SQL, Teil 7
Suchen, bis die Server schwitzen? Wer versteht, wie der SQL Server denkt, wird bessere Ergebnisse und bessere Performance erzielen.
7 Minuten
16. Feb 2026

Das könnte Dich auch interessieren

Elektronische Schaltkreise im Browser simulieren - Simulation
Statt mit Steckfeld oder Lötkolben kann man auf dieser Website Schaltungen per Drag and Drop zusammenstellen und deren Verhalten testen.
2 Minuten
26. Jul 2018
SignalRC – in Echtzeit ans Steuer - Der DDC-Truck: Auf in die Welt mit SignalR, Raspberry Pi und sechs Rädern
Ein vernetztes Fahrzeug, gesteuert per Weboberfläche und LTE. SignalR sorgt dabei für millisekundenschnelle Kommunikation – direkt, stabil und skalierbar.
16 Minuten
21. Jan 2026
Version 30 von List & Label mit neuen Cloud-Funktionen - Reportgenerator
Die neue Version des Reporting-Tools List & Label unterstützt Entwickler:innen mit neuen Features, die speziell für Cloud- und Webumgebungen konzipiert sind.
2 Minuten
21. Okt 2024
Anzeige
Anzeige
Anzeige
Anzeige
Anzeige