feat(uart): data frame break

This commit is contained in:
kerms 2024-05-24 16:17:41 +08:00
parent a7758ac69a
commit a2b7026f54
10 changed files with 357 additions and 39 deletions

20
package-lock.json generated
View File

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

View File

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

View File

@ -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(() => {

1
src/assets/icon/help.svg Normal file
View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 -960 960 960" fill="#5f6368"><path d="M478-240q21 0 35.5-14.5T528-290q0-21-14.5-35.5T478-340q-21 0-35.5 14.5T428-290q0 21 14.5 35.5T478-240Zm-36-154h74q0-33 7.5-52t42.5-52q26-26 41-49.5t15-56.5q0-56-41-86t-97-30q-57 0-92.5 30T342-618l66 26q5-18 22.5-39t53.5-21q32 0 48 17.5t16 38.5q0 20-12 37.5T506-526q-44 39-54 59t-10 73Zm38 314q-83 0-156-31.5T197-197q-54-54-85.5-127T80-480q0-83 31.5-156T197-763q54-54 127-85.5T480-880q83 0 156 31.5T763-763q54 54 85.5 127T880-480q0 83-31.5 156T763-197q-54 54-127 85.5T480-80Zm0-80q134 0 227-93t93-227q0-134-93-227t-227-93q-134 0-227 93t-93 227q0 134 93 227t227 93Zm0-320Z"/></svg>

After

Width:  |  Height:  |  Size: 668 B

View File

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

View File

@ -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")},

View File

@ -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);
}
@ -623,10 +802,10 @@ export const useDataViewerStore = defineStore('text-viewer', () => {
TxByteCount,
TxTotalByteCount,
forceToBottom,
frameBreakSequence,
frameBreakDelay,
filterChanged,
...frameBreakRet,
/* UART */
predefinedUartBaudFrequent,
uartBaudList,

View File

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

View File

@ -48,7 +48,7 @@
</el-select>
</div>
</el-form-item>
<p class="text-xs">实际波特率:{{store.uartBaudReal}}</p>
<p class="text-xs">实际波特率:{{ store.uartBaudReal }}</p>
<el-form-item label="数据位" class="mb-2">
<el-select v-model="store.uartConfig.data_bits" :teleported="false" placeholder="Select">
@ -140,6 +140,74 @@
</el-collapse-item>
<el-collapse-item name="2">
<template #title>
断帧策略
</template>
<VueDraggable v-model="store.frameBreakRules" target="tbody" handle=".sort-target:not(:first-child)"
:animation="150"
:on-move="checkMove">
<table class="w-full bg-white">
<thead>
<tr class="text-sm h-7">
<th>优先级</th>
<th>
<div class="flex justify-center">
规则
<el-tooltip placement="top" effect="light">
<template #content>
<p>超时=-1 禁用超时断帧</p>
<p>超时=0 当机立断收到任何数据都视为完整数据</p>
<p>匹配断后典型\n的场景</p>
<p>匹配断前用于有特殊帧头的场景</p>
<p>固定字节断帧传输大量数据比如可以每隔1024字节断帧方便查看数据</p>
</template>
<InlineSvg name="help" class="w-4 text-gray-500 cursor-help"></InlineSvg>
</el-tooltip>
</div>
</th>
<th></th>
</tr>
</thead>
<tbody class="text-xs text-center">
<tr v-for="(item, index) in store.frameBreakRules" :key="index"
:class="item.draggable ? '' : 'cursor-no-drop'">
<td :class="item.draggable ? 'sort-target' : 'cursor-no-drop'">
{{ item.draggable ? index : 'NaN' }}
</td>
<td :class="item.draggable ? 'sort-target' : 'cursor-no-drop'">{{ item.name }}</td>
<td>
<div v-if="item.type === 'number'">
<el-input-number v-model="item.ref" :min="item.min || 0" size="small" style="width: 100px"/>
</div>
<div v-else>
<el-input class="break-input" v-model="item.ref" placeholder="文本;支持\n\x" size="small"
style="width: 100px">
<template #prepend>
<el-button size="small" @click="store.frameBreakAfterSequence = false">
<span
:class="store.frameBreakAfterSequence ? 'text-gray-400' : 'text-blue-400 font-bold'">
</span>
</el-button>
</template>
<template #append>
<el-button size="small" @click="store.frameBreakAfterSequence = true">
<span
:class="store.frameBreakAfterSequence ? 'text-blue-400 font-bold' : 'text-gray-300'">
</span>
</el-button>
</template>
</el-input>
</div>
</td>
</tr>
</tbody>
</table>
</VueDraggable>
</el-collapse-item>
<el-collapse-item name="3">
<template #title>
其他
</template>
@ -179,7 +247,8 @@
<el-checkbox border v-model="store.dataFilterAutoUpdate">新数据自动刷新</el-checkbox>
</el-tooltip>
<el-tooltip content="提高间隔可减少CPU资源的使用" placement="right" effect="light" :show-after="500">
<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
@ -255,17 +324,20 @@
</template>
<script setup lang="ts">
import {VueDraggable} from 'vue-draggable-plus'
import {ref} from "vue";
import {useDataViewerStore} from "@/stores/dataViewerStore";
import {useWsStore} from "@/stores/websocket";
import {globalNotify} from "@/composables/notification";
import {ControlEvent} from "@/api";
import type {MoveEvent} from "sortablejs";
import InlineSvg from "@/components/InlineSvg.vue";
const store = useDataViewerStore()
const wsStore = useWsStore()
const collapseActiveName = ref(["1", "2"])
const collapseActiveName = ref(["1", "2", "3"])
const uartCustomBaud = ref(9600)
const uartCustomBaud = ref(114514)
const uartDataBitsOptions = [
{
@ -318,6 +390,12 @@ const onUseCustomUartBaud = () => {
}
}
function checkMove(event: MoveEvent) {
// Find index of related element
const toIndex: number = Array.from(event.to.children).indexOf(event.related);
return !!store.frameBreakRules[toIndex].draggable;
}
</script>
<style scoped>
@ -337,4 +415,21 @@ const onUseCustomUartBaud = () => {
transition: all 0s; /* Customize the duration and easing */
}
.sortable-chosen {
background-color: var(--el-color-primary-light-9);
}
.sort-target {
cursor: move;
}
tr td {
@apply p-1;
}
.break-input :deep(.el-input-group__prepend), .break-input :deep(.el-input-group__append) {
background-color: unset;
@apply p-0 min-w-6
}
</style>

View File

@ -18,7 +18,7 @@
</el-popover>
<div class="flex">
<el-checkbox size="small" v-model="store.forceToBottom" label="强制滚动至底部" border/>
<el-checkbox size="small" v-model="store.forceToBottom" label="自动滚动至底部" border/>
<el-tooltip
class="box-item"
effect="light"
@ -60,19 +60,19 @@
</div>
<div class="flex">
<el-button size="small" @click="addItem(1)">add1</el-button>
<el-button size="small" @click="addItem(10)">add10</el-button>
<el-button size="small" @click="addItem(100)">add100</el-button>
<el-button size="small" @click="addItem(1000)">add1000</el-button>
<el-button size="small" @click="scrollToBottom">scrollToBottom</el-button>
<!-- <div class="flex">-->
<!-- <el-button size="small" @click="addItem(1)">add1</el-button>-->
<!-- <el-button size="small" @click="addItem(10)">add10</el-button>-->
<!-- <el-button size="small" @click="addItem(100)">add100</el-button>-->
<!-- <el-button size="small" @click="addItem(1000)">add1000</el-button>-->
<!-- <el-button size="small" @click="scrollToBottom">scrollToBottom</el-button>-->
<!-- <el-button @click="toggleAutoBottom">autoBot: {{ forceToBottom }}</el-button>-->
<!-- <el-checkbox size="small" v-model="store.forceToBottom" :label="'autoBot:' + store.forceToBottom" border></el-checkbox>-->
<!-- <el-button>{{ count }}, {{ items.length }}, {{ vuetifyVirtualScrollBarRef.scrollTop }},-->
<!-- {{ vuetifyVirtualScrollBarRef.clientHeight }}, {{ vuetifyVirtualScrollBarRef.scrollHeight }}-->
<!-- </el-button>-->
<!-- <el-button @click="updateScroll">{{ scrollTop }}, {{ clientHeight }}, {{ scrollHeight }}</el-button>-->
</div>
<!-- </div>-->
<!-- <div>-->
<!-- <el-popover-->
@ -93,7 +93,7 @@
<!-- </div>-->
</div>
<div class="flex flex-grow overflow-hidden border-2 scroll-m-2">
<div class="flex flex-grow overflow-hidden border-2 rounded scroll-m-2">
<v-virtual-scroll
v-if="store.showVirtualScroll"
:items="store.dataFiltered"
@ -145,7 +145,6 @@
<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>
@ -164,6 +163,18 @@
</v-virtual-scroll>
</div>
<div class="shrink-0 flex max-h-14 mt-0.5 text-xs">
<div class="flex shrink-0">
<el-tooltip content="未满足断帧规则的数据(如:未超时),暂时实时显示在此区域。" effect="light">
<InlineSvg name="help" class="w-3.5 h-3.5 text-gray-500 cursor-help"></InlineSvg>
</el-tooltip>
<p></p>
</div>
<div class="p-0.5 border-2 rounded w-full overflow-auto font-mono text-nowrap">
<p v-html="store.RxRemainHexdump"></p>
</div>
</div>
<div class="shrink-0 min-h-6 flex gap-2 justify-between overflow-y-scroll">
<div class="flex gap-2">
<el-link @click="clearSendInput">
@ -196,14 +207,14 @@
</el-link>
</div>
<div class="flex gap-2">
<el-link @click="showTxTotalByte = !showTxTotalByte">
<el-link>
<el-tag class="font-mono font-bold" size="small">
{{ showTxTotalByte ? `TX统计:${store.TxTotalByteCount}B` : `上个TX帧:${store.TxByteCount}B` }}
{{ `TX:${store.TxByteCount}B/${store.TxTotalByteCount}B` }}
</el-tag>
</el-link>
<el-link type="success" @click="showRxTotalByte = !showRxTotalByte">
<el-link type="success">
<el-tag class="font-mono font-bold" size="small" type="success">
{{ showRxTotalByte ? `RX统计:${store.RxTotalByteCount}B` : `上个RX帧:${store.RxByteCount}B` }}
{{ `RX:${store.RxByteCount}B/${store.RxTotalByteCount}B` }}
</el-tag>
</el-link>
<div class="flex align-center">
@ -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);