How to structure a project to be compilable by Arduino IDE and PlatformIO – Part 1

How to structure a project to be compilable by Arduino IDE and PlatformIO – Part 1

Arduino IDE and PlatformIO – Part 1 

How to structure a project to be compilable by both

Introduction

The goal of this post is to provide hints on how to structure an Arduino project to be able to compile successfully on both Arduino IDE and PlatformIO (through VSCode). Each compiler / IDE brings its own limitations causing little headaches to developers that want to apply simple software development practices on an Arduino project. That is, code separation in folders, third-party library management, common use of source code by multiple example projects etc.

The project and the initial structure

The project on which we are going to work on is a stripped down example of a device that reads a value from a sensor, gets internet connectivity and uploads the measurement. For better versatility, the project will support WiFi and NBIoT connectivity. This leads us to create two example sketches, one for each connectivity type. The initial set of header and sketch files are:

senseAndUpload/
├── nbiot.h
├── wifi.h
├── sensor.h
├── exampleWifi.ino
└── exampleNBIoT.ino
  • nbiot.h: Supports function “NBIoT::connect”, “NBIoT::upload”
  • wifi.h: Supports function “WiFi::connect”, “WiFi::upload”
  • sensor.h: Supports function “Sensor::readSensorLevel”
  • exampleWifi.ino: example sketch
#include "sensor.h"
#include "wifi.h"
void setup() {}
void loop()
{
  int val = Sensor::readSensorLevel();
  WiFi::connect();
  WiFi::upload;
}
  • exampleNBIoT.ino: example sketch
#include "sensor.h"
#include "nbiot.h"
void setup() {}
void loop()
{
  int val = Sensor::readSensorLevel();
  NBIoT::connect();
  NBIoT::upload;
}

Setting up folder structure based on Arduino Documentation

Each folder must contain at most 1 .ino file

The first issue that arise is that there can not be two .ino files at the same folder. Thus, lets place each .ino file at a separate folder (it is advised to name the folder the same as the .ino file that it includes):
senseAndUpload/
├── nbiot.h
├── wifi.h
├── sensor.h
├── exampleWifi/
│ └── exampleWifi.ino
└── exampleNBIoT/
└── exampleNBIoT.ino

Arduino IDE does not handle relative paths to previous folders

Issue number two (Arduino IDE only). Even though PlatformIO is able to handle any relative path, when using Arduino IDE, the .ino files can not include headers with relative path that refer to an upper level folder. This is a limitation because prior to the compilation, the project’s source code gets copied to a temporary location. That is, only the files and sub-folders that reside at the project’s folder will be copied. The solution: follow the rules of Arduino layout of folders and files, put all the reusable code in a “src” folder and keep an “examples” folder for multiple projects (each one at a separate subfolder). Our new structure is:
senseAndUpload/
├── examples/
| ├── exampleWifi/
| │ └── exampleWifi.ino
| └── exampleNBIoT/
| └── exampleNBIoT.ino
└── src/
├── nbiot.h
├── wifi.h
└── sensor.h

PlatformIO recognizes library headers only in subfolders

This project structure allows the ArduinoIDE to properly compile both projects but what about PlatformIO? Lets create the platformio.ini file for each project to elaborate further:
  • senseAndUpload/examples/exampleWifi/platformio.ini
[env]
lib_extra_dirs =
../../src

[platformio]
src_dir = .

[env:mkrwifi1010]
platform = atmelsam
board = mkrwifi1010
framework = arduino
  • senseAndUpload/examples/exampleNBIoT/platformio.ini
[env]
lib_extra_dirs =
../../src

[platformio]
src_dir = .

[env:mkrnb1500]
platform = atmelsam
board = mkrnb1500
framework = arduino
These PlatformIO configuration files instruct that the “src” folder must be considered as a library folder (lets leave the board configuration for later) and this leads us to our 3rd issue. PlatformIO can only detect header files that reside in subfolders at the defined paths of “lib_extra_dirs” tag. This means that our example projects can not “see” our header files so lets comply to this new requirement. Create two new folders, “src/device” and “src/networking” and move the header files:
senseAndUpload/
├── examples/
|   ├── exampleWifi/
|   │   └── exampleWifi.ino
|   └── exampleNBIoT/
|       └── exampleNBIoT.ino
└── src/
  ├── device/
  |   └── sensor.h
  └── networking/
      ├── nbiot.h
      └── wifi.h

Arduino IDE recognizes library headers only at the base folder of each library

Now PlatformIO will happily detect the requested headers, though, Arduino IDE interestingly enough starts to complain that is unable to detect the headers. Issue number 4 and now it is time to start compromises with conditional code for each IDE.

Since Arduino IDE detects only header files at the “src” folder, lets create one helper header file that will be including all the headers needed from the subfolders.
  • senseAndUpload/src/customLibs.h
#include <device/sensor.h>
#include <networking/nbiot.h>
#include <networking/wifi.h>
Having two different behaviors from our compilers that can not be satisfied by one solution, this means that our code needs to be conditional. Using the “exampleWifi.ino” code as reference, here is an approach on how to handle this:
  • exampleWifi.ino:
//#define ENABLE_ARDUINO_IDE_SUPPORT

#ifdef ENABLE_ARDUINO_IDE_SUPPORT
#include
#else
#include
#include
#endif

void setup() {}
void loop()
{
  int val = Sensor::readSensorLevel();
  WiFi::connect();
  WiFi::upload;
}
When building through Arduino IDE, the ENABLE_ARDUINO_IDE_SUPPORT has to be uncommented and the “helper” header file will be included, handling all includes of the needed header files in subfolders. On the other hand, when building through PlatformIO, the macro remains commented and the header files are directly included. Sure, it is not the prettiest solution though it is viable.

Summary

The correct design of a maintainable and expandable project structure in the Arduino world is an ongoing trial & error based on the capabilities and limitations of the underlying tools. This task gets even trickier when combining the limitations of more than one tools, in our case Arduino IDE and PlatformIO.

Until now we have seen that:

  • Generic
    • Each folder must contain at most 1 .ino file
  • Arduino IDE only
    • inherently identifies “src” and “examples” folders.
    • does not handle relative paths to previous folders
    • recognizes library headers only at the base folder of each library
  • PlatformIO only
    • recognizes library headers only in subfolders

In the next post, we explain how cpp files affect the source code and/or the folder structure and will conclude with a fully working template code that will contain all the recommendations of this post series.