Hur hanterar man inläsning av USB med hjälp av Boost Asio i C++?

Permalänk

Hur hanterar man inläsning av USB med hjälp av Boost Asio i C++?

Jag använder mig av Boost Asio för att läsa och skriva via USB. Att skriva...inga problem.
Men att läsa är något lite svårare.

Kodmässigt är det inte svårare, utan det handlar om att pricka rätt på tiden.
Det jag vill göra är att jag vill skicka data visa USB från min dator, till en enhet. Enheten läser meddelandet och skickar genast tillbaka ett svar till min dator.

Om inte min dator hinner läsa meddelandet, så går meddelandet förlorat. Och hur ska jag lösa detta?

Jag har tidigare använt mig av trådar, men jag tror att trådar kanske inte riktigt var det mest skonsammaste för USB porten då man fick ha mycket try-catch för att fånga upp alla meddelanden. Det var dessutom mycket trixande med globala variabler för att stänga av tråden på ett snyggt sätt. Så jag tyckte lösningen verkade vara en ful-lösning.

Det alternativ jag använder nu är att

#include <iostream> #include <boost/asio.hpp> void handleRead(const boost::system::error_code& error, std::size_t bytesTransferred, std::vector<char>& buffer) { if (!error) { // Extrahera meddelandet från bufferten std::string message(buffer.begin(), buffer.begin() + bytesTransferred); // Hantera meddelandet std::cout << "Mottaget meddelande: " << message << std::endl; } else { // Hantera fel std::cerr << "Fel vid mottagning av meddelande: " << error.message() << std::endl; } } int main() { boost::asio::io_context io; boost::asio::serial_port serialPort(io); // Öppna seriell port serialPort.open("/dev/ttyUSB0"); // Ställ in seriella portinställningar serialPort.set_option(boost::asio::serial_port_base::baud_rate(9600)); serialPort.set_option(boost::asio::serial_port_base::character_size(8)); serialPort.set_option(boost::asio::serial_port_base::parity(boost::asio::serial_port_base::parity::none)); serialPort.set_option(boost::asio::serial_port_base::stop_bits(boost::asio::serial_port_base::stop_bits::one)); serialPort.set_option(boost::asio::serial_port_base::flow_control(boost::asio::serial_port_base::flow_control::none)); // Skapa en buffert för att lagra inkommande data std::vector<char> buffer(1024); // Skapa timer int timeOutMilliseconds = 1000; boost::asio::steady_timer timer(io, std::chrono::milliseconds(timeOutMilliseconds)); // Läs in meddelanden från seriell port serialPort.async_read_some(boost::asio::buffer(buffer), [&](const boost::system::error_code& error, std::size_t bytesTransferred) { handleRead(error, bytesTransferred, buffer); }); // Start asynkron timer timer.async_wait([&](const boost::system::error_code& errorCode) { // Timer löper ut, gör något här om det behövs }); // Kör event-loop io.run(); return 0; }

Trots detta, så fungerar det ändå inte. Det är som att meddelandet hinner att försvinna innan jag läser det.
Finns det några bättre sätt hur man ska bemästra dessa funktioner?

Så frågan är om man helt enkelt skulle kunna använda sig av en while loop som bara körs hela tiden?
Tänk om halva meddelandet läses bara?

Eller är tanken att jag ska
1. Först starta en läs process
2. Skicka mitt meddelande
3. Läs datan som har kommit tillbaka

Är det så Boost Asio ska fungera?

Permalänk
Hedersmedlem

Du vill nog typiskt anropa
serialPort.async_read_some
igen sist i handleRead. Serieporten ger strömmande data, så man måste ha någon metod för att veta när man har fått all data. För textbaserade protokoll kan man till exempel ofta fortsätta läsa tills man får en radbrytning och då skicka meddelandet vidare.

Permalänk
Skrivet av Elgot:

Du vill nog typiskt anropa
serialPort.async_read_some
igen sist i handleRead. Serieporten ger strömmande data, så man måste ha någon metod för att veta när man har fått all data. För textbaserade protokoll kan man till exempel ofta fortsätta läsa tills man får en radbrytning och då skicka meddelandet vidare.

Själva tidsfunktionen fungerar som den ska göra.
Så när jag anropar "io.run()" och jag har satt time out till 10000 millisekunder. Då tar det 10 sekunder innan programmet fortsätter, om den inte får in någon data.

Så allt är bra där.
Problemet är att jag måste koppla min async_read_some till min io.run() funktion.

Permalänk

Nu vet jag problemet!

Först så skapar jag min timer.
Sedan skapar jag min async_read_some. I detta fall så läser den också helt i tomluften.
Men när jag anropar io.run() så inväntar den ett svar i X millisekunder.
När X millisekunder har förbruktats, så går den vidare och skickar meddelande över USB porten till en enhet som skickar data tillbaka.

Där har vi problemet.
Jag måste alltså först skicka data, sedan starta io.run().

Men tänk om min dator kanske inte hinner starta io.run()?

Permalänk
Hedersmedlem
Skrivet av heretic16:

Själva tidsfunktionen fungerar som den ska göra.
Så när jag anropar "io.run()" och jag har satt time out till 10000 millisekunder. Då tar det 10 sekunder innan programmet fortsätter, om den inte får in någon data.

Så allt är bra där.
Problemet är att jag måste koppla min async_read_some till min io.run() funktion.

Nja, async_read_some läser just lite data från porten och returnerar sedan. Det kan finnas data kvar (eller komma in mer data senare). Du behöver inte ha en timeout (om du inte är ute just efter att avbryta efter en viss tid). Det går inte att ge async_read_some mera tid.

Permalänk
Hedersmedlem
Skrivet av heretic16:

Jag måste alltså först skicka data, sedan starta io.run().

Men tänk om min dator kanske inte hinner starta io.run()?

Du kan börja läsa från porten direkt, men buffra informationen och läs igen. Leta sedan efter radbrytningar och gör vad du nu vill göra.

Permalänk
Skrivet av Elgot:

Nja, async_read_some läser just lite data från porten och returnerar sedan. Det kan finnas data kvar (eller komma in mer data senare). Du behöver inte ha en timeout (om du inte är ute just efter att avbryta efter en viss tid). Det går inte att ge async_read_some mera tid.

Ja, om den skickar vi säger 1000 bytes och jag läser när det är 500 bytes kvar. Då har jag tappat 500 bytes.
Jag vill avbryta efter 2 sekunder.

async_read_some läser all data som finns på porten när async_read_some är vid porten. Om det kommer in 200 bytes data direkt efter, den missar jag också.

Jag skulle kunna ha blockerande läsning + tråd, men det blir inte så snyggt. Känns som man fulprogrammerar då.

Skrivet av Elgot:

Du kan börja läsa från porten direkt, men buffra informationen och läs igen. Leta sedan efter radbrytningar och gör vad du nu vill göra.

Kan man göra det med Boost Asio? Eller måste man ned på djupet i Windows API?
Jag försöker ha så mycket oberoende för vilken plattform jag kör.

Permalänk
Hedersmedlem
Skrivet av heretic16:

async_read_some läser all data som finns på porten när async_read_some är vid porten.

Jag är inte säker, men jag undrar om den verkligen garanterar all data returneras.

Skrivet av heretic16:

Kan man göra det med Boost Asio? Eller måste man ned på djupet i Windows API?
Jag försöker ha så mycket oberoende för vilken plattform jag kör.

Jadå. Anropa bara async_read_some sist i handleRead (och du behöver ingen timer).

Permalänk
Skrivet av Elgot:

Jag är inte säker, men jag undrar om den verkligen garanterar all data returneras.
Jadå. Anropa bara async_read_some sist i handleRead (och du behöver ingen timer).

Så du menar att jag ska göra async_read_some rekrusiv?

Permalänk
Hedersmedlem
Skrivet av heretic16:

Så du menar att jag ska göra async_read_some rekrusiv?

Lite så (fast egentligen anropar man den en gång i taget, om och om igen).

Permalänk
Skrivet av Elgot:

Lite så (fast egentligen anropar man den en gång i taget, om och om igen).

Fast detta blir lite problem.
Om jag anropar start så förväntar jag att inom X tid så kommer jag får ett svar.
Så vill jag ha det.

Om jag rekrusivt anropar så kommer jag fastna där.

Permalänk
Hedersmedlem
Skrivet av heretic16:

Fast detta blir lite problem.
Om jag anropar start så förväntar jag att inom X tid så kommer jag får ett svar.
Så vill jag ha det.

Om jag rekrusivt anropar så kommer jag fastna där.

Du kommer ju successivt få in data i handleRead. Där kan du till exempel anropa någon funktion, döda någon flagga eller liknade för att signalera att något har hänt.

Permalänk
Hedersmedlem

Om man är ute efter något enklare kan man öka mottagningsbuffertens storlek, vänta en stund och sedan läsa (men det blir lite mer av en fullösning).

Permalänk
Skrivet av Elgot:

Du kommer ju successivt få in data i handleRead. Där kan du till exempel anropa någon funktion, döda någon flagga eller liknade för att signalera att något har hänt.

Fast jag har koll på hur mycket jag ska få tillbaka.

Permalänk
Skrivet av Elgot:

Om man är ute efter något enklare kan man öka mottagningsbuffertens storlek, vänta en stund och sedan läsa (men det blir lite mer av en fullösning).

När jag använder denna kod

// Create timer boost::asio::steady_timer timer(io, std::chrono::milliseconds(timeOutMilliseconds)); // Read process devicesCDC.at(port)->async_read_some(boost::asio::buffer(dataRX), readMessage); // Write process auto writeHandler = [&](const boost::system::error_code& errorCode, std::size_t bytesTransferred) {}; devicesCDC.at(port)->async_write_some(boost::asio::buffer(dataTX, size), writeHandler); // Start asynkron timer timer.async_wait(timeOut); // Run io.run();

Det som händer är att när jag skickar med "async_write_some" så hinner jag inte läsa något med "async_read_some".

Permalänk
Hedersmedlem
Skrivet av heretic16:

Fast jag har koll på hur mycket jag ska få tillbaka.

Om du vet exakt hur mycket du förväntar dig är async_read ett smidigare alternativ.

Annars kan du ju bygga ett system som kontinuerligt läser från porten, delar upp till meddelanden och på något sätt meddelar resten av programmet. På så sätt slipper du kanske tänka på detaljerna hela tiden.

Permalänk
Hedersmedlem
Skrivet av heretic16:

När jag använder denna kod

// Create timer boost::asio::steady_timer timer(io, std::chrono::milliseconds(timeOutMilliseconds)); // Read process devicesCDC.at(port)->async_read_some(boost::asio::buffer(dataRX), readMessage); // Write process auto writeHandler = [&](const boost::system::error_code& errorCode, std::size_t bytesTransferred) {}; devicesCDC.at(port)->async_write_some(boost::asio::buffer(dataTX, size), writeHandler); // Start asynkron timer timer.async_wait(timeOut); // Run io.run();

Det som händer är att när jag skickar med "async_write_some" så hinner jag inte läsa något med "async_read_some".

Du kan också köra io.run i en egen tråd om du inte vill blockera huvudtråden.

Permalänk
Skrivet av Elgot:

Om du vet exakt hur mycket du förväntar dig är async_read ett smidigare alternativ.

Annars kan du ju bygga ett system som kontinuerligt läser från porten, delar upp till meddelanden och på något sätt meddelar resten av programmet. På så sätt slipper du kanske tänka på detaljerna hela tiden.

Fast vi säger att om jag inte får ett meddelande efter X sekunder. Ja då fastnar jag hela tiden då read_some är blockerande.

Jag hade ett sådant system förut som läste USB porten hela tiden i en egen tråd. Det var bra, lite grötigt med trådar bara.

Skulle det inte fungera så att man kan avbryta en blockerande läsning?

Permalänk
Hedersmedlem
Skrivet av heretic16:

Jag hade ett sådant system förut som läste USB porten hela tiden i en egen tråd. Det var bra, lite grötigt med trådar bara.

Det enda du behöver ändra för att använda flera trådar här är att anropa run från en annan tråd. Det blir inte krångligare än såhär.

Permalänk
Skrivet av Elgot:

Det enda du behöver ändra för att använda flera trådar här är att anropa run från en annan tråd. Det blir inte krångligare än såhär.

Jag löste problemet.

1. Starta en tråd som läser med blockerande
2. Skicka meddelande
3. Vänta på svar. Om svar ej har kommit inom X millisekunder -> förstör tråden.
4. När svar kommer, spara det och sedan förstör tråden.

Man använder .join() för att "förstöra" en tråd.

Permalänk
Hedersmedlem
Skrivet av heretic16:

Jag löste problemet.

1. Starta en tråd som läser med blockerande
2. Skicka meddelande
3. Vänta på svar. Om svar ej har kommit inom X millisekunder -> förstör tråden.
4. När svar kommer, spara det och sedan förstör tråden.

Detta blir väl dock i praktiken som din första lösning (fast med explicit trådhantering)?

Skrivet av heretic16:

Man använder .join() för att "förstöra" en tråd.

Nja, join blockerar tills tråden har avslutats; den påverkar inte körningen och frigör inga resurser.

Permalänk
Skrivet av Elgot:

[quote postid="20100365" userid="120146" name="heretic16"]
Jag löste problemet.

1. Starta en tråd som läser med blockerande
2. Skicka meddelande
3. Vänta på svar. Om svar ej har kommit inom X millisekunder -> förstör tråden.
4. När svar kommer, spara det och sedan förstör tråden.
[quote postid="20100365" userid="120146" name="heretic16"]
Detta blir väl dock i praktiken som din första lösning (fast med explicit trådhantering)?
Nja, join blockerar tills tråden har avslutats; den påverkar inte körningen och frigör inga resurser.

Den blir nästan som min första lösning. Förta lösningen var att en tråd hela tiden körs och läser. Jag behöver inte denna resurs hela tiden.

Så hur dödar jag en tråd då om tråden har en t.ex. "inbyggd while-loop"?

Permalänk
Hedersmedlem
Skrivet av heretic16:

Så hur dödar jag en tråd då om tråden har en t.ex. "inbyggd while-loop"?

Bäst är att stoppa loop:en på något kontrollerat sätt. Genom att kontrollera om en running-variabel fortfarande är satt till exempel.
Lösningen med en timeout-klocka som du hade förut är inte heller så tokig.

Permalänk
Skrivet av Elgot:

Bäst är att stoppa loop:en på något kontrollerat sätt. Genom att kontrollera om en running-variabel fortfarande är satt till exempel.
Lösningen med en timeout-klocka som du hade förut är inte heller så tokig.

Jag kan ju använda mig av async_read_some i Boost asio. Den läser oavsett om data finns. Men då finns det risk att den kanske läser när jag har halva datat.

I exemplet nedan så använder jag read_some, vilket är blockerande.

std::atomic<bool> dataReceived(false); // Start a read thread std::thread readThread([&]() { constexpr std::size_t buffer_size = 1024; std::array<char, buffer_size> buffer; boost::system::error_code error; std::size_t bytes_transferred = devicesCDC.at(port)->read_some(boost::asio::buffer(buffer), error); if (!error) { dataRX.assign(buffer.begin(), buffer.begin() + bytes_transferred); dataReceived = true; } });

Så jag utvecklade exemplet så här. Nu kan jag stoppa tråden, trots att det är blockerande läsning.
Jag tycker denna metod är riktigt bra för att många protokoll t.ex. Modbus kräver att man skickar meddelande och sedan ska det gå X antal sekunder innan man ska få helst svar. Om inte, så blir det timeout.

// Data std::vector<uint8_t> dataRX; // Atomic variable for the thread std::atomic<bool> dataReceived(false); // Start a read thread std::thread readThread([&]() { constexpr std::size_t buffer_size = 1024; std::array<char, buffer_size> buffer; boost::system::error_code error; std::size_t bytes_transferred = 0; try { bytes_transferred = devicesCDC.at(port)->read_some(boost::asio::buffer(buffer), error); }catch(...){} if (!error) { dataRX.assign(buffer.begin(), buffer.begin() + bytes_transferred); dataReceived = true; } }); // Write data devicesCDC.at(port)->write_some(boost::asio::buffer(dataTX, size)); // Check if data has been received or timeout has occurred bool timeout = false; auto startTime = std::chrono::steady_clock::now(); auto timeoutDuration = std::chrono::milliseconds(timeOutMilliseconds); while (!dataReceived && !timeout){ auto currentTime = std::chrono::steady_clock::now(); auto elapsedTime = std::chrono::duration_cast<std::chrono::milliseconds>(currentTime - startTime); // Check if timeout has occurred if (elapsedTime >= timeoutDuration){ timeout = true; readThread.detach(); // Kill the thread } } // End the thread if (readThread.joinable()) { readThread.join(); }

Permalänk
Hedersmedlem
Skrivet av heretic16:

Jag kan ju använda mig av async_read_some i Boost asio. Den läser oavsett om data finns. Men då finns det risk att den kanske läser när jag har halva datat.

Detta måste du hantera oavsett vilken metod du väljer (förutom kanske om du vet exakt hur många byte fatta det skall vara och läser precis så många). Visst kan man vänta en stund och hoppas på det bästa, men det är omständligt och troligen långsammare än det hade kunnat vara. Dessutom kan det ju tänkas att man råkar läsa hälften av efterföljande meddelande (om sådana kan skickas spontant).

Permalänk
Skrivet av Elgot:

Detta måste du hantera oavsett vilken metod du väljer (förutom kanske om du vet exakt hur många byte fatta det skall vara och läser precis så många). Visst kan man vänta en stund och hoppas på det bästa, men det är omständligt och troligen långsammare än det hade kunnat vara. Dessutom kan det ju tänkas att man råkar läsa hälften av efterföljande meddelande (om sådana kan skickas spontant).

Jag valde att använda "read_some" som blockerar. Sedan dödar jag tråden om meddelandet ej kommer i tid.
Jag använder mig av try-catch, för att kunna hantera fel.

Permalänk
Hedersmedlem
Skrivet av heretic16:

Jag valde att använda "read_some" som blockerar. Sedan dödar jag tråden om meddelandet ej kommer i tid.
Jag använder mig av try-catch, för att kunna hantera fel.

Nja, koden ovan gör nog inte riktigt vad du tror (fast det kan förstås fungera ändå ibland (eller ofta)).
read_some blockerar mycket riktigt, men bara till det finns något alls att returnera. Det din läs-tråd gör är alltså vänta till det finns minst en byte att läsa, läsa den (och eventuellt fler om det finns) och sedan avsluta. Detta sker dessutom samtidigt som du skriver till porten i huvudtråden.
detach dödar inte heller tråden utan låter den leva sitt eget liv. Och man kan inte heller köra join efter att man har kört detach.

Permalänk
Skrivet av Elgot:

Nja, koden ovan gör nog inte riktigt vad du tror (fast det kan förstås fungera ändå ibland (eller ofta)).
read_some blockerar mycket riktigt, men bara till det finns något alls att returnera. Det din läs-tråd gör är alltså vänta till det finns minst en byte att läsa, läsa den (och eventuellt fler om det finns) och sedan avsluta. Detta sker dessutom samtidigt som du skriver till porten i huvudtråden.
detach dödar inte heller tråden utan låter den leva sitt eget liv. Och man kan inte heller köra join efter att man har kört detach.

Intressant. Så om jag anropar "detatch", så är tråden fortfarande kvar?

Då kanske jag måste ta en async_read_some trots allt? Nu handlar det bara om 1-2 bytes som jag skickar.

Permalänk
Hedersmedlem
Skrivet av heretic16:

Intressant. Så om jag anropar "detatch", så är tråden fortfarande kvar?

Ja, men du har uttryckligen frånsagt dig kontrollen över den. I ditt fall är ju inte tråden så långlivad ändå eftersom den avslutar så fort något kan läsas från porten (förutsatt att det kommer något).

Skrivet av heretic16:

Då kanske jag måste ta en async_read_some trots allt? Nu handlar det bara om 1-2 bytes som jag skickar.

Det är nog det bästa sättet. Då kan man avbryta läsningen med cancel om det tar för lång tid.