I have a smart energy meter which sends energy consumption data every second. The daemon program I've written (C++/C Arch Linux) to read the data doesn't exit when the USB cable is disconnected and stalls indefinitely in the blocking read() call.
How to interrupt a blocking read() call (i.e. fail with EINTR instead of waiting for the next char)?
I extensively searched Google and looked here in SO and could not find an answer to this problem.
Details:
Smartmeter project source on Github
IR dongle with FT232RL USB to UART bridge
Datagrams have fixed length of 328 bytes sent every second
Read method detects beginning \ and end ! markers of a datagram
sigaction to catch CTRL+C SIGINT and SIGTERM signals
termios is setup to do blocking read() with VMIN = 1 and VTIME = 0.
Tried:
Playing with VMIN and VTIME
Removed SA_RESTART
Possible solution:
Use a non-blocking read method, maybe with select() and poll()
Or VMIN > 0 (datagram is longer than 255 characters and I would need to read the datagram in smaller chunks)
Not sure how to handle datagram begin/end detection and the one second interval between datagrams for a non-blocking read method
EDIT: The code below now buffers the read() call into an intermediate buffer of 255 bytes (VMIN = 255 and VTIME = 5) adapted from here. This avoids the small overhead of calling read() for every char. In practise this doesn't make a difference compared to reading one char at a time though. Read() still doesn't exit gracefully on cable disconnect. The daemon needs to be killed with kill -s SIGQUIT $PID. SIGKILL has no effect.
main.cpp:
volatile sig_atomic_t shutdown = false;
void sig_handler(int)
{
shutdown = true;
}
int main(int argc, char* argv[])
{
struct sigaction action;
action.sa_handler = sig_handler;
sigemptyset(&action.sa_mask);
action.sa_flags = SA_RESTART;
sigaction(SIGINT, &action, NULL);
sigaction(SIGTERM, &action, NULL);
while (shutdown == false)
{
if (!meter->Receive())
{
std::cout << meter->GetErrorMessage() << std::endl;
return EXIT_FAILURE;
}
}
Smartmeter.cpp:
bool Smartmeter::Receive(void)
{
memset(ReceiveBuffer, '\0', Smartmeter::ReceiveBufferSize);
if (!Serial->ReadBytes(ReceiveBuffer, Smartmeter::ReceiveBufferSize))
{
ErrorMessage = Serial->GetErrorMessage();
return false;
}
}
SmartMeterSerial.cpp:
#include <cstring>
#include <iostream>
#include <thread>
#include <unistd.h>
#include <termios.h>
#include <sys/file.h>
#include <sys/ioctl.h>
#include "SmartmeterSerial.h"
const unsigned char SmartmeterSerial::BufferSize = 255;
SmartmeterSerial::~SmartmeterSerial(void)
{
if (SerialPort > 0) {
close(SerialPort);
}
}
bool SmartmeterSerial::Begin(const std::string &device)
{
if (device.empty()) {
ErrorMessage = "Serial device argument empty";
return false;
}
if ((SerialPort = open(device.c_str(), (O_RDONLY | O_NOCTTY))) < 0)
{
ErrorMessage = std::string("Error opening serial device: ")
+ strerror(errno) + " (" + std::to_string(errno) + ")";
return false;
}
if(!isatty(SerialPort))
{
ErrorMessage = std::string("Error: Device ") + device + " is not a tty.";
return false;
}
if (flock(SerialPort, LOCK_EX | LOCK_NB) < 0)
{
ErrorMessage = std::string("Error locking serial device: ")
+ strerror(errno) + " (" + std::to_string(errno) + ")";
return false;
}
if (ioctl(SerialPort, TIOCEXCL) < 0)
{
ErrorMessage = std::string("Error setting exclusive access: ")
+ strerror(errno) + " (" + std::to_string(errno) + ")";
return false;
}
struct termios serial_port_settings;
memset(&serial_port_settings, 0, sizeof(serial_port_settings));
if (tcgetattr(SerialPort, &serial_port_settings))
{
ErrorMessage = std::string("Error getting serial port attributes: ")
+ strerror(errno) + " (" + std::to_string(errno) + ")";
return false;
}
cfmakeraw(&serial_port_settings);
// configure serial port
// speed: 9600 baud, data bits: 7, stop bits: 1, parity: even
cfsetispeed(&serial_port_settings, B9600);
cfsetospeed(&serial_port_settings, B9600);
serial_port_settings.c_cflag |= (CLOCAL | CREAD);
serial_port_settings.c_cflag &= ~CSIZE;
serial_port_settings.c_cflag |= (CS7 | PARENB);
// vmin: read() returns when x byte(s) are available
// vtime: wait for up to x * 0.1 second between characters
serial_port_settings.c_cc[VMIN] = SmartmeterSerial::BufferSize;
serial_port_settings.c_cc[VTIME] = 5;
if (tcsetattr(SerialPort, TCSANOW, &serial_port_settings))
{
ErrorMessage = std::string("Error setting serial port attributes: ")
+ strerror(errno) + " (" + std::to_string(errno) + ")";
return false;
}
tcflush(SerialPort, TCIOFLUSH);
return true;
}
char SmartmeterSerial::GetByte(void)
{
static char buffer[SmartmeterSerial::BufferSize] = {0};
static char *p = buffer;
static int count = 0;
if ((p - buffer) >= count)
{
if ((count = read(SerialPort, buffer, SmartmeterSerial::BufferSize)) < 0)
{
// read() never fails with EINTR signal on cable disconnect
ErrorMessage = std::string("Read on serial device failed: ")
+ strerror(errno) + " (" + std::to_string(errno) + ")";
return false;
}
p = buffer;
}
return *p++;
}
bool SmartmeterSerial::ReadBytes(char *buffer, const int &length)
{
int bytes_received = 0;
char *p = buffer;
bool message_begin = false;
tcflush(SerialPort, TCIOFLUSH);
while (bytes_received < length)
{
if ((*p = GetByte()) == '/')
{
message_begin = true;
}
if (message_begin)
{
++p;
++bytes_received;
}
}
if (*(p-3) != '!')
{
ErrorMessage = "Serial datagram stream not in sync.";
return false;
}
return true;
}
Many thanks for your help.
While the code below is not a solution to the original question on how to interrupt a blocking read() call, at least it would be a vialble workaround for me. With VMIN = 0 and VTIME = 0 this is now a non-blocking read():
bool SmartmeterSerial::ReadBytes(char *buffer, const int &length)
{
int bytes_received = 0;
char *p = buffer;
tcflush(SerialPort, TCIOFLUSH);
bool message_begin = false;
const int timeout = 10000;
int count = 0;
char byte;
while (bytes_received < length)
{
if ((byte = read(SerialPort, p, 1)) < 0)
{
ErrorMessage = std::string("Read on serial device failed: ")
+ strerror(errno) + " (" + std::to_string(errno) + ")";
return false;
}
if (*p == '/')
{
message_begin = true;
}
if (message_begin && byte)
{
++p;
bytes_received += byte;
}
if (count > timeout)
{
ErrorMessage = "Read on serial device failed: Timeout";
return false;
}
++count;
std::this_thread::sleep_for(std::chrono::microseconds(100));
}
if (*(p-3) != '!')
{
ErrorMessage = "Serial datagram stream not in sync.";
return false;
}
return true;
}
However, I am still curious to know if it is actually possible to interrupt a blocking `read()ยด as this workaround is constantly polling the serial port.
I believe reading one char at a time is not a problem as the received bytes from the UART are buffered by the OS - but constantly polling the buffer with read() is! Maybe I'll try a ioctl(SerialPort, FIONREAD, &bytes_available) before read(), although I don't know if this would actually make a difference.
Any suggestions?
I have this variable, DELAY_INTERVAL, which I set to measure how long my arduino mega board will delay before submitting a request to my server again.
I've found that when this variable is set to 30 seconds my board runs fine but as soon as I set it to 45 seconds my board crashes.
I've inserted a bunch of printlns for debugging purposes:
void loop() {
Serial.print("Free RAM: ");
Serial.println(getFreeRam(), DEC);
Serial.println(1);
getEC();
Serial.println(2);
getPH();
Serial.println(3);
createPoint(ecSensorString, phSensorString);
Serial.println(4);
delay(DELAY_INTERVAL*1000);
Serial.println(5);
}
Note that this runs indefinitely for 30s but not 45s.
This is what the console outputs on 45s:
Free RAM: 6551
1
getEC:*OK
2
getPH:
3
a
postPointToServer:ec=*OK&ph=&interval=45s
b
c
d
e
f
g
h
4
<output monitor just stalls>
So I have identified that delay is the culprit here. But why and how can I solve this?
The rest of my code if anyone is interested.
void setup() {
//initRGB();
Serial.begin(9600);
ecSensorString.reserve(30);
phSensorString.reserve(30);
initSensors();
initWireless();
Serial.println("Setup Complete");
}
void loop() {
Serial.print("Free RAM: ");
Serial.println(getFreeRam(), DEC);
Serial.println(1);
getEC();
Serial.println(2);
getPH();
Serial.println(3);
createPoint(ecSensorString, phSensorString);
Serial.println(4);
delay(DELAY_INTERVAL*1000);
Serial.println(5);
}
void initWireless() {
/* Initialise the module */
if (!cc3000.begin()) {
Serial.println(F("Couldn't begin()! Check your wiring?"));
//displayRed();
while(1);
}
Serial.println(F("Initialization Complete"));
Serial.print(F("Attempting to connect to "));
Serial.println(WLAN_SSID);
if (!cc3000.connectToAP(WLAN_SSID, WLAN_PASS, WLAN_SECURITY)) {
Serial.println(F("Failed!"));
//displayRed();
while(1);
}
Serial.println(F("Wireless Connected!"));
/* Wait for DHCP to complete */
while (!cc3000.checkDHCP()) {
delay(100); // ToDo: Insert a DHCP timeout!
}
Serial.println(F("DHCP Requested!"));
if (IP_NOT_DOMAIN) {
ip = cc3000.IP2U32(IP_1, IP_2, IP_3, IP_4);
} else {
ip = 0;
// Try looking up the website's IP address
while (ip == 0) {
if (! cc3000.getHostByName(WEBSITE, &ip)) {
Serial.println(F("Couldn't resolve!"));
}
delay(500);
}
}
}
void initSensors() {
Serial2.begin(38400);
Serial3.begin(38400);
// Tell the EC sensors to stop continuous mode
Serial2.print("c,0\r");
// Tell the pH sensor to stop all readings
Serial3.print("E\r");
// TODO: EC sensor's first reading is always empty, possibly delay?
}
void createPoint(String ec, String ph) {
if (ec && ph) {
postPointToServer("ec=" + ec + "&ph=" + ph);
} else if (ec) {
postPointToServer("ec=" + ec);
} else if (ph) {
postPointToServer("ph=" + ph);
} else {
Serial.println("createPoint: null");
}
}
void postPointToServer(String data) {
Serial.println("a");
char interval_c[20];
sprintf(interval_c, "&interval=%ds", DELAY_INTERVAL);
data += interval_c;
Serial.print("postPointToServer:");
Serial.println(data);
Serial.println("b");
Adafruit_CC3000_Client www = cc3000.connectTCP(ip, PORT);
Serial.println("c");
if (www.connected()) {
www.fastrprint(F("POST ")); www.fastrprint(WEBPAGE); www.fastrprint(F(" HTTP/1.1\r\n"));
www.fastrprint(F("Host: ")); www.fastrprint(WEBSITE); www.fastrprint(F("\r\n"));
Serial.println("d");
String auth_raw = serial + ":" + token;
char auth_input[200];
char auth_output[200];
auth_raw.toCharArray(auth_input, auth_raw.length() + 1);
base64_encode(auth_output, auth_input, auth_raw.length());
www.fastrprint(F("Authorization: Basic ")); www.fastrprint(auth_output); www.fastrprint(F("\r\n"));
www.fastrprint(F("Content-Type: application/x-www-form-urlencoded")); www.fastrprint(F("\r\n"));
Serial.println("e");
char len_c[7];
itoa(data.length(), len_c, 10);
www.fastrprint(F("Content-Length: ")); www.fastrprint(len_c); www.fastrprint(F("\r\n"));
// Extra empty line for post arguments
www.fastrprint(F("\r\n"));
Serial.println("f");
char data_c[200];
data.toCharArray(data_c, data.length() + 1);
www.fastrprint(data_c); www.fastrprint(F("\r\n"));
www.fastrprint(F("\r\n"));
www.println();
//displayGreen();
} else {
Serial.println(F("Connection failed"));
//displayRed();
}
Serial.println("g");
www.close();
Serial.println("h");
}
void getEC() {
ecSensorString = "";
Serial2.print("r\r"); // Read only 1
char inchar;
while (Serial2.available()) {
inchar = (char)Serial2.read();
if (inchar != '\r') {
ecSensorString += inchar;
}
}
Serial.print("getEC:");
Serial.println(ecSensorString);
}
void getPH() {
phSensorString = "";
Serial3.print("R\r"); // Read only 1
char inchar;
while (Serial3.available()) {
inchar = (char)Serial3.read();
if (inchar != '\r') {
phSensorString += inchar;
}
}
Serial.print("getPH:");
Serial.println(phSensorString);
}
I've had the same problem.
Do you need to put an L at the end to make an explicit conversion of this constant into a long integer:
delay(DELAY_INTERVAL*1000L);
I would like to use Boost Asio to read variable length messages from the serial port. I would like to read and wait long enough to be sure that the line is idle, but I do not want to block completely.
The following code is what I have so far, and I am in the process of testing it:
long readData(void *_pData, unsigned long _uSize, size_t millis)
{
size_t n = 0; // n will return the message size.
if (millis > 0) // millis is the acceptable idle time, 0 is invalid in my case.
{
size_t uBytesTransferred = 0;
boost::asio::deadline_timer timeout(m_ioService);
ReadCallback readCallback(uBytesTransferred, timeout);
WaitCallback waitCallback(m_port);
while (_uSize - (unsigned long)n > 0)
{
// Setup asynchronous read with timeout
m_ioService.reset();
m_port.async_read_some(boost::asio::buffer((char*)_pData + n, _uSize - (unsigned long)n), readCallback);
timeout.expires_from_now(boost::posix_time::milliseconds(millis));
timeout.async_wait(waitCallback);
// Block until asynchronous callbacks are finished
m_ioService.run();
// Continue if any bytes were received, stop otherwise
if (uBytesTransferred > 0)
{
n += uBytesTransferred;
m_uBytesReceived += uBytesTransferred;
}
else
{
break;
}
}
}
return n;
}
I would like to know if this is the correct way of doing this (that is, reading until the line is idle) with Boost Asio?
Here are my callback handlers:
struct ReadCallback
{
ReadCallback(std::size_t &_uBytesTransferred, boost::asio::deadline_timer &_timeout)
:m_uBytesTransferred(_uBytesTransferred), m_timeout(_timeout)
{}
void operator()(const boost::system::error_code &_error, std::size_t _uBytesTransferred)
{
m_uBytesTransferred = _uBytesTransferred;
if (!_error && (_uBytesTransferred > 0) )
{
m_timeout.cancel();
}
}
std::size_t &m_uBytesTransferred;
boost::asio::deadline_timer &m_timeout;
private:
ReadCallback();
ReadCallback &operator=(const ReadCallback&);
};
struct WaitCallback
{
WaitCallback(boost::asio::serial_port &_port)
:m_port(_port)
{}
void operator()(const boost::system::error_code &_error)
{
if (!_error)
{
m_port.cancel();
}
}
boost::asio::serial_port &m_port;
private:
WaitCallback();
WaitCallback &operator=(const WaitCallback&);
};
When you say that you "do not want to block completely", it suggests that you might be better off creating a separate thread to handle the socket I/O. That way your main thread can be available for other processing. I am using 2 threads in my client. One thread processes messages received from a server and the other thread handles sending messages to a server. My client talks to multiple servers. Here is the code:
#include "StdAfx.h"
#include "SSLSocket.h"
boost::shared_ptr< boost::asio::io_service > SSLSocket::IOService;
bool SSLSocket::LobbySocketOpen = false;
SSLSocket* SSLSocket::pSSLLobby = 0;
int SSLSocket::StaticInit = 0;
Callback SSLSocket::CallbackFunction;
BufferManagement SSLSocket::BufMang;
volatile bool SSLSocket::ReqAlive = true;
Logger SSLSocket::Log;
HANDLE SSLSocket::hEvent;
bool SSLSocket::DisplayInHex;
ConcurrentMsgQueue SSLSocket::SendMsgQ;
bool SSLSocket::RcvThreadCreated = 0;
BufferManagement* Message::pBufMang;
bool SSLSocket::ShuttingDown = false;
std::vector<SSLSocket *> SocketList;
SSLSocket::SSLSocket(const bool logToFile, const bool logToConsole, const bool displayInHex,
const LogLevel levelOfLog, const string& logFileName, const int bufMangLen) : pSocket(0)
{
// SSLSocket Constructor.
// If the static members have not been intialized yet, then initialize them.
LockCode = new Lock();
if (!StaticInit)
{
SocketList.push_back(this);
DisplayInHex = displayInHex;
BufMang.Init(bufMangLen);
Message::SetBufMang(&BufMang);
// This constructor enables logging according to the vars passed in.
Log.Init(logToFile, logToConsole, levelOfLog, logFileName);
StaticInit = 1;
hEvent = CreateEvent(NULL, false, false, NULL);
// Define the ASIO IO service object.
// IOService = new boost::shared_ptr<boost::asio::io_service>(new boost::asio::io_service);
boost::shared_ptr<boost::asio::io_service> IOServ(new boost::asio::io_service);
IOService = IOServ;
pSSLLobby = this;
}
}
SSLSocket::~SSLSocket(void)
{
if (pSocket)
delete pSocket;
if (--StaticInit == 0)
CloseHandle(hEvent);
}
void SSLSocket::Connect(SSLSocket* psSLS, const string& serverPath, string& port)
{
// Connects to the server.
// serverPath - specifies the path to the server. Can be either an ip address or url.
// port - port server is listening on.
//
try
{
LockCode->Acquire(); // Single thread the code.
// Locking CodeLock(SocketLock); // Single thread the code.
// If the user has tried to connect before, then make sure everything is clean before trying to do so again.
if (pSocket)
{
delete pSocket;
pSocket = 0;
}
// If serverPath is a URL, then resolve the address.
if ((serverPath[0] < '0') || (serverPath[0] > '9')) // Assumes that the first char of the server path is not a number when resolving to an ip addr.
{
// Create the resolver and query objects to resolve the host name in serverPath to an ip address.
boost::asio::ip::tcp::resolver resolver(*IOService);
boost::asio::ip::tcp::resolver::query query(serverPath, port);
boost::asio::ip::tcp::resolver::iterator EndpointIterator = resolver.resolve(query);
// Set up an SSL context.
boost::asio::ssl::context ctx(*IOService, boost::asio::ssl::context::tlsv1_client);
// Specify to not verify the server certificiate right now.
ctx.set_verify_mode(boost::asio::ssl::context::verify_none);
// Init the socket object used to initially communicate with the server.
pSocket = new boost::asio::ssl::stream<boost::asio::ip::tcp::socket>(*IOService, ctx);
//
// The thread we are on now, is most likely the user interface thread. Create a thread to handle all incoming socket work messages.
// Only one thread is created to handle the socket I/O reading and another thread is created to handle writing.
if (!RcvThreadCreated)
{
WorkerThreads.create_thread(boost::bind(&SSLSocket::RcvWorkerThread, this));
RcvThreadCreated = true;
WorkerThreads.create_thread(boost::bind(&SSLSocket::SendWorkerThread, this));
}
// Try to connect to the server. Note - add timeout logic at some point.
boost::asio::async_connect(pSocket->lowest_layer(), EndpointIterator,
boost::bind(&SSLSocket::HandleConnect, this, boost::asio::placeholders::error));
}
else
{
// serverPath is an ip address, so try to connect using that.
//
// Create an endpoint with the specified ip address.
const boost::asio::ip::address IP(boost::asio::ip::address::from_string(serverPath));
int iport = atoi(port.c_str());
const boost::asio::ip::tcp::endpoint EP(IP, iport);
// Set up an SSL context.
boost::asio::ssl::context ctx(*IOService, boost::asio::ssl::context::tlsv1_client);
// Specify to not verify the server certificiate right now.
ctx.set_verify_mode(boost::asio::ssl::context::verify_none);
// Init the socket object used to initially communicate with the server.
pSocket = new boost::asio::ssl::stream<boost::asio::ip::tcp::socket>(*IOService, ctx);
//
// Try to connect to the server. Note - add timeout logic at some point.
//pSocket->core_.engine_.do_connect(void*, int);
// pSocket->next_layer_.async_connect(EP, &SSLSocket::HandleConnect)
// pSocket->next_layer().async_connect(EP, &SSLSocket::HandleConnect);
boost::system::error_code EC;
pSocket->next_layer().connect(EP, EC);
if (EC)
{
// Log an error. This worker thread should exit gracefully after this.
stringstream ss;
ss << "SSLSocket::Connect: connect failed to " << sClientIp << " : " << uiClientPort << ". Error: " << EC.message() + ".\n";
Log.LogString(ss.str(), LogError);
}
HandleConnect(EC);
// boost::asio::async_connect(pSocket->lowest_layer(), EP,
// boost::bind(&SSLSocket::HandleConnect, this, boost::asio::placeholders::error));
}
}
catch (std::exception& e)
{
stringstream ss;
ss << "SSLSocket::Connect: threw an error - " << e.what() << ".\n";
Log.LogString(ss.str(), LogError);
Stop();
}
LockCode->Release();
}
void SSLSocket::SendToServer(const int bytesInMsg, Byte* pBuf)
{
// This method creates a msg object and saves it in the SendMsgQ object.
// sends the number of bytes specified by bytesInMsg in pBuf to the server.
//
Message* pMsg = Message::GetMsg(this, bytesInMsg, pBuf);
SendMsgQ.Push(pMsg);
// Signal the send worker thread to wake up and send the msg to the server.
SetEvent(hEvent);
}
void SSLSocket::SendWorkerThread(SSLSocket* psSLS)
{
// This thread method gets called to process the messages to be sent to the server.
//
// Since this has to be a static method, call a method on the class to handle server requests.
psSLS->ProcessSendRequests();
}
void SSLSocket::ProcessSendRequests()
{
// This method handles sending msgs to the server.
//
std::stringstream ss;
DWORD WaitResult;
Log.LogString("SSLSocket::ProcessSendRequests: Worker thread " + Logger::NumberToString(boost::this_thread::get_id()) + " started.\n", LogInfo);
// Loop until the user quits, or an error of some sort is thrown.
try
{
do
{
// If there are one or more msgs that need to be sent to a server, then send them out.
if (SendMsgQ.Count() > 0)
{
Message* pMsg = SendMsgQ.Front();
SSLSocket* pSSL = pMsg->pSSL;
SendMsgQ.Pop();
const Byte* pBuf = pMsg->pBuf;
const int BytesInMsg = pMsg->BytesInMsg;
boost::system::error_code Error;
LockCode->Acquire(); // Single thread the code.
// Locking CodeLock(SocketLock); // Single thread the code.
try
{
boost::asio::async_write(*pSSL->pSocket, boost::asio::buffer(pBuf, BytesInMsg), boost::bind(&SSLSocket::HandleWrite, this,
boost::asio::placeholders::error, boost::asio::placeholders::bytes_transferred));
}
catch (std::exception& e)
{
stringstream ss;
ss << "SSLSocket::ProcessSendRequests: threw an error - " << e.what() << ".\n";
Log.LogString(ss.str(), LogError);
// Stop();
}
ss.str(std::string());
ss << "SSLSocket::ProcessSendRequests: # bytes sent = " << BytesInMsg << "\n";
Log.LogString(ss.str(), LogDebug2);
Log.LogBuf(pBuf, BytesInMsg, DisplayInHex, LogDebug3);
LockCode->Release();
}
else
{
// Nothing to send, so go into a wait state.
WaitResult = WaitForSingleObject(hEvent, INFINITE);
if (WaitResult != 0L)
{
Log.LogString("SSLSocket::ProcessSendRequests: WaitForSingleObject event error. Code = " + Logger::NumberToString(GetLastError()) + ". \n", LogError);
}
}
} while (ReqAlive);
Log.LogString("SSLSocket::ProcessSendRequests: Worker thread " + Logger::NumberToString(boost::this_thread::get_id()) + " done.\n", LogInfo);
}
catch (std::exception& e)
{
stringstream ss;
ss << "SSLSocket::ProcessSendRequests: threw an error - " << e.what() << ".\n";
Log.LogString(ss.str(), LogError);
Stop();
}
}
void SSLSocket::HandleWrite(const boost::system::error_code& error, size_t bytesTransferred)
{
// This method is called after a msg has been written out to the socket. Nothing to do really since reading is handled by the HandleRead method.
//
std::stringstream ss;
try
{
if (error)
{
ss << "SSLSocket::HandleWrite: failed - " << error.message() << ".\n";
Log.LogString(ss.str(), LogError);
Stop();
}
}
catch (std::exception& e)
{
stringstream ss;
ss << "SSLSocket::HandleHandshake: threw an error - " << e.what() << ".\n";
Log.LogString(ss.str(), LogError);
Stop();
}
}
void SSLSocket::RcvWorkerThread(SSLSocket* psSLS)
{
// This is the method that gets called when the receive thread is created by this class.
// This thread method focuses on processing messages received from the server.
//
// Since this has to be a static method, call a method on the class to handle server requests.
psSLS->InitAsynchIO();
}
void SSLSocket::InitAsynchIO()
{
// This method is responsible for initiating asynch i/o.
boost::system::error_code Err;
string s;
stringstream ss;
//
try
{
ss << "SSLSocket::InitAsynchIO: Worker thread - " << Logger::NumberToString(boost::this_thread::get_id()) << " started.\n";
Log.LogString(ss.str(), LogInfo);
// Enable the handlers for asynch i/o. The thread will hang here until the stop method has been called or an error occurs.
// Add a work object so the thread will be dedicated to handling asynch i/o.
boost::asio::io_service::work work(*IOService);
IOService->run();
Log.LogString("SSLSocket::InitAsynchIO: receive worker thread done.\n", LogInfo);
}
catch (std::exception& e)
{
stringstream ss;
ss << "SSLSocket::InitAsynchIO: threw an error - " << e.what() << ".\n";
Log.LogString(ss.str(), LogError);
Stop();
}
}
void SSLSocket::HandleConnect(const boost::system::error_code& error)
{
// This method is called asynchronously when the server has responded to the connect request.
std::stringstream ss;
try
{
if (!error)
{
pSocket->async_handshake(boost::asio::ssl::stream_base::client,
boost::bind(&SSLSocket::HandleHandshake, this, boost::asio::placeholders::error));
ss << "SSLSocket::HandleConnect: From worker thread " << Logger::NumberToString(boost::this_thread::get_id()) << ".\n";
Log.LogString(ss.str(), LogInfo);
}
else
{
// Log an error. This worker thread should exit gracefully after this.
ss << "SSLSocket::HandleConnect: connect failed to " << sClientIp << " : " << uiClientPort << ". Error: " << error.message() + ".\n";
Log.LogString(ss.str(), LogError);
Stop();
}
}
catch (std::exception& e)
{
stringstream ss;
ss << "SSLSocket::InitAsynchIO: threw an error - " << e.what() << ".\n";
Log.LogString(ss.str(), LogError);
Stop();
}
}
void SSLSocket::HandleHandshake(const boost::system::error_code& error)
{
// This method is called asynchronously when the server has responded to the handshake request.
std::stringstream ss;
try
{
if (!error)
{
// Try to send the first message that the server is expecting. This msg tells the server we want to connect.
// The first 4 bytes specifies the msg length after the first 4 bytes. The next 2 bytes specifies the msg type.
// The next 4 bytes specifies the source code. The next 13 bytes specifies the msg "AttackPoker".
// The next 2 bytes specifies the locale length. The last 2 bytes specifies the locale - en for English.
//
unsigned char Msg[27] = {0x17, 0x00, 0x00, 0x00, 0x06, 0x00, 0x01, 0x00, 0x00, 0x00, 0x0b, 0x00, 0x41,
0x74, 0x74, 0x61, 0x63, 0x6b, 0x50, 0x6f, 0x6b, 0x65, 0x72, 0x02, 0x00, 0x65, 0x6e};
boost::system::error_code Err;
//
if (pSSLLobby == this)
LobbySocketOpen = true;
sClientIp = pSocket->lowest_layer().remote_endpoint().address().to_string();
uiClientPort = pSocket->lowest_layer().remote_endpoint().port();
ReqAlive = true;
// boost::asio::async_write(*pSocket, boost::asio::buffer(Msg), boost::bind(&SSLSocket::HandleFirstWrite, this,
// boost::asio::placeholders::error, boost::asio::placeholders::bytes_transferred));
int Count = boost::asio::write(*pSocket, boost::asio::buffer(Msg), boost::asio::transfer_exactly(27), Err);
if (Err)
{
ss << "SSLSocket::HandleHandshake: write failed - " << error.message() << ".\n";
Log.LogString(ss.str(), LogInfo);
}
HandleFirstWrite(Err, Count);
// boost::asio::async_write(pSocket, boost::asio::buffer(Msg, 27), boost::bind(&SSLSocket::HandleWrite, this,
// boost::asio::placeholders::error, boost::asio::placeholders::bytes_transferred));
ss.str("");
ss << "SSLSocket::HandleHandshake: From worker thread " << boost::this_thread::get_id() << ".\n";
}
else
{
ss << "SSLSocket::HandleHandshake: failed - " << error.message() << ".\n";
IOService->stop();
}
Log.LogString(ss.str(), LogInfo);
}
catch (std::exception& e)
{
stringstream ss;
ss << "SSLSocket::HandleHandshake: threw an error - " << e.what() << ".\n";
Log.LogString(ss.str(), LogError);
Stop();
}
}
void SSLSocket::HandleFirstWrite(const boost::system::error_code& error, size_t bytesTransferred)
{
// This method is called after a msg has been written out to the socket.
std::stringstream ss;
try
{
if (!error)
{
// boost::asio::async_read(pSocket, boost::asio::buffer(reply_, bytesTransferred), boost::bind(&SSLSocket::handle_read,
// this, boost::asio::placeholders::error, boost::asio::placeholders::bytes_transferred));
// boost::asio::async_read(pSocket, boost::asio::buffer(reply_, 84), boost::bind(&SSLSocket::handle_read,
// this, boost::asio::placeholders::error, boost::asio::placeholders::bytes_transferred));
// Locking CodeLock(ReadLock); // Single thread the code.
// Signal the other threads that msgs are now ready to be sent and received.
// boost::asio::async_read(pSocket, boost::asio::buffer(pRepBuf), boost::asio::transfer_exactly(4), boost::bind(&SSLSocket::HandleRead,
// this, boost::asio::placeholders::error, boost::asio::placeholders::bytes_transferred));
//
// Notify the UI that we are now connected. Create a 6 byte msg for this.
pDataBuf = BufMang.GetPtr(6);
BYTE* p = pDataBuf;
// Create msg type 500
*p = 244;
*++p = 1;
CallbackFunction(this, 2, (void*)pDataBuf);
// Get the 1st 4 bytes of the next msg, which is always the length of the that msg.
pDataBuf = BufMang.GetPtr(MsgLenBytes);
// int i1=1,i2=2,i3=3,i4=4,i5=5,i6=6,i7=7,i8=8,i9=9;
// (boost::bind(&nine_arguments,_9,_2,_1,_6,_3,_8,_4,_5,_7))
// (i1,i2,i3,i4,i5,i6,i7,i8,i9);
// boost::asio::read(*pSocket, boost::asio::buffer(pReqBuf, MsgLenBytes), boost::asio::transfer_exactly(MsgLenBytes), Err);
// boost::asio::async_read(pSocket, boost::asio::buffer(pReqBuf, MsgLenBytes), boost::bind(&SSLSocket::HandleRead, _1,_2,_3))
// (this, pReqBuf, boost::asio::placeholders::error, boost::asio::placeholders::bytes_transferred);
// boost::asio::async_read(*pSocket, boost::asio::buffer(reply_), boost::asio::transfer_exactly(ByteCount), boost::bind(&Client::handle_read,
// this, boost::asio::placeholders::error, boost::asio::placeholders::bytes_transferred));
// boost::asio::async_write(*pSocket, boost::asio::buffer(pDataBuf, MsgLenBytes), boost::bind(&SSLSocket::HandleWrite, this,
// boost::asio::placeholders::error, boost::asio::placeholders::bytes_transferred));
try
{
LockCode->Acquire(); // Single thread the code.
// Locking CodeLock(SocketLock); // Single thread the code.
boost::asio::async_read(*pSocket, boost::asio::buffer(pDataBuf, MsgLenBytes), boost::bind(&SSLSocket::HandleRead, this,
boost::asio::placeholders::error, boost::asio::placeholders::bytes_transferred));
}
catch (std::exception& e)
{
stringstream ss;
ss << "SSLSocket::HandleFirstWrite: threw an error - " << e.what() << ".\n";
Log.LogString(ss.str(), LogError);
Stop();
}
LockCode->Release();
}
else
{
ss << "SSLSocket::HandleFirstWrite: failed - " << error.message() << ".\n";
Log.LogString(ss.str(), LogError);
Stop();
}
}
catch (std::exception& e)
{
stringstream ss;
ss << "SSLSocket::HandleFirstWrite: threw an error - " << e.what() << ".\n";
Log.LogString(ss.str(), LogError);
Stop();
}
}
void SSLSocket::HandleRead(const boost::system::error_code& error, size_t bytesTransferred)
{
// This method is called to process an incomming message.
//
std::stringstream ss;
int ByteCount;
try
{
// ss << "SSLSocket::HandleRead: From worker thread " << boost::this_thread::get_id() << ".\n";
// Log.LogString(ss.str(), LogInfo);
// Set to exit this thread if the user is done.
if (!ReqAlive)
{
// IOService->stop();
return;
}
if (!error)
{
// Get the number of bytes in the message.
if (bytesTransferred == 4)
{
ByteCount = BytesToInt(pDataBuf);
}
else
{
// Call the C# callback method that will handle the message.
ss << "SSLSocket::HandleRead: From worker thread " << boost::this_thread::get_id() << "; # bytes transferred = " << bytesTransferred << ".\n";
Log.LogString(ss.str(), LogDebug2);
if (bytesTransferred > 0)
{
Log.LogBuf(pDataBuf, (int)bytesTransferred, true, LogDebug3);
Log.LogString("SSLSocket::HandleRead: sending msg to the C# client.\n\n", LogDebug2);
CallbackFunction(this, bytesTransferred, (void*)pDataBuf);
}
else
{
// # of bytes transferred = 0. Don't do anything.
bytesTransferred = 0; // For debugging.
}
// Prepare to read in the next message length.
ByteCount = MsgLenBytes;
}
pDataBuf = BufMang.GetPtr(ByteCount);
boost::system::error_code Err;
// boost::asio::async_read(pSocket, boost::asio::buffer(pDataBuf, ByteCount), boost::bind(&SSLSocket::HandleRead,
// this, boost::asio::placeholders::error, boost::asio::placeholders::bytes_transferred));
LockCode->Acquire(); // Single thread the code.
// Locking CodeLock(SocketLock); // Single thread the code.
try
{
boost::asio::async_read(*pSocket, boost::asio::buffer(pDataBuf, ByteCount), boost::bind(&SSLSocket::HandleRead,
this, boost::asio::placeholders::error, boost::asio::placeholders::bytes_transferred));
// boost::asio::read(pSocket, boost::asio::buffer(reply_), boost::asio::transfer_exactly(ByteCount), Err);
}
catch (std::exception& e)
{
stringstream ss;
ss << "SSLSocket::HandleRead: threw this error - " << e.what() << ".\n";
Log.LogString(ss.str(), LogError);
// Stop();
}
LockCode->Release();
}
else
{
Log.LogString("SSLSocket::HandleRead failed: " + error.message() + "\n", LogError);
Stop();
}
}
catch (std::exception& e)
{
stringstream ss;
ss << "SSLSocket::HandleRead: threw an error - " << e.what() << ".\n";
Log.LogString(ss.str(), LogError);
Stop();
}
}
void SSLSocket::Stop()
{
// This method calls the shutdown method on the socket in order to stop reads or writes that might be going on. If this is not done, then an exception will be thrown
// when it comes time to delete this object.
//
boost::system::error_code EC;
try
{
// This method can be called from the handler as well. So once the ShuttingDown flag is set, don't go throught the same code again.
if (ShuttingDown)
return;
LockCode->Acquire(); // Single thread the code.
if (!ShuttingDown)
{
ShuttingDown = true;
pSocket->next_layer().cancel();
pSocket->shutdown(EC);
if (EC)
{
stringstream ss;
ss << "SSLSocket::Stop: socket shutdown error - " << EC.message() << ".\n";
// Log.LogString(ss.str(), LogError); // Usually get this - probably not an error.
}
else
{
pSocket->next_layer().close();
}
delete pSocket;
pSocket = 0;
ReqAlive = false;
SetEvent(hEvent);
IOService->stop();
LobbySocketOpen = false;
WorkerThreads.join_all();
}
LockCode->Release();
delete LockCode;
LockCode = 0;
}
catch (std::exception& e)
{
stringstream ss;
ss << "SSLSocket::HandleRead: threw an error - " << e.what() << ".\n";
Log.LogString(ss.str(), LogError);
Stop();
}
}
Now using a boost thread to read and buffer the data from the serial port. I can now read the serial data as a stream, but also have the ability to peek, do non-blocking and blocking reads.
I wrapped the serial io in a boost thread -- just couldn't make the asio callbacks work the way I wanted. I'm now buffering the read data in the thread, but still using blocking writes to keep the latency as low as possible.
I'm writing a C++ program to communicate with multiple devices - an Arduino Pro Mini w/ATmega328 and an Arduino Uno with GPS shield - simultaneously via COM port, and I'm using the boost::asio package. I'm no C++ guru, so I occasionally get muddled trying to figure out where/when to pass by reference or value; I think this is one of those cases.
Here's the code that sets up the serial ports for communication
std::string IMU_COM_PORT = "COM5"; // or whatever... it varies
int IMU_BAUD_RATE = 57600;
std::string ARDUINO_COM_PORT = "COM3"; // also varies
int ARDUINO_BAUD_RATE = 115200;
int main() {
std::vector<boost::thread *> sensorThreads;
int sensorThreadCount = 0;
using namespace::boost::asio;
// get the COM port handle for the IMU sensor
boost::asio::io_service IMU_io;
boost::asio::serial_port IMU_port(IMU_io);
IMU_port.open(IMU_COM_PORT);
if (!IMU_port.is_open()) {
cerr << "Failed to connect to IMU." << endl;
}
else {
IMU_port.set_option(boost::asio::serial_port_base::baud_rate(IMU_BAUD_RATE));
// the IMU has to be put into triggered output mode with a specific command: #o0
// to put it back into continuous output mode we can use this command: #o1
std::string IMU_init_cmd = "#o0";
boost::asio::write(IMU_port, boost::asio::buffer(IMU_init_cmd.c_str(), IMU_init_cmd.size()));
// now the IMU should be ready
sensorThreads.push_back(new boost::thread(testIMUThread, IMU_port));
sensorThreadCount++;
}
// get the COM port handle for the ARDUINO board with GPS sensor
boost::asio::io_service ARDUINO_io;
boost::asio::serial_port ARDUINO_port(ARDUINO_io);
ARDUINO_port.open(ARDUINO_COM_PORT);
if (!ARDUINO_port.is_open()) {
cerr << "Failed to connect to ARDUINO." << endl;
}
else {
ARDUINO_port.set_option(boost::asio::serial_port_base::baud_rate(ARDUINO_BAUD_RATE));
// now the ARDUINO w/GPS sensor should be ready
sensorThreads.push_back(new boost::thread(testGPSThread, ARDUINO_port));
sensorThreadCount++;
}
for (int i = 0; i < sensorThreadCount; i++) {
sensorThreads[i]->join();
delete sensorThreads[i];
}
}
My two test thread functions are basically identical, the only difference being the byte(s) they send to the devices indicating that something (my computer) is waiting to read data from them.
void testIMUThread(boost::asio::serial_port *port) {
while(true) { cout << readCOMLine(port, "#f") << endl; }
}
void testGPSThread(boost::asio::serial_port *port) {
while(true) { cout << readCOMLine(port, "r") << endl; }
}
And both of these test threads make use of a single function that does the serial read/writes.
std::string readCOMLine(boost::asio::serial_port *port, std::string requestDataCmd) {
char c;
std::string s;
boost::asio::write(port, boost::asio::buffer(requestDataCmd.c_str(), requestDataCmd.size()));
while(true) {
boost::asio::read(port, boost::asio::buffer(&c,1));
switch(c) {
case '\r': break;
case '\n': return s;
default: s += c;
}
}
}
The above code does not compile but instead produces the following (in MSVC2010):
Error 11 error C2248: 'boost::asio::basic_io_object<IoObjectService>::basic_io_object' : cannot access private member declared in class 'boost::asio::basic_io_object<IoObjectService>' C:\boost_1_51_0\boost\asio\basic_serial_port.hpp
The error message is rather unhelpful - to me, at least - as I've not been able to track down the root cause. What am I missing here?
Thanks for your help.
Edit: Solutions (thanks to Sam Miller)
Using pass-by-reference (i.e. boost::ref())
sensorThreads.push_back(new boost::thread(testIMUThread, IMU_port));
sensorThreads.push_back(new boost::thread(testIMUThread, ARDUINO_port));
in main() become
sensorThreads.push_back(new boost::thread(testIMUThread, boost::ref(IMU_port)));
sensorThreads.push_back(new boost::thread(testIMUThread, boost::ref(ARDUINO_port)));
Then the workhorse and thread functions are modified as such
std::string readCOMLine(boost::asio::serial_port &port, std::string requestDataCmd) {
char c;
std::string s;
boost::asio::write(port, boost::asio::buffer(requestDataCmd.c_str(), requestDataCmd.size()));
while(true) {
boost::asio::read(port, boost::asio::buffer(&c,1));
switch(c) {
case '\r': break;
case '\n': return s;
default: s += c;
}
}
}
void testIMUThread(boost::asio::serial_port &port) {
while(true) { cout << readCOMLine(port, "#f") << endl; }
}
void testGPSThread(boost::asio::serial_port &port) {
while(true) { cout << readCOMLine(port, "r") << endl; }
}
On the other hand, if using pass-by-pointer, the thread initializations in main() look like this
sensorThreads.push_back(new boost::thread(testIMUThread, &IMU_port));
sensorThreads.push_back(new boost::thread(testIMUThread, &ARDUINO_port));
And the thread functions like this
void testIMUThread(boost::asio::serial_port *port) {
while(true) { cout << readCOMLine(port, "#f") << endl; }
}
void testGPSThread(boost::asio::serial_port *port) {
while(true) { cout << readCOMLine(port, "r") << endl; }
}
But I have to make some additional changes (on top of the input parameter modification) to the function that does all of the work - namely the read/write calls.
std::string readCOMLine(boost::asio::serial_port *port, std::string requestDataCmd) {
char c;
std::string s;
port->write_some(boost::asio::buffer(requestDataCmd.c_str(), requestDataCmd.size())); // this line
while(true) {
port->read_some(boost::asio::buffer(&c,1)); // and this line
switch(c) {
case '\r': break;
case '\n': return s;
default: s += c;
}
}
}
Both solutions compile and seem to work for my purposes.
Thanks!
boost::asio::serial_port objects are noncopyable
You should pass a reference
sensorThreads.push_back(new boost::thread(testIMUThread, boost::ref(IMU_port)));
or a pointer
sensorThreads.push_back(new boost::thread(testIMUThread, &IMU_port));
here's a coliru showing both methodologies.