First version - rotary not working.
This commit is contained in:
5
.gitignore
vendored
Normal file
5
.gitignore
vendored
Normal file
@@ -0,0 +1,5 @@
|
||||
.pio
|
||||
.vscode/.browse.c_cpp.db*
|
||||
.vscode/c_cpp_properties.json
|
||||
.vscode/launch.json
|
||||
.vscode/ipch
|
||||
10
.vscode/extensions.json
vendored
Normal file
10
.vscode/extensions.json
vendored
Normal file
@@ -0,0 +1,10 @@
|
||||
{
|
||||
// See http://go.microsoft.com/fwlink/?LinkId=827846
|
||||
// for the documentation about the extensions.json format
|
||||
"recommendations": [
|
||||
"platformio.platformio-ide"
|
||||
],
|
||||
"unwantedRecommendations": [
|
||||
"ms-vscode.cpptools-extension-pack"
|
||||
]
|
||||
}
|
||||
4
AGENTS.md
Normal file
4
AGENTS.md
Normal file
@@ -0,0 +1,4 @@
|
||||
- Prefer simple solutions over clever ones
|
||||
- Do not introduce new dependencies without reason
|
||||
- Always write tests for new features
|
||||
- Adapt SPEC.md if specification changes or features are added or removed
|
||||
27
SPEC.md
Normal file
27
SPEC.md
Normal file
@@ -0,0 +1,27 @@
|
||||
# Tasmota remote pulse timer trigger
|
||||
Das Programm für einen ESP8266 soll den PulseTimer einer Tasmota Steckdose triggern.
|
||||
Die Hardware hat einen Rotary Encoder sowie ein OLED Display.
|
||||
Mit dem Rotary Encoder (RT) stellt man eine Zeit im Bereich von 2s bis 10s mit einer Auflösung von 100ms.
|
||||
Über den RT soll der Wert pro Tick um 100ms angepasst werden können.
|
||||
Mit Druck auf den Taster des RT soll zunächst der PulseTimer eingestellt und dann getriggert werden.
|
||||
Die Einstellungen für die SSID und das Passwort sollen aus einer separaten config.h Datei ausgelesen werden.
|
||||
Dort soll auch die IP Adresse der Tasmota Steckdose eingestellt werden. Zudem dort noch den Initialwert für den Timer hinterlegen.
|
||||
Der Project soll mittels PlatformIO und Arduino Framework umgesetzt werden. Das SSD1309 OLED wird mit der Arduino-Library U8g2 angesteuert.
|
||||
|
||||
## Umsetzung
|
||||
Die Tasmota Steckdose wird per HTTP angesprochen. Beim Druck auf den Taster sendet das Programm:
|
||||
1. `PulseTime1 <wert>` mit `<wert>` in 100ms-Schritten. Der erlaubte Bereich 2s bis 10s entspricht damit `20` bis `100`.
|
||||
2. `Power1 ON`, um den zuvor gesetzten PulseTimer auszulösen.
|
||||
|
||||
Die Datei `include/config.h` enthält:
|
||||
- `WIFI_SSID`
|
||||
- `WIFI_PASSWORD`
|
||||
- `TASMOTA_HOST`
|
||||
- `INITIAL_TIMER_MS`
|
||||
|
||||
Als Vorlage liegt `include/config.example.h` bei.
|
||||
|
||||
## Hardware
|
||||
Board D1 Mini mit ESP8266.
|
||||
OLED über I2C an den GPIOs 4 und 5. Rotary Encoder mit A und B an GPIO 14 und 16 und der Taster an 12.
|
||||
OLED hat einen SSD1309 Controller mit 128x64 Pixel.
|
||||
15
include/PulseTimer.h
Normal file
15
include/PulseTimer.h
Normal file
@@ -0,0 +1,15 @@
|
||||
#pragma once
|
||||
|
||||
#include <stdint.h>
|
||||
|
||||
namespace trbc {
|
||||
|
||||
constexpr uint16_t kMinTimerMs = 2000;
|
||||
constexpr uint16_t kMaxTimerMs = 10000;
|
||||
constexpr uint16_t kTimerStepMs = 100;
|
||||
|
||||
uint16_t clampTimerMs(int valueMs);
|
||||
uint16_t adjustTimerMs(uint16_t currentMs, int ticks);
|
||||
uint16_t tasmotaPulseTimeValue(uint16_t timerMs);
|
||||
|
||||
} // namespace trbc
|
||||
8
include/config.example.h
Normal file
8
include/config.example.h
Normal file
@@ -0,0 +1,8 @@
|
||||
#pragma once
|
||||
|
||||
// Copy this file to include/config.h and adjust the values for your network.
|
||||
|
||||
#define WIFI_SSID "your-wifi"
|
||||
#define WIFI_PASSWORD "your-password"
|
||||
#define TASMOTA_HOST "192.168.1.50"
|
||||
#define INITIAL_TIMER_MS 5000
|
||||
9
include/config.h
Normal file
9
include/config.h
Normal file
@@ -0,0 +1,9 @@
|
||||
#pragma once
|
||||
|
||||
// Local defaults. Adjust for your network or keep this file out of version control
|
||||
// if the project is moved into a shared repository.
|
||||
|
||||
#define WIFI_SSID "Sternenlabor"
|
||||
#define WIFI_PASSWORD "!Sternenlabor99!"
|
||||
#define TASMOTA_HOST "192.168.240.101"
|
||||
#define INITIAL_TIMER_MS 5000
|
||||
16
platformio.ini
Normal file
16
platformio.ini
Normal file
@@ -0,0 +1,16 @@
|
||||
[platformio]
|
||||
default_envs = d1_mini
|
||||
|
||||
[env:d1_mini]
|
||||
platform = espressif8266
|
||||
board = d1_mini
|
||||
framework = arduino
|
||||
monitor_speed = 115200
|
||||
lib_deps =
|
||||
olikraus/U8g2 @ ^2.36.12
|
||||
|
||||
[env:native]
|
||||
platform = native
|
||||
test_filter = test_pulse_timer
|
||||
test_build_src = yes
|
||||
build_src_filter = +<PulseTimer.cpp> -<main.cpp>
|
||||
24
src/PulseTimer.cpp
Normal file
24
src/PulseTimer.cpp
Normal file
@@ -0,0 +1,24 @@
|
||||
#include "PulseTimer.h"
|
||||
|
||||
namespace trbc {
|
||||
|
||||
uint16_t clampTimerMs(int valueMs) {
|
||||
if (valueMs < kMinTimerMs) {
|
||||
return kMinTimerMs;
|
||||
}
|
||||
if (valueMs > kMaxTimerMs) {
|
||||
return kMaxTimerMs;
|
||||
}
|
||||
return static_cast<uint16_t>(valueMs);
|
||||
}
|
||||
|
||||
uint16_t adjustTimerMs(uint16_t currentMs, int ticks) {
|
||||
const int adjusted = static_cast<int>(currentMs) + ticks * kTimerStepMs;
|
||||
return clampTimerMs(adjusted);
|
||||
}
|
||||
|
||||
uint16_t tasmotaPulseTimeValue(uint16_t timerMs) {
|
||||
return clampTimerMs(timerMs) / kTimerStepMs;
|
||||
}
|
||||
|
||||
} // namespace trbc
|
||||
169
src/main.cpp
Normal file
169
src/main.cpp
Normal file
@@ -0,0 +1,169 @@
|
||||
#include <Arduino.h>
|
||||
#include <ESP8266HTTPClient.h>
|
||||
#include <ESP8266WiFi.h>
|
||||
#include <U8g2lib.h>
|
||||
|
||||
#include "PulseTimer.h"
|
||||
#include "config.h"
|
||||
|
||||
namespace {
|
||||
|
||||
constexpr uint8_t kOledAddress = 0x3C;
|
||||
constexpr uint8_t kPinEncoderA = 14;
|
||||
constexpr uint8_t kPinEncoderB = 16;
|
||||
constexpr uint8_t kPinButton = 12;
|
||||
constexpr uint8_t kPinSda = 4;
|
||||
constexpr uint8_t kPinScl = 5;
|
||||
|
||||
constexpr unsigned long kDisplayRefreshMs = 150;
|
||||
constexpr unsigned long kWifiRetryMs = 10000;
|
||||
constexpr unsigned long kButtonDebounceMs = 35;
|
||||
constexpr unsigned long kStatusHoldMs = 2500;
|
||||
|
||||
uint16_t timerMs = trbc::clampTimerMs(INITIAL_TIMER_MS);
|
||||
bool needsDisplayUpdate = true;
|
||||
unsigned long lastDisplayRefresh = 0;
|
||||
unsigned long lastWifiAttempt = 0;
|
||||
unsigned long statusUntil = 0;
|
||||
String statusLine = "Bereit";
|
||||
|
||||
uint8_t lastEncoderState = 0;
|
||||
int8_t encoderAccumulator = 0;
|
||||
bool lastButtonReading = HIGH;
|
||||
bool stableButtonState = HIGH;
|
||||
unsigned long lastButtonChange = 0;
|
||||
|
||||
U8G2_SSD1309_128X64_NONAME0_F_HW_I2C display(U8G2_R0, U8X8_PIN_NONE, kPinScl, kPinSda);
|
||||
|
||||
void setStatus(const String &text) {
|
||||
statusLine = text;
|
||||
statusUntil = millis() + kStatusHoldMs;
|
||||
needsDisplayUpdate = true;
|
||||
}
|
||||
|
||||
String formatTimer() {
|
||||
const uint16_t seconds = timerMs / 1000;
|
||||
const uint16_t tenths = (timerMs % 1000) / 100;
|
||||
return String(seconds) + "." + String(tenths) + " S";
|
||||
}
|
||||
|
||||
String wifiStatus() {
|
||||
if (WiFi.status() == WL_CONNECTED) {
|
||||
return String("IP ") + WiFi.localIP().toString();
|
||||
}
|
||||
return "WLAN verbindet";
|
||||
}
|
||||
|
||||
void refreshDisplay() {
|
||||
display.clearBuffer();
|
||||
display.setFont(u8g2_font_6x10_tf);
|
||||
display.drawStr(0, 10, "TASMOTA PULSE");
|
||||
display.drawStr(0, 26, ("ZEIT " + formatTimer()).c_str());
|
||||
display.drawStr(0, 42, wifiStatus().c_str());
|
||||
display.drawStr(0, 58, (millis() < statusUntil ? statusLine : "DRUECKEN START").c_str());
|
||||
display.sendBuffer();
|
||||
}
|
||||
|
||||
void connectWifi() {
|
||||
if (WiFi.status() == WL_CONNECTED) {
|
||||
return;
|
||||
}
|
||||
if (millis() - lastWifiAttempt < kWifiRetryMs && lastWifiAttempt != 0) {
|
||||
return;
|
||||
}
|
||||
lastWifiAttempt = millis();
|
||||
WiFi.mode(WIFI_STA);
|
||||
WiFi.begin(WIFI_SSID, WIFI_PASSWORD);
|
||||
needsDisplayUpdate = true;
|
||||
}
|
||||
|
||||
bool sendTasmotaCommand(const String &command) {
|
||||
if (WiFi.status() != WL_CONNECTED) {
|
||||
return false;
|
||||
}
|
||||
|
||||
WiFiClient client;
|
||||
HTTPClient http;
|
||||
const String url = String("http://") + TASMOTA_HOST + "/cm?cmnd=" + command;
|
||||
if (!http.begin(client, url)) {
|
||||
return false;
|
||||
}
|
||||
const int code = http.GET();
|
||||
http.end();
|
||||
return code >= 200 && code < 300;
|
||||
}
|
||||
|
||||
void triggerPulse() {
|
||||
setStatus("SENDE...");
|
||||
const uint16_t pulseValue = trbc::tasmotaPulseTimeValue(timerMs);
|
||||
const bool pulseOk = sendTasmotaCommand("PulseTime1%20" + String(pulseValue));
|
||||
const bool powerOk = pulseOk && sendTasmotaCommand("Power1%20ON");
|
||||
setStatus(powerOk ? "GETRIGGERT" : "FEHLER");
|
||||
}
|
||||
|
||||
void readEncoder() {
|
||||
const uint8_t state = (digitalRead(kPinEncoderA) << 1) | digitalRead(kPinEncoderB);
|
||||
if (state == lastEncoderState) {
|
||||
return;
|
||||
}
|
||||
|
||||
const uint8_t transition = (lastEncoderState << 2) | state;
|
||||
if (transition == 0b0001 || transition == 0b0111 || transition == 0b1110 || transition == 0b1000) {
|
||||
encoderAccumulator++;
|
||||
} else if (transition == 0b0010 || transition == 0b1011 || transition == 0b1101 || transition == 0b0100) {
|
||||
encoderAccumulator--;
|
||||
}
|
||||
lastEncoderState = state;
|
||||
|
||||
if (encoderAccumulator >= 4 || encoderAccumulator <= -4) {
|
||||
const int ticks = encoderAccumulator / 4;
|
||||
encoderAccumulator = 0;
|
||||
timerMs = trbc::adjustTimerMs(timerMs, ticks);
|
||||
needsDisplayUpdate = true;
|
||||
}
|
||||
}
|
||||
|
||||
void readButton() {
|
||||
const bool reading = digitalRead(kPinButton);
|
||||
if (reading != lastButtonReading) {
|
||||
lastButtonChange = millis();
|
||||
lastButtonReading = reading;
|
||||
}
|
||||
if (millis() - lastButtonChange <= kButtonDebounceMs || reading == stableButtonState) {
|
||||
return;
|
||||
}
|
||||
|
||||
stableButtonState = reading;
|
||||
if (stableButtonState == LOW) {
|
||||
triggerPulse();
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
void setup() {
|
||||
Serial.begin(115200);
|
||||
pinMode(kPinEncoderA, INPUT_PULLUP);
|
||||
pinMode(kPinEncoderB, INPUT_PULLUP);
|
||||
pinMode(kPinButton, INPUT_PULLUP);
|
||||
|
||||
lastEncoderState = (digitalRead(kPinEncoderA) << 1) | digitalRead(kPinEncoderB);
|
||||
|
||||
display.setI2CAddress(kOledAddress << 1);
|
||||
display.begin();
|
||||
connectWifi();
|
||||
refreshDisplay();
|
||||
}
|
||||
|
||||
void loop() {
|
||||
connectWifi();
|
||||
readEncoder();
|
||||
readButton();
|
||||
|
||||
if (needsDisplayUpdate || millis() - lastDisplayRefresh >= kDisplayRefreshMs) {
|
||||
refreshDisplay();
|
||||
lastDisplayRefresh = millis();
|
||||
needsDisplayUpdate = false;
|
||||
}
|
||||
delay(2);
|
||||
}
|
||||
34
test/test_pulse_timer/test_main.cpp
Normal file
34
test/test_pulse_timer/test_main.cpp
Normal file
@@ -0,0 +1,34 @@
|
||||
#include <unity.h>
|
||||
|
||||
#include "PulseTimer.h"
|
||||
|
||||
void test_clamps_timer_range() {
|
||||
TEST_ASSERT_EQUAL_UINT16(2000, trbc::clampTimerMs(1000));
|
||||
TEST_ASSERT_EQUAL_UINT16(5000, trbc::clampTimerMs(5000));
|
||||
TEST_ASSERT_EQUAL_UINT16(10000, trbc::clampTimerMs(12000));
|
||||
}
|
||||
|
||||
void test_adjusts_in_100_ms_steps() {
|
||||
TEST_ASSERT_EQUAL_UINT16(5100, trbc::adjustTimerMs(5000, 1));
|
||||
TEST_ASSERT_EQUAL_UINT16(4700, trbc::adjustTimerMs(5000, -3));
|
||||
}
|
||||
|
||||
void test_adjustment_stays_in_range() {
|
||||
TEST_ASSERT_EQUAL_UINT16(2000, trbc::adjustTimerMs(2000, -1));
|
||||
TEST_ASSERT_EQUAL_UINT16(10000, trbc::adjustTimerMs(10000, 1));
|
||||
}
|
||||
|
||||
void test_converts_to_tasmota_pulsetime_units() {
|
||||
TEST_ASSERT_EQUAL_UINT16(20, trbc::tasmotaPulseTimeValue(2000));
|
||||
TEST_ASSERT_EQUAL_UINT16(55, trbc::tasmotaPulseTimeValue(5500));
|
||||
TEST_ASSERT_EQUAL_UINT16(100, trbc::tasmotaPulseTimeValue(10000));
|
||||
}
|
||||
|
||||
int main() {
|
||||
UNITY_BEGIN();
|
||||
RUN_TEST(test_clamps_timer_range);
|
||||
RUN_TEST(test_adjusts_in_100_ms_steps);
|
||||
RUN_TEST(test_adjustment_stays_in_range);
|
||||
RUN_TEST(test_converts_to_tasmota_pulsetime_units);
|
||||
return UNITY_END();
|
||||
}
|
||||
Reference in New Issue
Block a user