Initial Setup

This commit is contained in:
XPS\Micro 2026-01-25 20:44:07 +01:00
commit 602e7f46f3
42 changed files with 11267 additions and 0 deletions

View File

@ -0,0 +1,7 @@
{
"permissions": {
"allow": [
"Bash(python html_to_header.py:*)"
]
}
}

66
.gitignore vendored Normal file
View File

@ -0,0 +1,66 @@
.pio
.vscode/.browse.c_cpp.db*
.vscode/c_cpp_properties.json
.vscode/launch.json
.vscode/ipch
.pio
.vscode/.browse.c_cpp.db*
.vscode/c_cpp_properties.json
.vscode/launch.json
.vscode/ipch
# Build-Artefakte und kompilierte Dateien
*.elf
*.bin
*.hex
*.map
*.o
*.a
*.zip
# Eagle Backup-Dateien
*.s#?
*.b#?
*.l#?
# PlatformIO
.pio/
.pioenvs/
.piolibdeps/
firmware/build/
# KiCad temporaere Dateien
*.000
*.bak
*.bck
*-bak
*-cache.lib
*.kicad_pcb-bak
*.kicad_sch-bak
fp-info-cache
*-save.pro
*-save.kicad_pcb
# Generierte Fertigungsdateien (optional, je nach Workflow)
hardware/gerber/*.gbr
hardware/gerber/*.drl
# CAD temporaere Dateien
*.stl.autosave
*.step.autosave
# Halte .gitkeep Dateien
!.gitkeep
# IDE-Einstellungen
.vscode/
.idea/
*.code-workspace
# Betriebssystem-Dateien
.DS_Store
Thumbs.db
desktop.ini
CLAUDE.md

1
doc/.gitkeep Normal file
View File

@ -0,0 +1 @@
# Dieser Ordner ist Teil der Projektstruktur

2624
html/index.html Normal file

File diff suppressed because it is too large Load Diff

64
html_to_header.py Normal file
View File

@ -0,0 +1,64 @@
import os
import gzip
html_path = "html/index.html"
header_path = "include/html.h"
# HTML-Datei lesen
with open(html_path, 'r', encoding='utf-8') as f:
html_content = f.read()
# HTML-Inhalt mit GZIP komprimieren
html_bytes = html_content.encode('utf-8')
compressed = gzip.compress(html_bytes, compresslevel=9)
# Original- und komprimierte Größe ausgeben
original_size = len(html_bytes)
compressed_size = len(compressed)
ratio = (1 - compressed_size / original_size) * 100
print(f"Original-Größe: {original_size:6d} Bytes")
print(f"Komprimierte Größe: {compressed_size:6d} Bytes")
print(f"Kompression: {ratio:5.1f}%")
# Header-Datei erstellen mit komprimierten Daten
header_content = f"""// Auto-generiert von html_to_header.py
// Original-Größe: {original_size} Bytes
// Komprimierte Größe: {compressed_size} Bytes
// Kompression: {ratio:.1f}%
#ifndef HTML_H
#define HTML_H
#include <Arduino.h>
// GZIP-komprimierte HTML-Seite
const uint8_t HTML_PAGE_GZIP[] PROGMEM = {{
"""
# Komprimierte Bytes als Hex-Array schreiben
for i, byte in enumerate(compressed):
if i % 16 == 0:
header_content += " "
header_content += f"0x{byte:02x}"
if i < len(compressed) - 1:
header_content += ","
if (i + 1) % 16 == 0:
header_content += "\n"
else:
header_content += " "
header_content += f"""
}};
const size_t HTML_PAGE_GZIP_LEN = {compressed_size};
#endif // HTML_H
"""
# Header-Datei schreiben
os.makedirs(os.path.dirname(header_path), exist_ok=True)
with open(header_path, 'w', encoding='utf-8') as f:
f.write(header_content)
print(f"\nHeader-Datei erstellt: {header_path}")

35
include/charPattern.inc Normal file
View File

@ -0,0 +1,35 @@
#ifndef CHARPATTERN_H_
#define CHARPATTERN_H_
//Hier Umlaute wie Ö und Ü bereits durch o und u wegen UTF8 ersetzen!!!!
const char DEFAULT_CHARSOAP[] = "ESxISTxFASTBALDKURZEHNFuNFZEITVORDREIVIERTELNACHRWDHALBxSECHSVIERxELFZWoLFuNFZEHNEUNACHTDREINSIEBENxZWEIxxUHRx";
char testPattern[]="ES-IST-WIR-HABEN-BALD-FAST-KURZ-FuNF-ZEHN-DREIVIERTEL-VIERTEL-ZWANZIG-VOR-NACH-EIN-EINS-ZWEI-DREI-VIER-FuNF-SECHS-SIEBEN-ACHT-NEUN-ZEHN-ELF-ZWoLF-UHR-NACHT-PAUSE-ALARM-ZEIT-RWD-KKS-KARLKUEBEL-MINT\0";
/*
"ES_IST_ZEHNFuNFVIERTELVORNACHBALDHALB_ZWEINS___________ZEHNEUNACHTELFuNFZWoLFSECHSIEBEN__DREIVIER____PAUSE_UHR"
"ES_IST_ZEHNFuNFVIERTEL_KURZ_FAST_VORNACHBALDHALB_SECHS__ZWEIVIER__ZEHNEUNACHTDREINSIEBENZWoLFELFuNF___ZEIT_UHR"
"ES_IST_FASTFuNFVIERTELZEHNBALDVORNACH___HALB_VIER______ELFZWoLFuNF_SECHSIEBENZEHNEUNACHT_ZWEINSDREI________UHR"
"ES_IST_BALDKURZ_FAST___FuNFZEHN__VIERTEL_VORNACH__HALB__SECHSIEBENZEHNEUNACHT_ZWEINSDREIVIERELFuNF__ZWoLF__UHR"
"ES_IST_BALDKURZ_FAST___FuNFZEHN__VIERTEL_VORNACH__HALB__SECHSIEBENZEHNEUNACHT_ZWEINSDREIELFuNFVIER_ZWoLFRWDUHR"
"ES_IST_FASTBALDKURZEHNFuNFVIERTELVORNACH___________HALBDREINSZWEI_ELFVIERFuNFZEHNEUNACHT_SECHSIEBENZWoLFRWDUHR"
"ES_IST_FASTBALDKURZEHNFuNFVIERTELVORNACHZEIT_______HALBDREINSZWEI_ELFVIERFuNFZEHNEUNACHT_SECHSIEBENZWoLFRWDUHR"
"ES_IST_FASTBALDKURZEHNFuNFZEITVORDREIVIERTELNACHRWDHALB_SECHSVIER_ELFZWoLFuNFZEHNEUNACHTDREINSIEBEN_ZWEI__UHR_"
"ES_IST_FASTFuNFKURZEHNVIERTEL_VORNACH_HALB______________ZWEIDREINSZEHNEUNACHTELFVIERFuNF_SECHSIEBENZWoLFRWDUHR"
"ES_IST_KURZBALDFuNFASTZEHNVIERTELDREI_NACH___VOR__HALB_SIEBENSECHSFuNFVIERELF_DREIZWEINSZEHNEUNACHT_ZWoLF_UHR_"
"ES_IST_FASTFuNFKURZEHNDREIVIERTELVORNACHBALDHALBVIERTELSECHS_VIER_ELFuNFZWoLFZEHNEUNACHTDREINSIEBEN_ZWEI___UHR"
"ES_IST_FASTFuNFKURZEHNVORNACHBALDDREIVIERTELHALB_______SECHS_VIER_ELFuNFZWoLFZEHNEUNACHTDREINSIEBEN_ZWEI___UHR"
"ES_IST_FASTZEHNZWANZIGDREIVIERTELFuNFBALDVOR_NACH__HALBSECHS_VIER_ELFuNFZWoLFZEHNEUNACHTDREINSIEBEN_ZWEI___UHR"
"ES_IST_BALDFuNFZWANZIGDREIVIERTELZEHNKURZEITFASTNACHVOR_HALB_VIER_ZWEINSIEBENELFZWoLFuNFZEHNEUNACHT_SECHS_DREI"
"ES_IST_FAST_FuNFZEHN__BALDKURZEITVIERTEL_VOR_NACH_HALB_DREINSIEBENELFZWoLFuNFZEHNEUNACHT_VIERSECHS_RWDZWEI_UHR"
"WIR_HABEN__BALDZWANZIGKURZEHNFuNFDREIVIERTELFASTNACHVOR_HALB_FuNF_DREINSIEBENZEHNEUNACHTELFVIERZWEI_SECHSZWoLF"
"WIR_HABEN__BALD_FuNF__DREIVIERTELZEHNZWANZIGNACH_VOR____HALB_ZWEI_ZEHNEUNACHTDREINSIEBENZWoLFuNFELF_SECHS_VIER"
"_WIR_HABEN_KURZBALD____FuNFZEHN___VOR_NACH__HALB_ZWEINSVIERSECHS__ELFuNFZWoLFZEHNEUNACHTDREINSIEBENPAUSE___UHR"
"_WIR_HABEN_KURZBALD____FuNFZEHN__VIERTEL_VORNACH__HALB_DREINSIEBENZWoLFuNFELFVIERSECHS__ZEHNEUNACHT_ZWEI__UHR_"
"WIR_HABEN__BALDKURZEIT_FuNFZEHN__VIERTEL_VOR_NACH_HALB_DREINSIEBENELFZWoLFuNFZEHNEUNACHT_VIERSECHS_RWDZWEI_UHR"
"ES_IST_FuNFZEHNZWANZIGDREIVIERTELVOR____NACHHALB_ELFuNFEINS___ZWEIDREI___VIERSECHS__ACHTSIEBENZWoLFZEHNEUN_UHR"
"ESKISTAFuNFZEHNZWANZIGDREIVIERTELVORFUNKNACHHALBAELFuNFEINSXAMZWEIDREIPMJVIERSECHSNLACHTSIEBENZWoLFZEHNEUNKUHR"
*/
int MINUTE_LEDS[4] = {112, 114, 116, 118};
#endif

24
include/common.inc Normal file
View File

@ -0,0 +1,24 @@
#ifndef _COMMON_INC_
#define _COMMON_INC_
#include <Arduino.h>
#include <FastLED.h>
#include <ESP8266WiFi.h>
#include <ESP8266WebServer.h>
#include <DNSServer.h>
#include <EEPROM.h>
#include <Wire.h>
#include <RTClib.h>
#include <ESP8266httpUpdate.h>
#include <time.h> // NTP support
#include <user_interface.h> // Für rst_info
#include <extern.inc>
#include <defines.inc>
#include <version.inc>
#include <vars.inc>
#include <info.h>
#include <PowerOnDetector.h>
#include <rgbPanel.h>
#include <CharGraphTimeLogic.h>
#endif

109
include/defines.inc Normal file
View File

@ -0,0 +1,109 @@
#ifndef _DEFINES_INC_
#define _DEFINES_INC_
#ifdef BRIDGE_LEDS_POS77
#warning "BRIDGE_LEDS_POS77 is definded, so RGB-LED on pos 77 would bridged"
#endif
#if defined(DEBUG_SOURCE) && (DEBUG_SOURCE == true)
#warning "DEBUG_MODE is set to true, so Serial is active"
#define DEBUG_PRINT(x) {Serial.print(x);Serial.flush(); yield();}
#define DEBUG_PRINTLN(x) {Serial.println(x);Serial.flush(); yield();}
#define DEBUG_PRINTF(...) {Serial.printf(__VA_ARGS__);Serial.flush(); yield();}
#else
#define DEBUG_PRINT(x)
#define DEBUG_PRINTLN(x)
#define DEBUG_PRINTF(...)
#endif
#define I2C_SDA D2
#define I2C_SCL D1
// APA102/SK9822 auf D7 (DATA), D5 (CLK)
#define DATA_PIN2 D7
#define CLK_PIN2 D5
#define AP_SSID_LENGHT 13
#define AP_PASSWORD_LENGTH 17
#define MAXWORDS 3 // Anzahl Spezialwörter
// ════════════════════════════════════════════════════════════════
// EEPROM ADRESSEN
// ════════════════════════════════════════════════════════════════
#define EEPROM_SIZE 512
#define ADDR_BRIGHTNESS 0
#define ADDR_COLOR_R 1
#define ADDR_COLOR_G 2
#define ADDR_COLOR_B 3
#define ADDR_SPECIAL_R 4
#define ADDR_SPECIAL_G 5
#define ADDR_SPECIAL_B 6
#define ADDR_CONFIGURED 7
#define ADDR_TIMESTAMP 8
#define ADDR_SSID (ADDR_TIMESTAMP+sizeof(savedTime))
#define ADDR_PASSWORD (ADDR_SSID + AP_SSID_LENGHT)
#define ADDR_WIFI_SET (ADDR_PASSWORD + AP_PASSWORD_LENGTH)
#define ADDR_LAST_SYNC 40
#define ADDR_DRIFT_RATE 44
#define ADDR_SYNC_COUNT 48
#define ADDR_CHARSOAP 50
#define ADDR_CHARSOAP_SET 161
#define ADDR_BOOT_COUNTER 163 // Boot-Counter
#define ADDR_CLEAN_SHUTDOWN 164 // Clean-Shutdown-Flag
#define ADDR_RUNNING_FLAG 165 // "System läuft" Flag
// ════════════════════════════════════════════════════════════════
// OTA UPDATE ADRESSEN (346 bytes frei: 166-511)
// ════════════════════════════════════════════════════════════════
#define ADDR_OTA_VERSION 166 // Firmware-Version (32 bytes)
#define ADDR_OTA_BUILD_DATE 198 // Build-Datum/Zeit (20 bytes)
#define ADDR_OTA_FLAGS 218 // OTA-Status-Flags (1 byte)
#define OTA_FLAG_UPDATE_SUCCESS 0x01
#define OTA_FLAG_UPDATE_FAILED 0x02
// ════════════════════════════════════════════════════════════════
// WIFI STATION & NTP ADRESSEN (293 bytes frei: 219-511)
// ════════════════════════════════════════════════════════════════
#define ADDR_STA_SSID 219 // Station SSID (32 bytes)
#define ADDR_STA_PASSWORD 251 // Station Password (64 bytes)
#define ADDR_STA_ENABLED 315 // Station enabled flag (1 byte)
#define ADDR_STA_DHCP 316 // DHCP enabled (1 byte)
#define ADDR_STA_IP 317 // Static IP (4 bytes)
#define ADDR_STA_GATEWAY 321 // Gateway (4 bytes)
#define ADDR_STA_SUBNET 325 // Subnet mask (4 bytes)
#define ADDR_STA_DNS 329 // DNS server (4 bytes)
#define ADDR_NTP_SERVER 333 // NTP server hostname (32 bytes)
#define ADDR_NTP_LAST_SYNC 365 // Last NTP sync timestamp (4 bytes)
#define ADDR_NTP_ENABLED 369 // NTP enabled flag (1 byte)
// ════════════════════════════════════════════════════════════════
// AUTO-BRIGHTNESS ADRESSEN (140 bytes frei: 377-511)
// ════════════════════════════════════════════════════════════════
#define ADDR_AUTO_BRIGHTNESS_ENABLED 370 // Auto-Brightness aktiviert (1 byte)
#define ADDR_AUTO_BRIGHTNESS_MIN_ADC 371 // Min ADC-Wert (2 bytes uint16_t)
#define ADDR_AUTO_BRIGHTNESS_MAX_ADC 373 // Max ADC-Wert (2 bytes uint16_t)
#define ADDR_AUTO_BRIGHTNESS_MIN 375 // Min Helligkeit (1 byte uint8_t)
#define ADDR_AUTO_BRIGHTNESS_MAX 376 // Max Helligkeit (1 byte uint8_t)
// ════════════════════════════════════════════════════════════════
// SPECIAL WORD & MINUTE LEDS ADRESSEN (93 bytes frei: 377-511)
// ════════════════════════════════════════════════════════════════
#define ADDR_SPECIAL_WORD_1 377 // Erstes Spezialwort (12 bytes)
#define ADDR_SPECIAL_WORD_2 389 // Zweites Spezialwort (12 bytes)
#define ADDR_SPECIAL_WORD_3 401 // Drittes Spezialwort (12 bytes)
#define ADDR_MINUTE_LEDS 413 // 4 Minuten-LEDs Positionen (4 bytes)
#define ADDR_SPECIAL_WORD_SET 417 // Flag: Custom Special Words (1 byte)
#define ADDR_MINUTE_LEDS_SET 418 // Flag: Custom Minute LEDs (1 byte)
#define ADDR_SPECIAL_WORD_INTERVAL 419 // Anzeigeintervall in Minuten (1 byte)
#define STA_ENABLED_MAGIC 0xAA
#define NTP_ENABLED_MAGIC 0xBB
#define AUTO_BRIGHTNESS_MAGIC 0xCC
#define SPECIAL_WORD_MAGIC 0xDD
#define MINUTE_LEDS_MAGIC 0xEE
#define RUNNING_FLAG_MAGIC 0x42 // Magic Byte
#define MAGIC_BYTE_INIT 0xA5
#endif

9
include/extern.inc Normal file
View File

@ -0,0 +1,9 @@
#ifndef _EXTERN_INC_
#define _EXTERN_INC_
extern unsigned long savedTime;
extern void showLEDs();
extern uint16_t bootCounter;
//extern CRGB leds[];
//extern RTC_DS1307 rtc;
#endif

1332
include/html.h Normal file

File diff suppressed because it is too large Load Diff

92
include/vars.inc Normal file
View File

@ -0,0 +1,92 @@
#ifndef _VARS_INC_
#define _VARS_INC_
#include <rgbPanel.h>
// ════════════════════════════════════════════════════════════════
// GLOBALE VARIABLEN
// ════════════════════════════════════════════════════════════════
unsigned long bootTime = 0;
unsigned long lastSyncTime = 0;
float driftRate = 0.0;
int syncCount = 0;
int8_t isConfigured = 0xFF;
const unsigned long AP_TIMEOUT = 300000;
bool powerLossDetected = false; //Flag für Stromausfall
char AP_SSID[AP_SSID_LENGHT] = "CharGrap-01\0";
char AP_PASSWORD[AP_PASSWORD_LENGTH] = "MeinPasswort123\0";
uint16_t bootCounter = 0;
bool apActive = false;
unsigned long apStartTime = 0;
static int lastDisplayedMinute = -1;
unsigned long lastUpdateTime = 0;
const unsigned long updateInterval = 1000; // 1 Sekunde
// ════════════════════════════════════════════════════════════════
// OTA UPDATE VARIABLEN
// ════════════════════════════════════════════════════════════════
char firmwareVersion[32] = "";
bool otaInProgress = false;
int otaProgress = 0;
String otaError = "";
unsigned long otaStartTime = 0;
// ════════════════════════════════════════════════════════════════
// WIFI STATION VARIABLEN
// ════════════════════════════════════════════════════════════════
char staSsid[32] = "";
char staPassword[64] = "";
bool staEnabled = false;
bool staDhcp = true;
IPAddress staIP(0, 0, 0, 0);
IPAddress staGateway(0, 0, 0, 0);
IPAddress staSubnet(255, 255, 255, 0);
IPAddress staDNS(0, 0, 0, 0);
// ════════════════════════════════════════════════════════════════
// NTP VARIABLEN
// ════════════════════════════════════════════════════════════════
char ntpServer[32] = "ptbtime1.ptb.de";
unsigned long lastNtpSync = 0;
bool ntpEnabled = false;
bool ntpSyncSuccessful = false;
unsigned long nextNtpCheck = 0;
// ════════════════════════════════════════════════════════════════
// AUTO-BRIGHTNESS VARIABLEN
// ════════════════════════════════════════════════════════════════
bool autoBrightnessEnabled = false;
uint16_t autoBrightnessMinADC = 200; // Min ADC-Wert (ca. 0.2V bei 1024=1V)
uint16_t autoBrightnessMaxADC = 820; // Max ADC-Wert (ca. 0.8V bei 1024=1V)
uint8_t autoBrightnessMin = 0; // Minimale Helligkeit (bei wenig Licht)
uint8_t autoBrightnessMax = 80; // Maximale Helligkeit (bei viel Licht) - 80 = 100%
const uint8_t MAX_BRIGHTNESS = 80; // Maximum für Helligkeit (100%)
// ════════════════════════════════════════════════════════════════
// SPECIAL WORD VARIABLEN
// ════════════════════════════════════════════════════════════════
uint8_t specialWordInterval = 60; // Anzeigeintervall in Minuten (Default: jede Stunde)
// ════════════════════════════════════════════════════════════════
// PATTERN TEST VARIABLEN
// ════════════════════════════════════════════════════════════════
bool patternTestRunning = false; // Pattern-Test aktiv
// Intervall-Konstanten
#define INTERVAL_EVERY_MINUTE 1
#define INTERVAL_EVERY_5_MINUTES 5
#define INTERVAL_EVERY_10_MINUTES 10
#define INTERVAL_EVERY_15_MINUTES 15
#define INTERVAL_EVERY_30_MINUTES 30
#define INTERVAL_EVERY_60_MINUTES 60
//#include <html.inc>
#include <html.h>
#endif

33
include/version.inc Normal file
View File

@ -0,0 +1,33 @@
#ifndef _VERSION_INC_
#define _VERSION_INC_
// Auto-generated version from build date/time
#define BUILD_DATE __DATE__
#define BUILD_TIME __TIME__
// Convert build date/time to version string format: YYYYMMDD-HHMM
// Example: 20250121-1430
inline void generateVersion(char* buffer, size_t bufferSize) {
const char* months[] = {"Jan", "Feb", "Mar", "Apr", "May", "Jun",
"Jul", "Aug", "Sep", "Oct", "Nov", "Dec"};
char month[4], day[3], year[5], time[9];
sscanf(BUILD_DATE, "%s %s %s", month, day, year);
strcpy(time, BUILD_TIME);
int monthNum = 1;
for (int i = 0; i < 12; i++) {
if (strcmp(month, months[i]) == 0) {
monthNum = i + 1;
break;
}
}
int h, m, s;
sscanf(time, "%d:%d:%d", &h, &m, &s);
snprintf(buffer, bufferSize, "%s%02d%02d-%02d%02d",
year, monthNum, atoi(day), h, m);
}
#endif

View File

@ -0,0 +1,8 @@
{
"permissions": {
"allow": [
"Bash(python -m py_compile:*)",
"Bash(node -c:*)"
]
}
}

View File

@ -0,0 +1,186 @@
# CharGraph TimeLogic - Black Box Test Suite
Automated testing framework for the CharGraphTimeLogic library.
## Overview
The Black Box Test Suite provides a standardized way to test the CharGraph time-to-words conversion logic across multiple patterns and time windows.
## Files
### blackboxtest.ino
Main test application. Loads patterns from `config.h`, runs tests, and outputs results to serial.
**Features:**
- Tests multiple patterns sequentially
- Configurable time window and minute increments
- Table-formatted output compatible with markdown
- Test statistics and pass/fail summary
### config.h
Configuration file for test parameters.
**Parameters:**
```cpp
// Test patterns (each must be exactly 110 characters)
const char testpatterns[][111] = {
"PATTERN1_110_CHARS",
"PATTERN2_110_CHARS",
// Add more patterns here
};
// Number of patterns to test
const uint8_t numPatterns = 2;
// Time window
uint8_t hourStart = 23; // Start hour (0-23)
uint8_t hourEnd = 0; // End hour (can wrap around midnight)
uint8_t minuteStart = 23; // Start minute (0-59)
uint8_t minuteEnd = 27; // End minute (0-59)
uint8_t hops = 1; // Minute increments (1=every min, 5=every 5 min, etc.)
```
## Usage
### Setup
1. Place `blackboxtest.ino` and `config.h` in your Arduino project folder
2. Ensure CharGraphTimeLogic library is properly installed
3. Modify patterns in `config.h` as needed
### Configuration
#### Adding Test Patterns
In `config.h`:
```cpp
const char testpatterns[][111] = {
"ESGISTWZEHNFÜNFVIERTELVORNACHBALDHALBKZWEINSYWAYOLOVDCQZEHNEUNACHTELFÜNFZWÖLFSECHSIEBENGEDREIVIERTMDCPAUSEUHRW", // Pattern 1
"ESUISTKFASTBALDKURZEHNFÜNFZEITVORDREIVIERTELNACHRWDHALBHSECHSVIERKELFZWÖLFÜNFZEHNEUNACHTDREINSIEBENMZWEIEHUHRQ", // Pattern 2
// "PATTERN3...",
};
const uint8_t numPatterns = 2;
```
#### Changing Time Window
Default (Standard Black Box Test):
```cpp
uint8_t hourStart = 23; // 23:23
uint8_t hourEnd = 0; // to 00:27
uint8_t minuteStart = 23;
uint8_t minuteEnd = 27;
uint8_t hops = 1; // Every minute
```
For testing just 1 hour:
```cpp
uint8_t hourStart = 14;
uint8_t hourEnd = 14;
uint8_t minuteStart = 0;
uint8_t minuteEnd = 59;
uint8_t hops = 1;
```
For sparse sampling (every 5 minutes):
```cpp
uint8_t hops = 5;
```
### Running Tests
1. Connect Arduino to computer via USB
2. Open Arduino IDE
3. Load `blackboxtest.ino`
4. Select correct board and port
5. Upload sketch
6. Open Serial Monitor (baud rate: 115200)
7. Tests will run automatically and output results
### Output Format
```
========================================
Pattern 1
========================================
Pattern: ESGISTWZEHNFÜNF...
Uhrzeit | Text | MinutenLeds | Links | Rechts
--------|------|-------------|-------|-------
23:23 | ES IST ZEHN VOR HALB ZWÖLF | *** | L | R
23:24 | ES IST ZEHN VOR HALB ZWÖLF | **** | L | R
...
========================================
TEST SUMMARY
========================================
Total Tests: 130
Passed: 130
Failed: 0
✓ ALL TESTS PASSED
```
## Test Statistics
After each pattern, a summary is printed:
- **Total Tests**: Number of minutes tested
- **Passed**: Successful word generation
- **Failed**: Errors or invalid results
## LED Output Format
In the serial output:
- `*` = One LED
- `****` = Four LEDs
- `L` = LED direction LEFT (additive)
- `R` = LED direction RIGHT (subtractive)
## Standard Time Windows
### Black Box Test (Default)
- **Start:** 23:23
- **End:** 00:27
- **Duration:** 65 minutes
- **Reason:** Tests full hour transition and critical minutes
### Full Hour Test
- **Start:** HH:00
- **End:** HH:59
- **Duration:** 60 minutes
### Sparse Test (Debug)
- **Start:** 23:23
- **End:** 00:27
- **Hops:** 5 minutes
- **Duration:** 13 tests (instead of 65)
## Troubleshooting
### Serial Output Garbled
- Check baud rate: Should be 115200
- Verify USB cable connection
- Try different USB port
### Pattern Not Found
- Verify pattern length is exactly 110 characters
- Check for accented characters (ä, ö, ü)
- Ensure pattern is uppercase
### Memory Issues
- Reduce number of patterns in config.h
- Increase hops value (test fewer minutes)
- Test on Arduino with more SRAM (Mega)
## Design Goals
✓ Simple, intuitive configuration
✓ Consistent time window (23:23-00:27 by default)
✓ Markdown-compatible output
✓ Deterministic, repeatable results
✓ Works on standard Arduino boards
## License
Part of CharGraph TimeLogic library. See main LICENSE for details.

View File

@ -0,0 +1,285 @@
/**
* CharGraph TimeLogic - Black Box Test
*
* Automated test suite for CharGraphTimeLogic library
* Tests multiple patterns across a configurable time window
*/
#include "config.h"
#include "../src/CharGraphTimeLogic.h"
// ============================================================================
// GLOBAL TEST STATISTICS
// ============================================================================
struct TestStats {
uint32_t totalTests;
uint32_t passedTests;
uint32_t failedTests;
};
TestStats stats = {0, 0, 0};
// ============================================================================
// SETUP AND INITIALIZATION
// ============================================================================
void setup() {
Serial.begin(SERIAL_BAUD);
delay(1000);
Serial.println("\n\n");
Serial.println("========================================");
Serial.println("CharGraph TimeLogic - Black Box Test");
Serial.println("========================================\n");
Serial.print("Test Patterns: ");
Serial.println(numPatterns);
Serial.print("Time Window: ");
Serial.print(hourStart);
Serial.print(":");
if (minuteStart < 10) Serial.print("0");
Serial.print(minuteStart);
Serial.print(" to ");
Serial.print(hourEnd);
Serial.print(":");
if (minuteEnd < 10) Serial.print("0");
Serial.print(minuteEnd);
Serial.println();
Serial.print("Minute Hops: ");
Serial.println(hops);
Serial.println();
// Run all tests
for (uint8_t patternIdx = 0; patternIdx < numPatterns; patternIdx++) {
runPatternTest(patternIdx, testpatterns[patternIdx]);
}
// Print summary
printTestSummary();
Serial.println("\nTest completed. Ready for next test.\n");
}
void loop() {
// Loop does nothing - tests run in setup
delay(1000);
}
// ============================================================================
// MAIN TEST RUNNER
// ============================================================================
void runPatternTest(uint8_t patternIdx, const char* pattern) {
Serial.println("========================================");
Serial.print("Pattern ");
Serial.print(patternIdx + 1);
Serial.println();
Serial.println("========================================");
Serial.print("Pattern: ");
Serial.println(pattern);
// Verify pattern length
if (strlen(pattern) != GRID_SIZE) {
Serial.print("ERROR: Pattern length ");
Serial.print(strlen(pattern));
Serial.print(" != ");
Serial.println(GRID_SIZE);
return;
}
Serial.println("\nUhrzeit | Text | MinutenLeds | Links | Rechts");
Serial.println("--------|------|-------------|-------|-------");
// Calculate total test count
uint16_t testCount = calculateTestCount();
// Iterate through time window
uint8_t hour = hourStart;
uint8_t minute = minuteStart;
uint16_t count = 0;
while (count < testCount) {
// Get words for this time
CharGraphTimeWords result;
bool success = getCharGraphWords(pattern, hour, minute, result);
// Print result line
printResultLine(hour, minute, result, success);
// Update statistics
stats.totalTests++;
if (success) {
stats.passedTests++;
} else {
stats.failedTests++;
}
// Advance time by hop increment
minute += hops;
if (minute > 59) {
minute -= 60;
hour++;
if (hour > 23) {
hour = 0;
}
}
count++;
}
Serial.println();
}
// ============================================================================
// HELPER FUNCTIONS
// ============================================================================
uint16_t calculateTestCount() {
/**
* Calculate how many minutes will be tested based on:
* - Start and end times
* - Hop increment
*/
uint16_t count = 0;
uint8_t hour = hourStart;
uint8_t minute = minuteStart;
// Safety limit: maximum 10000 iterations to prevent infinite loops
for (uint16_t i = 0; i < 10000; i++) {
count++;
// Check if we've reached the end
if (hour == hourEnd && minute == minuteEnd) {
break;
}
// Advance time
minute += hops;
if (minute > 59) {
minute -= 60;
hour++;
if (hour > 23) {
hour = 0;
}
}
}
return count;
}
void printResultLine(uint8_t hour, uint8_t minute, const CharGraphTimeWords& result, bool success) {
/**
* Print one result line in table format:
* Uhrzeit | Text | MinutenLeds | Links | Rechts
*/
// Uhrzeit
if (hour < 10) Serial.print("0");
Serial.print(hour);
Serial.print(":");
if (minute < 10) Serial.print("0");
Serial.print(minute);
Serial.print(" | ");
// Text (first 37 chars max, padded)
char textBuf[100];
if (success) {
buildTextFromWords(result.words, result.wordCount, textBuf);
} else {
strcpy(textBuf, "ERROR");
}
int len = strlen(textBuf);
Serial.print(textBuf);
for (int i = len; i < 37; i++) {
Serial.print(" ");
}
Serial.print(" | ");
// MinutenLeds (LED count display)
if (result.ledCount > 0) {
for (uint8_t i = 0; i < result.ledCount; i++) {
Serial.print("*"); // Use * instead of emoji for reliability
}
}
for (uint8_t i = result.ledCount; i < 11; i++) {
Serial.print(" ");
}
Serial.print(" | ");
// Links / Rechts
if (result.ledCount > 0) {
if (result.ledDirection == LEFT) {
Serial.print("L");
Serial.print(" | ");
Serial.print("R");
} else {
Serial.print("R");
Serial.print(" | ");
Serial.print("L");
}
} else {
Serial.print(" ");
Serial.print(" | ");
Serial.print(" ");
}
Serial.println();
}
void buildTextFromWords(const char* const* words, uint8_t wordCount, char* outText) {
/**
* Build readable text from word array
*/
if (!words || !outText || wordCount == 0) {
outText[0] = '\0';
return;
}
uint16_t pos = 0;
const uint16_t maxLen = 99;
for (uint8_t i = 0; i < wordCount; i++) {
if (i > 0 && pos < maxLen) {
outText[pos++] = ' ';
}
// Load word from PROGMEM and copy
char wordBuf[11];
strcpy_P(wordBuf, words[i]);
for (uint8_t j = 0; wordBuf[j] != '\0' && pos < maxLen; j++) {
outText[pos++] = wordBuf[j];
}
}
outText[pos] = '\0';
}
void printTestSummary() {
/**
* Print test statistics summary
*/
Serial.println("\n========================================");
Serial.println("TEST SUMMARY");
Serial.println("========================================");
Serial.print("Total Tests: ");
Serial.println(stats.totalTests);
Serial.print("Passed: ");
Serial.println(stats.passedTests);
Serial.print("Failed: ");
Serial.println(stats.failedTests);
if (stats.failedTests == 0) {
Serial.println("\n✓ ALL TESTS PASSED");
} else {
Serial.print("\n");
Serial.print(stats.failedTests);
Serial.println(" TESTS FAILED");
}
Serial.println("========================================");
}

View File

@ -0,0 +1,48 @@
/**
* CharGraph TimeLogic - Black Box Test Configuration
*
* Define test patterns and time windows here
*/
#ifndef CONFIG_H
#define CONFIG_H
#include <Arduino.h>
// ============================================================================
// TEST PATTERNS
// ============================================================================
// Each pattern must be exactly 110 characters
const char testpatterns[][111] = {
// Pattern 1: Standard Pattern
"ESGISTWZEHNFÜNFVIERTELVORNACHBALDHALBKZWEINSYWAYOLOVDCQZEHNEUNACHTELFÜNFZWÖLFSECHSIEBENGEDREIVIERTMDCPAUSEUHRW",
// Pattern 2: DREIVIERTEL Pattern
"ESUISTKFASTBALDKURZEHNFÜNFZEITVORDREIVIERTELNACHRWDHALBHSECHSVIERKELFZWÖLFÜNFZEHNEUNACHTDREINSIEBENMZWEIEHUHRQ",
// Add more patterns here as needed
// "PATTERN3...",
// "PATTERN4...",
};
// Number of test patterns
const uint8_t numPatterns = 2;
// ============================================================================
// TEST TIME WINDOW
// ============================================================================
uint8_t hourStart = 23; // Start hour (0-23)
uint8_t hourEnd = 0; // End hour (0-23, can wrap around)
uint8_t minuteStart = 23; // Start minute (0-59)
uint8_t minuteEnd = 27; // End minute (0-59)
uint8_t hops = 1; // Minute increments (1 = every minute, 5 = every 5 minutes, etc.)
// ============================================================================
// SERIAL OUTPUT CONFIGURATION
// ============================================================================
#define SERIAL_BAUD 115200
#endif // CONFIG_H

View File

@ -0,0 +1,117 @@
/**
* CharGraph Time Logic - Basic Example
*
* Simple example showing how to use the library:
* - Convert time to German words
* - Display LED hex value
*
* Tested on: WEMOS D1 Mini (ESP8266)
*
* Hardware:
* - ESP8266 / WEMOS D1 Mini
* - USB Serial connection
*
* Instructions:
* 1. Install library in Arduino/libraries/CharGraphTimeLogic
* 2. Open this sketch in Arduino IDE
* 3. Select Tools > Board > LOLIN(WEMOS) D1 mini (ESP8266)
* 4. Select Tools > Upload Speed > 921600
* 5. Upload and open Serial Monitor (115200 baud)
*/
#include <CharGraphTimeLogic.h>
// 110-character pattern (10 rows × 11 columns)
// Normal word order
const char PATTERN_NORMAL[] PROGMEM =
"ESIST-FUENFZEHNVIERTELVOR"
"NACHABFASTHALBALDZWEI----"
"DREEINSIEBENEFL-ZWOELF"
"FUENF-SECHSVIER-ZEHNEUNACHT"
"-------UHR";
// Swapped pattern: NACH before VIERTEL (triggers fallback)
// This tests the multi-level fallback for :15-:19
const char PATTERN_SWAPPED[] PROGMEM =
"ESIST-FUENFZEHN-NACH-VOR-"
"VIERTELFASTHALBALDZWEI----"
"DREEINSIEBENEFL-ZWOELF"
"FUENF-SECHSVIER-ZEHNEUNACHT"
"-------UHR";
void setup() {
Serial.begin(115200);
delay(100);
Serial.println("\n\n=== CharGraph Time Logic - Basic Example ===\n");
}
void loop() {
// Test 1: Normal pattern (reference)
Serial.println("=== TEST 1: NORMAL PATTERN ===");
testPattern(PATTERN_NORMAL, "NORMAL");
testTime(PATTERN_NORMAL, 6, 0); // 6:00 → ES IST SECHS
testTime(PATTERN_NORMAL, 6, 5); // 6:05 → ES IST FÜNF NACH SECHS
testTime(PATTERN_NORMAL, 6, 15); // 6:15 → ES IST VIERTEL NACH SECHS
testTime(PATTERN_NORMAL, 6, 30); // 6:30 → ES IST HALB SIEBEN
testTime(PATTERN_NORMAL, 6, 45); // 6:45 → ES IST VIERTEL VOR SIEBEN
testTime(PATTERN_NORMAL, 6, 55); // 6:55 → ES IST FÜNF VOR SIEBEN
// Test 2: Swapped pattern (triggers fallback)
Serial.println("\n=== TEST 2: SWAPPED PATTERN (Fallback Test) ===");
testPattern(PATTERN_SWAPPED, "SWAPPED");
testTime(PATTERN_SWAPPED, 1, 15); // 1:15 → Fallback: ES IST VIERTEL EINS (no NACH)
testTime(PATTERN_SWAPPED, 1, 16); // 1:16 → Fallback: ES IST ZEHN VOR HALB ZWEI (4 LEDs LEFT)
testTime(PATTERN_SWAPPED, 1, 17); // 1:17 → Fallback: ES IST ZEHN VOR HALB ZWEI (3 LEDs LEFT)
testTime(PATTERN_SWAPPED, 1, 18); // 1:18 → Fallback: ES IST ZEHN VOR HALB ZWEI (2 LEDs LEFT)
testTime(PATTERN_SWAPPED, 1, 19); // 1:19 → Fallback: ES IST ZEHN VOR HALB ZWEI (1 LED LEFT)
Serial.println("\n--- Test completed ---\n");
delay(5000); // Wait 5 seconds before next test
}
void testPattern(const char* pattern, const char* patternName) {
Serial.print("Pattern: ");
Serial.print(patternName);
Serial.print(" (Length: ");
Serial.print(strlen(pattern));
Serial.println(")");
delay(1000);
}
void testTime(const char* pattern, uint8_t hour, uint8_t minute) {
Serial.print(" Time: ");
if (hour < 10) Serial.print("0");
Serial.print(hour);
Serial.print(":");
if (minute < 10) Serial.print("0");
Serial.println(minute);
CharGraphTimeWords result;
// Call library function
if (getCharGraphWords(pattern, hour, minute, result)) {
// Success - print results
Serial.print(" Text: ");
Serial.println(result.text);
Serial.print(" Words: ");
for (uint8_t i = 0; i < result.wordCount; i++) {
Serial.print((const __FlashStringHelper*) result.words[i]);
if (i < result.wordCount - 1) Serial.print(" ");
}
Serial.println();
Serial.print(" LED: Count=");
Serial.print(result.ledCount);
Serial.print(", Direction=");
Serial.print((const __FlashStringHelper*) result.ledDirection);
Serial.print(", Hex=0x");
if (result.ledHex < 0x10) Serial.print("0");
Serial.println(result.ledHex, HEX);
} else {
// Error - validation failed
Serial.println(" ERROR: Validation failed!");
delay(200);
}
}

View File

@ -0,0 +1,199 @@
/**
* CharGraph Time Logic - Full Example with Time Sync
*
* Complete example showing:
* - Pattern validation
* - Real-time conversion
* - LED output
* - Debug information
*
* Tested on: WEMOS D1 Mini (ESP8266)
*
* Hardware:
* - ESP8266 / WEMOS D1 Mini
* - USB Serial connection
* - Optional: DS3231 RTC module (I2C)
*
* Instructions:
* 1. Install library
* 2. Configure pattern below
* 3. Upload and monitor serial output
*/
#include <CharGraphTimeLogic.h>
// Define this to enable debug output
// #define CHARGRAPH_DEBUG
// 110-character pattern - MUST be exactly 110 characters
// Format: 10 rows of 11 characters each
// Row 1: ESIST-FUENF... (E S I S T - F Ü E N F = 11 chars)
// Row 2-10: similar
const char PATTERN[] PROGMEM =
"ESIST-FUENFZEHNVIERTELVOR"
"NACHABFASTHALBALDZWEI----"
"DREEINSIEBENEFL-ZWOELF"
"FUENF-SECHSVIER-ZEHNEUNACHT"
"-------UHR"; // Total: 110 characters
// Current time (simulated or from RTC)
uint8_t currentHour = 14;
uint8_t currentMinute = 30;
void setup() {
Serial.begin(115200);
delay(100);
Serial.println("\n\n");
Serial.println("╔════════════════════════════════════════╗");
Serial.println("║ CharGraph Time Logic - Full Example ║");
Serial.println("╚════════════════════════════════════════╝");
Serial.println();
// Verify pattern length
size_t patternLen = strlen_P(PATTERN);
Serial.print("Pattern length: ");
Serial.println(patternLen);
if (patternLen == 110) {
Serial.println("✓ Pattern length is correct (110 chars)");
} else {
Serial.print("✗ Pattern length ERROR! Expected 110, got ");
Serial.println(patternLen);
}
Serial.println();
Serial.println("Starting time simulation...");
Serial.println();
}
void loop() {
// Convert time to words
CharGraphTimeWords result;
if (getCharGraphWords(PATTERN, currentHour, currentMinute, result)) {
// ===== Display current time =====
displayTime(currentHour, currentMinute);
// ===== Display words =====
Serial.print("Words: ");
for (uint8_t i = 0; i < result.wordCount; i++) {
Serial.print((const __FlashStringHelper*) result.words[i]);
if (i < result.wordCount - 1) Serial.print(" ");
}
Serial.println();
// ===== Display LED information =====
Serial.print("LED: ");
Serial.print(result.ledCount);
Serial.print(" × ");
Serial.print((const __FlashStringHelper*) result.ledDirection);
Serial.print(" = 0x");
if (result.ledHex < 0x10) Serial.print("0");
Serial.println(result.ledHex, HEX);
// ===== Display binary representation =====
Serial.print("Bits: ");
for (int8_t i = 3; i >= 0; i--) {
Serial.print((result.ledHex >> i) & 1);
}
Serial.println();
Serial.println();
} else {
Serial.print("ERROR at ");
if (currentHour < 10) Serial.print("0");
Serial.print(currentHour);
Serial.print(":");
if (currentMinute < 10) Serial.print("0");
Serial.println(currentMinute);
Serial.println("Pattern validation failed!");
Serial.println();
}
// ===== Advance time =====
currentMinute += 5;
if (currentMinute >= 60) {
currentMinute = 0;
currentHour += 1;
if (currentHour >= 24) {
currentHour = 0;
Serial.println("═══════════════════════════════════════");
Serial.println(" Daily cycle complete");
Serial.println("═══════════════════════════════════════");
Serial.println();
}
}
delay(1000); // 1 second per step (demonstrates all 60 minutes quickly)
}
// ============================================================================
// HELPER FUNCTIONS
// ============================================================================
void displayTime(uint8_t hour, uint8_t minute) {
Serial.print("Time: ");
if (hour < 10) Serial.print("0");
Serial.print(hour);
Serial.print(":");
if (minute < 10) Serial.print("0");
Serial.print(minute);
Serial.print("");
}
// ============================================================================
// OPTIONAL: RTC INTEGRATION
// ============================================================================
// If you have a DS3231 RTC module, uncomment and modify this section:
/*
#include <Wire.h>
#include <RTClib.h>
RTC_DS3231 rtc;
void setupRTC() {
if (!rtc.begin()) {
Serial.println("RTC not found!");
while (1);
}
// Set time (only on first run)
// rtc.adjust(DateTime(F(__DATE__), F(__TIME__)));
}
void updateTimeFromRTC() {
DateTime now = rtc.now();
currentHour = now.hour();
currentMinute = now.minute();
}
*/
// ============================================================================
// OPTIONAL: WIFI TIME SYNC (NTP)
// ============================================================================
// If you want to sync time from internet, add WiFi + time library:
/*
#include <time.h>
void setupWiFi() {
WiFi.begin("SSID", "PASSWORD");
while (WiFi.status() != WL_CONNECTED) {
delay(500);
Serial.print(".");
}
// Sync time from NTP
configTime(2 * 3600, 0, "pool.ntp.org", "time.nist.gov");
Serial.println("Time synced");
}
void updateTimeFromNTP() {
time_t now = time(nullptr);
struct tm* timeinfo = localtime(&now);
currentHour = timeinfo->tm_hour;
currentMinute = timeinfo->tm_min;
}
*/

View File

@ -0,0 +1,195 @@
/**
* CharGraph Time Logic - Minute Range Test
*
* Tests a full minute range with detailed LED and word output
* Time Range: 12:29 to 13:35 (67 minutes)
*
* Output Format:
* Uhrzeit | Text | MinutenLeds | Links | Rechts
* 12:29 | ES IST VOR HALB EINS | | |
* 12:30 | ES IST HALB DREI | | |
*
* Tested on: WEMOS D1 Mini (ESP8266)
*
* Instructions:
* 1. Install CharGraphTimeLogic library in Arduino/libraries/
* 2. Open this sketch in Arduino IDE
* 3. Select Tools > Board > LOLIN(WEMOS) D1 mini (ESP8266)
* 4. Upload and open Serial Monitor (115200 baud)
* 5. Copy the table output
*/
#include <CharGraphTimeLogic.h>
// 110-character pattern for testing
// User's pattern: "ESGISTWZEHNFÜNFVIERTELVORNACHBALDHALBKZWEINSYWAYOLOVDCQZEHNEUNACHTELFÜNFZWÖLFSECHSIEBENGEDREIVIERTMDCPAUSEUHRW"
const char TEST_PATTERN[] PROGMEM =
"ESGISTWZEHNFÜNFVIERTEL"
"VORNACHBALDHALBKZWEINS"
"YWAYOLOVDCQZEHNEUNACHT"
"ELFÜNFZWÖLFSECHSIEBENGED"
"REIVIERTMDCPAUSEUHRW";
// Test result structure
struct TestResult {
uint8_t hour;
uint8_t minute;
char text[100];
uint8_t ledCount;
const char* ledDirection;
uint8_t ledHex;
boolean success;
};
// Storage for all test results (67 minutes)
TestResult results[68];
uint8_t resultCount = 0;
void setup() {
Serial.begin(115200);
delay(100);
Serial.println("\n\n=== CharGraph Time Logic - Minute Range Test ===");
Serial.println("Time Range: 12:29 to 13:35");
Serial.println("Pattern Length: 110 characters\n");
// Validate pattern length
if (strlen_P(TEST_PATTERN) != 110) {
Serial.print("ERROR: Pattern length is ");
Serial.print(strlen_P(TEST_PATTERN));
Serial.println(" (expected 110)");
}
// Run all tests
runMinuteRangeTest();
// Print results table
printResultsTable();
Serial.println("\n=== Test Completed ===\n");
}
void loop() {
// Nothing to do after setup
delay(1000);
}
void runMinuteRangeTest() {
Serial.println("Running tests...");
uint8_t startHour = 12;
uint8_t startMinute = 29;
uint8_t endHour = 13;
uint8_t endMinute = 35;
uint8_t hour = startHour;
uint8_t minute = startMinute;
while (resultCount < 68) {
// Test this time
TestResult result;
result.hour = hour;
result.minute = minute;
CharGraphTimeWords timeWords;
// Call library function
if (getCharGraphWords(TEST_PATTERN, hour, minute, timeWords)) {
result.success = true;
strcpy(result.text, timeWords.text);
result.ledCount = timeWords.ledCount;
result.ledDirection = timeWords.ledDirection;
result.ledHex = timeWords.ledHex;
} else {
result.success = false;
strcpy(result.text, "ERROR");
result.ledCount = 0;
result.ledDirection = "left";
result.ledHex = 0;
}
results[resultCount++] = result;
// Move to next minute
minute++;
if (minute > 59) {
minute = 0;
hour++;
}
// Stop after 13:35
if (hour == endHour && minute > endMinute) {
break;
}
}
Serial.print("Tested ");
Serial.print(resultCount);
Serial.println(" minutes");
}
void printResultsTable() {
Serial.println("\n=== RESULTS TABLE ===\n");
// Header
Serial.println("Uhrzeit | Text | MinutenLeds | Links | Rechts");
Serial.println("--------|--------------------------|-------------|-------|-------");
// Data rows
for (uint8_t i = 0; i < resultCount; i++) {
TestResult& res = results[i];
// Uhrzeit
if (res.hour < 10) Serial.print("0");
Serial.print(res.hour);
Serial.print(":");
if (res.minute < 10) Serial.print("0");
Serial.print(res.minute);
Serial.print(" | ");
// Text (truncate to 24 characters)
char textBuf[25];
strncpy(textBuf, res.text, 24);
textBuf[24] = '\0';
// Pad to 24 characters
for (int j = strlen(textBuf); j < 24; j++) {
textBuf[j] = ' ';
}
textBuf[24] = '\0';
Serial.print(textBuf);
Serial.print(" | ");
// MinutenLeds (LED visualization)
if (res.ledCount > 0) {
for (uint8_t led = 0; led < res.ledCount; led++) {
Serial.print("");
}
// Pad with spaces if less than 4 LEDs
for (uint8_t space = res.ledCount; space < 4; space++) {
Serial.print(" ");
}
} else {
Serial.print(" "); // 9 spaces for no LEDs
}
Serial.print(" | ");
// Links (LEFT direction)
if (res.success && strcmp_P(res.ledDirection, PSTR("left")) == 0 && res.ledCount > 0) {
Serial.print("");
} else {
Serial.print("");
}
Serial.print(" | ");
// Rechts (RIGHT direction)
if (res.success && strcmp_P(res.ledDirection, PSTR("right")) == 0 && res.ledCount > 0) {
Serial.print("");
} else {
Serial.print("");
}
Serial.println();
}
}

View File

@ -0,0 +1,10 @@
name=CharGraph Time Logic
version=1.0.0
author=CharGraph
maintainer=CharGraph
sentence=German word clock time-to-words conversion library for ESP8266/Arduino
paragraph=Converts time (hour:minute) to German words and calculates minute LED positions. Includes pattern validation and 24-minute rules with modifiers (KURZ, BALD, FAST). Optimized for ESP8266/WEMOS D1 Mini with PROGMEM storage.
category=Timing
url=https://github.com/CharGraph/CharGraphTimeLogic
architectures=esp8266,esp32,avr
includes=CharGraphTimeLogic.h

View File

@ -0,0 +1,167 @@
/**
* CharGraph Time Logic Library - Main Implementation
*/
#include "CharGraphTimeLogic.h"
#include "WordMatcher.h"
#include "Validator.h"
#include "Constants.h"
#include <cstring>
// ============================================================================
// PUBLIC API: GET CHARGRAPH WORDS
// ============================================================================
int8_t getCharGraphWords(
const char* pattern,
uint8_t hour,
uint8_t minute,
CharGraphTimeWords& outResult
) {
if (!pattern) {
outResult.wordCount = 0;
return -1;
}
// Validate pattern length
if (strlen(pattern) != GRID_SIZE) {
outResult.wordCount = 0;
return -2;
}
// Validate input ranges
if (hour > 23 || minute > 59) {
outResult.wordCount = 0;
return -3;
}
// Validate pattern structure (ES/IST, HALB, UHR placement)
ValidationResult structValidation = validateStructure(pattern);
if (!structValidation.valid) {
outResult.wordCount = 0;
if (structValidation.reason) {
#ifdef CHARGRAPH_DEBUG
Serial.print("Structure validation failed: ");
Serial.println((const __FlashStringHelper*) structValidation.reason);
#endif
}
return -4;
}
// Validate mandatory words (FÜNF, ZEHN, VIERTEL, VOR, NACH with gaps)
ValidationResult mandatoryValidation = validateMandatoryWords(pattern);
if (!mandatoryValidation.valid) {
outResult.wordCount = 0;
if (mandatoryValidation.reason) {
#ifdef CHARGRAPH_DEBUG
Serial.print("Mandatory words validation failed: ");
Serial.println((const __FlashStringHelper*) mandatoryValidation.reason);
#endif
}
return -5;
}
// Get words for time
LEDInfo ledInfo;
const char* words[10];
uint8_t wordCount = getWordsForTime(
pattern,
hour,
minute,
words,
ledInfo
);
if (wordCount == 0) {
outResult.wordCount = 0;
return -6;
}
// Fill result structure
for (uint8_t i = 0; i < wordCount; i++) {
outResult.words[i] = words[i];
}
outResult.wordCount = wordCount;
outResult.ledCount = ledInfo.count;
outResult.ledDirection = ledInfo.direction;
outResult.ledHex = ledInfo.hex;
// Build text representation
buildCharGraphText(outResult.words, outResult.wordCount, outResult.text);
return 0;
}
// ============================================================================
// HELPER: BUILD TEXT FROM WORDS
// ============================================================================
uint16_t buildCharGraphText(
const char* const* words,
uint8_t wordCount,
char* outText
) {
if (!words || !outText || wordCount == 0) {
outText[0] = '\0';
return 0;
}
uint16_t pos = 0;
const uint16_t maxLen = 99; // Leave room for null terminator
for (uint8_t i = 0; i < wordCount; i++) {
if (i > 0 && pos < maxLen) {
outText[pos++] = ' ';
}
// Load word from PROGMEM
char wordBuf[12]; // Max 11 chars (DREIVIERTEL) + null terminator
strcpy_P(wordBuf, words[i]);
// Copy word to output
for (uint8_t j = 0; wordBuf[j] != '\0' && pos < maxLen; j++) {
outText[pos++] = wordBuf[j];
}
}
outText[pos] = '\0';
return pos;
}
// ============================================================================
// DEBUG FUNCTIONS
// ============================================================================
#ifdef CHARGRAPH_DEBUG
void debugPrintValidationError(const char* gridStr) {
if (!gridStr) return;
ValidationResult result = validateStructure(gridStr);
if (!result.valid) {
Serial.print("Validation Error: ");
if (result.reason) {
Serial.println((const __FlashStringHelper*) result.reason);
} else {
Serial.println("Unknown error");
}
} else {
Serial.println("Pattern structure is valid");
}
}
void debugPrintLEDInfo(const CharGraphTimeWords& result) {
Serial.print("LED Count: ");
Serial.println(result.ledCount);
Serial.print("LED Direction: ");
Serial.println((const __FlashStringHelper*) result.ledDirection);
Serial.print("LED Hex: 0x");
if (result.ledHex < 0x10) Serial.print("0");
Serial.println(result.ledHex, HEX);
}
#endif // CHARGRAPH_DEBUG

View File

@ -0,0 +1,114 @@
/**
* CharGraph Time Logic Library for Arduino/ESP8266
*
* Converts German time (hour:minute) to word sequence and calculates
* minute LED positions for word clock displays.
*
* Usage:
* #include <CharGraphTimeLogic.h>
*
* const char pattern[] = "ESIST..."; // 110 chars
* CharGraphTimeWords result;
*
* if (getCharGraphWords(pattern, 14, 30, result)) {
* Serial.println(result.text); // "ES IST HALB DREI"
* Serial.println(result.ledHex); // 0x00
* }
*/
#ifndef CHARGRAPH_TIME_LOGIC_H
#define CHARGRAPH_TIME_LOGIC_H
#include <Arduino.h>
// ============================================================================
// PUBLIC TYPES
// ============================================================================
/**
* Result structure for time-to-words conversion
*/
struct CharGraphTimeWords {
const char* words[10]; // Array of word pointers (PROGMEM)
uint8_t wordCount; // Number of words (1-10)
// LED Information
uint8_t ledCount; // 0-4 LEDs
const char* ledDirection; // "left" or "right" (PROGMEM)
uint8_t ledHex; // 4-bit value (0x00-0x0F)
// Text representation
char text[100]; // Full text (optional, built by helper)
};
// ============================================================================
// PUBLIC API
// ============================================================================
/**
* Convert time to CharGraph words and LED positions
*
* @param pattern 110-character grid (uppercase, uppercase A-Z and 0-9)
* @param hour Hour (0-23)
* @param minute Minute (0-59)
* @param outResult Result structure (filled on success)
* @return true if successful, false on error (pattern validation failed)
*
* Example:
* const char pattern[] PROGMEM = "ESIST-FÜNFZEHN...";
* CharGraphTimeWords result;
* if (getCharGraphWords(pattern, 14, 25, result)) {
* Serial.print("Words: ");
* for (int i = 0; i < result.wordCount; i++) {
* Serial.print((const __FlashStringHelper*) pgm_read_ptr(&result.words[i]));
* Serial.print(" ");
* }
* }
*/
int8_t getCharGraphWords(
const char* pattern,
uint8_t hour,
uint8_t minute,
CharGraphTimeWords& outResult
);
/**
* Build human-readable text from word array (optional helper)
*
* @param words Array of word pointers (PROGMEM strings)
* @param wordCount Number of words
* @param outText Output buffer (at least 100 bytes)
* @return Length of generated text
*
* Example:
* char text[100];
* buildCharGraphText(result.words, result.wordCount, text);
* Serial.println(text); // "ES IST HALB DREI"
*/
uint16_t buildCharGraphText(
const char* const* words,
uint8_t wordCount,
char* outText
);
// ============================================================================
// DEBUG FUNCTIONS (Optional, can be disabled)
// ============================================================================
#ifdef CHARGRAPH_DEBUG
/**
* Print validation error message to Serial
* Only available if CHARGRAPH_DEBUG is defined
*/
void debugPrintValidationError(const char* gridStr);
/**
* Print LED calculation details to Serial
* Only available if CHARGRAPH_DEBUG is defined
*/
void debugPrintLEDInfo(const CharGraphTimeWords& result);
#endif
#endif // CHARGRAPH_TIME_LOGIC_H

View File

@ -0,0 +1,151 @@
/**
* CharGraph Time Logic - Constants Implementation
*
* All string constants stored in PROGMEM
*/
#include "Constants.h"
// ============================================================================
// HOUR WORDS
// ============================================================================
const char HOUR_ZERO[] PROGMEM = "ZWoLF";
const char HOUR_ONE[] PROGMEM = "EINS";
const char HOUR_TWO[] PROGMEM = "ZWEI";
const char HOUR_THREE[] PROGMEM = "DREI";
const char HOUR_FOUR[] PROGMEM = "VIER";
const char HOUR_FIVE[] PROGMEM = "FuNF";
const char HOUR_SIX[] PROGMEM = "SECHS";
const char HOUR_SEVEN[] PROGMEM = "SIEBEN";
const char HOUR_EIGHT[] PROGMEM = "ACHT";
const char HOUR_NINE[] PROGMEM = "NEUN";
const char HOUR_TEN[] PROGMEM = "ZEHN";
const char HOUR_ELEVEN[] PROGMEM = "ELF";
const char* const HOURS[12] PROGMEM = {
HOUR_ONE,
HOUR_TWO,
HOUR_THREE,
HOUR_FOUR,
HOUR_FIVE,
HOUR_SIX,
HOUR_SEVEN,
HOUR_EIGHT,
HOUR_NINE,
HOUR_TEN,
HOUR_ELEVEN,
HOUR_ZERO
};
// ============================================================================
// MANDATORY/OPTIONAL WORDS
// ============================================================================
const char ES[] PROGMEM = "ES";
const char IST[] PROGMEM = "IST";
const char HALB[] PROGMEM = "HALB";
const char VIERTEL[] PROGMEM = "VIERTEL";
const char VOR[] PROGMEM = "VOR";
const char NACH[] PROGMEM = "NACH";
const char UHR[] PROGMEM = "UHR";
const char EIN[] PROGMEM = "EIN";
// ============================================================================
// OPTIONAL MODIFIERS
// ============================================================================
const char KURZ[] PROGMEM = "KURZ";
const char BALD[] PROGMEM = "BALD";
const char FAST[] PROGMEM = "FAST";
const char ZWANZIG[] PROGMEM = "ZWANZIG";
const char DREIVIERTEL[] PROGMEM = "DREIVIERTEL";
const char NACHT[] PROGMEM = "NACHT";
const char WIR[] PROGMEM = "WIR";
const char HABEN[] PROGMEM = "HABEN";
// ============================================================================
// MINUTE WORDS
// ============================================================================
const char FUENF[] PROGMEM = "FuNF";
const char ZEHN[] PROGMEM = "ZEHN";
const char EINS[] PROGMEM = "EINS";
// ============================================================================
// LED DIRECTION STRINGS
// ============================================================================
const char LEFT[] PROGMEM = "left";
const char RIGHT[] PROGMEM = "right";
// ============================================================================
// ERROR MESSAGES
// ============================================================================
const char ERR_NO_WORDS[] PROGMEM = "No words";
const char ERR_WORD_NOT_FOUND[] PROGMEM = "Word not found";
const char ERR_NO_GAP[] PROGMEM = "No gap between words";
const char ERR_NO_ES[] PROGMEM = "ES missing";
const char ERR_NO_IST[] PROGMEM = "IST missing";
const char ERR_NO_HALB[] PROGMEM = "HALB missing";
const char ERR_NO_WIR[] PROGMEM = "WIR missing";
const char ERR_NO_HABEN[] PROGMEM = "HABEN missing";
const char ERR_NO_GAP_ES_IST[] PROGMEM = "No gap between ES and IST";
const char ERR_NO_GAP_WIR_HABEN[] PROGMEM = "No gap between WIR and HABEN";
const char ERR_UHR_NOT_LAST[] PROGMEM = "UHR must be last word";
// ============================================================================
// MANDATORY WORDS ERROR MESSAGES
// ============================================================================
const char ERR_NO_FUENF[] PROGMEM = "FÜNF missing";
const char ERR_NO_ZEHN[] PROGMEM = "ZEHN missing";
const char ERR_NO_VIERTEL[] PROGMEM = "VIERTEL missing";
const char ERR_NO_VOR[] PROGMEM = "VOR missing";
const char ERR_NO_NACH[] PROGMEM = "NACH missing";
const char ERR_NO_GAP_VIERTEL_VOR[] PROGMEM = "No gap between VIERTEL and VOR";
const char ERR_NO_GAP_VIERTEL_NACH[] PROGMEM = "No gap between VIERTEL and NACH";
const char ERR_NO_VIERTEL_SEQUENCE[] PROGMEM = "Cannot display :45 (VIERTEL before VOR required or DREIVIERTEL missing)";
// ============================================================================
// OPTIONAL WORDS
// ============================================================================
const char ZEIT[] PROGMEM = "ZEIT";
const char ALARM[] PROGMEM = "ALARM";
const char PAUSE[] PROGMEM = "PAUSE";
const char RWD[] PROGMEM = "RWD";
// ============================================================================
// OPTIONAL WORDS WARNING MESSAGES
// ============================================================================
const char WARN_NO_GAP_NACHT[] PROGMEM = "INFO: No gap between IST and NACHT";
const char WARN_NO_GAP_ZEIT[] PROGMEM = "INFO: No gap between IST and ZEIT";
const char WARN_NO_GAP_ALARM[] PROGMEM = "INFO: No gap between IST and ALARM";
const char WARN_NO_GAP_PAUSE[] PROGMEM = "INFO: No gap between IST and PAUSE";
const char WARN_NO_GAP_RWD[] PROGMEM = "INFO: No gap between IST and RWD";
// ============================================================================
// HELPER: Compare PROGMEM string with C-string
// ============================================================================
bool wordEquals(const char* word_progmem, const char* cstr) {
if (!word_progmem || !cstr) return false;
char buf[12]; // Max 11 chars (DREIVIERTEL) + null terminator
strcpy_P(buf, word_progmem);
return strcmp(buf, cstr) == 0;
}
// ============================================================================
// HELPER: Get hour word from PROGMEM array
// ============================================================================
const char* getHourWord(uint8_t h12) {
if (h12 <= 0) h12 = 12;
else if (h12 > 12) h12 -= 12;
// Index into HOURS array (0-11 for h12 1-12)
return (const char*)pgm_read_ptr(&HOURS[h12 - 1]);
}

View File

@ -0,0 +1,139 @@
/**
* CharGraph Time Logic - Constants
*
* All arrays stored in PROGMEM for ESP8266 memory efficiency
* Port of lib/time-logic/constants.ts
*/
#ifndef CHARGRAPH_CONSTANTS_H
#define CHARGRAPH_CONSTANTS_H
#include <Arduino.h>
// ============================================================================
// GRID PARAMETERS
// ============================================================================
#define GRID_SIZE 110 // 10 rows × 11 cols
#define GRID_ROWS 10
#define GRID_COLS 11
// ============================================================================
// HOUR WORDS (PROGMEM)
// ============================================================================
extern const char* const HOURS[12] PROGMEM;
extern const char HOUR_ZERO[] PROGMEM;
extern const char HOUR_ONE[] PROGMEM;
extern const char HOUR_TWO[] PROGMEM;
extern const char HOUR_THREE[] PROGMEM;
extern const char HOUR_FOUR[] PROGMEM;
extern const char HOUR_FIVE[] PROGMEM;
extern const char HOUR_SIX[] PROGMEM;
extern const char HOUR_SEVEN[] PROGMEM;
extern const char HOUR_EIGHT[] PROGMEM;
extern const char HOUR_NINE[] PROGMEM;
extern const char HOUR_TEN[] PROGMEM;
extern const char HOUR_ELEVEN[] PROGMEM;
// ============================================================================
// MANDATORY/OPTIONAL WORDS (PROGMEM)
// ============================================================================
extern const char ES[] PROGMEM;
extern const char IST[] PROGMEM;
extern const char HALB[] PROGMEM;
extern const char VIERTEL[] PROGMEM;
extern const char VOR[] PROGMEM;
extern const char NACH[] PROGMEM;
extern const char UHR[] PROGMEM;
extern const char EIN[] PROGMEM;
// ============================================================================
// OPTIONAL MODIFIERS (PROGMEM)
// ============================================================================
extern const char KURZ[] PROGMEM;
extern const char BALD[] PROGMEM;
extern const char FAST[] PROGMEM;
extern const char ZWANZIG[] PROGMEM;
extern const char DREIVIERTEL[] PROGMEM;
extern const char NACHT[] PROGMEM;
extern const char WIR[] PROGMEM;
extern const char HABEN[] PROGMEM;
// ============================================================================
// MINUTE WORDS (PROGMEM)
// ============================================================================
extern const char FUENF[] PROGMEM;
extern const char ZEHN[] PROGMEM;
extern const char EINS[] PROGMEM;
// ============================================================================
// LED DIRECTION STRINGS (PROGMEM)
// ============================================================================
extern const char LEFT[] PROGMEM;
extern const char RIGHT[] PROGMEM;
// ============================================================================
// LED BIT VALUES
// ============================================================================
#define LED1_LEFT 0x08 // Bit 3
#define LED2_LEFT 0x04 // Bit 2
#define LED2_RIGHT 0x02 // Bit 1
#define LED1_RIGHT 0x01 // Bit 0
// ============================================================================
// ERROR MESSAGES (PROGMEM)
// ============================================================================
extern const char ERR_NO_WORDS[] PROGMEM;
extern const char ERR_WORD_NOT_FOUND[] PROGMEM;
extern const char ERR_NO_GAP[] PROGMEM;
extern const char ERR_NO_ES[] PROGMEM;
extern const char ERR_NO_IST[] PROGMEM;
extern const char ERR_NO_HALB[] PROGMEM;
extern const char ERR_NO_WIR[] PROGMEM;
extern const char ERR_NO_HABEN[] PROGMEM;
extern const char ERR_NO_GAP_ES_IST[] PROGMEM;
extern const char ERR_NO_GAP_WIR_HABEN[] PROGMEM;
extern const char ERR_UHR_NOT_LAST[] PROGMEM;
extern const char ERR_NO_FUENF[] PROGMEM;
extern const char ERR_NO_ZEHN[] PROGMEM;
extern const char ERR_NO_VIERTEL[] PROGMEM;
extern const char ERR_NO_VOR[] PROGMEM;
extern const char ERR_NO_NACH[] PROGMEM;
extern const char ERR_NO_GAP_VIERTEL_VOR[] PROGMEM;
extern const char ERR_NO_GAP_VIERTEL_NACH[] PROGMEM;
extern const char ERR_NO_VIERTEL_SEQUENCE[] PROGMEM;
// ============================================================================
// OPTIONAL WORDS
// ============================================================================
extern const char ZEIT[] PROGMEM;
extern const char ALARM[] PROGMEM;
extern const char PAUSE[] PROGMEM;
extern const char RWD[] PROGMEM;
// ============================================================================
// OPTIONAL WORDS WARNING MESSAGES
// ============================================================================
extern const char WARN_NO_GAP_NACHT[] PROGMEM;
extern const char WARN_NO_GAP_ZEIT[] PROGMEM;
extern const char WARN_NO_GAP_ALARM[] PROGMEM;
extern const char WARN_NO_GAP_PAUSE[] PROGMEM;
extern const char WARN_NO_GAP_RWD[] PROGMEM;
// ============================================================================
// HELPER FUNCTIONS
// ============================================================================
extern bool wordEquals(const char* word_progmem, const char* cstr);
extern const char* getHourWord(uint8_t h12);
#endif // CHARGRAPH_CONSTANTS_H

View File

@ -0,0 +1,370 @@
/**
* CharGraph Time Logic - LED Calculator Implementation
*/
#include "LEDCalculator.h"
#include "Constants.h"
#include <cstring>
// ============================================================================
// TARGET MINUTE CALCULATION
// ============================================================================
uint8_t getTargetMinute(
const char* const* words,
uint8_t wordCount,
bool& isLeftDirection
) {
// Default: left (NACH = count up)
isLeftDirection = true;
// Look for minute-indicator words (FÜNF, ZEHN, VIERTEL, ZWANZIG, DREIVIERTEL)
for (uint8_t i = 0; i < wordCount; i++) {
if (wordEquals(words[i], "FuNF")) {
// FÜNF can be NACH (5 min) or VOR (55 min)
for (uint8_t j = i + 1; j < wordCount; j++) {
if (wordEquals(words[j], "VOR")) {
isLeftDirection = false;
return 55; // FÜNF VOR = :55
}
if (wordEquals(words[j], "NACH")) {
isLeftDirection = true;
return 5; // FÜNF NACH = :05
}
}
// Default for FÜNF alone (rare)
return 5;
}
if (wordEquals(words[i], "ZEHN")) {
// ZEHN can be NACH (10 min) or VOR (50 min)
for (uint8_t j = i + 1; j < wordCount; j++) {
if (wordEquals(words[j], "VOR")) {
isLeftDirection = false;
return 50; // ZEHN VOR = :50
}
if (wordEquals(words[j], "NACH")) {
isLeftDirection = true;
return 10; // ZEHN NACH = :10
}
}
return 10;
}
if (wordEquals(words[i], "VIERTEL")) {
// VIERTEL is NACH (15 min) or VOR (45 min)
for (uint8_t j = i + 1; j < wordCount; j++) {
if (wordEquals(words[j], "VOR")) {
isLeftDirection = false;
return 45; // VIERTEL VOR = :45
}
if (wordEquals(words[j], "NACH")) {
isLeftDirection = true;
return 15; // VIERTEL NACH = :15
}
}
return 15;
}
if (wordEquals(words[i], "ZWANZIG")) {
// ZWANZIG is NACH (20 min) or VOR (40 min)
for (uint8_t j = i + 1; j < wordCount; j++) {
if (wordEquals(words[j], "VOR")) {
isLeftDirection = false;
return 40; // ZWANZIG VOR = :40
}
if (wordEquals(words[j], "NACH")) {
isLeftDirection = true;
return 20; // ZWANZIG NACH = :20
}
}
return 20;
}
if (wordEquals(words[i], "DREIVIERTEL")) {
// DREIVIERTEL is always :45
isLeftDirection = true;
return 45;
}
}
// Check for HALB
for (uint8_t i = 0; i < wordCount; i++) {
if (wordEquals(words[i], "HALB")) {
// HALB NACH or HALB VOR?
for (uint8_t j = i + 1; j < wordCount; j++) {
if (wordEquals(words[j], "VOR")) {
isLeftDirection = false;
return 20; // Something like "... VOR HALB" → target :20
}
if (wordEquals(words[j], "NACH")) {
isLeftDirection = true;
return 35; // Something like "... NACH HALB" → target :35
}
}
// HALB at :30
return 30;
}
}
return 0;
}
// ============================================================================
// LED COUNT TO HEX
// ============================================================================
uint8_t ledCountToHex(uint8_t count, bool isLeft) {
count = constrain(count, 0, 4);
if (isLeft) {
// LEFT: additive from top (bits 3→0)
// 0 LEDs: 0000 = 0x00
// 1 LED: 1000 = 0x08
// 2 LEDs: 1100 = 0x0C
// 3 LEDs: 1110 = 0x0E
// 4 LEDs: 1111 = 0x0F
const uint8_t leftTable[5] = {0x00, 0x08, 0x0C, 0x0E, 0x0F};
return leftTable[count];
} else {
// RIGHT: additive from bottom (bits 0→3)
// 0 LEDs: 0000 = 0x00
// 1 LED: 0001 = 0x01
// 2 LEDs: 0011 = 0x03
// 3 LEDs: 0111 = 0x07
// 4 LEDs: 1111 = 0x0F
const uint8_t rightTable[5] = {0x00, 0x01, 0x03, 0x07, 0x0F};
return rightTable[count];
}
}
// ============================================================================
// LED CALCULATION (11-TIER PRIORITY)
// ============================================================================
LEDInfo calculateLEDs(
const char* const* words,
uint8_t wordCount,
uint8_t mm
) {
LEDInfo result = {0, LEFT, 0x00};
if (!words || wordCount == 0) {
return result;
}
uint8_t remainder = 0;
bool isLeftDir = true;
// ===== PRIORITY 0: NACHT (special - Midnight context) =====
for (uint8_t i = 0; i < wordCount; i++) {
if (wordEquals(words[i], "NACHT")) {
// NACHT at :00-:04 - additive counting after midnight
remainder = mm; // 0, 1, 2, 3, 4
if (remainder > 4) remainder = 0;
isLeftDir = true;
result.direction = LEFT;
result.count = remainder;
result.hex = ledCountToHex(remainder, isLeftDir);
return result;
}
}
// ===== PRIORITY 1: DREIVIERTEL =====
// Only at :45, shows remainder towards :50
for (uint8_t i = 0; i < wordCount; i++) {
if (wordEquals(words[i], "DREIVIERTEL")) {
remainder = mm - 45;
if (remainder > 4) remainder = 0; // Out of valid range
isLeftDir = true;
result.direction = LEFT;
result.count = remainder;
result.hex = ledCountToHex(remainder, isLeftDir);
return result;
}
}
// ===== PRIORITY 2-3: BALD/FAST =====
bool hasBald = false;
bool hasFast = false;
bool hasHalb = false;
for (uint8_t i = 0; i < wordCount; i++) {
if (wordEquals(words[i], "BALD")) hasBald = true;
if (wordEquals(words[i], "FAST")) hasFast = true;
if (wordEquals(words[i], "HALB")) hasHalb = true;
}
if (hasFast || hasBald) {
if (hasHalb) {
// PRIORITY 2: BALD/FAST + HALB → remainder = target - mm, direction = right
bool dummy;
uint8_t target = getTargetMinute(words, wordCount, dummy);
remainder = (target > mm) ? (target - mm) : 0;
if (remainder > 4) remainder = 0;
isLeftDir = false;
} else {
// PRIORITY 3: BALD/FAST (no HALB, :57-59) → remainder = 60 - mm, direction = right
remainder = 60 - mm;
if (remainder > 4) remainder = 0;
isLeftDir = false;
}
result.direction = RIGHT;
result.count = remainder;
result.hex = ledCountToHex(remainder, isLeftDir);
return result;
}
// ===== PRIORITY 4-5: KURZ VOR =====
bool hasKurz = false;
bool hasVor = false;
bool hasZehn = false;
for (uint8_t i = 0; i < wordCount; i++) {
if (wordEquals(words[i], "KURZ")) hasKurz = true;
if (wordEquals(words[i], "VOR")) hasVor = true;
if (wordEquals(words[i], "ZEHN")) hasZehn = true;
}
if (hasKurz && hasVor) {
if (hasHalb) {
// PRIORITY 4: KURZ VOR HALB → remainder = target - mm, direction = right
bool dummy;
uint8_t target = getTargetMinute(words, wordCount, dummy);
remainder = (target > mm) ? (target - mm) : 0;
if (remainder > 4) remainder = 0;
} else {
// PRIORITY 5: KURZ VOR (no HALB, :58) → remainder = 60 - mm, direction = right
remainder = 60 - mm;
if (remainder > 4) remainder = 0;
}
isLeftDir = false;
result.direction = RIGHT;
result.count = remainder;
result.hex = ledCountToHex(remainder, isLeftDir);
return result;
}
// ===== PRIORITY 6-7: KURZ NACH =====
bool hasNach = false;
for (uint8_t i = 0; i < wordCount; i++) {
if (wordEquals(words[i], "NACH")) hasNach = true;
}
if (hasKurz && hasNach) {
if (hasHalb) {
// PRIORITY 7: KURZ NACH HALB → remainder = mm % 5, direction = left
remainder = mm % 5;
} else {
// PRIORITY 6: KURZ NACH (no HALB, :01-02) → remainder = mm % 5, direction = left
remainder = mm % 5;
}
isLeftDir = true;
result.direction = LEFT;
result.count = remainder;
result.hex = ledCountToHex(remainder, isLeftDir);
return result;
}
// ===== PRIORITY 8: NACH (no HALB) =====
if (hasNach && !hasHalb) {
bool dummy;
uint8_t target = getTargetMinute(words, wordCount, dummy);
remainder = (mm > target) ? (mm - target) : 0;
if (remainder > 4) remainder = 0;
isLeftDir = true;
result.direction = LEFT;
result.count = remainder;
result.hex = ledCountToHex(remainder, isLeftDir);
return result;
}
// ===== PRIORITY 9-10: HALB + NACH/VOR =====
if (hasHalb && hasNach) {
// PRIORITY 9: HALB + NACH → remainder = mm % 5, direction = left
remainder = mm % 5;
isLeftDir = true;
result.direction = LEFT;
result.count = remainder;
result.hex = ledCountToHex(remainder, isLeftDir);
return result;
}
// ===== PRIORITY 10b: SPECIAL CASE :16-:19 FALLBACK ZEHN VOR HALB =====
if (hasHalb && hasVor && hasZehn && mm >= 16 && mm <= 19) {
// Fallback scenario: :16-:19 with "ZEHN VOR HALB"
// LEDs = 20 - mm, direction = LEFT (additive towards :20)
remainder = 20 - mm;
if (remainder > 4) remainder = 0; // Safety
isLeftDir = true;
result.direction = LEFT;
result.count = remainder;
result.hex = ledCountToHex(remainder, isLeftDir);
return result;
}
// ===== PRIORITY 10c: SPECIAL CASE VIERTEL at :15-:19 (including fallback VIERTEL [next_hour]) =====
bool hasViertel = false;
for (uint8_t i = 0; i < wordCount; i++) {
if (wordEquals(words[i], "VIERTEL")) {
hasViertel = true;
break;
}
}
if (hasViertel && !hasHalb && mm >= 15 && mm <= 19) {
// :15-:19 VIERTEL (either "VIERTEL NACH [h]" or fallback "VIERTEL [next_hour]")
// Count minutes after :15
remainder = mm - 15; // 0, 1, 2, 3, 4 for :15-:19
isLeftDir = true;
result.direction = LEFT;
result.count = remainder;
result.hex = ledCountToHex(remainder, isLeftDir);
return result;
}
if (hasHalb && hasVor) {
// PRIORITY 10: HALB + VOR → remainder = mm % 5, direction = right
remainder = mm % 5;
isLeftDir = false;
result.direction = RIGHT;
result.count = remainder;
result.hex = ledCountToHex(remainder, isLeftDir);
return result;
}
// ===== PRIORITY 11: VOR (no HALB) =====
if (hasVor && !hasHalb) {
bool dummy;
uint8_t target = getTargetMinute(words, wordCount, dummy);
// Check for FÜNF word
bool hasFunf = false;
for (uint8_t i = 0; i < wordCount; i++) {
if (wordEquals(words[i], "FuNF")) hasFunf = true;
}
// ALL VOR (without HALB): Calculate distance to target minute
// If mm < target: still X minutes away → LEFT direction (additive)
// If mm >= target: already X minutes past → RIGHT direction (subtractive)
if (mm < target) {
// Haven't reached target yet: minutes remaining
remainder = target - mm;
isLeftDir = true;
result.direction = LEFT;
} else {
// Already past target: minutes since target
remainder = mm - target;
isLeftDir = false;
result.direction = RIGHT;
}
// Safety: cap at 4 LEDs
if (remainder > 4) remainder = 0;
result.count = remainder;
result.hex = ledCountToHex(remainder, isLeftDir);
return result;
}
// ===== DEFAULT: 0 LEDs =====
result.count = 0;
result.hex = 0x00;
return result;
}

View File

@ -0,0 +1,86 @@
/**
* CharGraph Time Logic - LED Calculator
*
* Port of lib/time-logic/led-calculator.ts
* Minute LED calculation with 11-tier priority system
*/
#ifndef CHARGRAPH_LED_CALCULATOR_H
#define CHARGRAPH_LED_CALCULATOR_H
#include "Types.h"
/**
* Get target minute for NACH/VOR-based LED calculation
*
* Returns the minute boundary that the current time counts towards or from.
* Examples:
* - FÜNF NACH :05 target :05
* - ZEHN NACH :10 target :10
* - VIERTEL NACH :15 target :15
* - ZEHN VOR :50 target :50 (for :40-:44)
* - FÜNF VOR :55 target :55 (for :50-:54)
* - VIERTEL VOR :45 target :45 (for :40-:44)
*
* @param words Array of word pointers
* @param wordCount Number of words
* @param isLeftDirection Returns true if additive (left/NACH), false if subtractive (right/VOR)
* @return Target minute value (0-59)
*/
uint8_t getTargetMinute(
const char* const* words,
uint8_t wordCount,
bool& isLeftDirection
);
/**
* Convert LED count and direction to 4-bit hex value
*
* LEFT direction (additive):
* - 0 LEDs 0x00
* - 1 LED 0x08 (bit 3)
* - 2 LEDs 0x0C (bits 3+2)
* - 3 LEDs 0x0E (bits 3+2+1)
* - 4 LEDs 0x0F (bits 3+2+1+0)
*
* RIGHT direction (subtractive):
* - 0 LEDs 0x00
* - 1 LED 0x01 (bit 0)
* - 2 LEDs 0x03 (bits 0+1)
* - 3 LEDs 0x07 (bits 0+1+2)
* - 4 LEDs 0x0F (bits 0+1+2+3)
*
* @param count LED count (0-4)
* @param isLeft true for left direction, false for right
* @return 4-bit hex value (0x00-0x0F)
*/
uint8_t ledCountToHex(uint8_t count, bool isLeft);
/**
* Calculate LED position and direction for given words and minute
*
* 11-Tier Priority System (from charMatrixV03.html):
* 1. DREIVIERTEL remainder = mm - 45, direction = left
* 2. BALD/FAST + HALB remainder = targetMinute - mm, direction = right
* 3. BALD/FAST (no HALB, :57-59) remainder = 60 - mm, direction = right
* 4. KURZ VOR + HALB remainder = targetMinute - mm, direction = right
* 5. KURZ VOR (no HALB, :58) remainder = 60 - mm, direction = right
* 6. KURZ NACH (no HALB, :01-02) remainder = mm - targetMinute, direction = left
* 7. KURZ NACH + HALB remainder = mm % 5, direction = left
* 8. NACH (no HALB) remainder = mm - targetMinute, direction = left
* 9. HALB + NACH remainder = mm % 5, direction = left
* 10. HALB + VOR remainder = mm % 5, direction = right
* 11. VOR (no HALB) remainder = |mm - targetMinute|, direction = right
*
* @param words Array of word pointers (PROGMEM strings)
* @param wordCount Number of words
* @param mm Current minute (0-59)
* @return LEDInfo with count, direction, and hex value
*/
LEDInfo calculateLEDs(
const char* const* words,
uint8_t wordCount,
uint8_t mm
);
#endif // CHARGRAPH_LED_CALCULATOR_H

View File

@ -0,0 +1,495 @@
/**
* CharGraph Time Logic - Minute Rules Implementation
*
* All 24 rules for minutes :00-:59
*/
#include "MinuteRules.h"
#include "Constants.h"
#include <cstring>
// ============================================================================
// INDIVIDUAL RULE HANDLERS
// ============================================================================
// :00 - Full hour (with or without UHR)
static uint8_t rule_00(const RuleContext& ctx, const char** outWords) {
if (ctx.hasUhrAtEnd) {
// Check if hour word starts with "EI" (EINS in PROGMEM)
// Must use pgm_read_byte() to safely read from PROGMEM
char first = pgm_read_byte(ctx.hourWord);
char second = pgm_read_byte(ctx.hourWord + 1);
outWords[0] = (first == 'E' && second == 'I') ? EIN : ctx.hourWord;
outWords[1] = UHR;
return 2;
}
outWords[0] = ctx.hourWord;
return 1;
}
// :01-:02 - KURZ NACH or NACH
static uint8_t rule_01_02(const RuleContext& ctx, const char** outWords) {
if (ctx.hasKurz) {
outWords[0] = KURZ;
outWords[1] = NACH;
outWords[2] = ctx.hourWord;
return 3;
}
outWords[0] = NACH;
outWords[1] = ctx.hourWord;
return 2;
}
// :03-:04 - NACH
static uint8_t rule_03_04(const RuleContext& ctx, const char** outWords) {
outWords[0] = NACH;
outWords[1] = ctx.hourWord;
return 2;
}
// :05-:09 - FÜNF NACH
static uint8_t rule_05_09(const RuleContext& ctx, const char** outWords) {
outWords[0] = FUENF;
outWords[1] = NACH;
outWords[2] = ctx.hourWord;
return 3;
}
// :10-:14 - ZEHN NACH
static uint8_t rule_10_14(const RuleContext& ctx, const char** outWords) {
outWords[0] = ZEHN;
outWords[1] = NACH;
outWords[2] = ctx.hourWord;
return 3;
}
// :15-:19 - VIERTEL NACH (with fallback support)
static uint8_t rule_15_19(const RuleContext& ctx, const char** outWords) {
if (ctx.fallbackLevel == 0) {
// Primary: VIERTEL NACH [h]
// BUT: Check if NACH comes BEFORE VIERTEL in pattern
// If so, VIERTEL NACH is not sequentially possible - fallback to VIERTEL [h+1]
int16_t nach_pos = -1;
int16_t viertel_pos = -1;
if (ctx.gridStr) {
// Find positions in pattern
char nach_buf[5]; // NACH = 4 chars + null terminator
strcpy_P(nach_buf, NACH);
const char* nach_ptr = strstr(ctx.gridStr, nach_buf);
if (nach_ptr) nach_pos = nach_ptr - ctx.gridStr;
char viertel_buf[8]; // VIERTEL = 7 chars + null terminator
strcpy_P(viertel_buf, VIERTEL);
const char* viertel_ptr = strstr(ctx.gridStr, viertel_buf);
if (viertel_ptr) viertel_pos = viertel_ptr - ctx.gridStr;
}
// If NACH comes BEFORE VIERTEL, use next hour instead
if (nach_pos != -1 && viertel_pos != -1 && nach_pos < viertel_pos) {
// Fallback: VIERTEL [next_hour]
uint8_t next_h12 = (ctx.h12 % 12) + 1;
const char* next_hour_word = getHourWord(next_h12);
outWords[0] = VIERTEL;
outWords[1] = next_hour_word;
return 2;
} else {
// Normal case: VIERTEL NACH [h]
outWords[0] = VIERTEL;
outWords[1] = NACH;
outWords[2] = ctx.hourWord;
return 3;
}
}
else if (ctx.fallbackLevel == 1) {
// Secondary Fallback: VIERTEL [h+1] (when NACH not usable)
uint8_t next_h12 = (ctx.h12 % 12) + 1;
const char* next_hour_word = getHourWord(next_h12);
outWords[0] = VIERTEL;
outWords[1] = next_hour_word;
return 2;
}
// Tertiary fallback (should not reach here)
else if (ctx.mm >= 16 && ctx.mm <= 19) {
// :16-:19 Fallback: ZEHN VOR HALB [h+1]
uint8_t next_h12 = (ctx.h12 % 12) + 1;
const char* next_hour_word = getHourWord(next_h12);
outWords[0] = ZEHN;
outWords[1] = VOR;
outWords[2] = HALB;
outWords[3] = next_hour_word;
return 4;
}
// Default (should not be reached)
return 0;
}
// :20-:24 - ZWANZIG NACH or ZEHN VOR HALB (with fallback support)
static uint8_t rule_20_24(const RuleContext& ctx, const char** outWords) {
if (ctx.fallbackLevel == 0 && ctx.hasZwanzig) {
// Primary: ZWANZIG NACH [h]
outWords[0] = ZWANZIG;
outWords[1] = NACH;
outWords[2] = ctx.hourWord;
return 3;
}
// Fallback: ZEHN VOR HALB [h+1]
outWords[0] = ZEHN;
outWords[1] = VOR;
outWords[2] = HALB;
outWords[3] = ctx.hourWord;
return 4;
}
// :25-:26 - FÜNF VOR HALB
static uint8_t rule_25_26(const RuleContext& ctx, const char** outWords) {
outWords[0] = FUENF;
outWords[1] = VOR;
outWords[2] = HALB;
outWords[3] = ctx.hourWord;
return 4;
}
// :27 - BALD HALB or FÜNF VOR HALB
static uint8_t rule_27(const RuleContext& ctx, const char** outWords) {
if (ctx.hasBald) {
outWords[0] = BALD;
outWords[1] = HALB;
outWords[2] = ctx.hourWord;
return 3;
}
outWords[0] = FUENF;
outWords[1] = VOR;
outWords[2] = HALB;
outWords[3] = ctx.hourWord;
return 4;
}
// :28 - KURZ VOR HALB > BALD HALB
static uint8_t rule_28(const RuleContext& ctx, const char** outWords) {
if (ctx.hasKurz) {
outWords[0] = KURZ;
outWords[1] = VOR;
outWords[2] = HALB;
outWords[3] = ctx.hourWord;
return 4;
}
if (ctx.hasBald) {
outWords[0] = BALD;
outWords[1] = HALB;
outWords[2] = ctx.hourWord;
return 3;
}
outWords[0] = FUENF;
outWords[1] = VOR;
outWords[2] = HALB;
outWords[3] = ctx.hourWord;
return 4;
}
// :29 - FAST > KURZ > BALD
static uint8_t rule_29(const RuleContext& ctx, const char** outWords) {
if (ctx.hasFast) {
outWords[0] = FAST;
outWords[1] = HALB;
outWords[2] = ctx.hourWord;
return 3;
}
if (ctx.hasKurz) {
outWords[0] = KURZ;
outWords[1] = VOR;
outWords[2] = HALB;
outWords[3] = ctx.hourWord;
return 4;
}
if (ctx.hasBald) {
outWords[0] = BALD;
outWords[1] = HALB;
outWords[2] = ctx.hourWord;
return 3;
}
outWords[0] = FUENF;
outWords[1] = VOR;
outWords[2] = HALB;
outWords[3] = ctx.hourWord;
return 4;
}
// :30 - HALB
static uint8_t rule_30(const RuleContext& ctx, const char** outWords) {
outWords[0] = HALB;
outWords[1] = ctx.hourWord;
return 2;
}
// :31-:32 - KURZ NACH HALB or NACH HALB
static uint8_t rule_31_32(const RuleContext& ctx, const char** outWords) {
if (ctx.hasKurz) {
outWords[0] = KURZ;
outWords[1] = NACH;
outWords[2] = HALB;
outWords[3] = ctx.hourWord;
return 4;
}
outWords[0] = NACH;
outWords[1] = HALB;
outWords[2] = ctx.hourWord;
return 3;
}
// :33-:34 - NACH HALB
static uint8_t rule_33_34(const RuleContext& ctx, const char** outWords) {
outWords[0] = NACH;
outWords[1] = HALB;
outWords[2] = ctx.hourWord;
return 3;
}
// :35-:39 - FÜNF NACH HALB
static uint8_t rule_35_39(const RuleContext& ctx, const char** outWords) {
outWords[0] = FUENF;
outWords[1] = NACH;
outWords[2] = HALB;
outWords[3] = ctx.hourWord;
return 4;
}
// :40-:44 - ZEHN NACH HALB or ZWANZIG VOR (with fallback support)
static uint8_t rule_40_44(const RuleContext& ctx, const char** outWords) {
if (ctx.fallbackLevel == 0 && ctx.hasZwanzig) {
// Primary: ZWANZIG VOR [h]
outWords[0] = ZWANZIG;
outWords[1] = VOR;
outWords[2] = ctx.hourWord;
return 3;
}
// Fallback: ZEHN NACH HALB [h]
outWords[0] = ZEHN;
outWords[1] = NACH;
outWords[2] = HALB;
outWords[3] = ctx.hourWord;
return 4;
}
// :45 - DREIVIERTEL or VIERTEL VOR (pattern validation ensures one is always possible)
static uint8_t rule_45(const RuleContext& ctx, const char** outWords) {
// DREIVIERTEL always takes priority if present, REGARDLESS of fallback level
// This is critical: DREIVIERTEL must be returned on ALL attempts
if (ctx.hasDreiviertel) {
outWords[0] = DREIVIERTEL;
outWords[1] = ctx.hourWord;
return 2;
}
// Fallback: VIERTEL VOR [h]
// This is only reached if DREIVIERTEL is NOT in the pattern
// Pattern validation guarantees that if DREIVIERTEL is missing,
// then VIERTEL must come BEFORE VOR (enabling VIERTEL VOR sequence)
outWords[0] = VIERTEL;
outWords[1] = VOR;
outWords[2] = ctx.hourWord;
return 3;
}
// :46-:47 - VIERTEL VOR (with fallback support)
static uint8_t rule_46_47(const RuleContext& ctx, const char** outWords) {
if (ctx.fallbackLevel == 0) {
// Primary: VIERTEL VOR [h]
outWords[0] = VIERTEL;
outWords[1] = VOR;
outWords[2] = ctx.hourWord;
return 3;
}
// Fallback: ZEHN VOR [h]
outWords[0] = ZEHN;
outWords[1] = VOR;
outWords[2] = ctx.hourWord;
return 3;
}
// :48-:49 - ZEHN VOR (target 50, LED from left/added)
static uint8_t rule_48_49(const RuleContext& ctx, const char** outWords) {
outWords[0] = ZEHN;
outWords[1] = VOR;
outWords[2] = ctx.hourWord;
return 3;
}
// :50-:52 - ZEHN VOR
static uint8_t rule_50_52(const RuleContext& ctx, const char** outWords) {
outWords[0] = ZEHN;
outWords[1] = VOR;
outWords[2] = ctx.hourWord;
return 3;
}
// :53-:54 - FÜNF VOR (with LEFT direction for clarity: 5+2=7, 5+1=6)
static uint8_t rule_53_54(const RuleContext& ctx, const char** outWords) {
outWords[0] = FUENF;
outWords[1] = VOR;
outWords[2] = ctx.hourWord;
return 3;
}
// :55-:56 - FÜNF VOR
static uint8_t rule_55_56(const RuleContext& ctx, const char** outWords) {
outWords[0] = FUENF;
outWords[1] = VOR;
outWords[2] = ctx.hourWord;
return 3;
}
// :57 - BALD [HOUR] or FÜNF VOR
static uint8_t rule_57(const RuleContext& ctx, const char** outWords) {
if (ctx.hasBald) {
outWords[0] = BALD;
outWords[1] = ctx.hourWord;
return 2;
}
outWords[0] = FUENF;
outWords[1] = VOR;
outWords[2] = ctx.hourWord;
return 3;
}
// :58 - KURZ VOR [HOUR] > BALD
static uint8_t rule_58(const RuleContext& ctx, const char** outWords) {
if (ctx.hasKurz) {
outWords[0] = KURZ;
outWords[1] = VOR;
outWords[2] = ctx.hourWord;
return 3;
}
if (ctx.hasBald) {
outWords[0] = BALD;
outWords[1] = ctx.hourWord;
return 2;
}
outWords[0] = FUENF;
outWords[1] = VOR;
outWords[2] = ctx.hourWord;
return 3;
}
// :59 - FAST [HOUR] > KURZ VOR > BALD
static uint8_t rule_59(const RuleContext& ctx, const char** outWords) {
if (ctx.hasFast) {
outWords[0] = FAST;
outWords[1] = ctx.hourWord;
return 2;
}
if (ctx.hasKurz) {
outWords[0] = KURZ;
outWords[1] = VOR;
outWords[2] = ctx.hourWord;
return 3;
}
if (ctx.hasBald) {
outWords[0] = BALD;
outWords[1] = ctx.hourWord;
return 2;
}
outWords[0] = FUENF;
outWords[1] = VOR;
outWords[2] = ctx.hourWord;
return 3;
}
// ============================================================================
// EXECUTE MINUTE RULE
// ============================================================================
uint8_t executeMinuteRule(uint8_t minute, const RuleContext& ctx, const char** outWords) {
switch (minute) {
case 0:
return rule_00(ctx, outWords);
case 1:
case 2:
return rule_01_02(ctx, outWords);
case 3:
case 4:
return rule_03_04(ctx, outWords);
case 5:
case 6:
case 7:
case 8:
case 9:
return rule_05_09(ctx, outWords);
case 10:
case 11:
case 12:
case 13:
case 14:
return rule_10_14(ctx, outWords);
case 15:
case 16:
case 17:
case 18:
case 19:
return rule_15_19(ctx, outWords);
case 20:
case 21:
case 22:
case 23:
case 24:
return rule_20_24(ctx, outWords);
case 25:
case 26:
return rule_25_26(ctx, outWords);
case 27:
return rule_27(ctx, outWords);
case 28:
return rule_28(ctx, outWords);
case 29:
return rule_29(ctx, outWords);
case 30:
return rule_30(ctx, outWords);
case 31:
case 32:
return rule_31_32(ctx, outWords);
case 33:
case 34:
return rule_33_34(ctx, outWords);
case 35:
case 36:
case 37:
case 38:
case 39:
return rule_35_39(ctx, outWords);
case 40:
case 41:
case 42:
case 43:
case 44:
return rule_40_44(ctx, outWords);
case 45:
return rule_45(ctx, outWords);
case 46:
case 47:
return rule_46_47(ctx, outWords);
case 48:
case 49:
return rule_48_49(ctx, outWords);
case 50:
case 51:
case 52:
return rule_50_52(ctx, outWords);
case 53:
case 54:
return rule_53_54(ctx, outWords);
case 55:
case 56:
return rule_55_56(ctx, outWords);
case 57:
return rule_57(ctx, outWords);
case 58:
return rule_58(ctx, outWords);
case 59:
return rule_59(ctx, outWords);
default:
outWords[0] = nullptr;
return 0;
}
}

View File

@ -0,0 +1,33 @@
/**
* CharGraph Time Logic - Minute Rules
*
* Port of lib/time-logic/minute-rules.ts
* 24 rules for minutes :00-:59 with handler functions
*/
#ifndef CHARGRAPH_MINUTE_RULES_H
#define CHARGRAPH_MINUTE_RULES_H
#include "Types.h"
/**
* Minute rule handler function type
* Called with RuleContext and returns array of word pointers
*
* @param ctx Rule context with modifiers and grid
* @param outWords Output array for result words (max 6 words)
* @return Number of words in output
*/
typedef uint8_t (*MinuteRuleHandler)(const RuleContext& ctx, const char** outWords);
/**
* Find and execute the minute rule for a given minute
*
* @param minute Minute value (0-59)
* @param ctx Rule context with pattern and modifiers
* @param outWords Output array for result words
* @return Number of words returned by handler
*/
uint8_t executeMinuteRule(uint8_t minute, const RuleContext& ctx, const char** outWords);
#endif // CHARGRAPH_MINUTE_RULES_H

View File

@ -0,0 +1,67 @@
/**
* CharGraph Time Logic - Data Types
*
* TypeScriptC++ Port of lib/time-logic/types.ts
* Optimized for ESP8266 with minimal memory footprint
*/
#ifndef CHARGRAPH_TYPES_H
#define CHARGRAPH_TYPES_H
#include <Arduino.h>
// ============================================================================
// VALIDATION RESULT
// ============================================================================
struct ValidationResult {
bool valid;
const char* reason; // Points to PROGMEM string
};
// ============================================================================
// MINUTE RULE CONTEXT
// ============================================================================
struct RuleContext {
uint8_t mm; // Minute (0-59)
uint8_t h12; // Hour 12-format (0-11)
const char* hourWord; // Points to PROGMEM hour word
bool hasUhrAtEnd; // UHR present at pattern end
// Optional modifiers
bool hasKurz; // Valid at :01-02, :28, :31-32, :58, :59
bool hasBald; // Valid at :27-29, :57-59
bool hasFast; // Valid at :29, :59
bool hasZwanzig; // Alternative for :20-24, :40-44
bool hasDreiviertel; // Alternative for :45
bool hasNacht; // Night indicator (optional)
const char* gridStr; // Pattern (110 chars)
// Fallback control (NEW)
uint8_t fallbackLevel; // 0 = primary, 1+ = fallback levels
};
// ============================================================================
// LED INFO
// ============================================================================
struct LEDInfo {
uint8_t count; // 0-4 LEDs
const char* direction; // "left" or "right" (PROGMEM)
uint8_t hex; // 4-bit value (0x00-0x0F)
};
// ============================================================================
// TIME WORDS RESPONSE
// ============================================================================
struct TimeWordsResponse {
const char** words; // Array of word pointers (PROGMEM)
uint8_t wordCount; // Number of words (max 10)
LEDInfo ledInfo;
uint8_t ledHex; // Same as ledInfo.hex
};
#endif // CHARGRAPH_TYPES_H

View File

@ -0,0 +1,337 @@
/**
* CharGraph Time Logic - Validator Implementation
*/
#include "Validator.h"
#include "Constants.h"
// ============================================================================
// HELPER: Check if gap exists between two positions
// ============================================================================
static bool hasGap(uint16_t endIdxPrev, uint16_t startIdxNext) {
const uint8_t rowPrev = endIdxPrev / GRID_COLS;
const uint8_t rowNext = startIdxNext / GRID_COLS;
// Gap exists if: different row OR gap in same row (>1 char distance)
return rowNext > rowPrev || startIdxNext > endIdxPrev + 1;
}
// ============================================================================
// HELPER: Find string in PROGMEM pattern
// ============================================================================
static int16_t findWord(const char* pattern, const char* word_progmem, uint16_t searchStart) {
if (!pattern || !word_progmem) return -1;
// Load word from PROGMEM into buffer (max 11 chars: DREIVIERTEL)
char wordBuf[12]; // 11 chars + null terminator
strcpy_P(wordBuf, word_progmem);
// Search from searchStart
const char* result = strstr(pattern + searchStart, wordBuf);
if (result) {
return (result - pattern);
}
return -1;
}
// ============================================================================
// HELPER: Check if PROGMEM string contains uppercase letters
// ============================================================================
static bool containsUppercase(const char* pattern, uint16_t startPos) {
if (!pattern) return false;
for (uint16_t i = startPos; pattern[i] != '\0'; i++) {
if (pattern[i] >= 'A' && pattern[i] <= 'Z') {
return true;
}
}
return false;
}
// ============================================================================
// WORD INTEGRITY: Check if word fits in line
// ============================================================================
bool wordFitsInLine(uint16_t startPos, uint8_t wordLen) {
const uint8_t startRow = startPos / GRID_COLS;
const uint8_t endRow = (startPos + wordLen - 1) / GRID_COLS;
// Word fits if start and end are in same row
return startRow == endRow;
}
// ============================================================================
// STRUCTURE VALIDATION (with Word Integrity Check)
// ============================================================================
ValidationResult validateStructure(const char* gridStr) {
if (!gridStr) {
return {false, ERR_NO_ES};
}
// ========== INTEGRITY CHECK: Words must not wrap over line boundaries ==========
// Find entry words
int16_t esPos = findWord(gridStr, ES, 0);
int16_t wirPos = findWord(gridStr, WIR, 0);
int16_t istPos = findWord(gridStr, IST, 0);
int16_t habenPos = findWord(gridStr, HABEN, 0);
int16_t halbPos = findWord(gridStr, HALB, 0);
int16_t uhrPos = findWord(gridStr, UHR, 0);
// Check ES integrity
if (esPos != -1 && !wordFitsInLine(esPos, 2)) { // ES = 2 chars
return {false, ERR_NO_ES}; // Generic error - word integrity issue
}
// Check IST integrity
if (istPos != -1 && !wordFitsInLine(istPos, 3)) { // IST = 3 chars
return {false, ERR_NO_IST};
}
// Check WIR integrity
if (wirPos != -1 && !wordFitsInLine(wirPos, 3)) { // WIR = 3 chars
return {false, ERR_NO_WIR};
}
// Check HABEN integrity
if (habenPos != -1 && !wordFitsInLine(habenPos, 5)) { // HABEN = 5 chars
return {false, ERR_NO_HABEN};
}
// Check HALB integrity
if (halbPos != -1 && !wordFitsInLine(halbPos, 4)) { // HALB = 4 chars
return {false, ERR_NO_HALB};
}
// Check UHR integrity
if (uhrPos != -1 && !wordFitsInLine(uhrPos, 3)) { // UHR = 3 chars
return {false, ERR_UHR_NOT_LAST};
}
// ========== INTRO WORDS VALIDATION ==========
const bool useAlternative = (esPos == -1 && wirPos != -1);
if (useAlternative) {
// WIR HABEN variant
if (wirPos == -1) {
return {false, ERR_NO_WIR};
}
if (habenPos == -1) {
return {false, ERR_NO_HABEN};
}
if (!hasGap(wirPos + 2, habenPos)) {
return {false, ERR_NO_GAP_WIR_HABEN};
}
} else {
// ES IST variant (standard)
if (esPos == -1) {
return {false, ERR_NO_ES};
}
if (istPos == -1) {
return {false, ERR_NO_IST};
}
if (!hasGap(esPos + 1, istPos)) {
return {false, ERR_NO_GAP_ES_IST};
}
}
// ========== HALB VALIDATION ==========
if (halbPos == -1) {
return {false, ERR_NO_HALB};
}
// ========== UHR VALIDATION ==========
if (uhrPos != -1) {
// UHR is present - check if at end
// After UHR, only placeholders allowed (no uppercase letters)
if (containsUppercase(gridStr, uhrPos + 3)) {
return {false, ERR_UHR_NOT_LAST};
}
}
return {true, nullptr};
}
// ============================================================================
// MANDATORY WORDS VALIDATION (with Word Integrity Check)
// ============================================================================
ValidationResult validateMandatoryWords(const char* gridStr) {
if (!gridStr) {
return {false, ERR_NO_FUENF};
}
// ========== INTEGRITY CHECK: Minute words must not wrap over line boundaries ==========
// Check FÜNF (mandatory for minute display)
int16_t fuenfPos = findWord(gridStr, FUENF, 0);
if (fuenfPos == -1) {
return {false, ERR_NO_FUENF};
}
if (!wordFitsInLine(fuenfPos, 4)) { // FÜNF = 4 chars
return {false, ERR_NO_FUENF};
}
// Check ZEHN (mandatory for minute display)
int16_t zehnPos = findWord(gridStr, ZEHN, 0);
if (zehnPos == -1) {
return {false, ERR_NO_ZEHN};
}
if (!wordFitsInLine(zehnPos, 4)) { // ZEHN = 4 chars
return {false, ERR_NO_ZEHN};
}
// Check VIERTEL (mandatory)
int16_t viertelPos = findWord(gridStr, VIERTEL, 0);
if (viertelPos == -1) {
return {false, ERR_NO_VIERTEL};
}
if (!wordFitsInLine(viertelPos, 7)) { // VIERTEL = 7 chars
return {false, ERR_NO_VIERTEL};
}
// Check VOR (mandatory)
int16_t vorPos = findWord(gridStr, VOR, 0);
if (vorPos == -1) {
return {false, ERR_NO_VOR};
}
if (!wordFitsInLine(vorPos, 3)) { // VOR = 3 chars
return {false, ERR_NO_VOR};
}
// Check NACH (mandatory)
int16_t nachPos = findWord(gridStr, NACH, 0);
if (nachPos == -1) {
return {false, ERR_NO_NACH};
}
if (!wordFitsInLine(nachPos, 4)) { // NACH = 4 chars
return {false, ERR_NO_NACH};
}
// ========== GAP VALIDATION ==========
// Check if DREIVIERTEL is present (must check BEFORE VIERTEL gap validation)
int16_t dreiviertelPos = findWord(gridStr, DREIVIERTEL, 0);
bool hasDreiviertel = (dreiviertelPos != -1);
// Check gap between VIERTEL and VOR
// BUT: Skip this check if DREIVIERTEL is present (VIERTEL is substring of DREIVIERTEL)
if (!hasDreiviertel && !hasGap(viertelPos + 6, vorPos)) {
return {false, ERR_NO_GAP_VIERTEL_VOR};
}
// Check gap between VIERTEL and NACH
// BUT: Skip this check if DREIVIERTEL is present (VIERTEL is substring of DREIVIERTEL)
if (!hasDreiviertel && !hasGap(viertelPos + 6, nachPos)) {
return {false, ERR_NO_GAP_VIERTEL_NACH};
}
// ========== CRITICAL SEQUENCE VALIDATION for :45 ==========
// :45 requires EITHER DREIVIERTEL OR the sequence VIERTEL VOR (in that order)
// If DREIVIERTEL is NOT present, VIERTEL MUST come BEFORE VOR
// Otherwise :45 cannot be displayed (e.g., "quarter to X" can't be formed)
if (!hasDreiviertel && viertelPos > vorPos) {
// VIERTEL comes AFTER VOR → can't form VIERTEL VOR sequence
// And DREIVIERTEL is missing → can't display :45 at all
return {false, ERR_NO_VIERTEL_SEQUENCE};
}
return {true, nullptr};
}
// ============================================================================
// OPTIONAL WORDS VALIDATION (INFO ONLY, NO ERROR)
// ============================================================================
ValidationResult validateOptionalWord(
const char* gridStr,
const char* optionalWord,
int16_t istPos
) {
if (!gridStr || !optionalWord || istPos == -1) {
return {true, nullptr}; // No error, just skip
}
// Find optional word in pattern
int16_t optWordPos = findWord(gridStr, optionalWord, 0);
if (optWordPos == -1) {
// Optional word not present - that's OK, no warning
return {true, nullptr};
}
// Optional word is present - check gap after IST/HABEN
// IST is 3 chars, HABEN is 5 chars
int16_t gapStart = istPos + 3; // Assuming IST (3 chars)
// Check if there's a gap (should be different row or at least 1 char distance)
if (!hasGap(gapStart - 1, optWordPos)) {
// No gap - return warning based on which word
if (wordEquals(optionalWord, "NACHT")) {
return {true, WARN_NO_GAP_NACHT};
} else if (wordEquals(optionalWord, "ZEIT")) {
return {true, WARN_NO_GAP_ZEIT};
} else if (wordEquals(optionalWord, "ALARM")) {
return {true, WARN_NO_GAP_ALARM};
} else if (wordEquals(optionalWord, "PAUSE")) {
return {true, WARN_NO_GAP_PAUSE};
} else if (wordEquals(optionalWord, "RWD")) {
return {true, WARN_NO_GAP_RWD};
}
}
return {true, nullptr};
}
// ============================================================================
// WORD SEQUENCE VALIDATION
// ============================================================================
ValidationResult validateWordSequence(
const char* const* words,
uint8_t wordCount,
const char* gridStr
) {
if (!words || wordCount == 0) {
return {false, ERR_NO_WORDS};
}
// Find all words sequentially
uint16_t searchStart = 0;
uint16_t positions[10]; // Max 10 words
uint16_t positionEnds[10];
for (uint8_t i = 0; i < wordCount; i++) {
int16_t pos = findWord(gridStr, words[i], searchStart);
if (pos == -1) {
return {false, ERR_WORD_NOT_FOUND};
}
// Load word from PROGMEM to get length
char wordBuf[12]; // Max 11 chars (DREIVIERTEL) + null terminator
strcpy_P(wordBuf, words[i]);
uint8_t wordLen = strlen(wordBuf);
positions[i] = pos;
positionEnds[i] = pos + wordLen - 1;
// Next search starts after this word
searchStart = pos + wordLen;
}
// Check gaps between words
for (uint8_t i = 0; i < wordCount - 1; i++) {
const uint16_t currentEnd = positionEnds[i];
const uint16_t nextStart = positions[i + 1];
const uint8_t currentRow = currentEnd / GRID_COLS;
const uint8_t nextRow = nextStart / GRID_COLS;
// If same row, must have gap (placeholder)
if (currentRow == nextRow && nextStart == currentEnd + 1) {
return {false, ERR_NO_GAP};
}
}
return {true, nullptr};
}

View File

@ -0,0 +1,88 @@
/**
* CharGraph Time Logic - Validator
*
* Port of lib/time-logic/validator.ts
* Pattern structure validation and word sequence validation
*/
#ifndef CHARGRAPH_VALIDATOR_H
#define CHARGRAPH_VALIDATOR_H
#include "Types.h"
/**
* Check if word fits completely in one 11-character line (no wrap)
*
* @param startPos Starting position of word
* @param wordLen Length of word
* @return true if word fits in line, false if wraps over boundary
*/
bool wordFitsInLine(uint16_t startPos, uint8_t wordLen);
/**
* Validate pattern structure (entry words + word integrity)
*
* Checks:
* - All words fit completely in one 11-character line (no wrap!)
* - ES/IST or WIR/HABEN with gap between
* - HALB is required
* - UHR is optional, but if present must be at pattern end
*
* @param gridStr 110-character pattern (uppercase)
* @return ValidationResult with valid flag and error message
*/
ValidationResult validateStructure(const char* gridStr);
/**
* Validate mandatory words presence and gaps
*
* Checks all mandatory words that MUST be in every pattern:
* - FÜNF (mandatory for minute display)
* - ZEHN (mandatory for minute display)
* - VIERTEL (mandatory, must have gap before VOR/NACH)
* - VOR (mandatory)
* - NACH (mandatory)
* - Gaps between VIERTEL and VOR, VIERTEL and NACH
*
* @param gridStr 110-character pattern (uppercase)
* @return ValidationResult with valid flag and error message
*/
ValidationResult validateMandatoryWords(const char* gridStr);
/**
* Validate optional words presence and gaps
*
* Info function (no error, just warning) to check optional words:
* - NACHT, ZEIT, ALARM, PAUSE, RWD
* - Must have gap between IST/HABEN and optional word
*
* @param gridStr 110-character pattern (uppercase)
* @param optionalWord PROGMEM pointer to optional word to check (e.g. NACHT)
* @param istPos Position of IST or HABEN in pattern
* @return ValidationResult with valid flag and optional warning message
*/
ValidationResult validateOptionalWord(
const char* gridStr,
const char* optionalWord,
int16_t istPos
);
/**
* Validate word sequence in pattern
*
* Checks:
* - All words found sequentially in pattern
* - Gaps/placeholders between words in same row
*
* @param words Array of word pointers (PROGMEM strings)
* @param wordCount Number of words
* @param gridStr 110-character pattern
* @return ValidationResult with valid flag
*/
ValidationResult validateWordSequence(
const char* const* words,
uint8_t wordCount,
const char* gridStr
);
#endif // CHARGRAPH_VALIDATOR_H

View File

@ -0,0 +1,213 @@
/**
* CharGraph Time Logic - Word Matcher Implementation
*/
#include "WordMatcher.h"
#include "Constants.h"
#include "MinuteRules.h"
#include "LEDCalculator.h"
#include "Validator.h"
#include <cstring>
// ============================================================================
// HELPER: Find string in pattern (case-sensitive)
// ============================================================================
static int16_t findWord(const char* pattern, const char* word_progmem) {
if (!pattern || !word_progmem) return -1;
char wordBuf[12]; // Max 11 chars (DREIVIERTEL) + null terminator
strcpy_P(wordBuf, word_progmem);
const char* result = strstr(pattern, wordBuf);
if (result) {
return (result - pattern);
}
return -1;
}
// ============================================================================
// HELPER: Check if PROGMEM string is in pattern
// ============================================================================
static bool hasWord(const char* pattern, const char* word_progmem) {
return findWord(pattern, word_progmem) != -1;
}
// ============================================================================
// MAIN FUNCTION: GET WORDS FOR TIME
// ============================================================================
uint8_t getWordsForTime(
const char* pattern,
uint8_t hour,
uint8_t minute,
const char** outWords,
LEDInfo& outLedInfo
) {
if (!pattern || !outWords) {
outLedInfo = {0, LEFT, 0x00};
return 0;
}
// ========== STEP 1: Recognize modifiers from pattern ==========
bool hasKurz = hasWord(pattern, KURZ);
bool hasBald = hasWord(pattern, BALD);
bool hasFast = hasWord(pattern, FAST);
bool hasZwanzig = hasWord(pattern, ZWANZIG);
bool hasDreiviertel = hasWord(pattern, DREIVIERTEL);
bool hasNacht = hasWord(pattern, NACHT);
// ========== STEP 2: Detect alternative intro (WIR/HABEN vs ES/IST) ==========
bool hasWir = hasWord(pattern, WIR);
bool hasHaben = hasWord(pattern, HABEN);
bool useAlternative = hasWir && hasHaben;
uint8_t wordIdx = 0;
if (useAlternative) {
outWords[wordIdx++] = WIR;
outWords[wordIdx++] = HABEN;
} else {
outWords[wordIdx++] = ES;
outWords[wordIdx++] = IST;
}
// ========== STEP 3: Check for UHR at pattern end ==========
int16_t uhrPos = findWord(pattern, UHR);
bool hasUhrAtEnd = false;
if (uhrPos != -1) {
// Check if UHR is truly at the end (only placeholders after)
bool afterUhrEmpty = true;
for (uint16_t i = uhrPos + 3; pattern[i] != '\0'; i++) {
if (pattern[i] >= 'A' && pattern[i] <= 'Z') {
afterUhrEmpty = false;
break;
}
}
hasUhrAtEnd = afterUhrEmpty;
}
// ========== STEP 4: Calculate display hour (advance at mm >= 20) ==========
uint8_t h12 = hour % 12;
if (minute >= 20) {
h12 = (h12 + 1) % 12;
}
const char* hourWord = getHourWord(h12);
// ========== STEP 4b: SPECIAL CASE - NACHT (:00-:04 at hour 0 only) ==========
// If NACHT is present and it's midnight (hour 0) and minute is 0-4, return only intro + NACHT (no minute words)
if (hasNacht && hour == 0 && minute <= 4) {
outWords[wordIdx++] = useAlternative ? WIR : ES;
outWords[wordIdx++] = useAlternative ? HABEN : IST;
outWords[wordIdx++] = NACHT;
// Validate NACHT sequence
ValidationResult nachtValidation = validateWordSequence(outWords, 3, pattern);
if (!nachtValidation.valid) {
outLedInfo = {0, LEFT, 0x00};
return 0;
}
// Calculate LEDs (should be 0 for NACHT at :00-:04)
outLedInfo = calculateLEDs(outWords, 3, minute);
return 3;
}
// ========== STEP 5: Apply minute rule handler ==========
RuleContext ctx;
ctx.mm = minute;
ctx.h12 = h12;
ctx.hourWord = hourWord;
ctx.hasUhrAtEnd = hasUhrAtEnd;
ctx.hasKurz = hasKurz;
ctx.hasBald = hasBald;
ctx.hasFast = hasFast;
ctx.hasZwanzig = hasZwanzig;
ctx.hasDreiviertel = hasDreiviertel;
ctx.hasNacht = hasNacht;
ctx.gridStr = pattern;
ctx.fallbackLevel = 0; // Initialize with 0 (primary)
const char* minuteWords[6];
uint8_t minuteWordCount = executeMinuteRule(minute, ctx, minuteWords);
if (minuteWordCount == 0) {
outLedInfo = {0, LEFT, 0x00};
return 0;
}
// Add minute words to output
for (uint8_t i = 0; i < minuteWordCount; i++) {
outWords[wordIdx++] = minuteWords[i];
}
uint8_t totalWords = wordIdx;
// ========== STEP 6: Validate word sequence ==========
ValidationResult validation = validateWordSequence(outWords, totalWords, pattern);
// ========== STEP 7: Multi-Level Fallback if validation fails ==========
if (!validation.valid) {
bool fallbackSuccess = false;
// Try up to 3 fallback levels
for (uint8_t fbLevel = 1; fbLevel <= 3 && !fallbackSuccess; fbLevel++) {
RuleContext ctxFallback = ctx;
// Level 1: Remove modifiers (existing logic)
if (fbLevel == 1 && (hasKurz || hasBald || hasFast)) {
ctxFallback.hasKurz = false;
ctxFallback.hasBald = false;
ctxFallback.hasFast = false;
ctxFallback.fallbackLevel = 0; // Still use primary rule
}
// Level 2: Use rule's first fallback alternative
else if (fbLevel == 2) {
ctxFallback.fallbackLevel = 1; // Signal first fallback
}
// Level 3: Use rule's second fallback (rare)
else if (fbLevel == 3) {
ctxFallback.fallbackLevel = 2;
}
else {
continue; // Skip this level
}
// Execute minute rule with fallback context
const char* minuteWordsFallback[6];
uint8_t minuteWordCountFallback = executeMinuteRule(minute, ctxFallback, minuteWordsFallback);
if (minuteWordCountFallback > 0) {
// Rebuild full word list
wordIdx = 0;
if (useAlternative) {
outWords[wordIdx++] = WIR;
outWords[wordIdx++] = HABEN;
} else {
outWords[wordIdx++] = ES;
outWords[wordIdx++] = IST;
}
for (uint8_t i = 0; i < minuteWordCountFallback; i++) {
outWords[wordIdx++] = minuteWordsFallback[i];
}
totalWords = wordIdx;
// Re-validate
ValidationResult validationFallback = validateWordSequence(outWords, totalWords, pattern);
if (validationFallback.valid) {
validation = validationFallback;
fallbackSuccess = true;
}
}
}
}
// ========== STEP 8: Calculate LED info ==========
outLedInfo = calculateLEDs(outWords, totalWords, minute);
return totalWords;
}

View File

@ -0,0 +1,40 @@
/**
* CharGraph Time Logic - Word Matcher
*
* Port of lib/time-logic/word-matcher.ts
* Main orchestration: getWordsForTime() function
*/
#ifndef CHARGRAPH_WORD_MATCHER_H
#define CHARGRAPH_WORD_MATCHER_H
#include "Types.h"
/**
* Convert time (hour:minute) to word sequence
*
* Main algorithm:
* 1. Recognize modifiers from pattern (KURZ, BALD, FAST, ZWANZIG, DREIVIERTEL, NACHT)
* 2. Detect alternative intro (WIR/HABEN vs ES/IST)
* 3. Check for UHR at pattern end
* 4. Calculate display hour (advance at mm >= 20)
* 5. Apply minute rule handler
* 6. Validate word sequence in pattern
* 7. Fallback: if validation fails, remove optional modifiers and retry
*
* @param pattern 110-character grid (uppercase)
* @param hour Hour (0-23)
* @param minute Minute (0-59)
* @param outWords Output array for words (up to 10 words)
* @param outLedInfo Output LED information
* @return Number of words, or 0 on error
*/
uint8_t getWordsForTime(
const char* pattern,
uint8_t hour,
uint8_t minute,
const char** outWords,
LEDInfo& outLedInfo
);
#endif // CHARGRAPH_WORD_MATCHER_H

View File

@ -0,0 +1,132 @@
#include <powerondetector.h>
// Flag setzen während System läuft
void setRunningFlag()
{
EEPROM.write(ADDR_RUNNING_FLAG, RUNNING_FLAG_MAGIC);
EEPROM.commit();
}
// Flag löschen bei sauberem Shutdown
void clearRunningFlag()
{
EEPROM.write(ADDR_RUNNING_FLAG, 0x00);
EEPROM.commit();
}
bool detectPowerLossFromResetReason()
{
Serial.println("\n╔════════════════════════════════════════╗");
Serial.println( "║ BOOT-GRUND ANALYSE ║");
Serial.println( "╚════════════════════════════════════════╝");
rst_info* resetInfo = ESP.getResetInfoPtr();
Serial.print("Reset Reason: ");
Serial.println(resetInfo->reason);
switch (resetInfo->reason) {
case REASON_DEFAULT_RST:
Serial.println(" → Power-On Reset");
Serial.println("⚠ STROMAUSFALL oder erste Inbetriebnahme");
return true;
case REASON_WDT_RST:
Serial.println(" → Watchdog Reset");
Serial.println("⚠ System abgestürzt");
return true;
case REASON_EXCEPTION_RST:
Serial.println(" → Exception Reset");
Serial.println("⚠ Software-Fehler");
return true;
case REASON_SOFT_WDT_RST:
Serial.println(" → Software Watchdog");
Serial.println("⚠ System hing");
return true;
case REASON_SOFT_RESTART:
Serial.println(" → Software Restart");
Serial.println("✓ Normaler Software-Neustart");
return false;
case REASON_DEEP_SLEEP_AWAKE:
Serial.println(" → Deep Sleep Awake");
Serial.println("✓ Aufwachen aus Deep Sleep");
return false;
case REASON_EXT_SYS_RST:
Serial.println(" → External Reset (Button)");
Serial.println("✓ Reset-Taste gedrückt");
return false;
default:
Serial.println(" → Unbekannt");
return false;
}
Serial.println("════════════════════════════════════════\n");
}
bool detectPowerLossWithoutRTC()
{
Serial.println("\n╔════════════════════════════════════════╗");
Serial.println( "║ STROMAUSFALL-PRÜFUNG (ohne RTC) ║");
Serial.println( "╚════════════════════════════════════════╝");
// Prüfe ob Running-Flag gesetzt war
uint8_t runningFlag = EEPROM.read(ADDR_RUNNING_FLAG);
if (runningFlag == RUNNING_FLAG_MAGIC) {
// Flag war gesetzt → System lief und wurde abrupt unterbrochen
Serial.println("\n╔═════════════════════════════════════════════╗");
Serial.println( "║ ⚠⚠⚠ STROMAUSFALL ERKANNT! ║");
Serial.println( "║ System wurde nicht sauber heruntergefahren ║");
Serial.println( "╚═════════════════════════════════════════════╝\n");
// Lösche Flag (wird später wieder gesetzt wenn System läuft)
EEPROM.write(ADDR_RUNNING_FLAG, 0x00);
EEPROM.commit();
return true;
} else {
Serial.println("\n╔═════════════════════════════════════════════╗");
Serial.println ("║ ✓ Normaler Start (Flag nicht gesetzt) ║");
Serial.println( "║ oder erste Inbetriebnahme ║");
Serial.println( "╚═════════════════════════════════════════════╝\n");
return false;
}
}
void checkPowerLoss()
{
// Lade Boot-Counter
bootCounter = EEPROM.read(ADDR_BOOT_COUNTER) << 8;
bootCounter |= EEPROM.read(ADDR_BOOT_COUNTER + 1);
// Prüfe Clean-Shutdown-Flag
bool cleanShutdown = EEPROM.read(ADDR_CLEAN_SHUTDOWN) == 0xAA;
bootCounter++;
DEBUG_PRINTLN("\n╔════════════════════════════════════════╗");
DEBUG_PRINTLN( "║ BOOT-ANALYSE ║");
DEBUG_PRINTLN( "╚════════════════════════════════════════╝");
DEBUG_PRINTF("\nBoot #%d\n", bootCounter);
if (cleanShutdown) {
DEBUG_PRINTLN("✓ Letzter Shutdown war sauber (Timeout)");
EEPROM.write(ADDR_CLEAN_SHUTDOWN, 0x00); // Reset
} else {
DEBUG_PRINTLN("⚠ STROMAUSFALL ERKANNT!");
DEBUG_PRINTLN(" (Kein Clean-Shutdown-Flag)");
}
// Speichere neuen Boot-Counter
EEPROM.write(ADDR_BOOT_COUNTER, (bootCounter >> 8) & 0xFF);
EEPROM.write(ADDR_BOOT_COUNTER + 1, bootCounter & 0xFF);
EEPROM.commit();
DEBUG_PRINTLN("════════════════════════════════════════\n");
}

View File

@ -0,0 +1,18 @@
#ifndef _POWERONDETECTOR_H_
#define _POWERONDETECTOR_H_
#include <Arduino.h>
//#include <FastLED.h>
#include <EEPROM.h>
//#include <RTClib.h>
#include <user_interface.h> // Für rst_info
#include <extern.inc>
#include <defines.inc>
//#include <vars.inc>
extern bool detectPowerLossWithoutRTC();
extern void setRunningFlag();
extern void clearRunningFlag();
extern bool detectPowerLossFromResetReason();
extern void powerLossLoop();
extern void checkPowerLoss();
extern uint16_t bootCounter;
#endif

View File

@ -0,0 +1,8 @@
{
"name": "PowerOnDetector",
"version": "1.0.0",
"dependencies": {
"fastled/FastLED": "^3.6.0",
"adafruit/RTClib": "^2.1.1"
}
}

47
lib/info/info.cpp Normal file
View File

@ -0,0 +1,47 @@
#include <info.h>
// ════════════════════════════════════════════════════════════════
// Verdrahtungshinweis
// ════════════════════════════════════════════════════════════════
void showConnect()
{
Serial.println("");
Serial.println(" ┌──────────────────────┐");
Serial.println(" │ 5V Netzteil │");
Serial.println(" │ (3-5 Ampere) │");
Serial.println(" └──────────┬───────────┘");
Serial.println("");
Serial.println(" ┌────────┴────────────┐");
Serial.println(" │ │");
Serial.println(" 5V GND");
Serial.println(" │ │");
Serial.println(" ┌─────────────┼─────────────────────┼──────────┐");
Serial.println(" │ │ │ │");
Serial.println(" │ ┌──────────┴───┐ ┌───────┴──────┐ │");
Serial.println(" │ │ │ │ │ │");
Serial.println(" ┌───┴──┴─────┐ ┌───┴─────────┴────┐ ┌───┴───┴────┐");
Serial.println(" │ Wemos │ │ WS2812B Strip │ │ Optional: │");
Serial.println(" │ D1 Mini │ │ (118 LEDs) │ │ 1000µF Cap │");
Serial.println(" ├────────── ┤ ├──────────────────┤ └────────────┘");
Serial.println(" │ │ │ │");
Serial.println(" │ D6(GPIO12 ├────┤ DIN │");
Serial.println(" │ │ │ (evtl. via 470Ω) │");
Serial.println(" │ 5V ├───┬┤ 5V │");
Serial.println(" │ │ ││ │");
Serial.println(" │ GND ├┬──│┤ GND │");
Serial.println(" │ ││ ││ │");
Serial.println(" │ ││ │└──────────────────┘");
Serial.println(" │ ││ │┌────────────────────┐");
Serial.println(" │ ││ ││ DS1307 RTC Modul │");
Serial.println(" │ ││ ││ │");
Serial.println(" │ ││ │├────────────────────┤");
Serial.println(" │ ││ ││ │");
Serial.println(" │ D1 (GPIO5) ├│──│┤ SCL │");
Serial.println(" │ ││ ││ │");
Serial.println(" │ D2 (GPIO4) ├│──│┤ SDA │");
Serial.println(" └────────────┘│ ││ │");
Serial.println(" │ └┤ VCC │");
Serial.println(" │ │ │");
Serial.println(" └───┤ GND │");
Serial.println(" │ │");
Serial.println(" └────────────────────┘");
}

6
lib/info/info.h Normal file
View File

@ -0,0 +1,6 @@
#ifndef _INFO_INC_
#define _INFO_INC_
#include <Arduino.h>
extern void showConnect();
#endif

499
lib/rgbPanel/rgbPanel.cpp Normal file
View File

@ -0,0 +1,499 @@
#include <Arduino.h>
#include <rgbPanel.h>
#include <charPattern.inc>
// ════════════════════════════════════════════════════════════════
// Optimiertes FastLED.show() mit Interrupt-Schutz
// ════════════════════════════════════════════════════════════════
CRGB normalColor = CRGB::White;
CRGB specialColor = CRGB::Green;
CRGB leds[NUM_LEDS];
uint8_t brightness = 80;
char charsoap[COLS * ROWS * 2]; //Char * 2 to fit all in, cause UTF (maybe ÄÖÜ)
bool customCharsoap = false;
// ── Konfiguration für Spezialanzeige ──
// MAXWORDS ist in defines.inc definiert
// Platzhalter - werden dynamisch aus EEPROM oder Defaults geladen (siehe loadSpecialWords() in main.cpp)
char SPECIAL_WORD[MAXWORDS][12] = {"", //max 11 Zeichen, \0 wird automatisch angefügt!
"",
"" }; //Text der ein- bzw. ausgeblendet werden soll
const uint16_t SPECIAL_HOLD_MS = 5000; // Anzeigedauer des Spezialworts
const int hourpattern[][2] = {
{46, 7},
{102, 5}
};
void showLEDs()
{
noInterrupts();
FastLED.show();
interrupts();
}
//eleminate bridged LEDS
int bridgeLED(int pos)
{
#ifndef BRIDGE_LEDS_POS77
return pos;
#else
//if(pos==77)
// return NUM_LEDS;
//else
return (pos >= 77 ? pos-1:pos);
#endif
}
// Sanftes Ausblenden des aktuellen Frames (komplette Matrix)
// OPTIMIERT: Weniger Schritte und kürzeres Delay für bessere WiFi-Performance
#define MAX_STEPS 200
void fadeOutAll(uint8_t steps = MAX_STEPS, uint16_t stepDelayMs = 15) {
if(steps > MAX_STEPS)
steps = MAX_STEPS;
for (uint8_t i = 0; i < MAX_STEPS; i++) {
fadeToBlackBy(leds, NUM_LEDS, MAX_STEPS / steps);
showLEDs();
yield();
delay(stepDelayMs);
}
FastLED.clear();
showLEDs();
}
// Sanftes Einblenden mittels globaler Helligkeit
// OPTIMIERT: Weniger Schritte und kürzeres Delay für bessere WiFi-Performance
void fadeInCurrentFrame(uint8_t targetBrightness, uint8_t steps = MAX_STEPS, uint16_t stepDelayMs = 15) {
if(steps > MAX_STEPS)
steps = MAX_STEPS;
uint8_t saved = targetBrightness;
FastLED.setBrightness(0);
showLEDs();
// Schrittweite so wählen, dass exakt targetBrightness erreicht wird
for (uint8_t s = 1; s <= steps; s++)
{
uint8_t b = (uint16_t)s * saved / steps;
FastLED.setBrightness(b);
showLEDs();
yield();
delay(stepDelayMs);
}
FastLED.setBrightness(saved);
showLEDs();
}
// Zeigt ein Wort (falls vorhanden) mit sanftem Fade-In, hält es und blendet es wieder aus
// OPTIMIERT: Schnellere Animation für bessere WiFi-Performance
bool showSpecialWordSequence(const char words[][12], CRGB color, uint8_t steps = 20, uint16_t stepDelayMs = 15)
{
int pos = findWord(words[0], 0);
if (pos < 0) {
// Wort nicht gefunden
return false;
}
// Volle Matrix zunächst aus
FastLED.clear();
for(uint i = 0; i < MAXWORDS; i++ )
{
if(findWord(words[i], 0))
{
// Wort setzen
setWord(words[i], color, 0);
}
}
// Helligkeit schonend einblenden
uint8_t savedBrightness = FastLED.getBrightness(); // optional; wenn nicht verfügbar, nimm die globale 'brightness'
if (savedBrightness == 0) savedBrightness = 80; // Fallback
fadeInCurrentFrame(savedBrightness, steps, stepDelayMs);
// Für die gewünschte Dauer halten
unsigned long t0 = millis();
while (millis() - t0 < SPECIAL_HOLD_MS) {
// Optional leichte "Herzschlag"-Animation vermeiden → einfach halten
yield();
delay(10);
}
// Ausblenden
fadeOutAll(steps, stepDelayMs);
return true;
}
// Orchestriert: bisherigen Text ausblenden → Spezialwort zeigen → neue Zeit rendern (mit sanftem Einblenden)
void showSpecialWordThenTime(int hours, int minutes)
{
// Aktuelle Helligkeit speichern (BEVOR wir sie auf 0 setzen!)
uint8_t savedBrightness = FastLED.getBrightness();
if (savedBrightness == 0) savedBrightness = map(brightness, 0, 80, 0, 204); // Fallback auf globale Variable (gemappt)
// 1) Bisherigen Frame ausblenden
fadeOutAll();
// 2) Spezialwort-Sequenz (falls im Layout vorhanden), Farbe: specialColor
showSpecialWordSequence(SPECIAL_WORD, specialColor);
// 3) Bisherigen Frame ausblenden
fadeOutAll();
// 4) Neue Zeit zeichnen
//FastLED.clear();
FastLED.setBrightness(0); // Helligkeit vor dem internen showLEDs der displayTime auf 0 setzen
displayTime(hours, minutes);
// 5) Neue Zeit sanft einblenden (falls displayTime viel setzt, ist der Effekt angenehm)
fadeInCurrentFrame(savedBrightness);
}
// ════════════════════════════════════════════════════════════════
// FUNKTIONEN: RGB-TEST
// ════════════════════════════════════════════════════════════════
void rgbTest()
{
DEBUG_PRINTLN("\n╔════════════════════════════════╗");
DEBUG_PRINTLN("║ RGB LED TEST ROUTINE ║");
DEBUG_PRINTLN("╚════════════════════════════════╝\n");
// Test 1: Erste LED
DEBUG_PRINTLN("Test 1: Erste LED (Rot)");
FastLED.clear();
leds[bridgeLED(0)] = CRGB::Red;
showLEDs();
delay(1000);
// Test 2: Letzte LED
DEBUG_PRINTLN("Test 2: Letzte LED (Blau)");
FastLED.clear();
leds[bridgeLED(NUM_LEDS - 1)] = CRGB::Blue;
showLEDs();
delay(1000);
// Test 3: Alle Rot
DEBUG_PRINTLN("Test 3: Alle LEDs Rot");
fill_solid(leds, NUM_LEDS, CRGB::Red);
showLEDs();
delay(1000);
// Test 4: Alle Grün
DEBUG_PRINTLN("Test 4: Alle LEDs Grün");
fill_solid(leds, NUM_LEDS, CRGB::Green);
showLEDs();
delay(1000);
// Test 5: Alle Blau
DEBUG_PRINTLN("Test 5: Alle LEDs Blau");
fill_solid(leds, NUM_LEDS, CRGB::Blue);
showLEDs();
delay(1000);
// Test 6: Alle Weiß
DEBUG_PRINTLN("Test 6: Alle LEDs Weiß");
fill_solid(leds, NUM_LEDS, CRGB::White);
showLEDs();
delay(1000);
// Test 7: Lauflicht
DEBUG_PRINTLN("Test 7: Lauflicht");
FastLED.clear();
for (int i = 0; i < NUM_LEDS; i++) {
leds[bridgeLED(i)] = CRGB::Green;
showLEDs();
delay(100);
leds[bridgeLED(i)] = CRGB::Black;
}
// Test 8: Regenbogen
DEBUG_PRINTLN("Test 8: Regenbogen");
for (int hue = 0; hue < 256; hue += 4) {
for (int i = 0; i < NUM_LEDS; i++) {
leds[bridgeLED(i)] = CHSV((hue + i * 2) % 256, 255, 255);
}
showLEDs();
delay(20);
}
// Test 9: Matrix Zeilen
DEBUG_PRINTLN("Test 9: Matrix Zeilen");
for (int row = 0; row < ROWS; row++) {
FastLED.clear();
for (int col = 0; col < COLS; col++) {
int index;
if (row % 2 == 0) {
index = row * COLS + col;
} else {
index = row * COLS + (COLS - 1 - col);
}
leds[bridgeLED(index)] = CRGB::Blue;
}
showLEDs();
delay(300);
}
// Test 10: Matrix Spalten
DEBUG_PRINTLN("Test 10: Matrix Spalten");
for (int col = 0; col < COLS; col++) {
FastLED.clear();
for (int row = 0; row < ROWS; row++) {
int index;
if (row % 2 == 0) {
index = row * COLS + col;
} else {
index = row * COLS + (COLS - 1 - col);
}
leds[bridgeLED(index)] = CRGB::Orange;
}
showLEDs();
delay(300);
}
//Test 11: Minuten-LEDs
DEBUG_PRINTLN("Test 11: Minuten-LEDs (4 Eck-LEDs)");
// Alle 4 nacheinander
CRGB minuteColors[] = {CRGB::Red, CRGB::Green, CRGB::Blue, CRGB::Yellow};
for (int i = 0; i < 4; i++) {
FastLED.clear();
leds[bridgeLED(MINUTE_LEDS[i])] = minuteColors[i];
DEBUG_PRINTF(" → Minuten-LED %d (Index %d)\n", i+1, MINUTE_LEDS[i]);
showLEDs();
delay(500);
}
FastLED.clear();
showLEDs();
DEBUG_PRINTLN("\n✓ RGB Test abgeschlossen!\n");
}
void getLedsFromPosition(int startPos, int length, int* ledArray)
{
for (int i = 0; i < length; i++) {
int pos = startPos + i;
int row = pos / COLS;
int col = pos % COLS;
if (row % 2 == 0) {
ledArray[i] = row * COLS + col;
} else {
ledArray[i] = row * COLS + (COLS - 1 - col);
}
}
}
int setWord(const char* word, CRGB color, int occurrence, bool searchBackward)
{
int pos = findWord(word, occurrence, searchBackward);
if (pos == -1)
{
return -1;
}
int length = strlen(word);
int ledIndices[length];
getLedsFromPosition(pos, length, ledIndices);
for (int i = 0; i < length; i++)
{
leds[bridgeLED(ledIndices[i])] = color;
}
return pos;
}
// ════════════════════════════════════════════════════════════════
// FUNKTIONEN: LED-ANSTEUERUNG
// ════════════════════════════════════════════════════════════════
int findWord(const char* word, int occurrence, bool searchBackward)
{
const int wordLen = strlen(word);
const int soapLen = strlen(charsoap);
//DEBUG_PRINTF("Search Word '%s' %s", word, searchBackward ? "(backward) " : "");
if (searchBackward)
{
// Rückwärtssuche: vom Ende zum Anfang
int foundCount = 0;
for (int pos = soapLen - wordLen; pos >= 0; --pos)
{
bool match = true;
for (int j = 0; j < wordLen; ++j)
{
unsigned char c1 = charsoap[pos + j];
unsigned char c2 = word[j];
//lowercase => FuNF => funf
if (c1 >= 'A' && c1 <= 'Z') c1 += ('a' - 'A');
if (c2 >= 'A' && c2 <= 'Z') c2 += ('a' - 'A');
if (c1 != c2)
{
match = false;
break;
}
}
if (match)
{
if (foundCount == occurrence)
{
//DEBUG_PRINTF(" found on pos %d (%d) ✓\n", pos, occurrence);
return pos;
}
foundCount++;
}
}
//DEBUG_PRINTF(" not found (%d) ❌\n", occurrence);
return -1;
}
else
{
// Vorwärtssuche: vom Anfang zum Ende (original)
int start = 0;
for (int i = 0; i <= occurrence; ++i)
{
int found = -1;
for (int pos = start; pos <= soapLen - wordLen; ++pos)
{
bool match = true;
for (int j = 0; j < wordLen; ++j)
{
unsigned char c1 = charsoap[pos + j];
unsigned char c2 = word[j];
//lowercase => FuNF => funf
if (c1 >= 'A' && c1 <= 'Z') c1 += ('a' - 'A');
if (c2 >= 'A' && c2 <= 'Z') c2 += ('a' - 'A');
if (c1 != c2)
{
match = false;
break;
}
}
if (match)
{
found = pos;
break;
}
}
if (found == -1)
{
//DEBUG_PRINTF(" not found (%d) ❌\n", occurrence);
return -1; // dieses i-te Vorkommen existiert nicht
}
if (i == occurrence)
{
//DEBUG_PRINTF(" found on pos %d (%d) ✓\n", found, occurrence);
return found;
}
start = found + 1; // ab nächster Position weiter suchen
}
}
return -1;
}
uint8_t testWords()
{
//return 0;
DEBUG_PRINT("╔════════════════════════╗\n");
DEBUG_PRINT("║ Teste Wörter ... ║\n");
DEBUG_PRINT("╚════════════════════════╝\n")
DEBUG_PRINTLN(DEFAULT_CHARSOAP);
if(strlen(DEFAULT_CHARSOAP) != (COLS * (ROWS-1)))
{
//return -1;
DEBUG_PRINT("\nLänge stimmt nicht: ");
DEBUG_PRINTLN(strlen(DEFAULT_CHARSOAP));
}
else
{
DEBUG_PRINT("\nLänge stimmt: ");
DEBUG_PRINTLN(strlen(DEFAULT_CHARSOAP));
}
uint8_t currentHour = 12;
uint8_t currentMinute = 29;
//CharGraphTimeWords result;
while(true)
{
yield();
currentMinute++;
if(currentMinute == 60)
{
currentHour = 1;
currentMinute = 0;
}
if(currentHour == 1 && currentMinute == 31)
{
DEBUG_PRINT ("\n╔════════════════════════╗\n");
DEBUG_PRINT ( "║ Teste Wörter fertig. ║\n");
DEBUG_PRINTLN( "╚════════════════════════╝\n")
delay(1500);
return 0;
}
DEBUG_PRINT("Zeit: '");
if (currentHour < 10) DEBUG_PRINT("0");
DEBUG_PRINT(currentHour);
DEBUG_PRINT(":");
if (currentMinute < 10) DEBUG_PRINT("0");
DEBUG_PRINT(currentMinute);
DEBUG_PRINT("' -> ");
//delay(1000);
//int8_t resultval = getCharGraphWords(DEFAULT_CHARSOAP, currentHour, currentMinute, result);
//if (resultval == 0)
{
// ===== Display current time =====
displayTime(currentHour, currentMinute);
}
//else
//{
// DEBUG_PRINT("ERROR: "); DEBUG_PRINT(resultval); DEBUG_PRINTLN(" Pattern validation failed!");
// delay(1000);
//}
delay(500);
}
}
void checkPattern()
{
testWords();
FastLED.clear();
uint8_t lengthPattern = strlen(testPattern);
for(uint8_t n = 0; n < lengthPattern && testPattern[n] != '\0' ;n++)
{
char pattern[12];
uint8_t patternPos = 0;
while(testPattern[n] != '-' &&
testPattern[n] != '\0' )
{
pattern[patternPos++] = testPattern[n++];
}
pattern[patternPos]='\0';
int firstPos = findWord(pattern, 0);
int secondPos = findWord(pattern, 1);
if(firstPos >= 0)
{
setWord(pattern, normalColor,0);
}
if(secondPos >= 0)
{
setWord(pattern, normalColor,1);
}
showLEDs();
//delay(500);
yield();
FastLED.clear();
}
}

38
lib/rgbPanel/rgbPanel.h Normal file
View File

@ -0,0 +1,38 @@
#ifndef _RGBPANEL_H_
#define _RGBPANEL_H_
#include <FastLED.h>
#include <defines.inc>
#include <CharGraphTimeLogic.h>
// ════════════════════════════════════════════════════════════════
// HARDWARE DEFINITIONEN
// ════════════════════════════════════════════════════════════════
#define LED_PIN D6
#define NUM_LEDS 121
#define COLS 11
#define ROWS 11
extern CRGB normalColor;
extern CRGB specialColor;
extern CRGB leds[NUM_LEDS];
extern uint8_t brightness;
extern const int hourpattern[][2];
extern int MINUTE_LEDS[4];
extern char testPattern[];
extern bool customCharsoap;
extern char charsoap[COLS * ROWS * 2];
extern const char DEFAULT_CHARSOAP[];
extern char SPECIAL_WORD[3][12]; // MAXWORDS = 3
extern void showLEDs();
extern int bridgeLED(int pos);
extern void rgbTest();
extern void checkPattern();
extern int findWord(const char* word, int occurrence = 0, bool searchBackward = false);
extern int setWord(const char* word, CRGB color, int occurrence = 0, bool searchBackward = false);
extern void getLedsFromPosition(int startPos, int length, int* ledArray);
extern void displayTime(int hours, int minutes);
extern void showSpecialWordThenTime(int hours, int minutes);
extern void fadeOutAll(uint8_t steps, uint16_t stepDelayMs);
extern void fadeInCurrentFrame(uint8_t targetBrightness, uint8_t steps, uint16_t stepDelayMs);
#endif

120
platformio.ini Normal file
View File

@ -0,0 +1,120 @@
[platformio]
default_envs = esp8266clone
;default_envs = d1_mini
[common]
lib_ldf_mode = deep
; Monitor-Einstellungen
monitor_speed = 115200
; RTS/DTR für Auto-Reset (wichtig für Clones!)
monitor_dtr = 1
monitor_rts = 1
monitor_filters =
default
time
esp8266_exception_decoder
[env]
framework = arduino
platform = espressif8266
; Bibliotheken
lib_deps =
fastled/FastLED @ ^3.6.0
adafruit/RTClib @ ^2.1.1 ; NEU: RTC-Bibliothek
;|-- DNSServer @ 1.1.1
;|-- EEPROM @ 1.0
;|-- ESP8266WebServer @ 1.0
;|-- ESP8266WiFi @ 1.0
;|-- Wire @ 1.0
; Build-Flags für Debug
build_flags =
-I include
-DPIO_FRAMEWORK_ARDUINO_LWIP2_LOW_MEMORY
-DDEBUG_ESP_PORT=Serial
-DDEBUG_ESP_CORE=false
-DDEBUG_MODE=false ;true = Debug, false = Produktiv
-DNDEBUG
-DDEBUG_SOURCE=true
-DDEBUG_ESP_WIFI=0
-DUSE_RTC=false
-DTEST_RGB_ONLY=false
-DTEST_RGB=false
-DPOWERLOSSDETECT=false
; Monitor-Einstellungen
monitor_filters =
default
time
esp8266_exception_decoder
extra_scripts = pre:html_to_header.py
[env:esp8266clone]
board = esp12e ; ESP8266MOD = ESP-12E/F kompatibel
framework = ${env.framework}
platform = ${env.platform}
; Serial-Einstellungen
monitor_speed = ${common.monitor_speed}
upload_speed = 115200 ; Langsam für Clone-Stabilität
upload_port = COM13
; Flash-Einstellungen (sicher für alle Clones)
board_build.flash_mode = dio ; Statt qio - kompatibler!
# CPU-Frequenz auf 160MHz erhöhen
board_build.f_cpu = 160000000L
; Flash-Einstellungen (4MB Flash für Standard D1 Mini)
board_build.f_flash = 40000000L
board_build.ldscript = eagle.flash.4m2m.ld ;2 MB Sketch, 2 MB OTA kein SPIFFS
; Build-Flags für Debug
build_flags =
${env.build_flags}
;-DBRIDGE_LEDS_POS77
; Bibliotheken
lib_deps = ${env.lib_deps}
; Monitor-Einstellungen
monitor_filters = ${common.monitor_filters}
; RTS/DTR für Auto-Reset (wichtig für Clones!)
monitor_dtr = 1
monitor_rts = 1
[env:d1_mini]
board = d1_mini
platform = ${env.platform}
framework = ${env.framework}
; Serial-Einstellungen
monitor_speed = ${common.monitor_speed}
; Upload-Einstellungen
;upload_speed = 921600
upload_speed = 115200
;upload_port = COM13 ; Automatische Erkennung oder manuell anpassen
upload_port = COM5
board_build.flash_mode = dio
# CPU-Frequenz auf 160MHz erhöht
board_build.f_cpu = 160000000L ; Statt 80000000L
; Flash-Einstellungen (4MB Flash für Standard D1 Mini)
board_build.f_flash = 40000000L
board_build.ldscript = eagle.flash.4m2m.ld
; Build-Flags für Debug
build_flags =
${env.build_flags}
;-DBRIDGE_LEDS_POS77
; Bibliotheken
lib_deps = ${env.lib_deps}
; Monitor-Einstellungen
monitor_filters = ${common.monitor_filters}

2655
src/main.cpp Normal file

File diff suppressed because it is too large Load Diff