mirror of
https://github.com/smittix/intercept.git
synced 2026-04-24 06:40:00 -07:00
feat(ook): add timing presets, RSSI, bit-order suggest, pattern filter, TSCM link
- Timing presets: five quick-fill buttons (300/600, 300/900, 400/800, 500/1500, 500 MC) that populate all six pulse-timing fields at once — maps to CTF flag timing profiles - RSSI per frame: add -M level to rtl_433 command; parse snr/rssi/level from JSON; display dB SNR inline with each frame; include rssi_db column in CSV export - Auto bit-order suggest: "Suggest" button counts printable chars across all stored frames for MSB vs LSB, selects the winner, shows count — no decoder restart needed - Pattern filter: live hex/ASCII filter input above the frame log; hides non-matching frames and highlights matches in green; respects current bit order - TSCM integration: "Decode (OOK)" button in RF signal device details panel switches to OOK mode and pre-fills frequency — frontend-only, no backend changes needed Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -152,7 +152,7 @@ def start_ook() -> Response:
|
|||||||
continue
|
continue
|
||||||
filtered_cmd.append(arg)
|
filtered_cmd.append(arg)
|
||||||
|
|
||||||
filtered_cmd.extend(['-R', '0', '-X', flex_spec])
|
filtered_cmd.extend(['-M', 'level', '-R', '0', '-X', flex_spec])
|
||||||
|
|
||||||
full_cmd = ' '.join(filtered_cmd)
|
full_cmd = ' '.join(filtered_cmd)
|
||||||
logger.info(f'OOK decoder running: {full_cmd}')
|
logger.info(f'OOK decoder running: {full_cmd}')
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ var OokMode = (function () {
|
|||||||
frames: [], // raw frame objects from SSE
|
frames: [], // raw frame objects from SSE
|
||||||
frameCount: 0,
|
frameCount: 0,
|
||||||
bitOrder: 'msb', // 'msb' | 'lsb'
|
bitOrder: 'msb', // 'msb' | 'lsb'
|
||||||
|
filterQuery: '', // active hex/ascii filter
|
||||||
};
|
};
|
||||||
|
|
||||||
// ---- Initialization ----
|
// ---- Initialization ----
|
||||||
@@ -209,7 +210,7 @@ var OokMode = (function () {
|
|||||||
|
|
||||||
var div = document.createElement('div');
|
var div = document.createElement('div');
|
||||||
div.className = 'ook-frame';
|
div.className = 'ook-frame';
|
||||||
div.dataset.bits = msg.bits;
|
div.dataset.bits = msg.bits || '';
|
||||||
div.dataset.bitCount = msg.bit_count;
|
div.dataset.bitCount = msg.bit_count;
|
||||||
div.dataset.inverted = msg.inverted ? '1' : '0';
|
div.dataset.inverted = msg.inverted ? '1' : '0';
|
||||||
|
|
||||||
@@ -217,10 +218,14 @@ var OokMode = (function () {
|
|||||||
var suffix = '';
|
var suffix = '';
|
||||||
if (msg.inverted) suffix += ' <span style="opacity:.5">(inv)</span>';
|
if (msg.inverted) suffix += ' <span style="opacity:.5">(inv)</span>';
|
||||||
|
|
||||||
|
var rssiStr = (msg.rssi !== undefined && msg.rssi !== null)
|
||||||
|
? ' <span style="color:#666; font-size:10px">' + msg.rssi.toFixed(1) + ' dB SNR</span>'
|
||||||
|
: '';
|
||||||
|
|
||||||
div.innerHTML =
|
div.innerHTML =
|
||||||
'<span style="color:var(--text-dim)">' + msg.timestamp + '</span>' +
|
'<span style="color:var(--text-dim)">' + msg.timestamp + '</span>' +
|
||||||
' <span style="color:#888">[' + msg.bit_count + 'b]</span>' +
|
' <span style="color:#888">[' + msg.bit_count + 'b]</span>' +
|
||||||
suffix +
|
rssiStr + suffix +
|
||||||
'<br>' +
|
'<br>' +
|
||||||
'<span style="padding-left:8em; color:' + color + '; font-family:var(--font-mono); font-size:10px">' +
|
'<span style="padding-left:8em; color:' + color + '; font-family:var(--font-mono); font-size:10px">' +
|
||||||
'hex: ' + interp.hex +
|
'hex: ' + interp.hex +
|
||||||
@@ -232,6 +237,16 @@ var OokMode = (function () {
|
|||||||
|
|
||||||
div.style.cssText = 'font-size:11px; padding: 4px 0; border-bottom: 1px solid #1a1a1a; line-height:1.6;';
|
div.style.cssText = 'font-size:11px; padding: 4px 0; border-bottom: 1px solid #1a1a1a; line-height:1.6;';
|
||||||
|
|
||||||
|
// Apply current filter
|
||||||
|
if (state.filterQuery) {
|
||||||
|
var q = state.filterQuery;
|
||||||
|
if (!interp.hex.includes(q) && !interp.ascii.toLowerCase().includes(q)) {
|
||||||
|
div.style.display = 'none';
|
||||||
|
} else {
|
||||||
|
div.style.background = 'rgba(0,255,136,0.05)';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
panel.appendChild(div);
|
panel.appendChild(div);
|
||||||
panel.scrollTop = panel.scrollHeight;
|
panel.scrollTop = panel.scrollHeight;
|
||||||
}
|
}
|
||||||
@@ -272,12 +287,13 @@ var OokMode = (function () {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function exportLog() {
|
function exportLog() {
|
||||||
var lines = ['timestamp,bit_count,hex_msb,ascii_msb,inverted'];
|
var lines = ['timestamp,bit_count,rssi_db,hex_msb,ascii_msb,inverted'];
|
||||||
state.frames.forEach(function (msg) {
|
state.frames.forEach(function (msg) {
|
||||||
var interp = interpretBits(msg.bits, 'msb');
|
var interp = interpretBits(msg.bits, 'msb');
|
||||||
lines.push([
|
lines.push([
|
||||||
msg.timestamp,
|
msg.timestamp,
|
||||||
msg.bit_count,
|
msg.bit_count,
|
||||||
|
msg.rssi !== undefined && msg.rssi !== null ? msg.rssi : '',
|
||||||
interp.hex,
|
interp.hex,
|
||||||
'"' + interp.ascii.replace(/"/g, '""') + '"',
|
'"' + interp.ascii.replace(/"/g, '""') + '"',
|
||||||
msg.inverted,
|
msg.inverted,
|
||||||
@@ -325,6 +341,80 @@ var OokMode = (function () {
|
|||||||
if (el) el.value = mhz;
|
if (el) el.value = mhz;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Apply a timing preset — fills all six pulse timing fields at once.
|
||||||
|
* @param {number} s Short pulse (µs)
|
||||||
|
* @param {number} l Long pulse (µs)
|
||||||
|
* @param {number} r Reset/gap limit (µs)
|
||||||
|
* @param {number} g Gap limit (µs)
|
||||||
|
* @param {number} t Tolerance (µs)
|
||||||
|
* @param {number} b Min bits
|
||||||
|
*/
|
||||||
|
function setTiming(s, l, r, g, t, b) {
|
||||||
|
var fields = {
|
||||||
|
ookShortPulse: s,
|
||||||
|
ookLongPulse: l,
|
||||||
|
ookResetLimit: r,
|
||||||
|
ookGapLimit: g,
|
||||||
|
ookTolerance: t,
|
||||||
|
ookMinBits: b,
|
||||||
|
};
|
||||||
|
Object.keys(fields).forEach(function (id) {
|
||||||
|
var el = document.getElementById(id);
|
||||||
|
if (el) el.value = fields[id];
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- Auto bit-order suggestion ----
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Count printable chars for MSB and LSB across all stored frames,
|
||||||
|
* then switch to whichever produces more readable output.
|
||||||
|
*/
|
||||||
|
function suggestBitOrder() {
|
||||||
|
if (state.frames.length === 0) return;
|
||||||
|
var msbCount = 0, lsbCount = 0;
|
||||||
|
state.frames.forEach(function (msg) {
|
||||||
|
msbCount += interpretBits(msg.bits, 'msb').printable.length;
|
||||||
|
lsbCount += interpretBits(msg.bits, 'lsb').printable.length;
|
||||||
|
});
|
||||||
|
var best = msbCount >= lsbCount ? 'msb' : 'lsb';
|
||||||
|
setBitOrder(best);
|
||||||
|
var label = document.getElementById('ookSuggestLabel');
|
||||||
|
if (label) {
|
||||||
|
var winner = best === 'msb' ? msbCount : lsbCount;
|
||||||
|
label.textContent = best.toUpperCase() + ' (' + winner + ' printable)';
|
||||||
|
label.style.color = '#00ff88';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- Pattern search / filter ----
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Show only frames whose hex or ASCII interpretation contains the query.
|
||||||
|
* Clears filter when query is empty.
|
||||||
|
* @param {string} query
|
||||||
|
*/
|
||||||
|
function filterFrames(query) {
|
||||||
|
state.filterQuery = query.toLowerCase().trim();
|
||||||
|
var q = state.filterQuery;
|
||||||
|
var panel = document.getElementById('ookOutput');
|
||||||
|
if (!panel) return;
|
||||||
|
var divs = panel.querySelectorAll('.ook-frame');
|
||||||
|
divs.forEach(function (div) {
|
||||||
|
if (!q) {
|
||||||
|
div.style.display = '';
|
||||||
|
div.style.background = '';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
var bits = div.dataset.bits || '';
|
||||||
|
var interp = interpretBits(bits, state.bitOrder);
|
||||||
|
var match = interp.hex.includes(q) || interp.ascii.toLowerCase().includes(q);
|
||||||
|
div.style.display = match ? '' : 'none';
|
||||||
|
div.style.background = match ? 'rgba(0,255,136,0.05)' : '';
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
// ---- UI ----
|
// ---- UI ----
|
||||||
|
|
||||||
function updateUI(running) {
|
function updateUI(running) {
|
||||||
@@ -351,7 +441,10 @@ var OokMode = (function () {
|
|||||||
stop: stop,
|
stop: stop,
|
||||||
setFreq: setFreq,
|
setFreq: setFreq,
|
||||||
setEncoding: setEncoding,
|
setEncoding: setEncoding,
|
||||||
|
setTiming: setTiming,
|
||||||
setBitOrder: setBitOrder,
|
setBitOrder: setBitOrder,
|
||||||
|
suggestBitOrder: suggestBitOrder,
|
||||||
|
filterFrames: filterFrames,
|
||||||
clearOutput: clearOutput,
|
clearOutput: clearOutput,
|
||||||
exportLog: exportLog,
|
exportLog: exportLog,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -3294,6 +3294,7 @@
|
|||||||
<!-- OOK Decoder Output Panel -->
|
<!-- OOK Decoder Output Panel -->
|
||||||
<div id="ookOutputPanel" style="display: none; margin-bottom: 12px;">
|
<div id="ookOutputPanel" style="display: none; margin-bottom: 12px;">
|
||||||
<div style="background: #0a0a0a; border: 1px solid #1a2e1a; border-radius: 6px; padding: 8px 10px;">
|
<div style="background: #0a0a0a; border: 1px solid #1a2e1a; border-radius: 6px; padding: 8px 10px;">
|
||||||
|
<!-- Toolbar row 1: bit order + actions -->
|
||||||
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 6px; font-size: 10px; color: #555; text-transform: uppercase; letter-spacing: 1px;">
|
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 6px; font-size: 10px; color: #555; text-transform: uppercase; letter-spacing: 1px;">
|
||||||
<span>Decoded Frames</span>
|
<span>Decoded Frames</span>
|
||||||
<div style="display: flex; gap: 6px; align-items: center;">
|
<div style="display: flex; gap: 6px; align-items: center;">
|
||||||
@@ -3303,10 +3304,21 @@
|
|||||||
style="background: var(--accent); color: #000;">MSB</button>
|
style="background: var(--accent); color: #000;">MSB</button>
|
||||||
<button class="btn btn-sm btn-ghost" id="ookBitLSB"
|
<button class="btn btn-sm btn-ghost" id="ookBitLSB"
|
||||||
onclick="OokMode.setBitOrder('lsb')">LSB</button>
|
onclick="OokMode.setBitOrder('lsb')">LSB</button>
|
||||||
|
<button class="btn btn-sm btn-ghost" onclick="OokMode.suggestBitOrder()"
|
||||||
|
title="Auto-detect best bit order from printable character count">
|
||||||
|
Suggest <span id="ookSuggestLabel" style="font-size:9px; margin-left:2px;"></span>
|
||||||
|
</button>
|
||||||
<button class="btn btn-sm btn-ghost" onclick="OokMode.clearOutput()">Clear</button>
|
<button class="btn btn-sm btn-ghost" onclick="OokMode.clearOutput()">Clear</button>
|
||||||
<button class="btn btn-sm btn-ghost" onclick="OokMode.exportLog()">CSV</button>
|
<button class="btn btn-sm btn-ghost" onclick="OokMode.exportLog()">CSV</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<!-- Toolbar row 2: pattern filter -->
|
||||||
|
<div style="margin-bottom: 6px;">
|
||||||
|
<input type="text" id="ookPatternFilter"
|
||||||
|
placeholder="Filter hex or ASCII..."
|
||||||
|
oninput="OokMode.filterFrames(this.value)"
|
||||||
|
style="width: 100%; background: #111; border: 1px solid #222; border-radius: 3px; color: var(--text-dim); font-family: var(--font-mono); font-size: 10px; padding: 3px 6px; box-sizing: border-box;">
|
||||||
|
</div>
|
||||||
<div id="ookOutput" style="max-height: 400px; overflow-y: auto; font-family: var(--font-mono); font-size: 10px; color: var(--text-dim);"></div>
|
<div id="ookOutput" style="max-height: 400px; overflow-y: auto; font-family: var(--font-mono); font-size: 10px; color: var(--text-dim);"></div>
|
||||||
</div>
|
</div>
|
||||||
<div style="margin-top: 4px; font-size: 10px; color: #555; text-align: right;">
|
<div style="margin-top: 4px; font-size: 10px; color: #555; text-align: right;">
|
||||||
@@ -13091,7 +13103,7 @@
|
|||||||
<div style="display: flex; gap: 8px; flex-wrap: wrap;">
|
<div style="display: flex; gap: 8px; flex-wrap: wrap;">
|
||||||
`;
|
`;
|
||||||
|
|
||||||
// Add "Listen" button for RF signals
|
// Add "Listen" and "Decode (OOK)" buttons for RF signals
|
||||||
if (protocol === 'rf' && device.frequency) {
|
if (protocol === 'rf' && device.frequency) {
|
||||||
const freq = device.frequency;
|
const freq = device.frequency;
|
||||||
html += `
|
html += `
|
||||||
@@ -13101,6 +13113,10 @@
|
|||||||
<button class="tscm-action-btn" onclick="listenToRfSignal(${freq}, 'am')">
|
<button class="tscm-action-btn" onclick="listenToRfSignal(${freq}, 'am')">
|
||||||
Listen (AM)
|
Listen (AM)
|
||||||
</button>
|
</button>
|
||||||
|
<button class="tscm-action-btn" onclick="decodeWithOok(${freq})"
|
||||||
|
title="Open OOK decoder tuned to this frequency">
|
||||||
|
Decode (OOK)
|
||||||
|
</button>
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -13114,7 +13130,7 @@
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div style="font-size: 10px; color: var(--text-secondary); margin-top: 8px;">
|
<div style="font-size: 10px; color: var(--text-secondary); margin-top: 8px;">
|
||||||
${protocol === 'rf' ? 'Listen buttons open Spectrum Waterfall. ' : ''}Known devices are excluded from threat scoring in future sweeps.
|
${protocol === 'rf' ? 'Listen opens Spectrum Waterfall. Decode (OOK) opens the OOK decoder tuned to this frequency. ' : ''}Known devices are excluded from threat scoring in future sweeps.
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
@@ -13910,6 +13926,17 @@
|
|||||||
}, 300);
|
}, 300);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function decodeWithOok(frequency) {
|
||||||
|
// Close the TSCM modal and switch to OOK decoder with the detected frequency pre-filled
|
||||||
|
closeTscmDeviceModal();
|
||||||
|
switchMode('ook');
|
||||||
|
setTimeout(function () {
|
||||||
|
if (typeof OokMode !== 'undefined' && typeof OokMode.setFreq === 'function') {
|
||||||
|
OokMode.setFreq(parseFloat(frequency).toFixed(3));
|
||||||
|
}
|
||||||
|
}, 300);
|
||||||
|
}
|
||||||
|
|
||||||
async function showDevicesByCategory(category) {
|
async function showDevicesByCategory(category) {
|
||||||
const modal = document.getElementById('tscmDeviceModal');
|
const modal = document.getElementById('tscmDeviceModal');
|
||||||
const content = document.getElementById('tscmDeviceModalContent');
|
const content = document.getElementById('tscmDeviceModalContent');
|
||||||
|
|||||||
@@ -95,6 +95,21 @@
|
|||||||
<input type="number" id="ookMinBits" value="8" step="1" min="1" max="512">
|
<input type="number" id="ookMinBits" value="8" step="1" min="1" max="512">
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="form-group" style="margin-top: 8px;">
|
||||||
|
<label style="font-size: 10px; color: var(--text-dim);">Quick presets (short/long μs)</label>
|
||||||
|
<div style="display: flex; flex-wrap: wrap; gap: 4px; margin-top: 4px;">
|
||||||
|
<button class="preset-btn" onclick="OokMode.setTiming(300,600,8000,5000,150,8)"
|
||||||
|
title="Generic ISM default">300/600</button>
|
||||||
|
<button class="preset-btn" onclick="OokMode.setTiming(300,900,8000,5000,150,16)"
|
||||||
|
title="PWM common variant">300/900</button>
|
||||||
|
<button class="preset-btn" onclick="OokMode.setTiming(400,800,8000,5000,150,16)"
|
||||||
|
title="Generic 2:1 ratio">400/800</button>
|
||||||
|
<button class="preset-btn" onclick="OokMode.setTiming(500,1500,10000,6000,200,16)"
|
||||||
|
title="Long-range keyfob">500/1500</button>
|
||||||
|
<button class="preset-btn" onclick="OokMode.setTiming(500,1000,8000,5000,150,8)"
|
||||||
|
title="Manchester clock period">500 MC</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="section">
|
<div class="section">
|
||||||
|
|||||||
18
utils/ook.py
18
utils/ook.py
@@ -126,6 +126,17 @@ def ook_parser_thread(
|
|||||||
if raw_data:
|
if raw_data:
|
||||||
codes = [str(raw_data)]
|
codes = [str(raw_data)]
|
||||||
|
|
||||||
|
# Extract signal level if rtl_433 was invoked with -M level
|
||||||
|
rssi: float | None = None
|
||||||
|
for _rssi_key in ('snr', 'rssi', 'level', 'noise'):
|
||||||
|
_rssi_val = data.get(_rssi_key)
|
||||||
|
if _rssi_val is not None:
|
||||||
|
try:
|
||||||
|
rssi = round(float(_rssi_val), 1)
|
||||||
|
except (TypeError, ValueError):
|
||||||
|
pass
|
||||||
|
break
|
||||||
|
|
||||||
if not codes:
|
if not codes:
|
||||||
logger.debug(
|
logger.debug(
|
||||||
f'[rtl_433/ook] no code field — keys: {list(data.keys())}'
|
f'[rtl_433/ook] no code field — keys: {list(data.keys())}'
|
||||||
@@ -176,7 +187,7 @@ def ook_parser_thread(
|
|||||||
continue
|
continue
|
||||||
|
|
||||||
try:
|
try:
|
||||||
output_queue.put_nowait({
|
event: dict[str, Any] = {
|
||||||
'type': 'ook_frame',
|
'type': 'ook_frame',
|
||||||
'hex': frame['hex'],
|
'hex': frame['hex'],
|
||||||
'bits': frame['bits'],
|
'bits': frame['bits'],
|
||||||
@@ -185,7 +196,10 @@ def ook_parser_thread(
|
|||||||
'inverted': inverted,
|
'inverted': inverted,
|
||||||
'encoding': encoding,
|
'encoding': encoding,
|
||||||
'timestamp': timestamp,
|
'timestamp': timestamp,
|
||||||
})
|
}
|
||||||
|
if rssi is not None:
|
||||||
|
event['rssi'] = rssi
|
||||||
|
output_queue.put_nowait(event)
|
||||||
except queue.Full:
|
except queue.Full:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user