feat(smart scroll, ANSI clear, ANSI refresh)

This commit is contained in:
kerms 2024-05-21 15:14:46 +08:00
parent 15c1143b25
commit 51783612bc
5 changed files with 380 additions and 169 deletions

View File

@ -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

View File

@ -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, '&nbsp;'); .replace(/ /g, '&nbsp;');
} }
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.getMinutes(), 2) + ":" + zeroPad(t.getHours(), 2) + ":"
+ zeroPad(t.getSeconds(), 2) + ":" + zeroPad(t.getMinutes(), 2) + ":"
+ zeroPad(t.getMilliseconds(), 3) + zeroPad(t.getSeconds(), 2) + ":"
+ "]", + 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 */ // frameBreakReady = true;
// if (frameBreakDelay.value === 0) { // }
// 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,

View File

@ -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) => {
console.log("uart", msg); if (isDevMode()) {
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;
} }
}; };

View File

@ -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)

View File

@ -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,13 @@
<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" <p v-show="store.showText"
v-html="item.str"></p> v-html="item.str"></p>
</div> </div>
@ -105,7 +121,41 @@
<div class="flex"> <div class="flex">
<p v-show="store.showHexdump" <p v-show="store.showHexdump"
class="text-nowrap" class="text-nowrap"
:style="{ 'background-color': item.isRX ? store.RxHexdumpColor : store.TxHexdumpColor }" :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"
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" v-html="item.hexdump"
></p> ></p>
</div> </div>
@ -125,18 +175,21 @@
</el-tag> </el-tag>
</el-link> </el-link>
<div class="flex align-center"> <el-tooltip content="实际频率受界面刷新率影响,如需要更精确,可以尝试关闭‘自动刷新’" placement="right" effect="light" :show-after="1000">
<el-checkbox v-model="enableLoopSend" class="font-mono font-bold max-h-5" size="small" border> <div class="flex align-center">
循环发送(ms) <el-checkbox v-model="enableLoopSend" class="font-mono font-bold max-h-5" size="small" border>
</el-checkbox> 循环发送(ms)
<el-input-number </el-checkbox>
v-model="loopSendFreq" <el-input-number
class="h-5" v-model="loopSendFreq"
size="small" class="h-5"
:step="10" size="small"
> :step="10"
</el-input-number> :min="1"
</div> >
</el-input-number>
</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>
@ -145,12 +198,12 @@
<div class="flex gap-2"> <div class="flex gap-2">
<el-link @click="showTxTotalByte = !showTxTotalByte"> <el-link @click="showTxTotalByte = !showTxTotalByte">
<el-tag class="font-mono font-bold" size="small"> <el-tag class="font-mono font-bold" size="small">
{{ showTxTotalByte ? `TX统计:${store.TxTotalByteCount}B`: `上个TX帧:${store.TxByteCount}B` }} {{ showTxTotalByte ? `TX统计:${store.TxTotalByteCount}B` : `上个TX帧:${store.TxByteCount}B` }}
</el-tag> </el-tag>
</el-link> </el-link>
<el-link type="success" @click="showRxTotalByte = !showRxTotalByte"> <el-link type="success" @click="showRxTotalByte = !showRxTotalByte">
<el-tag class="font-mono font-bold" size="small" type="success"> <el-tag class="font-mono font-bold" size="small" type="success">
{{ showRxTotalByte ? `RX统计:${store.RxTotalByteCount}B`: `上个RX帧:${store.RxByteCount}B` }} {{ showRxTotalByte ? `RX统计:${store.RxTotalByteCount}B` : `上个RX帧:${store.RxByteCount}B` }}
</el-tag> </el-tag>
</el-link> </el-link>
<div class="flex align-center"> <div class="flex align-center">
@ -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
@ -219,15 +275,25 @@ onMounted(() => {
vuetifyVirtualScrollBarRef.value.onscroll = handleScroll; vuetifyVirtualScrollBarRef.value.onscroll = handleScroll;
if (vuetifyVirtualScrollContainerRef.value) { if (vuetifyVirtualScrollContainerRef.value) {
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);
} }
} }
@ -283,7 +349,7 @@ function formatHexInput(input: string) {
str = str.replace(/[^0-9A-F]/gi, ' '); str = str.replace(/[^0-9A-F]/gi, ' ');
let segments = str.split(/\s+/); let segments = str.split(/\s+/);
let output:string[] = []; let output: string[] = [];
segments.forEach(segment => { segments.forEach(segment => {
// Check if segment length is odd and needs padding // Check if segment length is odd and needs padding
@ -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) {
clearInterval(loopSendIntervalID); if (loopSendIntervalID !== -1) {
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 */
clearInterval(loopSendIntervalID); if (loopSendIntervalID !== -1) {
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,12 +444,22 @@ function handleTextboxKeydown(ev: KeyboardEvent) {
} }
function onSendClick() { function onSendClick() {
if (isSendTextFormat.value) { if (store.acceptIncomingData) {
store.addString(uartInputTextBox.value, false, true); if (isSendTextFormat.value) {
} else if (!isHexStringValid.value) { store.addString(uartInputTextBox.value, false, true);
uartInputTextBox.value = formatHexInput(uartInputTextBox.value); } else if (!isHexStringValid.value) {
uartInputTextBox.value = formatHexInput(uartInputTextBox.value);
} else {
store.addHexString(uartInputTextBox.value, false, true);
}
} else { } else {
store.addHexString(uartInputTextBox.value, false, true); 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);
}
} }
} }