mirror of
https://github.com/Kpa-clawbot/meshcore-analyzer.git
synced 2026-10-23 16:52:42 +00:00
Merge branch 'areas-meshguide-sync'
This commit is contained in:
commit
601f1e4dd7
2 files changed
+157
-7
No files matched your search
+85
-6
@@ -2865,6 +2865,33 @@
|
|||||||
if (scrollContainer) scrollContainer.scrollTop = savedScrollTop;
|
if (scrollContainer) scrollContainer.scrollTop = savedScrollTop;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// #1868 — CONTROL DISCOVER_REQ/RESP node-type + SNR display helpers.
|
||||||
|
// node_type (RESP low nibble, single value) and filter (REQ byte, bitmask
|
||||||
|
// of these SAME type values -- firmware checks `filter & (1 << ADV_TYPE_x)`)
|
||||||
|
// share the ADV_TYPE_* enum already used for ADVERT role labels
|
||||||
|
// (cmd/ingestor/decoder.go's advertRole(), firmware/src/helpers/
|
||||||
|
// AdvertDataHelpers.h:7-12): 0 NONE, 1 CHAT, 2 REPEATER, 3 ROOM, 4 SENSOR.
|
||||||
|
var CTRL_TYPE_LABELS = { 0: 'None', 1: 'Companion', 2: 'Repeater', 3: 'Room Server', 4: 'Sensor' };
|
||||||
|
function ctrlTypeLabel(n) {
|
||||||
|
return CTRL_TYPE_LABELS[n] != null ? CTRL_TYPE_LABELS[n] : ('Unknown(' + n + ')');
|
||||||
|
}
|
||||||
|
function ctrlFilterLabels(filter) {
|
||||||
|
var labels = [];
|
||||||
|
for (var bit = 0; bit <= 4; bit++) {
|
||||||
|
if (filter & (1 << bit)) labels.push(ctrlTypeLabel(bit));
|
||||||
|
}
|
||||||
|
return labels;
|
||||||
|
}
|
||||||
|
// Firmware sends SNR as a wire-encoded int8 (value * 4); divide by 4.0 for
|
||||||
|
// real dB, same conversion already applied to TRACE's snrValues in both
|
||||||
|
// decoders (cmd/ingestor/decoder.go:1016, cmd/server/decoder.go:619) --
|
||||||
|
// CONTROL's SNR just hasn't had the same conversion applied yet, and doing
|
||||||
|
// it here (display-only) avoids any ambiguity with already-stored raw
|
||||||
|
// values from packets ingested before this fix.
|
||||||
|
function ctrlSnrDb(raw) {
|
||||||
|
return (Number(raw) / 4.0).toFixed(2);
|
||||||
|
}
|
||||||
|
|
||||||
function getDetailPreview(decoded) {
|
function getDetailPreview(decoded) {
|
||||||
if (!decoded) return '';
|
if (!decoded) return '';
|
||||||
// Channel messages (GRP_TXT) — show channel name and message text
|
// Channel messages (GRP_TXT) — show channel name and message text
|
||||||
@@ -2941,7 +2968,8 @@
|
|||||||
const parts = [];
|
const parts = [];
|
||||||
if (subtype === 'DISCOVER_REQ') {
|
if (subtype === 'DISCOVER_REQ') {
|
||||||
if (decoded.ctrlFilter != null) {
|
if (decoded.ctrlFilter != null) {
|
||||||
parts.push(`filter=0x${Number(decoded.ctrlFilter).toString(16).padStart(2, '0')}`);
|
const labels = ctrlFilterLabels(Number(decoded.ctrlFilter));
|
||||||
|
parts.push(`filter=${labels.length ? labels.join('+') : '0x' + Number(decoded.ctrlFilter).toString(16).padStart(2, '0')}`);
|
||||||
}
|
}
|
||||||
if (decoded.ctrlTag != null) {
|
if (decoded.ctrlTag != null) {
|
||||||
parts.push(`tag=0x${(Number(decoded.ctrlTag) >>> 0).toString(16).padStart(8, '0')}`);
|
parts.push(`tag=0x${(Number(decoded.ctrlTag) >>> 0).toString(16).padStart(8, '0')}`);
|
||||||
@@ -2951,16 +2979,21 @@
|
|||||||
}
|
}
|
||||||
} else if (subtype === 'DISCOVER_RESP') {
|
} else if (subtype === 'DISCOVER_RESP') {
|
||||||
if (decoded.ctrlNodeType != null) {
|
if (decoded.ctrlNodeType != null) {
|
||||||
parts.push(`type=${Number(decoded.ctrlNodeType)}`);
|
parts.push(`type=${ctrlTypeLabel(Number(decoded.ctrlNodeType))}`);
|
||||||
}
|
}
|
||||||
if (decoded.ctrlSNR != null) {
|
if (decoded.ctrlSNR != null) {
|
||||||
parts.push(`snr=${Number(decoded.ctrlSNR)}`);
|
parts.push(`snr=${ctrlSnrDb(decoded.ctrlSNR)}dB`);
|
||||||
}
|
}
|
||||||
if (decoded.ctrlTag != null) {
|
if (decoded.ctrlTag != null) {
|
||||||
parts.push(`tag=0x${(Number(decoded.ctrlTag) >>> 0).toString(16).padStart(8, '0')}`);
|
parts.push(`tag=0x${(Number(decoded.ctrlTag) >>> 0).toString(16).padStart(8, '0')}`);
|
||||||
}
|
}
|
||||||
if (decoded.ctrlPubKey) {
|
if (decoded.ctrlPubKey) {
|
||||||
parts.push(`pubkey=${escapeHtml(decoded.ctrlPubKey)}`);
|
// Row preview is synchronous/rendered per-row -- no live node
|
||||||
|
// lookup here (that would mean one API call per visible CONTROL
|
||||||
|
// row). Truncated to 8 hex chars, matching the same fallback
|
||||||
|
// used elsewhere in this file (e.g. srcLabel's pubKey.slice(0,8)
|
||||||
|
// in renderDetail) for an unresolved node identifier.
|
||||||
|
parts.push(`pubkey=${escapeHtml(decoded.ctrlPubKey.slice(0, 8))}…`);
|
||||||
}
|
}
|
||||||
} else if (decoded.ctrlFlags) {
|
} else if (decoded.ctrlFlags) {
|
||||||
parts.push(`flags=0x${escapeHtml(decoded.ctrlFlags)}`);
|
parts.push(`flags=0x${escapeHtml(decoded.ctrlFlags)}`);
|
||||||
@@ -3134,6 +3167,17 @@
|
|||||||
} catch {}
|
} catch {}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// #1868 — CONTROL DISCOVER_RESP carries a responder pubkey (full 32B or
|
||||||
|
// 8B prefix) with no name attached. /api/nodes/{pubkey} already handles
|
||||||
|
// prefix resolution server-side (issue #772's short-URL fallback), so
|
||||||
|
// this works for both lengths the same way. Falls back to null (plain
|
||||||
|
// truncated hex in buildFieldTable) when unresolved/unknown/blacklisted.
|
||||||
|
let ctrlPubKeyNode = null;
|
||||||
|
if (decoded.type === 'CONTROL' && decoded.ctrlPubKey) {
|
||||||
|
const nd = await api(`/nodes/${decoded.ctrlPubKey}`, { ttl: 30000 }).catch(() => null);
|
||||||
|
if (nd?.node?.public_key) ctrlPubKeyNode = nd.node;
|
||||||
|
}
|
||||||
|
|
||||||
// Resolve hops: prefer server-side resolved_path, fall back to client-side HopResolver
|
// Resolve hops: prefer server-side resolved_path, fall back to client-side HopResolver
|
||||||
if (pathHops.length) {
|
if (pathHops.length) {
|
||||||
try {
|
try {
|
||||||
@@ -3327,7 +3371,7 @@
|
|||||||
${hasRawHex ? `<div class="hex-legend">${buildHexLegend(ranges)}</div>
|
${hasRawHex ? `<div class="hex-legend">${buildHexLegend(ranges)}</div>
|
||||||
<div class="hex-dump">${createColoredHexDump(effectivePkt.raw_hex || pkt.raw_hex, ranges)}</div>` : ''}
|
<div class="hex-dump">${createColoredHexDump(effectivePkt.raw_hex || pkt.raw_hex, ranges)}</div>` : ''}
|
||||||
|
|
||||||
${hasRawHex ? buildFieldTable(effectivePkt.raw_hex ? effectivePkt : pkt, decoded, pathHops, ranges) : buildDecodedTable(decoded)}
|
${hasRawHex ? buildFieldTable(effectivePkt.raw_hex ? effectivePkt : pkt, decoded, pathHops, ranges, ctrlPubKeyNode) : buildDecodedTable(decoded)}
|
||||||
</details>` : ''}
|
</details>` : ''}
|
||||||
|
|
||||||
${observations.length > 1 ? `
|
${observations.length > 1 ? `
|
||||||
@@ -3508,7 +3552,7 @@
|
|||||||
return rows ? `<table class="detail-decoded" style="width:100%;border-collapse:collapse;margin-top:8px">${rows}</table>` : '';
|
return rows ? `<table class="detail-decoded" style="width:100%;border-collapse:collapse;margin-top:8px">${rows}</table>` : '';
|
||||||
}
|
}
|
||||||
|
|
||||||
function buildFieldTable(pkt, decoded, pathHops, ranges) {
|
function buildFieldTable(pkt, decoded, pathHops, ranges, ctrlPubKeyNode) {
|
||||||
const buf = pkt.raw_hex || '';
|
const buf = pkt.raw_hex || '';
|
||||||
const size = Math.floor(buf.length / 2);
|
const size = Math.floor(buf.length / 2);
|
||||||
let rows = '';
|
let rows = '';
|
||||||
@@ -3607,6 +3651,41 @@
|
|||||||
rows += fieldRow(off + 1, 'Src Hash (1B)', decoded.srcHash || '', '');
|
rows += fieldRow(off + 1, 'Src Hash (1B)', decoded.srcHash || '', '');
|
||||||
rows += fieldRow(off + 2, 'MAC (2B)', decoded.mac || '', '');
|
rows += fieldRow(off + 2, 'MAC (2B)', decoded.mac || '', '');
|
||||||
rows += fieldRow(off + 4, 'Encrypted Data', truncate(decoded.encryptedData || '', 30), '');
|
rows += fieldRow(off + 4, 'Encrypted Data', truncate(decoded.encryptedData || '', 30), '');
|
||||||
|
} else if (decoded.type === 'CONTROL') {
|
||||||
|
// #1868 — CONTROL DISCOVER_REQ/RESP field breakdown, matching decoder
|
||||||
|
// layout in cmd/ingestor/decoder.go decodeControl(). Body fields are
|
||||||
|
// length-gated there too, so each row is only added when present.
|
||||||
|
const subtype = decoded.ctrlSubtype || 'CONTROL';
|
||||||
|
rows += fieldRow(off, 'Subtype', escapeHtml(subtype), decoded.ctrlFlags ? 'byte0 high nibble, flags=0x' + escapeHtml(decoded.ctrlFlags) : '');
|
||||||
|
if (subtype === 'DISCOVER_REQ') {
|
||||||
|
if (decoded.ctrlFilter != null) {
|
||||||
|
const labels = ctrlFilterLabels(Number(decoded.ctrlFilter));
|
||||||
|
rows += fieldRow(off + 1, 'Filter (1B)', '0x' + Number(decoded.ctrlFilter).toString(16).padStart(2, '0'), labels.length ? 'Requesting: ' + labels.join(', ') : 'No types requested');
|
||||||
|
}
|
||||||
|
if (decoded.ctrlTag != null) {
|
||||||
|
rows += fieldRow(off + 2, 'Tag (4B)', '0x' + (Number(decoded.ctrlTag) >>> 0).toString(16).toUpperCase().padStart(8, '0'), '');
|
||||||
|
}
|
||||||
|
if (decoded.ctrlSince != null) {
|
||||||
|
rows += fieldRow(off + 6, 'Since (4B)', String(Number(decoded.ctrlSince) >>> 0), 'Unix epoch');
|
||||||
|
}
|
||||||
|
} else if (subtype === 'DISCOVER_RESP') {
|
||||||
|
if (decoded.ctrlNodeType != null) {
|
||||||
|
rows += fieldRow(off, 'Node Type', escapeHtml(ctrlTypeLabel(Number(decoded.ctrlNodeType))), 'byte0 low nibble');
|
||||||
|
}
|
||||||
|
if (decoded.ctrlSNR != null) {
|
||||||
|
rows += fieldRow(off + 1, 'SNR (1B)', ctrlSnrDb(decoded.ctrlSNR) + ' dB', 'wire value ' + decoded.ctrlSNR + ' ÷ 4.0');
|
||||||
|
}
|
||||||
|
if (decoded.ctrlTag != null) {
|
||||||
|
rows += fieldRow(off + 2, 'Tag (4B)', '0x' + (Number(decoded.ctrlTag) >>> 0).toString(16).toUpperCase().padStart(8, '0'), '');
|
||||||
|
}
|
||||||
|
if (decoded.ctrlPubKey) {
|
||||||
|
const pkLen = decoded.ctrlPubKey.length === 64 ? '32B' : '8B prefix';
|
||||||
|
const pkValue = ctrlPubKeyNode
|
||||||
|
? `<a href="#/nodes/${encodeURIComponent(ctrlPubKeyNode.public_key)}" class="hop-link hop-named" data-hop-link="true">${escapeHtml(ctrlPubKeyNode.name || ctrlPubKeyNode.public_key.slice(0, 8) + '…')}</a>`
|
||||||
|
: escapeHtml(truncate(decoded.ctrlPubKey, 24));
|
||||||
|
rows += fieldRow(off + 6, 'Pubkey (' + pkLen + ')', pkValue, ctrlPubKeyNode ? '' : 'Unknown node');
|
||||||
|
}
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
rows += fieldRow(off, 'Raw', truncate(buf.slice(off * 2), 40), '');
|
rows += fieldRow(off, 'Raw', truncate(buf.slice(off * 2), 40), '');
|
||||||
}
|
}
|
||||||
|
|||||||
+72
-1
@@ -517,6 +517,19 @@ console.log('\n=== packets.js: getDetailPreview ===');
|
|||||||
assert(result.includes('tag'), 'should render tag field');
|
assert(result.includes('tag'), 'should render tag field');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// #1868 — filter is a bitmask (ADV_TYPE_* bit-per-type, per firmware's
|
||||||
|
// `filter & (1 << ADV_TYPE_x)`); bit 2 = ADV_TYPE_REPEATER, so filter=4
|
||||||
|
// (1<<2) must render as the human-readable type name, not raw hex.
|
||||||
|
test('getDetailPreview renders CONTROL DISCOVER_REQ filter as type name(s), not raw hex', () => {
|
||||||
|
const result = api.getDetailPreview({
|
||||||
|
type: 'CONTROL',
|
||||||
|
ctrlSubtype: 'DISCOVER_REQ',
|
||||||
|
ctrlFilter: 4, // 1 << 2 = ADV_TYPE_REPEATER
|
||||||
|
});
|
||||||
|
assert(result.includes('Repeater'), 'should show "Repeater" for filter bit 2, got: ' + result);
|
||||||
|
assert(!/filter=0x/.test(result), 'should not fall back to raw hex when bits are known, got: ' + result);
|
||||||
|
});
|
||||||
|
|
||||||
test('getDetailPreview handles CONTROL DISCOVER_RESP', () => {
|
test('getDetailPreview handles CONTROL DISCOVER_RESP', () => {
|
||||||
const result = api.getDetailPreview({
|
const result = api.getDetailPreview({
|
||||||
type: 'CONTROL',
|
type: 'CONTROL',
|
||||||
@@ -528,7 +541,34 @@ console.log('\n=== packets.js: getDetailPreview ===');
|
|||||||
});
|
});
|
||||||
assert(result.includes('DISCOVER_RESP'), 'should label subtype');
|
assert(result.includes('DISCOVER_RESP'), 'should label subtype');
|
||||||
assert(result.includes('snr') || result.includes('SNR'), 'should render snr');
|
assert(result.includes('snr') || result.includes('SNR'), 'should render snr');
|
||||||
assert(result.includes('0001020304050607'), 'should render pubkey hex');
|
// #1868: pubkey truncated to first 8 hex chars for the per-row preview
|
||||||
|
// (no live node lookup per row -- see the async detail-panel resolution
|
||||||
|
// instead), and full raw hex must NOT leak into the row.
|
||||||
|
assert(result.includes('00010203'), 'should render truncated pubkey prefix, got: ' + result);
|
||||||
|
assert(!result.includes('0001020304050607'), 'should NOT render the full raw pubkey in the row preview, got: ' + result);
|
||||||
|
});
|
||||||
|
|
||||||
|
// #1868 — node_type (ADV_TYPE_REPEATER=2) must render as "Repeater", not
|
||||||
|
// the raw number.
|
||||||
|
test('getDetailPreview renders CONTROL DISCOVER_RESP node type as a name, not a raw number', () => {
|
||||||
|
const result = api.getDetailPreview({
|
||||||
|
type: 'CONTROL',
|
||||||
|
ctrlSubtype: 'DISCOVER_RESP',
|
||||||
|
ctrlNodeType: 2,
|
||||||
|
});
|
||||||
|
assert(result.includes('Repeater'), 'should show "Repeater" for node type 2, got: ' + result);
|
||||||
|
assert(!/type=2\b/.test(result), 'should not show the raw type number, got: ' + result);
|
||||||
|
});
|
||||||
|
|
||||||
|
// #1868 — SNR is wire-encoded (value * 4); a raw 16 must display as 4.00 dB.
|
||||||
|
test('getDetailPreview converts CONTROL DISCOVER_RESP SNR from wire units to dB', () => {
|
||||||
|
const result = api.getDetailPreview({
|
||||||
|
type: 'CONTROL',
|
||||||
|
ctrlSubtype: 'DISCOVER_RESP',
|
||||||
|
ctrlSNR: 16,
|
||||||
|
});
|
||||||
|
assert(result.includes('4.00dB') || result.includes('4.00 dB'), 'should show 16/4.0=4.00 dB, got: ' + result);
|
||||||
|
assert(!/snr=16(?!\.)/.test(result), 'should not show the raw wire SNR value, got: ' + result);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('getDetailPreview handles CONTROL UNKNOWN subtype', () => {
|
test('getDetailPreview handles CONTROL UNKNOWN subtype', () => {
|
||||||
@@ -830,6 +870,37 @@ console.log('\n=== packets.js: buildFieldTable ===');
|
|||||||
assert(result.includes('Raw'));
|
assert(result.includes('Raw'));
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// #1868 — CONTROL no longer falls through to the generic "Raw" row; it
|
||||||
|
// gets a proper field breakdown matching decodeControl()'s byte layout.
|
||||||
|
test('buildFieldTable renders CONTROL DISCOVER_REQ with human-readable filter', () => {
|
||||||
|
const pkt = { raw_hex: 'c040', route_type: 1, payload_type: 11 };
|
||||||
|
const decoded = { type: 'CONTROL', ctrlSubtype: 'DISCOVER_REQ', ctrlFilter: 4, ctrlTag: 0xDEADBEEF, ctrlSince: 0x11223344 };
|
||||||
|
const result = api.buildFieldTable(pkt, decoded, [], []);
|
||||||
|
assert(!result.includes('>Raw<'), 'should not fall through to the generic Raw row, got: ' + result);
|
||||||
|
assert(result.includes('DISCOVER_REQ'));
|
||||||
|
assert(result.includes('Repeater'), 'filter=4 (1<<2) should show "Repeater", got: ' + result);
|
||||||
|
assert(result.includes('DEADBEEF'));
|
||||||
|
});
|
||||||
|
|
||||||
|
test('buildFieldTable renders CONTROL DISCOVER_RESP with converted SNR and truncated pubkey when node is unresolved', () => {
|
||||||
|
const pkt = { raw_hex: 'c040', route_type: 1, payload_type: 11 };
|
||||||
|
const decoded = { type: 'CONTROL', ctrlSubtype: 'DISCOVER_RESP', ctrlNodeType: 2, ctrlSNR: 16, ctrlPubKey: '00'.repeat(32) };
|
||||||
|
// 5th arg (ctrlPubKeyNode) omitted -- unresolved case.
|
||||||
|
const result = api.buildFieldTable(pkt, decoded, [], []);
|
||||||
|
assert(result.includes('Repeater'), 'node type 2 should show "Repeater", got: ' + result);
|
||||||
|
assert(result.includes('4.00 dB'), 'SNR 16/4.0 should show 4.00 dB, got: ' + result);
|
||||||
|
assert(!result.includes('#/nodes/'), 'should not render a node link when unresolved, got: ' + result);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('buildFieldTable renders CONTROL DISCOVER_RESP pubkey as a clickable node link when resolved', () => {
|
||||||
|
const pkt = { raw_hex: 'c040', route_type: 1, payload_type: 11 };
|
||||||
|
const decoded = { type: 'CONTROL', ctrlSubtype: 'DISCOVER_RESP', ctrlPubKey: 'ab'.repeat(32) };
|
||||||
|
const ctrlPubKeyNode = { public_key: 'ab'.repeat(32), name: 'KnownRepeater' };
|
||||||
|
const result = api.buildFieldTable(pkt, decoded, [], [], ctrlPubKeyNode);
|
||||||
|
assert(result.includes('#/nodes/' + ctrlPubKeyNode.public_key), 'should link to the resolved node, got: ' + result);
|
||||||
|
assert(result.includes('KnownRepeater'), 'should show the resolved node name, got: ' + result);
|
||||||
|
});
|
||||||
|
|
||||||
test('buildFieldTable hash_size calculation', () => {
|
test('buildFieldTable hash_size calculation', () => {
|
||||||
// Path byte 0xC0 → bits 7-6 = 3 → hash_size = 4, but hash_count = 0
|
// Path byte 0xC0 → bits 7-6 = 3 → hash_size = 4, but hash_count = 0
|
||||||
// Since #653: when hashCount == 0, shows "hash_count=0 (direct advert)" instead of hash_size
|
// Since #653: when hashCount == 0, shows "hash_count=0 (direct advert)" instead of hash_size
|
||||||
|
|||||||
Reference in new issue
Block a user