espilon-source/tools/C3PO/templates/canbus.html
Eun0us 79c2a4d4bf c3po: full server rewrite with modular routes and honeypot dashboard
Replace monolithic CLI and web server with route-based Flask API.
New routes: api_commands, api_build, api_can, api_monitor, api_ota,
api_tunnel. Add honeypot security dashboard with real-time SSE,
MITRE ATT&CK mapping, kill chain analysis.

New TUI with commander/help modules. Add session management,
tunnel proxy core, CAN bus data store. Docker support.
2026-02-28 20:12:27 +01:00

226 lines
12 KiB
HTML

{% extends "base.html" %}
{% block title %}CAN Bus - ESPILON{% endblock %}
{% block content %}
<div class="page" x-data="canbusApp()" x-init="init()">
<div class="split-v" style="flex:1;">
<!-- Top: Frame table -->
<div class="panel flex-1">
<div class="toolbar">
<span class="toolbar-label">Frames</span>
<select class="select" x-model="selectedDevice" @change="refreshFrames()">
<option value="">all devices</option>
<template x-for="d in $store.app.connectedDevices()" :key="d.id">
<option :value="d.id" x-text="d.id"></option>
</template>
</select>
<input type="text" class="input" x-model="filterCanId" placeholder="CAN ID (hex)" style="width:120px;" @input="refreshFrames()">
<div class="toolbar-sep"></div>
<label class="checkbox-label"><input type="checkbox" x-model="autoRefresh"> Auto</label>
<button class="btn btn-sm" @click="refreshFrames()">Refresh</button>
<button class="btn btn-sm" @click="exportCsv()">Export CSV</button>
<div class="toolbar-sep"></div>
<span class="text-xs text-muted" x-text="rows.length + ' frames'"></span>
</div>
<div class="panel-body">
<table class="dt">
<thead>
<tr>
<th @click="toggleSort('received_at')" :class="sortClass('received_at')">Time</th>
<th @click="toggleSort('device_id')" :class="sortClass('device_id')">Device</th>
<th @click="toggleSort('can_id')" :class="sortClass('can_id')">CAN ID</th>
<th @click="toggleSort('dlc')" :class="sortClass('dlc')" class="col-center">DLC</th>
<th>Data</th>
</tr>
</thead>
<tbody>
<template x-for="f in pagedRows" :key="f.received_at + f.can_id + Math.random()">
<tr>
<td x-text="formatTimestamp(f.received_at)"></td>
<td x-text="f.device_id"></td>
<td x-text="'0x' + f.can_id"></td>
<td class="col-center" x-text="f.dlc"></td>
<td class="col-wrap" x-text="f.data_hex"></td>
</tr>
</template>
</tbody>
</table>
<template x-if="rows.length === 0 && !loading">
<div class="dt-empty">No CAN frames captured</div>
</template>
</div>
<div class="panel-footer" x-show="totalPages > 1">
Page <span x-text="page + 1"></span>/<span x-text="totalPages"></span>
<button class="btn btn-sm" @click="page = Math.max(0, page-1)" :disabled="page===0">&laquo;</button>
<button class="btn btn-sm" @click="page = Math.min(totalPages-1, page+1)" :disabled="page>=totalPages-1">&raquo;</button>
</div>
</div>
<div class="resizer resizer-h"></div>
<!-- Bottom: Controls -->
<div class="panel" style="flex:0 0 auto;max-height:40%;">
<div class="subtabs">
<div class="subtab" :class="bottomTab==='controls'?'active':''" @click="bottomTab='controls'">Controls</div>
<div class="subtab" :class="bottomTab==='uds'?'active':''" @click="bottomTab='uds'">UDS / OBD</div>
<div class="subtab" :class="bottomTab==='stats'?'active':''" @click="bottomTab='stats'">Stats</div>
</div>
<div class="panel-body panel-body-pad" style="overflow-y:auto;">
<!-- Controls Tab -->
<div x-show="bottomTab==='controls'">
<div class="form-row">
<span class="form-label">Device</span>
<select class="select" x-model="cmdDevice">
<template x-for="d in $store.app.connectedDevices()" :key="d.id">
<option :value="d.id" x-text="d.id"></option>
</template>
</select>
</div>
<div class="flex gap-2" style="flex-wrap:wrap;margin-top:8px;">
<button class="btn btn-sm btn-success" @click="cmd('can_start')">Start</button>
<button class="btn btn-sm btn-danger" @click="cmd('can_stop')">Stop</button>
<button class="btn btn-sm" @click="cmd('can_status')">Status</button>
<button class="btn btn-sm" @click="cmd('can_sniff', [sniffDuration])">Sniff</button>
<input type="number" class="input" x-model="sniffDuration" style="width:60px;" placeholder="10s">
<button class="btn btn-sm" @click="cmd('can_record', [recordDuration])">Record</button>
<input type="number" class="input" x-model="recordDuration" style="width:60px;" placeholder="10s">
<button class="btn btn-sm" @click="cmd('can_dump')">Dump</button>
<button class="btn btn-sm" @click="cmd('can_replay', [replaySpeed])">Replay</button>
<input type="number" class="input" x-model="replaySpeed" style="width:60px;" placeholder="100%">
</div>
<div class="form-row" style="margin-top:8px;">
<span class="form-label">Send</span>
<input type="text" class="input" x-model="sendId" placeholder="ID (hex)" style="width:90px;">
<input type="text" class="input flex-1" x-model="sendData" placeholder="Data (hex)">
<button class="btn btn-sm btn-primary" @click="cmd('can_send', [sendId, sendData])">TX</button>
</div>
<div class="form-row" style="margin-top:4px;">
<span class="form-label">Filter</span>
<input type="text" class="input" x-model="filterId" placeholder="CAN ID (hex)" style="width:90px;">
<button class="btn btn-sm" @click="cmd('can_filter_add', [filterId])">Add</button>
<button class="btn btn-sm" @click="cmd('can_filter_del', [filterId])">Del</button>
<button class="btn btn-sm" @click="cmd('can_filter_list')">List</button>
<button class="btn btn-sm btn-danger" @click="cmd('can_filter_clear')">Clear</button>
</div>
<!-- Command output -->
<template x-if="cmdOutput.length > 0">
<pre class="build-log" style="margin-top:8px;max-height:120px;" x-text="cmdOutput.join('\n')"></pre>
</template>
</div>
<!-- UDS/OBD Tab -->
<div x-show="bottomTab==='uds'">
<div class="flex gap-2" style="flex-wrap:wrap;">
<button class="btn btn-sm" @click="cmd('can_scan_ecu')">Scan ECUs</button>
<button class="btn btn-sm" @click="cmd('can_obd_vin')">Read VIN</button>
<button class="btn btn-sm" @click="cmd('can_obd_dtc')">Read DTCs</button>
<button class="btn btn-sm" @click="cmd('can_obd_supported')">Supported PIDs</button>
</div>
<div class="form-row" style="margin-top:8px;">
<span class="form-label">UDS Raw</span>
<input type="text" class="input" x-model="udsTxId" placeholder="TX ID" style="width:80px;">
<input type="text" class="input" x-model="udsSvc" placeholder="SVC (hex)" style="width:80px;">
<input type="text" class="input flex-1" x-model="udsData" placeholder="Data (hex)">
<button class="btn btn-sm btn-primary" @click="cmd('can_uds', [udsTxId, udsSvc, udsData].filter(Boolean))">Send</button>
</div>
<div class="form-row" style="margin-top:4px;">
<span class="form-label">OBD PID</span>
<input type="text" class="input" x-model="obdPid" placeholder="PID (hex)" style="width:80px;">
<button class="btn btn-sm" @click="cmd('can_obd', [obdPid])">Query</button>
<button class="btn btn-sm" @click="cmd('can_obd_monitor', [obdPid, obdInterval])">Monitor</button>
<input type="number" class="input" x-model="obdInterval" style="width:70px;" placeholder="1000ms">
<button class="btn btn-sm btn-danger" @click="cmd('can_obd_monitor_stop')">Stop</button>
</div>
<template x-if="cmdOutput.length > 0">
<pre class="build-log" style="margin-top:8px;max-height:120px;" x-text="cmdOutput.join('\n')"></pre>
</template>
</div>
<!-- Stats Tab -->
<div x-show="bottomTab==='stats'">
<template x-if="Object.keys(stats).length > 0">
<div class="kv">
<template x-for="(val, key) in stats" :key="key">
<div class="kv-row"><span class="kv-key" x-text="key"></span><span class="kv-val" x-text="val"></span></div>
</template>
</div>
</template>
<template x-if="Object.keys(stats).length === 0">
<div class="text-muted text-xs">No stats yet</div>
</template>
</div>
</div>
</div>
</div>
</div>
{% endblock %}
{% block scripts %}
<script>
function canbusApp() {
return {
...dataTable({ defaultSort: 'received_at', defaultDir: 'desc', pageSize: 200, extract: d => d.frames || [] }),
...commander(),
selectedDevice: '',
filterCanId: '',
autoRefresh: true,
stats: {},
bottomTab: 'controls',
cmdDevice: '',
cmdOutput: [],
sniffDuration: '10', recordDuration: '10', replaySpeed: '100',
sendId: '', sendData: '', filterId: '',
udsTxId: '', udsSvc: '', udsData: '', obdPid: '', obdInterval: '1000',
init() {
this.refreshFrames();
this.refreshStats();
setInterval(() => { if (this.autoRefresh) this.refreshFrames(); }, 1000);
setInterval(() => this.refreshStats(), 5000);
},
async refreshFrames() {
let url = '/api/can/frames?limit=500';
if (this.selectedDevice) url += '&device_id=' + this.selectedDevice;
if (this.filterCanId) url += '&can_id=' + this.filterCanId;
await this.refresh(url);
},
async refreshStats() {
try { const r = await fetch('/api/can/stats'); this.stats = await r.json(); } catch(e) {}
},
exportCsv() { window.open('/api/can/frames/export', '_blank'); },
async cmd(command, argv) {
const dev = this.cmdDevice || (this.$store.app.connectedDevices()[0] || {}).id;
if (!dev) { toast('No device selected', 'error'); return; }
argv = (argv || []).map(String).filter(Boolean);
this.cmdOutput = ['Sending ' + command + '...'];
try {
const data = await this.sendCommand([dev], command, argv);
const r = (data.results || [])[0];
if (r && r.status === 'ok' && r.request_id) {
// Poll for result
let attempts = 0;
const iv = setInterval(async () => {
attempts++;
const res = await fetch('/api/commands/' + encodeURIComponent(r.request_id));
const d = await res.json();
if (d.output) this.cmdOutput = d.output;
if (d.status === 'completed' || d.status === 'error' || attempts >= 30) clearInterval(iv);
}, 500);
} else if (r) {
this.cmdOutput = [r.message || 'Error'];
}
} catch (e) {
this.cmdOutput = ['Error: ' + e.message];
}
}
};
}
</script>
{% endblock %}