Initial Setup
This commit is contained in:
commit
602e7f46f3
7
.claude/settings.local.json
Normal file
7
.claude/settings.local.json
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
{
|
||||
"permissions": {
|
||||
"allow": [
|
||||
"Bash(python html_to_header.py:*)"
|
||||
]
|
||||
}
|
||||
}
|
||||
66
.gitignore
vendored
Normal file
66
.gitignore
vendored
Normal 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
1
doc/.gitkeep
Normal file
|
|
@ -0,0 +1 @@
|
|||
# Dieser Ordner ist Teil der Projektstruktur
|
||||
2624
html/index.html
Normal file
2624
html/index.html
Normal file
File diff suppressed because it is too large
Load Diff
64
html_to_header.py
Normal file
64
html_to_header.py
Normal 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
35
include/charPattern.inc
Normal 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
24
include/common.inc
Normal 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
109
include/defines.inc
Normal 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
9
include/extern.inc
Normal 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
1332
include/html.h
Normal file
File diff suppressed because it is too large
Load Diff
92
include/vars.inc
Normal file
92
include/vars.inc
Normal 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
33
include/version.inc
Normal 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
|
||||
8
lib/CharGraphTimeLogic/.claude/settings.local.json
Normal file
8
lib/CharGraphTimeLogic/.claude/settings.local.json
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
{
|
||||
"permissions": {
|
||||
"allow": [
|
||||
"Bash(python -m py_compile:*)",
|
||||
"Bash(node -c:*)"
|
||||
]
|
||||
}
|
||||
}
|
||||
186
lib/CharGraphTimeLogic/blackboxtest/README.md
Normal file
186
lib/CharGraphTimeLogic/blackboxtest/README.md
Normal 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.
|
||||
285
lib/CharGraphTimeLogic/blackboxtest/blackboxtest.ino
Normal file
285
lib/CharGraphTimeLogic/blackboxtest/blackboxtest.ino
Normal 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("========================================");
|
||||
}
|
||||
48
lib/CharGraphTimeLogic/blackboxtest/config.h
Normal file
48
lib/CharGraphTimeLogic/blackboxtest/config.h
Normal 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
|
||||
117
lib/CharGraphTimeLogic/examples/BasicExample.ino
Normal file
117
lib/CharGraphTimeLogic/examples/BasicExample.ino
Normal 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);
|
||||
}
|
||||
}
|
||||
199
lib/CharGraphTimeLogic/examples/FullExample.ino
Normal file
199
lib/CharGraphTimeLogic/examples/FullExample.ino
Normal 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;
|
||||
}
|
||||
*/
|
||||
195
lib/CharGraphTimeLogic/examples/MinuteRangeTest.ino
Normal file
195
lib/CharGraphTimeLogic/examples/MinuteRangeTest.ino
Normal 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();
|
||||
}
|
||||
}
|
||||
10
lib/CharGraphTimeLogic/library.properties
Normal file
10
lib/CharGraphTimeLogic/library.properties
Normal 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
|
||||
167
lib/CharGraphTimeLogic/src/CharGraphTimeLogic.cpp
Normal file
167
lib/CharGraphTimeLogic/src/CharGraphTimeLogic.cpp
Normal 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
|
||||
114
lib/CharGraphTimeLogic/src/CharGraphTimeLogic.h
Normal file
114
lib/CharGraphTimeLogic/src/CharGraphTimeLogic.h
Normal 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
|
||||
151
lib/CharGraphTimeLogic/src/Constants.cpp
Normal file
151
lib/CharGraphTimeLogic/src/Constants.cpp
Normal 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]);
|
||||
}
|
||||
139
lib/CharGraphTimeLogic/src/Constants.h
Normal file
139
lib/CharGraphTimeLogic/src/Constants.h
Normal 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
|
||||
370
lib/CharGraphTimeLogic/src/LEDCalculator.cpp
Normal file
370
lib/CharGraphTimeLogic/src/LEDCalculator.cpp
Normal 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;
|
||||
}
|
||||
86
lib/CharGraphTimeLogic/src/LEDCalculator.h
Normal file
86
lib/CharGraphTimeLogic/src/LEDCalculator.h
Normal 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
|
||||
495
lib/CharGraphTimeLogic/src/MinuteRules.cpp
Normal file
495
lib/CharGraphTimeLogic/src/MinuteRules.cpp
Normal 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;
|
||||
}
|
||||
}
|
||||
33
lib/CharGraphTimeLogic/src/MinuteRules.h
Normal file
33
lib/CharGraphTimeLogic/src/MinuteRules.h
Normal 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
|
||||
67
lib/CharGraphTimeLogic/src/Types.h
Normal file
67
lib/CharGraphTimeLogic/src/Types.h
Normal file
|
|
@ -0,0 +1,67 @@
|
|||
/**
|
||||
* CharGraph Time Logic - Data Types
|
||||
*
|
||||
* TypeScript→C++ 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
|
||||
337
lib/CharGraphTimeLogic/src/Validator.cpp
Normal file
337
lib/CharGraphTimeLogic/src/Validator.cpp
Normal 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};
|
||||
}
|
||||
88
lib/CharGraphTimeLogic/src/Validator.h
Normal file
88
lib/CharGraphTimeLogic/src/Validator.h
Normal 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
|
||||
213
lib/CharGraphTimeLogic/src/WordMatcher.cpp
Normal file
213
lib/CharGraphTimeLogic/src/WordMatcher.cpp
Normal 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;
|
||||
}
|
||||
40
lib/CharGraphTimeLogic/src/WordMatcher.h
Normal file
40
lib/CharGraphTimeLogic/src/WordMatcher.h
Normal 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
|
||||
132
lib/PowerOnDetector/PowerOnDetector.cpp
Normal file
132
lib/PowerOnDetector/PowerOnDetector.cpp
Normal 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");
|
||||
}
|
||||
18
lib/PowerOnDetector/PowerOnDetector.h
Normal file
18
lib/PowerOnDetector/PowerOnDetector.h
Normal 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
|
||||
8
lib/PowerOnDetector/library.json
Normal file
8
lib/PowerOnDetector/library.json
Normal 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
47
lib/info/info.cpp
Normal 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
6
lib/info/info.h
Normal 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
499
lib/rgbPanel/rgbPanel.cpp
Normal 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
38
lib/rgbPanel/rgbPanel.h
Normal 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
120
platformio.ini
Normal 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
2655
src/main.cpp
Normal file
File diff suppressed because it is too large
Load Diff
Loading…
Reference in New Issue
Block a user