I've recently started learning C++/Arduino and am working on abstracting some of my Arduino code in order to keep it more manageable.
I'm trying to construct a class with 2 arrays as attributes on it, one to store strings that represent commands, and a second one to store pointers to those functions.
The below code works (compiles), but when uploaded to the device both the listen and execute functions don't appear to work. I've searched around quite a lot, but can't find where I've gone wrong.
/* main.ino */
// SETUP
#include "SoftwareSerial.h"
SoftwareSerial bt(btRx, btTx);
#include "CMD.h"
const int cmdMax = 6;
ArriCMD cmd;
// COMMANDS
void cmdStatus()
{
Serial.println("OK");
}
// START
void setup()
{
Serial.begin(9600);
bt.begin(9600);
cmd.add("AH+STAT", cmdStatus);
}
void loop()
{
cmd.listen(bt);
}
/* ArriCMD.h */
#ifndef ArriCMD_h
#define ArriCMD_h
#include "Arduino.h"
#include "SoftwareSerial.h"
class ArriCMD
{
public:
ArriCMD();
void add(String cmd, void (*cb)());
void listen(SoftwareSerial serial);
void execute(String cmd);
private:
int _max = 16;
int _amt = 0;
String _cmds[16];
void (*_cbs[16])();
};
/* ArriCMD.cpp */
#include "Arduino.h"
#include "SoftwareSerial.h"
#include "ArriCMD.h"
ArriCMD::ArriCMD()
{
//
}
void ArriCMD::add(String cmd, void (*cb)())
{
if (_amt < _max) {
_cmds[_amt] = cmd;
_cbs[_amt] = *cb;
}
}
void ArriCMD::listen(SoftwareSerial serial)
{
if (serial.available()) {
String cmd = serial.readString();
Serial.print(cmd);
execute(cmd);
}
}
void ArriCMD::execute(String cmd)
{
for (int i = 0; i < _amt; i++) {
if (cmd == _cmds[i]) {
_cbs[i]();
}
}
}
While I've been programming for over a decade, C++ and microcontrollers are brand new to me, any and all help here would be hugely appreciated.
I do intend to open source these libraries, and the subsequent platforms they're built for, once I'm more comfortable with my code quality.
Looks like you forgot to increment your commands counter _amt
void ArriCMD::add(String cmd, void (*cb)())
{
if (_amt < _max) {
_cmds[_amt] = cmd;
_cbs[_amt] = *cb;
_amt++; // <-- here, don't you need it?
}
}
Apart from that, is there some particular reason for using raw array and raw function pointers in your code? I do not use Arduino, so I am not sure, but maybe this solution is a bit cleaner:
class ArriCMD
{
public:
ArriCMD();
void add(String cmd, std::function<void()> cb);
void listen(SoftwareSerial serial);
void execute(String cmd);
private:
std::map<String, std::function<void()> > _cmdMap;
};
EDIT:
How can I serialize an xml using tinyxml?
TiXmlDocument doc;
TiXmlElement * root;
root = new TiXmlElement( "Data" );
doc.SaveFile( "madeByHand.xml" );
Question 2: Can I deserialize the xml i've created with the same library tinyxml? does it have this advantage?
Have you tried reading the documentation? There's details of the print function on that first page. Those docs link to a tutorial which has an example of writing to and reading from a file:
#include <string>
#include <map>
using namespace std;
typedef std::map<std::string,std::string> MessageMap;
// a basic window abstraction - demo purposes only
class WindowSettings
{
public:
int x,y,w,h;
string name;
WindowSettings()
: x(0), y(0), w(100), h(100), name("Untitled")
{
}
WindowSettings(int x, int y, int w, int h, const string& name)
{
this->x=x;
this->y=y;
this->w=w;
this->h=h;
this->name=name;
}
};
class ConnectionSettings
{
public:
string ip;
double timeout;
};
class AppSettings
{
public:
string m_name;
MessageMap m_messages;
list<WindowSettings> m_windows;
ConnectionSettings m_connection;
AppSettings() {}
void save(const char* pFilename);
void load(const char* pFilename);
// just to show how to do it
void setDemoValues()
{
m_name="MyApp";
m_messages.clear();
m_messages["Welcome"]="Welcome to "+m_name;
m_messages["Farewell"]="Thank you for using "+m_name;
m_windows.clear();
m_windows.push_back(WindowSettings(15,15,400,250,"Main"));
m_connection.ip="Unknown";
m_connection.timeout=123.456;
}
};
int main(void)
{
// block: customise and save settings
{
AppSettings settings;
settings.m_name="HitchHikerApp";
settings.m_messages["Welcome"]="Don't Panic";
settings.m_messages["Farewell"]="Thanks for all the fish";
settings.m_windows.push_back(WindowSettings(15,25,300,250,"BookFrame"));
settings.m_connection.ip="192.168.0.77";
settings.m_connection.timeout=42.0;
settings.save("appsettings2.xml");
}
// block: load settings
{
AppSettings settings;
settings.load("appsettings2.xml");
printf("%s: %s\n", settings.m_name.c_str(),
settings.m_messages["Welcome"].c_str());
WindowSettings & w=settings.m_windows.front();
printf("%s: Show window '%s' at %d,%d (%d x %d)\n",
settings.m_name.c_str(), w.name.c_str(), w.x, w.y, w.w, w.h);
printf("%s: %s\n", settings.m_name.c_str(), settings.m_messages["Farewell"].c_str());
}
return 0;
}
Closed. This question is opinion-based. It is not currently accepting answers.
Want to improve this question? Update the question so it can be answered with facts and citations by editing this post.
Closed 7 months ago.
The community reviewed whether to reopen this question 7 months ago and left it closed:
Original close reason(s) were not resolved
Improve this question
I am looking for a small lightweight logging system in c++. I have found some existing frameworks but I don't need all of their features at this point in time. I primarily am looking for a small system that can for example configure the log level output file. I am looking for an existing solution as I don't want to reinvent the wheel.
I strongly recommend this simple logging system: http://www.drdobbs.com/cpp/201804215. It is composed of a single header file. I have successfully used it on Linux, Windows and Mac OS X.
You write to the log like this:
FILE_LOG(logWARNING) << "Ops, variable x should be " << expectedX << "; is " << realX;
I really like the stream syntax. It is unobtrusive, typesafe and expressive. The logging framework automatically adds a \n at the end of the line, plus date, time and indentation.
Configuring the logs is pretty easy:
FILELog::ReportingLevel() = logDEBUG3;
FILE* log_fd = fopen( "mylogfile.txt", "w" );
Output2FILE::Stream() = log_fd;
This framework is also easy to extend. At work, we have recently made some adaptations to it so that it now uses an std::ofstream instead of a FILE*. As a result, we are now able to add nice features such as encrypting the logs, by chaining the streams.
For anyone wanting a simple solution, I recommend: easylogging++
Single header only C++ logging library. It is extremely light-weight,
robust, fast performing, thread and type safe and consists of many
built-in features. It provides ability to write logs in your own
customized format. It also provide support for logging your classes,
third-party libraries, STL and third-party containers etc.
This library has everything built-in to prevent usage of external
libraries.
Simple example: (more advanced examples available on the link above).
#include "easylogging++.h"
INITIALIZE_EASYLOGGINGPP
int main(int argv, char* argc[]) {
LOG(INFO) << "My first info log using default logger";
return 0;
}
Example output inside a class:
2015-08-28 10:38:45,900 DEBUG [default] [user#localhost]
[Config::Config(const string)] [src/Config.cpp:7] Reading config file:
'config.json'
I tried log4cpp and boost::log but they are not as easy as this one.
EXTRA CONTENT: Minimal version - LOG header
I created a small code for even simpler applications based on easylogging but requires no initialization (be aware that it is probably not thread safe). Here is the code:
/*
* File: Log.h
* Author: Alberto Lepe <dev#alepe.com>
*
* Created on December 1, 2015, 6:00 PM
*/
#ifndef LOG_H
#define LOG_H
#include <iostream>
using namespace std;
enum typelog {
DEBUG,
INFO,
WARN,
ERROR
};
struct structlog {
bool headers = false;
typelog level = WARN;
};
extern structlog LOGCFG;
class LOG {
public:
LOG() {}
LOG(typelog type) {
msglevel = type;
if(LOGCFG.headers) {
operator << ("["+getLabel(type)+"]");
}
}
~LOG() {
if(opened) {
cout << endl;
}
opened = false;
}
template<class T>
LOG &operator<<(const T &msg) {
if(msglevel >= LOGCFG.level) {
cout << msg;
opened = true;
}
return *this;
}
private:
bool opened = false;
typelog msglevel = DEBUG;
inline string getLabel(typelog type) {
string label;
switch(type) {
case DEBUG: label = "DEBUG"; break;
case INFO: label = "INFO "; break;
case WARN: label = "WARN "; break;
case ERROR: label = "ERROR"; break;
}
return label;
}
};
#endif /* LOG_H */
Usage:
#include "Log.h"
int main(int argc, char** argv) {
//Config: -----(optional)----
structlog LOGCFG = {};
LOGCFG.headers = false;
LOGCFG.level = DEBUG;
//---------------------------
LOG(INFO) << "Main executed with " << (argc - 1) << " arguments";
}
This code print the message using "cout", but you can change it to use "cerr" or append a file, etc. I hope its useful to someone. (Note: I'm not C++ expert in any way, so this code may explode in extreme cases).
I recommend to try plog library (I'm the author). It's about 1000 lines of code, header only and easy to use:
#include <plog/Log.h>
int main()
{
plog::init(plog::debug, "Sample.log");
LOGD << "Hello log!";
LOGD_IF(true) << "conditional logging";
return 0;
}
all of the mentioned loggers so far make use of macros for logging calls. To me, that is so ugly, I don't care about what performance boost that gives, I won't go near it.
https://github.com/gabime/spdlog is what I like. Clean syntax, handles all typical usages. Fast and small. e.g. for a file logger it is:
auto my_logger = spd::basic_logger_mt("basic_logger", "logs/basic.txt");
my_logger->info("Some log message");
This question has my attempt with some fanciness. It is completely Standard C++ and makes no platform assumptions whatsoever. It basically consists of a temporary object used like this:
Debug(5) << "This is level 5 debug info.\n";
I'm sure you can figure out how to specify different files and other stuff when you have the basic layout. I tried to keep the class structured so that in a release build, every form of Debug output is removed as good as possible.
Mind you: if you specify a filename each time you construct it, and open the file and close it again, performance will suffer. In the case of multiple output files, it would certainly be best to have several static data members that open the different files when the program is run or if they are opened for the first time.
If you don't have size limitations on the project and you expect it to live a long time, I would suggest looking at Apache Log4cxx. It's not a small library, but it supports just about everything you ever wanted (including some things you didn't even knew you wanted) in logging, and it's portable.
In any larger project sooner or later you'll want your logging solution to do more than a "small logger class", so indeed why reinvent the wheel.
An update to Dr. Dobb's "A Lightweight Logger for C++":
There are actually a couple of loggers referred to in Dr. Dobb's. The first one Logging In C++ which is listed in one of the answers. I tried to use this one but the source is no longer available on the Dr. Dobb's site.
The second one that worked for me and that I recommend is A Lightweight Logger for C++ by Filip Janiszewski working at Nokia Siemens Networks. At first I had some problems getting this code to run so as I was searching for solutions, I ran across an update by the original author at: GitHub: fjanisze/logger. I found this code to be easy to understand, modify, and to use. It is thread safe and works with Visual Studio with Windows.
Another logger mentioned above is easylogging++ . When I first tried this one it looked promising. But when I added threading and sockets2 under Windows, it crashed. I did have the defines set for threading and Sock2 but I still couldn't get it to work so I can't recommend this one. The source code is also very complex so I had no chance to modify and fix it within a reasonable amount of time.
The above answers are all great.
Doubt anyone will ever see this answer, but this is what I use
https://github.com/asn10038/Cpp_Logger
Easy to set up after a configuration of 4-5 variable names in the .h file, and implemented without non standard dependencies.
Not header only but could be pretty easily.
Maybe this helps someone.
I, as well as many others, also answered this question with some code.
This isn't really "ready" in all ways, but it could be easily modified:
#pragma once
#include <codecvt>
#include <condition_variable>
#include <fstream>
#include <iostream>
#include <locale>
#include <memory>
#include <mutex>
#include <ostream>
#include <queue>
#include <sstream>
#include <string>
#include <thread>
#include <unordered_map>
#include <vector>
#ifdef _WIN32
#include <windows.h>
#endif
#include <string.h>
#define LOGL(level, msg) \
if (Loggy::isLevel(level)) { \
Loggy::writer(level, __FILE__, __LINE__) << msg; \
Loggy::queue(); \
}
#define LOG_FLUSH() \
{ \
Loggy::wait_queues(); \
}
#define LOGT(msg) LOGL(Loggy::LTRACE, msg)
#define LOGD(msg) LOGL(Loggy::LDEBUG, msg)
#define LOGI(msg) LOGL(Loggy::LINFO, msg)
#define LOGE(msg) LOGL(Loggy::LERROR, msg)
namespace Loggy {
using namespace std;
constexpr int DEFAULT_BUF_CNT = 1000;
constexpr const char *DEFAULT_TIME_FMT = "%Y%m%d.%H%M%S";
constexpr double DROP_NOTIFY_SECONDS = 5.0;
constexpr double FLUSH_SECONDS = 1.0;
enum {
LINVALID = 0,
LTRACE = 9,
LDEBUG = 10,
LINFO = 20,
LERROR = 40,
LWARN = 30,
LCRITICAL = 50,
LMAX = 50,
};
unordered_map<int, string> levelNames_ = {
{ LINVALID, "INVALID" },
{ LTRACE, "TRACE" },
{ LDEBUG, "DEBUG" },
{ LINFO, "INFO" },
{ LERROR, "ERROR" },
{ LWARN, "WARN" },
{ LCRITICAL, "CRITICAL" },
};
wstring str2w(const string &in)
{
#ifdef _WIN32
if (in.empty())
return std::wstring();
int size_needed = MultiByteToWideChar(CP_UTF8, 0, &in[0], (int)in.size(), NULL, 0);
std::wstring wstrTo(size_needed, 0);
MultiByteToWideChar(CP_UTF8, 0, &in[0], (int)in.size(), &wstrTo[0], size_needed);
return wstrTo;
#else
thread_local std::wstring_convert<std::codecvt_utf8<wchar_t>> wcu16;
return wcu16.from_bytes(in);
#endif
}
string w2str(const wstring &in)
{
#ifdef _WIN32
if (in.empty())
return std::string();
int size_needed
= WideCharToMultiByte(CP_UTF8, 0, &in[0], (int)in.size(), NULL, 0, NULL, NULL);
std::string strTo(size_needed, 0);
WideCharToMultiByte(CP_UTF8, 0, &in[0], (int)in.size(), &strTo[0], size_needed, NULL, NULL);
return strTo;
#else
thread_local std::wstring_convert<std::codecvt_utf8_utf16<wchar_t>> wcu8;
return wcu8.to_bytes( in );
#endif
}
template <class T> class SafeQueue {
public:
SafeQueue(void)
: q()
, m()
, c()
, x()
{
}
~SafeQueue(void) { lock_guard<mutex> lock(m); }
// Add an element to the queue.
void push(T t)
{
lock_guard<mutex> lock(m);
q.push(t);
c.notify_one();
}
// Get the "front"-element.
// If the queue is empty, wait till a element is avaiable.
T pop(void)
{
unique_lock<mutex> lock(m);
while (!x && q.empty()) {
// release lock as long as the wait and reaquire it afterwards.
c.wait(lock);
}
if (x) {
return T();
};
T val = q.front();
q.pop();
if (q.empty()) {
c.notify_all();
}
return val;
}
size_t size() { return q.size(); }
void join(void)
{
unique_lock<mutex> lock(m);
while (!q.empty()) {
c.wait(lock);
}
}
size_t drain(void)
{
unique_lock<mutex> lock(m);
std::queue<T> empty;
swap(q, empty);
c.notify_all();
return empty.size();
}
size_t quit()
{
x = true;
return drain();
}
private:
queue<T> q;
mutable mutex m;
condition_variable c;
bool x;
};
static string timestamp(const char format[], const time_t &rawtime)
{
struct tm timeinfo;
char buffer[120];
#ifdef _WIN32
localtime_s(&timeinfo, &rawtime);
#else
localtime_r(&rawtime, &timeinfo);
#endif
strftime(buffer, sizeof(buffer), format, &timeinfo);
return string(buffer);
}
#ifdef _WIN32
#define _LOGGY_CVT_FILENAME(s) s
#else
#define _LOGGY_CVT_FILENAME(s) Loggy::w2str(s)
#endif
class Output {
SafeQueue<wstring> queue_; // this should be first
wofstream fstream_;
wostream &wstream_;
size_t max_;
int level_;
size_t dropped_ = 0;
bool alive_ = true;
time_t firstDrop_ = 0;
std::thread thread_; // this must be last
public:
Output(wostream &s, int level, int max)
: wstream_(s)
, level_(level)
, max_(max)
, thread_(&Output::worker, this)
{
}
Output(const wstring &s, int level, size_t max)
: fstream_(_LOGGY_CVT_FILENAME(s), std::wofstream::out | std::wofstream::app)
, wstream_(fstream_)
, level_(level)
, max_(max)
, thread_(&Output::worker, this)
{
}
~Output()
{
alive_ = false;
dropped_ += queue_.quit();
if (dropped_) {
logDropped();
}
thread_.join();
}
void wait() { queue_.join(); wstream_.flush(); }
void logDropped()
{
wstringstream ws;
time_t t;
time(&t);
ws << Loggy::timestamp(DEFAULT_TIME_FMT, t).c_str();
ws << " dropped " << dropped_ << " entries";
queue_.push(ws.str());
dropped_ = 0;
}
void add(wstring &str, time_t &t)
{
if (alive_) {
if (max_ == 0 || queue_.size() < max_) {
queue_.push(str);
} else {
++dropped_;
if (dropped_ == 1) {
firstDrop_ = t;
} else if (difftime(t, firstDrop_) > DROP_NOTIFY_SECONDS) {
logDropped();
}
}
}
}
void worker()
{
int written = 0;
time_t lastFlush = 0;
while (alive_) {
if (!queue_.size() && written > 0) {
time_t t;
time(&t);
if (difftime(t, lastFlush) > FLUSH_SECONDS) {
wstream_.flush();
lastFlush = t;
written = 0;
}
}
auto t = queue_.pop();
if (alive_) {
wstream_ << t << std::endl;
written += 1;
}
}
}
};
class Log {
public:
~Log() { resetOutput(); };
int level_ = LINFO;
int trigFrom_ = LINVALID;
int trigTo_ = LINVALID;
int trigCnt_ = LINVALID;
string timeFormat_ = DEFAULT_TIME_FMT;
mutex mutex_;
deque<Output> outputs_;
Output default_output_;
vector<wstring> buffer_;
Log()
: default_output_(wcout, LINFO, 1) {};
bool isLevel(int level) { return level >= level_; }
void resetOutput()
{
lock_guard<mutex> lock(mutex_);
outputs_.clear();
}
void addOutput(const wstring &path, int level, int bufferSize)
{
lock_guard<mutex> lock(mutex_);
outputs_.emplace_back(path, level, bufferSize);
}
void addOutput(wostream &stream, int level, int bufferSize)
{
lock_guard<mutex> lock(mutex_);
outputs_.emplace_back(stream, level, bufferSize);
}
std::vector<const char *> getFiles()
{
std::vector<const char *> ret;
return ret;
}
void setTrigger(int levelFrom, int levelTo, int lookbackCount)
{
trigFrom_ = levelFrom;
trigTo_ = levelTo;
trigCnt_ = lookbackCount;
}
void setLevel(int level) { level_ = level; }
struct LastLog {
wstringstream ws;
time_t tm = 0;
};
static LastLog &lastLog()
{
thread_local LastLog ll_;
return ll_;
}
static const char *basename(const char *file)
{
const char *b = strrchr(file, '\\');
if (!b)
b = strrchr(file, '/');
return b ? b + 1 : file;
}
static const char *levelname(int level) { return levelNames_[level].c_str(); }
wostream &writer(int level, const char *file, int line)
{
auto &ll = lastLog();
time(&ll.tm);
ll.ws.clear();
ll.ws.str(L"");
return ll.ws << timestamp(timeFormat_.c_str(), ll.tm).c_str() << " " << basename(file)
<< ":" << line << " " << levelname(level) << " ";
}
void queue()
{
lock_guard<mutex> lock(mutex_);
auto &ll = lastLog();
auto s = ll.ws.str();
if (outputs_.empty()) {
default_output_.add(s, ll.tm);
} else {
for (auto &out : outputs_) {
out.add(s, ll.tm);
}
}
}
void wait_queues()
{
if (outputs_.empty()) {
default_output_.wait();
} else {
for (auto &out : outputs_) {
out.wait();
}
}
}
};
static Log &getInstance()
{
static Log l;
return l;
}
void resetOutput() { getInstance().resetOutput(); }
void addOutput(const wstring &path, int level = LDEBUG, int bufferSize = DEFAULT_BUF_CNT)
{
getInstance().addOutput(path, level, bufferSize);
}
void addOutput(wostream &stream, int level = LDEBUG, int bufferSize = DEFAULT_BUF_CNT)
{
getInstance().addOutput(stream, level, bufferSize);
}
void setTrigger(int levelFrom, int levelTo, int lookbackCount)
{
getInstance().setTrigger(levelFrom, levelTo, lookbackCount);
}
std::vector<const char *> getFiles() { return getInstance().getFiles(); }
void setLevel(int level) { getInstance().setLevel(level); }
bool isLevel(int level) { return getInstance().isLevel(level); }
wostream &writer(int level, const char *file, int line)
{
return getInstance().writer(level, file, line);
}
void queue() { getInstance().queue(); }
void wait_queues() { getInstance().wait_queues(); }
} // end namespace Loggy
Features:
writing to the log doesn't block on i/o
similar macros to other solutions (LOGE(blah << stream))
prefers discarding log entries to slowing down
lazy flushing
header only, very small, stl classes only
tested on osx/win/nix
time format is configurable
Missing stuff:
easy, flexible log formatting (predefining a macro would be fine)
triggers have an interface but don't work yet
microseconds aren't working yet
If anyone actually likes this solution in any way, lmk and I'll make a real repo out of it with tests, etc. It's pretty fast. Probably not as fast as speedlogger (a heavier feature complete library), but not sure.
Original gist:
https://gist.github.com/earonesty/977b14c93358fe9b9ee674baac5d42d7
i created a small logging class, cuz i had issues to include other examples in VSCode while compiling, here for a one file header:
#include <iostream>
#include <fstream>
#include <string>
#include <iomanip>
#include <ctime>
#include <sstream>
using namespace std;
class logging
{
private:
ofstream myfile;
std::string get_time()
{
auto t = std::time(nullptr);
auto tm = *std::localtime(&t);
std::ostringstream oss;
//2047-03-11 20:18:26
oss << std::put_time(&tm, "%Y-%m-%d-%H:%M:%S");
auto str = oss.str();
return str;
}
public:
logging(string filepath)
{
myfile.open (filepath);
}
~logging()
{
myfile.close();
}
void write(string line)
{
myfile << get_time() << " " << line <<std::endl;
}
};