mirror of
https://github.com/aljazceru/meshcore-web.git
synced 2025-12-17 08:14:19 +01:00
implement rx packet log
This commit is contained in:
42
package-lock.json
generated
42
package-lock.json
generated
@@ -9,7 +9,7 @@
|
|||||||
"version": "0.0.1",
|
"version": "0.0.1",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@liamcottle/meshcore.js": "^1.0.7",
|
"@liamcottle/meshcore.js": "^1.2.0",
|
||||||
"@tailwindcss/forms": "^0.5.10",
|
"@tailwindcss/forms": "^0.5.10",
|
||||||
"@vitejs/plugin-vue": "^5.2.1",
|
"@vitejs/plugin-vue": "^5.2.1",
|
||||||
"autoprefixer": "^10.4.20",
|
"autoprefixer": "^10.4.20",
|
||||||
@@ -25,12 +25,6 @@
|
|||||||
"vue-router": "^4.5.0"
|
"vue-router": "^4.5.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"../meshcore.js": {
|
|
||||||
"name": "@liamcottle/meshcore.js",
|
|
||||||
"version": "1.0.6",
|
|
||||||
"extraneous": true,
|
|
||||||
"license": "MIT"
|
|
||||||
},
|
|
||||||
"node_modules/@alloc/quick-lru": {
|
"node_modules/@alloc/quick-lru": {
|
||||||
"version": "5.2.0",
|
"version": "5.2.0",
|
||||||
"resolved": "https://registry.npmjs.org/@alloc/quick-lru/-/quick-lru-5.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/@alloc/quick-lru/-/quick-lru-5.2.0.tgz",
|
||||||
@@ -1122,9 +1116,12 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@liamcottle/meshcore.js": {
|
"node_modules/@liamcottle/meshcore.js": {
|
||||||
"version": "1.0.7",
|
"version": "1.2.0",
|
||||||
"resolved": "https://registry.npmjs.org/@liamcottle/meshcore.js/-/meshcore.js-1.0.7.tgz",
|
"resolved": "https://registry.npmjs.org/@liamcottle/meshcore.js/-/meshcore.js-1.2.0.tgz",
|
||||||
"integrity": "sha512-BevdkkJFeFsMfnki3upnxqRiJzAFg+DaPsu3C+wBuu/8Lra3vFeKySH5X2MEJSNT0iSNUnqVRlZVtiyDmUTzkw=="
|
"integrity": "sha512-EL1JaeYBhS4esFTNTWrBH0xWvhcPHxffa6kbOxEYXo5Wl9k4UBr3IUrHAHaK+0FGOSitUYSsrUOuzw0GvmA1gw==",
|
||||||
|
"dependencies": {
|
||||||
|
"@noble/curves": "^1.8.1"
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@mongodb-js/saslprep": {
|
"node_modules/@mongodb-js/saslprep": {
|
||||||
"version": "1.2.0",
|
"version": "1.2.0",
|
||||||
@@ -1134,6 +1131,31 @@
|
|||||||
"sparse-bitfield": "^3.0.3"
|
"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": {
|
"node_modules/@nodelib/fs.scandir": {
|
||||||
"version": "2.1.5",
|
"version": "2.1.5",
|
||||||
"resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz",
|
"resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz",
|
||||||
|
|||||||
@@ -11,7 +11,7 @@
|
|||||||
"author": "Liam Cottle <liam@liamcottle.com>",
|
"author": "Liam Cottle <liam@liamcottle.com>",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@liamcottle/meshcore.js": "^1.0.7",
|
"@liamcottle/meshcore.js": "^1.2.0",
|
||||||
"@tailwindcss/forms": "^0.5.10",
|
"@tailwindcss/forms": "^0.5.10",
|
||||||
"@vitejs/plugin-vue": "^5.2.1",
|
"@vitejs/plugin-vue": "^5.2.1",
|
||||||
"autoprefixer": "^10.4.20",
|
"autoprefixer": "^10.4.20",
|
||||||
|
|||||||
216
src/components/pages/RxLogPage.vue
Normal file
216
src/components/pages/RxLogPage.vue
Normal 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>
|
||||||
@@ -116,6 +116,29 @@
|
|||||||
|
|
||||||
<div class="bg-white p-2 font-semibold">Commands</div>
|
<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">
|
<div @click="reboot" class="flex cursor-pointer px-2 py-3 bg-white hover:bg-gray-50">
|
||||||
|
|
||||||
<!-- leading -->
|
<!-- leading -->
|
||||||
|
|||||||
@@ -41,6 +41,12 @@ const routes = [
|
|||||||
component: () => import("./components/pages/ChannelMessagesPage.vue"),
|
component: () => import("./components/pages/ChannelMessagesPage.vue"),
|
||||||
beforeEnter: handleRouteThatRequiresDatabase,
|
beforeEnter: handleRouteThatRequiresDatabase,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: "rxlog",
|
||||||
|
path: '/rxlog',
|
||||||
|
component: () => import("./components/pages/RxLogPage.vue"),
|
||||||
|
beforeEnter: handleRouteThatRequiresDatabase,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
name: "settings",
|
name: "settings",
|
||||||
path: '/settings',
|
path: '/settings',
|
||||||
|
|||||||
Reference in New Issue
Block a user