Mittwoch, 6. Februar 2013

Build-Ampel selbst bauen

Motivation

Selbst für kleine Projekte, an denen nur eine Hand voll Mitarbeiter beteiligt sind, wird man schnell die Vorteile eines Build-Servers schätzen lernen. Eine Funktion eines Build-Servers ist es anzuzeigen, ob das, was man gerade in die Quellcodeverwaltung "commited" hat auch auf einem Fremdsystem gebuildet werden kann. Zur Anzeige des Build-Status auf dem Entwicklungsrechner werden oft Programme verwendet, die den Status als Ampel-Icon im Tray anzeigen.

Als Beispiel möchte ich den Build-Server ThoughtWorks Cruise Control .Net erwähnen, hierfür existiert das Tool CCTray, welches ein Tray-Icon zur Verfügung stellt, um den Status anzuzeigen. Da ich mitbekommen habe, dass dieses kleine Icon von Entwicklern oft ignoriert wird, habe ich nach einer anderen Lösung gesucht und auch gefunden. 

Was wäre wenn man diese Software-Ampel aus dem Tray auf eine reale Ampel überträgt !? Naja, vielleicht ist eine echte Verkehrsampel etwas übers Ziel hinaus geschossen, aber da hat mein einjähriger Sohn auf die Richtige Idee gebracht: Für sein Bobby Car hat er eine Spiel-Ampel von BIG, die fürs Erste ganz gut geeignet für meine Anforderung ist.

Fehlt nur noch die Steuerung der Ampel, man könnte hier direkt über ein USB Relais-Interface die einzelnen Lampen an der Ampel ansteuern und schalten. An sich ein probates Mittel, aber so recht keine Herausforderung - ich denke da eher an eine Funk-Fernsteuerung oder übers Netzwerk. Da ich nach langem Suchen keine praktikable Lösung gefunden habe, wollte ich schon fast aufgeben, dann ist mir aber ein Angebot von Pollin Electronic ins Auge gesprungen, der Bausatz AVR-NET-IO. Damit kann man über TCP/IP bis zu 8 Ausgänge schalten, das sollte für 2 Ampeln und 2 Fußgängerampeln genügen.

Einkaufsliste

Bobby Car Ampel von Big (13,99 Euro)
Bausatz: AVR-NET-IO (19,95 Euro)
Lötkolben, Lötzinn, Werkzeug, .. (sollte man zu Hause haben)
Kabel / min. 12-adrig, ca.1 m (zB: billiges Scart-Kabel: ca. 1 Euro)
Netzteil 9V~ min. 200 mA  (ab 2 Euro)

Gesamtpreis: 36,94 Euro (zzgl. Versand)

Umsetzung

Quelle: Wikipedia, http://en.wikipedia.org/wiki/Parallel_port
Zuerst sollte man den AVR-NET-IO Bausatz nach der Anleitung zusammen löten, für uns interessant ist hier prinzipiell der DB-25F Port (J3), dieser entspricht in den Pins 2-13 und den GND-Pins 18-25 einem herkömmlichen LPT Port, was es theoretisch ermöglich hier eine entsprechende parallele Interface-Karte anzuschließen und anzusteuern. Speziell wenn man vor hat größere Ströme zu schalten, macht dies Sinn. Die Leuchtmittel in unserer Spielzeugampel kommen jedoch mit dem Steuerstrom von 5 Volt ganz gut aus, so können wir uns die 15 Euro für die Relaiskarte sparen. Wer es modular aufbauen will, kann eine entsprechende Anschlussplatine kaufen (ca. 4 Euro), um die einzelnen Ausgänge an  die Ampel anzuschließen.

Nachdem man nun die Ampel durch die Schraube an der Unterseite auseinander genommen hat und die Drähte von der Ampel-Steuerplatine trennt, wird man feststellen, dass man mehr Kabel hat, als Adern im Kabel. Jede Lampe an der Ampel hat 2 Adern, die abgehen - dabei handelt es sich zum einen um das GND-Kabel (also die Masse) und zum Anderen um die Phase. Da es sich in gegebenem Fall um Gleichstrom handelt, ist das eine der Pluspol, der andere der Minuspol. Aktuell ist irrelevant, was was ist, denn in unserer Ampel befinden sich Glühbirnen. Die GND-Kabel von den zwei Rot-Gelb-Grün Ampeln können zusammengefasst werden und durch eine Ader zum AVR-Modul geführt werden. Außerdem kann man die Grünen GND-Kabel auch da mit zusammenfassen. Zusätzlich brauchen wir ein "Kabelverbund", der immer Spannung erhält, dazu gehört jeweils ein Kabel der roten Lampe aus den Fußgängerampeln.

Jedes übrige Kabel (also die Restlichen 10) muss in einer separaten Ader zum AVR-IO-Modul geführt werden. In meinem Aufbau habe ich das AVR-IO-Modul unten auf den Sockel der Ampel aufgeschraubt und das Kabel durch die hohle Stange der Ampel geführt.Prinzipiell würde das Modul auch direkt in die Ampel passen (dann würde man sich das 12-Adrige Kabel sparen), aber da das Modul recht heiß wird, finde ich diese Lösung nicht sehr praktikabel.

Nachdem wir nun alle Kabel am Modul haben, können wir uns der Pin-Belegung widmen. Das GND-Kabel wird an eine der GND-Leitungen angeschlossen, wie es am Besten passt. Der andere "Kabelverbund" wird an die High-Leitung (5V), also Pin 15 angeschlossen.

Ampel 1
Grün: Pin 2 (D0)
Gelb: Pin 3 (D1)
Rot: Pin 4 (D2)

Ampel 2
Grün: Pin 5 (D3)
Gelb: Pin 6 (D4)
Rot: Pin 7 (D5)

Nun haben wir noch 4 Adern übrig, aber nur 2 Ausgänge. Wenn man etwas darüber nachdenkt, kommt man auf die Lösung - Die Kabel für rot und grün werden gemeinsam angeschlossen. Da die zweite Ader für Rot auf 5V und die zweite Ader für Grün auf GND liegt, ist die Ausgabe der Fußgängerampel abhängig von der jeweiligen Datenleitung, liegt diese auf GND, so leuchtet sie rot, liegt sie auf HIGH (5V), so leuchtet sie grün.

Ampel 3
Grün: Pin 8 (D6)
Rot: Pin 8 (D6)

Ampel 4
Grün: Pin 9 (D7)
Rot: Pin 9 (D7)

Steuerung & Software

Jetzt könnten wir eigentlich schon mal die Ampel testen, der Anschluss an einen PC erfolgt über ein Cross-Over Netzwerkkabel, am PC muss man dafür einfach die Adaptereinstellungen der Netzwerkkarte so ändern, dass man dem PC eine IP aus dem Subnetz 192.168.0.0/24 zuweist, zum Beispiel:
IP: 192.168.0.100
Subnetzmaske: 255.255.255.0

Die IP-Adresse des AVR-NET-IO ist 192.168.0.90, der TCP-Port für eine Verbindung ist  50290 . Verbinden kann man sich nun mit Hilfe von Telnet oder PuTTY (siehe Beschreibung des Bausatz) und die ersten Befehle testen. Für uns relevant ist lediglich der Befehl SETPORT. Mit Hilfe von SETPORT 1.1 kann man nun zum Beispiel die grüne Lampe an Ampel 1 aufleuchten lassen. Aber Vorsicht beim Testen, wenn ihr keine Relaisplatine verwendet können maximal etwa 6 Lampen gleichzeitig leuchten, sonst bekommt der Mikrocontroller nicht mehr genug Strom, um die Netzwerkkommunikation aufrecht zu erhalten.

Kommen wir nun zur Software in C#, um die Ampel anzusteuern. Die erste Abstraktionsschicht bildet die Klasse AVRNetIO:

    class AVRNetIO
    {
        /// <summary>
        /// Pin Status
        /// </pre>
        public enum AVRNetIOStatus
        {
            /// <summary>
            /// 5V
            /// </summary>
            High=1,
            /// <summary>
            /// GND
            /// </summary>
            Low=0
        }

        static System.Net.Sockets.TcpClient _client = new System.Net.Sockets.TcpClient(Properties.Settings.Default.NetIoHost, Properties.Settings.Default.NetIoPort);

        /// <summary>
        /// sets the Portstatus of a connected AVR-NET-IO modul
        /// </summary>
        /// <param name="pinId" />the pin id (1-8)
        /// <param name="status" />the status (high|low)
        public static void SetOutput(int pinId, AVRNetIOStatus status)
        { 
            if (pinId<1 data-blogger-escaped-pinid="">8)
                throw new NotImplementedException("The AVR-NET-IO only implements 8 output pins (1-8)")
            System.Text.ASCIIEncoding enc = new System.Text.ASCIIEncoding();
            string command = "SETPORT "+pinId+"."+(int)status+"\r\n";
            _client.Client.Send(enc.GetBytes(command));
            System.Threading.Thread.Sleep(100);
        }
    }
Nun ist es möglich das Modul übers Netzwerk direkt anzusteuern und die Ausgänge zu schalten. IP und Port sind hier per AppConfig einstellbar.

Kommen wir nun zur nächsten Abstraktionsebene, die Ampel an sich. Dazu gibt es erst einmal einen enum für den Ampelstatus und einen für die Art der Ampel (Fußgänger, Verkehr):

        public enum SignalLightTypes
        {
            TrafficLight,
            PedestrianLight
        }

        public enum SignalLightSignals
        {
            Red=2,
            Yellow=4,
            Green=8
        }

Die eigentliche Klasse für SignalLight wird folgendermaßen implementiert:

    public class SignalLight
    {
        SignalLightTypes _type;
        int _startPin;

        public SignalLight(SignalLightTypes type, int startPinId)
        {
            _startPin = startPinId;
            _type = type;
        }

        void SetStatus(SignalLightSignals signalStatus)
        {
            // we only use 1 pin for the pedestrian light
            if (_type == SignalLightTypes.PedestrianLight)
            {
                 if ((signalStatus & SignalLightSignals.Red) == SignalLightSignals.Red)
                     AVRNetIO.SetOutput(_startPin, AVRNetIO.AVRNetIOStatus.Low);
                 if ((signalStatus & SignalLightSignals.Green) == SignalLightSignals.Green)
                     AVRNetIO.SetOutput(_startPin, AVRNetIO.AVRNetIOStatus.High);
            }
            else
            {
                // startPin : green
                // startPin+1 : yellow
                // startPin+2 : red
                AVRNetIO.AVRNetIOStatus redStatus = AVRNetIO.AVRNetIOStatus.Low;
                AVRNetIO.AVRNetIOStatus yellowStatus = AVRNetIO.AVRNetIOStatus.Low;
                AVRNetIO.AVRNetIOStatus greenStatus = AVRNetIO.AVRNetIOStatus.Low;
                if ((signalStatus & SignalLightSignals.Red) == SignalLightSignals.Red)
                    redStatus = AVRNetIO.AVRNetIOStatus.High;
                if ((signalStatus & SignalLightSignals.Yellow) == SignalLightSignals.Yellow)
                    yellowStatus = AVRNetIO.AVRNetIOStatus.High;
                if ((signalStatus & SignalLightSignals.Green) == SignalLightSignals.Green)
                    greenStatus = AVRNetIO.AVRNetIOStatus.High;
                int pinId = _startPin;
                AVRNetIO.SetOutput(pinId, greenStatus);
                AVRNetIO.SetOutput(++pinId, yellowStatus);
                AVRNetIO.SetOutput(++pinId, redStatus);
            }
        }

        private SignalLightSignals _currentStatus;

        /// <summary>
        ///  gets/sets the current status of the signal light
        /// </summary>
        public SignalLightSignals CurrentStatus
        {
            get { return _currentStatus; }
            set { 
                _currentStatus = value;
                SetStatus(value);
            }
        }

    }
Kein Hexenwerk, je nachdem welcher enum als CurrentStatus gesetzt wird, wird der / die entsprechenden Pins auf High gesetzt. Bei Fußgängerampeln ist es ein kleiner Sonderfall, hier wird nur ein Pin verwendet. Bei rot wird dieser auf Low und bei grün auf High geschalten. Initial sind die Fußgängerampeln immer auf rot, denn die Pins vom AVR-Net-IO sind initial auf Low geschalten.

Wir könnten jetzt anhand dessen eine "sinnvolle" Ampelsteuerung entwickeln, das wollen wir aber nicht, denn der Initiale Gedanke war es, eine Build-Ampel zu entwickeln. Ich habe anfangs von CCTray bzw. CC.Net gesprochen, daher will ich jetzt eine Beispielimplementierung für CC.Net zeigen. Dazu brauchen wir die Datei ThoughtWorks.CruiseControl.Remote.dll aus dem Setup von CCTray, da ich nicht weiß in wie weit es Distributionsbeschränkungen gibt, gebe ich hier nur den Namen der Datei an, der Download muss dann direkt von der Seite von CC.Net geschehen (im Bundle mit der Software CCTray)

Um nun den Status aus CruiseControl.Net auszulesen, müssen wir eine neue Instanz von CruiseServerHttpClient erstellen, damit können wir nun den Projektstatus von jedem Projekt auslesen (GetProjectStatus()) und in den Ampelstatus umwandeln.


// initiate signal light classes
SignalLight _signal1 = new SignalLight(SignalLight.SignalLightTypes.TrafficLight, 1);
SignalLight _signal2 = new SignalLight(SignalLight.SignalLightTypes.TrafficLight, 4);
SignalLight _signal3 = new SignalLight(SignalLight.SignalLightTypes.PedestrianLight, 7);
SignalLight _signal4 = new SignalLight(SignalLight.SignalLightTypes.PedestrianLight, 8);

// get variables from config
var ipAddressOrHostNameOfCCServer = Settings.Default.buildserver; 
string project1 = Settings.Default.project1;
string project2 = Settings.Default.project2;
string project3 = Settings.Default.project4;
string project4 = Settings.Default.project4;
var client = new CruiseServerHttpClient(
    string.Format("http://{0}/ccnet/", ipAddressOrHostNameOfCCServer));

foreach (var projectStatus in client.GetProjectStatus())
{ // iterate through projects
    SignalLight.SignalLightSignals status;

    // convert project's status to SignalLightsSignals
    // building -> yellow, success -> green, failure -> red, everything else -> yellow
    if (projectStatus.Activity == ProjectActivity.Building)
        status = SignalLight.SignalLightSignals.Yellow;
    else if (projectStatus.BuildStatus == IntegrationStatus.Success)
        status = SignalLight.SignalLightSignals.Green;
    else if (projectStatus.BuildStatus == IntegrationStatus.Failure || projectStatus.BuildStatus == IntegrationStatus.Exception)
        status = SignalLight.SignalLightSignals.Red;
    else
        status = SignalLight.SignalLightSignals.Yellow;

    // set status on appropriate project
    if (projectStatus.Name == project1)
    {
        _signal1.CurrentStatus = status;
    }
    if (projectStatus.Name == project2)
    {
        _signal2.CurrentStatus = status;
    }
    if (projectStatus.Name == project3)
    {
        _signal3.CurrentStatus = status;
    }
    if (projectStatus.Name == project4)
    {
        _signal4.CurrentStatus = status;
    }
}

Bitte seht mir nach, dass ich momentan keine Bilder habe, sobald ich Zeit finde, werde ich ein paar Bilder machen und diese posten.