Compare commits

..

13 Commits

Author SHA1 Message Date
cryptogopher
c2af98d588 Display also humidity and pressure
Display update triggered by value change, not interval
2026-02-12 00:10:45 +01:00
cryptogopher
0c9a7a51d9 Add description, part list and build instructions 2026-02-07 18:01:00 +01:00
cryptogopher
6a0a954814 Update countdown 2026-02-07 16:58:15 +01:00
cryptogopher
d6f9bb6634 Add some TODOs 2022-10-10 15:40:01 +02:00
cryptogopher
1e206b248c Merge branch 'timerisr' 2022-10-09 22:25:05 +02:00
cryptogopher
8594905748 Add temperature hysteresis
Do not refresh display if value not changed
2022-10-09 22:18:59 +02:00
cryptogopher
814a99f733 Remove buzzer state variable 2022-10-09 21:48:16 +02:00
cryptogopher
7fe04d76cb Extract buzzer to separate file 2022-10-09 21:36:40 +02:00
cryptogopher
f8878d3c37 Replace tone() with custom timer 2022-10-09 20:34:25 +02:00
cryptogopher
374a72fb07 Move tube to separate file 2022-10-09 15:29:30 +02:00
cryptogopher
21512bc4f4 Add temperature and break countdown display 2022-10-09 15:09:52 +02:00
cryptogopher
3872ece37b Join ISRs 2022-10-09 14:40:33 +02:00
cryptogopher
315aa9996c Update break time and beep freq 2022-10-09 14:26:33 +02:00
7 changed files with 210 additions and 21 deletions

21
README.md Normal file
View File

@@ -0,0 +1,21 @@
Arduino based device acting as:
* metronome, for exercises that require timing,
* countdown timer, for rest between exercises,
* thermometer / hygrometer - when not uses for exercises.
Part list:
* Arduino MKR 1010 (ABX00023)
* Arduino MKR Connector Carrier, Grove compatible (ASX00007)
* BME280 sensor (Seeed Studio SEE-11355)
* quad, alphanumeric (14-segment), HT16K33 based display (SeedStudio SEE-14733)
* push button with backlight (Seeed Studio SEE-13660)
* passive buzzer (Seeed Studio SEE-17268)
* I2C 6 port hub (Seeed Studio SEE-15856)
* cables - as required
All modules based on Grove connections and cables.
All modules are connected to Arduino through Connector Carrier:
* buzzer - port D0,
* button - port D5,
* I2C hub - port TWI,
* sensor and display - to hub (any port)

50
bme280.ino Normal file
View File

@@ -0,0 +1,50 @@
#include "Seeed_BME280.h"
enum {
LOWER = 0,
HIGHER
};
BME280 sensor;
float bounds[2][SENSORS] = {};
const float hysteresisWidth[SENSORS] = {0.03, 0.5, 0.5};
void initBME280() {
sensor.init();
}
void readSensors() {
float newValue;
for (uint i = 0; i < SENSORS; i++) {
switch (i) {
case TEMPERATURE:
newValue = sensor.getTemperature();
break;
case HUMIDITY:
newValue = sensor.getHumidity();
break;
case PRESSURE:
newValue = sensor.getPressure();
break;
}
if (newValue > bounds[HIGHER][i]) {
sensorValue[i] = newValue;
bounds[HIGHER][i] = newValue;
bounds[LOWER][i] = bounds[HIGHER][i] - hysteresisWidth[i];
if ((countdown <= 0) && (displaySensor == i)) {
displayNeedsUpdate = true;
}
} else if (newValue < bounds[LOWER][i]) {
sensorValue[i] = newValue;
bounds[LOWER][i] = newValue;
bounds[HIGHER][i] = bounds[LOWER][i] + hysteresisWidth[i];
if ((countdown <= 0) && (displaySensor == i)) {
displayNeedsUpdate = true;
}
};
};
}

View File

@@ -1,5 +1,7 @@
volatile unsigned long lastButtonPress = millis(); volatile unsigned long lastButtonPress = millis();
// Debouncing using timer:
// https://github.com/khoih-prog/TimerInterrupt/blob/master/examples/SwitchDebounce/SwitchDebounce.ino
void initButton() { void initButton() {
pinMode(BUTTON_PIN, INPUT_PULLUP); pinMode(BUTTON_PIN, INPUT_PULLUP);
// Order of ISRs matter: RISING should be invoked first // Order of ISRs matter: RISING should be invoked first
@@ -7,15 +9,21 @@ void initButton() {
attachInterrupt(digitalPinToInterrupt(BUTTON_PIN), buttonISRstate, RISING); attachInterrupt(digitalPinToInterrupt(BUTTON_PIN), buttonISRstate, RISING);
} }
// FIXME: runMetronome ISR should be started/stopped by button. Otherwise fraction of first PERIOD_MS is lost from countdown.
void buttonISRstate() { void buttonISRstate() {
if ((millis() - lastButtonPress) > 100) { if ((millis() - lastButtonPress) > 100) {
if (countdown < 0) if (countdown < 0)
// TODO: enable metronome timer
countdown = 0; countdown = 0;
else if (countdown == 0) else if (countdown == 0) {
// TODO: restart metronome timer to align countdown
countdown = COUNTDOWN; countdown = COUNTDOWN;
else { updateTube();
} else {
countdown = -1; countdown = -1;
digitalWrite(BUTTON_LED_PIN, LOW); digitalWrite(BUTTON_LED_PIN, LOW);
updateTube();
// TODO: disable metronome timer
} }
} }
} }

22
buzzer.ino Normal file
View File

@@ -0,0 +1,22 @@
SAMDTimer BuzzerTimer(TIMER_TC5);
bool attached = false;
void initBuzzer() {
pinMode(BUZZER_PIN, OUTPUT);
}
void buzzerISR() {
digitalWrite(BUZZER_PIN, !digitalRead(BUZZER_PIN));
}
void buzz() {
if (attached)
BuzzerTimer.enableTimer();
else
BuzzerTimer.attachInterrupt(BUZZER_FREQ *2, buzzerISR);
}
void noBuzz() {
BuzzerTimer.disableTimer();
}

Binary file not shown.

View File

@@ -1,45 +1,82 @@
#include "SAMDTimerInterrupt.h" #include "SAMDTimerInterrupt.h"
#include "SAMD_ISR_Timer.h" #include "SAMD_ISR_Timer.h"
#define HW_TIMER_INTERVAL_MS 10 SAMDTimer TaskTimer(TIMER_TC3);
SAMDTimer ITimer(TIMER_TC3); SAMD_ISR_Timer TasksISRs;
SAMD_ISR_Timer ISR_Timer;
void TimerHandler(void) { ISR_Timer.run(); } enum {
TEMPERATURE = 0,
HUMIDITY,
PRESSURE,
SENSORS
};
const int COUNTDOWN = 300; // COUNTDOWN is effectively multiplied by METRONOME_INTERVAL
const int PERIOD_MS = 1000; const int COUNTDOWN = 600;
// INTERVALS and TIMESHARES given in [ms]
const uint TASK_HANDLER_INTERVAL = 20;
const uint METRONOME_INTERVAL = 1000;
const uint SENSOR_INTERVAL = 1000;
const uint SENSOR_TIMESHARE[SENSORS] = {90000, 20000, 10000};
// FREQUENCY in [Hz]
const uint BUZZER_FREQ = 300;
const uint BUTTON_PIN = 6; const uint BUTTON_PIN = 6;
const uint BUTTON_LED_PIN = 5; const uint BUTTON_LED_PIN = 5;
const uint BUZZER_PIN = 0; const uint BUZZER_PIN = 0;
/* Metronome state is expressed by countdown: /* Metronome state is expressed by countdown:
* -1 - IDLE -1 - IDLE
* 0 - BEATING 0 - BEATING
* >0 - COUNTDOWN >0 - COUNTDOWN
*/ Unless metronome is in COUNTDOWN mode, display rotates through sensor readings.
*/
volatile int countdown = -1; volatile int countdown = -1;
// Units: temperature [C], humidity [%], pressure [P]
volatile float sensorValue[SENSORS] = {};
volatile uint displaySensor = TEMPERATURE;
volatile bool displayNeedsUpdate = false;
volatile bool tubeRotated = false;
int tubeTimerID;
void TasksHandler(void) {
TasksISRs.run();
if (displayNeedsUpdate || tubeRotated)
updateTube();
if (tubeRotated) {
TasksISRs.changeInterval(tubeTimerID, SENSOR_TIMESHARE[displaySensor]);
tubeRotated = false;
}
}
void setup() { void setup() {
initButton(); initButton();
initBME280();
initBuzzer();
initTube();
ITimer.attachInterruptInterval_MS(10, TimerHandler); readSensors();
ISR_Timer.setInterval(PERIOD_MS, metronomeBeat); updateTube();
ISR_Timer.setInterval(PERIOD_MS, metronomeCountdown);
TaskTimer.attachInterruptInterval_MS(TASK_HANDLER_INTERVAL, TasksHandler);
TasksISRs.setInterval(METRONOME_INTERVAL, runMetronome);
TasksISRs.setInterval(SENSOR_INTERVAL, readSensors);
tubeTimerID = TasksISRs.setInterval(SENSOR_TIMESHARE[displaySensor], rotateTube);
} }
void metronomeCountdown() { void runMetronome() {
if (countdown > 0) { if (countdown > 0) {
countdown -= 1; countdown -= 1;
digitalWrite(BUTTON_LED_PIN, countdown % 2 ? HIGH : LOW); digitalWrite(BUTTON_LED_PIN, countdown % 2 ? HIGH : LOW);
displayNeedsUpdate = true;
} else if (countdown == 0) {
TasksISRs.setTimeout(100, noBuzz);
buzz();
} }
} }
void metronomeBeat() {
if (countdown == 0)
tone(BUZZER_PIN, 1000, 100);
}
void loop() {} void loop() {}

51
tube.ino Normal file
View File

@@ -0,0 +1,51 @@
#include "grove_alphanumeric_display.h"
Seeed_Digital_Tube tube;
char tubeText[5] = "";
void initTube() {
// Wire initialized by BME280 sensor
tube.setTubeType(TYPE_4, TYPE_4_DEFAULT_I2C_ADDR);
tube.setBrightness(15);
tube.setBlinkRate(BLINK_OFF);
}
void rotateTube() {
displaySensor = (displaySensor + 1) % SENSORS;
tubeRotated = true;
}
void updateTube() {
char newText[5];
bool highPoint = false, lowPoint = false;
uint value = countdown;
if (countdown <= 0) {
switch (displaySensor) {
case TEMPERATURE:
value = (uint) (sensorValue[TEMPERATURE] * 100);
lowPoint = true;
break;
case HUMIDITY:
// TODO: add % sign?
value = (uint) (sensorValue[HUMIDITY]);
break;
case PRESSURE:
value = (uint) (sensorValue[PRESSURE] / 100);
break;
}
}
sprintf(newText, "%4u", value);
// TODO: In the current mode of display refresh (displayNeedsUpdate) it should not be necessary to check previous text
// Maybe add LED switching for case when the strings match as a rough check before removing comparison?
// Or LED switching when display is updated?
if (strcmp(tubeText, newText)) {
strcpy(tubeText, newText);
tube.displayString(tubeText);
tube.setPoint(highPoint, lowPoint);
tube.display();
}
displayNeedsUpdate = false;
}