From a721fe28dc2ca291c6ca84c828b42d35aa1e4681 Mon Sep 17 00:00:00 2001 From: john30 Date: Sun, 25 Jan 2015 19:04:28 +0100 Subject: [PATCH] reworked main loop, switched command line argument parsing to argp --- src/ebusd/main.cpp | 510 +++++++++++++++++++++++++++++++++++ src/ebusd/main.h | 68 +++++ src/ebusd/mainloop.cpp | 597 +++++++++++++++++++++++++++++++++++++++++ src/ebusd/mainloop.h | 180 +++++++++++++ 4 files changed, 1355 insertions(+) create mode 100644 src/ebusd/main.cpp create mode 100644 src/ebusd/main.h create mode 100644 src/ebusd/mainloop.cpp create mode 100644 src/ebusd/mainloop.h diff --git a/src/ebusd/main.cpp b/src/ebusd/main.cpp new file mode 100644 index 00000000..cf0a0272 --- /dev/null +++ b/src/ebusd/main.cpp @@ -0,0 +1,510 @@ +/* + * Copyright (C) John Baier 2014-2015 + * + * This file is part of ebusd. + * + * ebusd is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * ebusd is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with ebusd. If not, see http://www.gnu.org/licenses/. + */ + +#ifdef HAVE_CONFIG_H +#include +#endif + +#include "main.h" +#include "mainloop.h" +#include "bushandler.h" +#include "log.h" +#include +#include +#include +#include +#include +#include +#include + +/** the name of the PID file. */ +#define PID_FILE_NAME "/var/run/ebusd.pid" + +/** the opened PID file, or NULL. */ +static FILE* pidFile = NULL; + +/** true when forked into daemon mode. */ +static bool isDaemon = false; + +/** the program options. */ +static struct options opt = { + "/dev/ttyUSB0", // device + false, // noDeviceCheck + "/etc/ebusd", // configPath + false, // checkConfig + 5, // pollInterval + 0xFF, // address + false, // answer + 9400, // acquireTimeout + 2, // acquireRetries + 2, // sendRetries + 15000, // receiveTimeout + 5, // numberMasters + false, // foreground + 8888, // port + false, // localhost + "/var/log/ebusd.log", // logFile + false, // logRaw + false, // dump + "/tmp/ebus_dump.bin", // dumpFile + 100 // dumpSize +}; + +/** the @a MainLoop instance, or NULL. */ +static MainLoop* mainLoop = NULL; + +/** the version string of the program. */ +const char *argp_program_version = ""PACKAGE_STRING""; + +/** the report bugs to address of the program. */ +const char *argp_program_bug_address = ""PACKAGE_BUGREPORT""; + +/** the documentation of the program. */ +static const char argpdoc[] = + PACKAGE " - a daemon for access to eBUS devices."; + +/** the definition of the known program arguments. */ +static const struct argp_option argpoptions[] = { + {NULL, 0, NULL, 0, "Device settings:", 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 }, + + {NULL, 0, NULL, 0, "Message configuration settings:", 2 }, + {"configpath", 'c', "PATH", 0, "Read CSV config files from PATH [/etc/ebusd]", 0 }, + {"checkconfig", 1, NULL, 0, "Only check CSV config files, then stop", 0 }, + {"pollinterval", 2, "SEC", 0, "Poll for data every SEC seconds (0=disable) [5]", 0 }, + + {NULL, 0, NULL, 0, "E-Bus settings:", 3 }, + {"address", 'a', "ADDR", 0, "Use ADDR as own bus address [FF]", 0 }, + {"answer", 3, NULL, 0, "Actively answer to requests from other masters", 0 }, + {"acquiretimeout", 4, "USEC", 0, "Stop bus acquisition after USEC us [9400]", 0 }, + {"acquireretries", 5, "COUNT", 0, "Retry bus acquisition COUNT times [2]", 0 }, + {"sendretries", 6, "COUNT", 0, "Repeat failed sends COUNT times [2]", 0 }, + {"receivetimeout", 7, "USEC", 0, "Expect a slave to answer within USEC us [15000]", 0 }, + {"numbermasters", 8, "COUNT", 0, "Expect COUNT masters on the bus [5]", 0 }, + + {NULL, 0, NULL, 0, "Daemon settings:", 4 }, + {"foreground", 'f', NULL, 0, "Run in foreground", 0 }, + {"port", 'p', "PORT", 0, "Listen for client connections on PORT [8888]", 0 }, + {"localhost", 9, NULL, 0, "Listen on 127.0.0.1 interface only", 0 }, + + {NULL, 0, NULL, 0, "Log settings:", 5 }, + {"logfile", 'l', "FILE", 0, "Write log to FILE (only for daemon) [/var/log/ebusd.log]", 0 }, + {"logareas", 10, "AREAS", 0, "Only write log for matching AREAS: main,network,bus,update,all [all]", 0 }, + {"loglevel", 11, "LEVEL", 0, "Only write log below or equal to LEVEL: error/notice/info/debug [notice]", 0 }, + {"lograwdata", 12, NULL, 0, "Log each received/sent byte on the bus", 0 }, + + {NULL, 0, NULL, 0, "Dump settings:", 6 }, + {"dump", 'D', NULL, 0, "Enable dump of received bytes", 0 }, + {"dumpfile", 13, "FILE", 0, "Dump received bytes to FILE [/tmp/ebus_dump.bin]", 0 }, + {"dumpsize", 14, "SIZE", 0, "Make dump files no larger than SIZE kB [100]", 0 }, + + {NULL, 0, NULL, 0, NULL, 0 }, +}; + +/** + * The program argument parsing function. + * @param key the key from @a options. + * @param arg the option argument, or NULL. + * @param state the parsing state. + */ +error_t parse_opt(int key, char *arg, struct argp_state *state) +{ + struct options *opt = (struct options*)state->input; + result_t result = RESULT_OK; + switch (key) { + + // Device settings: + case 'd': // --device=/dev/ttyUSB0 + if (arg == NULL || arg[0] == 0) { + argp_error(state, "invalid device"); + return EINVAL; + } + opt->device = arg; + break; + case 'n': // --nodevicecheck + opt->noDeviceCheck = true; + break; + + // Message configuration settings: + case 'c': // --configpath=/etc/ebusd + if (arg == NULL || arg[0] == 0 || strcmp("/", arg) == 0) { + argp_error(state, "invalid configpath"); + return EINVAL; + } + opt->configPath = arg; + break; + case 1: // --checkconfig + opt->checkConfig = true; + break; + case 2: // --pollinterval=5 + opt->pollInterval = parseInt(arg, 10, 0, 3600, result); + if (result != RESULT_OK) { + argp_error(state, "invalid pollinterval"); + return EINVAL; + } + break; + + // E-Bus settings: + case 'a': // --address=FF + opt->address = parseInt(arg, 16, 0, 0xff, result); + if (result != RESULT_OK || !isMaster(opt->address)) { + argp_error(state, "invalid address"); + return EINVAL; + } + break; + case 3: // --answer + opt->answer = true; + break; + case 4: // --acquiretimeout=9400 + opt->acquireTimeout = parseInt(arg, 10, 1000, 100000, result); + if (result != RESULT_OK) { + argp_error(state, "invalid acquiretimeout"); + return EINVAL; + } + break; + case 5: // --acquireretries=2 + opt->acquireRetries = parseInt(arg, 10, 0, 10, result); + if (result != RESULT_OK) { + argp_error(state, "invalid acquireretries"); + return EINVAL; + } + break; + case 6: // --sendretries=2 + opt->sendRetries = parseInt(arg, 10, 0, 10, result); + if (result != RESULT_OK) { + argp_error(state, "invalid sendretries"); + return EINVAL; + } + break; + case 7: // --receivetimeout=15000 + opt->receiveTimeout = parseInt(arg, 10, 1000, 100000, result); + if (result != RESULT_OK) { + argp_error(state, "invalid receivetimeout"); + return EINVAL; + } + break; + case 8: // --numbermasters=5 + opt->numberMasters = parseInt(arg, 10, 1, 10, result); + if (result != RESULT_OK) { + argp_error(state, "invalid numbermasters"); + return EINVAL; + } + break; + + // Daemon settings: + case 'f': // --foreground + opt->foreground = true; + break; + case 'p': // --port=8888 + opt->port = parseInt(arg, 10, 1, 65535, result); + if (result != RESULT_OK) { + argp_error(state, "invalid port"); + return EINVAL; + } + break; + case 9: // --localhost + opt->localhost = true; + break; + + // Log settings: + case 'l': // --logfile=/var/log/ebusd.log + if (arg == NULL || arg[0] == 0 || strcmp("/", arg) == 0) { + argp_error(state, "invalid logfile"); + return EINVAL; + } + opt->logFile = arg; + break; + case 10: // --logareas=all + if (!setLogFacilities(arg)) { + argp_error(state, "invalid logareas"); + return EINVAL; + } + break; + case 11: // --loglevel=event + if (!setLogLevel(arg)) { + argp_error(state, "invalid loglevel"); + return EINVAL; + } + break; + case 12: // --lograwdata + opt->logRaw = true; + break; + + // Dump settings: + case 'D': // --dump + opt->dump = true; + break; + case 13: // --dumpfile=/tmp/ebus_dump.bin + if (arg == NULL || arg[0] == 0 || strcmp("/", arg) == 0) { + argp_error(state, "invalid dumpfile"); + return EINVAL; + } + opt->dumpFile = arg; + break; + case 14: // --dumpsize=100 + opt->dumpSize = parseInt(arg, 10, 1, 1000000, result); + if (result != RESULT_OK) { + argp_error(state, "invalid dumpsize"); + return EINVAL; + } + break; + + default: + return ARGP_ERR_UNKNOWN; + } + return 0; +} + +void daemonize() +{ + // fork off the parent process + pid_t pid = fork(); + + if (pid < 0) { + logError(lf_main, "fork() failed"); + exit(EXIT_FAILURE); + } + + // If we got a good PID, then we can exit the parent process + if (pid > 0) + exit(EXIT_SUCCESS); + + // At this point we are executing as the child process + + // Create a new SID for the child process and + // detach the process from the parent (normally a shell) + if (setsid() < 0) { + logError(lf_main, "setsid() failed"); + exit(EXIT_FAILURE); + } + + // Change the current working directory. This prevents the current + // directory from being locked; hence not being able to remove it. + if (chdir("/tmp") < 0) { // TODO + logError(lf_main, "daemon chdir() failed"); + exit(EXIT_FAILURE); + } + + // Close stdin, stdout and stderr + close(STDIN_FILENO); + close(STDOUT_FILENO); + close(STDERR_FILENO); + + // create pid file and try to lock it + umask(077); // leads to pidFile created in mode 0600 + pidFile = fopen(PID_FILE_NAME, "w"); + umask(027); // Set file permissions 750 + + if (pidFile != NULL) { + if (lockf(fileno(pidFile), F_TLOCK, 0) < 0 + || fprintf(pidFile, "%d\n", getpid()) <=0) { + fclose(pidFile); + pidFile = NULL; + } + } + if (pidFile == NULL) { + logError(lf_main, "can't open pidfile: " PID_FILE_NAME); + exit(EXIT_FAILURE); + } + + isDaemon = true; +} + +void closePidFile() +{ + if (pidFile != NULL) { + if (fclose(pidFile) != 0) + return; + + remove(PID_FILE_NAME); + } +} + +/** + * Helper method performing shutdown. + */ +void shutdown() +{ + // stop main loop and all dependent components + if (mainLoop != NULL) { + delete mainLoop; + mainLoop = NULL; + } + + // reset all signal handlers to default + signal(SIGHUP, SIG_DFL); + signal(SIGINT, SIG_DFL); + signal(SIGTERM, SIG_DFL); + + // delete daemon pid file if necessary + closePidFile(); + + logNotice(lf_main, "ebusd stopped"); + closeLogFile(); + + exit(EXIT_SUCCESS); +} + +/** + * The signal handling function. + * @param sig the received signal. + */ +void signalHandler(int sig) +{ + switch (sig) { + case SIGHUP: + logNotice(lf_main, "SIGHUP received"); + break; + case SIGINT: + logNotice(lf_main, "SIGINT received"); + shutdown(); + break; + case SIGTERM: + logNotice(lf_main, "SIGTERM received"); + shutdown(); + break; + default: + logNotice(lf_main, "undefined signal %s", strsignal(sig)); + break; + } +} + +/** + * Read the configuration files from the specified path. + * @param path the path from which to read the files. + * @param extension the filename extension of the files to read. + * @param logFunc the function to call for logging, or @a NULL to be silent. + * @param templates the available @a DataFieldTemplates. + * @param messages the @a MessageMap to load the messages into. + * @param verbose whether to verbosely log problems. + * @return the result code. + */ +static result_t readConfigFiles(const string path, const string extension, DataFieldTemplates* templates, MessageMap* messages, bool verbose) +{ + DIR* dir = opendir(path.c_str()); + + if (dir == NULL) + return RESULT_ERR_NOTFOUND; + + dirent* d = readdir(dir); + + while (d != NULL) { + if (d->d_type == DT_DIR) { + string fn = d->d_name; + + if (fn != "." && fn != "..") { + const string p = path + "/" + d->d_name; + result_t result = readConfigFiles(p, extension, templates, messages, verbose); + if (result != RESULT_OK) + return result; + } + } + else if (d->d_type == DT_REG || d->d_type == DT_LNK) { + string fn = d->d_name; + + if (fn.find(extension, (fn.length() - extension.length())) != string::npos + && fn != "_templates" + extension) { + const string p = path + "/" + d->d_name; + result_t result = messages->readFromFile(p, templates, verbose); + if (result != RESULT_OK) + return result; + } + } + + d = readdir(dir); + } + closedir(dir); + + return RESULT_OK; +}; + +result_t loadConfigFiles(DataFieldTemplates* templates, MessageMap* messages, bool verbose) { + logInfo(lf_main, "path to ebus configuration files: %s", opt.configPath); + string path = string(opt.configPath); + messages->clear(); + templates->clear(); + result_t result = templates->readFromFile(path+"/_templates.csv", NULL, verbose); + if (result == RESULT_OK) + logInfo(lf_main, "read templates"); + else + logError(lf_main, "error reading templates: %s", getResultCode(result)); + + result = readConfigFiles(path, ".csv", templates, messages, verbose); + if (result == RESULT_OK) + logInfo(lf_main, "read config files"); + else + logError(lf_main, "error reading config files: %s", getResultCode(result)); + + logNotice(lf_main, "message DB: %d ", messages->size()); + logNotice(lf_main, "updates DB: %d ", messages->size(true)); + logNotice(lf_main, "polling DB: %d ", messages->sizePoll()); + + return result; +} + + +/** + * Main method. + * + * @param argc the number of command line arguments. + * @param argv the command line arguments. + */ +int main(int argc, char* argv[]) +{ + struct argp argp = { argpoptions, parse_opt, NULL, argpdoc, NULL, NULL, NULL }; + if (argp_parse(&argp, argc, argv, 0, 0, &opt) != 0) + return EINVAL; + + + DataFieldTemplates templates; + MessageMap messages; + if (opt.checkConfig == true) { + logNotice(lf_main, "Performing configuration check..."); + + loadConfigFiles(&templates, &messages, true); + + messages.clear(); + templates.clear(); + + return 0; + } + + if (opt.foreground == false) { + setLogFile(opt.logFile); + daemonize(); // make me daemon + } + + // trap signals that we expect to receive + signal(SIGHUP, signalHandler); + signal(SIGINT, signalHandler); + signal(SIGTERM, signalHandler); + + logNotice(lf_main, "ebusd started"); + + // load configuration files + loadConfigFiles(&templates, &messages); + + // create the MainLoop and run it + mainLoop = new MainLoop(opt, &templates, &messages); + mainLoop->run(); + + // shutdown + shutdown(); +} diff --git a/src/ebusd/main.h b/src/ebusd/main.h new file mode 100644 index 00000000..cac44102 --- /dev/null +++ b/src/ebusd/main.h @@ -0,0 +1,68 @@ +/* + * Copyright (C) John Baier 2014-2015 + * + * This file is part of ebusd. + * + * ebusd is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * ebusd is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with ebusd. If not, see http://www.gnu.org/licenses/. + */ + +#ifndef MAIN_H_ +#define MAIN_H_ + +#include "result.h" +#include "data.h" +#include "message.h" + +/** \file main.h */ + +/** A structure holding all program options. */ +struct options +{ + const char* device; //!< eBUS device (serial device or ip:port) [/dev/ttyUSB0] + bool noDeviceCheck; //!< skip serial eBUS device test + + const char* configPath; //!< path to CSV configuration files [/etc/ebusd] + bool checkConfig; //!< only check CSV config files, then stop + int pollInterval; //!< poll interval in seconds, 0 to disable [5] + + unsigned char address; //!< own bus address [FF] + bool answer; //!< answer to requests from other masters + int acquireTimeout; //!< bus acquisition timeout in us [9400] + int acquireRetries; //!< number of retries for bus acquisition [2] + int sendRetries; //!< number of retries for failed sends [2] + int receiveTimeout; //!< timeout for receiving answer from slave in us [15000] + int numberMasters; //!< expected number of masters for arbitration [5] + + bool foreground; //!< run in foreground + int port; //!< port to listen for client connections [8888] + bool localhost; //!< listen on 127.0.0.1 interface only + + const char* logFile; //!< log file name [/var/log/ebusd.log] + bool logRaw; //!< log each received/sent byte on the bus + + bool dump; //!< dump received bytes + const char* dumpFile; //!< dump file name [/tmp/ebus_dump.bin] + int dumpSize; //!< maximum size of dump file in kB [100] +}; + +/** + * Load the message definitions from the configuration files. + * @param templates the @a DataFieldTemplates to load the templates into. + * @param messages the @a MessageMap to load the messages into. + * @param verbose whether to verbosely log problems. + * @return the result code. + */ +result_t loadConfigFiles(DataFieldTemplates* templates, MessageMap* messages, bool verbose=false); + +#endif // MAIN_H_ diff --git a/src/ebusd/mainloop.cpp b/src/ebusd/mainloop.cpp new file mode 100644 index 00000000..80bbb12c --- /dev/null +++ b/src/ebusd/mainloop.cpp @@ -0,0 +1,597 @@ +/* + * Copyright (C) John Baier 2014-2015 + * + * This file is part of ebusd. + * + * ebusd is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * ebusd is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with ebusd. If not, see http://www.gnu.org/licenses/. + */ + +#include "mainloop.h" +#include +#include +#include "main.h" +#include "log.h" +#include "data.h" + +using namespace std; + +MainLoop::MainLoop(const struct options opt, DataFieldTemplates* templates, MessageMap* messages) + : m_templates(templates), m_messages(messages), m_address(opt.address) +{ + // create Port + m_port = new Port(opt.device, opt.noDeviceCheck, opt.logRaw, &logRaw, opt.dump, opt.dumpFile, opt.dumpSize); + m_port->open(); + + if (m_port->isOpen() == false) + logError(lf_bus, "can't open %s", m_port->getDeviceName()); + + // create BusHandler + m_busHandler = new BusHandler(m_port, m_messages, + m_address, opt.answer, + opt.acquireRetries, opt.sendRetries, + opt.acquireTimeout, opt.receiveTimeout, + opt.numberMasters, opt.pollInterval); + m_busHandler->start("bushandler"); + + // create network + m_network = new Network(opt.localhost, opt.port, &m_netQueue); + m_network->start("network"); +} + +MainLoop::~MainLoop() +{ + if (m_network != NULL) { + delete m_network; + m_network = NULL; + } + + if (m_busHandler != NULL) { + m_busHandler->stop(); + m_busHandler->join(); + delete m_busHandler; + m_busHandler = NULL; + } + + if (m_port != NULL) { + delete m_port; + m_port = NULL; + } + + m_messages->clear(); + m_templates->clear(); +} + +void MainLoop::run() +{ + for (;;) { + string result; + + // recv new message from client + NetMessage* message = m_netQueue.remove(); + string data = message->getData(); + + time_t since, until; + time(&until); + bool listening = message->isListening(since); + if (listening == false) + since = until; + + if (data.length() > 0) { + data.erase(remove(data.begin(), data.end(), '\r'), data.end()); + data.erase(remove(data.begin(), data.end(), '\n'), data.end()); + + logNotice(lf_main, ">>> %s", data.c_str()); + + // decode message + if (strcasecmp(data.c_str(), "STOP") != 0) + result = decodeMessage(data, listening); + else + result = "done"; + + logNotice(lf_main, "<<< %s", result.c_str()); + result += "\n\n"; + } + if (listening == true) { + result += getUpdates(since, until); + } + + // send result to client + message->setResult(result, listening, until); + + // stop daemon + if (strcasecmp(data.c_str(), "STOP") == 0) + return; + } +} + +void MainLoop::logRaw(const unsigned char byte, bool received) { + if (received == true) + logNotice(lf_bus, "<%02x", byte); + else + logNotice(lf_bus, ">%02x", byte); +} + +string MainLoop::decodeMessage(const string& data, bool& listening) +{ + ostringstream result; + + // prepare data + string token, previous; + istringstream stream(data); + vector args; + bool escaped = false; + + while (getline(stream, token, ' ') != 0) { + if (escaped == true) { + args.pop_back(); + if (token.length() > 0 && token[token.length()-1] == '"') { + token = token.substr(0, token.length() - 1); + escaped = false; + } + token = previous + " " + token; + } + else if (token.length() == 0) // allow multiple space chars for a single delimiter + continue; + else if (token[0] == '"') { + token = token.substr(1); + if (token.length() > 0 && token[token.length()-1] == '"') + token = token.substr(0, token.length() - 1); + else + escaped = true; + } + args.push_back(token); + previous = token; + } + + if (args.size() == 0) + return "command missing"; + + + const char* str = args[0].c_str(); + 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, "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, "H") == 0 || strcasecmp(str, "HELP") == 0) + return executeHelp(); + + return "command not found"; +} + +string MainLoop::executeRead(vector &args) +{ + size_t argPos = 1; + time_t maxAge = 5*60; + bool verbose = false; + string clazz; + while (args.size() > argPos && args[argPos][0] == '-') { + if (args[argPos] == "-f") { + maxAge = 0; + } + else if (args[argPos] == "-v") { + verbose = true; + } + else if (args[argPos] == "-m") { + argPos++; + if (args.size() > argPos) { + result_t result; + maxAge = parseInt(args[argPos].c_str(), 10, 0, 24*60*60, result); + if (result != RESULT_OK) { + argPos = 0; // print usage + break; + } + } + else { + argPos = 0; // print usage + break; + } + } + else if (args[argPos] == "-c") { + argPos++; + if (argPos >= args.size()) { + argPos = 0; // print usage + break; + } + clazz = args[argPos]; + } + else { + argPos = 0; // print usage + break; + } + argPos++; + } + if (argPos == 0 || args.size() < argPos + 1 || args.size() > argPos + 2) + return "usage: 'read [-v] [-f] [-m seconds] [-c class] name [field]'"; + + if (args.size() == argPos + 2) + maxAge = 0; // force refresh to filter single field + + time_t now; + time(&now); + + Message* updateMessage = NULL; + if (maxAge > 0 && verbose == false) { + updateMessage = m_messages->find(clazz, args[argPos], false, true); + + if (updateMessage != NULL && updateMessage->getLastUpdateTime() + maxAge > now) + return updateMessage->getLastValue(); // TODO switch from last value to last master/slave to support verbose cached/polled values as well + // else: check poll data or read directly from bus + } + + Message* message = m_messages->find(clazz, args[argPos], false); + + if (message == NULL) { + if (updateMessage != NULL) + return "no data stored"; + else + return "message not defined"; + } + if (maxAge > 0 && message->getPollPriority() > 0 + && message->getLastUpdateTime() + maxAge > now) { + // get poll data + return message->getLastValue(); + } // else: read directly from bus + + SymbolString master; + istringstream input; + result_t ret = message->prepareMaster(m_address, master, input); + if (ret != RESULT_OK) { + logError(lf_main, "prepare read: %s", getResultCode(ret)); + return getResultCode(ret); + } + logInfo(lf_main, "read cmd: %s", master.getDataStr().c_str()); + + // send message + SymbolString slave; + ret = m_busHandler->sendAndWait(master, slave); + + ostringstream result; + if (ret == RESULT_OK) { + if (args.size() == argPos + 2) + ret = message->decode(pt_slaveData, slave, result, false, verbose, args[argPos + 1].c_str()); + else + ret = message->decode(pt_slaveData, slave, result, false, verbose); // decode data + } + if (ret != RESULT_OK) { + logError(lf_main, "read: %s", getResultCode(ret)); + return getResultCode(ret); + } + return result.str(); +} + +string MainLoop::executeWrite(vector &args) +{ + size_t argPos = 1; + if (args.size() > argPos && args[argPos] == "-h") { + argPos++; + + if (args.size() < argPos + 1) + return "usage: 'write -h ZZPBSBNNDx'"; + + ostringstream msg; + msg << hex << setw(2) << setfill('0') << static_cast(m_address) << setw(0); + while (argPos < args.size()) { + if ((args[argPos].length() % 2) != 0) { + return "invalid hex string"; + } + msg << args[argPos++]; + } + + SymbolString master(msg.str()); + if (isValidAddress(master[1]) == false) + return "invalid destination"; + + logNotice(lf_main, "write hex cmd: %s", master.getDataStr().c_str()); + + // send message + SymbolString slave; + result_t ret = m_busHandler->sendAndWait(master, slave); + + if (ret == RESULT_OK) { + if (master[1] == BROADCAST || isMaster(master[1])) + return "done"; + return slave.getDataStr(); + } + logError(lf_main, "write hex: %s", getResultCode(ret)); + return getResultCode(ret); + } + + if (args.size() != argPos + 3) + return "usage: 'write class name value[;value]*' or 'write -h ZZPBSBNNDx'"; + + Message* message = m_messages->find(args[argPos], args[argPos + 1], true); + + if (message == NULL) + return "message not defined"; + + SymbolString master; + istringstream input(args[argPos + 2]); + result_t ret = message->prepareMaster(m_address, master, input); + if (ret != RESULT_OK) { + logError(lf_main, "prepare write: %s", getResultCode(ret)); + return getResultCode(ret); + } + logInfo(lf_main, "write cmd: %s", master.getDataStr().c_str()); + + // send message + SymbolString slave; + ret = m_busHandler->sendAndWait(master, slave); + + ostringstream result; + if (ret == RESULT_OK) { + if (master[1] == BROADCAST || isMaster(master[1])) + return "done"; + + ret = message->decode(pt_slaveData, slave, result); // decode data + if (ret == RESULT_OK && result.str().empty() == true) + return "done"; + } + if (ret != RESULT_OK) { + logError(lf_main, "write: %s", getResultCode(ret)); + return getResultCode(ret); + } + return result.str(); +} + +string MainLoop::executeFind(vector &args) +{ + size_t argPos = 1; + bool verbose = false, withRead = true, withWrite = true, withPassive = true, first = true, onlyWithData = false; + string clazz; + while (args.size() > argPos && args[argPos][0] == '-') { + if (args[argPos] == "-v") + verbose = true; + else if (args[argPos] == "-r") { + if (first == true) { + first = false; + withWrite = withPassive = false; + } + withRead = true; + } + else if (args[argPos] == "-w") { + if (first == true) { + first = false; + withRead = withPassive = false; + } + withWrite = true; + } + else if (args[argPos] == "-p") { + if (first == true) { + first = false; + withRead = withWrite = false; + } + withPassive = true; + } + else if (args[argPos] == "-d") { + onlyWithData = true; + } + else if (args[argPos] == "-c") { + argPos++; + if (argPos >= args.size()) { + argPos = 0; // print usage + break; + } + clazz = args[argPos]; + } + else { + argPos = 0; // print usage + break; + } + argPos++; + } + if (argPos == 0 || args.size() < argPos || args.size() > argPos + 1) + return "usage: 'find [-v] [-r] [-w] [-p] [-d] [-c class] [name]'"; + + deque messages; + if (args.size() == argPos) + messages = m_messages->findAll(clazz, "", -1, false, withRead, withWrite, withPassive); + else + messages = m_messages->findAll(clazz, args[argPos], -1, false, withRead, withWrite, withPassive); + + bool found = false; + ostringstream result; + char str[34]; + for (deque::iterator it = messages.begin(); it < messages.end();) { + Message* message = *it++; + unsigned char dstAddress = message->getDstAddress(); + if (dstAddress == SYN) + continue; + time_t lastup = message->getLastUpdateTime(); + if (onlyWithData == true && lastup == 0) + continue; + if (found == true) + result << endl; + result << message->getClass() << " " << message->getName() << " = "; + if (lastup == 0) + result << "no data stored"; + else + result << message->getLastValue(); + if (verbose == true) { + if (lastup == 0) + sprintf(str, "ZZ=%02x", dstAddress); + else { + struct tm* td = localtime(&lastup); + sprintf(str, "ZZ=%02x, lastup=%04d-%02d-%02d %02d:%02d:%02d", + dstAddress, td->tm_year+1900, td->tm_mon+1, td->tm_mday, + td->tm_hour, td->tm_min, td->tm_sec); + } + result << " [" << str << "]"; + } + found = true; + } + if (found == false) + return "no message found"; + + return result.str(); +} + +string MainLoop::executeListen(vector &args, bool& listening) +{ + if (args.size() <= 1) { + if (listening == true) + return "listen continued"; + + listening = true; + return "listen started"; + } + + if (args.size() != 2 || args[1] != "stop") + return "usage: 'listen [stop]'"; + + listening = false; + return "listen stopped"; +} + +string MainLoop::executeScan(vector &args) +{ + if (args.size() <= 1) { + result_t result = m_busHandler->startScan(); + if (result == RESULT_OK) + return "scan initiated"; + + logError(lf_main, "scan: %s", getResultCode(result)); + return getResultCode(result); + } + + if (strcasecmp(args[1].c_str(), "FULL") == 0) { + result_t result = m_busHandler->startScan(true); + if (result == RESULT_OK) + return "done"; + + logError(lf_main, "full scan: %s", getResultCode(result)); + return getResultCode(result); + } + + if (strcasecmp(args[1].c_str(), "RESULT") == 0) { + ostringstream result; + m_busHandler->formatScanResult(result); + return result.str(); + } + + return "usage: 'scan'\n" + " 'scan full'\n" + " 'scan result'"; +} + +string MainLoop::executeLog(vector &args) +{ + if (args.size() != 3) + return "usage: 'log areas area,area,..' (areas: bas|net|bus|upd|all)\n" // TODO + " 'log level level' (level: error|event|trace|debug)"; + + bool result; + if (strcasecmp(args[1].c_str(), "AREAS") == 0) + result = setLogFacilities(args[2].c_str()); + else if (strcasecmp(args[1].c_str(), "LEVEL") == 0) + result = setLogLevel(args[2].c_str()); + else + return "usage: 'log areas area,area,..' (areas: bas|net|bus|upd|all)\n" // TODO + " 'log level level' (level: error|event|trace|debug)"; + + if (result == true) + return "done"; + + return "invalid area/level"; +} + +string MainLoop::executeRaw(vector &args) +{ + if (args.size() != 1) + return "usage: 'raw'"; + + bool enabled = !m_port->getLogRaw(); + m_port->setLogRaw(enabled); + + return enabled ? "raw output enabled" : "raw output disabled"; +} + +string MainLoop::executeDump(vector &args) +{ + if (args.size() != 1) + return "usage: 'dump'"; + + bool enabled = !m_port->getDumpRaw(); + m_port->setDumpRaw(enabled); + + return enabled ? "dump enabled" : "dump disabled"; +} + +string MainLoop::executeReload(vector &args) +{ + if (args.size() != 1) + return "usage: 'reload'"; + + // reload commands + result_t result = loadConfigFiles(m_templates, m_messages); + if (result == RESULT_OK) + return "done"; + + return getResultCode(result); +} + +string MainLoop::executeHelp() +{ + return "commands:\n" + " read - read ebus values 'read [-v] [-f] [-m seconds] [-c class] name [field]'\n" + " write - write ebus values 'write class name value[;value]*' or 'write -h ZZPBSBNNDx'\n" + " find - find ebus values 'find [-v] [-r] [-w] [-p] [-d] [-c class] [name]'\n" + " listen - listen for updates 'listen [stop]'\n" + " scan - scan ebus known addresses 'scan'\n" + " - scan ebus all addresses 'scan full'\n" + " - show scan results 'scan result'\n" + " log - change log areas 'log areas area,area,..' (areas: bas|net|bus|upd|all)\n"//TODO + " - change log level 'log level level' (level: error|event|trace|debug)\n" + " raw - toggle log raw data 'raw'\n" + " dump - toggle dump state 'dump'\n" + " reload - reload ebus configuration 'reload'\n" + " stop - stop daemon 'stop'\n" + " quit - close connection 'quit'\n" + " help - print this page 'help'"; +} + +string MainLoop::getUpdates(time_t since, time_t until) +{ + ostringstream result; + + deque messages; + messages = m_messages->findAll("", "", -1, false, true, true, true); + + for (deque::iterator it = messages.begin(); it < messages.end();) { + Message* message = *it++; + unsigned char dstAddress = message->getDstAddress(); + if (dstAddress == SYN) + continue; + time_t lastchg = message->getLastChangeTime(); + if (lastchg < since || lastchg >= until) + continue; + result << message->getClass() << " " << message->getName() << " = "; + result << message->getLastValue() << endl; + } + + return result.str(); +} diff --git a/src/ebusd/mainloop.h b/src/ebusd/mainloop.h new file mode 100644 index 00000000..5e995071 --- /dev/null +++ b/src/ebusd/mainloop.h @@ -0,0 +1,180 @@ +/* + * Copyright (C) John Baier 2014-2015 + * + * This file is part of ebusd. + * + * ebusd is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * ebusd is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with ebusd. If not, see http://www.gnu.org/licenses/. + */ + +#ifndef MAINLOOP_H_ +#define MAINLOOP_H_ + +#include "message.h" +#include "network.h" +#include "bushandler.h" + +/** \file mainloop.h */ + +using namespace std; + +/** + * The main loop handling requests from connected clients. + */ +class MainLoop +{ + +public: + /** + * Construct the main loop and create network and bus handling components. + * @param opt the program options. + * @param templates the @a DataFieldTemplates instance. + * @param messages the @a MessageMap instance. + */ + MainLoop(const struct options opt, DataFieldTemplates* templates, MessageMap* messages); + + /** + * Destructor. + */ + ~MainLoop(); + + /** + * Run the main loop. + */ + void run(); + + /** + * Add a client @a NetMessage to the queue. + * @param message the client @a NetMessage to handle. + */ + void addMessage(NetMessage* message) { m_netQueue.add(message); } + + /** + * Create a log message for a received/sent raw data byte. + * @param byte the raw data byte. + * @param received true if the byte was received, false if it was sent. + */ + static void logRaw(const unsigned char byte, bool received); + +private: + + /** the @a DataFieldTemplates instance. */ + DataFieldTemplates* m_templates; + + /** the @a MessageMap instance. */ + MessageMap* m_messages; + + /** the own master address for sending on the bus. */ + unsigned char m_address; + + /** the created @a Port instance. */ + Port* m_port; + + /** the created @a BusHandler instance. */ + BusHandler* m_busHandler; + + /** the created @a Network instance. */ + Network* m_network; + + /** the queue for @a NetMessage instances. */ + WQueue m_netQueue; + + /** + * Decode and execute client message. + * @param data the data string to decode (may be empty). + * @param listening set to true when the client is in listening mode. + * @return result string to send back to the client. + */ + string decodeMessage(const string& data, bool& listening); + + /** + * Execute the read command. + * @param args the arguments passed to the command (starting with the command itself). + * @return the result string. + */ + string executeRead(vector &args); + + /** + * Execute the write command. + * @param args the arguments passed to the command (starting with the command itself). + * @return the result string. + */ + string executeWrite(vector &args); + + /** + * Execute the find command. + * @param args the arguments passed to the command (starting with the command itself). + * @return the result string. + */ + string executeFind(vector &args); + + /** + * Execute the listen command. + * @param args the arguments passed to the command (starting with the command itself). + * @param listening set to true when the client is in listening mode. + * @return the result string. + */ + string executeListen(vector &args, bool& listening); + + /** + * Execute the scan command. + * @param args the arguments passed to the command (starting with the command itself). + * @return the result string. + */ + string executeScan(vector &args); + + /** + * Execute the log command. + * @param args the arguments passed to the command (starting with the command itself). + * @return the result string. + */ + string executeLog(vector &args); + + /** + * Execute the raw command. + * @param args the arguments passed to the command (starting with the command itself). + * @return the result string. + */ + string executeRaw(vector &args); + + /** + * Execute the dump command. + * @param args the arguments passed to the command (starting with the command itself). + * @return the result string. + */ + string executeDump(vector &args); + + /** + * Execute the reload command. + * @param args the arguments passed to the command (starting with the command itself). + * @return the result string. + */ + string executeReload(vector &args); + + /** + * Execute the help command. + * @return the result string. + */ + string executeHelp(); + + /** + * Get the updates received since the specified time. + * @param since the start time from which to add updates (inclusive). + * @param until the end time to which to add updates (exclusive). + * @return result string to send back to client. + */ + string getUpdates(time_t since, time_t until); + +}; + +#endif // MAINLOOP_H_