Skip to content
GitLab
Projects
Groups
Snippets
/
Help
Help
Support
Community forum
Keyboard shortcuts
?
Submit feedback
Sign in
Toggle navigation
Menu
Open sidebar
Käppler
ampel-firmware
Commits
b0fc3f96
Commit
b0fc3f96
authored
Dec 25, 2020
by
Eric Duminil
Browse files
Merge branch 'co2_refactor' into develop
parents
b28542b7
6781d9d6
Changes
9
Hide whitespace changes
Inline
Side-by-side
ampel-firmware.ino
View file @
b0fc3f96
...
...
@@ -64,7 +64,7 @@ void setup() {
Serial
.
begin
(
BAUDS
);
pinMode
(
0
,
INPUT
);
// Flash button (used for forced calibration)
pinMode
(
0
,
INPUT
);
// Flash button (used for forced calibration)
LedEffects
::
setupRing
();
...
...
@@ -111,73 +111,12 @@ void loop() {
//TODO: Restart every day or week, in order to not let t0 overflow?
uint32_t
t0
=
millis
();
/**
* USER INTERACTION
*/
keepServicesAlive
();
// Short press for night mode, Long press for calibration.
checkFlashButton
();
/**
* GET DATA
*/
bool
freshData
=
sensor
::
scd30
.
dataAvailable
();
// Alternative : close to time-step AND dataAvailable, to avoid asking the sensor too often.
if
(
freshData
)
{
//TODO: Move to co2_sensor.cpp
//TODO: Save count of stable measurements
//TODO: Compare time to previous measurements, check that it's not too far away from config::measurement_interval
sensor
::
co2
=
sensor
::
scd30
.
getCO2
();
sensor
::
temperature
=
sensor
::
scd30
.
getTemperature
();
sensor
::
humidity
=
sensor
::
scd30
.
getHumidity
();
}
//NOTE: Data is available, but it's sometimes erroneous: the sensor outputs zero ppm but non-zero temperature and non-zero humidity.
if
(
sensor
::
co2
<=
0
)
{
// No measurement yet. Waiting.
LedEffects
::
showWaitingLED
(
color
::
blue
);
return
;
}
/**
* Fresh data. Show it and send it if needed.
*/
if
(
freshData
)
{
sensor
::
timestamp
=
ntp
::
getLocalTime
();
Serial
.
println
(
sensor
::
timestamp
);
Serial
.
print
(
F
(
"co2(ppm): "
));
Serial
.
print
(
sensor
::
co2
);
Serial
.
print
(
F
(
" temp(C): "
));
Serial
.
print
(
sensor
::
temperature
);
Serial
.
print
(
F
(
" humidity(%): "
));
Serial
.
println
(
sensor
::
humidity
);
csv_writer
::
logIfTimeHasCome
(
sensor
::
timestamp
,
sensor
::
co2
,
sensor
::
temperature
,
sensor
::
humidity
);
#ifdef MQTT
mqtt
::
publishIfTimeHasCome
(
sensor
::
timestamp
,
sensor
::
co2
,
sensor
::
temperature
,
sensor
::
humidity
);
#endif
}
if
(
sensor
::
co2
<
250
)
{
// Sensor should be calibrated.
LedEffects
::
showWaitingLED
(
color
::
magenta
);
return
;
}
/**
* Display data, even if it's "old" (with breathing).
* Those effects include a short delay.
*/
if
(
sensor
::
co2
<
2000
)
{
LedEffects
::
displayCO2color
(
sensor
::
co2
);
LedEffects
::
breathe
(
sensor
::
co2
);
}
else
{
// >= 2000: entire ring blinks red
LedEffects
::
redAlert
();
}
sensor
::
processData
();
uint32_t
duration
=
millis
()
-
t0
;
if
(
duration
>
max_loop_duration
)
{
...
...
co2_sensor.cpp
View file @
b0fc3f96
...
...
@@ -2,17 +2,17 @@
namespace
config
{
// Values should be defined in config.h
uint16_t
measurement_timestep
=
MEASUREMENT_TIMESTEP
;
// [s] Value between 2 and 1800 (range for SCD30 sensor)
const
uint16_t
altitude_above_sea_level
=
ALTITUDE_ABOVE_SEA_LEVEL
;
// [m]
uint16_t
co2_calibration_level
=
ATMOSPHERIC_CO2_CONCENTRATION
;
// [ppm]
uint16_t
measurement_timestep
=
MEASUREMENT_TIMESTEP
;
// [s] Value between 2 and 1800 (range for SCD30 sensor)
const
uint16_t
altitude_above_sea_level
=
ALTITUDE_ABOVE_SEA_LEVEL
;
// [m]
uint16_t
co2_calibration_level
=
ATMOSPHERIC_CO2_CONCENTRATION
;
// [ppm]
#ifdef TEMPERATURE_OFFSET
// Residual heat from CO2 sensor seems to be high enough to change the temperature reading. How much should it be offset?
// NOTE: Sign isn't relevant. The returned temperature will always be shifted down.
const
float
temperature_offset
=
TEMPERATURE_OFFSET
;
// [K]
const
float
temperature_offset
=
TEMPERATURE_OFFSET
;
// [K]
#else
const
float
temperature_offset
=
-
3.0
;
// [K] Temperature measured by sensor is usually at least 3K too high.
#endif
const
bool
auto_calibrate_sensor
=
AUTO_CALIBRATE_SENSOR
;
// [true / false]
const
bool
auto_calibrate_sensor
=
AUTO_CALIBRATE_SENSOR
;
// [true / false]
}
namespace
sensor
{
...
...
@@ -21,13 +21,16 @@ namespace sensor {
float
temperature
=
0
;
float
humidity
=
0
;
String
timestamp
=
""
;
int16_t
stable_measurements
=
0
;
uint32_t
waiting_color
=
color
::
blue
;
bool
should_calibrate
=
false
;
void
initialize
()
{
#if defined(ESP8266)
Wire
.
begin
(
12
,
14
);
// ESP8266 - D6, D5;
#endif
#if defined(ESP32)
Wire
.
begin
(
21
,
22
);
// ESP32
Wire
.
begin
(
21
,
22
);
// ESP32
/**
* SCD30 ESP32
* VCC --- 3V3
...
...
@@ -46,11 +49,12 @@ namespace sensor {
}
// SCD30 has its own timer.
//NOTE: The timer seems to be inaccurate, though, possibly depending on voltage. Should it be offset?
Serial
.
println
();
Serial
.
print
(
F
(
"Setting SCD30 timestep to "
));
Serial
.
print
(
config
::
measurement_timestep
);
Serial
.
println
(
" s."
);
scd30
.
setMeasurementInterval
(
config
::
measurement_timestep
);
// [s]
scd30
.
setMeasurementInterval
(
config
::
measurement_timestep
);
// [s]
Serial
.
print
(
F
(
"Setting temperature offset to -"
));
Serial
.
print
(
abs
(
config
::
temperature_offset
));
...
...
@@ -66,8 +70,43 @@ namespace sensor {
Serial
.
println
(
config
::
auto_calibrate_sensor
?
"ON."
:
"OFF."
);
}
void
waitUntilMeasurementsAreStable
()
{
//TODO: Refactor completely, in order to avoid very long loop?
//NOTE: should timer deviation be used to adjust measurement_timestep?
void
checkTimerDeviation
()
{
static
int32_t
previous_measurement_at
=
0
;
int32_t
now
=
millis
();
Serial
.
print
(
"Measurement time offset : "
);
Serial
.
print
(
now
-
previous_measurement_at
-
config
::
measurement_timestep
*
1000
);
Serial
.
println
(
" ms."
);
previous_measurement_at
=
now
;
}
void
countStableMeasurements
()
{
static
int16_t
previous_co2
=
0
;
if
(
co2
>
(
previous_co2
-
30
)
&&
co2
<
(
previous_co2
+
30
))
{
stable_measurements
++
;
Serial
.
print
(
F
(
"Number of stable measurements : "
));
Serial
.
println
(
stable_measurements
);
waiting_color
=
color
::
green
;
}
else
{
stable_measurements
=
0
;
waiting_color
=
color
::
red
;
}
previous_co2
=
co2
;
}
bool
updateDataIfAvailable
()
{
if
(
scd30
.
dataAvailable
())
{
// checkTimerDeviation();
timestamp
=
ntp
::
getLocalTime
();
co2
=
scd30
.
getCO2
();
temperature
=
scd30
.
getTemperature
();
humidity
=
scd30
.
getHumidity
();
return
true
;
}
return
false
;
}
void
startCalibrationProcess
()
{
/** From the sensor documentation:
* For best results, the sensor has to be run in a stable environment in continuous mode at
* a measurement rate of 2s for at least two minutes before applying the FRC command and sending the reference value.
...
...
@@ -76,41 +115,81 @@ namespace sensor {
scd30
.
setMeasurementInterval
(
2
);
// [s] The change will only take effect after next measurement.
Serial
.
println
(
F
(
"Waiting until the measurements are stable for at least 2 minutes."
));
Serial
.
println
(
F
(
"It could take a very long time."
));
//########################################################################################################
// (c) Mario Lukas
// https://github.com/mariolukas/Watterott-CO2-Ampel-Plus-Firmware/blob/main/CO2-Ampel_Plus/Sensor.cpp#L57
uint32_t
last_color
=
color
::
blue
;
int
stable_measurements
=
0
,
last_co2
=
0
;
for
(
stable_measurements
=
0
;
stable_measurements
<
60
;)
{
if
(
scd30
.
dataAvailable
())
{
co2
=
scd30
.
getCO2
();
//No more than +/-30ppm variation compared to previous measurement.
if
((
co2
>
(
last_co2
-
30
))
&&
(
co2
<
(
last_co2
+
30
)))
{
last_color
=
color
::
green
;
stable_measurements
++
;
}
else
{
last_color
=
color
::
red
;
stable_measurements
=
0
;
}
last_co2
=
co2
;
}
LedEffects
::
showKITTWheel
(
last_color
,
1
);
}
//########################################################################################################
should_calibrate
=
true
;
}
void
startCalibrationProcess
()
{
waitUntilMeasurementsAreStable
();
Serial
.
print
(
"Starting SCD30 calibration..."
);
void
calibrateAndRestart
()
{
Serial
.
print
(
F
(
"Calibrating SCD30 now..."
));
scd30
.
setAltitudeCompensation
(
config
::
altitude_above_sea_level
);
scd30
.
setForcedRecalibrationFactor
(
config
::
co2_calibration_level
);
Serial
.
println
(
" Done!"
);
Serial
.
println
(
"Sensor calibrated."
);
Serial
.
println
(
"Sensor will now restart."
);
LedEffects
::
showKITTWheel
(
color
::
green
,
5
);
//TODO: Add LEDs off and move to util::reset()
FS_LIB
.
end
();
ESP
.
restart
();
Serial
.
println
(
F
(
" Done!"
));
Serial
.
println
(
F
(
"Sensor calibrated."
));
resetAmpel
();
}
void
logToSerial
()
{
Serial
.
println
(
timestamp
);
Serial
.
print
(
F
(
"co2(ppm): "
));
Serial
.
print
(
co2
);
Serial
.
print
(
F
(
" temp(C): "
));
Serial
.
print
(
temperature
);
Serial
.
print
(
F
(
" humidity(%): "
));
Serial
.
println
(
humidity
);
}
void
displayCO2OnLedRing
()
{
if
(
co2
<
250
)
{
// Sensor should be calibrated.
LedEffects
::
showWaitingLED
(
color
::
magenta
);
return
;
}
/**
* Display data, even if it's "old" (with breathing).
* Those effects include a short delay.
*/
if
(
co2
<
2000
)
{
LedEffects
::
displayCO2color
(
co2
);
LedEffects
::
breathe
(
co2
);
}
else
{
// >= 2000: entire ring blinks red
LedEffects
::
redAlert
();
}
}
void
processData
()
{
bool
freshData
=
updateDataIfAvailable
();
//NOTE: Data is available, but it's sometimes erroneous: the sensor outputs zero ppm but non-zero temperature and non-zero humidity.
if
(
co2
<=
0
)
{
// No measurement yet. Waiting.
LedEffects
::
showWaitingLED
(
color
::
blue
);
return
;
}
/**
* Fresh data. Log it and send it if needed.
*/
if
(
freshData
)
{
if
(
should_calibrate
)
{
countStableMeasurements
();
}
logToSerial
();
csv_writer
::
logIfTimeHasCome
(
timestamp
,
co2
,
temperature
,
humidity
);
#ifdef MQTT
mqtt
::
publishIfTimeHasCome
(
timestamp
,
co2
,
temperature
,
humidity
);
#endif
}
if
(
should_calibrate
)
{
if
(
stable_measurements
==
60
)
{
calibrateAndRestart
();
}
LedEffects
::
showWaitingLED
(
waiting_color
);
return
;
}
displayCO2OnLedRing
();
}
}
co2_sensor.h
View file @
b0fc3f96
...
...
@@ -6,13 +6,18 @@
#include
"src/lib/SparkFun_SCD30_Arduino_Library/src/SparkFun_SCD30_Arduino_Library.h"
// From: http://librarymanager/All#SparkFun_SCD30
#include
"config.h"
#include
"led_effects.h"
#include
"util.h"
#include
"csv_writer.h"
// To close filesystem before restart.
#include
<Wire.h>
#ifdef MQTT
# include "mqtt.h"
#endif
namespace
config
{
extern
uint16_t
measurement_timestep
;
// [s] Value between 2 and 1800 (range for SCD30 sensor)
extern
uint16_t
measurement_timestep
;
// [s] Value between 2 and 1800 (range for SCD30 sensor)
extern
const
bool
auto_calibrate_sensor
;
// [true / false]
extern
uint16_t
co2_calibration_level
;
// [ppm]
extern
uint16_t
co2_calibration_level
;
// [ppm]
extern
const
float
temperature_offset
;
// [K] Sign isn't relevant.
}
...
...
@@ -24,6 +29,7 @@ namespace sensor {
extern
String
timestamp
;
void
initialize
();
void
processData
();
void
startCalibrationProcess
();
}
#endif
led_effects.cpp
View file @
b0fc3f96
...
...
@@ -11,7 +11,7 @@ namespace config {
/*****************************************************************
* Configuration (calculated from above values) *
*****************************************************************/
namespace
config
//TODO: Use a class instead. NightMode could then be another state.
namespace
config
//TODO: Use a class instead. NightMode could then be another state.
{
const
float
average_brightness
=
0.5
*
(
config
::
max_brightness
+
config
::
min_brightness
);
const
float
brightness_amplitude
=
0.5
*
(
config
::
max_brightness
-
config
::
min_brightness
);
...
...
@@ -34,12 +34,6 @@ const uint16_t CO2_TICKS[NUMPIXELS + 1] = { 0, 500, 600, 700, 800, 900, 1000, 12
const
uint16_t
LED_HUES
[
NUMPIXELS
]
=
{
21845
,
19114
,
16383
,
13653
,
10922
,
8191
,
5461
,
2730
,
0
,
0
,
0
,
0
};
// [hue angle]
Adafruit_NeoPixel
pixels
(
NUMPIXELS
,
NEOPIXELS_PIN
,
NEO_GRB
+
NEO_KHZ800
);
namespace
counter
{
uint16_t
wheel_offset
=
0
;
uint16_t
kitt_offset
=
0
;
uint16_t
breathing_offset
=
0
;
}
// namespace counter
namespace
LedEffects
{
//On-board LED on D4, aka GPIO02
const
int
ONBOARD_LED_PIN
=
2
;
...
...
@@ -56,18 +50,23 @@ namespace LedEffects {
digitalWrite
(
ONBOARD_LED_PIN
,
LOW
);
}
void
LEDsOff
()
{
pixels
.
clear
();
pixels
.
show
();
onBoardLEDOff
();
}
void
setupRing
()
{
pixels
.
begin
();
pixels
.
setBrightness
(
config
::
max_brightness
);
pixels
.
clear
();
LEDsOff
();
}
void
toggleNightMode
()
{
config
::
night_mode
=
!
config
::
night_mode
;
if
(
config
::
night_mode
)
{
Serial
.
println
(
F
(
"NIGHT MODE!"
));
pixels
.
clear
();
pixels
.
show
();
LEDsOff
();
}
else
{
Serial
.
println
(
F
(
"DAY MODE!"
));
}
...
...
@@ -79,13 +78,14 @@ namespace LedEffects {
if
(
config
::
night_mode
)
{
return
;
}
static
uint16_t
kitt_offset
=
0
;
pixels
.
clear
();
for
(
int
j
=
config
::
kitt_tail
;
j
>=
0
;
j
--
)
{
int
ledNumber
=
abs
((
counter
::
kitt_offset
-
j
+
NUMPIXELS
)
%
(
2
*
NUMPIXELS
)
-
NUMPIXELS
)
%
NUMPIXELS
;
// Triangular function
int
ledNumber
=
abs
((
kitt_offset
-
j
+
NUMPIXELS
)
%
(
2
*
NUMPIXELS
)
-
NUMPIXELS
)
%
NUMPIXELS
;
// Triangular function
pixels
.
setPixelColor
(
ledNumber
,
color
*
pixels
.
gamma8
(
255
-
j
*
76
)
/
255
);
}
pixels
.
show
();
counter
::
kitt_offset
+=
1
;
kitt_offset
++
;
}
// Start K.I.T.T. led effect. Red color as default.
...
...
@@ -135,12 +135,13 @@ namespace LedEffects {
if
(
config
::
night_mode
)
{
return
;
}
static
uint16_t
wheel_offset
=
0
;
unsigned
long
t0
=
seconds
();
pixels
.
setBrightness
(
config
::
max_brightness
);
while
(
seconds
()
<
t0
+
duration_s
)
{
for
(
int
i
=
0
;
i
<
NUMPIXELS
;
i
++
)
{
pixels
.
setPixelColor
(
i
,
pixels
.
ColorHSV
(
i
*
65535
/
NUMPIXELS
+
counter
::
wheel_offset
));
counter
::
wheel_offset
+=
hue_increment
;
pixels
.
setPixelColor
(
i
,
pixels
.
ColorHSV
(
i
*
65535
/
NUMPIXELS
+
wheel_offset
));
wheel_offset
+=
hue_increment
;
}
pixels
.
show
();
delay
(
10
);
...
...
@@ -165,14 +166,14 @@ namespace LedEffects {
void
breathe
(
int16_t
co2
)
{
if
(
!
config
::
night_mode
)
{
static
uint16_t
breathing_offset
=
0
;
//TODO: use integer sine
pixels
.
setBrightness
(
static_cast
<
int
>
(
config
::
average_brightness
+
cos
(
counter
::
breathing_offset
*
0.1
)
*
config
::
brightness_amplitude
));
static_cast
<
int
>
(
config
::
average_brightness
+
cos
(
breathing_offset
*
0.1
)
*
config
::
brightness_amplitude
));
pixels
.
show
();
counter
::
breathing_offset
+=
1
;
breathing_offset
++
;
}
delay
(
co2
>
1600
?
50
:
100
);
// faster breathing for higher CO2 values
delay
(
co2
>
1600
?
50
:
100
);
// faster breathing for higher CO2 values
}
/**
...
...
@@ -181,7 +182,7 @@ namespace LedEffects {
*/
int
countdownToZero
()
{
if
(
config
::
night_mode
)
{
Serial
.
println
(
"Night mode. Not doing anything."
);
Serial
.
println
(
F
(
"Night mode. Not doing anything."
)
)
;
delay
(
1000
);
// Wait for a while, to avoid coming back to this function too many times when button is pressed.
return
1
;
}
...
...
led_effects.h
View file @
b0fc3f96
...
...
@@ -22,6 +22,7 @@ namespace LedEffects {
void
onBoardLEDOff
();
void
onBoardLEDOn
();
void
toggleNightMode
();
void
LEDsOff
();
void
setupRing
();
void
redAlert
();
...
...
mqtt.cpp
View file @
b0fc3f96
...
...
@@ -162,8 +162,7 @@ namespace mqtt {
}
else
if
(
messageString
==
"local_ip"
)
{
sendInfoAboutLocalNetwork
();
}
else
if
(
messageString
==
"reset"
)
{
FS_LIB
.
end
();
ESP
.
restart
();
resetAmpel
();
}
else
{
LedEffects
::
showKITTWheel
(
color
::
red
,
1
);
Serial
.
println
(
F
(
"Message not supported. Doing nothing."
));
...
...
util.cpp
View file @
b0fc3f96
...
...
@@ -38,6 +38,14 @@ namespace ntp {
}
}
void
resetAmpel
()
{
Serial
.
print
(
"Resetting"
);
FS_LIB
.
end
();
LedEffects
::
LEDsOff
();
delay
(
1000
);
ESP
.
restart
();
}
uint32_t
max_loop_duration
=
0
;
//FIXME: Remove every instance of Strings, to avoid heap fragmentation problems. (Start: "Free heap space : 17104 bytes")
...
...
util.h
View file @
b0fc3f96
...
...
@@ -3,6 +3,7 @@
#include
<Arduino.h>
#include
"config.h"
#include
"wifi_util.h"
// To get MAC
#include
"csv_writer.h"
// To close filesystem before reset
#include
<WiFiUdp.h>
//required for NTP
#include
"src/lib/NTPClient-master/NTPClient.h"
// NTP
...
...
@@ -27,4 +28,6 @@ namespace ntp {
extern
uint32_t
max_loop_duration
;
const
extern
String
SENSOR_ID
;
void
resetAmpel
();
#endif
web_server.cpp
View file @
b0fc3f96
...
...
@@ -88,7 +88,7 @@ namespace web_server {
"<tr><td>Last MQTT publish</td><td>%s</td></tr>
\n
"
"<tr><td>MQTT publish timestep</td><td>%5d s</td></tr>
\n
"
#endif
"<tr><td>Temperature offset</td><td>%.1fK</td></tr>
\n
"
"<tr><td>Temperature offset</td><td>%.1fK</td></tr>
\n
"
//TODO: Read it from sensor?
"<tr><td>Local address</td><td><a href='http://%s.local/'>%s.local</a></td></tr>
\n
"
"<tr><td>Local IP</td><td><a href='http://%s'>%s</a></td></tr>
\n
"
"<tr><td>Free heap space</td><td>%6d bytes</td></tr>
\n
"
...
...
Write
Preview
Supports
Markdown
0%
Try again
or
attach a new file
.
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment