An open API service indexing awesome lists of open source software.

https://github.com/apadevices/apalcdgui

Non-blocking 20x4 LCD menu system with dual rotary encoders for APA Devices water treatment automation (AVR, ESP32, ESP8266, STM32)
https://github.com/apadevices/apalcdgui

arduino avr esp32 esp8266 hmi lcd menu platformio rotary-encoder stm32 water-treatment

Last synced: 3 days ago
JSON representation

Non-blocking 20x4 LCD menu system with dual rotary encoders for APA Devices water treatment automation (AVR, ESP32, ESP8266, STM32)

Awesome Lists containing this project

README

          

# APALCDGUI


APALCDGUI

**Parallel 20×4 LCD menu system with dual rotary encoders for APA Devices water treatment automation**
· ![v1.4.0](https://img.shields.io/badge/version-1.4.0-blue)
· ![Platforms](https://img.shields.io/badge/platforms-AVR%20ESP8266%20ESP32%20STM32-brightgreen)

---

## Key Features

### Display and navigation
- Parallel 4-bit LiquidCrystal 20×4 LCD — no I2C module required
- Two PEC11R quadrature rotary encoders, native ISR decoding — no external library
- 12-state non-blocking machine: HOME, NAV, EDIT, FLASH_SAVE, FLASH_BACK, FLASH_ACTION, BRIGHTNESS, CONFIRM, RTC_NAV, RTC_EDIT, TIMER, TIMER_EDIT
- Up to 4 submenu screens per side (left / right), configurable via `APA_LCD_MAX_SCREENS`
- Multiple home screen pages — register up to 4 via `addHomeScreen()`, scroll with KB2 rotation
- Inline timer schedule screen — up to 3 on/off time slots in 30-minute steps, auto-saved to EEPROM
- 1, 2, or 3 fields per screen: INT, FLOAT, CHOICE, BOOL, ACTION, or READONLY

### Alert system
- Passive alerts: corner indicator (`[i]` / `[*]` / `[!]` flashing) — navigation not blocked
- Active alerts: home screen takeover, queue of 3 — each requires operator acknowledgment
- Status indicator: `setStatusIndicator('M')` shows `[M]` in the corner when no alert is pending — alert always takes priority
- Automatic alert routing from APADOSE via user-supplied callback (libraries stay independent)

### Backlight
- PWM brightness: ACTIVE → DIM → OFF with configurable timeout
- Brightness persisted in EEPROM, adjusted via gesture (hold KB1 for 800 ms, then rotate knob1)
- Off-timeout suspended while active alerts are pending

### Flexibility
- Factory functions for all field types — beginners never fill a struct by hand
- BOOL toggle field and ACTION confirmation dialog added in v1.1
- Optional DS3231 RTC modal compiled out if DS3231.h not included
- Long-press and both-buttons-pressed gesture callbacks
- Beginner defaults: all pin numbers match APA Devices HMI board v1.0 — `begin()` takes no arguments

### Engineering
- No heap allocation — `LiquidCrystal` is a direct class member, initialised in the constructor
- ISR singleton: one static instance pointer, two static ISR stubs — fully portable
- `F()` macro on all string literals — zero SRAM cost for labels on AVR
- Zero `delay()` calls — `update()` always returns within one loop iteration

---

## Installation

**Arduino IDE:** Sketch → Include Library → Add .ZIP Library → select the APALCDGUI folder.

**PlatformIO:**
```ini
lib_deps =
arduino-libraries/LiquidCrystal @ ^1.0.7
https://github.com/apadevices/APALCDGUI
```

---

## What It Does

APALCDGUI drives a 20-column × 4-row parallel LCD and two rotary encoders as a complete menu system for pool automation hardware. The operator turns the right knob (knob1) to switch between parameter screens, turns the left knob (knob2) to move the cursor between fields, and presses the left knob to enter edit mode or confirm actions. Alarms appear either as a quiet corner indicator (passive) or as a full-screen takeover requiring acknowledgment (active). The backlight dims and extinguishes after inactivity, remembers the operator's preferred brightness, and wakes immediately on any knob movement.

---

## How It Works

```
HOME ──── knob1 (right) rotates ─────────────────► NAV (submenu screen)
──── knob2 (left) rotates ──────────────────► next / previous home page
◄─── menu timeout (60 s) ─────────────────────
◄─── BACK confirmed ──────────────────────────

NAV ──── knob2 (left) moves cursor 0 → 1 → BACK → SAVE → 0
──── knob2 press on field ──────────────────► EDIT
EDIT ──── knob2 press ───────────────────────────► NAV (cursor jumps to SAVE)

NAV ──── cursor on SAVE, press ─────────────────► FLASH_SAVE → onSave() → NAV
NAV ──── cursor on BACK, press ─────────────────► FLASH_BACK → HOME
NAV ──── cursor on ACTION field, press ─────────► FLASH_ACTION (► 300 ms) → action() → NAV

Both buttons pressed ────────────────────────────► RTC_NAV (if setRTC() called)
Hold KB1 for 800 ms, then rotate knob1 ──────────► BRIGHTNESS adjust
```

### Submenu screen layout

```
0 1 2
01234567890123456789
Row0: ► pH setpoint 7.24pH
Row1: ► ORP setpoint 680mV
Row2: Filter ON [!] ← cols 17–19: passive alert indicator
Row3: ►BACK →2/4 ►SAVE
```

- Col 0: cursor marker (space or `►`)
- Cols 1–12: field label (max 12 chars)
- Cols 14–17: value (4 chars)
- Cols 18–19: unit (2 chars)
- Row 2 cols 0–16: `setMenuRow2Callback` output or optional screen title
- Row 2 cols 17–19: `[i]` info / `[*]` warning / `[!]` critical alert indicator, or `[M]` status indicator when no alert is pending

### Home screen layout

The home screen is fully drawn by your callback(s). The library overlays two elements on top:

```
0 1 2
01234567890123456789
Row0: pH 7.24 ORP 680mV ← your callback writes all four rows
Row1: Cl 1.2 Temp 26°C
Row2: Filter ON [!] ← cols 17–19: passive alert indicator (library)
Row3: System OK 2/3 ← cols 17–19: page indicator (library, only when > 1 page)
```

- Rows 0–3 cols 0–16 are entirely yours — write whatever you need.
- **Row 2 cols 17–19**: alert indicator (`[!]`/`[*]`/`[i]`) or status indicator (`[M]` etc.) when no alert — always drawn by the library.
- **Row 3 cols 17–19**: page indicator (`1/3`, `2/3`, …) drawn automatically when more than one home page is registered. Only 1 page → no indicator, those 3 cols are yours.
- If your callback writes to cols 17–19 of rows 2 or 3, the library overwrites it — avoid those positions.

---

## Quick Start

```cpp
#include

APALCDGUI gui;

float phSetpoint = 7.20f;
int16_t orpSetpoint = 680;

void drawHome(LiquidCrystal& lcd) {
lcd.setCursor(0, 0); lcd.print(F("pH 7.24 ORP 680mV "));
lcd.setCursor(0, 1); lcd.print(F("Cl 1.2 Temp 26C "));
lcd.setCursor(0, 2); lcd.print(F("Filter ON 12:34 "));
lcd.setCursor(0, 3); lcd.print(F("System OK "));
}

void onSave() { /* write to EEPROM or send to APADOSE here */ }

void setup() {
gui.begin(); // all pin defaults match HMI board v1.0

gui.setHomeCallback(drawHome);

gui.addScreen(SCREEN_RIGHT,
APALCDGUI::fieldFloat(F("pH setpoint"), F("pH"), &phSetpoint, 6.8f, 7.8f, 0.01f, 2),
APALCDGUI::fieldInt( F("ORP setpoint"), F("mV"), &orpSetpoint, 400, 850, 10),
onSave
);
}

void loop() {
gui.update(); // non-blocking — call every loop()
}
```

---

## Multiple Home Screens

Register up to 4 home pages with `addHomeScreen()`. When more than one page is registered, the operator scrolls between them by rotating **knob2 (left knob)** while on the home screen. The library automatically draws a page indicator (`1/3`, `2/3`, `3/3`) at **row 3 cols 17–19** so the operator always knows how many pages there are and which one is showing.

`setHomeCallback()` still works as before — it is an alias for `addHomeScreen()`, so single-page sketches need no changes.

```cpp
#include

APALCDGUI gui;

// Page 1 — main sensor readings
void drawPage1(LiquidCrystal& lcd) {
lcd.setCursor(0, 0); lcd.print(F("pH 7.24 ORP 680mV "));
lcd.setCursor(0, 1); lcd.print(F("Cl 1.2 Temp 26 "));
lcd.write(CC_DEGREE); // ° at current cursor position
lcd.setCursor(0, 2); lcd.print(F("Filter ON 12:34 "));
lcd.setCursor(0, 3); lcd.print(F("System OK "));
// cols 17–19 of row 3 are the page indicator — do not write there
}

// Page 2 — weekly totals
void drawPage2(LiquidCrystal& lcd) {
lcd.setCursor(0, 0); lcd.print(F("Acid doses this week"));
lcd.setCursor(0, 1); lcd.print(F("pH-: 14 CL+: 22 "));
lcd.setCursor(0, 2); lcd.print(F("Last dose: 12:10 "));
lcd.setCursor(0, 3); lcd.print(F("Weekly total: 136 ml"));
}

// Page 3 — system status
void drawPage3(LiquidCrystal& lcd) {
lcd.setCursor(0, 0); lcd.print(F("Uptime: 3d 14h 22m "));
lcd.setCursor(0, 1); lcd.print(F("EEPROM writes: 1024"));
lcd.setCursor(0, 2); lcd.print(F("Backlight: 200/255"));
lcd.setCursor(0, 3); lcd.print(F("Firmware: v1.1.4 "));
}

void setup() {
gui.begin();
gui.addHomeScreen(drawPage1); // page 1 — shown first
gui.addHomeScreen(drawPage2); // page 2
gui.addHomeScreen(drawPage3); // page 3
// gui.addScreen(...) for submenus as usual
}

void loop() { gui.update(); }
```

**What the operator sees on page 2 of 3:**
```
0 1 2
01234567890123456789
Row0: Acid doses this week
Row1: pH-: 14 CL+: 22
Row2: Last dose: 12:10 [!]
Row3: Weekly total: 136 ml2/3
```

### Important layout constraint

The library writes at fixed positions on the home screen regardless of what your callback draws there:

| Position | What the library writes | Condition |
|----------|------------------------|-----------|
| Row 2 cols 17–19 | `[i]`, `[*]`, or `[!]` — passive alert indicator | always |
| Row 3 cols 17–19 | `1/3`, `2/3`, `3/3` — page indicator | only when `> 1` page registered |

If your callback writes anything to those positions, the library overwrites it on every `update()`. Leave them blank in your callbacks.

When only **one** page is registered (or `setHomeCallback()` used for a single screen), no page indicator is drawn and all 20 columns of row 3 belong to your callback.

---

## Timer Schedule Screen

The timer screen lets the operator set up to 3 on/off time slots directly on the LCD — no extra screens or menus needed. Each slot has a start and end time in 30-minute steps (00:00–23:30). A slot is disabled when both times are 00:00. Times are automatically saved to EEPROM when the operator presses SAVE and automatically loaded from EEPROM on `begin()`.

### What the operator sees

```
0 1 2
01234567890123456789
Row0: T1: 08:00-09:30
Row1: ►T2: 13:00-15:00 ← cursor is on this row
Row2: T3: 00:00-00:00 ← 00:00-00:00 means disabled
Row3: Total: 4h30m SAVE
```

When the operator presses KB2 on a timer row, the start time is selected for inline editing:

```
Row1: ►T2:[08:00]09:00 ← [ ] marks the active field; KB2 rotate changes the time
Row1: ►T2: 08:00[09:00] ← after confirming start, end is selected
```

### Controls

| Input | Action |
|-------|--------|
| KB2 rotate | Move cursor between T1 / T2 / T3 / SAVE |
| KB2 press on timer row | Enter inline edit — start time first, then end |
| KB2 press while editing | Confirm field and advance to the next (start → end → back to timer list) |
| KB2 press on SAVE row | Write to EEPROM, fire optional callback, return HOME |
| KB1 press | Return HOME, discard uncommitted edits |

### Registration

Call `addTimerScreen()` **after** all `addScreen()` calls on the same side. The timer screen always sits at the end of that side's rotation sequence.

```cpp
#include

APALCDGUI gui;

void drawHome(LiquidCrystal& lcd) { /* ... */ }

void onTimerSave() {
// Re-evaluate relay state immediately after the operator presses SAVE
}

void setup() {
gui.begin();
gui.addHomeScreen(drawHome);

// Regular submenu screens first
gui.addScreen(SCREEN_RIGHT, /* ... */);

// Timer screen last — always after addScreen() calls on the same side
gui.addTimerScreen(SCREEN_RIGHT, onTimerSave); // onSave is optional
}

void loop() { gui.update(); }
```

### Reading timer values in your control loop

```cpp
void checkSchedule() {
uint16_t nowMin = hour * 60 + minute; // minutes since midnight
bool shouldRun = false;
for (uint8_t i = 0; i < APA_LCD_MAX_TIMERS; i++) {
if (gui.isTimerEnabled(i) &&
nowMin >= gui.getTimerStart(i) &&
nowMin < gui.getTimerEnd(i)) {
shouldRun = true;
}
}
// drive relay from shouldRun
}
```

`getTimerStart(i)` and `getTimerEnd(i)` return minutes since midnight (0–1410). `isTimerEnabled(i)` returns `false` when both times are 00:00 (slot disabled). `getTimerTotalMinutes()` returns the sum of all enabled slot durations — useful as a daily target for external pump controllers:

```cpp
// Bridge to APAPUMP daily target
pump.begin(scheduleActive, nullptr, []() { return gui.getTimerTotalMinutes(); });
```

See `examples/06_timers/` for a complete pump control example.

### Extending to 6 timer slots

Define `APA_LCD_MAX_TIMERS` before including the library to increase slot count. The EEPROM address and layout expand automatically.

```cpp
#define APA_LCD_MAX_TIMERS 6
#include
```

> The LCD shows 3 timer slots at a time (rows 0–2); SAVE is always on row 3. When more than 3 slots are configured, the list scrolls: ↑ and ↓ indicators appear at the right edge of rows 0–1. `APA_LCD_MAX_TIMERS` supports up to 6.

---

## Understanding Field Factory Parameters

Every field on a screen is created by a factory function. Here is a full breakdown of `fieldFloat`:

```
┌── the function ────────────────────────────────────────────────────────────────┐
APALCDGUI::fieldFloat( F("pH setpoint"), F("pH"), &phSetpoint, 6.8f, 7.8f, 0.01f, 2 )
└── param 1 ───┘ └─ p2 ┘ └── p3 ─────┘ └─p4┘ └─p5┘ └─ p6┘ └p7
```

| # | Example | What it does |
|---|---------|--------------|
| Function | `APALCDGUI::fieldFloat(...)` | Creates a float editing field. Never called alone — always passed as an argument to `addScreen()`. Use `fieldInt` for whole numbers, `fieldChoice` for named options, etc. |
| 1 | `F("pH setpoint")` | Label shown on the left side of the row, up to 12 characters. `F(...)` keeps the text in flash memory — always use it for string literals. |
| 2 | `F("pH")` | Unit suffix shown after the value, up to 2 characters. Pass `nullptr` if no unit is needed. |
| 3 | `&phSetpoint` | Address of your `float` variable. The `&` gives the library direct access — it reads the current value and writes the new one when the operator presses SAVE. |
| 4 | `6.8f` | Minimum value. Rotating below this has no effect. The `f` suffix marks it as a float constant. |
| 5 | `7.8f` | Maximum value. Rotating above this has no effect. |
| 6 | `0.01f` | Change per encoder click. `0.01f` = fine, `0.1f` = medium, `1.0f` = coarse. |
| 7 | `2` | Decimal places shown on the LCD. `2` → `7.24`, `1` → `7.2`, `0` → `7`. |

**What the operator sees during editing:**
```
►pH setpoint 7.24pH
```

### All field factories at a glance

```cpp
// Integer value — operator scrolls between min and max by step
fieldInt(label, unit, int16_t* val, min, max, step = 1)

// Decimal value — same as INT but displays with a fixed number of decimal places
fieldFloat(label, unit, float* val, min, max, step, decimals)

// Named options — operator cycles through the list; each string MUST be exactly 4 chars
// Pad shorter strings with trailing spaces: {"AUTO", "MANU", "OFF ", nullptr}
fieldChoice(label, uint8_t* index, const char* choices[])

// On/off toggle — shows " ON " or "OFF "; rotate to preview, press to commit
fieldBool(label, bool* val)

// Button — shows "STRT" on SAVE; pressing flashes ► for 300 ms then fires fn()
// confirm=true adds a "Confirm action?" prompt (KB1=NO, KB2=YES) before firing
fieldAction(label, void (*fn)(), confirm = false)

// Display only — cursor skips this field; no editing possible
fieldReadonly(label, unit, float* val, decimals)
```

---

## API Reference

### Initialisation

| Method | Description |
|--------|-------------|
| `APALCDGUI(rs, en, d4..d7)` | Constructor — sets LCD pin assignments. All defaults match HMI board v1.0. LCD pins are fixed here; not in `begin()`. |
| `begin(blPin, enc1Clk, enc1Dt, enc1Btn, enc2Clk, enc2Dt, enc2Btn, det1, det2)` | Initialise LCD, attach encoder ISRs, load custom characters, restore brightness from EEPROM. All defaults match HMI board v1.0. |
| `update()` | Process encoders, buttons, timeouts, redraw LCD. Call every `loop()` — never blocks. |

### Screen registration

| Method | Description |
|--------|-------------|
| `addHomeScreen(fn)` | Register a home page. Call multiple times for a scrollable dashboard (KB2 scrolls pages). Returns `false` if `APA_LCD_MAX_HOME_SCREENS` is reached. |
| `setHomeCallback(fn)` | Alias for `addHomeScreen()` — single-page sketches need no changes. |
| `setMenuRow2Callback(fn)` | Draw live sensor data on row 2 of every 1- and 2-field screen (cols 0–16 only). |
| `addScreen(side, field1, onSave, title)` | Register a 1-field screen. |
| `addScreen(side, field1, field2, onSave, title)` | Register a 2-field screen (most common). |
| `addScreen(side, field1, field2, field3, onSave, title)` | Register a 3-field screen. |
| `addTimerScreen(side, onSave)` | Register the timer schedule screen on `side`. Call after all `addScreen()` on that side. `onSave` is optional — times are saved to EEPROM regardless. |
| `setRTC(DS3231*)` | Wire 800 ms both-buttons-hold gesture to built-in time/date modal. Requires build flag `-DAPA_LCD_USE_DS3231`. |

### Field factories

| Factory | Description |
|---------|-------------|
| `fieldInt(label, unit, int16_t*, min, max, step=1)` | Integer field, scrolls by step. |
| `fieldFloat(label, unit, float*, min, max, step, decimals)` | Float field, displayed with N decimal places. |
| `fieldChoice(label, uint8_t*, const char*[])` | Cycles through null-terminated string array. Each string must be exactly 4 chars. |
| `fieldBool(label, bool*)` | Toggle — shows `" ON "` / `"OFF "`. |
| `fieldAction(label, fn, confirm=false)` | Button — shows `"STRT"`. Press flashes `►` for 300 ms then fires `fn()`. `confirm=true` adds a confirmation prompt first. |
| `fieldReadonly(label, unit, float*, decimals)` | Display only — cursor skips this field. |

### Alerts and status indicator

| Method | Description |
|--------|-------------|
| `postAlert(l1, l2, level)` | Show passive corner indicator. Levels: `ALERT_INFO`, `ALERT_WARNING`, `ALERT_CRITICAL`. |
| `clearAlert()` | Dismiss passive alert. |
| `hasAlert()` | True if passive alert is active. |
| `postActiveAlert(l1, l2, level, ackCb)` | Add to active alert queue (max 3). Replaces home screen until acknowledged. |
| `cancelActiveAlert()` | Remove current active alert silently (for auto-cleared alarms). |
| `hasActiveAlert()` / `activeAlertCount()` | Query active alert state. |
| `setStatusIndicator(char c)` | Show `[c]` in cols 17–19 row 2 when no alert is pending. Example: `'M'` for pump manual mode. Alert always takes priority. |
| `clearStatusIndicator()` | Remove the status indicator; corner shows blank when no alert is pending. |

### Backlight and timeouts

| Method | Description |
|--------|-------------|
| `setBacklight(bool)` | Force on or off immediately. |
| `setBacklightTimeout(seconds)` | Off timeout; dim fires at 40% of this value. `0` = always on. Default 300 s. |
| `setMenuTimeout(seconds)` | Return to HOME after idle. `0` = disabled. Default 60 s. |

### Gestures and overlays

| Method | Description |
|--------|-------------|
| `setLongPressCallback(enc, fn)` | 800 ms hold: `0` = right knob (KB1), `1` = left knob (KB2). |
| `setBothPressedCallback(fn)` | Both buttons within 200 ms — always wins over RTC modal. |
| `showMessage(l1, l2, ms)` | Timed message covering all 4 rows (line1→row 0, line2→row 1, rows 2–3 blanked). Default 1500 ms. |
| `clearMessage()` | Dismiss overlay early. |
| `markDirty()` | Schedule a full LCD redraw on the next `update()`. |
| `isMenuActive()` | True when not at HOME. |
| `isEditActive()` | True during EDIT, RTC_EDIT, or TIMER_EDIT state. |
| `currentScreen()` | Screen position: `0` = HOME, `+N` = right screen N, `-N` = left screen N. |
| `currentHomePage()` | Index of the currently displayed home page (0-based). |
| `homePageCount()` | Number of home pages registered via `addHomeScreen()`. |
| `getBrightness()` | Current backlight level (0–255, EEPROM-persisted). |
| `getTimerStart(i)` | Start time for slot `i` in minutes since midnight (0–1410). Returns 0 if index out of range. |
| `getTimerEnd(i)` | End time for slot `i` in minutes since midnight (0–1410). Returns 0 if index out of range. |
| `isTimerEnabled(i)` | `true` when slot `i` has a non-zero start or end time (i.e., is not disabled). |
| `getTimerTotalMinutes()` | Sum of all enabled timer slot durations in minutes. Use as a daily target for external pump controllers. Returns 0 if no timers registered or all slots disabled. |

---

## Wiring APADOSE Alarms to APALCDGUI

Libraries are independent — user code is the bridge:

```cpp
dose1.setAlarmCallback([](AlarmType alarm) {
if (alarm == ALARM_NONE) {
gui.cancelActiveAlert();
return;
}
if (alarm == ALARM_INEFFECTIVE || alarm == ALARM_WRONG_DIRECTION
|| alarm == ALARM_TANK_EMPTY || alarm == ALARM_OFA) {
gui.postActiveAlert(F("ALARM: Ineffective"), F("pH-minus pump #1"),
ALERT_CRITICAL, []() { dose1.acknowledgeAlarm(); });
} else {
gui.postAlert(F("Warning: Safety band"), F("pH-minus"), ALERT_WARNING);
}
});
```

---

## DS3231 RTC — Time and Date Setting

The library includes a built-in time/date edit modal for the DS3231 module. Enable it by defining `APA_LCD_USE_DS3231` — the library then pulls in `Wire.h` and `DS3231.h` automatically.

**Wiring (Mega 2560):** SDA → pin 20, SCL → pin 21, VCC → 3.3 V, GND → GND.

**`platformio.ini`:**
```ini
lib_deps =
arduino-libraries/LiquidCrystal @ ^1.0.7
NorthernWidget/DS3231
build_flags = -DAPA_LCD_USE_DS3231
```

**Arduino IDE** — add before `#include`:
```cpp
#define APA_LCD_USE_DS3231
#include
```

**Sketch:**
```cpp
#include // Wire.h and DS3231.h are included automatically

APALCDGUI gui;
DS3231 rtc;

void setup() {
Wire.begin(); // required — initialise I2C bus
gui.begin();
gui.setRTC(&rtc); // wires both-buttons gesture to the modal
// addScreen() calls ...
}
```

**Operation:** hold both buttons for 800 ms → the TIME screen opens. Knob2 moves the cursor between the three time fields (HH / MM / SS); press to enter edit, rotate to change, press to confirm the new value. Press SAVE to advance to the DATE screen (DD / MM / YYYY). Press SAVE again to write all values to the DS3231 and return to HOME. BACK on the DATE screen returns to TIME; BACK on the TIME screen discards all changes.

> `setBothPressedCallback()` takes priority over the RTC modal — do not set it if you want the modal to work.

See `examples/04_rtc/04_rtc.ino` for a full working example with live clock display on the home screen.

---

## Examples

| Sketch | Description |
|--------|-------------|
| `examples/01_minimal/` | Simplest working sketch — one screen, two fields, home callback |
| `examples/02_8screens/` | All 6 field types across 8 screens — best starting point for new projects |
| `examples/03_alerts/` | Passive and active alert system demonstration |
| `examples/04_rtc/` | DS3231 real-time clock — live time display and time/date set modal |
| `examples/05_multi_home/` | Three scrollable home pages with automatic page indicator |
| `examples/06_timers/` | Timer schedule screen — pump control with 3 on/off slots and EEPROM persistence |

---

## Configurable Limits

Define these **before** `#include `:

```cpp
#define APA_LCD_MAX_SCREENS 4 // total submenu screens left+right (default 4)
#define APA_LCD_MAX_HOME_SCREENS 4 // home screen pages scrolled by KB2 (default 4)
#define APA_LCD_ACTIVE_ALERT_QUEUE 3 // simultaneous active alerts (default 3)
#define APA_LCD_EEPROM_ADDR 500 // EEPROM base address (default 500, uses 2 bytes)
#define APA_LCD_MAX_TIMERS 3 // timer slots on the schedule screen (default 3, max 6)
#define APA_LCD_TIMER_EEPROM_ADDR 502 // EEPROM start address for timer data (default 502, uses 7 bytes)
```

---

## Platform Verification

Compiled and size-checked with the `02_8screens` example using the default 4-screen limit on all supported platforms. Zero errors, zero library warnings.

| Platform | Board | Clock | RAM used | RAM total | Flash used | Flash total |
|----------|-------|-------|----------|-----------|------------|-------------|
| Arduino Mega 2560 | ATmega2560 | 16 MHz | 1 361 B | 8 192 B (17%) | 21 326 B | 253 952 B (8%) |
| Arduino Uno | ATmega328P | 16 MHz | 1 349 B | 2 048 B (66%) | 19 420 B | 32 256 B (60%) |
| ESP32 DevKit | ESP32 | 240 MHz | 23 380 B | 327 680 B (7%) | 299 581 B | 1 310 720 B (23%) |
| ESP8266 D1 Mini | ESP8266 | 80 MHz | 29 984 B | 81 920 B (37%) | 284 415 B | 1 044 464 B (27%) |
| STM32 Bluepill | STM32F103C8 | 72 MHz | 3 448 B | 20 480 B (17%) | 38 484 B | 65 536 B (59%) |

> The Uno row shows 66% RAM with the 4-screen example — that includes the full `02_8screens` sketch overhead (6 field types, 8 registrations capped at 4, home + alert callbacks). The library core alone is smaller. For production Uno use, a 2–3 screen sketch will sit comfortably below 50%.
>
> ESP32 and ESP8266 totals include the full Arduino framework (WiFi stack etc.) regardless of whether it is used.

---

## License

**Dual license — see LICENSE file for full terms.**

| Use | License |
|-----|---------|
| Personal, private, educational, hobby | Free — no charge, no paperwork |
| Commercial (products, services, OEM) | Separate written license required |

Commercial use includes selling or distributing hardware with this software, providing paid water treatment or automation services, or integrating it into any revenue-generating product or system.

To obtain a commercial license: [kecup@vazac.eu](mailto:kecup@vazac.eu)

---

*APALCDGUI — APA Devices · [kecup@vazac.eu](mailto:kecup@vazac.eu)*