Migration Guides for SensESP Major Versions
Migrating SensESP Version 2 Projects to Version 3
Quick Changes for the Impatient
SensESP v3 introduces some changes in the main program initialization and structure.
Update your project’s platformio.ini file to use the new version of SensESP:
lib_deps =
SignalK/SensESP @ ^3.0.0
Adjust the build flags in your project’s platformio.ini file as follows:
build_flags =
-D LED_BUILTIN=2
; Max (and default) debugging level in Arduino ESP32 Core
-D CORE_DEBUG_LEVEL=ARDUHAL_LOG_LEVEL_VERBOSE
; Arduino Core bug workaround: define the log tag for the Arduino
; logging macros.
-D TAG='"Arduino"'
; Use the ESP-IDF logging library - required by SensESP.
-D USE_ESP_IDF_LOG
If you have the following in the beginning of your setup() function:
#ifndef SERIAL_DEBUG_DISABLED
SetupSerialDebug(115200);
#endif
replace it with:
SetupLogging();
The reactesp namespace is no longer imported. If you have any references to classes in this namespace, you will need to update them to use the namespace explicitly.
Additionally, ReactESP classes have been renamed:
ReactESP->reactesp::EventLoop*Reaction->reactesp::*Event
For example, ReactESP class should be referred to as reactesp::EventLoop. In particular, this change probably needs to be made in your project’s main.cpp file.
SensESP uses the ReactESP framework for event-based programming. In previous versions, the ReactESP “app” object had to be instantiated in the main program file. This is no longer the case, and SensESP will take care of this for you. Remove any line such as
reactesp::ReactESP app;
or
ReactESP app;
from your main.cpp file. Similarly, the ReactESP main class has been renamed to EventLoop. If you want to set up ReactESP events (previously called “reactions”), use the event_loop() convenience function to get a pointer to the EventLoop object.
For example:
event_loop()->onRepeat(
1000,
[]() { Serial.println("Hello, world!"); }
);
SensESP v3 has removed the Startable class. In previous versions, you would have sensesp_app->start(); as the last line in your setup() function. This is no longer necessary. If you need to initialize something after the setup() function has finished, create a zero-delay event: event_loop()->onDelay(0, []() { /* your code here */ });.
See the next section how to update your code to the new config item system.
Exposing Sensors, Transforms, and Outputs to the Web Interface
In previous versions of SensESP, any object inheriting from Configurable that had a config path defined, would automatically be added to the web interface. In SensESP v3, this has changed. Now, you need to explicitly expose objects to the web interface. This isdone by calling ConfigItem with the object as an argument. For example:
auto input_calibration = new Linear(1.0, 0.0, "/input/calibration");
ConfigItem(input_calibration)
->set_title("Input Calibration")
->set_description("Analog input value adjustment.")
->set_sort_order(1100);
analog_input->connect_to(input_calibration);
New Transforms
SensESP v3 adds several new transform classes. For detailed usage, see the Concepts page.
ExpiringValue— output expires after a given duration unless updatedRepeat— repeats the last input value at a fixed intervalRepeatStopping— repeats the input value, stopping after a given timeRepeatExpiring— repeats the input value, emitting aNullable<T>null after expirationRepeatConstantRate— repeats at a constant rate regardless of input rate, usingNullable<T>Join— combines multiple inputs into a tuple, emitting when any input updatesZip— combines multiple inputs into a tuple, emitting only when all inputs have updatedFilter— passes the input value through only if it satisfies a predicateThrottle— limits the rate of output emissionsHysteresis— emits a boolean output based on upper and lower thresholds
StatusPageItem
UIOutput and UILambdaOutput have been renamed to StatusPageItem. Unlike the old classes, StatusPageItem is a ValueConsumer, so you can connect a producer to it directly rather than polling with a lambda.
Nullable<T>
Nullable<T> is a wrapper type that represents a value that may be invalid. It is used by RepeatExpiring and RepeatConstantRate to signal that a repeated value has expired (the output becomes null/invalid). Downstream consumers can check whether the value is valid before acting on it.
Smart Pointers
Smart pointers (std::shared_ptr) are used internally throughout SensESP v3, reducing the risk of memory leaks and dangling pointer crashes. Using smart pointers in your own code is supported but not required.
Logging
Previous SensESP versions logged to the serial port using the debugX functions, where X is the log level. This pattern was inherited from the RemoteDebug library. SensESP v3 has switched to using standard ESP-IDF logging functions. They allow redirecting log messages to different outputs, which will be used in future SensESP versions to provide logging to the web interface.
Adjust your build_flags and setup function as described in the Quick Changes section above.
The debugX functions are still available, but they are now just wrappers around the ESP-IDF logging functions. In any new code, use the ESP_LOGX functions, where X is the log level. These require a tag argument, which is a string that identifies the source of the log message. You can use any tag you like, but for simple programs, you can use the __FILE__ macro, which expands to the name of the current file.
The allowed log levels are:
NONE: No log outputERROR: Critical errors, software module can not recover on its ownWARN: Error conditions from which recovery measures have been takenINFO: Information messages which describe normal flow of eventsDEBUG: Extra information which is not necessary for normal use (values, pointers, sizes, etc).VERBOSE: Bigger chunks of debugging information, or frequent messages which can potentially flood the output.
Here is an example of how to use the ESP_LOGX functions:
ESP_LOGI(__FILE__, "Initializing NMEA2000");
...
ESP_LOGD(__FILE__, "Sending value %d to fobulator %s", value, fobulator_name);
...
ESP_LOGE(__FILE__, "Failed to initialize NMEA2000");
It is possible to change the log level for individual tags. Here is an example of how to set the overall log level to INFO and the log level for the main.cpp tag to DEBUG:
esp_log_level_set("*", ESP_LOG_INFO);
esp_log_level_set("main.cpp", ESP_LOG_DEBUG);
Migrating SensESP Version 1 Projects to Version 2
SensESP version 2 has a number of backwards-incompatible changes compared to the previous version. Most of the changes are trivial, while some others require a bit of more work to update the code.
This document walks through the most important changes, and explains how to update your project to the new version.
ESP8266 Support Removed
If your project uses ESP8266 hardware, you will either have to update to an ESP32 device, or you can keep using SensESP version 1. To peg your project to SensESP v1, change the SensESP dependency in your project’s platformio.ini file lib_deps section to this:
lib_deps =
SignalK/SensESP @ ^1.0.8
Main Program Structure
Setup and Loop Functions
SensESP builds on ReactESP, which is an event-based framework for developing ESP32 firmware. Previous versions of ReactESP defined the Arduino Framework default setup() and loop() functions internally and relied on a lambda function for initializing the program and defining the top-level functionality:
ReactESP app([] () {
pinMode(LED_PIN, OUTPUT);
app.onRepeat(400, [] () {
digitalWrite(LED_PIN, !digitalRead(LED_PIN));
});
});
This approach emphasized the event-based nature of ReactESP but also looked alien to newcomers expecting a more traditional program structure. Additionally, some use cases might benefit from being able to tweak the details of the setup() and loop() functions, which is not possible with the lambda function approach.
Therefore, ReactESP v2 was updated to explicitly define the setup() and loop() functions. To update your code, declare a default ReactESP app object and define the setup() and loop() functions in the main.cpp file. Then, move the contents of the lambda function into the setup() function. loop() should only have a call to app.tick():
ReactESP app;
void setup() {
pinMode(LED_PIN, OUTPUT);
app.onRepeat(400, [] () {
digitalWrite(LED_PIN, !digitalRead(LED_PIN));
});
}
void loop() { app.tick(); }
Namespace Usage
In projects with a lot of dependencies, it is common that some upstream library exports some very generic symbol names, which then causes conflicts or hard-to-debug issues in the code being developed. The standard C++ approach to mitigate these issues is to use a namespace.
All internal SensESP code is now wrapped in a sensesp namespace. To account for this, add a using namespace sensesp; statement right after the #include statements in your main.cpp file:
#include "..."
using namespace sensesp;
ReactESP app;
void setup() {
...
}
void loop() { app.tick(); }
If you’re developing or maintaining an add-on library, you should wrap the library code in the sensesp namespace:
// includes should remain outside the namespace
#include "..."
namespace sensesp {
// All your code here
}
External Sensors
All Sensor classes requiring external libraries have been removed. Reducing the number of external dependencies improves code stability and improves build times.
Some Sensors are now available as add-on libraries. Most, however, have been removed in favor of a more generic approach, namely the RepeatSensor class. The RepeatSensor class allows you to easily interface any external hardware sensor libraries with SensESP. See the RepeatSensor tutorials (part 1, part 2) for more details.
Renamed Classes and Types
Type-specific Consumer and Producer class names have been renamed to more closely match the native C++ types.
All class names including Numeric have been renamed to Float. For example, SKOutputNumeric has been renamed to SKOutputFloat and SKNumericListener has been renamed to FloatSKListener. In the latter case, the name has also been modified to reflect that float is the input type for the listener.
Similarly, names with other types have been renamed to more closely match the standard C++ builtin type names. For example, *Boolean* is now *Bool*, and *Integer* is now *Int*.
To better reflect the intent and the functionality, the Enable class has been renamed to Startable.
Class Interface Changes
Some class public interfaces have been changed.
The DigitalInputState constructor no longer accepts the interrupt_type argument because that class never used interrupts.
The DigitalInputChange implementation has been simplified and the constructor no longer requres the read_delay argument.
System Info Sensors
SensESP v1 had so called “standard sensors” that transmit information on the operation of the device: free memory, number of event loop executions per second, device IP address, and so on. The standard sensors were initialized using a SensESP constructor or builder bitfield argument:
sensesp_app = builder.set_standard_sensors(UPTIME)
->get_app();
The standard sensors have been renamed to system info sensors and they are initialized by regular builder methods:
sensesp_app = builder.enable_free_mem_sensor()
->enable_uptime_sensor()
->get_app();
Remote Debugger Disabled
The Remote Debugger allows you to connect to the device over telnet and view the log messages and even reset the device, all without a USB cable connection. Even though this is a neat and useful feature, it was not widely known and uses a lot of memory. And, it was a gaping security hole.
The Remote Debugger has now been disabled by default.
To enable the Remote Debugger, add the following line to your platformio.ini file build_flags:
build_flags =
...
-D REMOTE_DEBUG
After rebuilding and uploading, your device should listen to the default telnet port (23) on the device’s local network.
If you want to disable all debugging output whatsoever, add the following:
build_flags =
...
-D DEBUG_DISABLED
Over-The-Air (OTA) Firmware Updates
OTA firmware updates have been supported already for a long time. To improve security, OTA updates are now enabled only if an OTA password is defined in the App builder:
sensesp_app = builder.enable_free_mem_sensor()
->enable_ota("my_password")
->get_app();
Transport-agnostic networking (v3 minor update)
SensESP used to assume WiFi was the only way to reach the Signal K server. The networking layer has been refactored to be transport- agnostic: WiFi is still the default and existing sketches compile and behave identically, but the library can now also run over native Ethernet (on ESP32-P4) and is ready for future transports. The refactor RFC is #849.
No action required for WiFi users
Every public class name, enum name, and builder method that existed before still works via compatibility typedefs:
| Old name | New name |
|---|---|
Networking | WiFiProvisioner |
WiFiState (enum) | NetworkState |
WiFiStateProducer | NetworkStateProducer |
SensESPApp::get_networking() | SensESPApp::get_wifi_provisioner() |
The kept aliases mean a user sketch like
auto state = sensesp_app->get_networking()->get_wifi_state_producer();
compiles unchanged against the new release.
Optional: switch to the new names
For new code or if you want to use the new transport-agnostic API, prefer:
// Include the concrete header directly (not "networking.h")
#include "sensesp/net/wifi_provisioner.h"
#include "sensesp/net/network_state.h"
// Access the provisioner through its new accessor
auto wifi = sensesp_app->get_wifi_provisioner();
// Or, for code that should work on any transport, use the
// NetworkProvisioner base and the unified state producer:
auto provisioner = sensesp_app->get_network_provisioner();
auto ip = provisioner->local_ip();
auto mac = provisioner->mac_address();
auto state_producer = sensesp_app->get_network_state_producer();
state_producer->connect_to(&my_consumer);
Static-IP WiFi configurations
If you use a saved static-IP configuration on a WiFi client, the release in which this refactor lands also fixes a long-standing bug in WiFiProvisioner::start_client_autoconnect() where the arguments to WiFi.config() were passed in the wrong order — silently setting gateway = DNS server, subnet = gateway, and DNS1 = netmask. If your saved WiFi client config has useDHCP: false, verify your gateway / netmask / DNS values look correct after upgrading; existing saved configs should actually start working for the first time.
Adding Ethernet
See docs/pages/features/index.md (section “Network transports”) for a short usage example.