Boost Asio för seriell kommunikation - Kan man optimera detta kodexempel? C++

Permalänk

Boost Asio för seriell kommunikation - Kan man optimera detta kodexempel? C++

Jag använder Boost Asio för att kommunicera via USB uttaget. Orsaken varför jag använder Boost Asio har med att libusb kan inte enumerera korrekt port t.ex. COM1, COM2, COM3. Istället får man VendorID. Exempel: https://stackoverflow.com/questions/14722083/how-to-use-libus...

Som jag uppfattar detta så använder Boost Asio själva operativsystemets egna USB-bibliotek för att kommunicera. Medan libusb kommunicerar direkt till hårdvaran, därav snabbare, men man får inte med alla fina saker som Boost Asio erbjuder t.ex. buffer och enummerering av enheter.

Det jag har gjort är att jag har skrivit en läsfunktion och en skrivfunktion. Det jag tänkte fråga er är om det finns potential att kunna optimera denna kod? Så vad tror ni? Går det göra koden snabbare igenom att slippa använda trådar? Koden är klippt från en .cpp fil, därav försvann åäö.

Skrivfunktion:

static boost::asio::io_context io; static std::map<std::string, std::shared_ptr<boost::asio::serial_port>> devicesCDC; static bool existerarPorten(const char port[]) { return devicesCDC.find(port) != devicesCDC.end() ? true : false; } int32_t write(const char port[], const uint8_t data[], const uint16_t size, const int32_t timeout_ms) { int32_t writtenBytes = 0; if (existerarPorten(port)) { // Get the USB auto deviceUSB = devicesCDC.at(port); // Skapa en io_context och deadline_timer boost::asio::io_context io; boost::asio::deadline_timer timer(io); boost::system::error_code ec; // Starta deadline timer timer.expires_from_now(boost::posix_time::milliseconds(timeout_ms)); timer.async_wait([&](const boost::system::error_code& error) { if (!error) { // Om timeout intr ffar, st ng seriellporten ec = boost::asio::error::operation_aborted; deviceUSB->cancel(); } }); // Starta en separat tr d f r att k ra io_context std::thread io_thread([&]() { io.run(); }); // Utför skrivoperationen writtenBytes = boost::asio::write(*deviceUSB, boost::asio::buffer(data, size), ec); // Avbryt timern och v nta p io_context-tr den timer.cancel(); if (io_thread.joinable()) { io_thread.join(); } // Kontrollera fel if (ec) { if (ec == boost::asio::error::operation_aborted) { std::cerr << "CDC.cpp: Timeout intr ffade under skrivning till port: " << port << std::endl; return -1; // Returnera felkod f r timeout } std::cerr << "CDC.cpp: Fel vid skrivning: " << ec.message() << std::endl; return -1; } } return writtenBytes; }

Läsfunktion:

int32_t read(const char port[], uint8_t data[], const uint16_t size, const int32_t timeout_ms) { int32_t bytesRead = 0; if (existerarPorten(port)) { // Get the USB auto deviceUSB = devicesCDC.at(port); // Skapa en io_context och deadline_timer boost::asio::io_context io; boost::asio::deadline_timer timer(io); boost::system::error_code ec; // Starta deadline timer timer.expires_from_now(boost::posix_time::milliseconds(timeout_ms)); timer.async_wait([&](const boost::system::error_code& error) { if (!error) { // Om timeout intr ffar, st ng seriellporten ec = boost::asio::error::operation_aborted; deviceUSB->cancel(); } }); // Starta en separat tr d f r att k ra io_context std::thread io_thread([&]() { io.run(); }); // Utför operationen bytesRead = boost::asio::read(*deviceUSB, boost::asio::buffer(data, size), ec); // Avbryt timern och v nta p io_context-tr den timer.cancel(); if (io_thread.joinable()) { io_thread.join(); } // Kontrollera fel if (ec) { if (ec == boost::asio::error::operation_aborted) { std::cerr << "CDC.cpp: Timeout intr ffade under l sning fr n port: " << port << std::endl; return -1; // Returnera felkod f r timeout } std::cerr << "CDC.cpp: Fel vid l sning: " << ec.message() << std::endl; return -1; } } return bytesRead; }

Permalänk
Hedersmedlem

Den främsta anledningen till att inte använda libusb är som sagt att det du vill göra är att läsa och skriva från en serieport (förvisso ansluten via usb); inte "läsa/skriva till usb". Man kan göra det senare också (till exempel med libusb), men då måste man själv implementera alla serieportsdetaljer som mottagaren förväntar sig.

Det finns viss förbättringspotential tror jag:

  • Du borde inte skapa ett nytt io-objekt i funtionerna. Använd samma som när du skapade portarna.

  • Klockan kan vara global (eller statisk)

  • Du bör nog köra boost::asio::async_write om det skall fungera som du vill. Alternativt ändra deviceUSB->cancel() till deviceUSB->close()

  • Istället för att skapa en tråd och vänta på den borde du kunna köra io.run() direkt:

boost::asio::async_write(*deviceUSB, boost::asio::buffer(data, size), [&](const boost::system::error_code& error, std::size_t bytes_transferred) { writtenBytes = bytes_transferred; ec = error; timer.cancel(); }); io.run(); io.restart();

Motsvarande gäller även för läsning.

Permalänk

Jag testade att införa ditt. Men nu verkar inte något fungera.
Finns det inge bättre bibliotek än boost asio? Jag menar, biblioteket verkar vara skrivet som en kratta.

Permalänk
Hedersmedlem
Skrivet av heretic16:

Jag testade att införa ditt. Men nu verkar inte något fungera.
Finns det inge bättre bibliotek än boost asio? Jag menar, biblioteket verkar vara skrivet som en kratta.

Har du ett globalt io-objekt? Det borde inte sluta funger…

Asio är ett komplext bibliotek och är kanske lite overkill för detta ändamål om man inte kör boost i övrigt. Tittade du på detta:
https://github.com/wjwwood/serial
Det ser enkelt ut.

Permalänk
Skrivet av Elgot:

Har du ett globalt io-objekt? Det borde inte sluta funger…

Asio är ett komplext bibliotek och är kanske lite overkill för detta ändamål om man inte kör boost i övrigt. Tittade du på detta:
https://github.com/wjwwood/serial
Det ser enkelt ut.

Tackar.
Det jag har behov utav är någon typ av läsning som kan läsa och acceptera det den får.
Det ska inte vara blockerande.

Men ändå så borde Boost Asio fungera. Finns det inget färdigt exempel för Boost Asio? Jag har försökt hitta något, men har bara hitta blockerande läsning.

Permalänk
Medlem

I Linux är det bara att öppna och läsa från tty devicen, som vilken annan fil som helst, behövs inga special bibliotek. Men du kanske kör Windows?

Permalänk
Skrivet av ajn:

I Linux är det bara att öppna och läsa från tty devicen, som vilken annan fil som helst, behövs inga special bibliotek. Men du kanske kör Windows?

Jag kör Windows 11 och Lubuntu Linux. Men när jag skriver C++ kod så vill jag att dom ska passa båda. Write Once - Compile everywhere

Jag har hittat ett exempel
https://beta.boost.org/doc/libs/1_52_0/doc/html/boost_asio/re...

void handler( const boost::system::error_code& error, // Result of operation. std::size_t bytes_transferred // Number of bytes copied into the // buffers. If an error occurred, // this will be the number of // bytes successfully transferred // prior to the error. ); boost::asio::async_read(s, boost::asio::buffer(data, size), handler);

Med lite lambda-uttryck så kan man skriva

boost::asio::async_read(s, boost::asio::buffer(data, size), [&](const boost::system::error_code& error, std::size_t bytes_transferred){ denna kod körs när jag boost asio läser. });

Men då är ett problem! Time out! Viktigt!
Eller tror ni man kan köra egen timout? Typ en while loop.

Permalänk
Hedersmedlem
Skrivet av heretic16:

Tackar.
Det jag har behov utav är någon typ av läsning som kan läsa och acceptera det den får.
Det ska inte vara blockerande.

Men ändå så borde Boost Asio fungera. Finns det inget färdigt exempel för Boost Asio? Jag har försökt hitta något, men har bara hitta blockerande läsning.

Det som gör det blockerade i ditt fall är att man kör io.run() i huvudtråden. Om du gör det i en bakgrundstråd blir funktionen ickeblockerande (men kör inte join på tråden; då fastnar du ju där istället). Jag frågade chatgpt om ett enkelt exempel:

#include <iostream> #include <boost/asio.hpp> #include <boost/asio/streambuf.hpp> #include <thread> void readLine(boost::asio::serial_port& serial, boost::asio::streambuf& buffer) { boost::asio::async_read_until(serial, buffer, '\n', [&serial, &buffer](const boost::system::error_code& ec, std::size_t bytes_transferred) { if (!ec) { // Extract the line from the buffer std::istream is(&buffer); std::string line; std::getline(is, line); // Print the received line std::cout << "Received: " << line << std::endl; // Clear the buffer and read the next line buffer.consume(buffer.size()); readLine(serial, buffer); } else if (ec != boost::asio::error::operation_aborted) { std::cerr << "Error: " << ec.message() << std::endl; } }); } int main() { try { boost::asio::io_context io; // Open the serial port (replace with your port name, e.g., "COM3" for Windows) boost::asio::serial_port serial(io, "/dev/ttyS0"); // Configure the serial port serial.set_option(boost::asio::serial_port_base::baud_rate(9600)); serial.set_option(boost::asio::serial_port_base::character_size(8)); serial.set_option(boost::asio::serial_port_base::parity(boost::asio::serial_port_base::parity::none)); serial.set_option(boost::asio::serial_port_base::stop_bits(boost::asio::serial_port_base::stop_bits::one)); serial.set_option(boost::asio::serial_port_base::flow_control(boost::asio::serial_port_base::flow_control::none)); // Create a buffer for reading data boost::asio::streambuf buffer; // Start asynchronous reading readLine(serial, buffer); // Run the io_context in a separate thread std::thread io_thread([&io]() { io.run(); }); // Main thread can do other work or wait std::cout << "Press Enter to stop the program..." << std::endl; std::cin.get(); // Stop the io_context and join the thread io.stop(); io_thread.join(); } catch (const std::exception& e) { std::cerr << "Error: " << e.what() << std::endl; } return 0; }

Skrivet av heretic16:

Men då är ett problem! Time out! Viktigt!
Eller tror ni man kan köra egen timout? Typ en while loop.

Du kan starta en klocka som du gjorde förut. Det är en vanlig lösning.

Permalänk

Här är min senaste kod. Tyvärr så läser den inget. Kan det ha med att man måste starta något?

int32_t read(const char port[], uint8_t data[], const uint16_t size, const int32_t timeout_ms) { int32_t bytesRead = 0; if (CDCDeviceExist(port)) { // Get the USB auto deviceUSB = devicesCDC.at(port); int_least64_t tick = Tools_Software_Algorithms_getMilliSeconds(); const int_least64_t endTick = tick + timeout_ms; while (tick <= endTick) { boost::asio::async_read(*deviceUSB, boost::asio::buffer(data, size), [&](const boost::system::error_code& error, std::size_t bytes_transferred) { auto status = error; if (!error.failed()) { bytesRead += bytes_transferred; } }); tick = Tools_Software_Algorithms_getMilliSeconds(); } } return bytesRead; }

Edit:

Denna kod fungerar!

int32_t read(const char port[], uint8_t data[], const uint16_t size, const int32_t timeout_ms) { int32_t bytesRead = 0; if (CDCDeviceExist(port)) { // Get the USB auto deviceUSB = devicesCDC.at(port); // Important stuffs boost::asio::io_context io_context; boost::asio::deadline_timer timer(io_context); boost::system::error_code ec; // Create dead line timer timer.expires_from_now(boost::posix_time::milliseconds(timeout_ms)); timer.async_wait([&](const boost::system::error_code& error) { if (!error) { // If time out occurs ec = boost::asio::error::operation_aborted; deviceUSB->cancel(); } }); // Start the thread std::thread io_thread([&]() { io_context.run(); }); // Read with error code bytesRead = boost::asio::read(*deviceUSB, boost::asio::buffer(data, size), ec); // Cancle the timer and wait for the io_context to join timer.cancel(); if (io_thread.joinable()) { io_thread.join(); } } return bytesRead; }

Permalänk
Hedersmedlem
Skrivet av heretic16:

Här är min senaste kod. Tyvärr så läser den inget. Kan det ha med att man måste starta något?

int32_t read(const char port[], uint8_t data[], const uint16_t size, const int32_t timeout_ms) { int32_t bytesRead = 0; if (CDCDeviceExist(port)) { // Get the USB auto deviceUSB = devicesCDC.at(port); int_least64_t tick = Tools_Software_Algorithms_getMilliSeconds(); const int_least64_t endTick = tick + timeout_ms; while (tick <= endTick) { boost::asio::async_read(*deviceUSB, boost::asio::buffer(data, size), [&](const boost::system::error_code& error, std::size_t bytes_transferred) { auto status = error; if (!error.failed()) { bytesRead += bytes_transferred; } }); tick = Tools_Software_Algorithms_getMilliSeconds(); } } return bytesRead; }

Edit:

Denna kod fungerar!

int32_t read(const char port[], uint8_t data[], const uint16_t size, const int32_t timeout_ms) { int32_t bytesRead = 0; if (CDCDeviceExist(port)) { // Get the USB auto deviceUSB = devicesCDC.at(port); // Important stuffs boost::asio::io_context io_context; boost::asio::deadline_timer timer(io_context); boost::system::error_code ec; // Create dead line timer timer.expires_from_now(boost::posix_time::milliseconds(timeout_ms)); timer.async_wait([&](const boost::system::error_code& error) { if (!error) { // If time out occurs ec = boost::asio::error::operation_aborted; deviceUSB->cancel(); } }); // Start the thread std::thread io_thread([&]() { io_context.run(); }); // Read with error code bytesRead = boost::asio::read(*deviceUSB, boost::asio::buffer(data, size), ec); // Cancle the timer and wait for the io_context to join timer.cancel(); if (io_thread.joinable()) { io_thread.join(); } } return bytesRead; }

Ja, async-funktionerna kräver att någon kör run på det aktuella io_context-objektet. Detta kan, som ovan, ske i en annan tråd men om du inte vill blockera vill du typiskt ha en fristående global tråd som bara kör run(). Du kan skapa den innan du anropar dina funktioner, men man kan behöva hantera att run returnerar om det inte finns någon uppgift kvar att utföra. För att undvika det kan man skapa en work guard:

auto work_guard = boost::asio::make_work_guard(io);

Din lösning läser, men den är blockerande och tidsgränsen fungerar väl inte (reset är för asynkrona anrop)?

Permalänk
Skrivet av Elgot:

Ja, async-funktionerna kräver att någon kör run på det aktuella io_context-objektet. Detta kan, som ovan, ske i en annan tråd men om du inte vill blockera vill du typiskt ha en fristående global tråd som bara kör run(). Du kan skapa den innan du anropar dina funktioner, men man kan behöva hantera att run returnerar om det inte finns någon uppgift kvar att utföra. För att undvika det kan man skapa en work guard:

auto work_guard = boost::asio::make_work_guard(io);

Din lösning läser, men den är blockerande och tidsgränsen fungerar väl inte (reset är för asynkrona anrop)?

Jo. Det är just som den gör. Jag tycker Boost Asio är bra. Den har löst problemet för mig, utan att jag ens förstår lösningen. Vilket jag tycker är tragiskt.

Hade Boost Asio tagit bort lite onödiga saker så som io_context och io.run() osv. Då hade det blivit mycket enklare. Men det kanske finns ett behov utav sådant. Boost Asio är som sagt plattformsobereonde, vilket är bra.

Permalänk
Medlem
Skrivet av heretic16:

Hade Boost Asio tagit bort lite onödiga saker så som io_context och io.run() osv. Då hade det blivit mycket enklare. Men det kanske finns ett behov utav sådant. Boost Asio är som sagt plattformsobereonde, vilket är bra.

Fast spelar det verkligen så stor roll? Med vettig struktur på koden i övrigt kommer ju extern kommunikation vara dold av någon lämplig abstraktion ändå, tänk "dependency inversion", och då kan du ha simpel platforms beroende kod som du konfigurerar compile timme istället.

Permalänk
Hedersmedlem
Skrivet av heretic16:

Jo. Det är just som den gör. Jag tycker Boost Asio är bra. Den har löst problemet för mig, utan att jag ens förstår lösningen. Vilket jag tycker är tragiskt.

Hade Boost Asio tagit bort lite onödiga saker så som io_context och io.run() osv. Då hade det blivit mycket enklare. Men det kanske finns ett behov utav sådant. Boost Asio är som sagt plattformsobereonde, vilket är bra.

Är du säker på att det fungerar som du vill? boost::asio::read blockerar tills den har läst den efterfrågade mängden data eller fel uppstår, så utöver att den blockerar (och det ville du ju inte) bör den inte heller avbrytas av deviceUSB->cancel() (den stoppar som sagt asynkrona anrop). Och även om det skulle fungera kommer du att vänta på klockan (dvs. blockera).

Skälet till att man har alla "onödiga saker" är för att erbjuda kontroll. I just det här fallet är det som sagt kanske en lite väl stor slägga för problemet, men samma strukturer kan skalas upp till ett stort antal operationer (kanske en server med många klienter). Man kan till exempel köra run() i många trådar för att fördela arbetet på många kärnor.

Jag tror du har (i alla fall) dessa alternativ:

#include <boost/asio.hpp> int main(int argc, char **argv) { if (argc < 3) { std::printf("%s <port> <mode>\n", argv[0]); return 1; } int mode = atoi(argv[2]); char data[32]; memset(data, 0, 32); boost::system::error_code ec; boost::asio::io_context ctx; boost::asio::serial_port prt(ctx, argv[1]); if (mode == 0) { std::printf("Blocking read\n"); auto cnt = boost::asio::read(prt, boost::asio::buffer(data, 16), ec); std::printf("read %ld bytes: %s\n", cnt, data); } else if (mode == 1) { std::printf("Blocking read with timeout (bad)\n"); boost::asio::steady_timer timer(ctx); timer.expires_after(std::chrono::seconds(10)); bool timeout = false; timer.async_wait([&](const boost::system::error_code &e) { if(!e) { std::printf("timer expired\n"); prt.close(); timeout = true; } }); std::thread thread([&] { ctx.run(); }); auto cnt = boost::asio::read(prt, boost::asio::buffer(data, 16), ec); timer.cancel(); thread.join(); if (!timeout) std::printf("read %ld bytes: %s\n", cnt, data); else std::printf("timeout\n"); } else if (mode == 2) { std::printf("Blocking read with timeout\n"); boost::asio::steady_timer timer(ctx); timer.expires_after(std::chrono::seconds(10)); bool timeout = false; timer.async_wait([&](const boost::system::error_code &e) { if(!e) { prt.cancel(); timeout = true; } }); long cnt = 0; boost::asio::async_read(prt, boost::asio::buffer(data, 16), [&](const boost::system::error_code &error, std::size_t bytes_transferred) { cnt = bytes_transferred; ec = error; timer.cancel(); }); ctx.run(); if (!timeout) std::printf("read %ld bytes: %s\n", cnt, data); else std::printf("timeout\n"); } else if (mode == 3) { std::printf("Non-blocking read with timeout\n"); boost::asio::steady_timer timer(ctx); timer.expires_after(std::chrono::seconds(10)); bool timeout = false; std::thread thread([&] { ctx.run(); }); bool done = false; timer.async_wait([&](const boost::system::error_code &e) { if(!e) { prt.cancel(); timeout = true; done = true; std::printf("timeout\n"); } }); long cnt = 0; boost::asio::async_read(prt, boost::asio::buffer(data, 16), [&](const boost::system::error_code &error, std::size_t bytes_transferred) { cnt = bytes_transferred; ec = error; timer.cancel(); done = true; if (!error) std::printf("read %ld bytes: %s\n", cnt, data); }); while (!done) { std::printf("waiting...\n"); usleep(1000000); } thread.join(); } return 0; }

Permalänk

@Elgot

Men jag har testat använda boost::asio::async_read och denna resulterarde väldigt dåligt. Jag vet inte varför min boost::asio::read fungerar riktigt bra och verkar inte alls blockera. Jag känner att något är skumt här.

Problemet är att jag får inte boost::asio::async_read att fungera som det är tänkt. Det kanske är så att async_read bevarar inte meddelandet?

Permalänk
Hedersmedlem
Skrivet av heretic16:

Jag vet inte varför min boost::asio::read fungerar riktigt bra och verkar inte alls blockera. Jag känner att något är skumt här.

Har du testat alla knepiga fall? Om man läser en liten mängd data från en port som skickar data går det kanske så fort att det inte är ett praktiskt problem, men vad händer om du försöker läsa mycket (1 MB till exempel)?

Skrivet av heretic16:

Problemet är att jag får inte boost::asio::async_read att fungera som det är tänkt. Det kanske är så att async_read bevarar inte meddelandet?

Meddelandet hamnar på samma ställe som i det synkrona fallet, men inte innan operationen faktiskt är klar. Snyggast är att hantera det med funktionen som skickas med anropet, men man kan också vänta tills inget finns kvar att göra (och run() returnerar).

Fungerar koden i mitt förra inlägg?

Permalänk
Skrivet av Elgot:

Har du testat alla knepiga fall? Om man läser en liten mängd data från en port som skickar data går det kanske så fort att det inte är ett praktiskt problem, men vad händer om du försöker läsa mycket (1 MB till exempel)?

Meddelandet hamnar på samma ställe som i det synkrona fallet, men inte innan operationen faktiskt är klar. Snyggast är att hantera det med funktionen som skickas med anropet, men man kan också vänta tills inget finns kvar att göra (och run() returnerar).

Fungerar koden i mitt förra inlägg?

Jag ska testa din kod snart. Återkommer.