I am easily seduced by the new. I read about how wonderful something is, and I fall like a sack of potatoes for the sales pitch. In this case I fell for both Go’s and Rust’s sales pitch, without completely thinking things through, like how I would transition complex software already written in one language (C++) into equivalents in either Go or Rust. I also too easily bought into the “better, faster, safer” code argument for both Go and Rust.
I’ve had some time to try out both languages and various Raspberry Pi device frameworks, some of them written in Go, another written in Javascript, and at least one written in C/C++. It turns out that the C/C++ framework, WiringPi, is the most complete, and the one that works best for me. The Javascript framework is a very close second. In spite of some intense personal research into Go and Rust, I’m still a better C++ programmer (or at least, I understand it best) than I am in either Go or Rust. All of this combines to lead me one inevitable decision: write my code in C++ with what works best right now. And if I want “better, faster, safer” software then (re-) learn best C++ coding practices and use them diligently. And not just C++ specific best practices, but good old fashioned common sense best software engineering practices. If there’s one thing I’ve learned over time, it’s that no language will save you from a poor design: above all, KISS.
With that in mind, here’s the latest GPIO manipulation application source.
#include <wiringPi.h>#include <boost/algorithm/string.hpp>#include <boost/tokenizer.hpp>#include <iostream>#include <fstream>#include <sstream>struct glyph {unsigned int GPIOpindata = 0;int delayInMillis = 0;};std::vector<glyph> glyphs;bool loadBlinkData(std::string const fileName) {std::ifstream inFile(fileName);glyphs.clear();if (not inFile.is_open()) {std::cout << "Could not open " << fileName << std::endl;return false;}std::cout << "Using " << fileName << std::endl;std::string inLine;while (std::getline(inFile, inLine)) {boost::trim(inLine);if (not inLine.length() or inLine[0] == '#') {continue;}boost::tokenizer<> tokens(inLine);glyph g;for(auto &&token: tokens) {if (token.find("x") != std::string::npos or token.find("X") != std::string::npos) {g.GPIOpindata = std::stoul(token, nullptr, 16);}else {g.delayInMillis = std::stoi(token);}}glyphs.push_back(g);}inFile.close();return true;}std::string defaultDataFile("blinkdata.csv");int main (int argc, char *argv[]) {// Check to see if we passed another file on the command line.// If so, use it instead of the default.//if (argc > 1) {defaultDataFile = argv[1];}// Look to see if we can open and parse the file.// If not, stop.//if (not loadBlinkData(defaultDataFile)) {return 1;}// The input file was opened and parsed.// Now go to work.//wiringPiSetup();pinMode(0, OUTPUT);pinMode(1, OUTPUT);pinMode(2, OUTPUT);pinMode(3, OUTPUT);for (int i = 0; i < 5; ++i) { for (auto &&g : glyphs) { // Just a series of shifts and ANDs to create the bit necessary // write out to the GPIO pin. // digitalWrite(0, g.GPIOpindata & 0x1);digitalWrite(1, (g.GPIOpindata >> 1 ) & 0x1 );digitalWrite(2, (g.GPIOpindata >> 2 ) & 0x1 );digitalWrite(3, (g.GPIOpindata >> 3 ) & 0x1 );delay(g.delayInMillis);}}// Turn everything off.//digitalWrite(0, LOW);digitalWrite(1, LOW);digitalWrite(2, LOW);digitalWrite(3, LOW);return 0;}
Little improvements abound in this re-write.
- Idiomatic C++11 is used as much as possible to avoid older C++ hacks (see the use of std::stoul at line 41 to convert the hex data string, for example) and cleaner reading code (see the for loops at lines 38 and 83, for example).
- I use the One True Brace Style (1TBS) throughout, which frankly is just a variation of K&R brace style (the opening brace is on the same line as code). 1TBS helps these tired old eyes read code more easily. It also helps to minimize errors like Apple’s SSL/TLS “goto fail” bug.
- More descriptive variable names are used throughout, and variables are declared close to where they’re first needed. Scope actually means something.
- Rudimentary error checking now takes place, with quick exits that minimize resource usage on various failures. For example, the input data file is checked to see if it can be opened and parsed before any I/O devices are initialized.
- I performed a static code check with cppcheck to make sure I haven’t missed anything like improperly uninitialized variables.
- The code is compiled with ‘-Wall’ to get all warnings, and all warnings are treated as errors; they’re corrected.
- Write the namespace (std:: and boost::) in front of elements explicitly, rather than use the lazy way of declaring a namespace globally with “using namespace namespace” at the top of source. I know exactly what comes from where, which is quite useful when I revisit code six months after writing it.
It’s barely 100 lines long, but it’s an opportunity to get back into good C++ coding habits and to stay there than go off with a new, and unknown-to-me language.