implement rx packet log

This commit is contained in:
liamcottle
2025-03-10 14:17:19 +13:00
parent 84f9595513
commit 7104167b6c
5 changed files with 278 additions and 11 deletions

42
package-lock.json generated
View File

@@ -9,7 +9,7 @@
"version": "0.0.1",
"license": "MIT",
"dependencies": {
"@liamcottle/meshcore.js": "^1.0.7",
"@liamcottle/meshcore.js": "^1.2.0",
"@tailwindcss/forms": "^0.5.10",
"@vitejs/plugin-vue": "^5.2.1",
"autoprefixer": "^10.4.20",
@@ -25,12 +25,6 @@
"vue-router": "^4.5.0"
}
},
"../meshcore.js": {
"name": "@liamcottle/meshcore.js",
"version": "1.0.6",
"extraneous": true,
"license": "MIT"
},
"node_modules/@alloc/quick-lru": {
"version": "5.2.0",
"resolved": "https://registry.npmjs.org/@alloc/quick-lru/-/quick-lru-5.2.0.tgz",
@@ -1122,9 +1116,12 @@
}
},
"node_modules/@liamcottle/meshcore.js": {
"version": "1.0.7",
"resolved": "https://registry.npmjs.org/@liamcottle/meshcore.js/-/meshcore.js-1.0.7.tgz",
"integrity": "sha512-BevdkkJFeFsMfnki3upnxqRiJzAFg+DaPsu3C+wBuu/8Lra3vFeKySH5X2MEJSNT0iSNUnqVRlZVtiyDmUTzkw=="
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/@liamcottle/meshcore.js/-/meshcore.js-1.2.0.tgz",
"integrity": "sha512-EL1JaeYBhS4esFTNTWrBH0xWvhcPHxffa6kbOxEYXo5Wl9k4UBr3IUrHAHaK+0FGOSitUYSsrUOuzw0GvmA1gw==",
"dependencies": {
"@noble/curves": "^1.8.1"
}
},
"node_modules/@mongodb-js/saslprep": {
"version": "1.2.0",
@@ -1134,6 +1131,31 @@
"sparse-bitfield": "^3.0.3"
}
},
"node_modules/@noble/curves": {
"version": "1.8.1",
"resolved": "https://registry.npmjs.org/@noble/curves/-/curves-1.8.1.tgz",
"integrity": "sha512-warwspo+UYUPep0Q+vtdVB4Ugn8GGQj8iyB3gnRWsztmUHTI3S1nhdiWNsPUGL0vud7JlRRk1XEu7Lq1KGTnMQ==",
"dependencies": {
"@noble/hashes": "1.7.1"
},
"engines": {
"node": "^14.21.3 || >=16"
},
"funding": {
"url": "https://paulmillr.com/funding/"
}
},
"node_modules/@noble/hashes": {
"version": "1.7.1",
"resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.7.1.tgz",
"integrity": "sha512-B8XBPsn4vT/KJAGqDzbwztd+6Yte3P4V7iafm24bxgDe/mlRuK6xmWPuCNrKt2vDafZ8MfJLlchDG/vYafQEjQ==",
"engines": {
"node": "^14.21.3 || >=16"
},
"funding": {
"url": "https://paulmillr.com/funding/"
}
},
"node_modules/@nodelib/fs.scandir": {
"version": "2.1.5",
"resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz",

View File

@@ -11,7 +11,7 @@
"author": "Liam Cottle <liam@liamcottle.com>",
"license": "MIT",
"dependencies": {
"@liamcottle/meshcore.js": "^1.0.7",
"@liamcottle/meshcore.js": "^1.2.0",
"@tailwindcss/forms": "^0.5.10",
"@vitejs/plugin-vue": "^5.2.1",
"autoprefixer": "^10.4.20",

View File

@@ -0,0 +1,216 @@
<template>
<Page>
<!-- app bar -->
<AppBar title="RX Log">
<template v-slot:trailing>
<IconButton @click="clearLog" class="bg-transparent text-gray-500">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="size-6">
<path stroke-linecap="round" stroke-linejoin="round" d="m14.74 9-.346 9m-4.788 0L9.26 9m9.968-3.21c.342.052.682.107 1.022.166m-1.022-.165L18.16 19.673a2.25 2.25 0 0 1-2.244 2.077H8.084a2.25 2.25 0 0 1-2.244-2.077L4.772 5.79m14.456 0a48.108 48.108 0 0 0-3.478-.397m-12 .562c.34-.059.68-.114 1.022-.165m0 0a48.11 48.11 0 0 1 3.478-.397m7.5 0v-.916c0-1.18-.91-2.164-2.09-2.201a51.964 51.964 0 0 0-3.32 0c-1.18.037-2.09 1.022-2.09 2.201v.916m7.5 0a48.667 48.667 0 0 0-7.5 0" />
</svg>
</IconButton>
</template>
</AppBar>
<!-- search -->
<div v-if="logs.length > 0" class="flex bg-white border-b border-gray-300 divide-x">
<div class="flex p-1 w-full">
<input v-model="search" type="text" :placeholder="`Search ${logs.length} ${logs.length === 1 ? 'Log' : 'Logs'}...`" class="h-full bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5">
</div>
</div>
<!-- list -->
<div class="flex h-full w-full overflow-hidden">
<div class="overflow-y-auto divide-y w-full">
<div v-for="log of searchedLogs" class="bg-white">
<div v-if="log.packet" class="p-1">
<div class="font-semibold">{{ log.packet.getRouteTypeString() }} {{ log.packet.getPayloadTypeString() }}</div>
<div v-if="formatPacketPayload(log.packet)" class="text-sm text-gray-500">
<div v-for="line of formatPacketPayload(log.packet)">{{ line }}</div>
</div>
<div @click="showPath(log.packet.path)" class="text-sm text-gray-500 cursor-pointer">
<span v-if="log.packet.path.length > 0">Path: {{ formatPath(log.packet.path) }}</span>
<span v-else>Path: (direct)</span>
</div>
<div class="text-sm text-gray-500 space-x-1">
<span>Hops: {{ log.packet.path.length }}</span>
<span></span>
<span>SNR: {{ log.snr }}</span>
</div>
<div class="text-sm text-gray-500">Hash: {{ log.packet_hash.substring(0, 8).toUpperCase() }}</div>
</div>
<div v-else>Failed to decode packet</div>
</div>
</div>
</div>
</Page>
</template>
<script>
import Page from "./Page.vue";
import AppBar from "../AppBar.vue";
import GlobalState from "../../js/GlobalState.js";
import {Advert, Constants, Packet, BufferUtils} from "@liamcottle/meshcore.js";
import ChannelDropDownMenu from "../channels/ChannelDropDownMenu.vue";
import IconButton from "../IconButton.vue";
export default {
name: 'RxLogPage',
components: {
IconButton,
ChannelDropDownMenu,
AppBar,
Page,
},
data() {
return {
search: "",
logs: [],
};
},
mounted() {
GlobalState.connection.on(Constants.PushCodes.LogRxData, this.onLogRxData);
},
beforeUnmount() {
GlobalState.connection.off(Constants.PushCodes.LogRxData, this.onLogRxData);
},
methods: {
clearLog() {
this.logs = [];
},
byteToHex(byte) {
return ('0' + (byte & 0xFF).toString(16)).slice(-2);
},
formatPath(path) {
return Array.from(path, (byte) => {
return this.byteToHex(byte);
}).join(',');
},
showPath(path) {
const pathList = [`${path.length} Hops`];
const pathArray = Array.from(path);
for(var i = 0; i < pathArray.length; i++){
const pathByte = pathArray[i];
const contact = this.findContactByPublicKeyPrefix([pathByte]);
const contactName = contact?.advName ?? "?";
pathList.push(`[${i + 1}]: ${contactName}`);
}
alert(pathList.join("\n"));
},
formatPacketPayload(packet) {
const payloadType = packet.payload_type_string;
if(payloadType === "PATH"
|| payloadType === "REQ"
|| payloadType === "RESPONSE"
|| payloadType === "TXT_MSG"){
const parsed = packet.parsePayload();
const source = this.byteToHex(parsed.src);
const destination = this.byteToHex(parsed.dest);
const sourceContactName = this.getContactNameByPublicKeyPrefix([parsed.src]);
const destinationContactName = this.getContactNameByPublicKeyPrefix([parsed.dest]);
return [
`[${source} -> ${destination}]`,
`Source: ${sourceContactName}`,
`Dest: ${destinationContactName}`,
];
} else if(payloadType === "ADVERT"){
try {
const advert = Advert.fromBytes(packet.payload);
const publicKeyHex = BufferUtils.bytesToHex(advert.publicKey);
const parsedAppData = advert.parseAppData();
return [
`[${parsedAppData.type}] ${parsedAppData.name}`,
`Public Key: <${publicKeyHex.slice(0, 8)}...${publicKeyHex.slice(publicKeyHex.length - 8)}>`,
];
} catch(e) {
return "Failed to parse Advert";
}
} else if(payloadType === "ANON_REQ"){
const parsed = packet.parsePayload();
const source = this.byteToHex(parsed.src.subarray(0, 1));
const destination = this.byteToHex(parsed.dest);
const sourceContactName = this.getContactNameByPublicKeyPrefix(parsed.src);
const destinationContactName = this.getContactNameByPublicKeyPrefix([parsed.dest]);
return [
`[${source} -> ${destination}]`,
`Source: ${sourceContactName}`,
`Dest: ${destinationContactName}`,
];
}
return null;
},
getContactNameByPublicKeyPrefix(publicKeyPrefix) {
// check if self
const selfPublicKeyPrefix = GlobalState.selfInfo?.publicKey.slice(0, publicKeyPrefix.length);
if(BufferUtils.areBuffersEqual(publicKeyPrefix, selfPublicKeyPrefix)){
return GlobalState?.selfInfo?.name ?? "(this device)";
}
// check if we found a matching contact
const contact = this.findContactByPublicKeyPrefix(publicKeyPrefix);
if(contact != null){
return contact.advName;
}
// nothing found
return "Unknown Contact";
},
async getPacketHash(packet) {
return await this.sha256(new Uint8Array([
packet.payload_type,
...packet.payload,
]));
},
async onLogRxData(data) {
try {
const packet = Packet.fromBytes(data.raw);
const packetHash = await this.getPacketHash(packet);
this.logs.push({
snr: data.lastSnr,
rssi: data.lastRssi,
packet: packet,
packet_hash: packetHash.substring(0, 8).toUpperCase(),
});
} catch(e) {
console.log(e);
}
},
async sha256(data) {
const msgBuffer = new TextEncoder().encode(data);
const hashBuffer = await crypto.subtle.digest('SHA-256', msgBuffer);
const hashArray = Array.from(new Uint8Array(hashBuffer));
return hashArray.map(b => b.toString(16).padStart(2, '0')).join('');
},
findContactByPublicKeyPrefix(publicKeyPrefix) {
// find first contact that matches this public key prefix
return GlobalState.contacts.find((contact) => {
const contactPublicKeyPrefix = contact.publicKey.slice(0, publicKeyPrefix.length);
return BufferUtils.areBuffersEqual(publicKeyPrefix, contactPublicKeyPrefix);
});
},
},
computed: {
searchedLogs() {
return this.logs.filter((log) => {
const search = this.search.toLowerCase();
const matchesPacketHash = log.packet_hash.toLowerCase().includes(search);
const matchesPayloadType = log.packet.getPayloadTypeString().toLowerCase().includes(search);
const matchesRouteType = log.packet.getRouteTypeString().toLowerCase().includes(search);
return matchesPacketHash || matchesPayloadType || matchesRouteType;
});
},
},
}
</script>

View File

@@ -116,6 +116,29 @@
<div class="bg-white p-2 font-semibold">Commands</div>
<RouterLink :to="{ name: 'rxlog' }">
<div class="flex cursor-pointer px-2 py-3 bg-white hover:bg-gray-50">
<!-- leading -->
<div class="my-auto ml-2 mr-4 text-gray-500">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="size-6">
<path stroke-linecap="round" stroke-linejoin="round" d="M8.25 6.75h12M8.25 12h12m-12 5.25h12M3.75 6.75h.007v.008H3.75V6.75Zm.375 0a.375.375 0 1 1-.75 0 .375.375 0 0 1 .75 0ZM3.75 12h.007v.008H3.75V12Zm.375 0a.375.375 0 1 1-.75 0 .375.375 0 0 1 .75 0Zm-.375 5.25h.007v.008H3.75v-.008Zm.375 0a.375.375 0 1 1-.75 0 .375.375 0 0 1 .75 0Z" />
</svg>
</div>
<!-- title -->
<div class="my-auto mr-auto">RX Log</div>
<!-- trailing -->
<div class="my-auto mr-2 text-gray-500">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="size-5">
<path stroke-linecap="round" stroke-linejoin="round" d="m8.25 4.5 7.5 7.5-7.5 7.5" />
</svg>
</div>
</div>
</RouterLink>
<div @click="reboot" class="flex cursor-pointer px-2 py-3 bg-white hover:bg-gray-50">
<!-- leading -->

View File

@@ -41,6 +41,12 @@ const routes = [
component: () => import("./components/pages/ChannelMessagesPage.vue"),
beforeEnter: handleRouteThatRequiresDatabase,
},
{
name: "rxlog",
path: '/rxlog',
component: () => import("./components/pages/RxLogPage.vue"),
beforeEnter: handleRouteThatRequiresDatabase,
},
{
name: "settings",
path: '/settings',