From a2b7026f54569627c4ee6c5b7fb9180407cf06f8 Mon Sep 17 00:00:00 2001 From: kerms Date: Fri, 24 May 2024 16:17:41 +0800 Subject: [PATCH] feat(uart): data frame break --- package-lock.json | 20 ++ package.json | 5 +- src/App.vue | 4 + src/assets/icon/help.svg | 1 + src/composables/notification.ts | 4 +- src/router/index.ts | 2 +- src/stores/dataViewerStore.ts | 205 ++++++++++++++++-- src/views/Uart.vue | 2 +- src/views/text-data-viewer/textDataConfig.vue | 103 ++++++++- src/views/text-data-viewer/textDataViewer.vue | 50 +++-- 10 files changed, 357 insertions(+), 39 deletions(-) create mode 100644 src/assets/icon/help.svg diff --git a/package-lock.json b/package-lock.json index 22df8bf..18805a3 100644 --- a/package-lock.json +++ b/package-lock.json @@ -14,6 +14,7 @@ "mitt": "^3.0.1", "pinia": "^2.1.7", "vue": "^3.4.21", + "vue-draggable-plus": "^0.4.1", "vue-i18n": "^9.10.2", "vue-router": "^4.3.0", "vuetify": "^3.6.5" @@ -1051,6 +1052,12 @@ "integrity": "sha512-I8EUhyrgfLrcTkzV3TSsGyl1tSuPrEDzr0yd5m90UgNxQkyDXULk3b6MlQqTCpZpNtWe1K0hzclnZkTcLBe2UQ==", "dev": true }, + "node_modules/@types/sortablejs": { + "version": "1.15.8", + "resolved": "https://registry.npmjs.org/@types/sortablejs/-/sortablejs-1.15.8.tgz", + "integrity": "sha512-b79830lW+RZfwaztgs1aVPgbasJ8e7AXtZYHTELNXZPsERt4ymJdjV4OccDbHQAvHrCcFpbF78jkm0R6h/pZVg==", + "peer": true + }, "node_modules/@types/web-bluetooth": { "version": "0.0.20", "resolved": "https://registry.npmjs.org/@types/web-bluetooth/-/web-bluetooth-0.0.20.tgz", @@ -5479,6 +5486,19 @@ } } }, + "node_modules/vue-draggable-plus": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/vue-draggable-plus/-/vue-draggable-plus-0.4.1.tgz", + "integrity": "sha512-KNi+c482OQUZTZ2kXIGc41fEwknkNF+LlngjBr5TVtBLNvpX2dmwRJJ3J7dy5dGcijXb7V1j+mhqce4iHOoi6Q==", + "peerDependencies": { + "@types/sortablejs": "^1.15.0" + }, + "peerDependenciesMeta": { + "@vue/composition-api": { + "optional": true + } + } + }, "node_modules/vue-eslint-parser": { "version": "9.4.2", "resolved": "https://registry.npmjs.org/vue-eslint-parser/-/vue-eslint-parser-9.4.2.tgz", diff --git a/package.json b/package.json index 83255d9..725f8de 100644 --- a/package.json +++ b/package.json @@ -21,9 +21,10 @@ "mitt": "^3.0.1", "pinia": "^2.1.7", "vue": "^3.4.21", + "vue-draggable-plus": "^0.4.1", "vue-i18n": "^9.10.2", - "vuetify": "^3.6.5", - "vue-router": "^4.3.0" + "vue-router": "^4.3.0", + "vuetify": "^3.6.5" }, "devDependencies": { "@rushstack/eslint-patch": "^1.3.3", diff --git a/src/App.vue b/src/App.vue index 746b801..8262b80 100644 --- a/src/App.vue +++ b/src/App.vue @@ -11,6 +11,7 @@ import {ControlEvent, ControlMsgType} from "@/api"; import {routeCtrlMsg, routeModuleServerMsg} from "@/router/msgRouter"; import {globalNotify} from "@/composables/notification"; import {isDevMode} from "@/composables/buildMode"; +import {ElMessageBox} from "element-plus"; const wsState = useWsStore(); @@ -47,6 +48,9 @@ onMounted(() => { websocketService = getWebsocketService(); websocketService.init(host, onServerMsg, onClientCtrl); changeFavicon(); + ElMessageBox.alert('欢迎参与允斯无线串口助手固件内侧,有任何问题请在Q群踢群主:642246000', '2024-05-24', { + confirmButtonText: '好的', + }) }); onUnmounted(() => { diff --git a/src/assets/icon/help.svg b/src/assets/icon/help.svg new file mode 100644 index 0000000..7f11a89 --- /dev/null +++ b/src/assets/icon/help.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/composables/notification.ts b/src/composables/notification.ts index 0ffe27a..3c1cc76 100644 --- a/src/composables/notification.ts +++ b/src/composables/notification.ts @@ -2,7 +2,7 @@ import {ElMessage, ElNotification} from "element-plus"; type NotificationType = 'error' | 'warning' | 'info' | 'success' ; -export function globalNotify(msg: string, type: NotificationType) { +export function globalNotify(msg: string, type: NotificationType = "info") { ElMessage({ message: msg, grouping: true, @@ -13,7 +13,7 @@ export function globalNotify(msg: string, type: NotificationType) { }) } -export function globalNotifyRightSide(msg: string, type: NotificationType) { +export function globalNotifyRightSide(msg: string, type: NotificationType = "info") { ElNotification({ message: msg, type: type, diff --git a/src/router/index.ts b/src/router/index.ts index 72ca4a5..805a54d 100644 --- a/src/router/index.ts +++ b/src/router/index.ts @@ -16,7 +16,7 @@ const router = createRouter({ name: 'home', meta: {title: translate("page.home")}, // component: Wifi - redirect: () => '/wifi', + redirect: () => '/uart', }, { path: '/home:ext(.*)', meta: {title: translate("page.home")}, diff --git a/src/stores/dataViewerStore.ts b/src/stores/dataViewerStore.ts index cd82f24..9e90da1 100644 --- a/src/stores/dataViewerStore.ts +++ b/src/stores/dataViewerStore.ts @@ -5,11 +5,12 @@ import { type ShallowReactive, ref, shallowReactive, - watch + watch, reactive } from "vue"; import {AnsiUp} from 'ansi_up' import {debouncedWatch} from "@vueuse/core"; import {type IUartConfig, uart_send_msg} from "@/api/apiUart"; +import {isDevMode} from "@/composables/buildMode"; interface IDataArchive { time: number; @@ -164,7 +165,7 @@ const baudArr = [ } ] -function generateBaudArr(results: { baud: number;}[]) { +function generateBaudArr(results: { baud: number; }[]) { for (let i = 0; i < baudArr.length; ++i) { let start = baudArr[i].start; for (let j = 0; j < baudArr[i].count; ++j) { @@ -210,9 +211,188 @@ export const useDataViewerStore = defineStore('text-viewer', () => { const configPanelShow = ref(true); const quickAccessPanelShow = ref(true); const enableAnsiDecode = ref(true); + + + /* + * FRAME BREAK STUFS + * */ + + let RxSegment: Uint8Array = new Uint8Array(0); + const RxRemainHexdump = ref(""); + const frameBreakSequence = ref("\\n"); - const frameBreakSequenceNormalized = ref(new Uint8Array(0)); + const frameBreakAfterSequence = ref(true); + const frameBreakSequenceNormalized = computed(() => { + const unescapedStr = unescapeString(frameBreakSequence.value); + const encoder = new TextEncoder(); + return encoder.encode(unescapedStr); + }); const frameBreakDelay = ref(0); + let frameBreakDelayTimeoutID: number = -1; + + function frameBreakFlush() { + if (RxSegment.length) { + addItem(RxSegment, true); + RxSegment = new Uint8Array(); + RxRemainHexdump.value = ""; + } + } + + function frameBreakRefreshTimout() { + if (frameBreakDelay.value > 0) { + if (frameBreakDelayTimeoutID >= 0) { + clearTimeout(frameBreakDelayTimeoutID); + frameBreakDelayTimeoutID = -1; + } + frameBreakDelayTimeoutID = setTimeout(() => { + frameBreakFlush() + }, frameBreakDelay.value); + } else { + if (frameBreakDelayTimeoutID >= 0) { + clearTimeout(frameBreakDelayTimeoutID); + frameBreakDelayTimeoutID = -1; + } + if (frameBreakDelay.value === 0) { + frameBreakFlush(); + } + } + } + + debouncedWatch(() => frameBreakDelay.value, () => { + frameBreakRefreshTimout(); + console.log("timeout called"); + }, {debounce: 300}); + + + + const frameBreakSize = ref(0); + const frameBreakRules = reactive([{ + ref: frameBreakDelay, + name: '超时(ms)', + type: 'number', + min: -1, + draggable: false, + transformData: () => { + return {result: [] as Uint8Array[], remain: true}; + } + }, { + ref: frameBreakSequence, + name: '匹配', + type: 'text', + draggable: true, + transformData: (inputArray: Uint8Array[]) => { + if (frameBreakSequenceNormalized.value.length <= 0) { + return {result: inputArray, remain: true}; + } + const result: Uint8Array[] = []; + /* if split after, the matched array is appended to the previous */ + const appendedLength = frameBreakAfterSequence.value ? frameBreakSequenceNormalized.value.length : 0; + /* else after the first match, skip the matchArr at the beginning of array in subsequent match */ + const skipLength = frameBreakAfterSequence.value ? 0 : frameBreakSequenceNormalized.value.length; + let remain = false; + let startIndex = 0; + + inputArray.forEach(array => { + startIndex = 0; + let matchIndex = isArrayContained(frameBreakSequenceNormalized.value, array, 0, startIndex); + + while (matchIndex !== -1) { + const endIndex = matchIndex + appendedLength; + if (startIndex !== endIndex) { + result.push(array.subarray(startIndex, endIndex)); + } + startIndex = endIndex; + matchIndex = isArrayContained(frameBreakSequenceNormalized.value, array, + 0, startIndex + skipLength); + } + // Add the last segment if there's any remaining part of the array + if (startIndex < array.length) { + result.push(array.subarray(startIndex, array.length)); + } + }); + remain = startIndex < inputArray[inputArray.length - 1].length; + return {result, remain}; + } + }, { + ref: frameBreakSize, + name: '字节(B)', + type: 'number', + min: 0, + draggable: true, + transformData: (inputArray: Uint8Array[]) => { + if (frameBreakSize.value <= 0) { + return {result: inputArray, remain: true}; + } + const result: Uint8Array[] = []; + inputArray.forEach(item => { + for (let start = 0; start < item.length; start += frameBreakSize.value) { + const end = Math.min(start + frameBreakSize.value, item.length); + result.push(item.subarray(start, end)); + } + }); + const remain = result[result.length - 1].length < frameBreakSize.value; + return {result, remain}; + } + },]) + + function addStringMessage(input: string, isRX: boolean, doSend: boolean = false) { + const encoder = new TextEncoder(); + input = unescapeString(input); + const encodedStr = encoder.encode(input); + addSegment(encodedStr, isRX); + } + + function addSegment(input: Uint8Array, isRX: boolean, doSend: boolean = false) { + if (input.length <= 0) { + if (isDevMode()) { + console.log("input size =0"); + } + return; + } + + let frames: Uint8Array[] = [] + const data= new Uint8Array(RxSegment.length + input.length); + let remain = true; + data.set(RxSegment); + data.set(input, RxSegment.length); + RxSegment = data; + + frames.push(RxSegment); + /* ready for adding new items */ + for (let i = 1; i < frameBreakRules.length; i++) { + const ret: {result: Uint8Array[], remain: boolean} = frameBreakRules[i].transformData(frames); + /* check if last item changed */ + if (!ret.remain || ret.result[ret.result.length - 1].length !== frames[frames.length - 1].length) { + remain = ret.remain; + } + frames = ret.result; + } + + if (frameBreakDelay.value !== 0 && remain) { + RxSegment = frames.pop() || new Uint8Array(); + if (frameBreakDelay.value > 0) { + frameBreakRefreshTimout(); + } + } else { + RxSegment = new Uint8Array(); + } + RxRemainHexdump.value = u8toHexdump(RxSegment); + + for (let i = 0; i < frames.length; i++) { + addItem(frames[i], isRX, doSend); + } + } + + const frameBreakRet = { + frameBreakSequence, + frameBreakAfterSequence, + frameBreakDelay, + frameBreakSize, + frameBreakRules, + RxRemainHexdump, + addStringMessage, + addSegment, + } const showText = ref(true); const showHex = ref(false); @@ -230,7 +410,6 @@ export const useDataViewerStore = defineStore('text-viewer', () => { const TxByteCount = ref(0); const enableFilter = ref(true); - const enableMatch = ref(false); const forceToBottom = ref(true); const filterChanged = ref(false); @@ -343,7 +522,7 @@ export const useDataViewerStore = defineStore('text-viewer', () => { return addItem(encodedStr, isRX, doSend, type); } - function addHexString(item: string, isRX: boolean = false, doSend: boolean = false, type: number = 0){ + function addHexString(item: string, isRX: boolean = false, doSend: boolean = false, type: number = 0) { if (item === "") { return addItem(new Uint8Array(0), isRX); } @@ -459,12 +638,12 @@ export const useDataViewerStore = defineStore('text-viewer', () => { dataBuf.push({ time: - "[" - + zeroPad(t.getHours(), 2) + ":" - + zeroPad(t.getMinutes(), 2) + ":" - + zeroPad(t.getSeconds(), 2) + ":" - + zeroPad(t.getMilliseconds(), 3) - + "]", + "[" + + zeroPad(t.getHours(), 2) + ":" + + zeroPad(t.getMinutes(), 2) + ":" + + zeroPad(t.getSeconds(), 2) + ":" + + zeroPad(t.getMilliseconds(), 3) + + "]", type: type, data: item, isRX: isRX, @@ -623,10 +802,10 @@ export const useDataViewerStore = defineStore('text-viewer', () => { TxByteCount, TxTotalByteCount, forceToBottom, - frameBreakSequence, - frameBreakDelay, filterChanged, + ...frameBreakRet, + /* UART */ predefinedUartBaudFrequent, uartBaudList, diff --git a/src/views/Uart.vue b/src/views/Uart.vue index 4f6a838..d3d37f2 100644 --- a/src/views/Uart.vue +++ b/src/views/Uart.vue @@ -382,7 +382,7 @@ const onUartBinaryMsg = (msg: ApiBinaryMsg) => { } /* UART_NUM_1 msg */ - store.addItem(new Uint8Array(msg.payload), true); + store.addSegment(new Uint8Array(msg.payload), true); }; const onDataFlowJsonMsg = (msg: api.ApiJsonMsg) => { diff --git a/src/views/text-data-viewer/textDataConfig.vue b/src/views/text-data-viewer/textDataConfig.vue index 5c4b1ef..ba5907a 100644 --- a/src/views/text-data-viewer/textDataConfig.vue +++ b/src/views/text-data-viewer/textDataConfig.vue @@ -48,7 +48,7 @@ -

实际波特率:{{store.uartBaudReal}}

+

实际波特率:{{ store.uartBaudReal }}

@@ -140,6 +140,74 @@ + + + + + + + + + + + + + + + + + +
优先级 +
+ 规则 + + + + +
+
+ {{ item.draggable ? index : 'NaN' }} + {{ item.name }} +
+ +
+
+ + + + +
+
+
+
+ + @@ -179,7 +247,8 @@ 新数据自动刷新 - +
数据显示刷新间隔(ms) \ No newline at end of file diff --git a/src/views/text-data-viewer/textDataViewer.vue b/src/views/text-data-viewer/textDataViewer.vue index 3435283..d9f549c 100644 --- a/src/views/text-data-viewer/textDataViewer.vue +++ b/src/views/text-data-viewer/textDataViewer.vue @@ -18,7 +18,7 @@
- + -
- add1 - add10 - add100 - add1000 - scrollToBottom + + + + + + -
+ @@ -93,7 +93,7 @@
-
+
{{ item.time }}TX-►|

{{ item.time }}未发送►|

-

@@ -164,6 +163,18 @@
+
+
+ + + +

+
+
+

+
+
+
@@ -196,14 +207,14 @@
- + - {{ showTxTotalByte ? `TX统计:${store.TxTotalByteCount}B` : `上个TX帧:${store.TxByteCount}B` }} + {{ `TX:${store.TxByteCount}B/${store.TxTotalByteCount}B` }} - + - {{ showRxTotalByte ? `RX统计:${store.RxTotalByteCount}B` : `上个RX帧:${store.RxByteCount}B` }} + {{ `RX:${store.RxByteCount}B/${store.RxTotalByteCount}B` }}
@@ -238,10 +249,9 @@ import {useDataViewerStore} from "@/stores/dataViewerStore"; import InlineSvg from "@/components/InlineSvg.vue"; import TextDataConfig from "@/views/text-data-viewer/textDataConfig.vue"; import {debouncedWatch} from "@vueuse/core"; +import {globalNotify} from "@/composables/notification"; const count = ref(0); -const showTxTotalByte = ref(false); -const showRxTotalByte = ref(false); const vuetifyVirtualScrollBarRef = ref(document.body); const vuetifyVirtualScrollContainerRef = ref(document.body); @@ -423,6 +433,9 @@ const handleScroll = (ev: Event) => { if (vuetifyVirtualScrollBarRef.value.scrollTop - lastScrollHeight < 0) { store.forceToBottom = false; } + } else if ((vuetifyVirtualScrollBarRef.value.scrollHeight - + vuetifyVirtualScrollBarRef.value.scrollTop) <= vuetifyVirtualScrollBarRef.value.clientHeight) { + store.forceToBottom = true; } lastScrollHeight = vuetifyVirtualScrollBarRef.value.scrollTop; }; @@ -444,6 +457,11 @@ function handleTextboxKeydown(ev: KeyboardEvent) { } function onSendClick() { + if (!uartInputTextBox.value) { + globalNotify("发送框无数据发送") + return; + } + if (store.acceptIncomingData) { if (isSendTextFormat.value) { store.addString(uartInputTextBox.value, false, true);