Univerzális vezérlő interfész PC-hez     ASTLAB © 2007

Vezérlési példák


1. Vasúti sorompóval ellátott fénysorompó vezérlése

1.1 A fénysorompó felépítése és elvi működése

1.2 A hardver bekötése

1.3 A vezérlés implementálása Delphiben

1.3.1 Az állapotgép

1.3.2 A főprogram

1.4 Az eredmény



2. Közlekedési lámpa távvezérlése

2.1 A közlekedési lámpa felépítése és elvi működése

2.2 A hardver bekötése

2.3 A vezérlés implementálása Pythonban

2.3.1 Az _Options osztály

2.3.2 A _Debug osztály

2.3.3 A _TrafficLight osztály

2.3.4 A _WorkingThread osztály

2.3.5 A _TCRThread osztály

2.3.6 A _CI4ACommThread osztály

2.3.7 A főprogram

2.4 Az eredmény



3. További példák


1. Vasúti sorompóval ellátott fénysorompó vezérlése





1.1 A sorompó felépítése és elvi működése

Be kell valljam, hogy halovány lila dunsztom sincs arról, hogy a valóságban miként működik egy nagyvasúti sorompó, ezért most kitalálunk itt rá egy megoldást. Egy olyan megoldást ami akár terepasztalon használható is lehet. A fenti ábra mutatja a vasúti átjáró sematikus rajzát. Ezen látszik, hogy négy darab áthaladás érzékelő (A, B, C, D) van a pálya mentén elhelyezve. Ezek nagyvasúti esetben olyan nyomás érzékelők lehetnek, amelyek 2 tonna fölött indulnak csak be, így elkerűlhető, hogy fals jelet adjon, amikor a 9 éves Pistike szórakozásból ugrál rajta. Az érzékelőket természetesen csak a rajz szélességi méreteinek korlátja miatt helyeztük ilyen közel a vasúti átjáróhoz, a valóságban ezeknek olyan messze kell lenniük, hogy a 100 Km/h-val közlekedő vonat is legalább 10 perccel az érkezése elött lezárja a sorompót, különben sehogysem üti meg a guta a feleslegesen várakozó autósokat. Az A-B C-D párok távolsága pedig a legkisebb forgózsámoly tengelytávjánál kisebb kell legyen. A működési elve a következő: Ha a vonat a piros nyílak irányában közlekedik, akkor az első keréknek sorrendben az A-B-C-D érzékelőkön, ha a vonat a kék nyílak irányában közlekedik, akkor pedig a D-C-B-A érzékelőkön kell áthaladnia. Ebből a sorrendiség vizsgálatával egyértelműen megmondható, hogy az A-B és a C-D páros mikor zárja és mikor nyitja a sorompót.
A példánkban A, B, C, D érzékelőket mikrokapcsolók, a fénysorompó lámpáit LED-ek fogják helyettesíteni, a sorompó rudat meg egy RC szervóra kell felszerelni, így a valóságban lehet nyitogatni és csukogatni.

Az ilyen és ehhez hasonló ún. szekvenciális vezérlések a legegyszerűbben állapotgépekkel oldhatók meg. Az állapotgépnek bemenetei és kimenetei vannak. A bemenetek változása állapotváltozást idéz elő, melynek folyománya egy új allapotba kerülés. Az új állapotba kerüléssel egyidejűleg a kimenetek is megváltoz(hat)nak. Ezzel az egész működési folyamat lépésről lépésre hajtódik végre. A bemenetek nem csak fizikai érzékelők jelei lehetnek, hanem vizsgálhatjuk bármilyen szoftveres változó (pl. egy időzitő timeoutja) értékét is. A kimenetek szintén lehetnek fizikai kimenetek és szoftveres kimenetek (pl. egy időzítő indítása, megállítása, resetje) is. A kimeneteket bemeneti változóként is használhatjuk hiszen csak a logikai értékük számít.
A folyamat leírására az állapotgráf szolgál. Az állapotgráf krumplikból és nyilakból áll össze, minden krumpli egy állapotot, minden nyíl egy állapotváltozást jelöl. A nyilakra az állapotváltozást okozó bemeneti értékeket illetve az ezzel járó kimeneti érték változásokat szokás felrajzolni. A krumpliba beleírjuk az állapot nevét és egy egyedi állapotazonosítót is. A mi vasutas példánkban három állapotgép is együtt fog működni, az egyik az áthaladási folyamatot vezérli, a másik a fénysorompó villogtatásával fog foglalkozni, a harmadik pedig a sorompórudat mozgató szervót vezérli.

Az áthaladási folyamat bemenetei A, B, C, D érzékelők által szolgáltatott jelek, kimenete pedig egy virtuális van, ez pedig az, hogy szabad-e áthaladni. Ez a virtuális kimenet a két másik állapotgép bemenete lesz majd. A virtuális kimenet jele legyen W, értéke 1 ha "Tilos az Á" (ahogy azt a Micimackóban a Malacka háza közelében levő tábla is mutatta), és értéke 0, ha át szabad menni. Az érzékelők ha jelet adnak értékük 1, egyébként 0. Az állapotátmeneti nyilakon a be és kimenetek sorrendje ABCD/W. Az állapotgráf a következő:



Négy bemenet esetén minden állapotból 16 kiinduló nyílat kéne rajzolni. Itt felmerül az a kérdés, hogy ezek miért nincsenek mind feltüntetve? A magyarázat a helyspórolás, miszerint csak az állapotváltozást okozó kombinációkat rajzoljuk le, mert az összes többi úgy is egy a kiinduló állapotába visszamutató nyíl lenne.

A fénysorompó vezérlő folyamat bemenetei az előző állapotgép kimenete W, a villogást meghatározó időzítő timeoutja T, a ledek pillanatnyi állapotai L, R és G, kimenetei pedig a ledek vezérlő jelei: L a bal oldali piros ledet, R a jobboldali piros ledet, G az alsó zöld ledet jelöli. Mivel a villogtatást vezérlő időzítőt újra is kell néha indítani ezért szükséges az S kimenet is az időzítő vezérléséhez. Az állapotátmeneti nyilakon a be és kimenetek sorrendje WTLRG/LRGS. A bemeneti oldalon levő L R G tulajdonképpen egy visszacsatolás az L R G kimenetek pillanatnyi állapotáról. Az állapotgráf a következő:



A sorompó vezérlő folyamat bemenetei áthaladási folyamatot vezérlő állapotgép kimenete W, a léptetést meghatározó időzítő timeoutja Q valamint a sorompó nyitott illetve zárt állapotát jelző érzékelők. Mivel a sorompót RC szervó mozgatja amiből a mérőrendszer nincs kivezetve, és mikrokapcsolókat sem rendeltünk a végpoziciók érzékeléséhez, ezért szoftveres trükköt kell használni. Ez a trükk kiaknázza azt a lehetőséget, hogy az RC szervó biztos abba a pozícióba fordul, amelyikbe vezéreltük. Ez azt jelenti, hogy a szervónak pozícióparancsként kiadott számokat mért értéknek is tekintjük egyben, ezek közül egy szám a nyitott egy másik szám a zárt pozíciót fogja jelenteni. Figyelembe kell még venni azt is, hogy az RC szervók marha gyorsan fordulnak akár a két végállás között is, ezért a forgást lassítani kell, nehogy a sorompó rúdja elszálljon. A lassítást szintén időzítővel oldjuk meg, amikor az időzítő lejárt, mindig egy pár egységnyivel odébb fordítjuk a szervót, mig a kivánt pozíciót el nem éri. A szoftveres trükkre visszatérve ez úgy fog működni, hogy a szervókimenetre küldött értéket egy rutin a valódi kiküldés elött megvizsgálja és a nyitott pozíciónak megfelelő számértéknél a N szoftveres bemenetet, a zárt pozíciónak megfelelő számértéknél pedig Z szoftveres bemenetet állítja 1 be. Itt is szükséges a P kimenet az időzítő, F E kimenetek pedig a sorompó léptető vezérléséhez, ahol F a fel, E a leirányt jelöli. A be és kimenetek sorrendje WQNZ/FEP, az állapotgráf a következő:



Nos ez a leírási módszer, ezek az állapotgráfok első ránézésre bonyolultnak tűnhetnek azok számára akik még nem láttak ilyet, a feladat nyilván megoldható lenne egyszerűbb bedrótozós gányolással is. Mindazon által tessék nekem elhinni, hogy egy ennél sokoldalúbb, jóval több állapotú és nagyobb számú állapotátmenetet tartalmazó rendszer leírása az állapotgráfos módszerrel mindig tiszta, világos és egyértelmű, jól követhető és leprogramozható. A bonyolultabb rendszerekben való gányolások mindig olyan hazárdokat hoznak elő, amik nagyon nehezen írthatók ki később azokból.

1.2 A hardver bekötése

Mielött kódokat irogatnánk el kell dönteni, hogy a hardver ki és bemeneteit hogyan kötjük össze a ledekkel, a mikrokapcsolókkal illetve az RC szervóval. A Bal piros ledet a DIO kártya OUT1-es, a jobb piros ledet a DIO kártya OUT2-es, a zöld ledet a DIO kártya OUT3-as kimenetére csatlakoztatjuk. Az A mikrokapcsoló a DIO kártya IN0-ás, a B mikrokapcsoló a DIO kártya IN1-es, a C mikrokapcsoló a DIO kártya IN2-es, a D mikrokapcsoló pedig a DIO kártya IN3-as bemenetére kerül. Az RC szervót pedig az SC kártya SO1 kimenetére kötjük rá. A DIO kártya a SLOT0-ba az SC kártya a SLOT2-be van bedugva. A hardver kapcsolási rajza a következő:



1.3 A vezérlés implementálása Delphiben

Miután a sorompó vezérlési folyamatát megterveztük majd a hardver ki és bemeneteit összekötöttük a külvilággal, valamilyen programnyelven a vezérlést implementálni kell. Legyen ez most a sokak által közkedvelt és a C programozók által általában lefikázott Delphi. A kódolás elött itt is némi tervezést hajtunk végre. Először is ki kell találni a program működését amit a következő ábra szemléltet.



Létrehozunk tehát a főszálban egy közös IOMap-et ami az összes vezérlésre használt változó értékét egy stringben tárolja. Ezek az értékek karakterenként "0" és "1" lehetnek, pozíciójuk használható az állapotuk kiolvasásásra és változtatására. Az összes vezérlő, időzítő és IO rutin ebbe az IOMap-be rakja és innen is veszi a működéséhez szükséges értékeket. Mint az ábrából látható, az egyes részfeladatokat külön bontottuk, ezeket ha lehet külön szálakban is (multi thread) fogjuk futtatni. A program GUI-ját pedig felhasználjuk arra, hogy egy virtuális kapcsolósorral és egy virtuális fénysorompóval a folyamat a képernyőn is látható, illetve az egér segítségével is vezérelhető legyen. Ez teljesen párhuzamosan működik a CI4A-ra kötött hardverrel, igy az A gomb megnyomása a program kezelő felületén ekvivalens lesz az A érzékelőt szimbolizáló mikrokapcsoló megnyomásával.

1.3.1 Az állapotgép

Az előző fejezetben nem azért rajzolgattunk az állapotgráfokat, mert azok olyan viccesek, vagy mert jól mutatnának egy egyetemi irányítástechnika tankönyv oldalain, hanem mert az ott leírtakat egy az egyben meg akarjuk etetni az állapotgépeinkkel. Ehhez olyan text feldogozáson alapuló állapotgép objektumot készítünk, amelyik nem csak erre a feladatra van bedrótozva, hanem később újrahasznosítható, teljesen általános, nem kell benne bitenkénti egynullákkal, AND, OR, bitsiftelés és egyéb nyűgös butáskodássokkal foglalkozni. Az állapotgépünk a TThread osztályból fog származni, ezáltal minden létrehozott példány külön szálban futhat. Az osztály definíciója a következő.

    TStateMachine = class(TThread)
    public
      ControlTable:TStringlist;
      Fields:TStringlist;
      ActualState:string;
      Inputs:string;
      Outputs:string;
      pExtIOMap:TPString;
      InputMap:string;
      OutputMap:string;
      constructor Create(ExtIOMap:TPString;CStartState,CInputMap,COutputMap:string);
      destructor Destroy;
      procedure Execute; override;
      procedure MakeFields(InS:string; Separator:char; var Fields:TStringList);
      procedure RefreshState;
      procedure GetInputSlot;
      procedure SetOutputSlot;
    end;

Az osztálynak eleme néhány fontos változó, meg egy két eljárás ami a működtetést végzi. A ControlTable tartalmazza az állapotgráfot, a Fields a vezérlőtábla sorainak szétbontásához kell, az ActualState tartalmazza, hogy az állapotgép épp milyen állapotban van. Az Inputs Outputs tartalmazza az állapotgép ki és bemeneti értékeit, az InputMap OutputMap pedik ezek mappelésének sorrendjét. A pExtIOMap mutat arra az állapotgépen kívül elhelyezett közös IOmap-re amiben a működéshez szükséges változók vannak. (A TPString stringre mutató pointer.)

Az állapotgépet az osztály konstruktorával hozzuk létre. Itt kapásból megadunk néhány szükséges alapadatot ezek a következők:
  • ExtIOMap: a közös IO mapre mutató pointer
  • CStartState: az állapotgép kiinduló állapota
  • CInputMap: a bemenetek mappelési sorrendje
  • COutputMap: a kimenetek mappelési sorrendje
  constructor TStateMachine.Create(ExtIOMap:TPString;CStartState,CInputMap,COutputMap:string);
  
  begin
    inherited Create(true);
    ActualState:=CStartState;
    pExtIOMAp:=ExtIOMap;
    InputMap:=CInputMap;
    OutputMap:=COutputMap;
    ControlTable:=TStringlist.Create;
    ControlTable.Clear;
    Fields:=TStringlist.Create;
  end;

Meghívjuk az ősosztály konstruktorát, majd az osztály globális változóit beállítjuk a bemeneő adatoknak megfelelően. Létrehozzuk és töröljük a kontrolltáblát, és létrehozzuk a táblasorok felbontásához szükséges mezőlistát is.

Minden objektumnak van destruktora, ezzel szedjük ki őket a memóriából, ha már nincs rájuk szükség. Ebben kitakarítjuk az általunk hozzáadott memóriafoglaló elemeket, majd meghívjuk az ősosztály destruktorát.

  destructor TStateMachine.Destroy;
  
  begin
    Fields.Destroy;
    ControlTable.Destroy;
    inherited Destroy;
  end;

Minden thread-nek van egy Execute függvénye. Ebbe kerül az a kód amit végre kell hajtani amikor a thread fut. Ez eredetileg virtuálisnak van definiálva és ezt vágjuk felül a saját kódunkkal, ami jelen esetben abból áll, hogy beolvassuk a bemenetek pillanatnyi értékét és az aktuális állapot ismeretében a kimeneteket ennek megfelelően állítjuk be. Ezek után egy végtelen ciklusban meghívjuk az állapotgép RefreshState függvényét. Így ez addig fut, amig az állapotgépet a Terminate meghívásával el nem pusztítjuk.

  procedure TStateMachine.Execute;
  
  var i:integer;
  
  begin
    synchronize(GetInputSlot);
    for i:=0 to ControlTable.Count-1 do
    begin
      MakeFields(ControlTable[i],'/',Fields);
      if ActualState=Fields[3] then
      begin
        Outputs:=Fields[2];
        synchronize(SetOutputSlot);
        break;
      end;
    end;
    while not(Terminated) do RefreshState;
  end;

A RefreshState-ben van tulajdonképpen a lényegi rész. Mivel ez periodikusan lefut, a processzornak lélegzetvételnyi szünetet kell biztosítani, hogy ne csak ezzel a szállal foglalkozzon. Ezt csinálja a sleep(1). Ezek után a bemenetek beolvasása következik a GetInputSlot eljárás segítségével, majd egy ciklus végig fut a vezérlőtáblán és megvizsgálja, hogy melyik sorok nulladik mezője egyezik az aktuális állapottal. Amikor ilyet talál, akkor az első mezőben levő bemeneti kombinációt összehasonlítja az aktuálisan az Inputs-ba beolvasottal. Ha ezek egyeznek, akkor állapotváltozás következik be, tehát a kimenetet a második mezőnek megfelelőre, az aktuális állapotot pedig a harmadik mezőnek megfelelőre állítja be. Állapotváltozás esetén természetesen a kimenetek is frissítésre kerülnek. A frissítést a SetOutputSlot eljárás végzi.

  procedure TStateMachine.RefreshState;
  
  var i:integer;
  
  begin
    sleep(1);
    synchronize(GetInputSlot);
    for i:=0 to ControlTable.Count-1 do
      if pos(ActualState+'/',ControlTable[i])=1 then
      begin
        Makefields(ControlTable[i],'/',Fields);
        if Inputs=Fields[1] then
        begin
          ActualState:=Fields[3];
          Outputs:=Fields[2];
          synchronize(SetOutputSlot);
        end;
      end;
  end;

Ez a cucc azért lett ilyen végtelenül egyszerű, mert a text formátumban megadott állapotgráfot nem alakítottuk át bináris masszává. Tehettük ezt azért, mert a string karakterei az összehasonlítást tekintve kapásból logikai ÉS kapcsolatban vannak egymással.

A bemenetek és a kimenetek mappelése és frissítése a GetInputSlot és a SetOutputSlot eljárásokkal történik. Mivel ezek kinyulnak a thread-ből, ezeket csak a thread osztály synchronize eljárásán keresztül szabad átadni. A mappelés vagyis az adatok leválogatása a közös IOMap-ből mindig egy ciklus segítségével történik a konstruktorban megadott sorrendek alapján.

  procedure TStateMachine.GetInputSlot;
  
  var i:integer;
      index:integer;
  
  begin
    index:=1;
    Inputs:='';
    for i:=1 to length(InputMap) div 2 do
    begin
      Inputs:=Inputs+pExtIOMAp^[strtoint(copy(InputMap,index,2))];
      inc(index,2);
    end;
  end;
  
  
  procedure TStateMachine.SetOutputSlot;
  
  var i:integer;
      index:integer;
  
  begin
    index:=1;
    for i:=1 to length(OutputMap) div 2 do
    begin
      pExtIOMAp^[strtoint(copy(OutputMap,index,2))]:=Outputs[i];
      inc(index,2);
    end;
  end;

Az osztály tartalmaz még egy segédeljárást ami egy string sor elemeinek mezőkre bontását végzi egy tetszőlegesen megadott mezőszeparátor alapján.

  procedure TStateMachine.MakeFields(InS:string; Separator:char; var Fields:TStringList);
  
  var SepaPositions:array[0..100] of integer;
      Indx:integer;
      i:integer;
  
  begin
    Indx:=0;
    InS:=Separator+InS+Separator;
    for i:=1 to length(InS) do
      if InS[i]=Separator then
      begin
        inc(Indx);
        SepaPositions[Indx]:=i;
      end;
    Fields.Clear;
    for i:=1 to Indx-1 do
    begin
      Fields.Add(copy(InS,SepaPositions[i]+1,SepaPositions[i+1]-SepaPositions[i]-1));
    end;
  end;

Mi lesz ha a számítógépbe bemenő adatként szemetet táplálunk be? Hát kijön a semmire sem használható rendszerezett szemét. Ennél az állopotgépnél is ez a helyzet, hülyeség ellen nincs védve, azaz ha a kimenetei és bemeneti sorrendeket, valamit a vezérlőtáblát elcsesszük, akkor bizony - mivel semmilyen konzisztenciaellenőrzés nincs - hülyeségeket fog csinálni. Ezért a feltöltésnél legyünk körültekintőek.

1.3.2 A főprogram

A főprogram elkészítéséhez nyissunk a Delphiben egy új projectet és hozzuk létre a main formon a következő objektumokat:




A mikrokapcsolókat négy Button (A B C D) a ledeket 3 köralakú Shape szimbolizálja. A két pont .. egy Label, ebbe írjuk majd a sorompó mozgató szervó pillanatnyi helyzetét. Szükség lesz még három időzítőre, illetve egy kliens socketre a CI4A-hoz való kapcsolódás miatt. Felrakunk még egy Memo-t is ebbe lehet debug sorokat küldeni a működés közben.

Ha ez kész definiáljuk a program konstansait. Ezek a közös IO Map pozíciói, a szervó nyitott illetve zárt helyzetét meghatározó értékek, valamint a CI4A driver IP címe és IO portja (a CEI portot ebben a példában nem használjuk), meg egy pozíciótábla a stringben tárolt bitek kihalászásához:

  const MSWITCH_A = '01';
        MSWITCH_B = '02';
        MSWITCH_C = '03';
        MSWITCH_D = '04';
        VIRTUAL_W = '05';
        TIMEOUT_T = '06';
        REDLED_L  = '07';
        REDLED_R  = '08';
        GREENLED_G = '09';
        RESETTMR_S = '10';
        CRGATE_N = '11';
        CRGATE_Z = '12';
        CRGATE_F = '13';
        CRGATE_E = '14';
        RESETTMR_P = '15';
        TIMEOUT_Q = '16';
  
        GATE_OPENED = 120;
        GATE_CLOSED = 10;
  
        CI4A_HOST = 'localhost';
        CI4A_IO_PORT = 7500;
  
        BIT0 = 8;
        BIT1 = 7;
        BIT2 = 6;
        BIT3 = 5;
        BIT4 = 4;
        BIT5 = 3;
        BIT6 = 2;
        BIT7 = 1;

A TForm1 osztálydeklarációjának public részébe beleírjuk a globális változóinkat. Az IOMap a már sokszor emlegetett közös adattároló, a SERVO_POS a szervó pillanatnyi értéke. Ahhoz, hogy ne csináljunk állandóan kommunikációt a hálózaton, tárolni fogjuk a fénysorompót szimbolizáló ledek előző PREV_OUT_LEDS illetve a sorompót szimbolizáló szervó előző PREV_SERVO_POS értékét is. Az SM_PROC az áthaladási folyamatvezérlő, az SM_BLINK a villogási folyamatvezérlő, az SM_MOVE a szervó mozgatási folyamatot vezérlő állapotgép egy-egy példányai lesznek. Szintén itt kell megadni az osztályhoz csapott saját függvényeket is ezekről majd később lesz szó.

    public
      IOMap:string;
      SERVO_POS:byte;
      PREV_OUT_LEDS,PREV_SERVO_POS:string;
      SM_PROC,SM_BLINK,SM_MOVE:TStateMachine;
  
      procedure RefreshScreenOuts;
      procedure SendDataToCI4A;
      function ByteStr2ValueStr(InStr:string):string;
      function ValueStr2ByteStr(InStr:string):string;
    end;

A program indulásakor lefutó FormCreate-ben beállítjuk a driverhez csatlakozás paramétereit, ezek az IOCtrlSocket.Host és a IOCtrlSocket.Port. Majd aktívvá tesszük a socketet, hogy az a program indulásakor kapcsolódjon fel a driverre. Inicializáljuk a led és a szervókimenetek előző értékét, kinullázzuk a teljes IOMap-ot, valamint megadjuk, hogy a program indulásakor a sorompó nyitott állapotban van.

  procedure TForm1.FormCreate(Sender: TObject);
  
  begin
    IOCtrlSocket.Host:=CI4A_HOST;
    IOCtrlSocket.Port:=CI4A_IO_PORT;
    IOCtrlSocket.Active:=true;
  
    PREV_OUT_LEDS:='';
    PREV_SERVO_POS:='';
  
    IOMap:='0000000000000000';
    SERVO_POS:=GATE_OPENED;
  
    SM_PROC:=TStateMachine.Create(@Form1.IOMap,'0000',MSWITCH_A+MSWITCH_B+MSWITCH_C
             +MSWITCH_D,VIRTUAL_W);
    SM_PROC.ControlTable.Add('0000/0001/0/0100');
    SM_PROC.ControlTable.Add('0000/0010/0/0001');
    SM_PROC.ControlTable.Add('0001/0001/0/0011');
    SM_PROC.ControlTable.Add('0011/0000/0/0000');
    SM_PROC.ControlTable.Add('0010/0010/1/0001');
    SM_PROC.ControlTable.Add('0101/0100/1/0010');
    SM_PROC.ControlTable.Add('0000/1000/0/0101');
    SM_PROC.ControlTable.Add('0100/0010/1/0111');
    SM_PROC.ControlTable.Add('0000/0100/0/1000');
    SM_PROC.ControlTable.Add('0111/0100/1/1000');
    SM_PROC.ControlTable.Add('1000/1000/0/0110');
    SM_PROC.ControlTable.Add('0110/0000/0/0000');
    SM_PROC.Resume;
  
    SM_BLINK:=TStateMachine.Create(@Form1.IOMap,'011',VIRTUAL_W+TIMEOUT_T+REDLED_L
              +REDLED_R+GREENLED_G,REDLED_L+REDLED_R+GREENLED_G+RESETTMR_S);
    SM_BLINK.ControlTable.Add('011/01000/0001/000');
    SM_BLINK.ControlTable.Add('000/00000/0010/010');
    SM_BLINK.ControlTable.Add('010/01001/0011/000');
    SM_BLINK.ControlTable.Add('000/00001/0000/011');
    SM_BLINK.ControlTable.Add('011/10000/0100/100');
    SM_BLINK.ControlTable.Add('100/00010/0000/011');
    SM_BLINK.ControlTable.Add('100/11010/0101/000');
    SM_BLINK.ControlTable.Add('000/10100/0100/100');
    SM_BLINK.ControlTable.Add('001/11100/1001/000');
    SM_BLINK.ControlTable.Add('000/10010/1000/001');
    SM_BLINK.ControlTable.Add('010/10001/1000/001');
    SM_BLINK.ControlTable.Add('001/00100/0010/010');
    SM_BLINK.Resume;
  
    SM_MOVE:=TStateMachine.Create(@Form1.IOMap,'00',VIRTUAL_W+TIMEOUT_Q+CRGATE_N+CRGATE_Z,
             CRGATE_F+CRGATE_E+RESETTMR_P);
    SM_MOVE.ControlTable.Add('00/1110/011/01');
    SM_MOVE.ControlTable.Add('01/1000/010/11');
    SM_MOVE.ControlTable.Add('11/1100/011/01');
    SM_MOVE.ControlTable.Add('00/0101/101/10');
    SM_MOVE.ControlTable.Add('10/0000/100/11');
    SM_MOVE.ControlTable.Add('11/0100/101/10');
    SM_MOVE.ControlTable.Add('01/1101/000/00');
    SM_MOVE.ControlTable.Add('10/0110/000/00');
    SM_MOVE.Resume;
  end;

Ezek után az állapotgép osztályból létrehozunk három példányt. A létrehozáskor megadjuk nekik az IOMap-ot amit használni fognak, a kezdeti állapotokat ahonnan indulniuk kell, illetve a be és kimenetek IOMap-ból való kiolvasási sorrendjét. A létrehozás után fel kell tölteni mindegyik állapotgép vezérlő tábláját a ControlTable.Add eljárásának segítségével. Az SM_PROC nevűt az áthaladási folyamatvezérlő, az SM_BLINK nevűt a villogási folyamat vezérlő, az SM_MOVE nevűt a sorompó mozgatás vezérlő állapotgráffal etetjük meg olymódon, hogy az állapotgráfok állapotátmeneteit irjuk a vezérlőtábla soraiba a következő szintaxissal: MELYIK ÁLLAPOTBÓL/MILYEN BEMENETI KOMBINÁCIÓ HATÁSÁRA/MILYEN KIMENETI VÁLTOZTATÁSSAL/ MELYIK ÁLLAPOTBA jutunk. Nézzünk egy konkrét példát. Vegyük a villogási folyamatot leíró állapotgráfot. Itt a "Zöld led bekapcsolva Piros ledek kikapcsolva" 010 azonosítójú állapotból a "Bal piros led be, jobb piros led kikapcsolva zöld led kikapcsolva" 001 azonosítójú állapotba egyetlen átmeneti nyíl vezet. A bemenetek sorrendje WTLRG, értékük itt 10001, a kimeneteké LRGS, értékük itt 1000. Állapotváltozás 010-ból 001-be akkor következik be, ha a sorompó logikailag lezárt (W=1) és a zöld led világít (G=1). A kimenetek is változnak, a zöld led helyett a bal piros fog majd világítani. Ebből aztán szépen össze lehet rakni a vezérlő sort: 010/10001/1000/001, ezt az átmenetet itt a példában az SM_BLINK vezérlőtáblájának utolsó elötti sora tartalmazza. Az állapotátmenetek tetszőleges sorrendben bevihetők az állapotgépbe, csak arra kell ügyelni, hogy az összes átmenetet beletegyük. A feltöltés után mindhárom állapotgépet rögtön el is indítjuk a Resume eljárással.

A vezérlés részét képezi még két időzítő, a BlinkTimer a villogás sebességét, a MoveTimer a szervómozgatás sebességét határozza meg. Mindkét időzítőnek van reset bemenete és timeout kimenete. Ezeket is a közös IOMap-ban tároljuk, az időzítők a használatkor mappelik őket. Mivel a TTimer osztály kapásból külön thread-ben működik, a külön szálban indítással nem kell foglalkoznunk.

  procedure TForm1.BlinkTimerTimer(Sender: TObject);
  
  begin
    if IOMAP[strtoint(RESETTMR_S)]='1' then
    begin
      IOMAP[strtoint(RESETTMR_S)]:='0';
      IOMAP[strtoint(TIMEOUT_T)]:='0';
      BlinkTimer.Tag:=0;
    end;
    BlinkTimer.Tag:=BlinkTimer.Tag+10;
    if BlinkTimer.Tag>=700 then
    begin
      IOMAP[strtoint(TIMEOUT_T)]:='1';
    end;
  end;
  
  
  procedure TForm1.MoveTimerTimer(Sender: TObject);
  
  begin
    if IOMAP[strtoint(RESETTMR_P)]='1' then
    begin
      IOMAP[strtoint(RESETTMR_P)]:='0';
      IOMAP[strtoint(TIMEOUT_Q)]:='0';
      MoveTimer.Tag:=0;
      if IOMAP[strtoint(CRGATE_F)]='1' then inc(SERVO_POS,1);
      if IOMAP[strtoint(CRGATE_E)]='1' then dec(SERVO_POS,1);
    end;
    MoveTimer.Tag:=MoveTimer.Tag+1;
    if MoveTimer.Tag>=5 then
    begin
      IOMAP[strtoint(TIMEOUT_Q)]:='1';
    end;
  
    if SERVO_POS=GATE_OPENED then IOMAP[strtoint(CRGATE_N)]:='1'
      else IOMAP[strtoint(CRGATE_N)]:='0';
    if SERVO_POS=GATE_CLOSED then IOMAP[strtoint(CRGATE_Z)]:='1'
      else IOMAP[strtoint(CRGATE_Z)]:='0';
  end;

A MoveTimer kódjában kapott helyet a szervó vezérlő folyamatnál ismertetett szoftveres trükk: a szervókimenetre kiküldendő értéket ez a rutin a vizsgálja meg és a nyitott pozíciónak megfelelő számértéknél a N szoftveres bemenetet, a zárt pozíciónak megfelelő számértéknél pedig Z szoftveres bemenetet állítja 1 be.

A harmadik időzítő az OutUpd periódikusan meghívja a kimeneteket frissítő RefreshScreenOuts és SendDataToCI4A eljárásokat. A RefreshScreenOuts a formon levő kijelzők (ledeket imitáló shapek és a szervó pozícióját mutató label) értékét rajzolja újra.

  procedure TForm1.OutUpdTimer(Sender: TObject);
  
  begin
    RefreshScreenOuts;
    SendDataToCI4A;
  end;
  
  procedure TForm1.RefreshScreenOuts;
  
  begin
    if IOMAP[strtoint(GREENLED_G)]='1' then Shape3.Brush.Color:=ClGreen;
    if IOMAP[strtoint(GREENLED_G)]='0' then Shape3.Brush.Color:=ClWhite;
    if IOMAP[strtoint(REDLED_L)]='1' then Shape1.Brush.Color:=ClRed;
    if IOMAP[strtoint(REDLED_L)]='0' then Shape1.Brush.Color:=ClWhite;
    if IOMAP[strtoint(REDLED_R)]='1' then Shape2.Brush.Color:=ClRed;
    if IOMAP[strtoint(REDLED_R)]='0' then Shape2.Brush.Color:=ClWhite;
  
    Label1.Caption:=inttostr(SERVO_POS);
  end;
  
  procedure TForm1.SendDataToCI4A;
  
  var OutByteStr,OutValStr:string;
  
  begin
    if IOCtrlSocket.Socket.Connected then
    begin
      OutByteStr:='00000000';
      OutByteStr[BIT1]:=IOMAP[strtoint(REDLED_L)];
      OutByteStr[BIT2]:=IOMAP[strtoint(REDLED_R)];
      OutByteStr[BIT3]:=IOMAP[strtoint(GREENLED_G)];
      if OutByteStr<>PREV_OUT_LEDS then
      begin
        IOCtrlSocket.Socket.SendText('S0OB01V'+ByteStr2ValueStr(OutByteStr)+chr(13));
        PREV_OUT_LEDS:=OutByteStr;
        sleep(10);
      end;
      OutValStr:=inttostr(SERVO_POS);
      if length(OutValStr)=1 then OutValStr:='00'+OutValStr;
      if length(OutValStr)=2 then OutValStr:='0'+OutValStr;
      if OutValStr<>PREV_SERVO_POS then
      begin
        IOCtrlSocket.Socket.SendText('S2OB01V'+OutValStr+chr(13));
        PREV_SERVO_POS:=OutValStr;
        sleep(10);
      end;
    end;
  end;

A CI4A drivert a SendDataToCI4A értesíti a kimenetek megváltozásáról. Ha a kimenet az előző értékhez képest változott, akkor a digitális kimenetek esetén egy mappelést és egy string-string átalakítást, a szervókimenet esetén pedig egy byte-string átalakítást hajt végre, majd előállítja ezekből a driver IO bemenete által elfogadható stringe(ke)t és el is küldi az(oka)t.

A vezérlés bemenetére a CI4A driver felöl érkeznek a csomagok. A csomagok vételét a IOCtrlSocketRead függvény végzi. Amikor egy vezérlőstring érkezik, annak tartalmát bitleíró stringgé alakítja, majd a közös IOMap-ba mappeli.

  procedure TForm1.IOCtrlSocketRead(Sender: TObject;
    Socket: TCustomWinSocket);
  
  var InStr,InByteStr:string;
  
  begin
    InStr:=Socket.ReceiveText;
    if copy(InStr,1,7)='S0IB01V' then
    begin
      InByteStr:=ValueStr2ByteStr(copy(InStr,8,3));
      IOMAP[strtoint(MSWITCH_A)]:=InByteStr[BIT0];
      IOMAP[strtoint(MSWITCH_B)]:=InByteStr[BIT1];
      IOMAP[strtoint(MSWITCH_C)]:=InByteStr[BIT2];
      IOMAP[strtoint(MSWITCH_D)]:=InByteStr[BIT3];
    end;
  end;

A bemenetek mappelését ugyanúgy meg kell csinálni, ha formon nyomkodjuk az A, B, C, D gombokat. A mappelést a gombok állapotait figyelő ButtonDown és ButtonUp függvények végzik, amikre az összes button egérnyomogató függvénye fel van húzva.

  procedure TForm1.ButtonDown(Sender: TObject; Button: TMouseButton;
    Shift: TShiftState; X, Y: Integer);
  
  var psa,psb,psc,psi:string;
  
  begin
    if Sender=Button_A then IOMap[strtoint(MSWITCH_A)]:='1';
    if Sender=Button_B then IOMap[strtoint(MSWITCH_B)]:='1';
    if Sender=Button_C then IOMap[strtoint(MSWITCH_C)]:='1';
    if Sender=Button_D then IOMap[strtoint(MSWITCH_D)]:='1';
  end;
  
  
  procedure TForm1.ButtonUp(Sender: TObject; Button: TMouseButton;
    Shift: TShiftState; X, Y: Integer);
  
  begin
    if Sender=Button_A then IOMap[strtoint(MSWITCH_A)]:='0';
    if Sender=Button_B then IOMap[strtoint(MSWITCH_B)]:='0';
    if Sender=Button_C then IOMap[strtoint(MSWITCH_C)]:='0';
    if Sender=Button_D then IOMap[strtoint(MSWITCH_D)]:='0';
  end;

Az IO socket nevezetes függvényeit pedig a kapcsolat létrejöttének, esetleges hibájának nyomon követésére használjuk.

  procedure TForm1.IOCtrlSocketConnecting(Sender: TObject;
    Socket: TCustomWinSocket);
  
  begin
    Memo1.Lines.Add('Trying to connect to CI4A IO interface');
  end;
  
  procedure TForm1.IOCtrlSocketConnect(Sender: TObject;
    Socket: TCustomWinSocket);
  
  begin
    Memo1.Lines.Add('CWB controller connected to CI4A IO interface');
  end;
  
  procedure TForm1.IOCtrlSocketDisconnect(Sender: TObject;
    Socket: TCustomWinSocket);
  
  begin
    Memo1.Lines.Add('CWB controller disconnected from CI4A IO interface');
  end;
  
  procedure TForm1.IOCtrlSocketError(Sender: TObject;
    Socket: TCustomWinSocket; ErrorEvent: TErrorEvent;
    var ErrorCode: Integer);
  
  begin
    Memo1.Lines.Add('IO socket error:'+inttostr(ErrorCode));
    ErrorCode:=0;
  end;
  
  
  
  

A mappelési folyamatokhoz szükséges két segéd függvény a ByteStr2ValueStr és a ValueStr2ByteStr, ezek stringben leírt byte-ból csinálnak stringben leírt bináris formátumot illetve ugyan ezt de fordítva.

  function TForm1.ByteStr2ValueStr(InStr:string):string;
  
  var i:integer;
      res:string;
      depo:byte;
  
  begin
    depo:=0;
    for i:=1 to 8 do
      if InStr[i]='1' then
         depo:=depo+byte(trunc(power(2,8-i)));
    res:=inttostr(depo);
    if length(res)=1 then res:='00'+res;
    if length(res)=2 then res:='0'+res;
    ByteStr2ValueStr:=res;
  end;
  
  
  function TForm1.ValueStr2ByteStr(InStr:string):string;
  
  var i:integer;
      depo,h:byte;
      res:string;
  
  begin
    res:='';
    depo:=byte(strtoint(InStr));
    for i:=7 downto 0 do
    begin
      h:=byte(trunc(power(2,i)));
      res:=res+inttostr((depo and h) div h);
    end;
    ValueStr2ByteStr:=res;
  end;

A TForm osztály FormDestroy eljárása való arra, hogy kitakarítsuk magunk után a szemetet a memóriából illetve végrehajtsuk a kilépés elötti teendőinket. Így történik ez itt is, a Terminate függvényekkel meggyilkoljuk az állapotgépeinket illetve lezárjuk a CI4A driver felé menő kapcsolatot is.

  procedure TForm1.FormDestroy(Sender: TObject);
  
  begin
    SM_PROC.Terminate;
    SM_BLINK.Terminate;
    SM_MOVE.Terminate;
    if IOCtrlSocket.Socket.Connected then
      IOCtrlSocket.Active:=false;
  end;

1.4 Az eredmény

Az eredmény az természetesen egy működő ketyere, aminek lehet örülni. A működést pedig a következő videók szemléltetik:

Sorompó videó 1 (wmv 6.9MB)              Sorompó videó 2 (wmv 16.7MB)

Azért, hogy ne másolással kelljen beverned innen a kódot, ha akár hardver nélkül is ki akarod próbálni, megtalálod azt a letöltéseknél cwbproject.zip néven.




2. Közlekedési lámpa távvezérlése





2.1 A közlekedési lámpa felépítése és elvi működése

Nos itt van még egy egyszerű példa a forgalom irányítás témaköréből, nem kell hozzá más hardver, csak hat darab led meg 4 mikrokapcsoló. A fenti ábrán két lámpapár van, a T-mobile színű meg a barna. Ezeknek egy gyalogos átkelőhely nélküli szimpla kereszteződés forgalmát kell irányítaniuk. A közlekedési lámpa izzóit piros sárga és zöld ledek, a lámpa üzemmódkapcsolóit A, B, C, D mikrokapcsolók helyettesítik. Az A kapcsoló megnyomására az egyik lámpa piros, a másik zöld lesz. Majd eljátszák a váltás szekvenciáját amelyik zöld volt az sárga majd piros, amelyik piros volt az piros-sárga majd zöld lesz, utána fordítva és a végtelenségig ezt ismételve. A B mikrokapcsoló lenyomására a lámpák sárga villógóra kapcsolnak. A C mikrokapcsolóval pedig kikapcsolt állapotba hozhatjuk a rendszert. A D mikrokapcsolónak is lesz szerepe, de erről majd később.
Ezt a vezérlést kicsit másképp fogjuk megoldani mint azt az 1. pontban tettük. Tulajdonképpen itt is állapotok megváltozásáról van szó, de most nem klasszikus állapotgéppel, hanem időszekvencia vezérléssel fogunk dolgozni. Ez úgy néz ki, hogy egy-egy táblázatba felírjuk a lehetséges kimeneti állapotokat, és minden működési mód minden állapotához egy-egy időbélyeget rendelünk. Az időbélyeg fogja megmondani, hogy mikor kell az állapotváltozásnak bekövetkeznie. Itt három működési mód van: a Normal(a lámpa rendesen működik) a Semi(sárgán villog) és az Off(ki van kapcsolva) Ezekhez egy-egy időszekvencia tábla tartozik:



A táblázatok első oszlopa azt tartalmazza, hogy az állapotváltozásoknak hanyadik másodpercben kell bekövetkezniük, a második oszlop pedig azt mutatja, hogy melyik led fog ekkor világítani a két lámpán. Az egyes betűk jelentése a következő: Az R (Red,piros), az Y (Yellow,sárga), a G (Green,zöld) azt jelzik, hogy a lámpákon melyik szín(ek) van(nak) bekapcsolva. Az N azt jelenti, hogy semmi sem világít. A | elválasztó jel egyik oldalán a T-mobile színű, a másik oldalán a barna lámpa ledjeinek adatai szerepelnek.
A dolog tehát úgy működik, hogy egy időzítő által generált timekód alapján a kiválasztott üzemmódnak megfelelő táblázatból kiolvassuk a megfelelő értékeket és azt a kimenetre küldjük. Az üzzemmódok a timekódgenerátort is vezérlik: az első esetben ha az idő nagyobb mint 27 sec, a második és harmadikban ha az idő nagyobb mint 1 sec, akkor a nullázzni kell a timekódgenerátort is, hogy a számlálás előlről kezdődhessen.

2.2 A hardver bekötése

Mielött kódokat irogatnánk itt is el kell dönteni, hogy a hardver ki és bemeneteit hogyan kötjük össze a ledekkel és a mikrokapcsolókkal A T-mobile színő lámpa zöld ledjét a DIO kártya OUT0-ás, a sárga ledjét DIO kártya OUT1-es, a pirosat pedig a DIO kártya OUT2-es kimenetére, a barna lámpa zöld ledjét a DIO kártya OUT3-as, a sárga ledjét DIO kártya OUT4-es, a pirosat pedig a DIO kártya OUT5-ös kimenetére csatlakoztatjuk. Az A mikrokapcsoló a DIO kártya IN0-ás, a B mikrokapcsoló a DIO kártya IN1-es, a C mikrokapcsoló a DIO kártya IN2-es, a D mikrokapcsoló pedig a DIO kártya IN3-as bemenetére kerül. A kártya a legelső slot-ba van bedugva. A hardver kapcsolási rajza a következő:
Felmerül a kérdés, hogy az OUT0-ra csatlakoztatott led miért van fordítva bekötve. A válasz egyszerű: az én DIO kártyámon a 0-ás csatorna pozitív kimenetűre, az összes többi pedig negatív kimenetűre van jumperolva.

2.3 A vezérlés implementálása Pythonban

Nehogymár rámsüsse valaki azt a bélyeget, hogy windows-imádó vagyok, álljon itt egy programozási implementáció linuxra is, méghozzá az egyre népszerűbb pythonban. Egy ici-pici bökkenő akad csupán, ez pedig az, hogy nekem nincs itthon linuxom. Ezért fokozni fogom az élvezeteket és távvezérlést valósítunk meg. A ledek és a mikrokapcsolók itt lesznek Budapesten az asztalomon, a vezérlés meg Tökön fog futni HA5TS barátom linuxán. (A félreértések elkerülése végett: Tök, Magyarország, LAT=47ş33.1' LON=18ş42.9')

Itt is egy kis tervezéssel kezdjük ami abból áll, hogy átgondoljuk milyen objektumosztályokra lesz szükség a megvalósításhoz a működés ismeretében . Ezek az osztályok a következők:




Az ábrán a nyilak azt mutatják, hogy az egyes osztályok melyik osztályokhoz kapcsolódnak adatbeszerzés céljából. A program lelke a WORKING ez a CI4AComm-on keresztül kommunikál a driverrel. A TCR szolgáltatja az ütemezéshez szükséges timekódot, a TrafficLight-ban pedig a timekódok kimeneti értékekhez rendelhetőek. Az Options tárolja az indításkor megadott paramétereket, A Debug pedig az Options kivételével mindegyik osztályhoz hozzá van rendelve, bár a TrafficLight-ban semmi sem lesz implementálva belőle.

2.3.1 Az _Options osztály

Az _Options osztály tartalmazza a program indításakor feltöltendő változókat, a CI4A interface IP címét és portját és a debuglog nevét. Mivel egyetlen függvénye a _parseCommandLine az __init__-ben lefut, egy példány létrejöttekor elemzi a megadott opciókat és paramétereket, feltölti a megfelelő változókat, illetve hibajelzést ad, ha kevesli a kötelezően megadandó paraméterek számát.

  
class _Options:
    """Checks and stores command line options and arguments."""

    def __init__(self, commandLine):
        """Initialize option defaults, start parse"""
        self._server = None
        self._portnum = 0
        self._logname = None
        self._logwrite = False
        self._parseCommandLine(commandLine)

    def _parseCommandLine(self, commandLine):
        """Parsing command line, reading switches and parameters"""
        options, arguments = getopt.getopt(commandLine, "S:p:l:")
        if not options:
            raise Exception("ARGS_MISSED")
        for option in options:
           if option[0] == "-S":
               self._server = option[1]
           if option[0] == "-p":
               self._portnum = int(option[1])
           if option[0] == "-l":
               self._logwrite = True
               self._logname = option[1]
        if not(self._server and self._portnum):
           raise Exception("ARGS_MISSED")  
      

2.3.2 A _Debug osztály

A _Debug egy általánosan használható cucc, létrejöttekor megkapja azt az Options példányt amiből ki tudja olvasni a debuglog nevét és elérési útját, valamint egy paramétert amely meghatározza, hogy a logba tegyen-e időbélyegeket. Egyetlen függvénye a WriteToLog aminek segítségvel logolhatjuk a programunk működését.

  
class _Debug:
    """Debugger class"""

    def __init__(self, options, timewrite):
        """Open and append logfile for debugging"""
        self._timewrite = timewrite
        self._options = options

    def WriteToLog(self, text):
        """Write a debug string to logfile"""
        if self._options._logwrite:
            self._logfile = open(self._options._logname, "a")
            if self._timewrite:
               self._logfile.write(time.strftime('%X %x %Z')+" > ")
            self._logfile.write(text)
            self._logfile.close()  
      

2.3.3 A _TrafficLight osztály

A _TrafficLight egy adattároló objektum, itt vannak az időszekvencia táblák, illetve egy olyan dictionary ami a kimeneti szimbólumokat lefordítja a CI4A driver által értelmezhető stringekre. Egyetlen függvénye a getoutputdata ami az üzemmódnév és a timekód alapján a megfelelő táblázatból kiveszi az aktuális értéket, és leforditja CI4A vezérlő stringgé. Ha a timekód nem szerepel a vizsgált táblázatban, akkor visszatérési értéke DO_NOTHING, ebből fogja tudni a WORKING, hogy ezzel semmit se kell csinálnia.

  
class _TrafficLight:
    """Traffic light data and statechange times"""      
    
    def __init__(self, Debug):
        self.Debug = Debug

        self.OutputData = \
        {
            "N|N"  : "S0OB01V000",
            "R|G"  : "S0OB01V033",
            "R|Y"  : "S0OB01V034",
            "R|R"  : "S0OB01V036",
            "RY|R" : "S0OB01V052",
            "G|R"  : "S0OB01V012",
            "Y|R"  : "S0OB01V020",
            "R|RY" : "S0OB01V038",
            "Y|Y"  : "S0OB01V018"
        }

        self.Normal = \
        {
            0  : "R|G",
            10 : "R|Y",
            12 : "R|R",
            13 : "RY|R",
            14 : "G|R",
            25 : "Y|R",
            26 : "R|R",
            27 : "R|RY" 
        }

        self.Semi = \
        {
            0  : "N|N",
            1  : "Y|Y"
        }

        self.Off = \
        {
            0  : "N|N",
            1  : "N|N"
        }

    def getoutputdata(self, TCRindex, Mode):
        dict = getattr(self, Mode)
        return self.OutputData.get(dict.get(TCRindex, "NOTHING"),"DO_NOTHING")
        

2.3.4 A _WorkingThread osztály

A _WorkingThread osztály mint ahogy a neve is sejteti A thread.Threading-ból származik, tehát önálló szálként fog futni. Indulásakor megkap egy-egy példányt a már létrehozott TCR, CI4AComm, TrafficLight és Debug objektumokból, ezekből belső saját változókat csinál az __init__-jében. Mivel ő thread, van egy run() függvénye, ami akkor indul amikor meghivjuk az osztály start()-ját. Ez aztán addig fut, amig RunFlag-et valaki False-ra nem állítja. Minden körben frissíti az Action változóját amibe az aktuális timekód alapján a CI4A vezérlő string kerül. Amennyiben az érték nem DO_NOTHING akkor azt átadja a CI4AComm-nak továbbításra. Azért, hogy ne generáljunk állandó netforgalmat az Actiont beleírjuk a továbbítás után a self.Action-ba. Mivel ezeknek a nem egyezősége is a küldés feltétele az érvényes CI4A string mellett, így csak változás esetén kerül az adat a CI4AComm-ba. Az adatküldés mellett a CI4AComm-ba érkező adatokat is pollozzuk a self.CI4AComm.getinputflag() segítségével. Itt a beérkezett adatoknak megfelelően átállítjuk az üzemmódot. Itt jön a képbe a D mikrokapcsoló, aminek megnyomására keletkezik a S0IB01V008\r vezérlő string, ami ezt a threadet, majd a főprogramot is leállítja, ezáltal a program normál kilépéséhez vezet.
Az osztály stop() függvénye szolgál a thread futásának megállítására. A setmode() pedig átprogramozza timekódgenerátor reload értékét az üzemmódoknak megfelelően.

  
class _WorkingThread(threading.Thread):

    def __init__(self, TCR, TrafficLight, CI4AComm, Debug):
        threading.Thread.__init__(self)
        self.RunFlag = True
        self.Mode = 'Off'
        self.TCR = TCR
        self.TrafficLight = TrafficLight 
        self.CI4AComm = CI4AComm
        self.Debug = Debug
        self.Action = ''  
        self.Debug.WriteToLog("working thread started\n")

    def run(self):
        Input = ''
        while self.RunFlag:
            Action = self.TrafficLight.getoutputdata(self.TCR.gettimecode(), self.Mode)
            if (Action != "DO_NOTHING") and (Action != self.Action):
                self.Debug.WriteToLog("sended command to CI4A: %s\n"%Action)
                self.CI4AComm.send(Action+'\r')
                self.Action = Action 
            if self.CI4AComm.getinputflag():
                Input = self.CI4AComm.getinputdata()
                self.CI4AComm.clearinputflag()
                self.Debug.WriteToLog('received command from CI4A: %s\n'%Input)
                if Input == 'S0IB01V001\r':
                    self.setmode('Normal')                
                if Input == 'S0IB01V002\r':
                    self.setmode('Semi')                
                if Input == 'S0IB01V004\r':
                    self.setmode('Off')                
                if Input == 'S0IB01V008\r':
                    self.RunFlag = False
            time.sleep(0.1)

    def stop(self):
        self.RunFlag = False

    def setmode(self, Mode):
        self.Mode = Mode
        self.Debug.WriteToLog("change mode to %s\n"%Mode)
        if self.Mode == 'Normal':
            self.TCR.changereload(29)
        if (self.Mode == 'Semi') or (self.Mode == 'Off'):
            self.TCR.changereload(2)  
      

2.3.5 A _TCRThread osztály

Ez maga a timekódgenerátor, ő is önálló szálban fog futni. A run()-jában szépen 1 másodpercenként növeli a self.TCR értéket mindaddig, amíg az el nem éri a reload szintet. Ekkor a számlálót lenullázza. A stop() függvénnyel ennek is megállítható a futása, a changereload()-al lehet módósítani a túlcsordulási értéket. A gettimecode() alkalmas a pillanatnyi timekód kiolvasására.

  
class _TCRThread(threading.Thread):

    def __init__(self, debug):
        threading.Thread.__init__(self)
        self.RunFlag = True
        self.TCR = 0       
        self.Reload = 100  
        self.Debug = debug
        self.Debug.WriteToLog("time code generator thread started\n")

    def run(self):
        while self.RunFlag:
            self.TCR += 1
            if self.Reload <= self.TCR:
                self.TCR = 0
            time.sleep(1) 

    def stop(self):
        self.RunFlag = False

    def changereload(self, Value):
        self.TCR = 0
        self.Reload = Value

    def gettimecode(self):
        return self.TCR  
      

2.3.6 A _CI4ACommThread osztály

Ez az osztály felelős a CI4A driverrel való kapcsolattartásért. Az __init__-jében megnyit egy klienssocketet ami a CI4A IO interfészére fog csatlakozni. A run()-ban van egy select, ami nézegeti a socket inputqueue-ját. Ha adat érkezik, akkor a self.InputFlag True értékre vált. A kommunikációt használó objektumnak a getinputflag()-en keresztül kell pollozznia az új adat érkezését, amit a getinputdata() segítségével olvashat ki. A kiolvasás után a self.InputFlag-et a clearinputflag() függvénnyel kötelező törölni, nehogy adatvesztés legyen. Az osztály send() függvénye való a CI4A drivernek való vezérlő stringek küldésére. A disconnect()-el a kapcsolatot lehet lezárni, amit normális esetben a self.RunFlag = True beállítása után a run() mint utolsó gaztettet végre is hajt.

  
class _CI4ACommThread(threading.Thread):

    def __init__(self, options, debug):
        threading.Thread.__init__(self)
        self.RunFlag = True
        self.InputFlag = False
        self.Comm = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
        try:
            self.Comm.connect((options._server, options._portnum))
        except:
            debug.WriteToLog("CI4A server not available on %s:%d\n"%(options._server, options._portnum))
            debug.WriteToLog("tlctrl stopped with error\n\n")
            raise Exception("ERROR_IN_LOG")
        debug.WriteToLog("CI4A communication thread started\n")
    
    def run(self):
        while self.RunFlag:
            i, o, e = select.select([self.Comm], [], [], 0.01) 
            for x in i:
                self.Input = x.recv(20)
                if self.Input:
                    self.InputFlag = True 
        self.Comm.close()

    def stop(self):
        self.RunFlag = False

    def send(self, text):
        self.Comm.send(text)      
    
    def getinputflag(self):
        return(self.InputFlag)

    def clearinputflag(self):
        self.InputFlag = False 

    def getinputdata(self):
        return(self.Input)

    def disconnect(self):
         self.Comm.disconnect()  
      

2.3.7 A főprogram

A program indításakor a __main__-ban leírt kód fog lefutni. Először is megpróbálja értelmezni a parancssorban megadott opciókat, ha ez sikerül, akkor létrehoz egy egy példányt a _Debug, a _TrafficLight, a _CI4ACommThread, a _TCRThread, a _WorkingThread osztályokból, és a thread-eseket el is indítja. A főszál kilépési feltétele a WORKING futásának megállása. Ha a WORKING megállt, akkor az összes többi thread-et is megállítja, majd kilép. Kivételek fellépése esetén a kivételkezelő ág fog futni. Ez megvizsgálja, hogy paramétermegadási hiba van-e, ekkor kiirja a program docstringjét (lásd a letölthető forráskódban) és hibával kilép. Ha nem paramétermegadási hiba van, akkor kiirja, hogy a program megállt és a további információkat a hibalogban kell keresni.

  
if __name__ == "__main__":
    """Main program"""
 
    exitCode = 0
    try:    
        Options = _Options(sys.argv[1:])
        Debug = _Debug(Options, True)
        Debug.WriteToLog('tlctrl started\n')        
        TrafficLight = _TrafficLight(Debug)
        CI4AComm = _CI4ACommThread(Options, Debug) 
        CI4AComm.start() 
        TCR = _TCRThread(Debug)
        TCR.start()
        WORKING = _WorkingThread(TCR, TrafficLight, CI4AComm, Debug)
        WORKING.start()
        while WORKING.RunFlag:
            time.sleep(0.1)
        CI4AComm.stop()
        TCR.stop()
        Debug.WriteToLog('tlctrl stopped normally\n\n')
    except Exception, e:
        if "ARGS_MISSED" in e.args:
            exitCode = 1
            print
            print __doc__		
            print
        if  "ERROR_IN_LOG" in e.args:
            exitCode = 2
            print "Error, tlctrl stopped, see debug log for details!"    
    sys.exit(exitCode)   
      

2.4 Az eredmény

Az eredmény az természetesen egy másik működő ketyere, aminek mégjobban lehet örülni, hiszen a vezérlés Tökön van. A működést pedig a következő videó szemlélteti:

Közlekedési lámpa videó (wmv 11.6MB)

Ha kód megtetszett, ezt is megtalálod a letöltéseknél tlctrl.py néven.


3. További példák

Az előző két példa egyszerű, kevés vezérelt hardver elemet tartalmazó, a köznapi életből vett példa volt. De ezeket az egyszerű feladatokat kidolgozni két nap, leírni meg cirka két hét alatt sikerült. Ezért egyelőre tessék ezzel megelégedni. A saját interfészemet a mikrokontroller égető robot vezérlésén túl sokmindenre tervezem még használni, és remélem lesz időm majd azokból az ötletekből is egynéhányat itt példaként közzétenni.


VISSZA