Skip to content

apadevices/APALCDGUI

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

71 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

APALCDGUI

APALCDGUI

Parallel 20×4 LCD menu system with dual rotary encoders for APA Devices water treatment automation · v1.4.0 · Platforms


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:

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

#include <APALCDGUI.h>

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.

#include <APALCDGUI.h>

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.

#include <APALCDGUI.h>

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

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:

// 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.

#define APA_LCD_MAX_TIMERS 6
#include <APALCDGUI.h>

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. 27.24, 17.2, 07.

What the operator sees during editing:

►pH setpoint  7.24pH

All field factories at a glance

// 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:

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:

lib_deps =
    arduino-libraries/LiquidCrystal @ ^1.0.7
    NorthernWidget/DS3231
build_flags = -DAPA_LCD_USE_DS3231

Arduino IDE — add before #include:

#define APA_LCD_USE_DS3231
#include <APALCDGUI.h>

Sketch:

#include <APALCDGUI.h>   // 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 <APALCDGUI.h>:

#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


APALCDGUI — APA Devices · kecup@vazac.eu

About

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

Topics

Resources

License

Stars

Watchers

Forks

Packages

 
 
 

Contributors

Languages