);
}
-function SetSerialButton({ handleClickOpen }) {
- return (
-
- )
-}
-
-function showSerialPorts() {
- // Updates the ports once the SerialPort library returns
- // the promised list
- return SerialPort.list().then((portsLocal: any, err: any) => {
- if (err) {
- ports = [{ path: "Error." }];
- } else {
- if (portsLocal.length === 0) {
- ports = portsLocal;
- }
- else {
- ports = portsLocal;
- }
- }
- return ports;
- })
-}
-
diff --git a/src/Components/Spectrum.js b/src/Components/Spectrum.js
index 4fd5217..c62eaad 100644
--- a/src/Components/Spectrum.js
+++ b/src/Components/Spectrum.js
@@ -232,6 +232,12 @@ export default class Spectrum extends React.Component{
for (var i = 0; i < chart.data.datasets.length; i++){
chart.data.datasets[i].borderWidth = GlobalSettings.global.lineThickness;
chart.data.datasets[i].pointRadius = GlobalSettings.global.pointRadius;
+ // Atualiza o rótulo usando apenas o nome parseado, sem unidade
+ let lbl = (i + 1).toString();
+ if (Array.isArray(SerialDataObject.varNames) && typeof SerialDataObject.varNames[i] !== 'undefined' && SerialDataObject.varNames[i] !== null) {
+ lbl = SerialDataObject.varNames[i];
+ }
+ chart.data.datasets[i].label = lbl;
}
// Compute the sample rate
diff --git a/src/Components/UDPSelect.tsx b/src/Components/UDPSelect.tsx
new file mode 100644
index 0000000..2374360
--- /dev/null
+++ b/src/Components/UDPSelect.tsx
@@ -0,0 +1,45 @@
+import * as React from 'react';
+import TextField from '@mui/material/TextField';
+import Typography from '@mui/material/Typography';
+import { GlobalSettings } from "../Utils/GlobalSettings.js";
+import { SerialDataObject, StopUDP } from '../Utils/SerialData';
+
+const titleFs = GlobalSettings.style.menuFs;
+
+const selectStyles = {
+ marginTop: '8px',
+ marginBottom: '8px'
+};
+
+export function UDPPortSelect() {
+ const [port, setPort] = React.useState(SerialDataObject.udpPort || 5000);
+
+ return (
+
+ {
+ const val = parseInt(e.target.value);
+ if (!isNaN(val)) {
+ // Clamp to valid UDP port range
+ const clamped = Math.min(65535, Math.max(1, val));
+
+ // If connected, close the connection so the user must hit Play again
+ if (SerialDataObject.udpSocket) {
+ StopUDP();
+ }
+
+ setPort(clamped);
+ SerialDataObject.udpPort = clamped;
+ }
+ }}
+ InputProps={{ inputProps: { min: 1, max: 65535, step: 1 }, style: { fontSize: GlobalSettings.style.menuFs } }}
+ />
+
+ );
+}
\ No newline at end of file
diff --git a/src/Utils/DataRecording.js b/src/Utils/DataRecording.js
index 07e3a1e..41ecaac 100644
--- a/src/Utils/DataRecording.js
+++ b/src/Utils/DataRecording.js
@@ -44,6 +44,16 @@ export function writeDataIfRecording(data){
if(GlobalSettings.record.recording && GlobalSettings.record.directory !== null){
let filename = path.join(GlobalSettings.record.directory,GlobalSettings.record.outputFilename);
let dataWithNewline = data + "\r\n";
+ if (GlobalSettings.monitor && GlobalSettings.monitor.showTimestamp) {
+ const d = new Date();
+ const pad = (n, w=2) => String(n).padStart(w,'0');
+ const h = pad(d.getHours());
+ const m = pad(d.getMinutes());
+ const s = pad(d.getSeconds());
+ const ms = pad(d.getMilliseconds(),3);
+ const ts = `[${h}:${m}:${s}.${ms}]`;
+ dataWithNewline = `${ts} ${data}\r\n`;
+ }
ipcRenderer.invoke('fse','appendFile',filename,dataWithNewline);
}
}
diff --git a/src/Utils/DataUtils.js b/src/Utils/DataUtils.js
index 64d2b5c..16c2c9a 100644
--- a/src/Utils/DataUtils.js
+++ b/src/Utils/DataUtils.js
@@ -33,21 +33,41 @@ export function nextPowerOf2(x){
}
export function autoResize(){
-
if(GlobalSettings.timeSeries.autoScale && !SerialDataObject.pauseFlag){
- // Min and max of y
- var ymin = Math.min(...SerialDataObject.data.flat());
- var ymax = Math.max(...SerialDataObject.data.flat());
- // Give it some standardized space
- var ymid = (ymax+ymin)/2;
- var dy = ymax - ymid;
- var dyNext = nextPowerOf2(dy);
- var nearestValue = 1;
- if(dy > 2){
- nearestValue = 5;
+ // Collect values only from visible variables
+ const dataRows = SerialDataObject.data;
+ if(!Array.isArray(dataRows) || dataRows.length === 0){ return; }
+ const nvars = dataRows[dataRows.length - 1].length || 0;
+ let vis = SerialDataObject.visibleVars;
+ // Initialize visibility if missing or mismatched
+ if(!Array.isArray(vis) || vis.length !== nvars){
+ vis = new Array(nvars).fill(true);
+ SerialDataObject.visibleVars = vis;
+ }
+
+ const values = [];
+ for(const row of dataRows){
+ for(let j=0;j= SerialDataObject.bufferSize) {
// If the buffer is full, remove the first line of the raw data
SerialDataObject.rawData.shift();
+ SerialDataObject.rawDataTime.shift();
}
// If we're recording, write each data row to the file
writeDataIfRecording(data);
- // Now parse the numeric data
+ // Now parse tokens supporting "name(unit): value" or plain numeric
var splitData = data.split(/\s+|,\s+/);
- var nums = splitData.map(parseFloat);
+ var namesParsed = [];
+ var unitsParsed = [];
+ var nums = [];
+ for (let tok of splitData) {
+ if (!tok) { continue; }
+ const colonIdx = tok.indexOf(":");
+ if (colonIdx !== -1) {
+ const namePart = tok.slice(0, colonIdx).trim();
+ const valPart = tok.slice(colonIdx + 1).trim();
+ let name = namePart;
+ let unit = null;
+ const m = namePart.match(/^(.*?)\s*\((.*?)\)\s*$/);
+ if (m) {
+ name = m[1].trim();
+ unit = m[2].trim();
+ }
+ namesParsed.push(name);
+ unitsParsed.push(unit);
+ nums.push(parseFloat(valPart));
+ } else {
+ namesParsed.push(null);
+ unitsParsed.push(null);
+ nums.push(parseFloat(tok));
+ }
+ }
var t = 0;
if (GlobalSettings.global.firstColumnTime) {
t = nums[0];
nums = nums.slice(1, nums.length);
+ namesParsed = namesParsed.slice(1, namesParsed.length);
+ unitsParsed = unitsParsed.slice(1, unitsParsed.length);
}
else {
t = performance.now();
@@ -181,6 +235,18 @@ function serialSetup(port) {
SerialDataObject.data.push(nums);
SerialDataObject.sampleHistory.push(SerialDataObject.Iter);
SerialDataObject.timeHistory.push(t);
+ // Update variable names/units (keep latest non-null names)
+ const nvars = nums.length;
+ const newNames = new Array(nvars);
+ const newUnits = new Array(nvars);
+ for (let i = 0; i < nvars; i++) {
+ const nm = namesParsed[i];
+ const un = unitsParsed[i];
+ newNames[i] = (nm && nm.length > 0) ? nm : (SerialDataObject.varNames[i] || null);
+ newUnits[i] = (un && un.length > 0) ? un : (SerialDataObject.varUnits[i] || null);
+ }
+ SerialDataObject.varNames = newNames;
+ SerialDataObject.varUnits = newUnits;
}
var n = SerialDataObject.data.length;
@@ -214,3 +280,320 @@ function idxData(n) {
return (idxVec);
}
+// ---------------------- File Import ---------------------- //
+const { ipcRenderer } = window.require('electron');
+const path = window.require('path');
+
+function parseTimestampTag(line) {
+ // Matches [hh:mm:ss.mmm] prefix
+ const m = line.match(/^\[(\d{2}):(\d{2}):(\d{2})\.(\d{3})\]\s+(.*)$/);
+ if (!m) { return { ts: null, text: line }; }
+ const hh = parseInt(m[1], 10);
+ const mm = parseInt(m[2], 10);
+ const ss = parseInt(m[3], 10);
+ const ms = parseInt(m[4], 10);
+ const rest = m[5];
+ const now = new Date();
+ const d = new Date(now.getFullYear(), now.getMonth(), now.getDate(), hh, mm, ss, ms);
+ return { ts: d.getTime(), text: rest };
+}
+
+function processDataLine(data, tsOverride = null) {
+ // Push raw line and timestamp (either override or wall clock)
+ SerialDataObject.rawData.push(data);
+ SerialDataObject.rawDataTime.push(tsOverride !== null ? tsOverride : Date.now());
+ if (SerialDataObject.rawData.length >= SerialDataObject.bufferSize) {
+ SerialDataObject.rawData.shift();
+ SerialDataObject.rawDataTime.shift();
+ }
+
+ // Parse tokens supporting "name(unit): value" or plain numeric
+ var splitData = data.split(/\s+|,\s+/);
+ var namesParsed = [];
+ var unitsParsed = [];
+ var nums = [];
+ for (let tok of splitData) {
+ if (!tok) { continue; }
+ const colonIdx = tok.indexOf(":");
+ if (colonIdx !== -1) {
+ const namePart = tok.slice(0, colonIdx).trim();
+ const valPart = tok.slice(colonIdx + 1).trim();
+ let name = namePart;
+ let unit = null;
+ const m = namePart.match(/^(.*?)\s*\((.*?)\)\s*$/);
+ if (m) {
+ name = m[1].trim();
+ unit = m[2].trim();
+ }
+ namesParsed.push(name);
+ unitsParsed.push(unit);
+ nums.push(parseFloat(valPart));
+ } else {
+ namesParsed.push(null);
+ unitsParsed.push(null);
+ nums.push(parseFloat(tok));
+ }
+ }
+ var t = 0;
+ if (GlobalSettings.global.firstColumnTime) {
+ t = nums[0];
+ nums = nums.slice(1, nums.length);
+ namesParsed = namesParsed.slice(1, namesParsed.length);
+ unitsParsed = unitsParsed.slice(1, unitsParsed.length);
+ } else {
+ t = performance.now();
+ }
+
+ if (nums.every((value) => { return !isNaN(value) })) {
+ SerialDataObject.data.push(nums);
+ SerialDataObject.Iter += 1;
+ SerialDataObject.sampleHistory.push(SerialDataObject.Iter);
+ SerialDataObject.timeHistory.push(t);
+ const nvars = nums.length;
+ const newNames = new Array(nvars);
+ const newUnits = new Array(nvars);
+ for (let i = 0; i < nvars; i++) {
+ const nm = namesParsed[i];
+ const un = unitsParsed[i];
+ newNames[i] = (nm && nm.length > 0) ? nm : (SerialDataObject.varNames[i] || null);
+ newUnits[i] = (un && un.length > 0) ? un : (SerialDataObject.varUnits[i] || null);
+ }
+ SerialDataObject.varNames = newNames;
+ SerialDataObject.varUnits = newUnits;
+ }
+
+ // Buffer management
+ var n = SerialDataObject.data.length;
+ if (n > SerialDataObject.bufferSize) {
+ const resize = (v) => v.slice(v.length - SerialDataObject.bufferSize, v.length);
+ SerialDataObject.data = resize(SerialDataObject.data);
+ SerialDataObject.sampleHistory = resize(SerialDataObject.sampleHistory);
+ SerialDataObject.timeHistory = resize(SerialDataObject.timeHistory);
+ if (!GlobalSettings.timeSeries.scroll) {
+ SerialDataObject.data = [];
+ SerialDataObject.sampleHistory = [];
+ SerialDataObject.timeHistory = [];
+ }
+ }
+ SerialDataObject.dataIdx = idxData(SerialDataObject.data.length);
+}
+
+export async function LoadFromFileDialog() {
+ const result = await ipcRenderer.invoke('dialog', 'showOpenDialog', { properties: ['openFile'], filters: [{ name: 'Text', extensions: ['txt'] }, { name: 'All Files', extensions: ['*'] }] });
+ if (!result.canceled && result.filePaths && result.filePaths.length > 0) {
+ await LoadFromFile(result.filePaths[0]);
+ }
+}
+
+export async function LoadFromFile(filePath) {
+ try {
+ // Read content
+ const content = await ipcRenderer.invoke('fse', 'readFile', filePath, 'utf8');
+ const lines = content.split(/\r?\n/).filter(l => l.length > 0);
+
+ // Reset buffers
+ SerialDataObject.dataSource = 'file';
+ SerialDataObject.inputMode = 'serial';
+ SerialDataObject.rawData = [];
+ SerialDataObject.rawDataTime = [];
+ SerialDataObject.data = [];
+ SerialDataObject.dataIdx = [];
+ SerialDataObject.pauseFlag = false;
+ SerialDataObject.Iter = 0;
+ SerialDataObject.sampleHistory = [];
+ SerialDataObject.timeHistory = [];
+ SerialDataObject.varNames = [];
+ SerialDataObject.varUnits = [];
+ SerialDataObject.port = { path: null, friendlyName: `File: ${path.basename(filePath)}` };
+
+ // Process lines
+ for (const line of lines) {
+ const { ts, text } = parseTimestampTag(line);
+ processDataLine(text, ts);
+ }
+ } catch (e) {
+ console.log('Failed to load file:', e);
+ }
+}
+// ---------------------- UDP Support ---------------------- //
+export function StartUDP(portNumber) {
+ SerialDataObject.inputMode = 'udp';
+
+ // Clear data buffers and flags
+ SerialDataObject.rawData = [];
+ SerialDataObject.rawDataTime = [];
+ SerialDataObject.data = [];
+ SerialDataObject.dataIndex = [];
+ SerialDataObject.pauseFlag = false;
+ SerialDataObject.Iter = 0;
+ SerialDataObject.sampleHistory = [];
+ SerialDataObject.timeHistory = [];
+
+ // Close serial if open
+ if (SerialDataObject.serialObj !== null) {
+ try {
+ SerialDataObject.serialObj.close();
+ } catch (e) {
+ console.log('Error closing serial before UDP start:', e);
+ }
+ SerialDataObject.serialObj = null;
+ }
+
+ // Close previous UDP socket if exists
+ if (SerialDataObject.udpSocket !== null) {
+ try {
+ SerialDataObject.udpSocket.close();
+ } catch (e) {
+ console.log('Error closing previous UDP socket:', e);
+ }
+ SerialDataObject.udpSocket = null;
+ }
+
+ // Create UDP socket and bind
+ let toastId = toast(`Starting UDP listener on port ${portNumber}`);
+ try {
+ const socket = dgram.createSocket('udp4');
+ SerialDataObject.udpSocket = socket;
+ SerialDataObject.udpPort = portNumber;
+
+ socket.on('error', (err) => {
+ toast.dismiss(toastId);
+ toast.error(`UDP socket error on port ${portNumber}\n\n${err}`);
+ console.log('UDP socket error:', err);
+ });
+
+ socket.on('message', (msg/* Buffer */, rinfo) => {
+ if (SerialDataObject.pauseFlag) { return; }
+ let dataStr = '';
+ try {
+ dataStr = msg.toString('utf8').trim();
+ } catch (e) {
+ // ignore malformed buffer
+ return;
+ }
+ // Allow multiple lines in one datagram
+ const lines = dataStr.split(/\r?\n/).filter(l => l.length > 0);
+ for (const line of lines) {
+ addUdpData(line);
+ }
+ });
+
+ socket.bind(portNumber, () => {
+ console.log('UDP socket bound on port', portNumber);
+ toast.update(toastId, { render: `UDP listening on port ${portNumber}`, type: toast.TYPE.INFO, autoClose: 2000 });
+ });
+
+ } catch (e) {
+ toast.dismiss(toastId);
+ toast.error(`Failed to start UDP on port ${portNumber}\n\n${e}`);
+ console.log('Failed to start UDP:', e);
+ }
+
+ function addUdpData(data) {
+ // Decimation
+ if (decIndex >= GlobalSettings.global.decimation) {
+ decIndex = 1;
+ } else {
+ decIndex += 1;
+ return;
+ }
+
+ SerialDataObject.Iter += 1;
+ if (SerialDataObject.Iter % SerialDataObject.NsampleRateUpdate === 0) {
+ if (SerialDataObject.sampleHistory.length > 1) {
+ var deltaT = SerialDataObject.timeHistory[SerialDataObject.timeHistory.length - 1] - SerialDataObject.timeHistory[0];
+ var samples = SerialDataObject.sampleHistory[SerialDataObject.sampleHistory.length - 1] - SerialDataObject.sampleHistory[0];
+ SerialDataObject.sampleRate = samples / deltaT * 1000;
+ }
+ }
+
+ SerialDataObject.rawData.push(data);
+ SerialDataObject.rawDataTime.push(Date.now());
+ if (SerialDataObject.rawData.length >= SerialDataObject.bufferSize) {
+ SerialDataObject.rawData.shift();
+ SerialDataObject.rawDataTime.shift();
+ }
+
+ writeDataIfRecording(data);
+
+ var splitData = data.split(/\s+|,\s+/);
+ var namesParsed = [];
+ var unitsParsed = [];
+ var nums = [];
+ for (let tok of splitData) {
+ if (!tok) { continue; }
+ const colonIdx = tok.indexOf(":");
+ if (colonIdx !== -1) {
+ const namePart = tok.slice(0, colonIdx).trim();
+ const valPart = tok.slice(colonIdx + 1).trim();
+ let name = namePart;
+ let unit = null;
+ const m = namePart.match(/^(.*?)\s*\((.*?)\)\s*$/);
+ if (m) {
+ name = m[1].trim();
+ unit = m[2].trim();
+ }
+ namesParsed.push(name);
+ unitsParsed.push(unit);
+ nums.push(parseFloat(valPart));
+ } else {
+ namesParsed.push(null);
+ unitsParsed.push(null);
+ nums.push(parseFloat(tok));
+ }
+ }
+ var t = 0;
+ if (GlobalSettings.global.firstColumnTime) {
+ t = nums[0];
+ nums = nums.slice(1, nums.length);
+ namesParsed = namesParsed.slice(1, namesParsed.length);
+ unitsParsed = unitsParsed.slice(1, unitsParsed.length);
+ } else {
+ t = performance.now();
+ }
+
+ if (nums.every((value) => { return !isNaN(value) })) {
+ SerialDataObject.data.push(nums);
+ SerialDataObject.sampleHistory.push(SerialDataObject.Iter);
+ SerialDataObject.timeHistory.push(t);
+ const nvars = nums.length;
+ const newNames = new Array(nvars);
+ const newUnits = new Array(nvars);
+ for (let i = 0; i < nvars; i++) {
+ const nm = namesParsed[i];
+ const un = unitsParsed[i];
+ newNames[i] = (nm && nm.length > 0) ? nm : (SerialDataObject.varNames[i] || null);
+ newUnits[i] = (un && un.length > 0) ? un : (SerialDataObject.varUnits[i] || null);
+ }
+ SerialDataObject.varNames = newNames;
+ SerialDataObject.varUnits = newUnits;
+ }
+
+ var n = SerialDataObject.data.length;
+ if (n > SerialDataObject.bufferSize) {
+ const resize = (v) => v.slice(v.length - SerialDataObject.bufferSize, v.length);
+ SerialDataObject.data = resize(SerialDataObject.data);
+ SerialDataObject.sampleHistory = resize(SerialDataObject.sampleHistory);
+ SerialDataObject.timeHistory = resize(SerialDataObject.timeHistory);
+ if (!GlobalSettings.timeSeries.scroll) {
+ SerialDataObject.data = [];
+ SerialDataObject.sampleHistory = [];
+ SerialDataObject.timeHistory = [];
+ }
+ }
+ SerialDataObject.dataIdx = idxData(SerialDataObject.data.length);
+ }
+}
+
+export function StopUDP() {
+ if (SerialDataObject.udpSocket) {
+ try {
+ SerialDataObject.udpSocket.close();
+ } catch (e) {
+ console.log('Error closing UDP socket:', e);
+ }
+ SerialDataObject.udpSocket = null;
+ SerialDataObject.udpPort = null;
+ }
+}
+