C# - BackgroundWorker és SerialPort használata egyszerre

C# - BackgroundWorker és SerialPort használata egyszerre
2014-04-02T09:28:25+02:00
2014-04-12T15:56:20+02:00
2022-11-30T22:20:34+01:00
makrodom
Sziasztok!
Elég kezdő vagyok a témában, de szeretnék megoldani egy feladatot. Első ránézésre nem tűnt túl bonyolultnak, de nehézségekbe ütköztem.
A feladat a következő: WinForm-ban készítettem egy kisebb adatbáziskezelést (MS Access), tudok hozzáadni, törölni, rákeresni személyekre ID alapján. Ezzel a részével nincs gond. Viszont a számítógép össze van kötve virtuális soros porton keresztül két olvasóval. A probléma itt kezdődik: egy végtelen ciklusban meg kell szólítanom mindkét olvasót egymás után, amik visszaküldenek egy válaszüzenetet, és ha a válaszüzenetben megjelenik egy személy ID-je is, akkor szünetel a szólítgatás, és ki kell értékelni azt, hogy szerepel-e az adott ID az adatbázisban vagy sem. Ha igen akkor visszaküld egy OK-ot az adott olvasónak és vár max. 10s-ig egy válaszára. Amennyiben érkezik válasz vagy az idő lejár, folytatódik a kérdezgetés.

A gondom az, hogy nem tudom megoldani párhuzamosan az adatbázisban való manipulálást és a soros port kommunikációs részét. Több Form-ot használok, próbáltam külön a BackgroundWorker-t használni, valamint a SerialPort-ot, hogy megértsem őket, azok valamelyest mennek, de nem tökéletesek külön-külön sem, ebben kérnék segítséget.

A forráskódom a következő, amelyben próbáltam a SorosPort-ot és a BackgroundWorkert ötvözni:

using System; using System.Collections.Generic; using System.ComponentModel; using System.Data; using System.Drawing; using System.Text; using System.Windows.Forms; using System.Threading; namespace SorosPortProba { public partial class Form1 : Form { int i, x = 0, y = 0; string RxString; BackgroundWorker bgw = new BackgroundWorker(); public Form1() { InitializeComponent(); serialPort1.DataReceived += serialPort1_DataReceived; timer1.Enabled = true; } private void buttonStart_Click(object sender, EventArgs e) { i = 0; serialPort1.PortName = textBox2.Text; serialPort1.BaudRate = 9600; serialPort1.Open(); if (serialPort1.IsOpen) { buttonStart.Enabled = false; buttonStop.Enabled = true; textBox1.ReadOnly = false; } } private void buttonStop_Click(object sender, EventArgs e) { bgw.WorkerReportsProgress = true; bgw.DoWork += new DoWorkEventHandler(bgw_DoWork); bgw.ProgressChanged += new ProgressChangedEventHandler(bgw_ProgressChanged); bgw.RunWorkerAsync(); if (serialPort1.IsOpen) { serialPort1.Close(); buttonStart.Enabled = true; buttonStop.Enabled = false; textBox1.ReadOnly = true; i = 0; } } private void Form1_FormClosing(object sender, FormClosingEventArgs e) { if (serialPort1.IsOpen) serialPort1.Close(); } private void textBox1_KeyPress(object sender, KeyPressEventArgs e) { if (!serialPort1.IsOpen) return; char[] buff = new char[1]; buff[0] = e.KeyChar; serialPort1.Write(buff, 0, 1); e.Handled = true; } private void DisplayText(object sender, EventArgs e) { textBox1.AppendText(RxString); } private void serialPort1_DataReceived(object sender, System.IO.Ports.SerialDataReceivedEventArgs e) { RxString = serialPort1.ReadExisting(); this.Invoke(new EventHandler(DisplayText)); } private void buttonClear_Click(object sender, EventArgs e) { textBox1.Text = ""; textBox3.Text = ""; } private void buttonSend_Click(object sender, EventArgs e) { if (serialPort1.IsOpen) { serialPort1.Write(textBox3.Text); } } private void timer1_Tick(object sender, EventArgs e) { if (serialPort1.IsOpen && i == 0) { serialPort1.Write(textBox3.Text); i = 1; } if (RxString == "1") { i = 0; x = 1; RxString = ""; } } public void bgw_DoWork(object sender, DoWorkEventArgs e) { BackgroundWorker bgw = sender as BackgroundWorker; if (x == 0) { Thread.Sleep(2000); if (y == 0) { bgw.ReportProgress(y); } } } public void bgw_ProgressChanged(object sender, ProgressChangedEventArgs e) { textBox1.Text += e.ProgressPercentage + "atadva"; } }

}
Mutasd a teljes hozzászólást!
Helló!

Ez a feladat tipikus esete annak, ahol az egyes részfeladatok egyszerűek, de az egészet megbízhatóan működésre bírni már nem annyira triviális. Több különböző területet kell jól ismerni hozzá, nem kezdőknek való.

Kezdjük a soros porttal.

Leírásod alapján ez a program automata üzeműnek tűnik, egyáltalán nem igényel felhasználói beavatkozást. A kódodban nem teljesen világos, hogy miért küldöd a TextBox-ba írt adatot a soros portra (a leírásod nem erről szól).

Több különböző módszer van, hogy adatot olvassunk, a te esetedben az esemény alapú olvasás használata nem ajánlott. Ez aszinkron használat esetén célszerű, azaz, amikor az eszközöd magától szólít meg téged. A te eseted nem ez.

Leírásod alapján jobban jársz, ha Write vagy WriteLine stb. metódussal elküldöd a kérést az eszközödnek, és utána Read stb. metódussal timeout-ig vársz a válaszra.

Ezek szinkron módon működnek, tehát addig blokkolják a száladat, amíg választ nem kapsz vagy timeout nem történik.

Mivel a szálad blokkolódik, ezért az egész kommunikációt célszerű külön szálba helyezni. A BackgroundWorker nagyon hasznos egyszerűbb szálkezelési feladatokra, azonban ebben az esetben a feladat túlnő a képességein (biztosan meg lehetne oldani így is, de nem ajánlott). Célszerű egy Thread-et létrehozni erre a célra, abba elhelyezed a végtelen ciklust, ami időről-időre kérdezgeti az eszközödet.

A szálkezelés nem egyszerű terület, és meghaladja a topic kereteit, ajánlom ezt a leírást, hogy képbe kerülj, mi hogy működik.

Ezen a kommunikációs szálon tudod elhelyezni az adatbázis-műveletet is, azaz, ha visszajön az ID az eszközről, le tudod kérdezni az adatbázisban ill. egyéb módon tudod kezelni.

Célszerű továbbá az egyes részeket jobban szétszedni, egy ilyen komplexitású feladatot spagetti-kódban megírni (ahogy te csináltad) elég rossz ötlet. Külön osztály végezze a kommunikációt, az adatbázis-kezelést stb. A végén a Form-od legfeljebb kereted az az egésznek, illetve ha szükséges, megjeleníthet információt a folyamatokról.
Mutasd a teljes hozzászólást!

  • Most néztem, hogy ahogy módosítgattam a kódot jobbra-balra és bemásoltam ide, a buttonStop_Click-be raktam be, nos ez téves, mert a buttonStart_Click-be kell, lennie: 

    bgw.WorkerReportsProgress = true; bgw.DoWork += new DoWorkEventHandler(bgw_DoWork); bgw.ProgressChanged += new ProgressChangedEventHandler(bgw_ProgressChanged); bgw.RunWorkerAsync();

    A válaszokat előre is köszönöm!
    Üdv!
    Mutasd a teljes hozzászólást!
  • Helló!

    Ez a feladat tipikus esete annak, ahol az egyes részfeladatok egyszerűek, de az egészet megbízhatóan működésre bírni már nem annyira triviális. Több különböző területet kell jól ismerni hozzá, nem kezdőknek való.

    Kezdjük a soros porttal.

    Leírásod alapján ez a program automata üzeműnek tűnik, egyáltalán nem igényel felhasználói beavatkozást. A kódodban nem teljesen világos, hogy miért küldöd a TextBox-ba írt adatot a soros portra (a leírásod nem erről szól).

    Több különböző módszer van, hogy adatot olvassunk, a te esetedben az esemény alapú olvasás használata nem ajánlott. Ez aszinkron használat esetén célszerű, azaz, amikor az eszközöd magától szólít meg téged. A te eseted nem ez.

    Leírásod alapján jobban jársz, ha Write vagy WriteLine stb. metódussal elküldöd a kérést az eszközödnek, és utána Read stb. metódussal timeout-ig vársz a válaszra.

    Ezek szinkron módon működnek, tehát addig blokkolják a száladat, amíg választ nem kapsz vagy timeout nem történik.

    Mivel a szálad blokkolódik, ezért az egész kommunikációt célszerű külön szálba helyezni. A BackgroundWorker nagyon hasznos egyszerűbb szálkezelési feladatokra, azonban ebben az esetben a feladat túlnő a képességein (biztosan meg lehetne oldani így is, de nem ajánlott). Célszerű egy Thread-et létrehozni erre a célra, abba elhelyezed a végtelen ciklust, ami időről-időre kérdezgeti az eszközödet.

    A szálkezelés nem egyszerű terület, és meghaladja a topic kereteit, ajánlom ezt a leírást, hogy képbe kerülj, mi hogy működik.

    Ezen a kommunikációs szálon tudod elhelyezni az adatbázis-műveletet is, azaz, ha visszajön az ID az eszközről, le tudod kérdezni az adatbázisban ill. egyéb módon tudod kezelni.

    Célszerű továbbá az egyes részeket jobban szétszedni, egy ilyen komplexitású feladatot spagetti-kódban megírni (ahogy te csináltad) elég rossz ötlet. Külön osztály végezze a kommunikációt, az adatbázis-kezelést stb. A végén a Form-od legfeljebb kereted az az egésznek, illetve ha szükséges, megjeleníthet információt a folyamatokról.
    Mutasd a teljes hozzászólást!
  • Hello!
    Köszi a gyors választ!
    Megpróbáltam megcsinálni ahogyan írtad, érdekel jól indultam-e neki?!

    using System; using System.Collections.Generic; using System.ComponentModel; using System.Data; using System.Drawing; using System.Linq; using System.Text; using System.Windows.Forms; using System.Threading; namespace LoginSystem { public partial class LoginForm : Form { public LoginForm() { InitializeComponent(); } static SerialComm m_serialcomm = new SerialComm(); static Tablakezeles m_tablakezeles = new Tablakezeles(); Thread SerialComm = new Thread(new ThreadStart(m_serialcomm.SerialcommThread)); Thread Tablakezeles = new Thread(new ThreadStart(m_tablakezeles.TablakezelesThread)); private void Form1_Load(object sender, EventArgs e) { SerialComm.Start(); Tablakezeles.Start(); } private void btnLogin_Click(object sender, EventArgs e) { } private void LoginForm_FormClosing(object sender, FormClosingEventArgs e) { SerialComm.Abort(); Tablakezeles.Abort(); }
    }
    }

    using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Windows.Forms; using System.Threading; namespace LoginSystem { class Tablakezeles { public void TablakezelesThread() { MessageBox.Show("Ez a Tablakezeles szal..."); }
    }
    }

    using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Windows.Forms; using System.Threading; using System.IO.Ports; namespace LoginSystem { class SerialComm { string RxString = "", OlvasoAllapot1 = "", OlvasoAllapot2 = ""; SerialPort serialPort1 = new SerialPort(); public void SerialcommThread() { MessageBox.Show("Ez a SerialComm szal..."); serialPort1.PortName = "COM8"; serialPort1.BaudRate = 9600; serialPort1.Open(); for (int n = 2; n > 1;n++ ) { if (serialPort1.IsOpen) { int x; for (x = 0; x < 3; x++) { serialPort1.Write("#BCQ"); Thread.Sleep(260); RxString = serialPort1.ReadExisting(); if (RxString == "#ANN") { OlvasoAllapot1 = "Rendben"; RxString = ""; break; } } if (x == 3){ MessageBox.Show("Kommunikacios hiba"); break; } int y; for (y = 0; y < 3; y++) { serialPort1.Write("#CCQ"); Thread.Sleep(260); RxString = serialPort1.ReadExisting(); if (RxString == "#ANN") { OlvasoAllapot2 = "Rendben"; RxString = ""; break; } } if (y == 3){ MessageBox.Show("Kommunikacios hiba"); break; } } if (n > 100) { n = 2; } } } public string SerialcommString1(){return OlvasoAllapot1;} public string SerialcommString2(){return OlvasoAllapot2;} } }
    Mutasd a teljes hozzászólást!
  • Helló!

    Alapvetően igen, néhány gondolat:

    1. Vigyázz a static member-ekkel, csak akkor legyen static, ha indokolt. Inkább példány legyen, mivel későbbi programbővítés során nem várt bugokat viszel a rendszerbe, ha nem teljesen thread-safe módon írod meg őket.

    2. A táblakezelést nem feltétlenül kell külön szálon futtatni: természetesen legyen külön osztályban, de a kommunikációs szálon használd azt. Nyilván meg lehet így is csinálni, ahogy írod, de csak feleslegesen bonyolítja a dolgot, hiszen szinkronizálnod kell a két külön száladat (kommunikáció + adatelérés). Bugok melegágya: csak akkor csináld, ha feltétlenül szükséges.

    3. Thread.Abort(): lehet így is használni, de szerintem ez elég erőszakos mód. Épp elég feladat rendesen lekezelni, ha elvágják a programod torkát, hogy az összes unmanaged erőforrás (adatbázis-elérés, soros port stb.) rendesen le legyen zárva.

    Én úgy csinálnám inkább, hogy a végtelen ciklus valójában egy:

    do { ... } while(running);


    ahol a running egy bool, amit kívülről false-ra tudsz állítani. Amikor a kód kilép ebből a ciklusból, szépen lezárod az adatbázist és a soros portot, és utána a szál magától kilép.

    4. A funkcionalitást megvalósító osztályaidban (pl. SerialComm osztály) ne használj GUI-specifikus kódot (pl. MessageBox)!

    Ha probléma van, akkor ezek dobjanak csak Exception-t, a hibát egy másik rétegben kezeld le! Feladattól függően a hibát ki lehet íratni vagy log-olni, de ezt ne a feladatot végző osztályod tegye!

    Az egyes osztályok optimális esetben egyféle feladatot látnak el, ez a Single responsibility principle, ami a SOLID egyik eleme.

    Tehát a kommunikációért felelős osztályod csak azzal foglalkozzon, hogy kommunikáljon, a GUI-ért felelős csak a GUI-val, a log-olásért felelős csak a log-olással és így tovább. Így ha bármelyiken változtatni kell (pl. holnaptól küldje el a hibát e-mailben, vagy mostantól adatbázisba írja a MessageBox helyett), kevesebb eséllyel rontasz el valamit, hiszen a kommunikációs részhez hozzá sem kell nyúlni, ha a hibakezelésen változtatsz.

    5. Port írás-olvasás:

    serialPort1.Write("#BCQ"); Thread.Sleep(260); RxString = serialPort1.ReadExisting();


    a. Szerintem ne hard-code-old így a paramétereket (soros port adatai, parancsok stb.), ezeket tárold külön, és csak használd fel a programodban. Holnap megváltozik az eszközöd interface-e, és nem mindegy, hogy hol kell turkálni, hogy módosítsd a programodat.

    b. A Thread.Sleep() a ReadExisting()-gel együtt nem túl robusztus megoldás. Valami elakad a rendszerben (pl. víruskereső pörögni kezd), és kevés lesz a 260ms, azaz az eszközöd kommunikációs hibát fog dobni, miközben ott van és működik.

    Ehelyett használd (Thread.Sleep() nélkül) a Read() (stb., linkek fenn) metódusokat, mivel alapvető különbség, hogy a ReadExisting() kiolvassa a buffer-t és azonnal továbblép (akár van benne valami, akár még nincs), a Read() pedig blokkolódik, amíg nincs a bufferben valami (azaz amíg nincs válasz).

    Tehát ha 5 másodpercig tart, amíg válasz érkezik, akkor addig vár, ha 100ms-ig, akkor addig. Nincs bizonytalan várakozási tényező, amit jelen kódban a Sleep-pel iktatsz be.

    Az MSDN-en a SerialPort metódusai részletesen dokumentálva vannak, célszerű tanulmányozni őket. Le van írva, hogy melyik pontosan hogyan viselkedik (blokkolódik, továbblép, másik szálon fut stb.), tehát mindaz, amit ebben a topicban én kivonatként összefoglaltam, az onnan származik. Szerintem olvasd át őket alaposan, könnyen lehet, hogy felfedezel köztük olyat, ami még hasznosabb számodra.
    Mutasd a teljes hozzászólást!
  • Hello!
    Ismét köszi a gyors választ!
    Próbáltam úgy csinálni ahogy lett írva! Ez az eredménye:

    using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Windows.Forms; using System.Threading; using System.IO.Ports; using System.Timers; namespace Beleptetorendszer2014 { class SerialComm { string RxString = ""; string RxNev = "", RxMS1 = "", RxMS2 = "", RxData = ""; string OlvasoAllapot1 = "", OlvasoAllapot2 = ""; bool running = true; bool valto = false; bool adatErkezett = false; SerialPort serialPort1 = new SerialPort(); public void SerialcommThread() { try { serialPort1.PortName = "COM8"; serialPort1.BaudRate = 9600; serialPort1.Open(); if (serialPort1.IsOpen) { do { byte[] cmdByteArray = new byte[1]; serialPort1.DiscardInBuffer(); serialPort1.DiscardOutBuffer(); if (valto == false) { serialPort1.Write("#BCQ"); } else if (valto == true) { serialPort1.Write("#CCQ"); } try { while ((serialPort1.BytesToRead == 0)) { Application.DoEvents(); if (running == false) { break; } } if (serialPort1.BytesToRead > 0) { RxString = serialPort1.ReadExisting(); if (RxString.Length >= 4) { RxNev = RxString[0].ToString() + RxString[1].ToString(); RxMS1 = RxString[2].ToString(); RxMS2 = RxString[3].ToString(); } if (RxNev == "#A") { RxNev = ""; if (valto == false) { OlvasoAllapot1 = "Komm. OK"; } else if (valto == true) { OlvasoAllapot2 = "Komm. OK"; } if (RxMS1 != "N" || RxMS2 != "N") { if (RxString.Length >= 8) { RxData = RxString[4].ToString() + RxString[5].ToString() + RxString[6].ToString() + RxString[7].ToString(); } adatErkezett = true; RxMS1 = ""; RxMS2 = ""; RxString = ""; } else if (RxMS1 == "N" && RxMS2 == "N") { RxString = ""; } if (valto == false) { valto = true; } else if (valto == true) { valto = false; } } } } catch (TimeoutException) { MessageBox.Show("Az idot tullepte"); } serialPort1.DiscardInBuffer(); serialPort1.DiscardOutBuffer(); } while (running); } if (running == false) { //serialPort1.Close(); } } catch (Exception ex) { MessageBox.Show("Can't open COM port!"); } } public string SerialcommString1() { return OlvasoAllapot1; } public string SerialcommString2() { return OlvasoAllapot2; } public string Adatkuldes() { return RxData; } public bool SorosPortSzalFut() { return running; } public bool AdatErtekeles() { return adatErkezett; } public void AdatErtekelesKesz(bool ertekeles){adatErkezett = ertekeles; } public void Allapot(bool runn) { running = runn; } } }
    Itt végtelen ciklusban  (amig a running false-ra nem vált pörög a ciklus) kérdezgeti a szolgákat és a válasz ideje nem lényeges... Használom a ReadExisting() megódust továbbra is használom, de csak miután valóban érkezett is adat. A MessageBox-okat csak azért használom,hogy lássam hol tart a program... viszont a catch-ben már ott hagyhatom? Ott úgyis hibára leáll a program...

    Még csak annyi lenne a problémám, hogy nem tudok rájönni hogyan tudom használni ezeket: 

    serialPort1.WriteTimeout = 10000; serialPort1.ReadTimeout = 10000;
    Hova kell beillesztenem, és hogyan tudom az idő lejártával értelmezni (kijeleztetni)?
    Tulajdonképpen az írás az nem is annyira fontos ennél a példánál mivel csak akkor írok valamit, ha már nyitva van a Port, és az olvasással tudom értelmezni, hogy a másik Port-on érkezik-e időben adat.
    Azt szeretném megoldani, hogyha egy adott időn belül nem érkezik adat akkor hibát dobjon.
    Még egy kérdés lenne: Hogyan tudom szüneteltetni a ciklust, amit küldözgeti a kéréseket a szolgák felé? (Pl. ha érkezik a szolgától adat, kiküldök még egy kérvényt és várjon 10s-t)...

    Üdv!
    Mutasd a teljes hozzászólást!
  • Helló!

    Elfogadtam a válaszodat megoldásként, ez a része nagyon jól sikerült és jól működik, még csak a soros port olvasásával vannak gondjaim!

    Üdv!
    Mutasd a teljes hozzászólást!
Tetszett amit olvastál? Szeretnél a jövőben is értesülni a hasonló érdekességekről?
abcd