From 47d4e70f50363c2087d63f98aee16e21b05613a5 Mon Sep 17 00:00:00 2001 From: Shurong Cao <170531907+CAOShurong@users.noreply.github.com> Date: Wed, 12 Aug 2026 16:49:43 +0800 Subject: [PATCH] Export text telemetry values to CSV --- server/package.json | 3 +- server/test/session-csv.test.js | 75 +++++++++++++++++++++++ server/www/utils/import_export/session.js | 23 +++++-- 3 files changed, 94 insertions(+), 7 deletions(-) create mode 100644 server/test/session-csv.test.js diff --git a/server/package.json b/server/package.json index f52d3d9..fbfc42e 100644 --- a/server/package.json +++ b/server/package.json @@ -7,7 +7,8 @@ "scripts": { "test": "echo \"Error: no test specified\" && exit 1", "start": "node main.js", - "make": "npm i -g pkg && pkg . && chmod -R 777 build" + "make": "npm i -g pkg && pkg . && chmod -R 777 build", + "test:csv": "node test/session-csv.test.js" }, "author": "", "license": "MIT", diff --git a/server/test/session-csv.test.js b/server/test/session-csv.test.js new file mode 100644 index 0000000..f24634e --- /dev/null +++ b/server/test/session-csv.test.js @@ -0,0 +1,75 @@ +const assert = require("assert"); +const fs = require("fs"); +const path = require("path"); +const vm = require("vm"); + +const www = path.join(__dirname, "..", "www"); + +const uPlot = { + join: tables => { + const timestamps = Array.from(new Set(tables.flatMap(table => table[0]))).sort((a, b) => a - b); + return [timestamps].concat(tables.map(table => { + const values = new Map(table[0].map((timestamp, index) => [timestamp, table[1][index]])); + return timestamps.map(timestamp => values.has(timestamp) ? values.get(timestamp) : null); + })); + } +}; + +function exportCSV(app) { + let savedFile; + const context = { + app, + uPlot, + saveFile: (content, name) => { savedFile = { content, name }; }, + buildFileName: (name, extension) => `${name}.${extension}`, + module: { exports: {} } + }; + + vm.runInNewContext( + `${fs.readFileSync(path.join(www, "utils", "import_export", "session.js"), "utf8")}\nmodule.exports = exportSessionCSV;`, + context + ); + context.module.exports(); + return savedFile; +} + +const defaultExportApp = { + csvCellSeparator: ",", + csvDecimalSeparator: ".", + csvExportView: true, + telemetries: { + temperature: { type: "default", unit: "C", data: [[1000, 2000], [20.5, 21.5]] }, + state: { type: "text", data: [[1500, 2500], ["warming", 'ready, "steady"']] } + } +}; +const defaultExport = exportCSV(defaultExportApp); + +assert.strictEqual(defaultExport.name, "session.csv"); +assert.strictEqual(defaultExportApp.csvExportView, false); +assert.strictEqual(defaultExport.content, [ + "timestamp(ms),temperature (C),state,", + '"1000","20.5",,', + '"1500",,"warming",', + '"2000","21.5",,', + '"2500",,"ready, ""steady""",', + "" +].join("\n")); + +const commaDecimalExport = exportCSV({ + csvCellSeparator: ";", + csvDecimalSeparator: ",", + csvExportView: true, + telemetries: { + temperature: { type: "default", data: [[1000], [20.5]] }, + display: { type: "text", data: [[1000], ["3.14"]] }, + invalid: { type: "default", data: [[1000], [NaN]] } + } +}); + +assert.strictEqual(commaDecimalExport.content, [ + "timestamp(ms);temperature;display;invalid;", + '"1000";"20,5";"3.14";;', + "" +].join("\n")); + +console.log("CSV export preserves text values, CSV quoting, and numeric decimal formatting"); diff --git a/server/www/utils/import_export/session.js b/server/www/utils/import_export/session.js index 010a192..63df895 100644 --- a/server/www/utils/import_export/session.js +++ b/server/www/utils/import_export/session.js @@ -21,6 +21,21 @@ function getFormatedSerieUnit(dataSerie) return ""; } +function formatCSVValue(value) { + if (value == null) { + return ""; + } + + if (typeof value == "number") { + if (!Number.isFinite(value)) { + return ""; + } + value = ("" + value).replace(".", app.csvDecimalSeparator); + } + + return '"' + ("" + value).replace(/"/g, '""') + '"'; +} + function exportSessionCSV() { let csv = "timestamp(ms)"+app.csvCellSeparator; @@ -39,11 +54,7 @@ function exportSessionCSV() { for(let i=0;i