mirror of
https://github.com/MarekZegare4/MeshCore-Solo.git
synced 2026-09-14 15:16:40 +00:00
fix(ui): message bubble color, received-message hop count, list wrap-around, age fallback; add sim WASM build workflow
- Unselected message bubbles no longer carry a stray light-filled header strip behind the sender name -- the one place with a filled patch on an otherwise plain row. Selected fill is unchanged. - Received DMs and channel posts now show their hop count as the same tiny digit icon a sent message already uses for its repeater/echo count, using the hop path the mesh already records for incoming messages. - DM/channel history list: UP at the oldest message now wraps straight to the newest (not the compose row), closing the loop the same way every other list in the UI does at its ends. - A received message's age no longer goes blank when its timestamp reads slightly ahead of the device's own clock (sender/receiver skew, or an unsynced receiver) or is genuinely unknown -- both now show the receipt time instead of leaving the row with just a name and hop count. - release-notes.md: add the v1.28 section covering the above plus this cycle's already-committed dev fixes (GPS altitude field, units compliance, noise floor, Msg wake location, bot relay tracking, PR #34). - Add .github/workflows/build-solo-sim.yml: builds the companion+repeater WASM sim via Emscripten and attaches it as a fixed-name asset to the tag's draft release, independent of the hardware firmware build job. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,67 @@
|
||||
name: Build Solo Sim (WASM)
|
||||
|
||||
# Builds the companion_radio browser sim (real firmware compiled to WASM via
|
||||
# Emscripten, see variants/sim/) and attaches it to the same draft release
|
||||
# build-solo-firmwares.yml creates for this tag, as a single fixed-name
|
||||
# asset: solo-sim-wasm.zip. Fixed name (no version in it) so
|
||||
# meshcore-solo-site's server-side deploy script can always fetch it from
|
||||
# the stable "latest release" download URL without knowing the tag --
|
||||
# see meshcore-solo-site/scripts/fetch-wasm-release.sh.
|
||||
#
|
||||
# Independent job/workflow from the hardware firmware builds: different
|
||||
# toolchain (Emscripten, not PlatformIO), and a WASM build failure shouldn't
|
||||
# block or delay the actual firmware release.
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
push:
|
||||
tags:
|
||||
- 'v*'
|
||||
|
||||
jobs:
|
||||
build-sim-wasm:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Clone Repo
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Cache emsdk
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: variants/sim/tools/emsdk
|
||||
key: emsdk-6.0.9-${{ runner.os }}
|
||||
|
||||
- name: Install emsdk 6.0.9
|
||||
run: |
|
||||
if [ ! -x variants/sim/tools/emsdk/upstream/emscripten/em++ ]; then
|
||||
rm -rf variants/sim/tools/emsdk
|
||||
git clone --depth 1 https://github.com/emscripten-core/emsdk.git variants/sim/tools/emsdk
|
||||
cd variants/sim/tools/emsdk
|
||||
python3 ./emsdk.py install 6.0.9
|
||||
python3 ./emsdk.py activate 6.0.9
|
||||
else
|
||||
echo "emsdk 6.0.9 already cached"
|
||||
fi
|
||||
|
||||
- name: Build companion WASM
|
||||
run: variants/sim/build_wasm.sh
|
||||
|
||||
- name: Build repeater WASM
|
||||
run: variants/sim/build_wasm_repeater.sh
|
||||
|
||||
- name: Package
|
||||
run: |
|
||||
mkdir -p out/wasm/companion out/wasm/repeater
|
||||
cp variants/sim/web/build/meshcore_sim.js variants/sim/web/build/meshcore_sim.wasm out/wasm/companion/
|
||||
cp variants/sim/web/build/repeater/meshcore_sim_repeater.js variants/sim/web/build/repeater/meshcore_sim_repeater.wasm out/wasm/repeater/
|
||||
cd out && zip -r solo-sim-wasm.zip wasm
|
||||
|
||||
- name: Attach to release
|
||||
if: startsWith(github.ref, 'refs/tags/')
|
||||
uses: softprops/action-gh-release@v2
|
||||
with:
|
||||
draft: true
|
||||
files: out/solo-sim-wasm.zip
|
||||
@@ -63,11 +63,17 @@ static inline void fmtDist(char* buf, int n, float km, bool imperial) {
|
||||
|
||||
// Compact age tag for a timestamp, e.g. "12s" / "5m" / "3h" / "2d" — sized to
|
||||
// sit inline after a name (unlike a full "X ago" sentence). Empty string for
|
||||
// an unknown (0) or future timestamp. Takes `now` rather than reading the RTC
|
||||
// itself, so this stays a pure function like the rest of this file.
|
||||
// a genuinely unknown (0) timestamp (callers like NearbyScreen::fmtAge() rely
|
||||
// on this to show "unknown" for a contact never actually heard from, which a
|
||||
// clamped "0s ago" would misreport as just-seen). A timestamp slightly ahead
|
||||
// of `now` (sender/receiver clock skew -- e.g. an incoming message whose
|
||||
// sender's clock runs a little fast, or ours hasn't synced yet) clamps to
|
||||
// "0s" instead of going blank, since it did just arrive. Takes `now` rather
|
||||
// than reading the RTC itself, so this stays a pure function like the rest
|
||||
// of this file.
|
||||
static inline void fmtAgeShort(char* buf, int n, uint32_t now, uint32_t lastmod) {
|
||||
if (lastmod == 0 || now < lastmod) { buf[0] = '\0'; return; }
|
||||
uint32_t age = now - lastmod;
|
||||
if (lastmod == 0) { buf[0] = '\0'; return; }
|
||||
uint32_t age = (now > lastmod) ? (now - lastmod) : 0;
|
||||
if (age < 60) snprintf(buf, n, "%us", (unsigned)age);
|
||||
else if (age < 3600) snprintf(buf, n, "%um", (unsigned)(age / 60));
|
||||
else if (age < 86400) snprintf(buf, n, "%uh", (unsigned)(age / 3600));
|
||||
|
||||
@@ -460,19 +460,24 @@ class MessagesScreen : public UIScreen {
|
||||
}
|
||||
|
||||
// Selection frame for one history message box, shared by the DM/room and
|
||||
// channel history lists. Selected = solid fill; unselected = outline with a
|
||||
// filled header strip. Leaves the ink DARK (for the sender row drawn next).
|
||||
// channel history lists. Selected = solid fill, dark text (the same
|
||||
// drawSelectionRow() convention every other list in the UI uses). Unselected
|
||||
// = plain black box (like the rest of the UI) with a thin light frame
|
||||
// separating it from its neighbours -- no filled header strip, which used
|
||||
// to leave a patch of light background on every bubble whether selected or
|
||||
// not, unlike anywhere else in the UI. Leaves the ink colour set correctly
|
||||
// for the sender row drawn next (and everything after it, until something
|
||||
// else changes it): LIGHT for the whole unselected box, DARK for selected.
|
||||
// Spans exactly [box_x, box_x+box_w) — the caller sizes/positions the bubble
|
||||
// (see computeBubbleBox below), this just draws whatever box it's given.
|
||||
static void drawHistRowFrame(DisplayDriver& d, int box_x, int box_w, int y, int bh, int lh, bool sel) {
|
||||
d.setColor(DisplayDriver::LIGHT);
|
||||
if (sel) {
|
||||
d.fillRect(box_x, y, box_w, bh);
|
||||
d.setColor(DisplayDriver::DARK);
|
||||
} else {
|
||||
d.drawRect(box_x, y, box_w, bh);
|
||||
d.fillRect(box_x + 1, y + 1, box_w - 2, lh);
|
||||
}
|
||||
d.setColor(DisplayDriver::DARK);
|
||||
}
|
||||
|
||||
// Width of an ack/delivery glyph (see drawAckGlyph) — needed up front to size
|
||||
@@ -1560,14 +1565,23 @@ public:
|
||||
const char* body = skipReplyPrefix(dmDisplayParts(e, is_room, filtered_name, sender_buf, sizeof(sender_buf)));
|
||||
const char* sender = sender_buf;
|
||||
|
||||
char age[6]; geo::fmtAgeShort(age, sizeof(age), now_ts, e.timestamp);
|
||||
// e.timestamp==0 shouldn't happen (storeDMMsg() falls back to receipt
|
||||
// time at write time), but if it ever does, show the receipt time
|
||||
// (now_ts) rather than leave the row with no age at all.
|
||||
char age[6]; geo::fmtAgeShort(age, sizeof(age), now_ts, e.timestamp ? e.timestamp : now_ts);
|
||||
int age_w = age[0] ? display.getTextWidth(age) + 3 : 0;
|
||||
|
||||
// Size the bubble to its own content before drawing anything (see
|
||||
// computeBubbleBox): the header (sender+ack+age) vs the body, measured
|
||||
// once here and reused below instead of re-wrapping.
|
||||
int full_avail = display.width() - reserve;
|
||||
int ack_w = e.outgoing ? (3 + ackGlyphWidth(display, _history.dmEffectiveStatus(e), e.attempt + 1)) : 0;
|
||||
// Incoming: the hop count the DM actually took to reach us, shown as
|
||||
// the same tiny digit icon an outgoing send uses for its relay/echo
|
||||
// count -- no ack glyph exists for incoming (there's nothing to
|
||||
// deliver), so this slot is otherwise empty.
|
||||
int in_hop_count = !e.outgoing ? (e.path_len & 63) : 0;
|
||||
int ack_w = e.outgoing ? (3 + ackGlyphWidth(display, _history.dmEffectiveStatus(e), e.attempt + 1))
|
||||
: (in_hop_count > 0 ? (3 + miniIconNumberWidth(display, in_hop_count)) : 0);
|
||||
int header_w = 3 + display.getTextWidth(sender) + ack_w + age_w + 3;
|
||||
int body_w, nl = 0;
|
||||
if (portrait_expand) {
|
||||
@@ -1590,9 +1604,13 @@ public:
|
||||
if (e.outgoing) { // delivery marker after "Me"
|
||||
int gx = box.x + 3 + display.getTextWidth(sender) + 3;
|
||||
drawAckGlyph(display, gx, y + 1, _history.dmEffectiveStatus(e), e.attempt + 1);
|
||||
} else if (in_hop_count > 0) { // hop count after the sender name
|
||||
int gx = box.x + 3 + display.getTextWidth(sender) + 3;
|
||||
miniIconDrawNumber(display, gx, y + 1, in_hop_count);
|
||||
}
|
||||
if (age[0]) { display.setCursor(box.x + box.w - age_w, y + 1); display.print(age); }
|
||||
if (!sel) display.setColor(DisplayDriver::LIGHT);
|
||||
// Ink is already LIGHT (unselected) or DARK (selected) from
|
||||
// drawHistRowFrame above, and nothing since has changed it.
|
||||
if (portrait_expand) {
|
||||
for (int li = 0; li < nl; li++) { display.setCursor(box.x + 3, y + (li + 1) * lh + 1); display.print(s_wrap_lines[li]); }
|
||||
} else {
|
||||
@@ -1751,7 +1769,11 @@ public:
|
||||
msg_part[sizeof(msg_part) - 1] = '\0';
|
||||
}
|
||||
|
||||
char age[6]; geo::fmtAgeShort(age, sizeof(age), now_ts, _history.chAtPos(ring_pos).timestamp);
|
||||
// See the DM history block above: 0 shouldn't happen (addChannelMsg()
|
||||
// also falls back to receipt time at write time), but fall back to
|
||||
// now_ts here too rather than leave the row with no age at all.
|
||||
uint32_t ch_ts = _history.chAtPos(ring_pos).timestamp;
|
||||
char age[6]; geo::fmtAgeShort(age, sizeof(age), now_ts, ch_ts ? ch_ts : now_ts);
|
||||
int age_w = age[0] ? display.getTextWidth(age) + 3 : 0;
|
||||
const char* body = skipReplyPrefix(msg_part);
|
||||
|
||||
@@ -1763,8 +1785,12 @@ public:
|
||||
bool outgoing = strcmp(sender, "Me") == 0;
|
||||
bool show_ack = outgoing && _history.chAtPos(ring_pos).relay_status == ACK_OK;
|
||||
int relay_count = show_ack ? (_history.chAtPos(ring_pos).path_len & 63) : 0;
|
||||
// Incoming: the hop count this post actually took to reach us, shown
|
||||
// the same way an outgoing post's repeater-echo count is.
|
||||
int in_hop_count = !outgoing ? (_history.chAtPos(ring_pos).path_len & 63) : 0;
|
||||
int full_avail = display.width() - reserve;
|
||||
int ack_w = show_ack ? (3 + ackGlyphWidth(display, ACK_OK, 1, relay_count)) : 0;
|
||||
int ack_w = show_ack ? (3 + ackGlyphWidth(display, ACK_OK, 1, relay_count))
|
||||
: (in_hop_count > 0 ? (3 + miniIconNumberWidth(display, in_hop_count)) : 0);
|
||||
int header_w = 3 + display.getTextWidth(sender) + ack_w + age_w + 3;
|
||||
int body_w, nl = 0;
|
||||
if (portrait_expand) {
|
||||
@@ -1785,9 +1811,13 @@ public:
|
||||
if (show_ack) {
|
||||
int gx = box.x + 3 + display.getTextWidth(sender) + 3;
|
||||
drawAckGlyph(display, gx, y + 1, ACK_OK, 1, relay_count);
|
||||
} else if (in_hop_count > 0) {
|
||||
int gx = box.x + 3 + display.getTextWidth(sender) + 3;
|
||||
miniIconDrawNumber(display, gx, y + 1, in_hop_count);
|
||||
}
|
||||
if (age[0]) { display.setCursor(box.x + box.w - age_w, y + 1); display.print(age); }
|
||||
if (!sel) display.setColor(DisplayDriver::LIGHT);
|
||||
// Ink is already LIGHT (unselected) or DARK (selected) from
|
||||
// drawHistRowFrame above, and nothing since has changed it.
|
||||
if (portrait_expand) {
|
||||
for (int li = 0; li < nl; li++) { display.setCursor(box.x + 3, y + (li + 1) * lh + 1); display.print(s_wrap_lines[li]); }
|
||||
} else {
|
||||
@@ -2305,11 +2335,12 @@ public:
|
||||
if (_dm_hist_sel >= _dm_hist_scroll + _hist_visible)
|
||||
_dm_hist_scroll = _dm_hist_sel - _hist_visible + 1;
|
||||
} else if (_dm_hist_sel == dm_count - 1) {
|
||||
// Oldest (top of the list) -> wrap to the compose row, the list's
|
||||
// own bottom-most stop -- same ring-wrap every other list in the UI
|
||||
// does at its ends, just closing the loop through the compose row
|
||||
// instead of straight back to index 0.
|
||||
_dm_hist_sel = -1;
|
||||
// Oldest (top of the list) -> wrap straight to the newest (bottom
|
||||
// of the list), same ring-wrap every other list in the UI does at
|
||||
// its ends -- the compose row sits below the list itself and is
|
||||
// not part of this wrap.
|
||||
_dm_hist_sel = 0;
|
||||
_dm_hist_scroll = 0;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
@@ -2399,8 +2430,9 @@ public:
|
||||
_hist_sel++;
|
||||
if (_hist_sel >= _hist_scroll + _hist_visible) _hist_scroll = _hist_sel - _hist_visible + 1;
|
||||
} else if (_hist_sel == ch_hist_count - 1) {
|
||||
// Oldest -> wrap to the compose row, same as the DM history handler.
|
||||
_hist_sel = -1;
|
||||
// Oldest -> wrap straight to the newest, same as the DM history handler.
|
||||
_hist_sel = 0;
|
||||
_hist_scroll = 0;
|
||||
}
|
||||
updateChannelUnread();
|
||||
return true;
|
||||
|
||||
@@ -1,3 +1,23 @@
|
||||
## MeshCore Solo Companion Firmware v1.28
|
||||
|
||||
### What's new
|
||||
|
||||
- **The Clock/Lock dashboard gets a separate "Altitude (GPS)" field**, alongside the existing barometric one (now labelled "Altitude (Baro)") — the original single Altitude field only ever read a barometric sensor's telemetry, showing `--` on any board without one even with a perfectly good GPS fix.
|
||||
- **Received messages now show how many hops they actually took to reach you**, right in the message list — the same tiny digit-icon a sent message already uses for its repeater/echo count, now shown for incoming DMs and channel posts too, using the hop path the mesh already records for them.
|
||||
|
||||
### Fixes
|
||||
|
||||
- **Every altitude reading (baro + GPS, Clock and Lock screen) and the GPS home page's "alt" row now respect Settings › System › Units**, same as every other distance in the UI — none of them checked metric/imperial before. Locator's Radius row had the same gap and is fixed the same way.
|
||||
- **The Radio home page's noise floor showed "n/a" whenever Pwr save (RX duty-cycle) was on**, on the assumption duty-cycle RX can't sample it. That's stale — the radio already recalibrates it periodically even under duty-cycle, which is why Diagnostics' own reading was already showing something real. Radio home page now matches.
|
||||
- **"Msg wake" moved from Settings › Sound to Settings › Display** — a more logical home for a display-wake toggle than notifications. Menu order only; the setting itself is unaffected.
|
||||
- **Remote Bot's channel replies (trigger reply, channel command reply, `!gps` fix result) never showed which repeaters echoed them back**, unlike every other way of posting to a channel. Bot-originated posts now arm the same relay tracking an app or on-device send already gets.
|
||||
- **Creating a new message channel didn't refresh the channel list until leaving and re-entering the screen.** Contributed by @3urobeat.
|
||||
- **The DM and channel history lists didn't wrap at their ends like every other list in the UI.** UP at the oldest message now wraps straight to the newest, and DOWN past the newest wraps to the compose row and back to the oldest, closing the loop both ways.
|
||||
- **Unselected message bubbles had a stray light-filled header strip behind the sender name**, the one place in the UI with a filled patch on an otherwise plain row. Unselected bubbles are now plain (matching every other unselected row); the selected fill is unchanged.
|
||||
- **A received message could show no time at all — just the sender name and hop count** — whenever its timestamp read slightly ahead of the device's own clock (sender/receiver clock skew, or the receiver's clock not synced yet) or was genuinely unknown. Both cases now show the receipt time instead of going blank.
|
||||
|
||||
---
|
||||
|
||||
## MeshCore Solo Companion Firmware v1.27
|
||||
|
||||
### What's new
|
||||
|
||||
Reference in New Issue
Block a user