Folgendes hexadezimale Byte-Protokoll ist für das Intelligent Interface offengelegt:
Soll ein Java-Programm auf äußere Einflüsse reagieren, muß man die Eingänge möglichst häufig lesen und in einem Objekt im Programm speichern, das das Modell repräsentiert.
Die wesentliche Klasse ist ft.Interface . Eine Instanz ist über das Communications API mit einer Schnittstelle verbunden und hält den Zustand des angeschlossenen Geräts in einer Reihe von Objekten innerer Klassen. Ein Thread sorgt in der Instanz dafür, daß der Zustand möglichst aktuell gehalten wird.
Mit dem Hauptprogramm kann man die Geschwindigkeit der Aktualisierung testen. Eine Vielzahl von Parametern des Pakets sind über eine Properties-Datei ft.properties kontrollierbar, die sich im aktuellen Katalog befinden muß, deshalb kann man die Leistung mit oder ohne Zugriff auf die analogen Eingänge leicht messen:
C> java ft.Interface COM1 100 8 true 1 100 in 20050 msec, 4 Hz
Mit dem Communications API von Sun kann ein Java-Programm nur 4 mal pro Sekunde den Zustand der Eingänge erfahren -- das reicht natürlich nicht.
Es ist relativ einfach, den Teil des Communications API für serielle Schnittstellen unter Windows zu implementieren, den man braucht, um darüber von Java aus das Intelligent Interface zu betreiben.
ft.comm.Driver implements javax.comm.CommDriver
initialize() wird von CommPortIdentifier aufgerufen und lädt aus der Properties-Datei ft.properties unter anderem eine Liste von Namen wie COM1 , die als Port-Namen hinterlegt werden und die unter Windows als Dateinamen zum Zugriff auf serielle Schnittstellen geeignet sein müssen.
getCommPort() wird später von CommPortIdentifier aufgerufen und muß zu einem Port-Namen dann ein Port-Objekt liefern.
ft.comm.SerialPort extends javax.comm.SerialPort
SerialPort() erhält den Port-Namen und konstruiert zwei Byte-Ströme zum Zugriff auf die Schnittstelle, die dann getInputStream() und getOutputStream() als Resultat liefern.
Alle anderen Methoden sind trivial implementiert und verbieten so weit als möglich alle anderen Operationen. Die Übertragungsparameter sind unveränderlich 9600 Baud, 8 Bit, keine Parität und 1 Stop-Bit.
Der wesentliche Geschwindigkeitsvorteil gegenüber der Referenzimplementierung von Sun entsteht offenbar dadurch, daß man für diesen speziellen Zweck auf alle Listener verzichten kann.
ft.comm.SerialInputStream extends java.io.InputStream
SerialInputStream() erhält den Port-Namen und notiert einen Windows-Handle zum Zugriff auf die Schnittstelle. Die verschiedenen read -Methoden liefern Bytes.
ft.comm.SerialOutputStream extends java.io.OutputStream
SerialOutputStream() erhält den Port-Namen und notiert einen Windows-Handle zum Zugriff auf die Schnittstelle. Die verschiedenen write -Methoden akzeptieren Bytes.
Die wesentlichen Methoden dieser Klassen werden in einer Bibliothek mit dem Java Native Interface in C implementiert und verwenden Windows Funktionen. Die C-Deklarationen für die Java-Methoden kann man sich von javah generieren lassen. Die Bibliothek wird von ft.comm.Driver beim ersten Zugriff dynamisch geladen.
ft.comm.ParallelPort extends javax.comm.ParallelPort
Es stellt sich heraus, daß man die Implementierung so konstruieren kann, daß das Universal Interface aus der Sicht von Java unter Windows das gleiche Byte-Protokoll verarbeitet. Unter Windows kann man also mit der gleichen Software wahlweise das Intelligent Interface oder das Universal Interface betreiben. Der einzige Unterschied liegt im Port-Namen für den Zugriff und in der Geschwindigkeit.
ParallelPort() erhält den Port-Namen und konstruiert zwei Byte-Ströme zum Zugriff auf die Schnittstelle, die dann getInputStream() und getOutputStream() als Resultat liefern. Der Port-Name muß die hexadezimale Basisadresse einer parallelen Schnittstelle sein, zum Beispiel 378 oder 3bc .
Alle anderen Methoden sind trivial implementiert und verbieten so weit als möglich alle anderen Operationen.
ft.comm.ParallelOutputStream extends java.io.OutputStream
ParallelOutputStream() wird zuerst aufgerufen, erhält den Port-Namen und notiert einen Index zum Zugriff auf die Schnittstelle. Die verschiedenen write -Methoden akzeptieren Bytes.
ft.comm.ParallelInputStream extends java.io.InputStream
ParallelInputStream() erhält den Port-Namen und notiert einen Index zum Zugriff auf die Schnittstelle. Die verschiedenen read -Methoden liefern Bytes.
Die wesentlichen Methoden dieser Klassen werden ebenfalls mit dem Java Native Interface in C implementiert und sind Teil von ft.comm.dll .
Eine write -Methode muß zwei Bytes schreiben und ruft ftOutput() mit der Port-Adresse und dem zweiten Byte auf. Diese Funktion schreibt Bytes so in das Daten-Register der Schnittstelle, daß folgendes Zeitdiagramm auf den Datenleitungen entsteht:
Der Zeittakt hängt dabei von einer Funktion ftIdle() ab und kann in der Properties-Datei ft.properties beeinflußt werden.
Liest eine read -Methode ein Byte, wird ftInput() mit der Port-Adresse aufgerufen. Diese Funktion schreibt Bytes so in das Daten-Register der Schnittstelle und beobachtet das busy -Bit des Status-Registers, daß folgendes Zeitdiagramm auf den Datenleitungen entsteht:
Das beobachtete Byte wird als Resultat geliefert. Man muß den Zeittakt so einstellen, daß die Eingänge E1 und E8 korrekt erkannt werden.
Liest eine read -Methode 3 Bytes, wird zuerst ftDigital() und dann ftAnalog() mit der Port-Adresse und einem Wert aufgerufen, der vom letzten Kommando bei ftOutput() abhängt und die Leitung triggerX oder triggerY wählt. Diesmal wird folgendes Zeitdiagramm bearbeitet:
Das Resultat entsteht, indem die Zeit beobachtet wird, bis busy wieder im Ruhezustand ist. Dabei wird ebenfalls f tIdle() aufgerufen, aber mit einem größeren Wert, der ebenfalls in ft.properties beeinflußt werden kann.
Mit ft.Interface kann man die Geschwindigkeit messen, mit der das Objekt und die Schnittstelle aktualisiert werden:
Die mögliche Geschwindigkeit ist beim Universal Interface so hoch, daß man in ft.properties einen Taktgeber vorsehen sollte -- idle zu vergrößern bedeutet nur, daß viel CPU-Zeit vergeudet wird.
Aufbauend auf dem Communications API baut man nach dem Model-View-Controller-Prinzip eine Klassenhierarchie auf, mit der man Modelle beobachten und steuern kann.
|
stellt Werte dar, die vom Programm oder von der Umgebung geliefert werden, kann Eingaben in Anfragen verwandeln. |
|
In diesem Fall speichert ein Model-Objekt den aktuellen Zustand eines Modells, der über ein Interface beobachtet wird. View- und Controller-Objekte beobachten das Model. Ein Controller wickelt eine Ablaufsteuerung ab, indem er das Model dazu veranlaßt, seinen Zustand zu ändern.
Dieses interface vereinbart eine Reihe von konstanten Masken, die als Argumente der verschiedenen Methoden angegeben werden sollten.
Dies ist das Model. Eine Instanz ist über das Communications API mit einer Schnittstelle verbunden und hält den Zustand des angeschlossenen Geräts in einer Reihe von inneren Model-Objekten. run() sorgt in der Instanz dafür, daß der Zustand insgesamt möglichst aktuell gehalten und der Observer-Thread benachrichtigt wird. Über ft.properties kann ein weiterer Thread erzeugt werden, der eine untere Zeitschranke für einen Aktualisierungszyklus setzt.
Mit main() kann man die Geschwindigkeit der Aktualisierung testen. Eine Vielzahl von Parametern des Pakets sind über ft.properties kontrollierbar, deshalb kann man auch zum Beispiel die Leistung bei Zugriff auf die analogen Eingänge leicht messen -- das kostet beim Intelligent Interface etwa 20%.
Die Klassenmethode open(portname) liefert eine Interface -Instanz für eine Schnittstelle wie COM1 .
Ein Interface kann Observer benachrichtigen, wenn sich der Zustand seines Geräts ändert oder ein Aktualisierungszyklus beendet ist. Verschiedene Observer können jederzeit mit addObserver(mask, observer) angeschlossen oder mit removeObserver(mask, observer) entfernt werden. Wenn Observer angemeldet sind, benachrichtigt sie nacheinander ein Dämon-Thread. Die Observer sollten möglichst schnell reagieren, damit dieser Thread einigermaßen aktuell bleibt; sie sollten aber die Aktualisierung der Interface -Instanz selbst nicht beeinflussen können.
Ein Observer muß ein spezifisches interface implementieren, das jeweils eine edge -Methode definiert, mit der er benachrichtigt wird.
ft.Interface. MotorObserver
edge(iface, motor, state) wird aufgerufen, wenn sich die Laufrichtung eines Motors ändert, und berichtet die neue Laufrichtung.
set(motors, state) legt in ft.Interface eine neue Laufrichtung für einen oder mehrere Motoren fest.
Vermutlich sollte eine asynchrone get -Methode definiert werden.
ft.Interface. DigitalObserver
edge(iface, sensor, state, count) wird aufgerufen, wenn sich der Zustand eines digitalen Eingangs ändert, und berichtet den neuen Zustand und den aktuellen Wert eines Zählers, der jedesmal geändert wird, wenn sich der Zustand ändert.
set(sensors, count, delta) setzt in ft.Interface für einen oder mehrere digitale Eingänge den aktuellen Zählerwert und den Wert, der bei einer Änderung addiert oder subtrahiert wird. setDelta(sensors, delta) setzt nur den Änderungswert.
Aktuellen Zustand und Zähler eines Eingangs liefern isSet(sensor) und isAt(sensor) in ft.Interface .
ft.Interface. AnalogObserver
edge(iface, sensor, value) wird aufgerufen, wenn sich der skalierte Wert eines analogen Eingangs ändert, und berichtet den aktuellen, skalierten Wert des Eingangs.
getScale(sensor) liefert in ft.Interface die Koeffizienten a und b , mit denen ein analoger Eingang linear skaliert wird. setScale(sensors, a, b) setzt in ft.Interface diese Koeffizienten für einen oder mehrere Eingänge.
Ein analoger Eingang wird nur abgefragt, wenn ein AnalogObserver dafür angemeldet ist. Das Universal Interface wird wesentlich langsamer, wenn man analoge Eingänge berücksichtigen muß.
In einem Zyklus wird mindestens der Motor-Zustand gesetzt und der Zustand der digitalen Eingaben abgefragt. setQuery(count) setzt in ft.Interface die Anzahl Zyklen, innerhalb derer der Zustand der analogen Eingaben einmal abgefragt wird. getQuery() liefert in ft.Interface diese Anzahl; typisch ist der Wert 10, das heißt, daß die analogen Eingaben nur mit etwa 10 Hz aktualisiert werden. Werden sie ständig aktualisiert, sinkt die Leistung insgesamt um etwa 20%.
ft.Interface. CycleObserver
edge(iface, cycles) wird aufgerufen, wenn ein Aktualisierungszyklus beendet ist, und berichtet den aktuellen Wert des Zyklenzählers.
set(cycles) setzt in ft.Interface den aktuellen Zählerwert.
Vermutlich sollte eine asynchrone get -Methode definiert werden.
Eine Reihe von JavaBeans auf der Basis des Abstract Window Toolkit können als Observer angemeldet werden. Sie sind Views oder auch Controller und berücksichtigen Konfigurationsparameter in der Properties-Datei ft.properties .
ft.TouchView stellt den Zustand einer digitalen Eingabe dar.
ft.EdgeView enthält eine TouchView und zeigt außerdem den aktuellen Zählerstand an.
ft.EdgeButton erweitert EdgeView ; man kann den Zählerwert eingeben und über einen Knopf kontrollieren, ob jeweils aufwärts oder abwärts gezählt werden soll.
ft.MotorView stellt die Laufrichtung eines Motors dar.
ft.MotorButton erweitert MotorView ; man kann per Maus-Click einen Motor links- oder rechts-laufen lassen.
ft.StepMotor erweitert MotorButton und kombiniert einen Motor mit zwei digitalen Eingängen; eine TouchView zeigt den Nullpunkt und eine EdgeView zeigt die aktuelle Position des Motors. Man kann per Maus-Click einen Motor links- oder rechts-laufen lassen, allerdings nur zwischen dem Nullpunkt und einem Grenzwert.
ft.Stepper erweitert StepMotor ; mit home() und position(count) kann man positionieren, isRunning() zeigt, ob der Ausgang noch beschäftigt ist. Ein Stepper schickt sich notifyAll() , wenn er nicht mehr beschäftigt ist.
ft.AnalogView stellt den Zustand einer analogen Eingabe dar.
ft.ScaledView erweitert AnalogView und enthält einen Knopf mit einem modalen Dialog, mit dem man die Skalierung und die Zyklusrate sehen und ändern kann.
ft.Diagnose extends java.awt.Panel
Diese Klasse implementiert ein Panel , das mit EdgeButton -, MotorButton - und ScaledView -Objekten ein Interface beobachtet und kontrolliert.:
C> java ft.Diagnose COM1 $ java ft.Diagnose /dev/ttyS0
In ft.properties kann man die Anzeige der analogen Eingänge ausblenden.
Mit main() kann man Modelle manuell betreiben und kritische Werte kalibrieren.
Diagnose könnte in einer Web-Seite als Applet betrieben werden, wenn man dem Applet Zugriff auf das Communications API ermöglicht. Insgesamt könnte man eine Umgebung realisieren, bei der Steuerungen in Web-Seiten als Hilfesystem eingebettet sind.
ft.industry.Record extends java.awt.Panel
Diese Klasse implementiert ein Panel , das mit StepMotor -Objekten ein Interface beobachtet und kontrolliert. Drückt man auf record , wird die aktuelle Position in die Standard-Ausgabe geschrieben -- Record ist eine rudimentäre, nicht konfigurierbare Teach-In Applikation.
C> java ft.industry.Record COM1 1 1 1 1 17 9 9 7 9 9 9 7
Mit main() kann man ein Modell manuell betreiben und ein Skript erstellen.
ft.industry.Play extends java.awt.Panel
Diese Klasse implementiert ein Panel , das mit Stepper -Objekten ein Interface beobachtet und kontrolliert. record gibt es hier nicht, denn die Stepper sollen von der Standard-Eingabe her positioniert werden -- Play ist ein rudimentärer, nicht konfigurierbarer Abspieler für Record -Skripte.
C> java ft.industry.Record COM1 >skript C> java ft.industry.Play COM1 <skript
Ein objekt-orientiertes Steuerprogramm für ein Fahrzeug wie Trusty sollte Java mit Objekten und Methoden kombinieren, die sich auf Ein- und Ausgänge beziehen. Das kann ungefähr so aussehen:
// Trusty.java
package ft.mobile;
import ft.Controller;
import ft.Interface;
/** simple control program for a moving robot that can back off.
*/
public class Trusty extends Controller {
public Trusty (Interface iface) { super(iface); }
/** m1, m2 run forward.
if e3 (or e4) is open, back off, turn away, and go forward again.
*/
public void run () {
m1.left(); m2.left();
for (int count = 0; count < 10; )
if (!e3.isOn()) { turn(m1, m2, e1); ++ count; }
else if (!e4.isOn()) { turn(m2, m1, e2); ++ count; }
m1.off(); m2.off();
}
/** use impulse counter to back off, turn away from motor a.
*/
protected void turn (Output a, Output b, Input t) {
a.right(); b.right(); t.count(8);
a.left(); t.count(2);
b.left();
}
}
$ java ft.Controller /dev/ttyS0 ft.mobile.Trusty
Damit das funktioniert, stellt ft.Controller entsprechende Objekte und Methoden bereit, lädt im Hauptprogramm ein Interface -Objekt mit einem eigenen Thread sowie eine Instanz des Steuerprogramms und führt run() erst dann im main -Thread aus, wenn dies mit dem start -Knopf freigegeben wird.
Auch der Sortierer (Pneumatik) kann in Java programmiert werden:
/** process about 5 wheels.
*/
public void run () {
if (diodes) left(M2); // start compressor and lamp
if (isOff(E2)) { // retract piston: wait 1 edge
left(M4); e2.count(1); off(M4);
}
sleep(recover); // additional pressure recovery period
moveSlider(OFF); // center slider
for (int wheel = 0; wheel < 7; ++ wheel) {
boolean white = isOn(E1);
cyclePiston();
moveSlider(white ? LEFT : RIGHT); moveSlider(OFF);
}
off(Mall);
}
/** run piston back and forth once -- E2 must go off and back on.
*/
protected void cyclePiston () { right(M4); e2.count(2); off(M4); }
/** move slider.
*/
protected void moveSlider (int how) {
sleep(recover);
switch (how) {
case LEFT: left(M1); break;
case RIGHT: if (diodes) right(M1); else right(M2); break;
case OFF: left(M3);
}
sleep(press);
off(M1|M3); if (!diodes) off(M2);
}
/** sleep in tenths of seconds.
*/
protected void sleep (int tsec) {
try { Thread.sleep(tsec*100); } catch (InterruptedException e) { }
}
Hier sieht man einen weniger objekt-orientierten Programmierstil, bei dem die Aus- und Eingänge zumeist mit Masken durch Methoden in ft.Controller angesprochen werden.
Sorter liest Optionen in ft.properties , die die Reaktionszeiten der Zylinder festlegen, und berücksichtigt, ob die Ventile über Dioden angeschlossen sind.
/** true if wired with diodes */ protected boolean diodes;
/** pressure time for slider */ protected int press;
/** recovery time for compressor */ protected int recover;
static { Interface.class.getName(); } // load properties
{ String prefix = getClass().getName();
press = Integer.getInteger(prefix+".press", 10).intValue();
recover = Integer.getInteger(prefix+".recover", 20).intValue();
diodes = Boolean.getBoolean(prefix+".diodes");
}
}
Man kann Geräte nach dem Model-View-Controller-Muster objekt-orientiert programmieren. Interface agiert als Model, die grafischen Bausteine sind Views. Ein Steuerprogramm für ein spezielles Gerät muß man als Controller ansehen, der sich als Observer am Interface anmeldet und dann das Interface beeinflußt. Passive Views können gleichzeitig als Observer angemeldet sein und den Zustand eines Geräts schildern.
Durch die Koppelung über Thread-Synchronisation und das Observer-Modell erhält man eine geringe Systembelastung und eine hohe Wiederverwendbarkeit der Klassen.
Für ein Beispiel wie den Schwenkroboter oder ein Fahrzeug mit Sensoren sollte man untersuchen, ob man den Controller in weitere JavaBeans zerlegen kann, die man vielleicht nur mit einer einfachen Skripting-Sprache ( Properties , endlicher Automat als Tabelle etc.) konfigurieren müßte.
Interface ist im Code auf ein Intelligent oder Universal Interface ohne zusätzlichen Adapter zugeschnitten. Es ließe sich leicht auf einen Adapter mit weiteren Eingaben und Ausgaben erweitern -- wenn offengelegt wäre, wie die überhaupt angesteuert werden.
Verschiedene Erweiterungen sind denkbar. Man kann Interface verteilen, das heißt, daß man den Interface -Thread in einem mobilen Prozessor abwickelt und die Observer über ein Protokoll (Funk, IR, 10baseT etc.) verbindet. Man sollte dann aber, ähnlich wie beim Skalieren der analogen Eingaben, eine programmierbare Filterung der Observer-Events vorsehen, um das Protokoll zu entlasten. Da jedoch die Systemlast durch Interface für das Intelligent Interface sehr klein ist, ist unklar, wann sich das überhaupt lohnt.
Vermutlich kann man Interface über das Communications API auch mit dem RCX verbinden -- wenn man die Konfiguration von Interface etwas variabler gestaltet. Gelingt das, müßten Views und Controller austauschbar auch für LEGO zu verwenden sein...