finished power meter and speed meter
This commit is contained in:
parent
3d9aee6d0a
commit
b136342c2c
29
ESP32/ADC.cpp
Normal file
29
ESP32/ADC.cpp
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
#include "ADC.h"
|
||||||
|
|
||||||
|
namespace
|
||||||
|
{
|
||||||
|
const int8_t calibrationNumValues = sizeof(ADC_calibration)/sizeof(ADC_calibration[0]);
|
||||||
|
}
|
||||||
|
|
||||||
|
ADC::ADC(int8_t gpioPin)
|
||||||
|
: pin_(gpioPin)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
float ADC::read()
|
||||||
|
{
|
||||||
|
int16_t rawValue = analogRead(pin_);
|
||||||
|
|
||||||
|
for(int8_t i = 1; i < calibrationNumValues; ++i)
|
||||||
|
{
|
||||||
|
if(i == calibrationNumValues - 1 || ADC_calibration[i].ADC_Value >= rawValue)
|
||||||
|
{
|
||||||
|
const auto& p = ADC_calibration[i - 1];
|
||||||
|
const auto& n = ADC_calibration[i];
|
||||||
|
return (float)(rawValue - p.ADC_Value) / (float)(n.ADC_Value - p.ADC_Value) * (n.MeasuredVoltage - p.MeasuredVoltage) + p.MeasuredVoltage;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// This should never happen if the calibration array contains at least 2 entries
|
||||||
|
return -std::numeric_limits<float>::max();
|
||||||
|
}
|
33
ESP32/ADC.h
Normal file
33
ESP32/ADC.h
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
#include <Arduino.h>
|
||||||
|
|
||||||
|
struct ADC_CalibrationValue
|
||||||
|
{
|
||||||
|
float MeasuredVoltage;
|
||||||
|
int16_t ADC_Value;
|
||||||
|
};
|
||||||
|
|
||||||
|
// To improve ADC precision, these values must be measured with a voltmeter
|
||||||
|
// You can use a potentiometer to generate different voltages, measure them, and add an entry with the corresponding ADC value returned by analogRead()
|
||||||
|
const ADC_CalibrationValue ADC_calibration[] = {
|
||||||
|
{ 0.118f, 1 },
|
||||||
|
{ 0.343f, 253 },
|
||||||
|
{ 0.791f, 801 },
|
||||||
|
{ 1.512f, 1705 },
|
||||||
|
{ 2.013f, 2306 },
|
||||||
|
{ 2.406f, 2796 },
|
||||||
|
{ 2.606f, 3058 },
|
||||||
|
{ 2.839f, 3423 },
|
||||||
|
{ 2.996f, 3726 },
|
||||||
|
{ 3.16f, 4094 }
|
||||||
|
};
|
||||||
|
|
||||||
|
class ADC
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
ADC(int8_t gpioPin);
|
||||||
|
|
||||||
|
float read();
|
||||||
|
|
||||||
|
private:
|
||||||
|
int8_t pin_;
|
||||||
|
};
|
5
ESP32/IDECompat.h
Normal file
5
ESP32/IDECompat.h
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
#include <Arduino.h>
|
||||||
|
|
||||||
|
#ifndef IRAM_ATTR
|
||||||
|
#define IRAM_ATTR
|
||||||
|
#endif
|
@ -1,54 +1,94 @@
|
|||||||
#include <Arduino.h>
|
#include <Arduino.h>
|
||||||
|
#include "IDECompat.h"
|
||||||
|
|
||||||
#include <WiFi.h>
|
#include <WiFi.h>
|
||||||
#include <FS.h>
|
#include <FS.h>
|
||||||
#include <SPIFFS.h>
|
#include <SPIFFS.h>
|
||||||
#include <ESPAsyncWebServer.h>
|
#include <ESPAsyncWebServer.h>
|
||||||
|
|
||||||
|
#include "ADC.h"
|
||||||
|
|
||||||
#include "wifi-credentials.h"
|
#include "wifi-credentials.h"
|
||||||
|
|
||||||
AsyncWebServer server(80);
|
AsyncWebServer server(80);
|
||||||
|
|
||||||
float averageBatteryVoltage = 0.0f;
|
ADC currentSensor(36);
|
||||||
int16_t batteryVoltage = -1; //in mV
|
ADC batterySensor(39);
|
||||||
|
const int8_t speedSensorPin = 13;
|
||||||
|
const int8_t debugLedPin = 12;
|
||||||
|
|
||||||
struct ADC_CalibrationValue
|
const float wheelDiameterInches = 20;
|
||||||
|
const int numImpulsesPerTurn = 2;
|
||||||
|
const float wheelCircumferenceMeters = wheelDiameterInches * 0.0254f * 3.1415f / (float)numImpulsesPerTurn;
|
||||||
|
|
||||||
|
int16_t batteryVoltage = -1; // in mV
|
||||||
|
int16_t batteryCurrent = -1; // in mV
|
||||||
|
|
||||||
|
volatile bool debugLedState = true;
|
||||||
|
|
||||||
|
volatile bool speedSensorState = false;
|
||||||
|
volatile unsigned long speedSensorRiseTime = 0;
|
||||||
|
volatile unsigned long speedSensorLastImpulseTime = 0;
|
||||||
|
volatile unsigned long speedSensorLastImpulseInterval = (unsigned long)-1; // in milliseconds
|
||||||
|
void IRAM_ATTR onSpeedSensorChange(bool newState)
|
||||||
{
|
{
|
||||||
float Measure;
|
if(speedSensorState == newState) return;
|
||||||
int16_t ADC_Value;
|
unsigned long now = millis();
|
||||||
};
|
speedSensorState = newState;
|
||||||
|
|
||||||
const ADC_CalibrationValue calibration[] = {
|
bool magnetDetected = !speedSensorState; // the magnet closes the contact which pulls the pin low
|
||||||
{ 0.118f, 1 },
|
|
||||||
{ 0.343f, 253 },
|
|
||||||
{ 0.791f, 801 },
|
|
||||||
{ 1.512f, 1705 },
|
|
||||||
{ 2.013f, 2306 },
|
|
||||||
{ 2.406f, 2796 },
|
|
||||||
{ 2.606f, 3058 },
|
|
||||||
{ 2.839f, 3423 },
|
|
||||||
{ 2.996f, 3726 },
|
|
||||||
{ 3.16f, 4094 }
|
|
||||||
};
|
|
||||||
const int8_t calibrationNumValues = sizeof(calibration)/sizeof(calibration[0]);
|
|
||||||
|
|
||||||
float getCalibratedVoltage(int16_t adcOutput)
|
if(magnetDetected)
|
||||||
{
|
|
||||||
for(int8_t i = 1; i < calibrationNumValues; ++i)
|
|
||||||
{
|
{
|
||||||
if(i == calibrationNumValues - 1 || calibration[i].ADC_Value >= adcOutput)
|
speedSensorRiseTime = now;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
unsigned long impulseDuration = now > speedSensorRiseTime ? now - speedSensorRiseTime : (4294967295 - speedSensorRiseTime) + now; // if now is lower than speedSensorRiseTime, it means millis() has overflowed (happens every 50 days)
|
||||||
|
if(impulseDuration > 1000) return; // impulse was too long, ignore it (maybe magnet stopped near the sensor)
|
||||||
|
|
||||||
|
debugLedState = !debugLedState;
|
||||||
|
digitalWrite(debugLedPin, debugLedState ? HIGH : LOW);
|
||||||
|
|
||||||
|
unsigned long timeSinceLastImpulse = now > speedSensorLastImpulseTime ? now - speedSensorLastImpulseTime : (4294967295 - speedSensorLastImpulseTime) + now;
|
||||||
|
speedSensorLastImpulseTime = now;
|
||||||
|
if(timeSinceLastImpulse > 30 && timeSinceLastImpulse < 4000)
|
||||||
{
|
{
|
||||||
const auto& p = calibration[i - 1];
|
speedSensorLastImpulseInterval = timeSinceLastImpulse;
|
||||||
const auto& n = calibration[i];
|
}
|
||||||
return (float)(adcOutput - p.ADC_Value) / (float)(n.ADC_Value - p.ADC_Value) * (n.Measure - p.Measure) + p.Measure;
|
else
|
||||||
|
{
|
||||||
|
speedSensorLastImpulseInterval = (unsigned long)-1;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return -1.0f;
|
void IRAM_ATTR onSpeedSensorChange() { onSpeedSensorChange(digitalRead(speedSensorPin) == HIGH); }
|
||||||
|
|
||||||
|
float getSpeed()
|
||||||
|
{
|
||||||
|
unsigned long now = millis();
|
||||||
|
|
||||||
|
unsigned long lastImpulseInterval = speedSensorLastImpulseInterval;
|
||||||
|
unsigned long lastImpulseTime = speedSensorLastImpulseTime;
|
||||||
|
unsigned long timeSinceLastImpulse = now > lastImpulseTime ? now - lastImpulseTime : (4294967295 - lastImpulseTime) + now;
|
||||||
|
|
||||||
|
unsigned long interval = timeSinceLastImpulse > lastImpulseInterval * 10 / 9 ? timeSinceLastImpulse : lastImpulseInterval;
|
||||||
|
|
||||||
|
float speed = wheelCircumferenceMeters / (float)interval * 1000.0f; // in meters per second
|
||||||
|
|
||||||
|
if(speed < 0.25f) return 0.0f; // if speed is very low (less than 1km/h) it probably means we've stopped
|
||||||
|
return speed;
|
||||||
}
|
}
|
||||||
|
|
||||||
void setup()
|
void setup()
|
||||||
{
|
{
|
||||||
|
pinMode(speedSensorPin, INPUT_PULLUP);
|
||||||
|
attachInterrupt(speedSensorPin, &onSpeedSensorChange, CHANGE);
|
||||||
|
|
||||||
|
pinMode(debugLedPin, OUTPUT);
|
||||||
|
digitalWrite(debugLedPin, debugLedState ? HIGH : LOW);
|
||||||
|
|
||||||
Serial.begin(115200);
|
Serial.begin(115200);
|
||||||
|
|
||||||
if(!SPIFFS.begin(false)){
|
if(!SPIFFS.begin(false)){
|
||||||
@ -70,8 +110,11 @@ void setup()
|
|||||||
|
|
||||||
server.on("/api/status", HTTP_GET, [](AsyncWebServerRequest *request){
|
server.on("/api/status", HTTP_GET, [](AsyncWebServerRequest *request){
|
||||||
int v = batteryVoltage;
|
int v = batteryVoltage;
|
||||||
|
int c = batteryCurrent;
|
||||||
|
int s = (int)(getSpeed() * 1000.0f + 0.5f);
|
||||||
|
|
||||||
char json[128];
|
char json[128];
|
||||||
sprintf(json, "{\"v\":%d,\"c\":1000}", v);
|
sprintf(json, "{\"v\":%d,\"c\":%d,\"s\":%d}", v, c, s);
|
||||||
request->send(200, "text/json", json);
|
request->send(200, "text/json", json);
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -86,22 +129,33 @@ void setup()
|
|||||||
Serial.println("HTTP server started");
|
Serial.println("HTTP server started");
|
||||||
}
|
}
|
||||||
|
|
||||||
float avgAnalogV = 0.0f;
|
|
||||||
|
|
||||||
void loop()
|
void loop()
|
||||||
{
|
{
|
||||||
const int potPin = 34;
|
const int numSamples = 100;
|
||||||
|
|
||||||
const float minV = 0.14f; // 1
|
float averageV = 0.0f;
|
||||||
const float maxV = 3.16f; // 4094
|
float averageC = 0.0f;
|
||||||
|
for(int sample = 0; sample < numSamples; ++sample)
|
||||||
|
{
|
||||||
|
delay(1);
|
||||||
|
float v = batterySensor.read();
|
||||||
|
float c = currentSensor.read();
|
||||||
|
|
||||||
|
averageV += v;
|
||||||
|
averageC += c;
|
||||||
|
}
|
||||||
|
|
||||||
|
averageV /= (float)numSamples;
|
||||||
|
averageC /= (float)numSamples;
|
||||||
|
|
||||||
|
if(averageV < 0.2f) averageV = 0.0f;
|
||||||
|
|
||||||
|
averageV *= 27.000f; // account for voltage divider to retrieve the input voltage
|
||||||
|
averageC = max(0.0f, averageC - 2.5f) / 0.0238f; // convert voltage to current, according to the sensor linear relation
|
||||||
|
|
||||||
|
// TODO: mutex ?
|
||||||
|
batteryVoltage = (int16_t)(averageV * 1000.0f + 0.5f);
|
||||||
|
batteryCurrent = (int16_t)(averageC * 1000.0f + 0.5f);
|
||||||
|
|
||||||
delay(10);
|
delay(10);
|
||||||
int16_t analogV = analogRead(potPin);
|
|
||||||
float v = getCalibratedVoltage(analogV);
|
|
||||||
|
|
||||||
averageBatteryVoltage = averageBatteryVoltage * 0.95f + v * 0.05f;
|
|
||||||
batteryVoltage = (int16_t)(averageBatteryVoltage * 1000.0f + 0.5f);
|
|
||||||
|
|
||||||
//avgAnalogV = avgAnalogV * 0.9f + (float)analogV * 0.1f;
|
|
||||||
//batteryVoltage = (int16_t)(avgAnalogV + 0.5f);
|
|
||||||
}
|
}
|
||||||
|
@ -26,6 +26,7 @@ export default class MainPage {
|
|||||||
<p>Tension batterie : {this.status.batteryVoltage.toFixed(3)}V</p>
|
<p>Tension batterie : {this.status.batteryVoltage.toFixed(3)}V</p>
|
||||||
<p>Courant : {this.status.motorCurrent.toFixed(3)}A</p>
|
<p>Courant : {this.status.motorCurrent.toFixed(3)}A</p>
|
||||||
<p>Puissance : {(this.status.batteryVoltage * this.status.motorCurrent).toFixed(1)}W</p>
|
<p>Puissance : {(this.status.batteryVoltage * this.status.motorCurrent).toFixed(1)}W</p>
|
||||||
|
<p>Vitesse : {(this.status.speed * 3.6).toFixed(1)}km/h</p>
|
||||||
</div>
|
</div>
|
||||||
: <p>Chargement...</p>;
|
: <p>Chargement...</p>;
|
||||||
}
|
}
|
||||||
|
@ -3,11 +3,13 @@ import m from 'mithril';
|
|||||||
export interface Status {
|
export interface Status {
|
||||||
batteryVoltage: number;
|
batteryVoltage: number;
|
||||||
motorCurrent: number;
|
motorCurrent: number;
|
||||||
|
speed: number; // in meters per second
|
||||||
};
|
};
|
||||||
|
|
||||||
interface ApiStatus {
|
interface ApiStatus {
|
||||||
v: number;
|
v: number;
|
||||||
c: number;
|
c: number;
|
||||||
|
s: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class MonitorApi {
|
export class MonitorApi {
|
||||||
@ -21,7 +23,8 @@ export class MonitorApi {
|
|||||||
await new Promise(resolve => setTimeout(resolve, 200));
|
await new Promise(resolve => setTimeout(resolve, 200));
|
||||||
apiStatus = {
|
apiStatus = {
|
||||||
v: Math.random() * 20000 + 20000,
|
v: Math.random() * 20000 + 20000,
|
||||||
c: Math.random() * 30000
|
c: Math.random() * 30000,
|
||||||
|
s: Math.random() * 14000
|
||||||
}
|
}
|
||||||
setTimeout(() => m.redraw(), 0);
|
setTimeout(() => m.redraw(), 0);
|
||||||
} else {
|
} else {
|
||||||
@ -33,7 +36,8 @@ export class MonitorApi {
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
batteryVoltage: apiStatus.v / 1000,
|
batteryVoltage: apiStatus.v / 1000,
|
||||||
motorCurrent: apiStatus.c / 1000
|
motorCurrent: apiStatus.c / 1000,
|
||||||
|
speed: apiStatus.s / 1000
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -122,11 +122,11 @@
|
|||||||
(effects (font (size 1 1) (thickness 0.15)))
|
(effects (font (size 1 1) (thickness 0.15)))
|
||||||
(tstamp 7f5881c0-cd18-48ee-b734-9fc646780ef5)
|
(tstamp 7f5881c0-cd18-48ee-b734-9fc646780ef5)
|
||||||
)
|
)
|
||||||
(fp_text value "180K" (at 5.08 2.37 90) (layer "F.Fab")
|
(fp_text value "180K" (at 5.08 0 90) (layer "F.Fab")
|
||||||
(effects (font (size 1 1) (thickness 0.15)))
|
(effects (font (size 1 1) (thickness 0.15)))
|
||||||
(tstamp 5a0e8fb0-01f2-49ab-8dc9-86484b088050)
|
(tstamp 5a0e8fb0-01f2-49ab-8dc9-86484b088050)
|
||||||
)
|
)
|
||||||
(fp_text user "${REFERENCE}" (at 5.08 0 90) (layer "F.Fab")
|
(fp_text user "${REFERENCE}" (at 5.08 -2.54 90) (layer "F.Fab")
|
||||||
(effects (font (size 1 1) (thickness 0.15)))
|
(effects (font (size 1 1) (thickness 0.15)))
|
||||||
(tstamp 8c5fdfe4-7881-43aa-b104-783c04c26c7c)
|
(tstamp 8c5fdfe4-7881-43aa-b104-783c04c26c7c)
|
||||||
)
|
)
|
||||||
@ -462,11 +462,11 @@
|
|||||||
(effects (font (size 1 1) (thickness 0.15)))
|
(effects (font (size 1 1) (thickness 0.15)))
|
||||||
(tstamp c00665c7-238f-42f0-9cc8-e3a8fa63e130)
|
(tstamp c00665c7-238f-42f0-9cc8-e3a8fa63e130)
|
||||||
)
|
)
|
||||||
(fp_text value "12K" (at 5.08 2.37 90) (layer "F.Fab")
|
(fp_text value "12K" (at 5.08 0 90) (layer "F.Fab")
|
||||||
(effects (font (size 1 1) (thickness 0.15)))
|
(effects (font (size 1 1) (thickness 0.15)))
|
||||||
(tstamp 2f1d6d2c-caeb-48e9-bf74-39571b5d016d)
|
(tstamp 2f1d6d2c-caeb-48e9-bf74-39571b5d016d)
|
||||||
)
|
)
|
||||||
(fp_text user "${REFERENCE}" (at 5.08 0 90) (layer "F.Fab")
|
(fp_text user "${REFERENCE}" (at 5.08 -2.54 90) (layer "F.Fab")
|
||||||
(effects (font (size 1 1) (thickness 0.15)))
|
(effects (font (size 1 1) (thickness 0.15)))
|
||||||
(tstamp 6d29d228-5ab1-4850-93ed-cc89252531fb)
|
(tstamp 6d29d228-5ab1-4850-93ed-cc89252531fb)
|
||||||
)
|
)
|
||||||
|
Loading…
Reference in New Issue
Block a user