You need analog to digital converter, and the most used one AFAIK is the ADS1115 (4 inputs, wire through I2C to the board).
If you get the Halmet board, it already has it. If you get any other one (SailorHat is one I use for my water tank level sensors), just make sure you add an ADS1115 and you're good to go.
Now there's the software. As I said, if you're not used to coding, i would go for
SesnsESP. It basically has all the core features you'll need. You start with the template, and then just configure inputs and outputs like this:
Code:
auto* tank_level = new AnalogInput(pin, read_delay, analog_in_config_path);
tank_level->connect_to(new SKOutputNumber(sk_path));
There's plenty of docs and samples. With SensESP it would even expose a website so you can configure the relationship between analog input value and angle. Here's my OLD code for the Davis windvane. Use it only as inspiration, because SensESP has changed A LOT, and you could write this in a much simpler way.
Code:
#include <Adafruit_ADS1X15.h>
#include <Wire.h>
//#include "sensesp/sensors/analog_input.h"
#include "sensesp/sensors/digital_input.h"
#include "sensesp/sensors/sensor.h"
#include "sensesp/signalk/signalk_output.h"
#include "sensesp/system/lambda_consumer.h"
#include "sensesp/transforms/curveinterpolator.h"
#include "sensesp/transforms/linear.h"
#include "sensesp_app_builder.h"
using namespace sensesp;
class AngleInterpreter : public CurveInterpolator {
public:
AngleInterpreter(String config_path = "")
: CurveInterpolator(NULL, config_path) {
// Populate a lookup table tp translate the ohm values returned by
// our temperature sender to degrees Kelvin
clear_samples();
// addSample(CurveInterpolator::Sample(knownOhmValue, knownKelvin));
add_sample(CurveInterpolator::Sample(-350, 0));
add_sample(CurveInterpolator::Sample(20, 5));
add_sample(CurveInterpolator::Sample(3350, 45));
add_sample(CurveInterpolator::Sample(6950, 90));
add_sample(CurveInterpolator::Sample(10150, 135));
add_sample(CurveInterpolator::Sample(13650, 180));
add_sample(CurveInterpolator::Sample(17350, 225));
add_sample(CurveInterpolator::Sample(19900, 270));
add_sample(CurveInterpolator::Sample(22710, 315));
add_sample(CurveInterpolator::Sample(26000, 360));
}
};
reactesp::ReactESP app;
Adafruit_ADS1115 ads;
// Pin connected to the ALERT/RDY signal for new sample notification.
// constexpr int READY_PIN = 17;
// This is required on ESP32 to put the ISR in IRAM. Define as
// empty for other platforms. Be careful - other platforms may have
// other requirements.
#ifndef IRAM_ATTR
#define IRAM_ATTR
#endif
// volatile bool new_data = false;
// void IRAM_ATTR NewDataReadyISR() { new_data = true; }
String awa_sk_path = "environment.wind.angleApparent";
String awa_sk_path_config_path = "/windAngleApparent/skKey";
String awa_options_config_path = "/windAngleApparent/options";
String awa_full_range_config_path = "/windAngleApparent/useFullRange";
String awa_use_average_config_path = "/windAngleApparent/useAverage";
String awa_curve_config_path = "/windAngleApparent/curve";
// GPIO number to use for the analog input
const uint8_t WIND_ANGLE_PIN = 16; // 32;//34 inestable
// Define how often (in milliseconds) new samples are acquired
const unsigned int AWA_READ_INTERVAL = 50;
const unsigned int AWA_SEND_INTERVAL = 100;
bool _default_FullRange = false;
bool _default_UseAverage = true;
double angleToSend = 0;
int lastReading = -1;
const int READ_BUFFER_SIZE = 10;
int readBuffer[READ_BUFFER_SIZE] = {};
int readingCount = 1;
int readingCumulated = 1;
const int maxAngleValue = 26000;
const int maxAngleDifferential = 512;
const double maxAnglePossibleValue = 26350.0f;
int getReadingAverage() { return readingCumulated / readingCount; }
void resetReadingCount() {
readingCount = 1;
readingCumulated = 1;
}
int averagedValue(int lastValues[], int size, int newValue) {
int total = 0;
// Discard the first value and move all elements to the left
for (int i = 0; i < size - 1; i++) {
lastValues[i] = lastValues[i + 1];
total += lastValues[i];
}
// Add the new value to the right
lastValues[size - 1] = newValue;
total += newValue;
return total / size;
}
void SetupDavisAngle() {
SKMetadata* metadata = new SKMetadata();
metadata->units_ = "rad";
metadata->description_ = "Aparent Wind Angle";
metadata->display_name_ = "Aparent Wind Angle";
metadata->short_name_ = "AWA";
auto awaOutput =
new SKOutputFloat(awa_sk_path, awa_sk_path_config_path, metadata);
/************************** READING AND STORAGE *****************************/
auto* readingStorage =
new LambdaConsumer<double>([](double input) { angleToSend = input; });
auto* windAngleReader =
new RepeatSensor<double>(AWA_READ_INTERVAL, []() -> double {
int reading = getReadingAverage();
resetReadingCount();
int raw = reading;
int filtered = reading;
if (reading > maxAngleValue) filtered = maxAngleValue - reading;
double deg = (double)filtered / maxAnglePossibleValue * 360.0f;
Serial.printf("Raw: %d | ", raw);
Serial.printf("Filtered: %d | Deg: %.1f | ", filtered, deg);
if (lastReading < 0) lastReading = filtered;
int averaged = averagedValue(readBuffer, READ_BUFFER_SIZE, filtered);
Serial.printf("Avg: %d | ", averaged);
int16_t result = 0;
if (abs(lastReading - averaged) > maxAngleDifferential) {
Serial.print("-------IGNORED-------");
result = lastReading;
}
else {
result = averaged;
}
Serial.println("");
lastReading = averaged;
return result;
});
/******************************** SENDING ***********************************/
auto rangeConverterFunction = [](double angle, bool fullRange) -> float {
if (!fullRange && angle > 180) {
angle -= 360;
}
return angle;
};
auto* rangeConverter = new LambdaTransform<float, float, bool>(
rangeConverterFunction, _default_FullRange,
new ParamInfo[1]{ {"fullRange", "Use Full 360 Range"} },
awa_options_config_path);
auto* radianConverter = new LambdaTransform<double, float>(
[](double deg) -> float { return deg / 180.0f * PI; });
auto* windAngleSender = new RepeatSensor<double>(
AWA_SEND_INTERVAL, []() -> double { return angleToSend; });
auto* logger = new LambdaConsumer<double>([](double angleToSend) {
Serial.printf("Reads: %d\r\n", getReadingAverage());
resetReadingCount();
});
windAngleReader->connect_to(new AngleInterpreter(awa_curve_config_path))
->connect_to(rangeConverter)
->connect_to(radianConverter)
->connect_to(readingStorage);
windAngleSender->connect_to(awaOutput);
// windAngleSender->connect_to(logger);
}
String aws_sk_path = "environment.wind.speedApparent";
String aws_delay_config_path = "/windSpeedApparent/readInterval";
String aws_sk_path_config_path = "/windSpeedApparent/skKey";
const uint8_t WIND_SPEED_PIN = 25;
unsigned int AWS_READ_DELAY = 1000;
unsigned int AWS_DEBOUNCE_DELAY = 10;
void SetupDavisSpeed() {
auto* wind_speed_rev_counter = new DigitalInputDebounceCounter(
WIND_SPEED_PIN, INPUT_PULLUP, RISING, AWS_READ_DELAY, AWS_DEBOUNCE_DELAY,
aws_delay_config_path);
SKMetadata* metadata = new SKMetadata();
metadata->units_ = "m/s";
metadata->description_ = "Aparent Wind Speed";
metadata->display_name_ = "Aparent Wind Speed";
metadata->short_name_ = "AWS";
auto awsOutput =
new SKOutputFloat(aws_sk_path, aws_sk_path_config_path, metadata);
auto speed_detector_function = [](int input) -> float {
// mph = P * (2.25 / I) Where P is the pulse count, and I is the interval
// in SECONDS (i.e: AWS_READ_DELAY / 1000)
float mph = input * (2.25f);
float m_s = mph * 0.44704;
return m_s;
};
auto* directionDetector =
new LambdaTransform<int, float>(speed_detector_function);
wind_speed_rev_counter->connect_to(directionDetector)->connect_to(awsOutput);
}
/**************** ADS1115 ***********************/
constexpr int READY_PIN = 17;
volatile bool new_data = false;
void IRAM_ATTR NewDataReadyISR() { new_data = true; }
void setupADC() {
pinMode(READY_PIN, INPUT);
// We get a falling edge every time a new sample is ready.
//attachInterrupt(digitalPinToInterrupt(READY_PIN), NewDataReadyISR, FALLING);
// Start continuous conversions.
ads.startADCReading(ADS1X15_REG_CONFIG_MUX_SINGLE_0, /*continuous=*/true);
}
void loopADC() {
// If we don't have new data, skip this iteration.
if (!new_data) {
return;
}
int16_t reading = ads.getLastConversionResults();
readingCount++;
readingCumulated += reading;
new_data = false;
}
/*************************************************/
// The setup function performs one-time application initialization.
void setup() {
#ifndef SERIAL_DEBUG_DISABLED
SetupSerialDebug(115200);
#endif
// Construct the global SensESPApp() object
SensESPAppBuilder builder;
sensesp_app = (&builder)
// Set a custom hostname for the app.
->set_hostname("WindESP")
// Optionally, hard-code the WiFi and Signal K server
// settings. This is normally not needed.
//->set_wifi("openplotter", the password")
->set_sk_server("10.10.10.1", 3000)
->get_app();
/************************** DAVIS *******************************/
SetupDavisSpeed();
SetupDavisAngle();
/****************************************************************/
ads.setGain(GAIN_ONE);
ads.setDataRate(RATE_ADS1115_860SPS);
if (!ads.begin()) {
Serial.println("Failed to initialize ADS.");
while (1)
;
}
setupADC();
app.onInterrupt(READY_PIN, INPUT, NewDataReadyISR);
// Start networking, SK server connections and other SensESP internals
sensesp_app->start();
}
void loop() {
loopADC();
app.tick();
}
As for the wiring, is actually quite simple. If you go with builtin analog and digital inputs, just plug the wires there. If I recall correctly, Davis has 1 wire for speed (digital), 1 for angle (analog), and 2 for +-.
If you have and external ADC, just wire the analog pin to one of the ADC's input, and the 4 wires for I2C to the ESP32 (check your board for pin numbers). The speed wire can go to any ESP32 input.
BTW, if you're just "playing around", you don't even need the ADS1115: ESP32 already has a couple analog input. Just that the ADC is pure garbage, so wind angle information won't be very reliable.
Oh, also make sure you use the proper resistances between analog and 3.3v, and same for speed (i think this is discussed at some point in this thread)