// Copyright (C) 2026 Hector van der Aa // Copyright (C) 2026 Association Exergie // SPDX-License-Identifier: GPL-3.0-or-later #include "lcd.h" #include #include #include "modules/gps/gps.h" #include "data/general_store.h" #define MOD "modules/lcd/lcd.h" void Lcd::clear() { if (!display_cleared_) { display_->clear(); display_cleared_ = true; } } void Lcd::print(const String &msg) { display_->print(msg); display_cleared_ = false; } void Lcd::print(char c) { display_->print(c); display_cleared_ = false; } void Lcd::print(const char c[]) { display_->print(c); display_cleared_ = false; } void Lcd::print(float d, int digits) { display_->print(d, digits); display_cleared_ = false; } void Lcd::print(unsigned long l, int base) { display_->print(l, base); display_cleared_ = false; } void Lcd::print(long l, int base) { display_->print(l, base); display_cleared_ = false; } void Lcd::print(unsigned int i, int base) { display_->print(i, base); display_cleared_ = false; } void Lcd::print(int i, int base) { display_->print(i, base); display_cleared_ = false; } bool Lcd::isMessageTask(task::Type type) { switch (type) { case task::DisplayMsgGpsFix: case task::DisplayMsgTrackDetectOk: case task::DisplayMsgConfigNoTracks: case task::DisplayMsgBatteryLow: case task::DisplayMsgEngineTempLow: case task::DisplayMsgEngineTempHigh: return true; default: return false; } } void Lcd::activateMessage(screen::LcdScreen msg_screen, unsigned long duration_ms) { if (duration_ms == 0) { duration_ms = frame_duration_; } message_screen_ = msg_screen; message_active_ = true; message_end_ = millis() + duration_ms; screen_ = message_screen_; force_render_ = true; } void Lcd::expireMessageIfNeeded(unsigned long now) { if (!message_active_) { return; } if ((long)(now - message_end_) >= 0) { message_active_ = false; message_screen_ = screen::Blank; screen_ = data_screen_; force_render_ = true; } } screen::LcdScreen Lcd::getActiveScreen() const { if (message_active_) { return message_screen_; } return data_screen_; } int Lcd::renderGpsDebug() { this->clear(); GpsData gps_data; gpsGlobalRead(gps_data); display_->setCursor(0, 0); this->print("Alt: "); if (gps_data.altitude_.valid_) { this->print(gps_data.altitude_.value_, 5); } else { this->print("not valid"); } display_->setCursor(0, 1); this->print("Lat: "); if (gps_data.lat_.valid_) { this->print(gps_data.lat_.value_, 5); } else { this->print("not valid"); } display_->setCursor(0, 2); this->print("Lng: "); if (gps_data.lng_.valid_) { this->print(gps_data.lng_.value_, 5); } else { this->print("not valid"); } display_->setCursor(0, 3); this->print("Spd: "); if (gps_data.speed_.valid_) { this->print(gps_data.speed_.value_, 5); } else { this->print("not valid"); } return 0; } int Lcd::renderDriverPrimary() { this->clear(); GpsData gps_data; gpsGlobalRead(gps_data); float vbat; vbatGlobalRead(vbat); float teng; tengGlobalRead(teng); int line_trigger; gpsTriggerGlobalRead(line_trigger); display_->setCursor(0,0); this->print("GPS:"); if (gps_data.num_fix_ != 0) { this->print("Y"); } else { this->print("N"); } display_->setCursor(7,0); this->print("TRIG:"); switch (line_trigger) { case 0: this->print("I"); break; case 1: this->print("A"); break; case 2: this->print("T"); break; default: this->print("NULL"); break; } display_->setCursor(0,2); this->print("SPD:"); if (gps_data.speed_.valid_) { this->print(gps_data.speed_.value_, 1); } else { this->print("NA"); } display_->setCursor(0,3); this->print("V:"); this->print(vbat, 1); display_->setCursor(10,3); this->print("T:"); this->print(teng, 1); return 0; } int Lcd::renderMsgGpsFix() { this->clear(); display_->setCursor(6, 1); this->print("GPS INFO"); display_->setCursor(7, 2); this->print("FIX OK"); return 0; } int Lcd::renderMsgGpsTrigger() { this->clear(); display_->setCursor(6, 1); this->print("GPS INFO"); display_->setCursor(4, 2); this->print("LINE TRIGGER"); return 0; } int Lcd::renderMsgTrackDetectOk() { this->clear(); display_->setCursor(6, 0); this->print("GPS INFO"); display_->setCursor(3, 1); this->print("TRACK DETECTED"); GlobalTrackData track_data; trackGlobalRead(track_data); display_->setCursor((20 - strlen(track_data.root_.name_)) / 2, 2); this->print(track_data.root_.name_); return 0; } int Lcd::renderMsgConfigNoTracks() { this->clear(); display_->setCursor(4, 1); this->print("CONFIG INFO"); display_->setCursor(2, 2); this->print("NO TRACKS LOADED"); return 0; } int Lcd::renderMsgBatteryLow() { this->clear(); display_->setCursor(6, 1); this->print("WARNING"); display_->setCursor(6, 2); this->print("VBAT LOW"); return 0; } int Lcd::renderMsgEngineTempLow() { this->clear(); display_->setCursor(6, 1); this->print("WARNING"); display_->setCursor(2, 2); this->print("ENGINE TEMP LOW"); return 0; } int Lcd::renderMsgEngineTempHigh() { this->clear(); display_->setCursor(6, 1); this->print("WARNING"); display_->setCursor(2, 2); this->print("ENGINE TEMP HIGH"); return 0; } int Lcd::push(const Task &task) { return queue_.push(task); } Lcd::Lcd() : display_cleared_(false), logger_(nullptr), screen_(screen::Blank), data_screen_(screen::Blank), message_screen_(screen::Blank), last_render_(0), frame_duration_(2000) { display_ = new LiquidCrystal_I2C(0x27, 20, 4); } Lcd::Lcd(SystemLogger *logger) : display_cleared_(false), logger_(logger), screen_(screen::Blank), data_screen_(screen::Blank), message_screen_(screen::Blank), last_render_(0), frame_duration_(2000) { display_ = new LiquidCrystal_I2C(0x27, 20, 4); } Lcd::~Lcd() { delete display_; display_ = nullptr; } int Lcd::init() { #ifdef DEEP_DEBUG if (logger_ != nullptr) { logger_->deepDebug(String(MOD) + ": init: Begin"); } #endif display_->init(); Wire.setClock(400000); display_->backlight(); this->clear(); display_->setCursor(0, 0); force_render_ = true; #ifdef DEEP_DEBUG if (logger_ != nullptr) { logger_->deepDebug(String(MOD) + ": init: End"); } #endif return 0; } int Lcd::printMessage(String message) { #ifdef DEEP_DEBUG if (logger_ != nullptr) { logger_->deepDebug(String(MOD) + ": printMessage: Begin"); } #endif String original = message; this->clear(); if (message.length() > 80) { message = message.substring(0, 80); } String lines[4] = {"", "", "", ""}; int lineIndex = 0; while (message.length() > 0 && lineIndex < 4) { if (message.length() <= 20) { lines[lineIndex++] = message; break; } int splitIndex = message.lastIndexOf(' ', 20); if (splitIndex == -1 || splitIndex == 0) { splitIndex = 20; } lines[lineIndex++] = message.substring(0, splitIndex); if (splitIndex < message.length() && message.charAt(splitIndex) == ' ') { message = message.substring(splitIndex + 1); } else { message = message.substring(splitIndex); } } int usedLines = 0; for (int i = 0; i < 4; i++) { if (lines[i].length() > 0) { usedLines++; } } int startRow = 0; if (usedLines == 1) { startRow = 1; } else if (usedLines == 2) { startRow = 1; } else { startRow = 0; } int currentRow = startRow; for (int i = 0; i < 4; i++) { if (lines[i].length() == 0) { continue; } int col = (20 - lines[i].length()) / 2; if (col < 0) { col = 0; } display_->setCursor(col, currentRow++); this->print(lines[i]); } #ifdef INFO if (logger_ != nullptr) { logger_->info(original); } #endif #ifdef DEEP_DEBUG if (logger_ != nullptr) { logger_->deepDebug(String(MOD) + ": printMessage: End"); } #endif return 0; } int Lcd::loop(unsigned long timeout_ms) { unsigned long now = millis(); unsigned long start = now; expireMessageIfNeeded(now); while (true) { Task next_task; bool have_task = false; if (deferred_task_valid_) { next_task = deferred_task_; deferred_task_valid_ = false; have_task = true; } else { if (queue_.pop(next_task) == 0) { have_task = true; } } if (!have_task) { break; } if (message_active_ && isMessageTask(next_task.type_)) { deferred_task_ = next_task; deferred_task_valid_ = true; break; } switch (next_task.type_) { case task::DisplayGpsDebug: data_screen_ = screen::GpsDebug; if (!message_active_) { screen_ = data_screen_; force_render_ = true; } break; case task::DisplayDriverPrimary: data_screen_ = screen::DriverPrimary; if (!message_active_) { screen_ = data_screen_; force_render_ = true; } break; case task::DisplayMsgGpsFix: activateMessage(screen::MsgGpsFix, next_task.data_); break; case task::DisplayMsgTrackDetectOk: activateMessage(screen::MsgTrackDetectOk, next_task.data_); break; case task::DisplayMsgConfigNoTracks: activateMessage(screen::MsgConfigNoTracks, next_task.data_); break; case task::DisplayMsgBatteryLow: activateMessage(screen::MsgBatteryLow, next_task.data_); break; case task::DisplayMsgEngineTempLow: activateMessage(screen::MsgEngineTempLow, next_task.data_); break; case task::DisplayMsgEngineTempHigh: activateMessage(screen::MsgEngineTempHigh, next_task.data_); break; default: break; } now = millis(); expireMessageIfNeeded(now); if ((unsigned long)(now - start) >= timeout_ms) { break; } } now = millis(); expireMessageIfNeeded(now); screen::LcdScreen active_screen = getActiveScreen(); if (screen_ != active_screen) { screen_ = active_screen; force_render_ = true; } if (!force_render_ && (unsigned long)(now - last_render_) < frame_duration_) { return 1; } switch (screen_) { case screen::Blank: this->clear(); break; case screen::GpsDebug: this->renderGpsDebug(); break; case screen::DriverPrimary: this->renderDriverPrimary(); break; case screen::MsgGpsFix: this->renderMsgGpsFix(); break; case screen::MsgGpsTrigger: this->renderMsgGpsTrigger(); break; case screen::MsgTrackDetectOk: this->renderMsgTrackDetectOk(); break; case screen::MsgConfigNoTracks: this->renderMsgConfigNoTracks(); break; case screen::MsgBatteryLow: this->renderMsgBatteryLow(); break; case screen::MsgEngineTempLow: this->renderMsgEngineTempLow(); break; case screen::MsgEngineTempHigh: this->renderMsgEngineTempHigh(); break; default: break; } last_render_ = now; force_render_ = false; return 1; } #undef MOD