+ {/* Header */}
+
+
+
+
+
+
Channels
+
+ Channels configured on each connected radio. Read-only.
+
+
+
+
+ {loading ? (
+
+ ) : error ? (
+
+ {error}
+
+ ) : (
+ <>
+ {/* Meshtastic channels */}
+
+
+
Meshtastic Channels
+
Routes by channel index.
+
+ {mtChannels.length > 0 ? (
+
+
+
+
+ | Index |
+ Name |
+ Role |
+
+
+
+ {mtChannels.map((ch) => (
+
+ | {ch.index} |
+ {ch.name} |
+ {ch.role} |
+
+ ))}
+
+
+
+ ) : (
+
+ Node offline — channels unavailable
+
+ )}
+
+
+ {/* MeshCore channels */}
+
+
+
MeshCore Channels
+
Routes by channel name.
+
+ {mcChannels.length > 0 ? (
+
+
+
+
+ | Name |
+ On-air hash |
+
+
+
+ {mcChannels.map((ch) => (
+
+ | {ch.name} |
+
+ {ch.hash != null ? `0x${ch.hash}` : '—'}
+ |
+
+ ))}
+
+
+
+ ) : (
+
+ MeshCore not connected
+
+ )}
+
+ >
+ )}
+
+ )
+}
diff --git a/work/meshai/dashboard/api/mesh_send_routes.py b/work/meshai/dashboard/api/mesh_send_routes.py
index 73e234b..321699d 100644
--- a/work/meshai/dashboard/api/mesh_send_routes.py
+++ b/work/meshai/dashboard/api/mesh_send_routes.py
@@ -39,6 +39,24 @@ async def meshcore_channels(request: Request):
return {"active": False, "channels": []}
+@router.get("/meshcore/channels/detail")
+async def meshcore_channels_detail(request: Request):
+ """Enumerated MeshCore channels with on-air hash, if connected.
+
+ Returns {"active": bool, "channels": [{"name": str, "hash": str|null}]}.
+ Routes by channel NAME (no slot/index), so no index is exposed here.
+ """
+ connector = getattr(request.app.state, "connector", None)
+ mc = _find_child(connector, "meshcore")
+ if mc is not None and getattr(mc, "connected", False):
+ try:
+ channels = list(mc.channel_details())
+ except Exception:
+ channels = []
+ return {"active": True, "channels": channels}
+ return {"active": False, "channels": []}
+
+
@router.get("/meshcore/contacts")
async def meshcore_contacts(request: Request):
"""Roster of known MeshCore contacts if a meshcore transport is connected."""
diff --git a/work/meshai/transport/meshcore_transport.py b/work/meshai/transport/meshcore_transport.py
index 5527011..604702c 100644
--- a/work/meshai/transport/meshcore_transport.py
+++ b/work/meshai/transport/meshcore_transport.py
@@ -117,6 +117,10 @@ class MeshCoreTransport(MeshTransport):
# Companion channel table: channel NAME -> slot index, built at
# connect time by _enumerate_channels(). Empty until connected.
self._chan_name_to_idx: dict[str, int] = {}
+ # Ordered [{name, hash}] detail for each enumerated channel (incl.
+ # Public), captured alongside _chan_name_to_idx at connect. Read-only
+ # view for the dashboard Channels page. Empty until connected.
+ self._chan_details: list[dict] = []
# Self-advertisement tracking.
self._last_advert_sent: Optional[float] = None # epoch seconds or None
# asyncio.Task handle for the periodic advert loop; None when inactive.
@@ -758,6 +762,7 @@ class MeshCoreTransport(MeshTransport):
with a hard cap of 40 slots.
"""
self._chan_name_to_idx = {}
+ self._chan_details = []
try:
empty_run = 0
for idx in range(40):
@@ -787,6 +792,10 @@ class MeshCoreTransport(MeshTransport):
# Named slot: exact, case-sensitive (firmware name is already
# null-truncated / utf-8-decoded — do NOT trim or lowercase).
self._chan_name_to_idx[name] = slot
+ # Additive read-only detail: preserve order, capture on-air hash.
+ self._chan_details.append(
+ {"name": name, "hash": payload.get("channel_hash")}
+ )
empty_run = 0
except Exception as exc:
logger.warning("MeshCore: channel enumeration error: %s", exc)
@@ -799,6 +808,12 @@ class MeshCoreTransport(MeshTransport):
"""Enumerated MeshCore channel names (from _chan_name_to_idx, populated at connect)."""
return list(self._chan_name_to_idx.keys())
+ def channel_details(self) -> list[dict]:
+ """Ordered [{name, hash}] for each enumerated MeshCore channel (incl.
+ Public); ``hash`` is the on-air channel hash or None. Read-only view
+ for the dashboard; captured alongside _chan_name_to_idx at connect."""
+ return list(self._chan_details)
+
def get_contacts(self) -> list[dict]:
"""Roster of known MeshCore contacts. [] if not connected."""
if self._mc is None or not self._connected: