Skip to content

Code_Design

Carl edited this page Aug 6, 2022 · 9 revisions

4.1 Coding Introduction

Coding the cloudSmoker project proved to be one of the steepest learning curves that I faced during this project. I ended up with a largish code base over 1600 lines long (including debug scaffolding) and spanning ~20 library files!

At the project start, I was only a novice C coder (think Arduino "blink" program) and I really didn't have knowledge of the deeper particulars of C++ and, importantly, almost no understanding of object oriented programming concepts and the application of OOP within C++. This certainly changed over the course of this project and I've emerged with a much stronger coding foundation.

I started preparing for my coding journey by working through through a free online C++ course at learncpp.com. Working through this course was time consuming but certainly helped me get started with an understanding of the basics.

4.2 IDE

Given the size of this project, I decided to move away from the Arduino Integrated Development Environment (IDE) software and to Micorsoft's Visual Studio Code and the PlatformIO plugin. Although I was faced with another learning curve, the depth of features in a professional IDE like VS Code / PlatformIO certainly helped me code this large project.

4.3 Code Design

My philosophy was to try to code the project as effectively and professionally as possible. Recognising that the code was going to be lengthy, I did my best to modularise the code base by separating, as much as possible, functionality into multiple files consisting of both public and personal library modules. Not only did I hope that this would make the code easier to understand and to debug, but it would also potentially make the code more portable for future projects. At the end of the day, modularisation also turned out to be a major source of frustration and time, as pursuing this approach often led to compiler and linker errors until I learned to more effectively manage critical interaction issues between files including variable, function and object definition, declaration, scope, duration, passing arguments and return values. Regardless, I expect I still use too many global variables despite seeking to avoid them where possible!

Many times, I ended up writing a "wrapper" library to augment, extend or amalgamate public library functionality. If nothing else, these wrapper libraries allowed me to abstract away much of the setup, instantiation or initialisation code that would otherwise clutter up the main program. I often found the easiest approach was to use class inheritance to create a child-class which would then have the useful functionality of the parent class but allow me to extend the functionality through new methods.

After investigation and consideration, I decided that the core structure of my program would be best implemented within a State Machine framework. I really like the State Machine approach as it provides a clear, logical and methodological way to structure and code the required functionality.

I took the time to draw up a State Machine diagram for the cloudSmoker project and referred to this roadmap diagram often as I coded.


As seen in the state diagram, the required states can essentially be grouped into two main functions within the code: 1) menu display and control and 2) temperature measurement and reporting.

4.4 WiFi Management

Wifi management, fundamental to the ESP8266, is provided in the background. A side benefit of the State Machine approach is that it facilitates non-blocking code, critical to keeping the ESP8266 properly functioning. The ESP8266 must share resources between running the user code and microprocessor utility background activities, such as maintaining the internal WiFi and TCP processes. Every time the main loop() is repeated, the Arduino core is designed to have the microcontroller yield to these background utility processes. If your code functions take too long to read this yield point, the chip's watchdog timers (WDT) will trigger an unwanted hard reset.

Triggering a WDT takes quite some time, about 3 seconds for the software watchdog and 8 seconds for the hardware watchdog, which is forever in terms of processor cycles on a modern microcontroller. So, in practice, unless you write blocking code, for example using a blocking delay() call, you are unlikely to trigger WDT resets. The heart of the State Machine implementation is a non-blocking processState() method in the main loop which checks and implements state changes as required. Nevertheless, within the various libraries, I occasionally added a yield() statement within a for or while loops if I was concerned that the loop might be lengthy and trigger a WDT reset. Other programming tips to avoid WDT resets can be found in the blog post Beware of the (watch)Dog!.

4.5 Display and Control

Similar to the Bald Engineer experience with his Open Vapors Project, I found that a significant effort was required to code the menu system, display it on a simple 1602 (16 char by 2 lines) backlit LCD and provide menu interface through the rotary encoder and associated button switch. The linked article has some excellent tips and tricks on LCD buffering and display that I put to good use in my project.

Working with text in C / C++ on a memory-limited microcontroller is quite tricky and, as I painfully learned, full of gotchas. Newbies often gravitate to using Arduino's String class but, as explained in the classic blog post, The Evils of Arduino Strings, String objects have so many shortcomings, particularly heap memory fragmentation, so that a better approach, although not code efficient, is to use lower level C-strings. It was no surprise that my LCD library containing menu display functionality ended up being my largest library, at over 600 lines of code.

The low cost KY-20 rotary encoder module proved to be an incredibly useful interface tool once paired with a couple of key public libraries:

  1. newEncoder library by gfvalvo provided excellent functionality to flawlessly software debounce the encoder values using a state table approach, allowed the rotary encoder scale to be redefined as needed and also provided "wrap" ability where scale min/max limits were reached.
  2. Bounce2 and Yabl (yet another button library) made the integrated rotary encoder push-button switch quite useful with the Bounce2 library providing effective switch debouncing (without the hassles of hardware debouncing solutions) and the Yabl library extending the button functionality to recognise three button push types (gestures): tap, double tap and press-hold. In effect, one button becomes three, all dependant on the press-type!

I would not hesitate to use a rotary encoder again as an effective low-cost user interface tool in any new project now that I've worked through the library and interface issues.

4.5 Temperature Measurement and Reporting

The remainder of the state machine deals with reading and smoothing the thermistor measurements, converting ADC readings into temperature and then uploading the data into the cloud before finally entering a sleep (wait) state.

Note that although cloudSmoker can operate on either degrees Fahrenheit or Celsius basis, I made the decision to store all internal temperature values in degF and, if required, convert to Celsius only for display, as necessary.

ADS1015 ADC

I wrote a wrapper library for the ADS1015 ADC to abstract the set-up and measurement read functionality. See cwg_ads1015 library for processing details. I use three of the four analog channels (the ADS1015 has built-in multiplexing); the first is to measure supply voltage which is used in the ultimate calculation of temperature and the other two channels are for the Meat and Pit Probe thermistor voltage measurements.

To smooth out any read errors, I read in eleven consecutive values (as fast as the ADS1015 can process them; I coded a loop to check whether the ADS1015 is ready) and then use a clever public median filter library to filter out spurious values or data spikes and return a single median filtered value.

While testing the median filter library by running a continuous loop of data reading / filtering, I found that it crashed my ESP8266, giving me an exception stack dump before resetting the chip. Decoding the stack dump (another learning exercise!) and digging into the code led me to discover that the library had a memory leak; I fixed this by writing an appropriate destructor and the library author updated his code based on my pull request. Note that there are popular forks of this median filter library that are listed in PlatformIO / Arduino library managers that have not responded to pull requests to fix the memory leak. User beware!

Clone this wiki locally