From 7e98f485e13ce5530a24595c67c7bb7a3b0d1914 Mon Sep 17 00:00:00 2001 From: john30 Date: Fri, 1 May 2015 08:38:08 +0200 Subject: [PATCH] added read-only mode, added optional HTTP JSON port --- src/ebusd/main.cpp | 31 +++++-- src/ebusd/main.h | 4 +- src/ebusd/mainloop.cpp | 196 ++++++++++++++++++++++++++++++----------- src/ebusd/mainloop.h | 9 +- 4 files changed, 177 insertions(+), 63 deletions(-) diff --git a/src/ebusd/main.cpp b/src/ebusd/main.cpp index b664c016..1f4b5fe7 100644 --- a/src/ebusd/main.cpp +++ b/src/ebusd/main.cpp @@ -64,6 +64,7 @@ static bool isDaemon = false; static struct options opt = { "/dev/ttyUSB0", // device false, // noDeviceCheck + false, // readonly CONFIG_PATH, // configPath 0, // checkConfig 5, // pollInterval @@ -78,6 +79,7 @@ static struct options opt = { false, // foreground 8888, // port false, // localOnly + 0, // httpPort PACKAGE_LOGFILE, // logFile false, // logRaw false, // dump @@ -109,17 +111,19 @@ static const char argpdoc[] = #define O_MASCNT 9 #define O_GENSYN 10 #define O_LOCAL 11 -#define O_LOGARE 12 -#define O_LOGLEV 13 -#define O_LOGRAW 14 -#define O_DMPFIL 15 -#define O_DMPSIZ 16 +#define O_HTTPPT 12 +#define O_LOGARE 13 +#define O_LOGLEV 14 +#define O_LOGRAW 15 +#define O_DMPFIL 16 +#define O_DMPSIZ 17 /** the definition of the known program arguments. */ static const struct argp_option argpoptions[] = { {NULL, 0, NULL, 0, "Device options:", 1 }, {"device", 'd', "DEV", 0, "Use DEV as eBUS device (serial device or ip:port) [/dev/ttyUSB0]", 0 }, {"nodevicecheck", 'n', NULL, 0, "Skip serial eBUS device test", 0 }, + {"readonly", 'r', NULL, 0, "Only read from device, never write to it", 0 }, {NULL, 0, NULL, 0, "Message configuration options:", 2 }, {"configpath", 'c', "PATH", 0, "Read CSV config files from PATH [" CONFIG_PATH "]", 0 }, @@ -139,8 +143,9 @@ static const struct argp_option argpoptions[] = { {NULL, 0, NULL, 0, "Daemon options:", 4 }, {"foreground", 'f', NULL, 0, "Run in foreground", 0 }, - {"port", 'p', "PORT", 0, "Listen for client connections on PORT [8888]", 0 }, - {"localhost", O_LOCAL, NULL, 0, "Listen on 127.0.0.1 interface only", 0 }, + {"port", 'p', "PORT", 0, "Listen for command line connections on PORT [8888]", 0 }, + {"localhost", O_LOCAL, NULL, 0, "Listen for command line on 127.0.0.1 interface only", 0 }, + {"httpport", O_HTTPPT, "PORT", 0, "Listen for HTTP connections on PORT, 0 to disable [0]", 0 }, {NULL, 0, NULL, 0, "Log options:", 5 }, {"logfile", 'l', "FILE", 0, "Write log to FILE (only for daemon) [" PACKAGE_LOGFILE "]", 0 }, @@ -179,6 +184,9 @@ error_t parse_opt(int key, char *arg, struct argp_state *state) case 'n': // --nodevicecheck opt->noDeviceCheck = true; break; + case 'r': // --readonly + opt->readonly = true; + break; // Message configuration options: case 'c': // --configpath=/etc/ebusd @@ -266,6 +274,13 @@ error_t parse_opt(int key, char *arg, struct argp_state *state) case O_LOCAL: // --localhost opt->localOnly = true; break; + case O_HTTPPT: // --httpport + opt->httpPort = (uint16_t)parseInt(arg, 10, 1, 65535, result); + if (result != RESULT_OK) { + argp_error(state, "invalid port"); + return EINVAL; + } + break; // Log options: case 'l': // --logfile=/var/log/ebusd.log @@ -548,7 +563,7 @@ int main(int argc, char* argv[]) } // open the device - Device *device = Device::create(opt.device, !opt.noDeviceCheck, &logRawData); + Device *device = Device::create(opt.device, !opt.noDeviceCheck, opt.readonly, &logRawData); if (device == NULL) { logError(lf_main, "unable to create device %s", opt.device); return EINVAL; diff --git a/src/ebusd/main.h b/src/ebusd/main.h index 098ee90d..ca252759 100644 --- a/src/ebusd/main.h +++ b/src/ebusd/main.h @@ -32,6 +32,7 @@ struct options { const char* device; //!< eBUS device (serial device or ip:port) [/dev/ttyUSB0] bool noDeviceCheck; //!< skip serial eBUS device test + bool readonly; //!< read-only access to the device const char* configPath; //!< path to CSV configuration files [/etc/ebusd] int checkConfig; //!< check CSV config files (!=0) and optionally dump (2), then stop @@ -47,8 +48,9 @@ struct options bool generateSyn; //!< enable AUTO-SYN symbol generation bool foreground; //!< run in foreground - uint16_t port; //!< port to listen for client connections [8888] + uint16_t port; //!< port to listen for command line connections [8888] bool localOnly; //!< listen on 127.0.0.1 interface only + uint16_t httpPort; //!< optional port to listen for HTTP connections, 0 to disable [0] const char* logFile; //!< log file name [/var/log/ebusd.log] bool logRaw; //!< log each received/sent byte on the bus diff --git a/src/ebusd/mainloop.cpp b/src/ebusd/mainloop.cpp index 95eba5b5..ddf31075 100644 --- a/src/ebusd/mainloop.cpp +++ b/src/ebusd/mainloop.cpp @@ -52,7 +52,7 @@ MainLoop::MainLoop(const struct options opt, Device *device, DataFieldTemplates* m_busHandler->start("bushandler"); // create network - m_network = new Network(opt.localOnly, opt.port, &m_netQueue); + m_network = new Network(opt.localOnly, opt.port, opt.httpPort, &m_netQueue); m_network->start("network"); } @@ -84,21 +84,18 @@ void MainLoop::run() // pick the next message to handle NetMessage* message = m_netQueue.remove(); - string data = message->getData(); + string request = message->getRequest(); time_t since, until; time(&until); - bool listening = message->isListening(since); + bool listening = message->isListening(&since); if (!listening) since = until; bool connected = true; - if (data.length() > 0) { - data.erase(remove(data.begin(), data.end(), '\r'), data.end()); - data.erase(remove(data.begin(), data.end(), '\n'), data.end()); - - logDebug(lf_main, ">>> %s", data.c_str()); - result = decodeMessage(data, connected, listening, running); + if (request.length() > 0) { + logDebug(lf_main, ">>> %s", request.c_str()); + result = decodeMessage(request, message->isHttp(), connected, listening, running); logDebug(lf_main, "<<< %s", result.c_str()); if (result.length() == 0) @@ -115,7 +112,7 @@ void MainLoop::run() } } -string MainLoop::decodeMessage(const string& data, bool& connected, bool& listening, bool& running) +string MainLoop::decodeMessage(const string& data, const bool isHttp, bool& connected, bool& listening, bool& running) { ostringstream result; @@ -125,11 +122,16 @@ string MainLoop::decodeMessage(const string& data, bool& connected, bool& listen vector args; bool escaped = false; - while (getline(stream, token, ' ') != 0) { + char delim = ' '; + while (getline(stream, token, delim) != 0) { + if (isHttp && delim == '/' && token.length() > 0 && token[0] == '?') { + token.erase(0, 1); + delim = '&'; + } if (escaped) { args.pop_back(); if (token.length() > 0 && token[token.length()-1] == '"') { - token = token.substr(0, token.length() - 1); + token.erase(token.length() - 1, 1); escaped = false; } token = previous + " " + token; @@ -137,58 +139,64 @@ string MainLoop::decodeMessage(const string& data, bool& connected, bool& listen else if (token.length() == 0) // allow multiple space chars for a single delimiter continue; else if (token[0] == '"') { - token = token.substr(1); + token.erase(0, 1); if (token.length() > 0 && token[token.length()-1] == '"') - token = token.substr(0, token.length() - 1); + token.erase(token.length() - 1, 1); else escaped = true; } args.push_back(token); previous = token; + if (isHttp) + delim = '/'; } if (args.size() == 0) return executeHelp(); const char* str = args[0].c_str(); - if (args.size() == 2) { - // check for "CMD -h" - if (strcasecmp(args[1].c_str(), "-h") == 0 || strcasecmp(args[1].c_str(), "-?") == 0 || strcasecmp(args[1].c_str(), "--help") == 0) - args.clear(); // empty args is used as command help indicator - else if (strcasecmp(args[0].c_str(), "H") == 0 || strcasecmp(args[0].c_str(), "HELP") == 0) { // check for "HELP CMD" - str = args[1].c_str(); - args.clear(); // empty args is used as command help indicator + if (isHttp) { + if (strcmp(str, "GET") == 0) + return executeGet(args); + } else { + if (args.size() == 2) { + // check for "CMD -h" + if (strcasecmp(args[1].c_str(), "-h") == 0 || strcasecmp(args[1].c_str(), "-?") == 0 || strcasecmp(args[1].c_str(), "--help") == 0) + args.clear(); // empty args is used as command help indicator + else if (strcasecmp(args[0].c_str(), "H") == 0 || strcasecmp(args[0].c_str(), "HELP") == 0) { // check for "HELP CMD" + str = args[1].c_str(); + args.clear(); // empty args is used as command help indicator + } } + if (strcasecmp(str, "R") == 0 || strcasecmp(str, "READ") == 0) + return executeRead(args); + if (strcasecmp(str, "W") == 0 || strcasecmp(str, "WRITE") == 0) + return executeWrite(args); + if (strcasecmp(str, "F") == 0 || strcasecmp(str, "FIND") == 0) + return executeFind(args); + if (strcasecmp(str, "L") == 0 || strcasecmp(str, "LISTEN") == 0) + return executeListen(args, listening); + if (strcasecmp(str, "S") == 0 || strcasecmp(str, "STATE") == 0) + return executeState(args); + if (strcasecmp(str, "G") == 0 || strcasecmp(str, "GRAB") == 0) + return executeGrab(args); + if (strcasecmp(str, "SCAN") == 0) + return executeScan(args); + if (strcasecmp(str, "LOG") == 0) + return executeLog(args); + if (strcasecmp(str, "RAW") == 0) + return executeRaw(args); + if (strcasecmp(str, "DUMP") == 0) + return executeDump(args); + if (strcasecmp(str, "RELOAD") == 0) + return executeReload(args); + if (strcasecmp(str, "STOP") == 0) + return executeStop(args, running); + if (strcasecmp(str, "Q") == 0 || strcasecmp(str, "QUIT") == 0) + return executeQuit(args, connected); + if (strcasecmp(str, "H") == 0 || strcasecmp(str, "HELP") == 0) + return executeHelp(); } - if (strcasecmp(str, "R") == 0 || strcasecmp(str, "READ") == 0) - return executeRead(args); - if (strcasecmp(str, "W") == 0 || strcasecmp(str, "WRITE") == 0) - return executeWrite(args); - if (strcasecmp(str, "F") == 0 || strcasecmp(str, "FIND") == 0) - return executeFind(args); - if (strcasecmp(str, "L") == 0 || strcasecmp(str, "LISTEN") == 0) - return executeListen(args, listening); - if (strcasecmp(str, "S") == 0 || strcasecmp(str, "STATE") == 0) - return executeState(args); - if (strcasecmp(str, "G") == 0 || strcasecmp(str, "GRAB") == 0) - return executeGrab(args); - if (strcasecmp(str, "SCAN") == 0) - return executeScan(args); - if (strcasecmp(str, "LOG") == 0) - return executeLog(args); - if (strcasecmp(str, "RAW") == 0) - return executeRaw(args); - if (strcasecmp(str, "DUMP") == 0) - return executeDump(args); - if (strcasecmp(str, "RELOAD") == 0) - return executeReload(args); - if (strcasecmp(str, "STOP") == 0) - return executeStop(args, running); - if (strcasecmp(str, "Q") == 0 || strcasecmp(str, "QUIT") == 0) - return executeQuit(args, connected); - if (strcasecmp(str, "H") == 0 || strcasecmp(str, "HELP") == 0) - return executeHelp(); - return "ERR: command not found"; } @@ -284,7 +292,7 @@ string MainLoop::executeRead(vector &args) cacheMessage = message; // message is newer/better if (cacheMessage != NULL && (cacheMessage->getLastUpdateTime() + maxAge > now || (cacheMessage->isPassive() && cacheMessage->getLastUpdateTime() != 0))) { - result_t ret = cacheMessage->decodeLastData(result, verbose, fieldIndex==-2 ? NULL : fieldName.c_str(), fieldIndex); + result_t ret = cacheMessage->decodeLastData(result, false, verbose?df_verbose:df_standard, fieldIndex==-2 ? NULL : fieldName.c_str(), fieldIndex); if (ret != RESULT_OK) return getResultCode(ret); @@ -316,7 +324,7 @@ string MainLoop::executeRead(vector &args) ret = m_busHandler->sendAndWait(master, slave); if (ret == RESULT_OK) { - ret = message->decode(pt_slaveData, slave, result, false, verbose, fieldIndex==-2 ? NULL : fieldName.c_str(), fieldIndex); + ret = message->decode(pt_slaveData, slave, result, false, verbose?df_verbose:df_standard, fieldIndex==-2 ? NULL : fieldName.c_str(), fieldIndex); } if (ret < RESULT_OK) { logError(lf_main, "read: %s", getResultCode(ret)); @@ -530,7 +538,7 @@ string MainLoop::executeFind(vector &args) if (lastup == 0) result << "no data stored"; else - message->decodeLastData(result, verbose); + message->decodeLastData(result, verbose?df_verbose:df_standard); if (verbose) { if (lastup == 0) sprintf(str, "%02x", dstAddress); @@ -751,6 +759,88 @@ string MainLoop::executeHelp() " help|h Print help help [COMMAND]"; } +string MainLoop::executeGet(vector &args) +{ + size_t argPos = 1; + bool onlyWithData = true; + + deque messages; + if (args.size() >= argPos+2) + messages = m_messages->findAll(args[argPos], args[argPos+1], -1, false, true, false, true); + else if (args.size() == argPos+1) + messages = m_messages->findAll(args[argPos], "", -1, false, true, false, true); + else + messages = m_messages->findAll("", "", -1, false, true, false, true); + + bool first = true; + ostringstream result; + result << "{"; + string lastCircuit = ""; + result_t ret = RESULT_OK; + for (deque::iterator it = messages.begin(); it < messages.end();) { + Message* message = *it++; + time_t lastup = message->getLastUpdateTime(); + if (onlyWithData && lastup == 0) + continue; + unsigned char dstAddress = message->getDstAddress(); + if (dstAddress == SYN) + continue; + if (message->getCircuit() != lastCircuit) { + if (lastCircuit.length() > 0) + result << "\n },"; + lastCircuit = message->getCircuit(); + result << "\n \"" << lastCircuit << "\": {"; + first = true; + } + if (first) + first = false; + else + result << ","; + result << "\n \"" << message->getName() << "\": {"; + result << "\n \"lastup\": " << setw(0) << dec << static_cast(lastup); + if (lastup != 0) { + result << ",\n \"zz\": \"" << setfill('0') << setw(2) << hex << static_cast(dstAddress) << "\""; + result << ",\n \"fields\": ["; + ret = message->decodeLastData(result, false, df_json); + if (ret < RESULT_OK) + break; + result << "\n ]"; + } + result << ",\n \"passive\": " << (message->isPassive() ? "true" : "false"); + result << ",\n \"write\": " << (message->isWrite() ? "true" : "false"); + result << "\n }"; + } + if (lastCircuit.length() > 0) + result << "\n }"; + result << "\n}"; + + if (ret == RESULT_OK ) { + string str = result.str(); + result.str(""); + result.clear(); + result << "HTTP/1.0 200 OK\r\n"; + result << "Content-Type: application/json\r\n"; + result << "Content-Length: " << setw(0) << dec << static_cast(str.length()) << "\r\n"; + result << "\r\n"; + result << str; + } else { + result.str(""); + result.clear(); + result << "HTTP/1.0 "; + switch (ret) { + case RESULT_ERR_NOTFOUND: + result << "404 Not Found"; + break; + default: + result << "500 Internal Server Error"; + break; + } + result << "\r\n"; + result << "\r\n"; + } + return result.str(); +} + string MainLoop::getUpdates(time_t since, time_t until) { ostringstream result; diff --git a/src/ebusd/mainloop.h b/src/ebusd/mainloop.h index 4b26b636..a002be13 100644 --- a/src/ebusd/mainloop.h +++ b/src/ebusd/mainloop.h @@ -91,7 +91,7 @@ private: * @param running set to false when the server shall be stopped. * @return result string to send back to the client. */ - string decodeMessage(const string& data, bool& connected, bool& listening, bool& running); + string decodeMessage(const string& data, const bool isHttp, bool& connected, bool& listening, bool& running); /** * Execute the read command. @@ -193,6 +193,13 @@ private: */ string executeHelp(); + /** + * Execute the HTTP GET command. + * @param args the arguments passed to the command (starting with the command itself). + * @return the result string. + */ + string executeGet(vector &args); + /** * Get the updates received since the specified time. * @param since the start time from which to add updates (inclusive).