mirror of
https://github.com/pelgraine/Meck.git
synced 2026-03-28 17:42:44 +01:00
Compare commits
17 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
15fcb08441 | ||
|
|
48863a13aa | ||
|
|
98addc5bd3 | ||
|
|
a10ce4e7ca | ||
|
|
dcb59f2e33 | ||
|
|
9e40dc7f30 | ||
|
|
5ed1edc287 | ||
|
|
ba057c8aad | ||
|
|
8e91327230 | ||
|
|
7444a34e5a | ||
|
|
17bad1ddf7 | ||
|
|
a4c86d2d31 | ||
|
|
8dd661246a | ||
|
|
f07032182e | ||
|
|
1d39d9f314 | ||
|
|
a4285d2f24 | ||
|
|
fad548b995 |
@@ -8,11 +8,11 @@
|
||||
#define FIRMWARE_VER_CODE 8
|
||||
|
||||
#ifndef FIRMWARE_BUILD_DATE
|
||||
#define FIRMWARE_BUILD_DATE "14 Feb 2026"
|
||||
#define FIRMWARE_BUILD_DATE "17 Feb 2026"
|
||||
#endif
|
||||
|
||||
#ifndef FIRMWARE_VERSION
|
||||
#define FIRMWARE_VERSION "Meck v0.8.8A"
|
||||
#define FIRMWARE_VERSION "Meck v0.9.1A-NB"
|
||||
#endif
|
||||
|
||||
#if defined(NRF52_PLATFORM) || defined(STM32_PLATFORM)
|
||||
|
||||
@@ -29,4 +29,5 @@ struct NodePrefs { // persisted to file
|
||||
uint32_t gps_interval; // GPS read interval in seconds
|
||||
uint8_t autoadd_config; // bitmask for auto-add contacts config
|
||||
int8_t utc_offset_hours; // UTC offset in hours (-12 to +14), default 0
|
||||
uint8_t kb_flash_notify; // Keyboard backlight flash on new message (0=off, 1=on)
|
||||
};
|
||||
@@ -1455,8 +1455,12 @@ void audio_info(const char *info) {
|
||||
|
||||
void audio_eof_mp3(const char *info) {
|
||||
Serial.printf("Audio: End of file - %s\n", info);
|
||||
// Playback finished — the player screen will detect this
|
||||
// via audio.isRunning() returning false
|
||||
// Signal the player screen for auto-advance to next track
|
||||
AudiobookPlayerScreen* abPlayer =
|
||||
(AudiobookPlayerScreen*)ui_task.getAudiobookScreen();
|
||||
if (abPlayer) {
|
||||
abPlayer->onEOF();
|
||||
}
|
||||
}
|
||||
|
||||
#endif // LilyGo_TDeck_Pro
|
||||
@@ -18,6 +18,8 @@
|
||||
// PLAYER mode: Enter = play/pause, A = -30s, D = +30s,
|
||||
// W = volume up, S = volume down,
|
||||
// [ = prev chapter, ] = next chapter,
|
||||
// N = next track in playlist,
|
||||
// Z = toggle 45-min sleep timer,
|
||||
// Q = leave (audio continues) / close book (if paused)
|
||||
//
|
||||
// Library dependencies (add to platformio.ini lib_deps):
|
||||
@@ -29,6 +31,7 @@
|
||||
#include <helpers/ui/DisplayDriver.h>
|
||||
#include <SD.h>
|
||||
#include <vector>
|
||||
#include <algorithm>
|
||||
#include "M4BMetadata.h"
|
||||
|
||||
// Audio library — ESP32-audioI2S by schreibfaul1
|
||||
@@ -200,6 +203,15 @@ private:
|
||||
int _transportSel;
|
||||
bool _showingInfo;
|
||||
|
||||
// Sleep timer — press Z to start 45min countdown, Z again or pause to cancel
|
||||
bool _sleepTimerActive;
|
||||
unsigned long _sleepTimerEnd; // millis() when timer expires
|
||||
|
||||
// Playlist / track queue — all playable files in the current directory
|
||||
std::vector<String> _playlist; // Sorted filenames in _currentPath
|
||||
int _playlistIdx; // Current track index (-1 = no playlist)
|
||||
volatile bool _eofFlag; // Set by audio_eof_mp3 callback
|
||||
|
||||
// Power on the PCM5102A DAC via GPIO 41 (BOARD_6609_EN).
|
||||
// On the audio variant, this pin supplies power to the DAC circuit.
|
||||
// TDeckBoard::begin() sets it LOW ("disable modem") which starves the DAC.
|
||||
@@ -671,6 +683,17 @@ private:
|
||||
int dot = cleaned.lastIndexOf('.');
|
||||
if (dot > 0) cleaned = cleaned.substring(0, dot);
|
||||
cleaned.replace("_", " ");
|
||||
|
||||
// In subdirectories, filenames often follow "Artist - Album - NN Track"
|
||||
// pattern. The folder already provides context, so extract just the
|
||||
// last segment after " - " to show the track-relevant part.
|
||||
if (_currentPath != String(AUDIOBOOKS_FOLDER)) {
|
||||
int lastSep = cleaned.lastIndexOf(" - ");
|
||||
if (lastSep > 0 && lastSep < (int)cleaned.length() - 3) {
|
||||
cleaned = cleaned.substring(lastSep + 3);
|
||||
}
|
||||
}
|
||||
|
||||
entry.displayTitle = cleaned;
|
||||
}
|
||||
|
||||
@@ -694,6 +717,18 @@ private:
|
||||
root.close();
|
||||
digitalWrite(SDCARD_CS, HIGH);
|
||||
|
||||
// Sort directories and files alphabetically (case-insensitive)
|
||||
std::sort(dirs.begin(), dirs.end(), [](const AudiobookFileEntry& a, const AudiobookFileEntry& b) {
|
||||
String la = a.name; la.toLowerCase();
|
||||
String lb = b.name; lb.toLowerCase();
|
||||
return la < lb;
|
||||
});
|
||||
std::sort(files.begin(), files.end(), [](const AudiobookFileEntry& a, const AudiobookFileEntry& b) {
|
||||
String la = a.name; la.toLowerCase();
|
||||
String lb = b.name; lb.toLowerCase();
|
||||
return la < lb;
|
||||
});
|
||||
|
||||
// Append directories first, then files
|
||||
for (auto& d : dirs) _fileList.push_back(d);
|
||||
for (auto& fi : files) _fileList.push_back(fi);
|
||||
@@ -709,6 +744,154 @@ private:
|
||||
_currentPath.c_str(), (int)dirs.size(), (int)files.size(), cacheHits);
|
||||
}
|
||||
|
||||
// ---- Playlist / Track Queue ----
|
||||
// Builds a sorted list of all playable files in the current directory.
|
||||
// Called when opening a file — enables auto-advance and skip.
|
||||
|
||||
void buildPlaylist(const String& startingFile) {
|
||||
_playlist.clear();
|
||||
_playlistIdx = -1;
|
||||
|
||||
File dir = SD.open(_currentPath.c_str());
|
||||
if (!dir || !dir.isDirectory()) return;
|
||||
|
||||
File f = dir.openNextFile();
|
||||
while (f) {
|
||||
if (!f.isDirectory()) {
|
||||
String name = String(f.name());
|
||||
int slash = name.lastIndexOf('/');
|
||||
if (slash >= 0) name = name.substring(slash + 1);
|
||||
if (!name.startsWith(".") && !name.startsWith("._") && isAudiobookFile(name)) {
|
||||
_playlist.push_back(name);
|
||||
}
|
||||
}
|
||||
f = dir.openNextFile();
|
||||
}
|
||||
dir.close();
|
||||
digitalWrite(SDCARD_CS, HIGH);
|
||||
|
||||
// Sort alphabetically (case-insensitive)
|
||||
std::sort(_playlist.begin(), _playlist.end(), [](const String& a, const String& b) {
|
||||
String la = a; la.toLowerCase();
|
||||
String lb = b; lb.toLowerCase();
|
||||
return la < lb;
|
||||
});
|
||||
|
||||
// Find the starting file's index
|
||||
for (int i = 0; i < (int)_playlist.size(); i++) {
|
||||
if (_playlist[i] == startingFile) {
|
||||
_playlistIdx = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
Serial.printf("AB: Playlist built — %d tracks, current idx %d\n",
|
||||
(int)_playlist.size(), _playlistIdx);
|
||||
}
|
||||
|
||||
// Advance to next/previous track in playlist.
|
||||
// direction: +1 = next, -1 = previous
|
||||
// Returns true if a new track was started.
|
||||
bool advanceTrack(int direction) {
|
||||
if (_playlist.size() <= 1 || _playlistIdx < 0) return false;
|
||||
|
||||
int newIdx = _playlistIdx + direction;
|
||||
if (newIdx < 0 || newIdx >= (int)_playlist.size()) {
|
||||
Serial.println("AB: End of playlist reached");
|
||||
// End of playlist — stop playback
|
||||
stopPlayback();
|
||||
return false;
|
||||
}
|
||||
|
||||
// Stop current track cleanly
|
||||
if (_audio) {
|
||||
_audio->stopSong();
|
||||
}
|
||||
_isPlaying = false;
|
||||
_isPaused = false;
|
||||
_pendingSeekSec = 0;
|
||||
_streamReady = false;
|
||||
restoreM4bRename();
|
||||
// Power down DAC briefly (startPlayback will re-enable it)
|
||||
disableDAC();
|
||||
_i2sInitialized = false;
|
||||
|
||||
// Save bookmark for current track before switching
|
||||
saveBookmark();
|
||||
|
||||
// Free old cover art and metadata
|
||||
freeCoverBitmap();
|
||||
_metadata.clear();
|
||||
|
||||
// Switch to new track
|
||||
_playlistIdx = newIdx;
|
||||
String nextFile = _playlist[_playlistIdx];
|
||||
Serial.printf("AB: Advancing to track %d/%d: %s\n",
|
||||
_playlistIdx + 1, (int)_playlist.size(), nextFile.c_str());
|
||||
|
||||
// Reset state for new track
|
||||
_currentFile = nextFile;
|
||||
_currentPosSec = 0;
|
||||
_durationSec = 0;
|
||||
_currentChapter = -1;
|
||||
_lastPosUpdate = 0;
|
||||
_currentFileSize = 0;
|
||||
_eofFlag = false;
|
||||
|
||||
// Find file size from the file list (if available)
|
||||
for (const auto& fe : _fileList) {
|
||||
if (fe.name == nextFile) {
|
||||
_currentFileSize = fe.fileSize;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Parse metadata for new track
|
||||
String fullPath = _currentPath + "/" + nextFile;
|
||||
File file = SD.open(fullPath.c_str(), FILE_READ);
|
||||
if (file) {
|
||||
if (_currentFileSize == 0) _currentFileSize = file.size();
|
||||
String lower = nextFile;
|
||||
lower.toLowerCase();
|
||||
if (lower.endsWith(".m4b") || lower.endsWith(".m4a")) {
|
||||
_metadata.parse(file);
|
||||
yield();
|
||||
decodeCoverArt(file);
|
||||
yield();
|
||||
} else if (lower.endsWith(".mp3")) {
|
||||
_metadata.parseID3v2(file);
|
||||
yield();
|
||||
decodeCoverArt(file);
|
||||
yield();
|
||||
if (_metadata.title[0] == '\0') {
|
||||
String base = nextFile;
|
||||
int dot = base.lastIndexOf('.');
|
||||
if (dot > 0) base = base.substring(0, dot);
|
||||
strncpy(_metadata.title, base.c_str(), M4B_MAX_TITLE - 1);
|
||||
}
|
||||
} else {
|
||||
String base = nextFile;
|
||||
int dot = base.lastIndexOf('.');
|
||||
if (dot > 0) base = base.substring(0, dot);
|
||||
strncpy(_metadata.title, base.c_str(), M4B_MAX_TITLE - 1);
|
||||
}
|
||||
file.close();
|
||||
}
|
||||
digitalWrite(SDCARD_CS, HIGH);
|
||||
|
||||
// Load bookmark for new track (may resume a previous position)
|
||||
loadBookmark();
|
||||
|
||||
if (_audio) _audio->setVolume(_volume);
|
||||
if (_metadata.durationMs > 0) _durationSec = _metadata.durationMs / 1000;
|
||||
|
||||
// Start playing the new track
|
||||
_lastPositionSave = millis();
|
||||
startPlayback();
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
// ---- Book Open / Close ----
|
||||
|
||||
void openBook(const String& filename, DisplayDriver* display) {
|
||||
@@ -803,6 +986,11 @@ private:
|
||||
}
|
||||
|
||||
_mode = PLAYER;
|
||||
_eofFlag = false;
|
||||
|
||||
// Build playlist from current directory for track queuing
|
||||
buildPlaylist(filename);
|
||||
|
||||
Serial.printf("AB: Opened '%s' -- %s by %s, %us, %d chapters\n",
|
||||
filename.c_str(), _metadata.title, _metadata.author,
|
||||
_durationSec, _metadata.chapterCount);
|
||||
@@ -818,6 +1006,10 @@ private:
|
||||
_metadata.clear();
|
||||
_bookOpen = false;
|
||||
_currentFile = "";
|
||||
_sleepTimerActive = false;
|
||||
_playlist.clear();
|
||||
_playlistIdx = -1;
|
||||
_eofFlag = false;
|
||||
_mode = FILE_LIST;
|
||||
}
|
||||
|
||||
@@ -826,6 +1018,8 @@ private:
|
||||
void startPlayback() {
|
||||
if (!_audio || _currentFile.length() == 0) return;
|
||||
|
||||
_eofFlag = false; // Clear any stale EOF from previous track
|
||||
|
||||
// Ensure DAC has power (must be re-enabled after each stop)
|
||||
enableDAC();
|
||||
|
||||
@@ -886,6 +1080,8 @@ private:
|
||||
_isPaused = false;
|
||||
_pendingSeekSec = 0;
|
||||
_streamReady = false;
|
||||
_sleepTimerActive = false; // Cancel sleep timer on stop
|
||||
_eofFlag = false;
|
||||
saveBookmark();
|
||||
|
||||
// Restore .m4b filename if we renamed it for playback
|
||||
@@ -906,6 +1102,7 @@ private:
|
||||
if (_isPlaying && !_isPaused) {
|
||||
_audio->pauseResume();
|
||||
_isPaused = true;
|
||||
_sleepTimerActive = false; // Cancel sleep timer on pause
|
||||
saveBookmark();
|
||||
} else if (_isPaused) {
|
||||
_audio->pauseResume();
|
||||
@@ -1216,25 +1413,54 @@ private:
|
||||
{
|
||||
display.setTextSize(1);
|
||||
display.setColor(DisplayDriver::LIGHT);
|
||||
|
||||
// Show track position in playlist (if multiple tracks)
|
||||
if (_playlist.size() > 1 && _playlistIdx >= 0) {
|
||||
char trackBuf[24];
|
||||
snprintf(trackBuf, sizeof(trackBuf), "Track %d/%d",
|
||||
_playlistIdx + 1, (int)_playlist.size());
|
||||
display.drawTextCentered(display.width() / 2, y, trackBuf);
|
||||
y += 10;
|
||||
}
|
||||
|
||||
display.drawTextCentered(display.width() / 2, y, "Enter: Play/Pause");
|
||||
y += 10;
|
||||
|
||||
// Only show second hint when there's space (no cover art)
|
||||
// Sleep timer or additional hints
|
||||
if (y < display.height() - 26) {
|
||||
display.setColor(DisplayDriver::YELLOW);
|
||||
if (_isPlaying && !_isPaused) {
|
||||
if (_sleepTimerActive) {
|
||||
// Show countdown
|
||||
unsigned long remaining = 0;
|
||||
if (millis() < _sleepTimerEnd) remaining = (_sleepTimerEnd - millis()) / 1000;
|
||||
int mins = remaining / 60;
|
||||
int secs = remaining % 60;
|
||||
char sleepBuf[32];
|
||||
snprintf(sleepBuf, sizeof(sleepBuf), "Sleep: %d:%02d (Z:Off)", mins, secs);
|
||||
display.drawTextCentered(display.width() / 2, y, sleepBuf);
|
||||
} else if (_isPlaying && !_isPaused) {
|
||||
display.drawTextCentered(display.width() / 2, y,
|
||||
"Screen updates on keypress");
|
||||
"Z: Start 45m sleep timer");
|
||||
} else if (_metadata.chapterCount > 0) {
|
||||
display.drawTextCentered(display.width() / 2, y,
|
||||
"[/]: Prev/Next Chapter");
|
||||
} else if (_playlist.size() > 1) {
|
||||
display.drawTextCentered(display.width() / 2, y,
|
||||
"N: Next Track");
|
||||
}
|
||||
}
|
||||
}
|
||||
// Transport controls drawn — footer is at fixed position below
|
||||
|
||||
// ---- Footer Nav Bar ----
|
||||
drawFooter(display, "A/D:Seek W/S:Vol", (_isPlaying && !_isPaused) ? "Q:Leave" : "Q:Close");
|
||||
{
|
||||
const char* rightText = (_isPlaying && !_isPaused) ? "Q:Leave" : "Q:Close";
|
||||
if (_playlist.size() > 1) {
|
||||
drawFooter(display, "A/D:Seek N:Next", rightText);
|
||||
} else {
|
||||
drawFooter(display, "A/D:Seek W/S:Vol", rightText);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public:
|
||||
@@ -1253,7 +1479,9 @@ public:
|
||||
_pendingSeekSec(0), _streamReady(false),
|
||||
_currentFileSize(0),
|
||||
_m4bRenamed(false),
|
||||
_transportSel(2), _showingInfo(false) {}
|
||||
_transportSel(2), _showingInfo(false),
|
||||
_sleepTimerActive(false), _sleepTimerEnd(0),
|
||||
_playlistIdx(-1), _eofFlag(false) {}
|
||||
|
||||
~AudiobookPlayerScreen() {
|
||||
freeCoverBitmap();
|
||||
@@ -1317,12 +1545,41 @@ public:
|
||||
saveBookmark();
|
||||
_lastPositionSave = millis();
|
||||
}
|
||||
|
||||
// Sleep timer — auto-pause when countdown expires
|
||||
if (_sleepTimerActive && millis() >= _sleepTimerEnd) {
|
||||
_sleepTimerActive = false;
|
||||
Serial.println("AB: Sleep timer expired — pausing playback");
|
||||
togglePause();
|
||||
return; // Don't process further this tick
|
||||
}
|
||||
|
||||
// EOF auto-advance — when a track finishes, play the next one
|
||||
if (_eofFlag) {
|
||||
_eofFlag = false;
|
||||
Serial.println("AB: EOF detected");
|
||||
if (_playlist.size() > 1 && _playlistIdx >= 0 &&
|
||||
_playlistIdx < (int)_playlist.size() - 1) {
|
||||
// More tracks in playlist — advance
|
||||
advanceTrack(1);
|
||||
} else {
|
||||
// Last track or no playlist — just stop
|
||||
stopPlayback();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
bool isAudioActive() const { return _isPlaying && !_isPaused; }
|
||||
bool isPaused() const { return _isPaused; }
|
||||
bool isBookOpenAndPaused() const { return _bookOpen && (_isPaused || !_isPlaying); }
|
||||
|
||||
// Called from audio_eof_mp3 callback in main.cpp to signal end of file
|
||||
void onEOF() { _eofFlag = true; }
|
||||
|
||||
// Playlist info for external queries
|
||||
int getPlaylistSize() const { return (int)_playlist.size(); }
|
||||
int getPlaylistIndex() const { return _playlistIdx; }
|
||||
|
||||
// Public method to close the current book (stops playback, saves bookmark,
|
||||
// returns to file list). Used by main.cpp when user presses Q while paused.
|
||||
void closeCurrentBook() {
|
||||
@@ -1385,11 +1642,11 @@ public:
|
||||
|
||||
// E-ink refresh takes ~648ms during which audio.loop() can't run
|
||||
// (SPI bus shared between display and SD card). This causes audible
|
||||
// glitches during playback. Solution: during active playback, only
|
||||
// auto-refresh once per minute (for time/progress updates). Key presses
|
||||
// glitches during playback. Solution: during active playback, auto-refresh
|
||||
// every 30 seconds for progress/time/sleep timer updates. Key presses
|
||||
// always trigger immediate refresh regardless of this interval.
|
||||
// When paused or stopped, refresh every 5s as normal.
|
||||
if (_isPlaying && !_isPaused) return 60000; // 1 min between auto-refreshes
|
||||
if (_isPlaying && !_isPaused) return 30000; // 30 sec between auto-refreshes
|
||||
return 5000;
|
||||
}
|
||||
|
||||
@@ -1493,7 +1750,7 @@ public:
|
||||
return true; // Always consume & refresh
|
||||
}
|
||||
|
||||
// [ - previous chapter
|
||||
// [ - previous chapter (shift key required on T-Deck Pro)
|
||||
if (c == '[') {
|
||||
if (_metadata.chapterCount > 0 && _currentChapter > 0) {
|
||||
seekToChapter(_currentChapter - 1);
|
||||
@@ -1502,7 +1759,7 @@ public:
|
||||
return false;
|
||||
}
|
||||
|
||||
// ] - next chapter
|
||||
// ] - next chapter (shift key required on T-Deck Pro)
|
||||
if (c == ']') {
|
||||
if (_metadata.chapterCount > 0 && _currentChapter < _metadata.chapterCount - 1) {
|
||||
seekToChapter(_currentChapter + 1);
|
||||
@@ -1511,6 +1768,28 @@ public:
|
||||
return false;
|
||||
}
|
||||
|
||||
// N - next track in playlist (always, regardless of chapters)
|
||||
if (c == 'n') {
|
||||
if (_playlist.size() > 1 && _playlistIdx < (int)_playlist.size() - 1) {
|
||||
advanceTrack(1);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// Z - toggle 45-minute sleep timer
|
||||
if (c == 'z') {
|
||||
if (_sleepTimerActive) {
|
||||
_sleepTimerActive = false;
|
||||
Serial.println("AB: Sleep timer cancelled");
|
||||
} else {
|
||||
_sleepTimerActive = true;
|
||||
_sleepTimerEnd = millis() + (45UL * 60UL * 1000UL);
|
||||
Serial.println("AB: Sleep timer set for 45 minutes");
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
// Q - handled by main.cpp (leave screen or close book depending on play state)
|
||||
// Not handled here - main.cpp intercepts Q before it reaches the player
|
||||
|
||||
|
||||
@@ -30,7 +30,7 @@ private:
|
||||
|
||||
// Cached filtered contact indices for efficient scrolling
|
||||
// We rebuild this on filter change or when entering the screen
|
||||
static const int MAX_VISIBLE = 400; // matches MAX_CONTACTS build flag
|
||||
static const int MAX_VISIBLE = 700; // must be >= MAX_CONTACTS build flag
|
||||
uint16_t _filteredIdx[MAX_VISIBLE]; // indices into contact table
|
||||
uint32_t _filteredTs[MAX_VISIBLE]; // cached last_advert_timestamp for sorting
|
||||
int _filteredCount; // how many contacts match current filter
|
||||
@@ -88,7 +88,7 @@ private:
|
||||
}
|
||||
}
|
||||
// Sort by last_advert_timestamp descending (most recently seen first)
|
||||
// Simple insertion sort — fine for up to 400 entries on ESP32
|
||||
// Simple insertion sort - fine for up to 700 entries on ESP32
|
||||
for (int i = 1; i < _filteredCount; i++) {
|
||||
uint16_t tmpIdx = _filteredIdx[i];
|
||||
uint32_t tmpTs = _filteredTs[i];
|
||||
|
||||
@@ -55,6 +55,7 @@ enum SettingsRowType : uint8_t {
|
||||
ROW_CR, // Coding rate (5-8)
|
||||
ROW_TX_POWER, // TX power (1-20 dBm)
|
||||
ROW_UTC_OFFSET, // UTC offset (-12 to +14)
|
||||
ROW_MSG_NOTIFY, // Keyboard flash on new msg toggle
|
||||
ROW_CH_HEADER, // "--- Channels ---" separator
|
||||
ROW_CHANNEL, // A channel entry (dynamic, index stored separately)
|
||||
ROW_ADD_CHANNEL, // "+ Add Hashtag Channel"
|
||||
@@ -126,6 +127,7 @@ private:
|
||||
addRow(ROW_CR);
|
||||
addRow(ROW_TX_POWER);
|
||||
addRow(ROW_UTC_OFFSET);
|
||||
addRow(ROW_MSG_NOTIFY);
|
||||
addRow(ROW_CH_HEADER);
|
||||
|
||||
// Enumerate current channels
|
||||
@@ -465,6 +467,12 @@ public:
|
||||
display.print(tmp);
|
||||
break;
|
||||
|
||||
case ROW_MSG_NOTIFY:
|
||||
snprintf(tmp, sizeof(tmp), "Msg Rcvd LED Light Pulse: %s",
|
||||
_prefs->kb_flash_notify ? "ON" : "OFF");
|
||||
display.print(tmp);
|
||||
break;
|
||||
|
||||
case ROW_CH_HEADER:
|
||||
display.setColor(DisplayDriver::YELLOW);
|
||||
display.print("--- Channels ---");
|
||||
@@ -824,6 +832,12 @@ public:
|
||||
case ROW_UTC_OFFSET:
|
||||
startEditInt(_prefs->utc_offset_hours);
|
||||
break;
|
||||
case ROW_MSG_NOTIFY:
|
||||
_prefs->kb_flash_notify = _prefs->kb_flash_notify ? 0 : 1;
|
||||
the_mesh.savePrefs();
|
||||
Serial.printf("Settings: Msg flash notify = %s\n",
|
||||
_prefs->kb_flash_notify ? "ON" : "OFF");
|
||||
break;
|
||||
case ROW_ADD_CHANNEL:
|
||||
startEditText("");
|
||||
break;
|
||||
|
||||
@@ -89,13 +89,18 @@ class HomeScreen : public UIScreen {
|
||||
FIRST,
|
||||
RECENT,
|
||||
RADIO,
|
||||
#ifdef BLE_PIN_CODE
|
||||
BLUETOOTH,
|
||||
#endif
|
||||
ADVERT,
|
||||
#if ENV_INCLUDE_GPS == 1
|
||||
GPS,
|
||||
#endif
|
||||
#if UI_SENSORS_PAGE == 1
|
||||
SENSORS,
|
||||
#endif
|
||||
#if HAS_BQ27220
|
||||
BATTERY,
|
||||
#endif
|
||||
SHUTDOWN,
|
||||
Count // keep as last
|
||||
@@ -226,12 +231,12 @@ public:
|
||||
|
||||
int render(DisplayDriver& display) override {
|
||||
char tmp[80];
|
||||
// node name
|
||||
display.setTextSize(1);
|
||||
// node name (tinyfont to avoid overlapping clock)
|
||||
display.setTextSize(0);
|
||||
display.setColor(DisplayDriver::GREEN);
|
||||
char filtered_name[sizeof(_node_prefs->node_name)];
|
||||
display.translateUTF8ToBlocks(filtered_name, _node_prefs->node_name, sizeof(filtered_name));
|
||||
display.setCursor(0, 0);
|
||||
display.setCursor(0, -3);
|
||||
display.print(filtered_name);
|
||||
|
||||
// battery voltage
|
||||
@@ -290,18 +295,22 @@ public:
|
||||
display.drawTextCentered(display.width() / 2, y, tmp);
|
||||
y += 12;
|
||||
#endif
|
||||
#if defined(BLE_PIN_CODE) || defined(WIFI_SSID)
|
||||
if (_task->hasConnection()) {
|
||||
display.setColor(DisplayDriver::GREEN);
|
||||
display.setTextSize(1);
|
||||
display.drawTextCentered(display.width() / 2, y, "< Connected >");
|
||||
y += 12;
|
||||
#ifdef BLE_PIN_CODE
|
||||
} else if (_task->isSerialEnabled() && the_mesh.getBLEPin() != 0) {
|
||||
display.setColor(DisplayDriver::RED);
|
||||
display.setTextSize(2);
|
||||
sprintf(tmp, "Pin:%d", the_mesh.getBLEPin());
|
||||
display.drawTextCentered(display.width() / 2, y, tmp);
|
||||
y += 18;
|
||||
#endif
|
||||
}
|
||||
#endif
|
||||
|
||||
// Menu shortcuts - tinyfont monospaced grid
|
||||
y += 6;
|
||||
@@ -364,6 +373,7 @@ public:
|
||||
display.setCursor(0, 53);
|
||||
sprintf(tmp, "Noise floor: %d", radio_driver.getNoiseFloor());
|
||||
display.print(tmp);
|
||||
#ifdef BLE_PIN_CODE
|
||||
} else if (_page == HomePage::BLUETOOTH) {
|
||||
display.setColor(DisplayDriver::GREEN);
|
||||
display.drawXbm((display.width() - 32) / 2, 18,
|
||||
@@ -371,6 +381,7 @@ public:
|
||||
32, 32);
|
||||
display.setTextSize(1);
|
||||
display.drawTextCentered(display.width() / 2, 64 - 11, "toggle: " PRESS_LABEL);
|
||||
#endif
|
||||
} else if (_page == HomePage::ADVERT) {
|
||||
display.setColor(DisplayDriver::GREEN);
|
||||
display.drawXbm((display.width() - 32) / 2, 18, advert_icon, 32, 32);
|
||||
@@ -420,7 +431,7 @@ public:
|
||||
display.drawTextRightAlign(display.width()-1, y, buf);
|
||||
y = y + 12;
|
||||
|
||||
// NMEA sentence counter  confirms baud rate and data flow
|
||||
// NMEA sentence counter  confirms baud rate and data flow
|
||||
display.drawTextLeftAlign(0, y, "sentences");
|
||||
if (gpsDuty.isHardwareOn()) {
|
||||
uint16_t sps = gpsStream.getSentencesPerSec();
|
||||
@@ -550,6 +561,58 @@ public:
|
||||
}
|
||||
if (sensors_scroll) sensors_scroll_offset = (sensors_scroll_offset+1)%sensors_nb;
|
||||
else sensors_scroll_offset = 0;
|
||||
#endif
|
||||
#if HAS_BQ27220
|
||||
} else if (_page == HomePage::BATTERY) {
|
||||
char buf[30];
|
||||
int y = 18;
|
||||
|
||||
// Title
|
||||
display.setColor(DisplayDriver::GREEN);
|
||||
display.drawTextCentered(display.width() / 2, y, "Battery Gauge");
|
||||
y += 12;
|
||||
|
||||
display.setColor(DisplayDriver::LIGHT);
|
||||
|
||||
// Time to empty
|
||||
uint16_t tte = board.getTimeToEmpty();
|
||||
display.drawTextLeftAlign(0, y, "remaining");
|
||||
if (tte == 0xFFFF || tte == 0) {
|
||||
strcpy(buf, tte == 0 ? "depleted" : "charging");
|
||||
} else if (tte >= 60) {
|
||||
sprintf(buf, "%dh %dm", tte / 60, tte % 60);
|
||||
} else {
|
||||
sprintf(buf, "%d min", tte);
|
||||
}
|
||||
display.drawTextRightAlign(display.width()-1, y, buf);
|
||||
y += 10;
|
||||
|
||||
// Average current
|
||||
int16_t avgCur = board.getAvgCurrent();
|
||||
display.drawTextLeftAlign(0, y, "avg current");
|
||||
sprintf(buf, "%d mA", avgCur);
|
||||
display.drawTextRightAlign(display.width()-1, y, buf);
|
||||
y += 10;
|
||||
|
||||
// Average power
|
||||
int16_t avgPow = board.getAvgPower();
|
||||
display.drawTextLeftAlign(0, y, "avg power");
|
||||
sprintf(buf, "%d mW", avgPow);
|
||||
display.drawTextRightAlign(display.width()-1, y, buf);
|
||||
y += 10;
|
||||
|
||||
// Voltage (already available)
|
||||
uint16_t mv = board.getBattMilliVolts();
|
||||
display.drawTextLeftAlign(0, y, "voltage");
|
||||
sprintf(buf, "%d.%03d V", mv / 1000, mv % 1000);
|
||||
display.drawTextRightAlign(display.width()-1, y, buf);
|
||||
y += 10;
|
||||
|
||||
// Remaining capacity
|
||||
uint16_t remCap = board.getRemainingCapacity();
|
||||
display.drawTextLeftAlign(0, y, "remaining cap");
|
||||
sprintf(buf, "%d mAh", remCap);
|
||||
display.drawTextRightAlign(display.width()-1, y, buf);
|
||||
#endif
|
||||
} else if (_page == HomePage::SHUTDOWN) {
|
||||
display.setColor(DisplayDriver::GREEN);
|
||||
@@ -610,6 +673,7 @@ public:
|
||||
}
|
||||
return true;
|
||||
}
|
||||
#ifdef BLE_PIN_CODE
|
||||
if (c == KEY_ENTER && _page == HomePage::BLUETOOTH) {
|
||||
if (_task->isSerialEnabled()) { // toggle Bluetooth on/off
|
||||
_task->disableSerial();
|
||||
@@ -618,6 +682,7 @@ public:
|
||||
}
|
||||
return true;
|
||||
}
|
||||
#endif
|
||||
if (c == KEY_ENTER && _page == HomePage::ADVERT) {
|
||||
_task->notify(UIEventType::ack);
|
||||
if (the_mesh.advert()) {
|
||||
@@ -785,6 +850,12 @@ void UITask::begin(DisplayDriver* display, SensorManager* sensors, NodePrefs* no
|
||||
vibration.begin();
|
||||
#endif
|
||||
|
||||
// Keyboard backlight for message flash notifications
|
||||
#ifdef KB_BL_PIN
|
||||
pinMode(KB_BL_PIN, OUTPUT);
|
||||
digitalWrite(KB_BL_PIN, LOW);
|
||||
#endif
|
||||
|
||||
ui_started_at = millis();
|
||||
_alert_expiry = 0;
|
||||
|
||||
@@ -839,7 +910,7 @@ switch(t){
|
||||
|
||||
void UITask::msgRead(int msgcount) {
|
||||
_msgcount = msgcount;
|
||||
if (msgcount == 0) {
|
||||
if (msgcount == 0 && curr == msg_preview) {
|
||||
gotoHomeScreen();
|
||||
}
|
||||
}
|
||||
@@ -868,9 +939,12 @@ void UITask::newMsg(uint8_t path_len, const char* from_name, const char* text, i
|
||||
#if defined(LilyGo_TDeck_Pro)
|
||||
// T-Deck Pro: Don't interrupt user with popup - just show brief notification
|
||||
// Messages are stored in channel history, accessible via 'M' key
|
||||
char alertBuf[40];
|
||||
snprintf(alertBuf, sizeof(alertBuf), "New: %s", from_name);
|
||||
showAlert(alertBuf, 2000);
|
||||
// Suppress alert entirely on admin screen - it needs focused interaction
|
||||
if (!isOnRepeaterAdmin()) {
|
||||
char alertBuf[40];
|
||||
snprintf(alertBuf, sizeof(alertBuf), "New: %s", from_name);
|
||||
showAlert(alertBuf, 2000);
|
||||
}
|
||||
#else
|
||||
// Other devices: Show full preview screen (legacy behavior)
|
||||
setCurrScreen(msg_preview);
|
||||
@@ -885,6 +959,14 @@ void UITask::newMsg(uint8_t path_len, const char* from_name, const char* text, i
|
||||
_next_refresh = 100; // trigger refresh
|
||||
}
|
||||
}
|
||||
|
||||
// Keyboard flash notification
|
||||
#ifdef KB_BL_PIN
|
||||
if (_node_prefs->kb_flash_notify) {
|
||||
digitalWrite(KB_BL_PIN, HIGH);
|
||||
_kb_flash_off_at = millis() + 200; // 200ms flash
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
void UITask::userLedHandler() {
|
||||
@@ -1020,6 +1102,14 @@ void UITask::loop() {
|
||||
|
||||
userLedHandler();
|
||||
|
||||
// Turn off keyboard flash after timeout
|
||||
#ifdef KB_BL_PIN
|
||||
if (_kb_flash_off_at && millis() >= _kb_flash_off_at) {
|
||||
digitalWrite(KB_BL_PIN, LOW);
|
||||
_kb_flash_off_at = 0;
|
||||
}
|
||||
#endif
|
||||
|
||||
#ifdef PIN_BUZZER
|
||||
if (buzzer.isPlaying()) buzzer.loop();
|
||||
#endif
|
||||
@@ -1130,13 +1220,13 @@ void UITask::toggleGPS() {
|
||||
|
||||
if (_sensors != NULL) {
|
||||
if (_node_prefs->gps_enabled) {
|
||||
// Disable GPS  cut hardware power
|
||||
// Disable GPS  cut hardware power
|
||||
_sensors->setSettingValue("gps", "0");
|
||||
_node_prefs->gps_enabled = 0;
|
||||
gpsDuty.disable();
|
||||
notify(UIEventType::ack);
|
||||
} else {
|
||||
// Enable GPS  start duty cycle
|
||||
// Enable GPS  start duty cycle
|
||||
_sensors->setSettingValue("gps", "1");
|
||||
_node_prefs->gps_enabled = 1;
|
||||
gpsDuty.enable();
|
||||
|
||||
@@ -32,6 +32,7 @@ class UITask : public AbstractUITask {
|
||||
GenericVibration vibration;
|
||||
#endif
|
||||
unsigned long _next_refresh, _auto_off;
|
||||
unsigned long _kb_flash_off_at; // Keyboard flash turn-off timer
|
||||
NodePrefs* _node_prefs;
|
||||
char _alert[80];
|
||||
unsigned long _alert_expiry;
|
||||
@@ -74,6 +75,7 @@ public:
|
||||
|
||||
UITask(mesh::MainBoard* board, BaseSerialInterface* serial) : AbstractUITask(board, serial), _display(NULL), _sensors(NULL) {
|
||||
next_batt_chck = _next_refresh = 0;
|
||||
_kb_flash_off_at = 0;
|
||||
ui_started_at = 0;
|
||||
curr = NULL;
|
||||
}
|
||||
|
||||
@@ -72,10 +72,11 @@ void TDeckBoard::begin() {
|
||||
rtc_gpio_deinit((gpio_num_t)P_LORA_DIO_1);
|
||||
}
|
||||
|
||||
// Test BQ27220 communication
|
||||
// Test BQ27220 communication and configure design capacity
|
||||
#if HAS_BQ27220
|
||||
uint16_t voltage = getBattMilliVolts();
|
||||
MESH_DEBUG_PRINTLN("TDeckBoard::begin() - Battery voltage: %d mV", voltage);
|
||||
configureFuelGauge();
|
||||
#endif
|
||||
|
||||
MESH_DEBUG_PRINTLN("TDeckBoard::begin() - complete");
|
||||
@@ -123,4 +124,233 @@ uint8_t TDeckBoard::getBatteryPercent() {
|
||||
#else
|
||||
return 0;
|
||||
#endif
|
||||
}
|
||||
|
||||
// ---- BQ27220 extended register helpers ----
|
||||
|
||||
#if HAS_BQ27220
|
||||
// Read a 16-bit register from BQ27220. Returns 0 on I2C error.
|
||||
static uint16_t bq27220_read16(uint8_t reg) {
|
||||
Wire.beginTransmission(BQ27220_I2C_ADDR);
|
||||
Wire.write(reg);
|
||||
if (Wire.endTransmission(false) != 0) return 0;
|
||||
if (Wire.requestFrom((uint8_t)BQ27220_I2C_ADDR, (uint8_t)2) != 2) return 0;
|
||||
uint16_t val = Wire.read();
|
||||
val |= (Wire.read() << 8);
|
||||
return val;
|
||||
}
|
||||
|
||||
// Read a single byte from BQ27220 register.
|
||||
static uint8_t bq27220_read8(uint8_t reg) {
|
||||
Wire.beginTransmission(BQ27220_I2C_ADDR);
|
||||
Wire.write(reg);
|
||||
if (Wire.endTransmission(false) != 0) return 0;
|
||||
if (Wire.requestFrom((uint8_t)BQ27220_I2C_ADDR, (uint8_t)1) != 1) return 0;
|
||||
return Wire.read();
|
||||
}
|
||||
|
||||
// Write a 16-bit subcommand to BQ27220 Control register (0x00).
|
||||
// Subcommands control unsealing, config mode, sealing, etc.
|
||||
static bool bq27220_writeControl(uint16_t subcmd) {
|
||||
Wire.beginTransmission(BQ27220_I2C_ADDR);
|
||||
Wire.write(0x00); // Control register
|
||||
Wire.write(subcmd & 0xFF); // LSB first
|
||||
Wire.write((subcmd >> 8) & 0xFF); // MSB
|
||||
return Wire.endTransmission() == 0;
|
||||
}
|
||||
#endif
|
||||
|
||||
// ---- BQ27220 Design Capacity configuration ----
|
||||
// The BQ27220 ships with a 3000 mAh default. The T-Deck Pro uses a 1400 mAh
|
||||
// cell. This function checks on boot and writes the correct value via the
|
||||
// MAC Data Memory interface if needed. The value persists in battery-backed
|
||||
// RAM, so this typically only writes once (or after a full battery disconnect).
|
||||
//
|
||||
// Procedure follows TI TRM SLUUBD4A Section 6.1:
|
||||
// 1. Unseal → 2. Full Access → 3. Enter CFG_UPDATE
|
||||
// 4. Write Design Capacity via MAC → 5. Exit CFG_UPDATE → 6. Seal
|
||||
|
||||
bool TDeckBoard::configureFuelGauge(uint16_t designCapacity_mAh) {
|
||||
#if HAS_BQ27220
|
||||
// Read current design capacity from standard command register
|
||||
uint16_t currentDC = bq27220_read16(BQ27220_REG_DESIGN_CAP);
|
||||
Serial.printf("BQ27220: Design Capacity = %d mAh (target %d)\n", currentDC, designCapacity_mAh);
|
||||
|
||||
if (currentDC == designCapacity_mAh) {
|
||||
Serial.println("BQ27220: Design Capacity already correct, skipping");
|
||||
return true;
|
||||
}
|
||||
|
||||
Serial.printf("BQ27220: Updating Design Capacity from %d to %d mAh\n", currentDC, designCapacity_mAh);
|
||||
|
||||
// Step 1: Unseal (default unseal keys)
|
||||
bq27220_writeControl(0x0414);
|
||||
delay(2);
|
||||
bq27220_writeControl(0x3672);
|
||||
delay(2);
|
||||
|
||||
// Step 2: Enter Full Access mode
|
||||
bq27220_writeControl(0xFFFF);
|
||||
delay(2);
|
||||
bq27220_writeControl(0xFFFF);
|
||||
delay(2);
|
||||
|
||||
// Step 3: Enter CFG_UPDATE mode
|
||||
bq27220_writeControl(0x0090);
|
||||
|
||||
// Wait for CFGUPMODE bit (bit 10) in OperationStatus register
|
||||
bool cfgReady = false;
|
||||
for (int i = 0; i < 50; i++) {
|
||||
delay(20);
|
||||
uint16_t opStatus = bq27220_read16(BQ27220_REG_OP_STATUS);
|
||||
Serial.printf("BQ27220: OperationStatus = 0x%04X (attempt %d)\n", opStatus, i);
|
||||
if (opStatus & 0x0400) { // CFGUPMODE is bit 10
|
||||
cfgReady = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!cfgReady) {
|
||||
Serial.println("BQ27220: ERROR - Timeout waiting for CFGUPDATE mode");
|
||||
bq27220_writeControl(0x0092); // Try to exit cleanly
|
||||
bq27220_writeControl(0x0030); // Re-seal
|
||||
return false;
|
||||
}
|
||||
Serial.println("BQ27220: Entered CFGUPDATE mode");
|
||||
|
||||
// Step 4: Write Design Capacity via MAC Data Memory interface
|
||||
// Design Capacity mAh lives at data memory address 0x929F
|
||||
|
||||
// 4a. Select the data memory block by writing address to 0x3E-0x3F
|
||||
Wire.beginTransmission(BQ27220_I2C_ADDR);
|
||||
Wire.write(0x3E); // MACDataControl register
|
||||
Wire.write(0x9F); // Address low byte
|
||||
Wire.write(0x92); // Address high byte
|
||||
Wire.endTransmission();
|
||||
delay(10);
|
||||
|
||||
// 4b. Read old data (MSB, LSB) and checksum for differential update
|
||||
uint8_t oldMSB = bq27220_read8(0x40);
|
||||
uint8_t oldLSB = bq27220_read8(0x41);
|
||||
uint8_t oldChksum = bq27220_read8(0x60);
|
||||
uint8_t dataLen = bq27220_read8(0x61);
|
||||
|
||||
Serial.printf("BQ27220: Old DC bytes=0x%02X 0x%02X chk=0x%02X len=%d\n",
|
||||
oldMSB, oldLSB, oldChksum, dataLen);
|
||||
|
||||
// 4c. Compute new values (BQ27220 stores big-endian in data memory)
|
||||
uint8_t newMSB = (designCapacity_mAh >> 8) & 0xFF;
|
||||
uint8_t newLSB = designCapacity_mAh & 0xFF;
|
||||
|
||||
// Differential checksum: remove old bytes, add new bytes
|
||||
uint8_t temp = (255 - oldChksum - oldMSB - oldLSB);
|
||||
uint8_t newChksum = 255 - ((temp + newMSB + newLSB) & 0xFF);
|
||||
|
||||
Serial.printf("BQ27220: New DC bytes=0x%02X 0x%02X chk=0x%02X\n",
|
||||
newMSB, newLSB, newChksum);
|
||||
|
||||
// 4d. Write address + new data as a single block transaction
|
||||
// BQ27220 MAC requires: [0x3E] [addr_lo] [addr_hi] [data...]
|
||||
Wire.beginTransmission(BQ27220_I2C_ADDR);
|
||||
Wire.write(0x3E); // Start at MACDataControl
|
||||
Wire.write(0x9F); // Address low byte
|
||||
Wire.write(0x92); // Address high byte
|
||||
Wire.write(newMSB); // Data byte 0 (at 0x40)
|
||||
Wire.write(newLSB); // Data byte 1 (at 0x41)
|
||||
uint8_t writeResult = Wire.endTransmission();
|
||||
Serial.printf("BQ27220: Write block result = %d\n", writeResult);
|
||||
|
||||
// 4e. Write updated checksum and length
|
||||
Wire.beginTransmission(BQ27220_I2C_ADDR);
|
||||
Wire.write(0x60);
|
||||
Wire.write(newChksum);
|
||||
Wire.write(dataLen);
|
||||
writeResult = Wire.endTransmission();
|
||||
Serial.printf("BQ27220: Write checksum result = %d\n", writeResult);
|
||||
delay(10);
|
||||
|
||||
// 4f. Verify the write took effect before exiting config mode
|
||||
// Re-read the block to confirm
|
||||
Wire.beginTransmission(BQ27220_I2C_ADDR);
|
||||
Wire.write(0x3E);
|
||||
Wire.write(0x9F);
|
||||
Wire.write(0x92);
|
||||
Wire.endTransmission();
|
||||
delay(10);
|
||||
uint8_t verMSB = bq27220_read8(0x40);
|
||||
uint8_t verLSB = bq27220_read8(0x41);
|
||||
Serial.printf("BQ27220: Verify in CFGUPDATE: DC bytes=0x%02X 0x%02X (%d mAh)\n",
|
||||
verMSB, verLSB, (verMSB << 8) | verLSB);
|
||||
|
||||
// Step 5: Exit CFG_UPDATE (with reinit to apply changes immediately)
|
||||
bq27220_writeControl(0x0091); // EXIT_CFG_UPDATE_REINIT
|
||||
Serial.println("BQ27220: Sent EXIT_CFG_UPDATE_REINIT, waiting...");
|
||||
delay(200); // Allow gauge to reinitialize
|
||||
|
||||
// Verify
|
||||
uint16_t verifyDC = bq27220_read16(BQ27220_REG_DESIGN_CAP);
|
||||
Serial.printf("BQ27220: Design Capacity now reads %d mAh (expected %d)\n",
|
||||
verifyDC, designCapacity_mAh);
|
||||
|
||||
if (verifyDC == designCapacity_mAh) {
|
||||
Serial.println("BQ27220: Configuration SUCCESS");
|
||||
} else {
|
||||
Serial.println("BQ27220: Configuration FAILED");
|
||||
}
|
||||
|
||||
// Step 7: Seal the device
|
||||
bq27220_writeControl(0x0030);
|
||||
delay(5);
|
||||
|
||||
return verifyDC == designCapacity_mAh;
|
||||
#else
|
||||
return false;
|
||||
#endif
|
||||
}
|
||||
|
||||
int16_t TDeckBoard::getAvgCurrent() {
|
||||
#if HAS_BQ27220
|
||||
return (int16_t)bq27220_read16(BQ27220_REG_AVG_CURRENT);
|
||||
#else
|
||||
return 0;
|
||||
#endif
|
||||
}
|
||||
|
||||
int16_t TDeckBoard::getAvgPower() {
|
||||
#if HAS_BQ27220
|
||||
return (int16_t)bq27220_read16(BQ27220_REG_AVG_POWER);
|
||||
#else
|
||||
return 0;
|
||||
#endif
|
||||
}
|
||||
|
||||
uint16_t TDeckBoard::getTimeToEmpty() {
|
||||
#if HAS_BQ27220
|
||||
return bq27220_read16(BQ27220_REG_TIME_TO_EMPTY);
|
||||
#else
|
||||
return 0xFFFF;
|
||||
#endif
|
||||
}
|
||||
|
||||
uint16_t TDeckBoard::getRemainingCapacity() {
|
||||
#if HAS_BQ27220
|
||||
return bq27220_read16(BQ27220_REG_REMAIN_CAP);
|
||||
#else
|
||||
return 0;
|
||||
#endif
|
||||
}
|
||||
|
||||
uint16_t TDeckBoard::getFullChargeCapacity() {
|
||||
#if HAS_BQ27220
|
||||
return bq27220_read16(BQ27220_REG_FULL_CAP);
|
||||
#else
|
||||
return 0;
|
||||
#endif
|
||||
}
|
||||
|
||||
uint16_t TDeckBoard::getDesignCapacity() {
|
||||
#if HAS_BQ27220
|
||||
return bq27220_read16(BQ27220_REG_DESIGN_CAP);
|
||||
#else
|
||||
return 0;
|
||||
#endif
|
||||
}
|
||||
@@ -7,11 +7,23 @@
|
||||
#include <driver/rtc_io.h>
|
||||
|
||||
// BQ27220 Fuel Gauge Registers
|
||||
#define BQ27220_REG_VOLTAGE 0x08
|
||||
#define BQ27220_REG_CURRENT 0x0C
|
||||
#define BQ27220_REG_SOC 0x2C
|
||||
#define BQ27220_REG_VOLTAGE 0x08
|
||||
#define BQ27220_REG_CURRENT 0x0C // Instantaneous current (mA, signed)
|
||||
#define BQ27220_REG_SOC 0x2C
|
||||
#define BQ27220_REG_REMAIN_CAP 0x10 // Remaining capacity (mAh)
|
||||
#define BQ27220_REG_FULL_CAP 0x12 // Full charge capacity (mAh)
|
||||
#define BQ27220_REG_AVG_CURRENT 0x14 // Average current (mA, signed)
|
||||
#define BQ27220_REG_TIME_TO_EMPTY 0x16 // Minutes until empty
|
||||
#define BQ27220_REG_AVG_POWER 0x24 // Average power (mW, signed)
|
||||
#define BQ27220_REG_DESIGN_CAP 0x3C // Design capacity (mAh, read-only standard cmd)
|
||||
#define BQ27220_REG_OP_STATUS 0x3A // Operation status
|
||||
#define BQ27220_I2C_ADDR 0x55
|
||||
|
||||
// T-Deck Pro battery capacity (all variants use 1400 mAh cell)
|
||||
#ifndef BQ27220_DESIGN_CAPACITY_MAH
|
||||
#define BQ27220_DESIGN_CAPACITY_MAH 1400
|
||||
#endif
|
||||
|
||||
class TDeckBoard : public ESP32Board {
|
||||
public:
|
||||
void begin();
|
||||
@@ -52,6 +64,27 @@ public:
|
||||
// Read state of charge percentage from BQ27220
|
||||
uint8_t getBatteryPercent();
|
||||
|
||||
// Read average current in mA (negative = discharging, positive = charging)
|
||||
int16_t getAvgCurrent();
|
||||
|
||||
// Read average power in mW (negative = discharging, positive = charging)
|
||||
int16_t getAvgPower();
|
||||
|
||||
// Read time-to-empty in minutes (0xFFFF if charging/unavailable)
|
||||
uint16_t getTimeToEmpty();
|
||||
|
||||
// Read remaining capacity in mAh
|
||||
uint16_t getRemainingCapacity();
|
||||
|
||||
// Read full charge capacity in mAh (learned value, may need cycling to update)
|
||||
uint16_t getFullChargeCapacity();
|
||||
|
||||
// Read design capacity in mAh (the configured battery size)
|
||||
uint16_t getDesignCapacity();
|
||||
|
||||
// Configure BQ27220 design capacity (checks on boot, writes only if wrong)
|
||||
bool configureFuelGauge(uint16_t designCapacity_mAh = BQ27220_DESIGN_CAPACITY_MAH);
|
||||
|
||||
const char* getManufacturerName() const {
|
||||
return "LilyGo T-Deck Pro";
|
||||
}
|
||||
|
||||
@@ -80,7 +80,7 @@ build_flags =
|
||||
-D PIN_DISPLAY_BL=45
|
||||
-D PIN_USER_BTN=0
|
||||
-D CST328_PIN_RST=38
|
||||
-D FIRMWARE_VERSION='"Meck v0.8.8A"'
|
||||
-D FIRMWARE_VERSION='"Meck v0.9.1A"'
|
||||
-D ARDUINO_LOOP_STACK_SIZE=32768
|
||||
build_src_filter = ${esp32_base.build_src_filter}
|
||||
+<../variants/LilyGo_TDeck_Pro>
|
||||
@@ -131,6 +131,38 @@ lib_deps =
|
||||
https://github.com/schreibfaul1/ESP32-audioI2S.git#2.0.6
|
||||
bitbank2/JPEGDEC
|
||||
|
||||
[env:LilyGo_TDeck_Pro_standalone]
|
||||
extends = LilyGo_TDeck_Pro
|
||||
build_flags =
|
||||
${LilyGo_TDeck_Pro.build_flags}
|
||||
-I examples/companion_radio/ui-new
|
||||
-D MAX_CONTACTS=700
|
||||
-D MAX_GROUP_CHANNELS=20
|
||||
-D OFFLINE_QUEUE_SIZE=256
|
||||
-D NO_OTA=1
|
||||
-D FIRMWARE_VERSION='"Meck v0.9.1A-NB"'
|
||||
; === NO BLE_PIN_CODE, NO WIFI_SSID ===
|
||||
; By omitting these, the preprocessor selects ArduinoSerialInterface (USB only).
|
||||
; The BLE stack is never initialized, never linked, and the ESP32-S3 BT/WiFi
|
||||
; radio hardware stays in its boot-default OFF state — zero RF power draw.
|
||||
; This is the ONLY reliable way to fully disable BLE on the ESP32-S3.
|
||||
; Calling esp_bt_controller_disable() after init does NOT fully power off the radio.
|
||||
build_src_filter = ${LilyGo_TDeck_Pro.build_src_filter}
|
||||
+<helpers/esp32/*.cpp>
|
||||
+<helpers/ui/MomentaryButton.cpp>
|
||||
+<../examples/companion_radio/*.cpp>
|
||||
+<../examples/companion_radio/ui-new/*.cpp>
|
||||
+<helpers/ui/GxEPDDisplay.cpp>
|
||||
lib_deps =
|
||||
${LilyGo_TDeck_Pro.lib_deps}
|
||||
densaugeo/base64 @ ~1.4.0
|
||||
https://github.com/schreibfaul1/ESP32-audioI2S.git#2.0.6
|
||||
bitbank2/JPEGDEC
|
||||
; Audio libs included because AudiobookPlayerScreen.h unconditionally #includes
|
||||
; Audio.h when LilyGo_TDeck_Pro is defined. No power cost — the Audio object is
|
||||
; only heap-allocated when user presses 'P'. Without BLE competing for heap,
|
||||
; audiobook playback actually works better in standalone mode.
|
||||
|
||||
[env:LilyGo_TDeck_Pro_repeater]
|
||||
extends = LilyGo_TDeck_Pro
|
||||
build_flags =
|
||||
|
||||
Reference in New Issue
Block a user