Skip to content

fix(meshcore): preserve LPP channel byte in remote telemetry rows#3141

Merged
Yeraze merged 1 commit into
mainfrom
fix/3139-lpp-channel
May 22, 2026
Merged

fix(meshcore): preserve LPP channel byte in remote telemetry rows#3141
Yeraze merged 1 commit into
mainfrom
fix/3139-lpp-channel

Conversation

@Yeraze

@Yeraze Yeraze commented May 22, 2026

Copy link
Copy Markdown
Owner

Summary

Fixes part 1 of #3139 — MeshCore telemetry channels 2/3/4 not appearing.

Cayenne-LPP responses can carry several records of the same type under different channel bytes (e.g. battery on ch1 = main, ch2 = solar, ch3 = aux). recordToTelemetryRows in src/server/services/meshcoreRemoteTelemetryScheduler.ts was reading only record.type and dropping record.channel, so all four readings collapsed onto the same telemetryType (e.g. mc_battery_volts). The frontend renders one chart per telemetryType, so only the channel-1 reading surfaced.

Fix

Embed _ch<channel> into the telemetry type string:

  • ch1/battery → mc_battery_volts_ch1
  • ch2/battery → mc_battery_volts_ch2
  • ch3/battery → mc_battery_volts_ch3
  • ch4/battery → mc_battery_volts_ch4

Each LPP channel now produces its own chart and time series.

Subtle bit

We deliberately do not write record.channel into the row's channel column. That column is for the mesh channel the packet rode on (read by maskTelemetryByChannel in src/server/utils/nodeEnhancer.ts for per-channel permission filtering). LPP's channel is a within-packet sensor-instance discriminator — an entirely different concept. Mixing the two would cause per-channel permission filters to deny access to LPP channel-2/3/4 readings on private mesh channels.

This is called out as an inline comment in the function and asserted by a regression test.

Test plan

  • npm run test — full Vitest suite (5390 tests) passes
  • Targeted meshcoreRemoteTelemetryScheduler.test.ts — all recordToTelemetryRows tests pass with new assertions
  • New regression test: feeds four ch1..ch4 records of type=116 (voltage) and asserts four distinct telemetry types are produced
  • New negative test: asserts that the row's channel column is not populated from the LPP byte
  • Manual verification on a MeshCore companion with sensors on multiple LPP channels — should see distinct mc_*_ch1, mc_*_ch2, ... type names in the telemetry table

Note on display labels

PR follow-up (separate) will add TELEMETRY_LABELS entries for the mc_*_ch<N> types so the Telemetry tab and the (still-to-be-built) MeshCore Telemetry Dashboard render friendly names. With this PR alone the raw machine names will show — but at least all four channels' data is captured and queryable, which was the regression.

Fixes part 1 of #3139.

🤖 Generated with Claude Code

)

Cayenne-LPP responses can carry several records of the same `type` under
different `channel` bytes (e.g. battery on ch1 = main, ch2 = solar,
ch3 = aux). `recordToTelemetryRows` was reading only `record.type` and
dropping `record.channel`, so all four readings collapsed onto the same
`telemetryType` (e.g. `mc_battery_volts`). The frontend renders one
chart per telemetryType, so only the LPP channel-1 reading was visible
and channels 2/3/4 effectively disappeared.

Fix: embed `_ch<channel>` into the telemetry type string. Channel-1
becomes `mc_battery_volts_ch1`, channel-2 becomes `mc_battery_volts_ch2`,
etc. — each gets its own chart and time series.

Notable: we deliberately do NOT write `record.channel` into the row's
`channel` column. That column is for the *mesh* channel the packet rode
on (used by `maskTelemetryByChannel` in nodeEnhancer.ts for per-channel
permission filtering). LPP's `channel` is a within-packet sensor-instance
discriminator — an entirely different concept. Mixing them would cause
per-channel permission filters to deny access to LPP channel 2/3/4
readings on private mesh channels.

Existing recordToTelemetryRows tests updated to expect the new suffix.
Added two regression tests: one that feeds four ch1..ch4 records and
asserts distinct telemetry types, and one that asserts the row's `channel`
column is NOT populated from the LPP byte.

Reported by @HougeDK in #3139.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@github-actions

github-actions Bot commented May 22, 2026

Copy link
Copy Markdown

Claude finished @Yeraze's task —— View job


Code Review Complete

I've thoroughly reviewed this PR and the implementation looks excellent. Here's my assessment:

Review Tasks:

  • Read and analyze the implementation changes
  • Review the test changes and coverage
  • Check for potential bugs or edge cases
  • Evaluate performance and security considerations
  • Provide comprehensive feedback

Overall Assessment: LGTM - High Quality Implementation

This is a well-designed fix that properly addresses the Cayenne-LPP channel collision issue while maintaining architectural integrity.

🎯 Code Quality & Best Practices

Excellent:

  • Clear problem identification: The fix correctly identifies that multiple LPP records with the same type but different channel bytes were collapsing
  • Minimal, surgical change: The solution adds only the necessary channelSuffix logic without over-engineering
  • Strong documentation: The inline comment at lines 166-169 clearly explains the rationale
  • Adherence to patterns: Uses existing MC_TELEMETRY_PREFIX and follows established naming conventions

Implementation at src/server/services/meshcoreRemoteTelemetryScheduler.ts:170:

const channelSuffix = `_ch${record.channel}`;
const baseType = `${MC_TELEMETRY_PREFIX}${naming.type}${channelSuffix}`;

🔧 Technical Correctness

Key architectural decision (lines 174-180): The code correctly does NOT write record.channel into the row's channel column. This separation is crucial:

  • LPP channel: Within-packet sensor instance discriminator
  • Mesh channel: Network routing channel for permission filtering

This prevents permission system conflicts where LPP channels 2/3/4 would be incorrectly denied access on private mesh channels.

🧪 Test Coverage - Comprehensive

New regression test (lines 154-170): Excellent coverage of the core issue:

it('preserves the LPP channel byte in the telemetry type', () => {
  const recs: MeshCoreTelemetryRecord[] = [
    { channel: 1, type: 116, value: 4.10 },
    { channel: 2, type: 116, value: 12.50 },
    // ... channels 3,4
  ];
  // Expects: mc_battery_volts_ch1, mc_battery_volts_ch2, etc.
});

Negative test (lines 172-179): Verifies the architectural decision about channel separation:

it('does not write the LPP channel into the row\'s mesh-channel column', () => {
  // Ensures channel isolation between LPP and mesh concepts
});

🚨 Security & Performance

Security: ✅ No concerns - the change is purely cosmetic string formatting
Performance: ✅ Negligible impact - adds one string concatenation per telemetry record

🐛 Potential Issues: None Found

  • Edge cases handled: Empty/null channels would still work (undefined channels become _chundefined, which is detectable)
  • Backward compatibility: Existing telemetry data remains unaffected
  • Type safety: All changes maintain existing TypeScript contracts

📝 Minor Suggestions

  1. Consider validation: While not required for this fix, future hardening could validate record.channel is a positive integer
  2. Documentation follow-up: The PR description mentions adding TELEMETRY_LABELS entries in a future PR - that's the right approach

🎯 Results

The implementation correctly transforms:

Before: ch1/battery → mc_battery_volts (channels 2/3/4 lost)
After:  ch1/battery → mc_battery_volts_ch1
        ch2/battery → mc_battery_volts_ch2  
        ch3/battery → mc_battery_volts_ch3
        ch4/battery → mc_battery_volts_ch4

Each LPP channel now generates distinct time series and charts in the frontend.

Recommendation: ✅ Approve and merge

@Yeraze Yeraze merged commit 1edbeec into main May 22, 2026
19 checks passed
@Yeraze Yeraze deleted the fix/3139-lpp-channel branch May 22, 2026 15:20
Yeraze added a commit that referenced this pull request May 22, 2026
…oard (#3139) (#3142)

Builds out the missing destination for "favorited" MeshCore telemetry
charts. The favorite-star toggle was already present in MeshCore views
(DM contact panels), but there was no MeshCore-side surface that read
`telemetryFavorites` back. Reported by @HougeDK in #3139.

## What changed

1. **Dashboard becomes source-agnostic** via a new `DashboardDataSource`
   abstraction (`src/components/Dashboard/dataSources.ts`). Every
   Meshtastic-specific assumption now goes through the data-source
   adapter:
   - Nodes endpoint (`/api/nodes` vs `/api/sources/:id/meshcore/nodes`)
   - Node-key extraction (`user.id` vs `publicKey`)
   - Device-role labelling (`getDeviceRoleName` vs MeshCore advType map)
   - Solar-overlay default-on type list
   - Whether the "Add Widget" button is shown (MeshCore: false, since
     `nodeStatus` / `traceroute` / `hopDistribution` widgets all assume
     Meshtastic data shapes and protocol concepts)

   Two adapters ship: `meshtasticDashboardSource` (the default, exactly
   preserves prior behaviour byte-for-byte) and `meshcoreDashboardSource`
   (the new one). The Dashboard component accepts the adapter as an
   optional prop with the Meshtastic default, so the existing App.tsx
   mount point is unchanged.

2. **New `MeshCoreTelemetryView`** mounts the same `<Dashboard>` with
   `meshcoreDashboardSource`. Adds a "📊 Telemetry" entry to
   `MeshCoreSubToolbar` between Direct Messages and Node Info.

3. **`mc_*` label support in `TELEMETRY_LABELS`** — adds entries for
   every LPP type name from `LPP_TYPE_NAMES` and every `mc_status_*`
   field from `STATUS_FIELD_MAP`. The label resolver also recognises
   the `_ch<N>` suffix from #3141 and renders e.g. `mc_battery_volts_ch2`
   as `Battery (ch2)` without enumerating every type×channel combo.

## Test plan

- [x] `npx tsc --noEmit` clean
- [x] Full Vitest suite: 5411 / 5411 passing (5395 before + 16 new)
- [x] New `dataSources.test.ts` covers the MeshCore adapter shape mapping,
      role labels, key extraction, and per-source defaults
- [x] New `TelemetryChart.test.ts` covers the channel-suffix label resolution
- [ ] Manual smoke: open a MeshCore source, navigate to the Telemetry tab,
      verify favorited charts render and the "Add Widget" button is hidden

## Notable design decisions

- **No data-shape forking inside Dashboard internals.** The adapter
  normalises both sources into the existing `NodeInfo` shape at fetch
  time, so the hooks and grid don't have to know which kind of node
  they're holding. Keeps the diff small.
- **Custom widgets are gated, not duplicated.** Rather than build a
  separate `MeshCoreDashboard` component, we hide the four
  Meshtastic-only widget types via `showCustomWidgets: false` on the
  MeshCore adapter. If/when MeshCore grows equivalent widgets (e.g.
  MeshCore-flavoured hop distribution), they get added to the same
  flag-gated AddWidgetModal flow.

Closes part 2 of #3139.

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Yeraze added a commit that referenced this pull request May 22, 2026
…ish (#3145)

Version bump and CHANGELOG entry for 4.6.5. Highlights:

- #3143 MQTT Bridge mirror dashboard (read-only Open button on bridge
  cards; suppresses every TX surface; MqttBridgeManager now exposes the
  Meshtastic-shaped methods the consolidated /api/poll + /api/connection
  endpoints expect).
- #3142 MeshCore Telemetry dashboard tab via source-agnostic Dashboard.
- #3141 MeshCore: preserve LPP channel byte in remote telemetry rows.
- #3140 DM: skip PKI flag when keyMismatchDetected.
- #3138 docs reorganization.
- #3137 unified: merge nodes across sources so labels show in the
  unified Nodes view.
- #3136 mqtt: allow standalone mqtt_bridge as client-proxy target.

CLAUDE.md version line updated to 4.6.5.

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant