Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions src/config.h
Original file line number Diff line number Diff line change
Expand Up @@ -253,6 +253,14 @@ static_assert((BEEP_FREQUENCY_MAX_HZ - BEEP_FREQUENCY_MIN_HZ) / BEEP_FREQUENCY_S
// The INA219 is polled at most this often; everything else reads the cached
// value. Keeps the shared I2C bus (OLED!) free and the main loop fast.
#define BATTERY_READ_INTERVAL_MS 1000UL
// EMA weight (percent) given to each new voltage reading before it becomes
// the cached lastMillivolts. The percent-vs-voltage curve is steep in its
// middle segments (e.g. 0.2 %/mV around 3.6-3.7 V), so a few mV of
// load-induced noise per ~1 s read otherwise shows up as the displayed
// battery percent flickering back and forth. At one read per
// BATTERY_READ_INTERVAL_MS, 30% smooths over a few seconds without making
// the reading feel sluggish. Lower = smoother/slower to react.
#define BATTERY_EMA_ALPHA_PERCENT 30
// "Low" latches below LOW and only clears above CLEAR -- the 100 mV hysteresis
// stops the warning from flapping while the voltage bounces around under load.
// Same thresholds are used for the peers' battery level from their heartbeats.
Expand Down
14 changes: 13 additions & 1 deletion src/power.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,19 @@ void refreshIfDue() {
// Out-of-range readings simply keep the last known-good value rather than
// being trusted.
if (busVoltage >= 0.0f && busVoltage <= 5.0f) {
lastMillivolts = static_cast<uint16_t>(busVoltage * 1000.0f);
uint16_t raw = static_cast<uint16_t>(busVoltage * 1000.0f);
// EMA rather than taking the raw reading verbatim: smooths out the load
// noise that otherwise turns into a flickering displayed percent (see
// BATTERY_EMA_ALPHA_PERCENT). Skip it on the very first-ever reading so
// the display doesn't ramp up from a zeroed lastMillivolts.
if (!hasEverRead) {
lastMillivolts = raw;
} else {
lastMillivolts = static_cast<uint16_t>(
(static_cast<uint32_t>(raw) * BATTERY_EMA_ALPHA_PERCENT +
static_cast<uint32_t>(lastMillivolts) * (100 - BATTERY_EMA_ALPHA_PERCENT)) /
100);
}
}
// Adafruit_INA219::begin() calibrates for 32V/2A internally (see init()),
// so getCurrent_mA() works out of the box -- no extra setCalibration_*()
Expand Down
135 changes: 122 additions & 13 deletions src/ui.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -413,6 +413,16 @@ void renderIdle() {
display.setFont(u8g2_font_6x10_tf);
display.drawStr(0, 9, DeviceConfig::nickname());

// Radio group channel, one blank column right of the name -- a permanent
// reminder of which of the 10 channels this device is on, since it
// otherwise only shows up buried in the settings menu.
int leftEndX = 6 * static_cast<int>(strlen(DeviceConfig::nickname()));
char chStr[6];
snprintf(chStr, sizeof(chStr), "CH%u", DeviceConfig::channel());
int chX = leftEndX + 6;
display.drawStr(chX, 9, chStr);
leftEndX = chX + 6 * static_cast<int>(strlen(chStr));

// Right-aligned header block: "active/total" rider counter, then own battery.
int rightX = 128;
if (batteryAvailableForDisplay) {
Expand All @@ -439,12 +449,11 @@ void renderIdle() {
}

if (muted) {
// Centered in whatever is free between the own name and the right block,
// nudged right of the name when the counters get wide.
int nameEndX = 6 * static_cast<int>(strlen(DeviceConfig::nickname()));
int x = (nameEndX + rightX) / 2 - (6 * 5) / 2;
if (x < nameEndX + 3) {
x = nameEndX + 3;
// Centered in whatever is free between the name/channel block and the
// right block, nudged right when the counters get wide.
int x = (leftEndX + rightX) / 2 - (6 * 5) / 2;
if (x < leftEndX + 3) {
x = leftEndX + 3;
}
display.drawStr(x, 9, "MUTE");
}
Expand Down Expand Up @@ -564,15 +573,115 @@ void renderMenu() {
display.drawStr(0, 61, "short=next long=send");
}

void renderIncoming() {
display.setFont(u8g2_font_6x10_tf);
char header[24];
snprintf(header, sizeof(header), "WARNING from %s", incomingSenderNickname);
display.drawStr(0, 9, header);
display.drawHLine(0, 12, 128);
// Font ladder for the big warning-label auto-fit in renderIncoming(), largest
// first. The fub* faces (Bitstream Vera Bold at a few pixel sizes) are only
// used here; the 9x15 bold at the end is the pre-existing font already used
// elsewhere in this file, kept as the guaranteed-fits floor (it's what
// renderIncoming() used exclusively before this auto-fit existed).
const uint8_t *const kWarningFontLadder[] = {
u8g2_font_fub25_tf,
u8g2_font_fub20_tf,
u8g2_font_fub17_tf,
u8g2_font_9x15B_tf,
};
const size_t kWarningFontLadderCount =
sizeof(kWarningFontLadder) / sizeof(kWarningFontLadder[0]);

// Splits `text` into two lines at its last space or '/' (dropping the
// delimiter) -- e.g. "CAR BEHIND" -> "CAR" / "BEHIND", "BRAKING/STOP" ->
// "BRAKING" / "STOP". Returns false if there is no such break point (a
// single word, e.g. "ATTENTION!"), leaving the buffers untouched.
bool splitLabelInTwo(const char *text, char *line1, size_t line1Size, char *line2,
size_t line2Size) {
const char *breakAt = nullptr;
for (const char *p = text; *p != '\0'; ++p) {
if (*p == ' ' || *p == '/') {
breakAt = p; // keep scanning -- we want the LAST match
}
}
if (breakAt == nullptr) {
return false;
}
size_t firstLen = static_cast<size_t>(breakAt - text);
size_t copyLen = firstLen < line1Size - 1 ? firstLen : line1Size - 1;
memcpy(line1, text, copyLen);
line1[copyLen] = '\0';
strncpy(line2, breakAt + 1, line2Size - 1);
line2[line2Size - 1] = '\0';
return true;
}

// Full-screen alert: deliberately built around the two-tone panel rather than
// fighting it. The yellow strip (rows 0-15) answers "who" -- the affected
// rider's name, centered and bold. The blue region below answers "what" --
// the warning label, picked as large as it fits from kWarningFontLadder and
// word-wrapped to two lines when a single line would run off the sides. No
// "WARNING from" prefix: taking over the whole screen plus the beep pattern
// already make the context unambiguous, so that space goes to a bigger name.
// Stays up for the full UI_INCOMING_DISPLAY_MS (see kTimeouts); the footer's
// short-click dismiss still works throughout.
void renderIncoming() {
display.setFont(u8g2_font_9x15B_tf);
display.drawStr(0, 34, Protocol::warningLabel(incomingType));
int nameW = display.getStrWidth(incomingSenderNickname);
int nameX = (128 - nameW) / 2;
if (nameX < 0) {
nameX = 0;
}
display.drawStr(nameX, 12, incomingSenderNickname);

const char *label = Protocol::warningLabel(incomingType);
constexpr int kMaxWidth = 124; // 2px margin either side of the 128px panel
constexpr int kBandTop = 16; // top of the panel's blue region
constexpr int kBandBottom = 51; // leaves room for the footer below
constexpr int kLineGap = 2;

char line1Buf[24];
char line2Buf[24];
bool hasTwoLines = splitLabelInTwo(label, line1Buf, sizeof(line1Buf), line2Buf, sizeof(line2Buf));

// Default/fallback: smallest font, single line -- this is exactly what
// renderIncoming() always did before the auto-fit, so it's a safe floor if
// (unexpectedly) nothing in the ladder measures as fitting.
const uint8_t *chosenFont = kWarningFontLadder[kWarningFontLadderCount - 1];
bool chosenTwoLines = false;
for (size_t i = 0; i < kWarningFontLadderCount; ++i) {
display.setFont(kWarningFontLadder[i]);
int ascent = display.getAscent();
int descent = -display.getDescent(); // getDescent() is <= 0
int lineHeight = ascent + descent;

if (display.getStrWidth(label) <= kMaxWidth && lineHeight <= (kBandBottom - kBandTop)) {
chosenFont = kWarningFontLadder[i];
chosenTwoLines = false;
break;
}
if (hasTwoLines) {
int twoLineHeight = 2 * lineHeight + kLineGap;
if (display.getStrWidth(line1Buf) <= kMaxWidth && display.getStrWidth(line2Buf) <= kMaxWidth &&
twoLineHeight <= (kBandBottom - kBandTop)) {
chosenFont = kWarningFontLadder[i];
chosenTwoLines = true;
break;
}
}
}

display.setFont(chosenFont);
int ascent = display.getAscent();
int descent = -display.getDescent();
int lineHeight = ascent + descent;
if (chosenTwoLines) {
int blockHeight = 2 * lineHeight + kLineGap;
int top = kBandTop + (kBandBottom - kBandTop - blockHeight) / 2;
int baseline1 = top + ascent;
int baseline2 = baseline1 + lineHeight + kLineGap;
display.drawStr((128 - display.getStrWidth(line1Buf)) / 2, baseline1, line1Buf);
display.drawStr((128 - display.getStrWidth(line2Buf)) / 2, baseline2, line2Buf);
} else {
int top = kBandTop + (kBandBottom - kBandTop - lineHeight) / 2;
int baseline = top + ascent;
display.drawStr((128 - display.getStrWidth(label)) / 2, baseline, label);
}

display.setFont(u8g2_font_6x10_tf);
display.drawStr(0, 61, "short = dismiss");
Expand Down
Loading