Converting my many ino tabs to cpp/h files

Thanks to encouragement and tutelage from Gfvalvo and in0, I have some time to start experimenting / learning how to put my multiple ino tabs into cpp/h files.

Of course, it's mostly greek to me. Everything I know about C++ has been self-taught using the Arduino IDE (with lots of help from this forum), so namespace and classes is a somewhat foreign concept.

My first tab on this learning experience is wifi.ino. I use it to connect to my wifi and set up some global variables. I am getting a 'not in scope' error for a #define in the main ino file. I am probably missing something simple. Any tips would be appreciated.

Here is my main ino file:

#define SKETCH "cppTest2"
/*
 * Experiment with changing my multiple ino files into cpp/h files.
 */

#include "setupWiFi.h"

void setup(){
  Serial.begin(115200);
  Serial.println("\nTest begins");

  setup_wifi();        //Connect to my WiFi
  
}

void loop(){}

Here is my .cpp file:

//setupWiFi.cpp

#include "setupWiFi.h"
#include <Arduino.h> //needed for Serial.println()

// setup_wifi() function
// ============== Connect the ESP to the router ==============
// Connect to WiFi network so we can reach the MQTT broker and 
// publish messages to topics.

void setup_wifi() {

  byte mac[6];                    // The MAC address of your Wifi

  Serial.println(F("\n"));
  Serial.print(F("Connecting to "));
  Serial.println(MY_SSID);

  WiFi.mode(WIFI_STA);
  WiFi.begin(MY_SSID, MY_PASSWORD);
  while (WiFi.status() != WL_CONNECTED) {
    Serial.print(WiFi.status()); Serial.print(F(" "));
    delay(500);
  }
  Serial.println(F("\nWiFi connected, "));
  Serial.print(F("MAC Address: "));
  Serial.println(WiFi.macAddress());
  Serial.print(F("IP address: "));
  Serial.println(WiFi.localIP());
  Serial.print(F("RSSI: "));
  Serial.println(WiFi.RSSI());
  Serial.println();


  // Get the last three numbers of the mac address.
  // "4C:11:AE:0D:83:86" becomes "0D8386" in macBuffer.
  WiFi.macAddress(mac);
  snprintf(macBuffer, sizeof(macBuffer), "%02X%02X%02X", mac[3], mac[4], mac[5]);

  // Build hostName from prefix + last three bytes of the MAC address.
  strcpy(hostName, nodeName);
  strcat(hostName, "-");
  strcat(hostName, macBuffer);
  WiFi.hostname(hostName);
  Serial.print(F("hostname= "));
  Serial.println(hostName);
}

And finally the .h file:

//setupWiFi.h

#ifndef _setupWiFi_h
#define _setupWiFi_h

//Prototype of function in the .cpp file
void setup_wifi();

//--------------- WiFi declarations ---------------
// WiFi declarations
#include <ESP8266WiFi.h>        // Not needed if also using the Arduino OTA Library...
#include <Kaywinnet.h>          // WiFi credentials
char macBuffer[24];             // Holds the last three digits of the MAC, in hex.
char hostName[24];              // Holds nodeName + the last three bytes of the MAC address.
char nodeName[] = SKETCH;       // (SKETCH is a #define at the top of my main ino file)

#endif // _setupWiFi_h

And, of course, the errors:

Arduino: 1.8.15 (Windows 10), Board: "LOLIN(WEMOS) D1 R2 & mini, 80 MHz, Flash, Disabled (new aborts on oom), Disabled, All SSL ciphers (most compatible), 32KB cache + 32KB IRAM (balanced), Use pgm_read macros for IRAM/PROGMEM, 4MB (FS:2MB OTA:~1019KB), v2 Lower Memory, Disabled, None, Only Sketch, 921600"


In file included from C:\Users\steve\Documents\Arduino\cppTest2\setupWiFi.cpp:3:

setupWiFi.h:16:19: error: 'SKETCH' was not declared in this scope

   16 | char nodeName[] = SKETCH;       // (SKETCH is a #define at the top of my main ino file)

      |                   ^~~~~~

exit status 1

'SKETCH' was not declared in this scope



This report would have more information with
"Show verbose output during compilation"
option enabled in File -> Preferences.

UPDATE

I still haven't figured out why my #define doesn't work..
But for testing, I replaced the define with a literal, and that error is not stopping me.

Now, a new problem. I am getting "multiple definitions" of the declarations in my .h file:
multiple definition of macBuffer';`

It's as if my .h file is loading twice. I thought that this code in the top of the .h file was supposed to block that:

#ifndef _setupWiFi_h
#define _setupWiFi_h

Full error code:

Arduino: 1.8.15 (Windows 10), Board: "LOLIN(WEMOS) D1 R2 & mini, 80 MHz, Flash, Disabled (new aborts on oom), Disabled, All SSL ciphers (most compatible), 32KB cache + 32KB IRAM (balanced), Use pgm_read macros for IRAM/PROGMEM, 4MB (FS:2MB OTA:~1019KB), v2 Lower Memory, Disabled, None, Only Sketch, 921600"



c:/users/steve/appdata/local/arduino15/packages/esp8266/tools/xtensa-lx106-elf-gcc/3.0.4-gcc10.3-1757bed/bin/../lib/gcc/xtensa-lx106-elf/10.3.0/../../../../xtensa-lx106-elf/bin/ld.exe: sketch\setupWiFi.cpp.o:C:\Users\steve\Documents\Arduino\cppTest2/setupWiFi.h:14: multiple definition of `macBuffer'; sketch\cppTest2.ino.cpp.o:C:\Users\steve\Documents\Arduino\cppTest2/setupWiFi.h:14: first defined here

c:/users/steve/appdata/local/arduino15/packages/esp8266/tools/xtensa-lx106-elf-gcc/3.0.4-gcc10.3-1757bed/bin/../lib/gcc/xtensa-lx106-elf/10.3.0/../../../../xtensa-lx106-elf/bin/ld.exe: sketch\setupWiFi.cpp.o:C:\Users\steve\Documents\Arduino\cppTest2/setupWiFi.h:16: multiple definition of `nodeName'; sketch\cppTest2.ino.cpp.o:C:\Users\steve\Documents\Arduino\cppTest2/setupWiFi.h:16: first defined here

c:/users/steve/appdata/local/arduino15/packages/esp8266/tools/xtensa-lx106-elf-gcc/3.0.4-gcc10.3-1757bed/bin/../lib/gcc/xtensa-lx106-elf/10.3.0/../../../../xtensa-lx106-elf/bin/ld.exe: sketch\setupWiFi.cpp.o:C:\Users\steve\Documents\Arduino\cppTest2/setupWiFi.h:15: multiple definition of `hostName'; sketch\cppTest2.ino.cpp.o:C:\Users\steve\Documents\Arduino\cppTest2/setupWiFi.h:15: first defined here

collect2.exe: error: ld returned 1 exit status

exit status 1

Error compiling for board LOLIN(WEMOS) D1 R2 & mini.



This report would have more information with
"Show verbose output during compilation"
option enabled in File -> Preferences.

Hello

If you want to use multiple files for your project I suggest using something else than the Arduino IDE, I recommend VS Code with PlatformIO

No. The include guard prevents the header from being loaded twice in a single translation unit.

The tricky thing about this translation is that with your old system of .ino files, the entire sketch was a single translation unit. But you now have multiple translation units in the sketch:

  • The cppTest2.ino.cpp translation unit
  • The setupWiFi.cpp translation unit

The contents of the header file can only be included once in each of those translation units, but if you have an #include directive for the header in each translation unit, then it will be included in both of them, meaning if you have definitions in the header then you will get "multiple definition" errors.

The multiple inclusions of the header file was also the cause of your first error. You referenced SKETCH in the header, but declared it only in one of the two translation units. So when the header file was #included in the other translation unit the name was not declared and you got that error.

A header is usually used only for declarations; to provide an API. The definitions are placed in the source files.

Maybe it's more clear now why I don't support the advice from some forum members that if you want to split your sketch up into multiple files it must be done with .cpp and .h files. However, I also very much support anyone who wants to take this approach. It can sometimes be useful to separate your sketch into multiple translation units. Another great thing is that once you can do this you can just as easily create Arduino libraries for sharing your reusable code. The support for .h, .cpp, .c, and .S is one of the things I love about the Arduino IDE. The IDE is very beginner friendly, but also allows us to do advanced things if we want.

I'm confused. It appears that those two sentences contradict each other.

That's because you defined 'macBuffer' in the .h file instead of just declaring it there (using extern). A variable can be declared in multiple places, but it can only be defined once. That's covered in my post on this topic that I linked in your other thread:

As I said above, define the global variable in exactly one .cpp file.
See also: https://www.cprogramming.com/declare_vs_define.html

1 Like

Nope. There are multiple options for splitting a sketch up into files. One of those is to put them in .ino files, and for many users that is often the best choice. So I don't at all agree with the people who say that the use of .cpp/.h files the only acceptable approach.

But if the user truly wants to to follow that paradigm, rather than being mislead into thinking it's the only way by FUD, then that's great.

Fair enough. I just fail to see the advantage of doing that since it only gives the illusion of providing modularity.

The sole advantage of breaking a sketch up into multiple .ino files is it makes large sketches easier to navigate. It really improves the experience of working on a large sketch.

I don't fully agree this is even an advantage.
Multiple .ino file may, of course, help the user to break a problem down, but not necessarily in any logical way. It is also confusing because of the alphabetical order in which the .ino files are aggregated. This means that some variables are available in some .ino file and not others. Functions, however are available wherever these have been declared because of another twist of the IDE that generates function prototypes, in a sort of way, and puts these near the top of the code collection assembled out of the various .ino files. Using the .h / .cpp approach imposes a discipline on the users because only explicitly defined objects can be accessed outside the translation unit. Also, unpicking a collection of .ino files and converting afterwards to a more structured .h / .cpp type structure is a miserable task.

This is annoying indeed, but easily mitigated with a progressive naming convention, e.g.

01_foo
02_bar
03_baz

doh(300)

I remember now that the ino and each cpp file are compiled separately and compiler directives like #define SKETCH would be unknown between them.

I am making progress. I am down to one error.

I define SKETCH in my main ino file"

const char SKETCH[20] = "mySketchname";

And then in the .h file of my .cpp files that need to know about SKETCH, I simply declare them thus:

extern const char SKETCH[20];

and trust the linker to find the definition.

But, now the error is "undefined reference to SKETCH"
I can't see where I am misusing extern.

BACKGROUND. Or, why do I want to do this?
As I said before, I've been using multiple tabs for a few years. For example when I want to add MQTT or OTA to a sketch, I copy my boilerplate ino file for MQTT or OTA into the project folder, then copy the declarations into the main ino, add the setup and loop lines then compile. My motivation for switching to cpp is twofold. First, my understanding of cpp as a separate translation unit (compile unit- what's the difference?) is that my declarations stay in the cpp file, keeping my main ino cleaner. Second, it would advance my understanding for making my own libraries.

Is my supposition wrong?

Post the smallest possible version of your current code that produces the error.

Thanks, your assistance is appreciated.

The main sketch:

const char SKETCH[20] = "cppTest2";
/*
 * Experiment with changing my multiple ino files into cpp/h files.
 */

#include "setupWiFi.h"

void setup(){
  Serial.begin(115200);
  Serial.println("\nTest begins");

  setup_wifi();        //Connect to my WiFi
  
}

void loop(){}

The cpp file:

//setupWiFi.cpp

#include "setupWiFi.h"
#include <Arduino.h> //needed for Serial.println()


// setup_wifi() function
// ============== Connect the ESP to the router ==============
// Connect to WiFi network so we can reach the MQTT broker and
// publish messages to topics.

void setup_wifi() {
  byte mac[6];                    // The MAC address of your Wifi
  char nodeName[24];
  char hostName[24];              // Holds nodeName + the last three bytes of the MAC address.
  char macBuffer[20];             // Holds the last three digits of the MAC, in hex.

  Serial.println(F("\n"));
  Serial.print(F("Connecting to "));
  Serial.println(MY_SSID);

  WiFi.mode(WIFI_STA);
  WiFi.begin(MY_SSID, MY_PASSWORD);
  while (WiFi.status() != WL_CONNECTED) {
    Serial.print(WiFi.status()); Serial.print(F(" "));
    delay(500);
  }
  Serial.println(F("\nWiFi connected, "));
  Serial.print(F("MAC Address: "));
  Serial.println(WiFi.macAddress());
  Serial.print(F("IP address: "));
  Serial.println(WiFi.localIP());
  Serial.print(F("RSSI: "));
  Serial.println(WiFi.RSSI());
  Serial.println();


  // Get the last three numbers of the mac address.
  // "4C:11:AE:0D:83:86" becomes "0D8386" in macBuffer.
  WiFi.macAddress(mac);
  snprintf(macBuffer, sizeof(macBuffer), "%02X%02X%02X", mac[3], mac[4], mac[5]);

  // Build hostName from prefix + last three bytes of the MAC address.
  strcpy(hostName, nodeName);
  strcat(hostName, "-");
  strcat(hostName, macBuffer);

  WiFi.hostname(hostName);
  Serial.print(F("hostname= "));
  Serial.println(hostName);
}

And the .h file:

//setupWiFi.h

#ifndef _setupWiFi_h
#define _setupWiFi_h

//Prototype of functions in the .cpp file
void setup_wifi();

//Variable defined in the main ino file
extern const char SKETCH[20];


//--------------- WiFi declarations ---------------
// WiFi declarations
#include <ESP8266WiFi.h>
#include <Kaywinnet.h>          // WiFi credentials

#endif // _setupWiFi_h

No. Implementing a feature in a .h and one or more .cpp files is a perfect precursor to making a library. You can move your 'boilerplate' into a library folder and include it by adding a "#include" of the .h file to your sketch.

It looks like your problem is that the 'boilerplate'/'library' needs you to provide values for some variables. The 'library' gets compiled separately from your sketch so values known to the main sketch are not available to the 'library' at compile time.

If the 'library' needs build-time information, you can wrap the library in an object class and use a combination of the constructor arguments and a .begin() method to initialize the library.

By that, I meant the smallest possible ... an MRE. No Bloat. Wouldn't compile at all without 'Kaywinnet.h'.

So, really minimal is below and it compiles without error. The code you posted only mentioned SKETCH in the .h and .ino files. Don't know what the real errors were, but they didn't include "undefined reference to SKETCH". Perhaps add to my code below until you get that error. Then post it back.

Main .ino:

const char SKETCH[20] = "cppTest2";

#include "setupWiFi.h"

void setup(){
  Serial.begin(115200);
  Serial.println("\nTest begins");
  setup_wifi();        //Connect to my WiFi
}

void loop(){}

setupWiFi.h:

//setupWiFi.h

#ifndef _setupWiFi_h
#define _setupWiFi_h

//Prototype of functions in the .cpp file
void setup_wifi();

//Variable defined in the main ino file
extern const char SKETCH[20];

#endif // _setupWiFi_h

setupWiFi.cpp

//setupWiFi.cpp
#include "setupWiFi.h"
void setup_wifi() {
}

Instead of doing it in this order:

Do you get better results when you do it in this order:

#include "setupWiFi.h"
const char SKETCH[20] = "cppTest2";

I've just tried this while attempting to duplicate the OP's problem in post #12

This :

//main.ino
const char SKETCH[20] = "mySketchname";
#include "test.h"

void setup() {
  // put your setup code here, to run once:
  Serial.begin(9600) ;
  pr() ;
}

void loop() {
  // put your main code here, to run repeatedly:
}


//test.h
#include <Arduino.h>

#ifndef _guard1
#define _guard1

extern const char SKETCH[20];
void pr() ;

#endif


//test.cpp
#include "test.h"

void pr() {
Serial.println( SKETCH ) ;
}

fails with the following error:

Linking everything together...
"C:\\Users\\6v6gt\\Documents\\ArduinoData\\packages\\arduino\\tools\\avr-gcc\\7.3.0-atmel3.6.1-arduino7/bin/avr-gcc" -Wall -Wextra -Os -g -flto -fuse-linker-plugin -Wl,--gc-sections -mmcu=atmega328p -o "C:\\Users\\6v6gt\\AppData\\Local\\Temp\\arduino_build_400343/sketch_oct13a.ino.elf" "C:\\Users\\6v6gt\\AppData\\Local\\Temp\\arduino_build_400343\\sketch\\sketch_oct13a.ino.cpp.o" "C:\\Users\\6v6gt\\AppData\\Local\\Temp\\arduino_build_400343\\sketch\\test.cpp.o" "C:\\Users\\6v6gt\\AppData\\Local\\Temp\\arduino_build_400343/core\\core.a" "-LC:\\Users\\6v6gt\\AppData\\Local\\Temp\\arduino_build_400343" -lm
C:\Users\6v6gt\AppData\Local\Temp\cc4TmB3B.ltrans0.ltrans.o: In function `write':
C:\Program Files\WindowsApps\ArduinoLLC.ArduinoIDE_1.8.51.0_x86__mdqgnx93n4wtt\hardware\arduino\avr\cores\arduino/Print.h:53: undefined reference to `SKETCH'
C:\Program Files\WindowsApps\ArduinoLLC.ArduinoIDE_1.8.51.0_x86__mdqgnx93n4wtt\hardware\arduino\avr\cores\arduino/Print.h:53: undefined reference to `SKETCH'
C:\Program Files\WindowsApps\ArduinoLLC.ArduinoIDE_1.8.51.0_x86__mdqgnx93n4wtt\hardware\arduino\avr\cores\arduino/Print.h:54: undefined reference to `SKETCH'
C:\Program Files\WindowsApps\ArduinoLLC.ArduinoIDE_1.8.51.0_x86__mdqgnx93n4wtt\hardware\arduino\avr\cores\arduino/Print.h:54: undefined reference to `SKETCH'
C:\Program Files\WindowsApps\ArduinoLLC.ArduinoIDE_1.8.51.0_x86__mdqgnx93n4wtt\hardware\arduino\avr\cores\arduino/Print.h:54: undefined reference to `SKETCH'
C:\Users\6v6gt\AppData\Local\Temp\cc4TmB3B.ltrans0.ltrans.o:C:\Program Files\WindowsApps\ArduinoLLC.ArduinoIDE_1.8.51.0_x86__mdqgnx93n4wtt\hardware\arduino\avr\cores\arduino/Print.h:54: more undefined references to `SKETCH' follow
collect2.exe: error: ld returned 1 exit status
exit status 1
Error compiling for board Arduino Uno.

This works.
The only difference is the order of the first 2 lines, the #include and the const definition.

//main.ino
#include "test.h"
const char SKETCH[20] = "mySketchname";

void setup() {
  // put your setup code here, to run once:
  Serial.begin(9600) ;
  pr() ;
}

void loop() {
  // put your main code here, to run repeatedly:
}


//test.h
#include <Arduino.h>

#ifndef _guard1
#define _guard1

extern const char SKETCH[20];
void pr() ;

#endif


//test.cpp
#include "test.h"

void pr() {
Serial.println( SKETCH ) ;
}

Maybe I've overlooked something simple.

Ahhhhhh .... but the code OP posted as a minimal example didn't reference 'SKETCH' inside the .cpp file at all.

Um, the first line?

This does fix the issue, but why?

For reference, if anyone else is following this thread, here is my working code.

The main cpp file:

/*
 * Experiment with changing my multiple ino files into cpp/h files.
 */

#include "setupWiFi.h"
const char SKETCH[20] = "cppTest2";

void setup(){
  Serial.begin(115200);
  Serial.println("\nTest begins");

  setup_wifi();        //Connect to my WiFi
  
}

void loop(){}

The .cpp file:

//setupWiFi.cpp

#include "setupWiFi.h"
#include <Arduino.h> //needed for Serial.println()


// setup_wifi() function
// ============== Connect the ESP to the router ==============
// Connect to WiFi network so we can reach the MQTT broker and
// publish messages to topics.

void setup_wifi() {
  byte mac[6];                    // The MAC address of your Wifi
  char nodeName[24];
  char hostName[24];              // Holds nodeName + the last three bytes of the MAC address.
  char macBuffer[20];             // Holds the last three digits of the MAC, in hex.

  Serial.println(F("\n"));
  Serial.print(F("Connecting to "));
  Serial.println(MY_SSID);

  WiFi.mode(WIFI_STA);
  WiFi.begin(MY_SSID, MY_PASSWORD);
  while (WiFi.status() != WL_CONNECTED) {
    Serial.print(WiFi.status()); Serial.print(F(" "));
    delay(500);
  }
  Serial.println(F("\nWiFi connected, "));
  Serial.print(F("MAC Address: "));
  Serial.println(WiFi.macAddress());
  Serial.print(F("IP address: "));
  Serial.println(WiFi.localIP());
  Serial.print(F("RSSI: "));
  Serial.println(WiFi.RSSI());


  // Get the last three numbers of the mac address.
  // "4C:11:AE:0D:83:86" becomes "0D8386" in macBuffer.
  WiFi.macAddress(mac);
  snprintf(macBuffer, sizeof(macBuffer), "%02X%02X%02X", mac[3], mac[4], mac[5]);

  // Build hostName from prefix + last three bytes of the MAC address.
  strcpy(hostName, SKETCH);
  strcat(hostName, "-");
  strcat(hostName, macBuffer);

  WiFi.hostname(hostName);
  Serial.print(F("hostname: "));
  Serial.println(hostName);
}

The .h file:

//setupWiFi.h

#ifndef _setupWiFi_h
#define _setupWiFi_h

//Prototype of functions in the .cpp file
void setup_wifi();

//Variable defined in the main ino file
extern const char SKETCH[20];


//--------------- WiFi declarations ---------------
// WiFi declarations
#include <ESP8266WiFi.h>
#include <Kaywinnet.h>          // WiFi credentials

#endif // _setupWiFi_h

And the mystery Kaywinnet.h file contains my WiFi credentials:

#ifndef Kaywinnet
#define Kaywinnet

#define MY_SSID "Kaywinnet"
#define MY_PASSWORD "XXXXXXXXXX"

#define MQTT_SERVER "192.168.1.124"
#define MQTT_PORT 1883

#endif