import {defineStore} from "pinia";
import {
computed,
type Ref,
type ShallowReactive,
ref,
shallowReactive,
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";
import {useUartStore} from "@/stores/useUartStore";
interface IDataArchive {
time: number;
isRX: boolean;
data: Uint8Array;
}
export interface IDataBuf {
time: string;
isRX: boolean;
type: number;
data: Uint8Array;
str: string;
hex: string;
hexdump: string;
}
function decodeUtf8(u8Arr: Uint8Array) {
try {
const decoder = new TextDecoder();
const decodedText = decoder.decode(u8Arr); // Attempt to decode
return decodedText.replace(/\uFFFD/g, ''); // Remove all � characters
} catch (error) {
return "";
}
}
function escapeHTML(text: string) {
const element = document.createElement('p');
element.textContent = text;
return element.innerHTML;
}
function unescapeString(str: string): Uint8Array {
const resultArray = [];
let i = 0;
while (i < str.length) {
if (str[i] === '\\' && i + 1 < str.length) {
i++;
switch (str[i]) {
case 'n':
resultArray.push(0x0A); // LF
break;
case 'r':
resultArray.push(0x0D); // CR
break;
case 't':
resultArray.push(0x09); // Tab
break;
case 'b':
resultArray.push(0x08); // Backspace
break;
case 'v':
resultArray.push(0x0B); // Vertical Tab
break;
case '0':
resultArray.push(0x00); // Null
break;
case 'x':
if (i + 2 < str.length) {
resultArray.push(parseInt(str.substr(i + 1, 2), 16));
i += 2;
}
break;
case 'u':
if (i + 4 < str.length) {
const codePoint = parseInt(str.substr(i + 1, 4), 16);
if (codePoint < 0x80) {
resultArray.push(codePoint);
} else if (codePoint < 0x800) {
resultArray.push(192 | (codePoint >> 6));
resultArray.push(128 | (codePoint & 63));
} else if (codePoint < 0x10000) {
resultArray.push(224 | (codePoint >> 12));
resultArray.push(128 | ((codePoint >> 6) & 63));
resultArray.push(128 | (codePoint & 63));
} else {
resultArray.push(240 | (codePoint >> 18));
resultArray.push(128 | ((codePoint >> 12) & 63));
resultArray.push(128 | ((codePoint >> 6) & 63));
resultArray.push(128 | (codePoint & 63));
}
i += 4;
}
break;
default:
// This handles any escaped character that is not a special character
resultArray.push(str[i].charCodeAt(0));
break;
}
} else {
// This section now properly converts a direct character to UTF-8 when it's beyond the ASCII range
const code = str.codePointAt(i);
if (code == undefined) {
continue
}
if (code < 0x80) {
resultArray.push(code);
} else if (code < 0x800) {
resultArray.push(192 | (code >> 6));
resultArray.push(128 | (code & 63));
} else if (code < 0x10000) {
resultArray.push(224 | (code >> 12));
resultArray.push(128 | ((code >> 6) & 63));
resultArray.push(128 | (code & 63));
} else {
resultArray.push(240 | (code >> 18));
resultArray.push(128 | ((code >> 12) & 63));
resultArray.push(128 | ((code >> 6) & 63));
resultArray.push(128 | (code & 63));
i++; // Move past the second part of the surrogate pair
}
}
i++;
}
return new Uint8Array(resultArray);
}
const zeroPad = (num: number, places: number) => String(num).padStart(places, '0');
const ansi_up = new AnsiUp();
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 */
const byteToHex: string[] = new Array(256);
for (let n = 0; n <= 0xff; ++n) {
byteToHex[n] = n.toString(16).padStart(2, "0").toUpperCase();
}
function u8toHexString(buff: Uint8Array) {
const hexOctets = []; // new Array(buff.length) is even faster (preallocates necessary array size), then use hexOctets[i] instead of .push()
if (buff.length === 0) {
return ""
}
for (let i = 0; i < buff.length; ++i)
hexOctets.push(byteToHex[buff[i]]);
return hexOctets.join(" ");
}
function u8toHexdump(buffer: Uint8Array) {
const lines: string[] = [];
const bytesPerRow = 16;
for (let lineStart = 0; lineStart < buffer.length; lineStart += bytesPerRow) {
let result = lineStart.toString(16).padStart(4, '0') + ": ";
let ascii = "";
// Process each byte in the row
for (let i = 0; i < bytesPerRow; i++) {
const byteIndex = lineStart + i;
if (byteIndex >= buffer.length) {
// Pad the row if it's shorter than the full width
result += " ";
} else {
const byte = buffer[byteIndex];
result += byteToHex[byte] + " ";
// Prepare the ASCII representation, non-printable as '.'
if (byte >= 32 && byte <= 126) {
ascii += String.fromCharCode(byte);
} else {
ascii += ".";
}
}
if (i === 8) {
result += " "
}
}
result += "|" + ascii + "|" + " ".repeat(16 - ascii.length);
lines.push(result);
}
return strToHTML(escapeHTML(lines.join('\n')));
}
function strToHTML(str: string) {
return str.replace(/\n/g, '
') // Replace newline with
tag
.replace(/\t/g, ' ') // Replace tab with spaces (or you could use ' ' for single spaces)
.replace(/\r/, '')
.replace(/ /g, ' ');
}
function isArrayContained(subArray: Uint8Array, mainArray: Uint8Array,
sub_start: number = 0, main_start: number = 0) {
if (subArray.length === 0) return -1;
outerLoop: for (let i = main_start; i <= mainArray.length - subArray.length; i++) {
// Check if subArray is found starting at position i in mainArray
for (let j = sub_start; j < subArray.length; j++) {
if (mainArray[i + j] !== subArray[j]) {
continue outerLoop; // Continue the outer loop if mismatch found
}
}
return i; // subArray is found within mainArray
}
return -1;
}
const baudArr = [
{
start: 300,
count: 9,
}, {
start: 14400,
count: 9,
}
]
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) {
results.push({baud: start});
start += start;
}
}
results.sort((a, b) => {
return a.baud - b.baud;
});
}
export const useDataViewerStore = defineStore('text-viewer', () => {
const uartStore = useUartStore()
/* private value */
const predefineColors = [
'#f0f9eb',
'#ecf4ff',
'rgba(255, 69, 0, 0.68)',
'rgb(255, 120, 0)',
'hsv(51, 100, 98)',
'hsva(120, 40, 94, 0.5)',
'hsl(181, 100%, 37%)',
'hsla(209, 100%, 56%, 0.73)',
'rgba(85,155,197,0.44)',
]
const predefinedUartBaudFrequent = Object.freeze([
{
baud: 115200,
}, {
baud: 921600,
}, {
baud: 9600,
}
])
const uartBaudList: { baud: number; }[] = [];
generateBaudArr(uartBaudList);
/* public value */
const configPanelTab = ref("second");
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 frameBreakAfterSequence = ref(true);
const frameBreakSequenceNormalized = computed(() => {
return unescapeString(frameBreakSequence.value);
});
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 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));
}
});
const 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 unescaped = unescapeString(input);
addSegment(unescaped, isRX, doSend);
}
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();
}
for (let i = 0; i < frames.length; i++) {
addItem(frames[i], isRX, doSend);
}
if (RxSegment.length > 8192) {
addItem(RxSegment, isRX, doSend);
RxSegment = new Uint8Array();
}
}
const frameBreakRet = {
frameBreakSequence,
frameBreakAfterSequence,
frameBreakDelay,
frameBreakSize,
frameBreakRules,
RxRemainHexdump,
addStringMessage,
addSegment,
}
const showText = ref(true);
const showHex = ref(false);
const showHexdump = ref(false);
const enableLineWrap = ref(true);
const showTimestamp = ref(true);
const showVirtualScroll = ref(true);
const RxHexdumpColor = ref("#f0f9eb");
const RxTotalByteCount = ref(0);
const RxByteCount = ref(0);
const TxHexdumpColor = ref("#ecf4ff");
const TxTotalByteCount = ref(0);
const TxByteCount = ref(0);
let TxByteCountLocal = 0;
let TxTotalByteCountLocal = 0;
let RxByteCountLocal = 0;
let RxTotalByteCountLocal = 0;
function clearRxCounter() {
RxByteCountLocal = 0;
RxTotalByteCountLocal = 0;
}
function clearTxCounter() {
TxByteCountLocal = 0;
TxTotalByteCountLocal = 0;
}
const enableFilter = ref(true);
const forceToBottom = ref(true);
const filterChanged = ref(false);
const textPrefixValue = ref("")
const textSuffixValue = ref("\\r\\n")
const hasAddedText = computed(() => {
return textPrefixValue.value.length > 0 || textSuffixValue.value.length > 0;
});
const uartBaud = ref(115200);
const uartBaudReal = ref(115200);
const uartConfig: Ref = ref({
data_bits: 8,
parity: 0,
stop_bits: 1,
});
const filterValue = ref("");
const computedFilterValue = computed(() => {
return unescapeString(filterValue.value);
})
const computedSuffixValue = computed(() => {
return unescapeString(textSuffixValue.value);
})
const computedPrefixValue = computed(() => {
return unescapeString(textPrefixValue.value);
})
const dataBuf: IDataBuf[] = [];
const dataBufLength = ref(0);
/* actual data shown on screen */
const dataFiltered: ShallowReactive = shallowReactive([]);
const dataFilterAutoUpdate = ref(true);
const acceptIncomingData = ref(false);
// let frameBreakReady = false;
// let frameBreakTimeoutID = setTimeout(() => {
// }, 0);
debouncedWatch(computedFilterValue, () => {
refreshFilteredBuff()
}, {debounce: 300});
let batchDataUpdateIntervalID: number = -1;
const batchUpdateTime = ref(80); /* ms */
let batchStartIndex: number = 0;
watch(batchUpdateTime, () => {
if (batchDataUpdateIntervalID >= 0) {
clearInterval(batchDataUpdateIntervalID);
batchDataUpdateIntervalID = -1;
}
batchUpdate();
if (dataFilterAutoUpdate.value && batchDataUpdateIntervalID < 0) {
batchDataUpdateIntervalID = setInterval(batchUpdate, batchUpdateTime.value);
}
}, {immediate: true});
/* delayed value update, prevent quick unnecessary update */
setInterval(() => {
dataBufLength.value = dataBuf.length;
TxByteCount.value = TxByteCountLocal;
TxTotalByteCount.value = TxTotalByteCountLocal;
RxByteCount.value = RxByteCountLocal;
RxTotalByteCount.value = RxTotalByteCountLocal;
if (RxSegment.length) {
RxRemainHexdump.value = u8toHexdump(RxSegment);
} else {
RxRemainHexdump.value = "";
}
}, 200);
function addString(item: string, isRX: boolean = false, doSend: boolean = false, type: number = 0) {
console.log(item);
const unescaped = unescapeString(item);
return addItem(unescaped, isRX, doSend, type);
}
function addHexString(item: string, isRX: boolean = false, doSend: boolean = false, type: number = 0) {
if (item === "") {
return addItem(new Uint8Array(0), isRX);
}
const hexArray = item.split(' ');
// 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)));
return addItem(uint8Array, isRX, doSend, type);
}
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() {
/* 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) {
if (!acceptIncomingData.value && isRX) {
return;
}
const t = new Date();
// dataArchive.push({
// time: t.getMilliseconds(),
// isRX: isRX,
// data: u8arr,
// });
if (isRX) {
RxTotalByteCountLocal += item.length;
RxByteCountLocal = item.length;
} else {
/* append prefix and suffix */
if (computedPrefixValue.value.length || computedSuffixValue.value.length) {
const newArr = new Uint8Array(computedPrefixValue.value.length +
computedSuffixValue.value.length + item.length);
newArr.set(computedPrefixValue.value);
newArr.set(item, computedPrefixValue.value.length);
newArr.set(computedSuffixValue.value, computedPrefixValue.value.length + item.length);
item = newArr;
}
if (acceptIncomingData.value) {
if (doSend) {
/* INFO: hard coded for the moment */
uart_send_msg(item, uartStore.uartNum);
}
} else {
type = 1;
}
TxTotalByteCountLocal += item.length;
TxByteCountLocal = item.length;
}
let str: string;
str = decodeUtf8(item);
str = escapeHTML(str);
str = strToHTML(str);
/* unescape data \n */
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 */
str = ansi_up.ansi_to_html("\x1b[0m" + str);
}
dataBuf.push({
time:
"["
+ zeroPad(t.getHours(), 2) + ":"
+ zeroPad(t.getMinutes(), 2) + ":"
+ zeroPad(t.getSeconds(), 2) + ":"
+ zeroPad(t.getMilliseconds(), 3)
+ "]",
type: type,
data: item,
isRX: isRX,
str: str,
hex: u8toHexString(item),
hexdump: u8toHexdump(item),
});
}
function clearByteCount(isRX: boolean) {
if (isRX) {
RxTotalByteCountLocal = 0;
} else {
TxTotalByteCountLocal = 0;
}
}
function clearDataBuff() {
clearFilteredBuff();
dataBuf.length = 0;
dataBufLength.value = 0;
batchStartIndex = 0;
RxByteCountLocal = 0;
RxTotalByteCountLocal = 0;
TxByteCountLocal = 0;
TxTotalByteCountLocal = 0;
RxSegment = new Uint8Array();
RxRemainHexdump.value = "";
}
function clearFilteredBuff() {
/* prevent virtual scroll not displaying new data */
showVirtualScroll.value = !showVirtualScroll.value;
/* the actual clear buff clear */
dataFiltered.length = 0;
}
function refreshFilteredBuff() {
clearFilteredBuff()
for (const item of dataBuf) {
const index = isArrayContained(computedFilterValue.value, item.data);
if (index >= 0 || filterValue.value.length === 0) {
dataFiltered.push(item);
}
}
filterChanged.value = true;
}
function setUartBaud(baud: number) {
uartBaudReal.value = baud;
for (let i = 0; i < uartBaudList.length; i++) {
const difference = Math.abs(uartBaudList[i].baud - baud);
const percentageDifference = (difference / baud);
if (percentageDifference !== 0 && percentageDifference < 0.001) {
uartBaud.value = uartBaudList[i].baud;
return;
}
}
uartBaud.value = baud;
}
return {
addItem,
addString,
addHexString,
clearFilteredBuff,
clearDataBuff,
refreshFilteredBuff,
softRefreshFilterBuf,
textSuffixValue,
textPrefixValue,
hasAddedText,
clearByteCount,
dataBufLength,
configPanelTab,
configPanelShow,
quickAccessPanelShow,
dataFiltered,
dataFilterAutoUpdate,
filterValue,
batchUpdateTime,
acceptIncomingData,
showVirtualScroll,
enableAnsiDecode,
showHex,
showHexdump,
showText,
showTimestamp,
enableLineWrap,
RxHexdumpColor,
TxHexdumpColor,
predefineColors,
RxByteCount,
RxTotalByteCount,
TxByteCount,
TxTotalByteCount,
clearRxCounter,
clearTxCounter,
forceToBottom,
filterChanged,
...frameBreakRet,
/* UART */
predefinedUartBaudFrequent,
uartBaudList,
uartBaud,
uartConfig,
uartBaudReal,
setUartBaud,
}
});