feat(smart scroll, ANSI clear, ANSI refresh)
This commit is contained in:
parent
15c1143b25
commit
51783612bc
|
@ -58,6 +58,7 @@ class OneTimeWebsocket implements IWebsocket {
|
||||||
/* did not receive packet "heartBeatTimeout" times,
|
/* did not receive packet "heartBeatTimeout" times,
|
||||||
* connection may be lost: close the socket */
|
* connection may be lost: close the socket */
|
||||||
if (this.socket.readyState === this.socket.OPEN) {
|
if (this.socket.readyState === this.socket.OPEN) {
|
||||||
|
console.log("No heart beat, break connection");
|
||||||
this.close();
|
this.close();
|
||||||
this.clear();
|
this.clear();
|
||||||
}
|
}
|
||||||
|
@ -91,7 +92,10 @@ class OneTimeWebsocket implements IWebsocket {
|
||||||
this.msgCallback(msg);
|
this.msgCallback(msg);
|
||||||
}
|
}
|
||||||
|
|
||||||
this.socket.onclose = () => {
|
this.socket.onclose = (ev) => {
|
||||||
|
if (isDevMode()) {
|
||||||
|
console.log("ws closed", ev.reason, ev.code);
|
||||||
|
}
|
||||||
this.socket.onclose = null
|
this.socket.onclose = null
|
||||||
this.socket.onopen = null
|
this.socket.onopen = null
|
||||||
this.socket.onerror = null
|
this.socket.onerror = null
|
||||||
|
|
|
@ -1,5 +1,12 @@
|
||||||
import {defineStore} from "pinia";
|
import {defineStore} from "pinia";
|
||||||
import {computed, type Ref, ref, shallowReactive} from "vue";
|
import {
|
||||||
|
computed,
|
||||||
|
type Ref,
|
||||||
|
type ShallowReactive,
|
||||||
|
ref,
|
||||||
|
shallowReactive,
|
||||||
|
watch
|
||||||
|
} from "vue";
|
||||||
import {AnsiUp} from 'ansi_up'
|
import {AnsiUp} from 'ansi_up'
|
||||||
import {debouncedWatch} from "@vueuse/core";
|
import {debouncedWatch} from "@vueuse/core";
|
||||||
import {type IUartConfig, uart_send_msg} from "@/api/apiUart";
|
import {type IUartConfig, uart_send_msg} from "@/api/apiUart";
|
||||||
|
@ -13,6 +20,7 @@ interface IDataArchive {
|
||||||
export interface IDataBuf {
|
export interface IDataBuf {
|
||||||
time: string;
|
time: string;
|
||||||
isRX: boolean;
|
isRX: boolean;
|
||||||
|
type: number;
|
||||||
data: Uint8Array;
|
data: Uint8Array;
|
||||||
str: string;
|
str: string;
|
||||||
hex: string;
|
hex: string;
|
||||||
|
@ -67,6 +75,9 @@ const zeroPad = (num: number, places: number) => String(num).padStart(places, '0
|
||||||
const ansi_up = new AnsiUp();
|
const ansi_up = new AnsiUp();
|
||||||
ansi_up.escape_html = false;
|
ansi_up.escape_html = false;
|
||||||
|
|
||||||
|
const ANSI_REFRESH_WINDOW = new Uint8Array([0x1B, 0x5B, 0x37, 0x74]);
|
||||||
|
const ANSI_CLEAR_WINDOW = new Uint8Array([0x1B, 0x5B, 0x32, 0x4A]);
|
||||||
|
|
||||||
/* quick HEX lookup table */
|
/* quick HEX lookup table */
|
||||||
const byteToHex: string[] = new Array(256);
|
const byteToHex: string[] = new Array(256);
|
||||||
for (let n = 0; n <= 0xff; ++n) {
|
for (let n = 0; n <= 0xff; ++n) {
|
||||||
|
@ -128,11 +139,12 @@ function strToHTML(str: string) {
|
||||||
.replace(/ /g, ' ');
|
.replace(/ /g, ' ');
|
||||||
}
|
}
|
||||||
|
|
||||||
function isArrayContained(subArray: Uint8Array, mainArray: Uint8Array) {
|
function isArrayContained(subArray: Uint8Array, mainArray: Uint8Array,
|
||||||
|
sub_start: number = 0, main_start: number = 0) {
|
||||||
if (subArray.length === 0) return -1;
|
if (subArray.length === 0) return -1;
|
||||||
outerLoop: for (let i = 0; i <= mainArray.length - subArray.length; i++) {
|
outerLoop: for (let i = main_start; i <= mainArray.length - subArray.length; i++) {
|
||||||
// Check if subArray is found starting at position i in mainArray
|
// Check if subArray is found starting at position i in mainArray
|
||||||
for (let j = 0; j < subArray.length; j++) {
|
for (let j = sub_start; j < subArray.length; j++) {
|
||||||
if (mainArray[i + j] !== subArray[j]) {
|
if (mainArray[i + j] !== subArray[j]) {
|
||||||
continue outerLoop; // Continue the outer loop if mismatch found
|
continue outerLoop; // Continue the outer loop if mismatch found
|
||||||
}
|
}
|
||||||
|
@ -207,8 +219,7 @@ export const useDataViewerStore = defineStore('text-viewer', () => {
|
||||||
const showHexdump = ref(false);
|
const showHexdump = ref(false);
|
||||||
const enableLineWrap = ref(true);
|
const enableLineWrap = ref(true);
|
||||||
const showTimestamp = ref(true);
|
const showTimestamp = ref(true);
|
||||||
|
const showVirtualScroll = ref(true);
|
||||||
const pauseAutoRefresh = ref(false);
|
|
||||||
|
|
||||||
const RxHexdumpColor = ref("#f0f9eb");
|
const RxHexdumpColor = ref("#f0f9eb");
|
||||||
const RxTotalByteCount = ref(0);
|
const RxTotalByteCount = ref(0);
|
||||||
|
@ -253,36 +264,40 @@ export const useDataViewerStore = defineStore('text-viewer', () => {
|
||||||
return encoder.encode(str);
|
return encoder.encode(str);
|
||||||
})
|
})
|
||||||
|
|
||||||
debouncedWatch(() => frameBreakSequence.value, (newValue) => {
|
// debouncedWatch(() => frameBreakSequence.value, (newValue) => {
|
||||||
const unescapedStr = unescapeString(newValue);
|
// const unescapedStr = unescapeString(newValue);
|
||||||
const encoder = new TextEncoder();
|
// const encoder = new TextEncoder();
|
||||||
frameBreakSequenceNormalized.value = encoder.encode(unescapedStr);
|
// frameBreakSequenceNormalized.value = encoder.encode(unescapedStr);
|
||||||
}, {debounce: 300, immediate: true});
|
// }, {debounce: 300, immediate: true});
|
||||||
|
//
|
||||||
|
// debouncedWatch(() => frameBreakDelay.value, (newValue) => {
|
||||||
|
// if (newValue < 0) {
|
||||||
|
// frameBreakReady = false;
|
||||||
|
// clearTimeout(frameBreakTimeoutID);
|
||||||
|
// } else if (newValue === 0) {
|
||||||
|
// frameBreakReady = true;
|
||||||
|
// clearTimeout(frameBreakTimeoutID);
|
||||||
|
// } else {
|
||||||
|
// refreshTimeout();
|
||||||
|
// }
|
||||||
|
// }, {debounce: 300, immediate: true});
|
||||||
|
|
||||||
debouncedWatch(() => frameBreakDelay.value, (newValue) => {
|
// let frameBreakReady = false;
|
||||||
if (newValue < 0) {
|
// let frameBreakTimeoutID = setTimeout(() => {
|
||||||
frameBreakReady = false;
|
// }, 0);
|
||||||
clearTimeout(frameBreakTimeoutID);
|
|
||||||
} else if (newValue === 0) {
|
|
||||||
frameBreakReady = true;
|
|
||||||
clearTimeout(frameBreakTimeoutID);
|
|
||||||
} else {
|
|
||||||
refreshTimeout();
|
|
||||||
}
|
|
||||||
}, {debounce: 300, immediate: true});
|
|
||||||
|
|
||||||
const dataArchive: IDataArchive[] = [];
|
const dataArchive: IDataArchive[] = [];
|
||||||
const dataBuf: IDataBuf[] = [];
|
const dataBuf: IDataBuf[] = [];
|
||||||
const dataBufLength = ref(0);
|
const dataBufLength = ref(0);
|
||||||
|
|
||||||
/* actual data shown on screen */
|
/* actual data shown on screen */
|
||||||
const dataFiltered: IDataBuf[] = shallowReactive([]);
|
const dataFiltered: ShallowReactive<IDataBuf[]> = shallowReactive([]);
|
||||||
const dataFilteredLength = ref(0);
|
const dataFilterAutoUpdate = ref(true);
|
||||||
|
const acceptIncomingData = ref(false);
|
||||||
|
|
||||||
|
// let frameBreakReady = false;
|
||||||
let frameBreakReady = false;
|
// let frameBreakTimeoutID = setTimeout(() => {
|
||||||
let frameBreakTimeoutID = setTimeout(() => {
|
// }, 0);
|
||||||
}, 0);
|
|
||||||
|
|
||||||
debouncedWatch(computedFilterValue, () => {
|
debouncedWatch(computedFilterValue, () => {
|
||||||
dataFiltered.length = 0; // Clear the array efficiently
|
dataFiltered.length = 0; // Clear the array efficiently
|
||||||
|
@ -302,24 +317,93 @@ export const useDataViewerStore = defineStore('text-viewer', () => {
|
||||||
}
|
}
|
||||||
}, {debounce: 300});
|
}, {debounce: 300});
|
||||||
|
|
||||||
function addString(item: string, isRX: boolean = false, doSend: boolean = false) {
|
let batchDataUpdateIntervalID: number = -1;
|
||||||
|
const batchUpdateTime = ref(80); /* ms */
|
||||||
|
let batchStartIndex: number = 0;
|
||||||
|
|
||||||
|
watch(batchUpdateTime, value => {
|
||||||
|
if (batchDataUpdateIntervalID >= 0) {
|
||||||
|
clearInterval(batchDataUpdateIntervalID);
|
||||||
|
batchDataUpdateIntervalID = -1;
|
||||||
|
}
|
||||||
|
batchUpdate();
|
||||||
|
if (dataFilterAutoUpdate.value && batchDataUpdateIntervalID < 0) {
|
||||||
|
batchDataUpdateIntervalID = setInterval(batchUpdate, batchUpdateTime.value);
|
||||||
|
}
|
||||||
|
}, {immediate: true});
|
||||||
|
|
||||||
|
setInterval(() => {
|
||||||
|
dataBufLength.value = dataBuf.length;
|
||||||
|
}, 500);
|
||||||
|
|
||||||
|
function addString(item: string, isRX: boolean = false, doSend: boolean = false, type: number = 0) {
|
||||||
const encoder = new TextEncoder();
|
const encoder = new TextEncoder();
|
||||||
item = unescapeString(item);
|
item = unescapeString(item);
|
||||||
const encodedStr = encoder.encode(item);
|
const encodedStr = encoder.encode(item);
|
||||||
return addItem(encodedStr, isRX, doSend);
|
return addItem(encodedStr, isRX, doSend, type);
|
||||||
}
|
}
|
||||||
|
|
||||||
function addHexString(item: string, isRX: boolean = false, doSend: boolean = false){
|
function addHexString(item: string, isRX: boolean = false, doSend: boolean = false, type: number = 0){
|
||||||
if (item === "") {
|
if (item === "") {
|
||||||
return addItem(new Uint8Array(0), isRX);
|
return addItem(new Uint8Array(0), isRX);
|
||||||
}
|
}
|
||||||
const hexArray = item.split(' ');
|
const hexArray = item.split(' ');
|
||||||
// Map each hex value to a decimal (integer) and create a Uint8Array from these integers
|
// Map each hex value to a decimal (integer) and create a Uint8Array from these integers
|
||||||
const uint8Array = new Uint8Array(hexArray.map(hex => parseInt(hex, 16)));
|
const uint8Array = new Uint8Array(hexArray.map(hex => parseInt(hex, 16)));
|
||||||
return addItem(uint8Array, isRX, doSend);
|
return addItem(uint8Array, isRX, doSend, type);
|
||||||
}
|
}
|
||||||
|
|
||||||
function addItem(item: Uint8Array, isRX: boolean, doSend: boolean = false){
|
function batchUpdate() {
|
||||||
|
if (batchStartIndex >= dataBuf.length) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* handle data buf array */
|
||||||
|
if (dataBuf.length >= 30000) {
|
||||||
|
/* make array size to 15000 */
|
||||||
|
const deleteCount = dataBuf.length - 30000 + 5000;
|
||||||
|
batchStartIndex -= deleteCount;
|
||||||
|
dataBuf.splice(0, deleteCount);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!dataFilterAutoUpdate.value) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
softRefreshFilterBuf();
|
||||||
|
}
|
||||||
|
|
||||||
|
function softRefreshFilterBuf(delayTime: number = 0) {
|
||||||
|
/* handle filtered buf array */
|
||||||
|
const totalBufLength = dataBuf.length - batchStartIndex + dataFiltered.length;
|
||||||
|
|
||||||
|
if (batchStartIndex < 0) {
|
||||||
|
dataFiltered.length = 1;
|
||||||
|
dataFiltered.pop();
|
||||||
|
} else if (totalBufLength >= 30000) {
|
||||||
|
dataFiltered.splice(0, totalBufLength - 30000 + 5000);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!enableFilter.value || computedFilterValue.value.length === 0) {
|
||||||
|
|
||||||
|
/* no filter, do normal push */
|
||||||
|
if (batchStartIndex < 0) {
|
||||||
|
dataFiltered.push(...dataBuf);
|
||||||
|
} else {
|
||||||
|
dataFiltered.push(...dataBuf.slice(batchStartIndex));
|
||||||
|
}
|
||||||
|
batchStartIndex = dataBuf.length;
|
||||||
|
} else if (enableFilter.value && computedFilterValue.value.length !== 0) {
|
||||||
|
for (let i = batchStartIndex; i < dataBuf.length; i++) {
|
||||||
|
if (isArrayContained(computedFilterValue.value, dataBuf[i].data) >= 0) {
|
||||||
|
dataFiltered.push(dataBuf[i]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
batchStartIndex = dataBuf.length;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function addItem(item: Uint8Array, isRX: boolean, doSend: boolean = false, type: number = 0) {
|
||||||
|
|
||||||
const t = new Date();
|
const t = new Date();
|
||||||
|
|
||||||
// dataArchive.push({
|
// dataArchive.push({
|
||||||
|
@ -349,119 +433,107 @@ export const useDataViewerStore = defineStore('text-viewer', () => {
|
||||||
TxByteCount.value = item.length;
|
TxByteCount.value = item.length;
|
||||||
}
|
}
|
||||||
|
|
||||||
let str = decodeUtf8(item);
|
let str = ""
|
||||||
|
str = decodeUtf8(item);
|
||||||
str = escapeHTML(str);
|
str = escapeHTML(str);
|
||||||
str = strToHTML(str);
|
str = strToHTML(str);
|
||||||
|
|
||||||
/* unescape data \n */
|
/* unescape data \n */
|
||||||
if (enableAnsiDecode.value) {
|
if (enableAnsiDecode.value) {
|
||||||
|
if (isArrayContained(ANSI_CLEAR_WINDOW, item) >= 0) {
|
||||||
|
clearFilteredBuff();
|
||||||
|
}
|
||||||
|
if (isArrayContained(ANSI_REFRESH_WINDOW, item) >= 0) {
|
||||||
|
batchUpdate()
|
||||||
|
softRefreshFilterBuf();
|
||||||
|
}
|
||||||
/* ansi_to_html will escape HTML sequence */
|
/* ansi_to_html will escape HTML sequence */
|
||||||
str = ansi_up.ansi_to_html("\x1b[0m" + str);
|
str = ansi_up.ansi_to_html("\x1b[0m" + str);
|
||||||
}
|
}
|
||||||
|
|
||||||
dataBuf.push({
|
dataBuf.push({
|
||||||
time: "["
|
time:
|
||||||
|
"["
|
||||||
+ zeroPad(t.getHours(), 2) + ":"
|
+ zeroPad(t.getHours(), 2) + ":"
|
||||||
+ zeroPad(t.getMinutes(), 2) + ":"
|
+ zeroPad(t.getMinutes(), 2) + ":"
|
||||||
+ zeroPad(t.getSeconds(), 2) + ":"
|
+ zeroPad(t.getSeconds(), 2) + ":"
|
||||||
+ zeroPad(t.getMilliseconds(), 3)
|
+ zeroPad(t.getMilliseconds(), 3)
|
||||||
+ "]",
|
+ "]",
|
||||||
|
type: type,
|
||||||
data: item,
|
data: item,
|
||||||
isRX: isRX,
|
isRX: isRX,
|
||||||
str: str,
|
str: str,
|
||||||
hex: u8toHexString(item),
|
hex: u8toHexString(item),
|
||||||
hexdump: u8toHexdump(item),
|
hexdump: u8toHexdump(item),
|
||||||
});
|
});
|
||||||
|
|
||||||
if (dataBuf.length >= 20000) {
|
|
||||||
dataBuf.splice(0, 5000);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!enableFilter.value || computedFilterValue.value.length === 0) {
|
|
||||||
dataFiltered.push(dataBuf[dataBuf.length - 1]);
|
|
||||||
if (dataFiltered.length >= 20000) {
|
|
||||||
dataFiltered.splice(0, 5000);
|
|
||||||
}
|
|
||||||
} else if (enableFilter.value && isArrayContained(computedFilterValue.value, dataBuf[dataBuf.length - 1].data) >= 0) {
|
|
||||||
dataFiltered.push(dataBuf[dataBuf.length - 1]);
|
|
||||||
if (dataFiltered.length >= 20000) {
|
|
||||||
dataFiltered.splice(0, 5000);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
dataBufLength.value = dataBuf.length;
|
|
||||||
}
|
|
||||||
|
|
||||||
function popItem() {
|
|
||||||
dataBuf.pop();
|
|
||||||
dataFiltered.pop();
|
|
||||||
}
|
|
||||||
|
|
||||||
function doFrameBreak() {
|
|
||||||
frameBreakReady = true;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
function refreshTimeout() {
|
// function doFrameBreak() {
|
||||||
/* always break */
|
|
||||||
// if (frameBreakDelay.value === 0) {
|
|
||||||
// frameBreakReady = true;
|
// frameBreakReady = true;
|
||||||
// }
|
// }
|
||||||
|
|
||||||
|
|
||||||
if (!frameBreakReady && frameBreakDelay.value > 0) {
|
// function refreshTimeout() {
|
||||||
clearTimeout(frameBreakTimeoutID);
|
// /* always break */
|
||||||
frameBreakTimeoutID = setTimeout(doFrameBreak, frameBreakDelay.value);
|
// // if (frameBreakDelay.value === 0) {
|
||||||
}
|
// // frameBreakReady = true;
|
||||||
}
|
// // }
|
||||||
|
//
|
||||||
|
//
|
||||||
|
// if (!frameBreakReady && frameBreakDelay.value > 0) {
|
||||||
|
// clearTimeout(frameBreakTimeoutID);
|
||||||
|
// frameBreakTimeoutID = setTimeout(doFrameBreak, frameBreakDelay.value);
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
|
||||||
function addChunk(item: Uint8Array, isRX: boolean) {
|
// function addChunk(item: Uint8Array, isRX: boolean) {
|
||||||
let newArray: Uint8Array;
|
// let newArray: Uint8Array;
|
||||||
|
//
|
||||||
if (frameBreakSequence.value === "") {
|
// if (frameBreakSequence.value === "") {
|
||||||
if (frameBreakReady || dataBuf.length === 0 || dataBuf[dataBuf.length - 1].isRX != isRX) {
|
// if (frameBreakReady || dataBuf.length === 0 || dataBuf[dataBuf.length - 1].isRX != isRX) {
|
||||||
addItem(item, isRX);
|
// addItem(item, isRX);
|
||||||
frameBreakReady = false;
|
// frameBreakReady = false;
|
||||||
} else {
|
// } else {
|
||||||
/* TODO: append item to last */
|
// /* TODO: append item to last */
|
||||||
newArray = new Uint8Array(dataBuf[dataBuf.length - 1].data.length + item.length + 1);
|
// newArray = new Uint8Array(dataBuf[dataBuf.length - 1].data.length + item.length + 1);
|
||||||
newArray.set(dataBuf[dataBuf.length - 1].data);
|
// newArray.set(dataBuf[dataBuf.length - 1].data);
|
||||||
newArray.set(item, dataBuf[dataBuf.length - 1].data.length);
|
// newArray.set(item, dataBuf[dataBuf.length - 1].data.length);
|
||||||
popItem();
|
// popItem();
|
||||||
addItem(newArray, isRX);
|
// addItem(newArray, isRX);
|
||||||
}
|
// }
|
||||||
refreshTimeout();
|
// refreshTimeout();
|
||||||
return;
|
// return;
|
||||||
}
|
// }
|
||||||
|
//
|
||||||
|
//
|
||||||
if (frameBreakReady) {
|
// if (frameBreakReady) {
|
||||||
newArray = item;
|
// newArray = item;
|
||||||
} else {
|
// } else {
|
||||||
if (dataBuf.length) {
|
// if (dataBuf.length) {
|
||||||
newArray = new Uint8Array(dataBuf[dataBuf.length - 1].data.length + item.length + 1);
|
// newArray = new Uint8Array(dataBuf[dataBuf.length - 1].data.length + item.length + 1);
|
||||||
newArray.set(dataBuf[dataBuf.length - 1].data);
|
// newArray.set(dataBuf[dataBuf.length - 1].data);
|
||||||
newArray.set(item, dataBuf[dataBuf.length - 1].data.length);
|
// newArray.set(item, dataBuf[dataBuf.length - 1].data.length);
|
||||||
popItem();
|
// popItem();
|
||||||
} else {
|
// } else {
|
||||||
newArray = item;
|
// newArray = item;
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
|
//
|
||||||
console.log(newArray)
|
// console.log(newArray)
|
||||||
console.log(frameBreakSequenceNormalized.value)
|
// console.log(frameBreakSequenceNormalized.value)
|
||||||
|
//
|
||||||
/* break frame at sequence match */
|
// /* break frame at sequence match */
|
||||||
let matchIndex = isArrayContained(frameBreakSequenceNormalized.value, newArray);
|
// let matchIndex = isArrayContained(frameBreakSequenceNormalized.value, newArray);
|
||||||
while (matchIndex < 0) {
|
// while (matchIndex < 0) {
|
||||||
console.log(matchIndex)
|
// console.log(matchIndex)
|
||||||
/* update last buf item */
|
// /* update last buf item */
|
||||||
addItem(newArray.slice(0, matchIndex + frameBreakSequenceNormalized.value.length), isRX);
|
// addItem(newArray.slice(0, matchIndex + frameBreakSequenceNormalized.value.length), isRX);
|
||||||
newArray = newArray.slice(matchIndex + frameBreakSequenceNormalized.value.length);
|
// newArray = newArray.slice(matchIndex + frameBreakSequenceNormalized.value.length);
|
||||||
matchIndex = isArrayContained(frameBreakSequenceNormalized.value, newArray);
|
// matchIndex = isArrayContained(frameBreakSequenceNormalized.value, newArray);
|
||||||
}
|
// }
|
||||||
addItem(newArray.slice(0, matchIndex + frameBreakSequenceNormalized.value.length), isRX);
|
// addItem(newArray.slice(0, matchIndex + frameBreakSequenceNormalized.value.length), isRX);
|
||||||
}
|
// }
|
||||||
|
|
||||||
function clearByteCount(isRX: boolean) {
|
function clearByteCount(isRX: boolean) {
|
||||||
if (isRX) {
|
if (isRX) {
|
||||||
|
@ -472,9 +544,10 @@ export const useDataViewerStore = defineStore('text-viewer', () => {
|
||||||
}
|
}
|
||||||
|
|
||||||
function clearDataBuff() {
|
function clearDataBuff() {
|
||||||
|
clearFilteredBuff();
|
||||||
dataBuf.length = 0;
|
dataBuf.length = 0;
|
||||||
dataFiltered.length = 0;
|
|
||||||
dataBufLength.value = 0;
|
dataBufLength.value = 0;
|
||||||
|
batchStartIndex = 0;
|
||||||
|
|
||||||
RxByteCount.value = 0;
|
RxByteCount.value = 0;
|
||||||
RxTotalByteCount.value = 0;
|
RxTotalByteCount.value = 0;
|
||||||
|
@ -484,6 +557,7 @@ export const useDataViewerStore = defineStore('text-viewer', () => {
|
||||||
}
|
}
|
||||||
|
|
||||||
function clearFilteredBuff() {
|
function clearFilteredBuff() {
|
||||||
|
showVirtualScroll.value = !showVirtualScroll.value;
|
||||||
dataFiltered.length = 0;
|
dataFiltered.length = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -508,22 +582,27 @@ export const useDataViewerStore = defineStore('text-viewer', () => {
|
||||||
|
|
||||||
return {
|
return {
|
||||||
addItem,
|
addItem,
|
||||||
addChunk,
|
|
||||||
addString,
|
addString,
|
||||||
addHexString,
|
addHexString,
|
||||||
clearFilteredBuff,
|
clearFilteredBuff,
|
||||||
clearDataBuff,
|
clearDataBuff,
|
||||||
refreshFilteredBuff,
|
refreshFilteredBuff,
|
||||||
|
softRefreshFilterBuf,
|
||||||
textSuffixValue,
|
textSuffixValue,
|
||||||
textPrefixValue,
|
textPrefixValue,
|
||||||
clearByteCount,
|
clearByteCount,
|
||||||
dataBufLength,
|
dataBufLength,
|
||||||
configPanelTab,
|
configPanelTab,
|
||||||
configPanelShow,
|
configPanelShow,
|
||||||
pauseAutoRefresh,
|
|
||||||
quickAccessPanelShow,
|
quickAccessPanelShow,
|
||||||
dataFiltered,
|
dataFiltered,
|
||||||
|
dataFilterAutoUpdate,
|
||||||
filterValue,
|
filterValue,
|
||||||
|
batchUpdateTime,
|
||||||
|
acceptIncomingData,
|
||||||
|
|
||||||
|
showVirtualScroll,
|
||||||
|
|
||||||
enableAnsiDecode,
|
enableAnsiDecode,
|
||||||
showHex,
|
showHex,
|
||||||
showHexdump,
|
showHexdump,
|
||||||
|
|
|
@ -368,8 +368,9 @@ const onUartJsonMsg = (msg: api.ApiJsonMsg) => {
|
||||||
case WtUartCmd.GET_CONFIG:
|
case WtUartCmd.GET_CONFIG:
|
||||||
case WtUartCmd.SET_CONFIG:{
|
case WtUartCmd.SET_CONFIG:{
|
||||||
const uartMsg = msg as IUartMsgConfig;
|
const uartMsg = msg as IUartMsgConfig;
|
||||||
|
store.uartConfig.data_bits = uartMsg.data_bits;
|
||||||
|
store.uartConfig.stop_bits = uartMsg.stop_bits;
|
||||||
|
store.uartConfig.parity = uartMsg.parity;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
default:
|
default:
|
||||||
|
@ -381,7 +382,9 @@ const onUartJsonMsg = (msg: api.ApiJsonMsg) => {
|
||||||
};
|
};
|
||||||
|
|
||||||
const onUartBinaryMsg = (msg: ApiBinaryMsg) => {
|
const onUartBinaryMsg = (msg: ApiBinaryMsg) => {
|
||||||
|
if (isDevMode()) {
|
||||||
console.log("uart", msg);
|
console.log("uart", msg);
|
||||||
|
}
|
||||||
|
|
||||||
if (msg.sub_mod !== 1) {
|
if (msg.sub_mod !== 1) {
|
||||||
/* ignore other num for the moment */
|
/* ignore other num for the moment */
|
||||||
|
@ -410,9 +413,10 @@ const onClientCtrl = (msg: api.ControlMsg) => {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (msg.data === ControlEvent.DISCONNECTED) {
|
if (msg.data === ControlEvent.DISCONNECTED) {
|
||||||
|
store.acceptIncomingData = false;
|
||||||
} else if (msg.data === ControlEvent.CONNECTED) {
|
} else if (msg.data === ControlEvent.CONNECTED) {
|
||||||
updateUartData();
|
updateUartData();
|
||||||
|
store.acceptIncomingData = true;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -85,7 +85,10 @@
|
||||||
|
|
||||||
<el-form-item label=" ">
|
<el-form-item label=" ">
|
||||||
<div class="flex">
|
<div class="flex">
|
||||||
<el-button type="primary">连接</el-button>
|
<el-button :type="store.acceptIncomingData ? 'danger': 'success'"
|
||||||
|
:disabled="wsStore.state !== ControlEvent.CONNECTED">
|
||||||
|
{{ store.acceptIncomingData ? "停止数据收发" : "开始数据收发" }}
|
||||||
|
</el-button>
|
||||||
</div>
|
</div>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
</el-form>
|
</el-form>
|
||||||
|
@ -161,6 +164,34 @@
|
||||||
过滤
|
过滤
|
||||||
</template>
|
</template>
|
||||||
</el-input>
|
</el-input>
|
||||||
|
|
||||||
|
<div class="border rounded flex flex-col">
|
||||||
|
<el-tooltip
|
||||||
|
class="box-item"
|
||||||
|
effect="light"
|
||||||
|
placement="right-start"
|
||||||
|
>
|
||||||
|
<template #content>
|
||||||
|
|
||||||
|
</template>
|
||||||
|
<el-checkbox border v-model="store.dataFilterAutoUpdate">新数据自动刷新</el-checkbox>
|
||||||
|
</el-tooltip>
|
||||||
|
|
||||||
|
<el-tooltip content="提高间隔可减少CPU资源的使用" placement="right" effect="light" :show-after="500">
|
||||||
|
<div class="flex gap-4 p-2">
|
||||||
|
<el-text>数据显示刷新间隔(ms)</el-text>
|
||||||
|
<el-input-number
|
||||||
|
:step="10"
|
||||||
|
:min="10"
|
||||||
|
size="small"
|
||||||
|
v-model="store.batchUpdateTime"
|
||||||
|
>
|
||||||
|
</el-input-number>
|
||||||
|
</div>
|
||||||
|
</el-tooltip>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</template>
|
</template>
|
||||||
|
@ -224,9 +255,12 @@
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import {ref} from "vue";
|
import {ref} from "vue";
|
||||||
import {useDataViewerStore} from "@/stores/dataViewerStore";
|
import {useDataViewerStore} from "@/stores/dataViewerStore";
|
||||||
|
import {useWsStore} from "@/stores/websocket";
|
||||||
import {globalNotify} from "@/composables/notification";
|
import {globalNotify} from "@/composables/notification";
|
||||||
|
import {ControlEvent} from "@/api";
|
||||||
|
|
||||||
const store = useDataViewerStore()
|
const store = useDataViewerStore()
|
||||||
|
const wsStore = useWsStore()
|
||||||
const collapseActiveName = ref(["1", "2"])
|
const collapseActiveName = ref(["1", "2"])
|
||||||
|
|
||||||
const uartCustomBaud = ref(9600)
|
const uartCustomBaud = ref(9600)
|
||||||
|
|
|
@ -39,14 +39,24 @@
|
||||||
placement="top"
|
placement="top"
|
||||||
>
|
>
|
||||||
<template #content>
|
<template #content>
|
||||||
<p>与缓存同步</p>
|
<p>与缓存同步+过滤</p>
|
||||||
</template>
|
</template>
|
||||||
<el-button size="small" @click="store.refreshFilteredBuff">
|
<el-button size="small" @click="store.refreshFilteredBuff">
|
||||||
刷新
|
刷新
|
||||||
</el-button>
|
</el-button>
|
||||||
</el-tooltip>
|
</el-tooltip>
|
||||||
|
<el-tooltip
|
||||||
<!-- <el-checkbox class="hover:bg-blue-200" size="small" v-model="store.pauseAutoRefresh" label="暂停数据刷新" border/>-->
|
class="box-item"
|
||||||
|
effect="light"
|
||||||
|
placement="top"
|
||||||
|
>
|
||||||
|
<template #content>
|
||||||
|
<p>仅停止刷新显示区,后台继续接收数据</p>
|
||||||
|
</template>
|
||||||
|
<el-checkbox size="small" border v-model="store.dataFilterAutoUpdate">
|
||||||
|
自动刷新
|
||||||
|
</el-checkbox>
|
||||||
|
</el-tooltip>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
@ -85,6 +95,7 @@
|
||||||
|
|
||||||
<div class="flex flex-grow overflow-hidden border-2 scroll-m-2">
|
<div class="flex flex-grow overflow-hidden border-2 scroll-m-2">
|
||||||
<v-virtual-scroll
|
<v-virtual-scroll
|
||||||
|
v-if="store.showVirtualScroll"
|
||||||
:items="store.dataFiltered"
|
:items="store.dataFiltered"
|
||||||
id="myScrollerID"
|
id="myScrollerID"
|
||||||
ref="vuetifyVirtualScrollRef"
|
ref="vuetifyVirtualScrollRef"
|
||||||
|
@ -94,8 +105,47 @@
|
||||||
<template v-slot:default="{ item, }">
|
<template v-slot:default="{ item, }">
|
||||||
<div>
|
<div>
|
||||||
<div class="flex">
|
<div class="flex">
|
||||||
<p class="text-nowrap text-sm text-lime-500" v-if="item.isRX" type="success" v-show="store.showTimestamp"><span>{{ item.time }}</span>◄-RX|</p>
|
<p class="text-nowrap text-sm text-lime-500" v-if="item.isRX" type="success" v-show="store.showTimestamp">
|
||||||
<p class="text-nowrap text-sm text-sky-500" v-else type="primary" v-show="store.showTimestamp"><span>{{ item.time }}</span>TX-►|</p>
|
<span>{{ item.time }}</span>◄-RX|</p>
|
||||||
|
<p class="text-nowrap text-sm text-sky-500" v-else-if="item.type === 0" type="primary" v-show="store.showTimestamp">
|
||||||
|
<span>{{ item.time }}</span>TX-►|</p>
|
||||||
|
<p class="text-nowrap text-sm text-amber-800" v-else type="primary" v-show="store.showTimestamp">
|
||||||
|
<span>{{ item.time }}</span>未发送►|</p>
|
||||||
|
|
||||||
|
<p v-show="store.showText"
|
||||||
|
v-html="item.str"></p>
|
||||||
|
</div>
|
||||||
|
<div class="flex text-wrap">
|
||||||
|
<p v-show="store.showHex" class="">{{ item.hex }}</p>
|
||||||
|
</div>
|
||||||
|
<div class="flex">
|
||||||
|
<p v-show="store.showHexdump"
|
||||||
|
class="text-nowrap"
|
||||||
|
:style="{ 'background-color': item.isRX ? store.RxHexdumpColor : store.TxHexdumpColor }"
|
||||||
|
v-html="item.hexdump"
|
||||||
|
></p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</v-virtual-scroll>
|
||||||
|
<v-virtual-scroll
|
||||||
|
v-else
|
||||||
|
:items="store.dataFiltered"
|
||||||
|
id="myScrollerID"
|
||||||
|
ref="vuetifyVirtualScrollRef2"
|
||||||
|
class="font-mono break-all text-sm"
|
||||||
|
:class="[store.enableLineWrap ? 'break-all' : 'text-nowrap']"
|
||||||
|
>
|
||||||
|
<template v-slot:default="{ item, }">
|
||||||
|
<div>
|
||||||
|
<div class="flex">
|
||||||
|
<p class="text-nowrap text-sm text-lime-500" v-if="item.isRX" type="success" v-show="store.showTimestamp">
|
||||||
|
<span>{{ item.time }}</span>◄-RX|</p>
|
||||||
|
<p class="text-nowrap text-sm text-sky-500" v-else-if="item.type === 0" type="primary" v-show="store.showTimestamp">
|
||||||
|
<span>{{ item.time }}</span>TX-►|</p>
|
||||||
|
<p class="text-nowrap text-sm text-amber-800" v-else type="primary" v-show="store.showTimestamp">
|
||||||
|
<span>{{ item.time }}</span>未发送►|</p>
|
||||||
|
|
||||||
<p v-show="store.showText"
|
<p v-show="store.showText"
|
||||||
v-html="item.str"></p>
|
v-html="item.str"></p>
|
||||||
</div>
|
</div>
|
||||||
|
@ -125,6 +175,7 @@
|
||||||
</el-tag>
|
</el-tag>
|
||||||
</el-link>
|
</el-link>
|
||||||
|
|
||||||
|
<el-tooltip content="实际频率受界面刷新率影响,如需要更精确,可以尝试关闭‘自动刷新’" placement="right" effect="light" :show-after="1000">
|
||||||
<div class="flex align-center">
|
<div class="flex align-center">
|
||||||
<el-checkbox v-model="enableLoopSend" class="font-mono font-bold max-h-5" size="small" border>
|
<el-checkbox v-model="enableLoopSend" class="font-mono font-bold max-h-5" size="small" border>
|
||||||
循环发送(ms)
|
循环发送(ms)
|
||||||
|
@ -134,9 +185,11 @@
|
||||||
class="h-5"
|
class="h-5"
|
||||||
size="small"
|
size="small"
|
||||||
:step="10"
|
:step="10"
|
||||||
|
:min="1"
|
||||||
>
|
>
|
||||||
</el-input-number>
|
</el-input-number>
|
||||||
</div>
|
</div>
|
||||||
|
</el-tooltip>
|
||||||
|
|
||||||
<el-link @click="isSendTextFormat = !isSendTextFormat">
|
<el-link @click="isSendTextFormat = !isSendTextFormat">
|
||||||
<el-tag class="font-mono font-bold" size="small">发送格式:{{ isSendTextFormat ? "文本" : "HEX" }}</el-tag>
|
<el-tag class="font-mono font-bold" size="small">发送格式:{{ isSendTextFormat ? "文本" : "HEX" }}</el-tag>
|
||||||
|
@ -184,30 +237,33 @@ import {nextTick, onMounted, onUnmounted, ref, watch} from "vue";
|
||||||
import {useDataViewerStore} from "@/stores/dataViewerStore";
|
import {useDataViewerStore} from "@/stores/dataViewerStore";
|
||||||
import InlineSvg from "@/components/InlineSvg.vue";
|
import InlineSvg from "@/components/InlineSvg.vue";
|
||||||
import TextDataConfig from "@/views/text-data-viewer/textDataConfig.vue";
|
import TextDataConfig from "@/views/text-data-viewer/textDataConfig.vue";
|
||||||
|
import {debouncedWatch} from "@vueuse/core";
|
||||||
|
|
||||||
const count = ref(0);
|
const count = ref(0);
|
||||||
const showTxTotalByte = ref(false);
|
const showTxTotalByte = ref(false);
|
||||||
const showRxTotalByte = ref(false);
|
const showRxTotalByte = ref(false);
|
||||||
const vuetifyVirtualScrollRef = ref(document.body);
|
|
||||||
const vuetifyVirtualScrollBarRef = ref(document.body);
|
const vuetifyVirtualScrollBarRef = ref(document.body);
|
||||||
const vuetifyVirtualScrollContainerRef = ref(document.body);
|
const vuetifyVirtualScrollContainerRef = ref(document.body);
|
||||||
|
|
||||||
const enableLoopSend = ref(false);
|
const enableLoopSend = ref(false);
|
||||||
const loopSendFreq = ref(1000);
|
const loopSendFreq = ref(1000);
|
||||||
let loopSendIntervalID: number;
|
let loopSendIntervalID: number = -1;
|
||||||
|
|
||||||
const isSendTextFormat = ref(true)
|
const isSendTextFormat = ref(true)
|
||||||
const isHexStringValid = ref(false);
|
const isHexStringValid = ref(false);
|
||||||
|
|
||||||
const uartInputTextBox = ref("")
|
const uartInputTextBox = ref("")
|
||||||
const store = useDataViewerStore();
|
const store = useDataViewerStore();
|
||||||
|
|
||||||
|
let lastScrollHeight = 0;
|
||||||
|
|
||||||
const mutationObserver = new MutationObserver(() => {
|
const mutationObserver = new MutationObserver(() => {
|
||||||
if (store.forceToBottom) {
|
if (store.forceToBottom) {
|
||||||
scrollToBottom();
|
scrollToBottom();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
onMounted(() => {
|
function attachScroll() {
|
||||||
const parent = document.getElementById('myScrollerID') || document.body;
|
const parent = document.getElementById('myScrollerID') || document.body;
|
||||||
|
|
||||||
// used to scroll to bottom
|
// used to scroll to bottom
|
||||||
|
@ -222,12 +278,22 @@ onMounted(() => {
|
||||||
const config = {childList: true, subtree: true, attributes: true};
|
const config = {childList: true, subtree: true, attributes: true};
|
||||||
mutationObserver.observe(vuetifyVirtualScrollBarRef.value, config)
|
mutationObserver.observe(vuetifyVirtualScrollBarRef.value, config)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
attachScroll();
|
||||||
})
|
})
|
||||||
|
|
||||||
onUnmounted(() => {
|
onUnmounted(() => {
|
||||||
mutationObserver.disconnect();
|
mutationObserver.disconnect();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
debouncedWatch(() => store.showVirtualScroll, () => {
|
||||||
|
lastScrollHeight = 0;
|
||||||
|
mutationObserver.disconnect();
|
||||||
|
attachScroll();
|
||||||
|
}, {debounce: 80});
|
||||||
|
|
||||||
|
|
||||||
function addItem(nr: number) {
|
function addItem(nr: number) {
|
||||||
let rawText = "";
|
let rawText = "";
|
||||||
|
@ -253,7 +319,7 @@ function addItem(nr: number) {
|
||||||
rawText = count.value + "<p class=\"border-4\"> 666666666b\n6666 666\x1b[33m6666666666666666666666666</p>b\n"
|
rawText = count.value + "<p class=\"border-4\"> 666666666b\n6666 666\x1b[33m6666666666666666666666666</p>b\n"
|
||||||
const encoder = new TextEncoder();
|
const encoder = new TextEncoder();
|
||||||
const arr = encoder.encode(rawText);
|
const arr = encoder.encode(rawText);
|
||||||
store.addItem(arr, true);
|
store.addItem(arr, false, false, 1);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -315,7 +381,7 @@ watch(isSendTextFormat, (value) => {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
watch(() => uartInputTextBox.value, (newValue) => {
|
watch(() => uartInputTextBox.value, () => {
|
||||||
if (!isSendTextFormat.value) {
|
if (!isSendTextFormat.value) {
|
||||||
checkHexTextValid()
|
checkHexTextValid()
|
||||||
}
|
}
|
||||||
|
@ -323,17 +389,22 @@ watch(() => uartInputTextBox.value, (newValue) => {
|
||||||
|
|
||||||
watch(enableLoopSend, (newValue) => {
|
watch(enableLoopSend, (newValue) => {
|
||||||
if (newValue) {
|
if (newValue) {
|
||||||
|
if (loopSendIntervalID !== -1) {
|
||||||
clearInterval(loopSendIntervalID);
|
clearInterval(loopSendIntervalID);
|
||||||
|
}
|
||||||
loopSendIntervalID = setInterval(onSendClick, loopSendFreq.value);
|
loopSendIntervalID = setInterval(onSendClick, loopSendFreq.value);
|
||||||
} else {
|
} else {
|
||||||
clearInterval(loopSendIntervalID);
|
clearInterval(loopSendIntervalID);
|
||||||
|
loopSendIntervalID = -1;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
watch(loopSendFreq, (value) => {
|
watch(loopSendFreq, (value) => {
|
||||||
if (enableLoopSend.value && value) {
|
if (enableLoopSend.value && value) {
|
||||||
/* update interval with new value */
|
/* update interval with new value */
|
||||||
|
if (loopSendIntervalID !== -1) {
|
||||||
clearInterval(loopSendIntervalID);
|
clearInterval(loopSendIntervalID);
|
||||||
|
}
|
||||||
loopSendIntervalID = setInterval(onSendClick, loopSendFreq.value);
|
loopSendIntervalID = setInterval(onSendClick, loopSendFreq.value);
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
@ -341,17 +412,26 @@ watch(loopSendFreq, (value) => {
|
||||||
/* patch scroll container does not update clear filter */
|
/* patch scroll container does not update clear filter */
|
||||||
watch(() => store.filterChanged, (value) => {
|
watch(() => store.filterChanged, (value) => {
|
||||||
if (value) {
|
if (value) {
|
||||||
scrollToBottom();
|
|
||||||
scrollToTop()
|
scrollToTop()
|
||||||
|
scrollToBottom();
|
||||||
store.filterChanged = false;
|
store.filterChanged = false;
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
const handleScroll = (ev: Event) => {
|
const handleScroll = (ev: Event) => {
|
||||||
if (store.forceToBottom) {
|
if (store.forceToBottom) {
|
||||||
|
if (vuetifyVirtualScrollBarRef.value.scrollTop - lastScrollHeight < 0) {
|
||||||
|
store.forceToBottom = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
lastScrollHeight = vuetifyVirtualScrollBarRef.value.scrollTop;
|
||||||
|
};
|
||||||
|
|
||||||
|
watch(() => store.forceToBottom, value => {
|
||||||
|
if (value) {
|
||||||
setTimeout(scrollToBottom, 0);
|
setTimeout(scrollToBottom, 0);
|
||||||
}
|
}
|
||||||
};
|
});
|
||||||
|
|
||||||
function clearSendInput() {
|
function clearSendInput() {
|
||||||
uartInputTextBox.value = ""
|
uartInputTextBox.value = ""
|
||||||
|
@ -364,6 +444,7 @@ function handleTextboxKeydown(ev: KeyboardEvent) {
|
||||||
}
|
}
|
||||||
|
|
||||||
function onSendClick() {
|
function onSendClick() {
|
||||||
|
if (store.acceptIncomingData) {
|
||||||
if (isSendTextFormat.value) {
|
if (isSendTextFormat.value) {
|
||||||
store.addString(uartInputTextBox.value, false, true);
|
store.addString(uartInputTextBox.value, false, true);
|
||||||
} else if (!isHexStringValid.value) {
|
} else if (!isHexStringValid.value) {
|
||||||
|
@ -371,6 +452,15 @@ function onSendClick() {
|
||||||
} else {
|
} else {
|
||||||
store.addHexString(uartInputTextBox.value, false, true);
|
store.addHexString(uartInputTextBox.value, false, true);
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
if (isSendTextFormat.value) {
|
||||||
|
store.addString(uartInputTextBox.value, false, true, 1);
|
||||||
|
} else if (!isHexStringValid.value) {
|
||||||
|
uartInputTextBox.value = formatHexInput(uartInputTextBox.value);
|
||||||
|
} else {
|
||||||
|
store.addHexString(uartInputTextBox.value, false, true, 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|
Loading…
Reference in New Issue