2025-01-20, 06:16 PM
Hey Ormingtrude! I'm sorry but i'm afraid i won't be of any help. I no longer have that windvane nor that boat :S
I used an ESP32 with SensESP btw, not directly OpenPlotter's I2C app. If it's of any help, this is the last (i think) code I used, and was working quite decently:
I used an ESP32 with SensESP btw, not directly OpenPlotter's I2C app. If it's of any help, this is the last (i think) code I used, and was working quite decently:
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 + 1];
}
// 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;
const int SPEED_READ_BUFFER_SIZE = 10;
int speedReadBuffer[SPEED_READ_BUFFER_SIZE] = {};
float speedAveragedValue(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 + 1];
}
// Add the new value to the right
lastValues[size - 1] = newValue;
total += newValue;
return (float)total / (float)size;
}
void SetupDavisSpeed() {
/* Initialize buffer */
for (int i = 0; i < SPEED_READ_BUFFER_SIZE; i++) {
speedReadBuffer[i] = 0;
}
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 / (AWS_READ_DELAY / 1000));
float m_s = mph * 0.44704;
//Serial.printf("Pulses: %d", input);
//Serial.println("");
float averaged = speedAveragedValue(speedReadBuffer, SPEED_READ_BUFFER_SIZE, m_s);
// Serial.printf(" - Average: %lf", averaged);
// Serial.println("");
return averaged;
//return m_s;
};
auto* speedDetector =
new LambdaTransform<int, float>(speed_detector_function);
wind_speed_rev_counter->connect_to(speedDetector)->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", "guardian")
->set_sk_server("10.10.10.1", 3000)
->get_app();
/************************** DAVIS *******************************/
SetupDavisSpeed();
SetupDavisAngle();
/****************************************************************/
/// Usually SDA is 21 and SCL is 22, but in this case I designed the PCB wrong
Wire.begin(22,21);
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();
}
