Treiberentwicklung

Dieses Dokument gibt eine grobe Übersicht über die effiziente Entwicklung von Treibern für Standards

Einführung

In kalibriware können Standards mit einem ‘Treiber’ versehen werden. Das Ziel dazu ist ähnlich zum bestehenden Makrosystem in KalibriScript gemeinsam genutzte Funktionalitäten zu abstrahieren und Hardware einfacher austauschbar zu machen.

Dazu wird pro Standard eine Klasse mit gerätespezifischem Code geschrieben, welche oft genutzte Funktionalitäten in einer geteilten, abstrakten Schnittstelle implementiert.

Im Folgenden wird die Funktion am Beispiel eines Multimeters demonstriert.

Am mächtigsten ist diese Funktion, wenn die Gerätespezifika vollständig abstrahiert sind, also zum Benutzer hin das eigentliche Gerät nicht mehr sichtbar ist. Das kann z.B. durch Kapselung aller Funktionen innerhalb eines interface{} erfolgen, was wir stark empfehlen.

Im Multimeterbeispiel wäre es dann ein gemeinsam (= von allen unterstützten Multimetern) genutztes Interface, welches die Funktionen (z.B. Spannungsmessung, Strommessung, Frequenzmessung) unterstützt. Alle Multimeter-Treiber implementieren dann diese Klasse, sodass die Schnittstelle erfüllt wird.

Schnittstellendefinition

Zuerst wollen wir am Multimeter-Beispiel eine Schnittstelle definieren:

export interface IMultimeter {
  Spannungsmessung(kind: 'ac' | 'dc', messbereich: number): Promise<number>;
  Strommessung(kind: 'ac' | 'dc', messbereich: number): Promise<number>;
  Frequenzmessung(messbereich: number): Promise<number>;
}

Wichtig dabei zu beachten, dass die meisten Funktionen in Kalibriware asynchron sind, d.h. die Rückgabewerte sind in einem Promise verpackt, welches asynchrone Operationen erlaubt.

Weiterhin ist es guter Stil, die Funktionen direkt in der Schnittstelle zu dokumentieren. Dazu bringt kalibriware eine Dokumentationssyntax (JSDoc) mit, welche direkt durch die Entwicklungsumgebung ausgewertet wird.

Im Beispiel sieht das ganze so aus:

export interface IMultimeter {
  /**
   * Führt eine Spannungsmessung in einem wählbaren Messbereich durch
   * @param kind Wechsel- oder Gleichspannungsmessung
   * @param messbereich Maximaler Erwartungswert in Volt (Messbereich)
   * @returns gelesene Spannung in Volt
   */
  Spannungsmessung(kind: 'ac' | 'dc', messbereich: number): Promise<number>;

  /**
   * Führt eine Strommessing in einem wählbaren Messbereich durch
   * @param kind Wechselstrom- oder Gleichstrommessung
   * @param messbereich Maximaler Erwartungswert in Ampere (Messbereich)
   * @returns gelesener Strom in Ampere
   */
  Strommessung(kind: 'ac' | 'dc', messbereich: number): Promise<number>;
  
  /**
   * Führt eine Frequenzmessung in einem Wählbaren messbereich durch
   * @param messbereich Maximaler Erwartungswert in Hertz (Messbereich)
   * @returns gelesene Frequenz in Hertz
   */
  Frequenzmessung(messbereich: number): Promise<number>;
}

Durch die Auswertung dieser Dokumentation kann z.B. beim Entwickeln eines Skripts folgendes angezeigt werden:

devenv-docs

Treiber für ein Gerät

Grundlegend sieht ein Treiber sehr ähnlich zu einem Skript aus:

@Injectable()
export class Keithley2000 implements IMultimeter {
  constructor() {}
  
  async Spannungsmessung(kind: 'ac' | 'dc', messbereich: number): Promise<number> {
    throw new Error('Method not implemented.');
  }
  async Strommessung(kind: 'ac' | 'dc', messbereich: number): Promise<number> {
    throw new Error('Method not implemented.');
  }
  async Frequenzmessung(messbereich: number): Promise<number> {
    throw new Error('Method not implemented.');
  }

}

Wichtig an der Stelle ist, die Klasse über @Injectable() als erzeugbar zu markieren und über implements IMultimeter die vorher definierte Schnittstelle zu erfüllen. Wenn die Schnittstelle nicht vollständig implementiert ist, so wird kalibriware einen Fehler anzeigen.

Falls dem Standard eine Hardwareschnittstelle zugeordnet ist, so kann diese wie folgt erreicht werden (hier am Beispiel GPIB):

@Injectable()
export class Keithley2000 implements IMultimeter {
  // GPIB fordert die zugeordnete GPIB-Schnittstelle an
  // cfg (GPIBDeviceConfig) enthält die in der Administrationsoberfläche konfigurierte Geräteaddresse
  constructor(private gpib: GPIB, @Inject(CalibratorConfig) private cfg: GPIBDeviceConfig) {}
  
  // Beispielhafte Implementierung der Spannungsmessung:
  // Schreibe "READ:VOLT:AC?" ans Gerät, dann lies den Buffer vom Gerät und konvertiere ihn in eine Zahl.
  async Spannungsmessung(kind: 'ac' | 'dc', messbereich: number): Promise<number> {
    await this.gpib.Write(this.cfg.address, "READ:VOLT:AC?");
    const inputString = await this.gpib.Read(this.cfg.address);
    return Number.parseFloat(inputString);
  }


  // ... restliche Methoden
}

Beschränkungen

Wir empfehlen, Standards immer über ihre Aliase (CalibratorByAlias.createOrGet("ALIAS")) anzufordern - darüber kann gewährleistet werden, dass auch bei mehreren gleichen Standards (z.B. zwei Multimeter für Mehrkanalmessungen) die Zuordnung korrekt gewährleistet wird.