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",
|
||||
"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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
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>
|
||||
|
||||
<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 -->
|
||||
|
||||
@@ -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',
|
||||
|
||||
Reference in New Issue
Block a user