-
Notifications
You must be signed in to change notification settings - Fork 83
Home
SensESP is a library of functions that makes it relatively simple to create a program that will run on an ESP8266 microcontroller ("MCU"), that collects data from various sensors, processes the data, and sends it along to a final "consumer", typically a Signal K server. It must be used with the PlatformIO IDE - it won't work with the Arduino IDE.
As the end user, you create a single main.cpp
file, most of which will be boilerplate - all the code necessary to connect to your network and your SignalK server. Then, with only a few lines of code, you create an instance of a sensor, process the data from the sensor, then send the final result to the Signal K server.
The best way to learn how to use SensESP is by looking at a few examples. In the /examples
folder, rpm_counter.cpp
is a good one to start with, as it illustrates a simple implementation - reading the value of a sensor connected to a GPIO pin on the MCU, converting that value to hertz (the native unit of measurement for a tachometer in Signal K), then sending the value in hertz to the Signal K server.
A Sensor is an object that knows how to get data from an actual, physical sensor and pass it along to a Transport. For example, an AnalogInput Sensor knows how to read the AnalogIn pin on the MCU, and a DigitalInputCounter Sensor knows how to read a GPIO pin on the MCU using an interrupt.
A Transport is an object that takes a value as input (from a Sensor, or from another Transport), does something with that value, then outputs the new value, either to another Transport, or to the Signal K server. In the rpm_counter.cpp
example referred to above, the meat of the code is these three lines:
auto* pSensor = new DigitalInputCounter(D5, INPUT_PULLUP, RISING, read_delay);
pSensor->connectTo(new Frequency(multiplier, config_path_calibrate))
->connectTo(new SKOutputNumber(sk_path, config_path_skpath));
The first line instantiates a Sensor of type DigitalInputCounter. The second line is a Transport of type Frequency - it takes a raw number from the DigitalInputCounter Sensor and converts it to hertz, then passes it along to SKOutputNumber. SKOutputNumber is a special Transport whose purpose is to send a value to the Signal K server.
A much more complex example is temperature_sender.cpp
, where the meat of the program is this:
auto* pAnalogInput = new AnalogInput();
pAnalogInput->connectTo(new AnalogVoltage()) ->
connectTo(new VoltageDividerR2(R1, Vin, "/gen/temp/sender")) ->
connectTo(new TemperatureInterpreter("/gen/temp/curve")) ->
connectTo(new Linear(1.0, 0.0, "/gen/temp/calibrate")) ->
connectTo(new SKOutputNumber(sk_path, "/gen/temp/sk"));
In this example, there is still only one Sensor (AnalogInput), but several Transports, all required to turn the raw value from the Analog Input pin on the MCU into a temperature that's sent to the Signal K server.
You can also include multiple Sensors, each with at least one Transport, in the same program, such as combining both of the examples above into the same main.cpp
, one after the other. (TODO - add such an example to /examples.)
Some Sensors and Transports have parameters that can be configured "live", by accessing the MCU through its IP address, entered as a URL in any browser. For example, entering 192.168.1.236
(the IP address of the MCU I'm currently using as my example) in a browser will bring up the following web page that's hosted by the MCU itself:
"Device information" does not display any information at this time. "Configure device" is explained below. "Restart device" will restart the MCU. "Reset device" will erase all the network information and all the Sensor and Transport configuration, and the program. The MCU will be returned to the way it came from the factory.
Click on "Configure device" to bring up the /setup page, which will look something like this:
The only menu item that will always be present is "system", which has two sub-menus: "networking" (which lets you see and edit your wifi SSID and password, and the MCU's hostname), and "sk", which displays some networking info, but can't yet be used for editing that info, although it looks like it can.
The other menu item(s) (in this example, only "blackWater") are there only if you've provided a configuration path in the constructor of one or more of the Sensors and Transports in main.cpp
. Naming of these is important, and covered below.
Look at the three lines from the rpm_counter.cpp
example above. There are three constructors - the one for the DigitalInputCounter Sensor, the one for the Frequency Transport, and the one for the SKOutputNumber Transport. The latter two have a configuration path included as the last item in their parameter list, but the first one doesn't. That means that the first one - the DigitalInputCounter - has no values that can be configured "live", but the latter two do. Actually, it's a little more complicated than that, because it's not entirely consistent among all Sensors and Transports.
As of this writing (March 2020), here are the ** possible ** configurable parameters for each Sensor and Transport:
** SENSORS **
- AnalogInput: Read delay (Number of milliseconds between each analogRead(A0))
- DigitalInputValue: none
- DigitalInputCounter: Read delay (The time, in milliseconds, between each read of the input)
- GPSInput: none
- OneWireTemperature: OneWire address (the unique address of the specific sensor being used)
** TRANSFORMS **
- AnalogVoltage: none
- AngleCorrection: Constant offset (value to be added, in radians); Minimum angle value (pi, or 0)
- ChangeFilter: Minimum delta (Minimum difference in change of value before forwarding); Maximum delta (maximum difference in change of value to allow forwarding); Max skips (maximum number of consecutive filtered values before one is allowed through)
- CurveInterpolator: input, output (each pair is added to the table of samples)
- Difference: Input #1 multiplier, Input #2 multiplier
- Frequency: Multiplier
- Integrator: Multiplier
- Linear: Multiplier; Constant offset
- Median: Sample size (number of samples to take before outputing a value)
- MovingAverage: Number of samples in average; Multiplier
- VoltageDivider: Voltage in; Resistance (ohms) of R1
- SKOutputNumber, SKOutputInt, SKOutputBool, SKOutputString: Signal K data path
In every case of a configurable value, that value can be set in main.cpp
, but by making it configurable, it's easier to make adjustments to your output based on what you're seeing in the real world. For example, the Median Transport is used to smooth output from a "noisy" sensor. By making the "Sample size" configurable, you can experiment with different sample sizes while the MCU is running and outputting data to Signal K, so you can decide when you have the sample size where you want it.
Even though a Sensor or Transport has the ability to be configurable, it won't be unless you provide a configuration path in the constructor when you use it in main.cpp
. For example,
auto* pAnalogInput = new AnalogInput();
creates an AnalogInput Sensor whose Read Delay will be the default 200 ms, and which can't be configured.
auto* pAnalogInput = new AnalogInput(250);
creates a Sensor with a 250 ms Read Delay that still can't be adjusted in real time, because of the lack of a config_path parameter.
But
auto* pAnalogInput = new AnalogInput(250, "/sensors/analogInput");
creates a Sensor with a 250 ms Read Delay that can be adjusted in real time, because of the presence of the config_path parameter ("/sensors/analogInput"
).
Your configuration path parameter can be passed with a variable you create, like this:
const char* sensor_config_path = "/sensors/analogInput";
auto* pAnalogInput = new AnalogInput(250, sensor_config_path);
or by putting the configuration path String directly into the parameter list of the constructor, like this:
auto* pAnalogInput = new AnalogInput(250, "/sensors/analogInput");
The naming of the paths is important, especially when you have multiple Sensors and / or multiple Transports in your main.cpp
, so to be safe, please follow these guidelines:
- Every configuration path name MUST begin with a forward slash.
- Use two levels in your names, so that they look like
"/firstLevel/secondLevel"
, with the first level being a word that groups entries together in a logical manner, and the second level referring to the specific Sensor or Transport that the configuration path relates to. For example:
Two Sensors (one for black water and one for fresh water), each using a Moving Average Transport and outputting to the Signal K Server with SKOutputNumber:
- "/blackWater/analogInput" (for the blackwater AnalogInput() constructor in
main.cpp
) - "/blackWater/movingAvg" (for the blackwater MovingAverage() constructor in
main.cpp
) - "/blackWater/skPath" (for the blackwater SKOutputNumber() constructor in
main.cpp
) - "/freshWater/analogInput" (for the fresh water AnalogInput() constructor)
- "/freshWater/movingAvg" (for the fresh water MovingAverage() constructor)
- "/freshWater/skPath" (for the fresh water SKOutputNumber() constructor)
This will group the configuration entries in the web interface into two groups: "blackWater" and "freshWater". Each group will have three entries: "analogInput", "movingAvg", and "SKOutput". Each "analogInput" entry will have one configurabe value: "Read delay"; each "movingAvg" entry will have two configurable values: "Number of samples" and "Multiplier"; and each "skPath" entry will have one configurable value: "SignalK Path".