feat: refactor parsing and scanning logic for test files

- Updated `parseFilename` to `parseTargetFilename` and modified its return structure to include `test_id` instead of `file_id`.
- Introduced `parseResultFilename` to extract `test_id` and `device` from result file names.
- Enhanced `fullScan` to separately handle target and results directories, improving clarity and functionality.
- Updated database interactions to use `test_id` instead of `file_id` across various modules.
- Added a new `/rescan` endpoint to trigger a full scan of target and results directories.
- Improved logging and error handling throughout the scanning process.
- Introduced `parseTputRssi` to extract throughput and RSSI data from log files.
This commit is contained in:
2026-05-21 14:53:08 -04:00
parent ed0f93036d
commit a67815c61a
22 changed files with 9791 additions and 228 deletions
-2
View File
@@ -1,7 +1,5 @@
{
"target_dir": "C:\\Users\\26005101\\Desktop\\CGW453\\CGW453",
"results_dir": "C:\\Users\\26005101\\Desktop\\MIA\\Test_results",
"avg_time_coe": "6600",
"avg_time_p2p": "5400",
"avg_time_p3p": "6000"
}
Binary file not shown.
Binary file not shown.
Binary file not shown.
+128 -45
View File
@@ -1,64 +1,147 @@
'use strict';
/**
* In-memory test store + JSON-file config persistence.
* No native modules required.
*/
const fs = require('fs');
const path = require('path');
const fs = require('fs');
const path = require('path');
const Database = require('better-sqlite3');
const CONFIG_PATH = path.join(__dirname, 'config.json');
const DB_PATH = path.join(__dirname, 'dashboard.db');
const CONFIG_JSON = path.join(__dirname, 'config.json');
const db = new Database(DB_PATH);
// Enable WAL for better concurrent read performance
db.pragma('journal_mode = WAL');
// ── Schema ───────────────────────────────────────────────────────────────────
db.exec(`
CREATE TABLE IF NOT EXISTS config (
key TEXT PRIMARY KEY,
value TEXT NOT NULL
);
CREATE TABLE IF NOT EXISTS tests (
id TEXT PRIMARY KEY,
test_id TEXT,
parent_dir TEXT,
filename TEXT,
interference TEXT,
device TEXT,
rotation TEXT,
test_point TEXT,
station TEXT,
band TEXT,
channel TEXT,
bandwidth TEXT,
rssi TEXT,
direction TEXT,
completed INTEGER NOT NULL DEFAULT 0,
completed_at TEXT,
duration_seconds REAL
);
CREATE INDEX IF NOT EXISTS idx_tests_test_id ON tests (test_id);
CREATE INDEX IF NOT EXISTS idx_tests_device ON tests (device);
`);
// Add new columns to existing databases (idempotent)
for (const colDef of ['tput_mbps REAL', 'dl_rssi_dbm REAL', 'ul_rssi_dbm REAL', 'tput_results TEXT']) {
try { db.exec(`ALTER TABLE tests ADD COLUMN ${colDef}`); } catch { /* already exists */ }
}
// ── One-time migration from config.json ──────────────────────────────────────
{
const alreadyMigrated = db.prepare("SELECT COUNT(*) AS n FROM config").get().n > 0;
if (!alreadyMigrated && fs.existsSync(CONFIG_JSON)) {
try {
const legacy = JSON.parse(fs.readFileSync(CONFIG_JSON, 'utf8'));
const insert = db.prepare('INSERT OR IGNORE INTO config (key, value) VALUES (?, ?)');
const migrate = db.transaction((obj) => {
for (const [k, v] of Object.entries(obj)) {
if (v != null) insert.run(k, String(v));
}
});
migrate(legacy);
console.log('[db] Migrated config.json → SQLite');
} catch (e) {
console.warn('[db] Could not migrate config.json:', e.message);
}
}
}
// ── Config ───────────────────────────────────────────────────────────────────
let _config = {};
try {
if (fs.existsSync(CONFIG_PATH)) {
_config = JSON.parse(fs.readFileSync(CONFIG_PATH, 'utf8'));
}
} catch { _config = {}; }
const _stmtGetConfig = db.prepare('SELECT value FROM config WHERE key = ?');
const _stmtSetConfig = db.prepare('INSERT OR REPLACE INTO config (key, value) VALUES (?, ?)');
const _stmtDelConfig = db.prepare('DELETE FROM config WHERE key = ?');
function _saveConfig() {
fs.writeFileSync(CONFIG_PATH, JSON.stringify(_config, null, 2));
}
function getConfig(key) { return _stmtGetConfig.get(key)?.value ?? null; }
function setConfig(key, value) { _stmtSetConfig.run(key, value); }
function delConfig(key) { _stmtDelConfig.run(key); }
function getConfig(key) { return _config[key] ?? null; }
function setConfig(key, value) { _config[key] = value; _saveConfig(); }
function delConfig(key) { delete _config[key]; _saveConfig(); }
// ── Tests ─────────────────────────────────────────────────────────────────────
const _stmtUpsertTest = db.prepare(`
INSERT INTO tests
(id, test_id, parent_dir, filename, interference, device, rotation,
test_point, station, band, channel, bandwidth, rssi, direction)
VALUES
(@id, @test_id, @parent_dir, @filename, @interference, @device, @rotation,
@test_point, @station, @band, @channel, @bandwidth, @rssi, @direction)
ON CONFLICT(id) DO UPDATE SET
test_id = excluded.test_id,
parent_dir = excluded.parent_dir,
filename = excluded.filename,
interference = excluded.interference,
device = excluded.device,
rotation = excluded.rotation,
test_point = excluded.test_point,
station = excluded.station,
band = excluded.band,
channel = excluded.channel,
bandwidth = excluded.bandwidth,
rssi = excluded.rssi,
direction = excluded.direction
-- completed / completed_at / duration_seconds intentionally preserved
`);
// ── Tests (keyed by full derived ID) ─────────────────────────────────────────
/** @type {Map<string, object>} */
const _tests = new Map();
const _stmtMarkCompleted = db.prepare(`
UPDATE tests
SET completed = 1, completed_at = ?, duration_seconds = ?, tput_results = ?
WHERE test_id = ? AND (? IS NULL OR device = ?)
`);
const _stmtResetTest = db.prepare(`
UPDATE tests
SET completed = 0, completed_at = NULL, duration_seconds = NULL, tput_results = NULL
WHERE test_id = ? AND (? IS NULL OR device = ?)
`);
const _stmtGetStation = db.prepare('SELECT station FROM tests WHERE test_id = ? AND device = ? LIMIT 1');
const _stmtClearTests = db.prepare('DELETE FROM tests');
const _stmtGetAllTests = db.prepare('SELECT * FROM tests');
const _stmtCountTests = db.prepare('SELECT COUNT(*) AS n FROM tests');
function upsertTest(test) {
const existing = _tests.get(test.id);
_tests.set(test.id, {
...test,
// Preserve completion state when re-inserting from a target scan
completed: existing ? existing.completed : 0,
completed_at: existing ? existing.completed_at : null,
duration_seconds: existing ? existing.duration_seconds : null,
});
_stmtUpsertTest.run(test);
}
function markCompleted(file_id, device, completed_at, duration_seconds) {
for (const [id, test] of _tests) {
if (test.file_id === file_id && (!device || test.device === device)) {
_tests.set(id, { ...test, completed: 1, completed_at, duration_seconds });
}
}
function markCompleted(test_id, device, completed_at, duration_seconds, tputResults = null) {
const json = tputResults && tputResults.length > 0 ? JSON.stringify(tputResults) : null;
_stmtMarkCompleted.run(completed_at, duration_seconds, json, test_id, device, device);
}
function resetByFileIdAndDevice(file_id, device) {
for (const [id, test] of _tests) {
if (test.file_id === file_id && (!device || test.device === device)) {
_tests.set(id, { ...test, completed: 0, completed_at: null, duration_seconds: null });
}
}
function getStationForTest(test_id, device) {
return _stmtGetStation.get(test_id, device)?.station ?? null;
}
function clearTests() { _tests.clear(); }
function getAllTests() { return Array.from(_tests.values()); }
function resetByFileIdAndDevice(test_id, device) {
_stmtResetTest.run(test_id, device, device);
}
function clearTests() { _stmtClearTests.run(); }
function getAllTests() { return _stmtGetAllTests.all(); }
function countTests() { return _stmtCountTests.get().n; }
module.exports = {
getConfig, setConfig, delConfig,
upsertTest, markCompleted, resetByFileIdAndDevice, clearTests, getAllTests,
upsertTest, markCompleted, resetByFileIdAndDevice, clearTests, getAllTests, countTests,
getStationForTest,
};
+12 -7
View File
@@ -5,9 +5,9 @@ const cors = require('cors');
const path = require('path');
const fs = require('fs');
const { getConfig } = require('./db');
const { getConfig, countTests } = require('./db');
const { fullScan } = require('./scanner');
const { startWatching } = require('./watcher');
const { startWatching } = require('./watcher')
const app = express();
const PORT = process.env.PORT || 3001;
@@ -35,11 +35,16 @@ async function start() {
const resultsDir = getConfig('results_dir');
if (targetDir && resultsDir) {
console.log('[server] Scanning directories...');
await fullScan(targetDir, resultsDir);
const { getAllTests } = require('./db');
const tests = getAllTests();
console.log(`[server] Startup scan complete — ${tests.length} tests found, ${tests.filter(t => t.completed).length} completed`);
const existing = countTests();
if (existing > 0) {
console.log(`[server] Resuming from DB — ${existing} tests already loaded.`);
} else {
console.log('[server] No cached data, scanning directories...');
await fullScan(targetDir, resultsDir);
const { getAllTests } = require('./db');
const tests = getAllTests();
console.log(`[server] Scan complete — ${tests.length} tests found, ${tests.filter(t => t.completed).length} completed`);
}
startWatching(targetDir, resultsDir);
console.log('[server] Watching for changes.');
} else {
+4463
View File
File diff suppressed because it is too large Load Diff
+4883
View File
File diff suppressed because it is too large Load Diff
+6 -1
View File
@@ -5,12 +5,17 @@
"main": "index.js",
"scripts": {
"start": "node index.js",
"dev": "node --watch index.js"
"dev": "node --watch index.js",
"test": "jest"
},
"dependencies": {
"better-sqlite3": "^12.10.0",
"chokidar": "^4.0.3",
"cors": "^2.8.5",
"dotenv": "^16.4.5",
"express": "^4.19.2"
},
"devDependencies": {
"jest": "^30.4.2"
}
}
+29 -11
View File
@@ -5,22 +5,22 @@ const DIRECTION_TYPES = new Set(['UL', 'DL', 'BI']);
/**
* Parse a TC_WIFI_*.ini filename and its parent directory name into tags.
* Returns an object with all tag fields plus id and file_id.
* Returns an object with all tag fields plus id and test_id.
*/
function parseFilename(filename, parentDir) {
// Strip prefix and extension → full derived ID used as primary key
const base = filename.replace(/^TC_WIFI_/, '').replace(/\.ini$/, '');
const id = base;
const segments = base.split('_');
function parseTargetFilename(filename, parentDir) {
const baseName = filename.replace(/\.ini$/i, '');
const segments = baseName.split('_').slice(2); // Skip "TC" and "WIFI" prefix
const id = `${parentDir}/${baseName}`;
// Interference: first segment, fixed enum
const interference = INTERFERENCE_TYPES.has(segments[0]) ? segments[0] : null;
// File ID: segment matching R<digit><ALNUM>+
const file_id = segments.find(s => /^R\d+[A-Z0-9]+$/i.test(s)) || null;
const test_id = segments.find(s => /^R\d+[A-Z0-9]+$/i.test(s)) || null;
// Device: segment matching CGW<digits>q
const device = segments.find(s => /^CGW\d+$/.test(s)) || null;
// Device: segment after interference
const device = segments.length > 1 ? segments[1] : null;
// Test Point: segment starting with TPT
const test_point = segments.find(s => /^TPT\w+$/.test(s)) || null;
@@ -49,7 +49,16 @@ function parseFilename(filename, parentDir) {
? (parentDir.split('_').find(s => /^ROT\d+$/.test(s)) || null)
: null;
return { id, file_id, interference, device, test_point, rssi, station, band, channel, bandwidth, direction, rotation };
return { id, test_id, interference, device, test_point, rssi, station, band, channel, bandwidth, direction, rotation };
}
function parseResultFilename(filename){
// Extract test_id and device from result file name
// COE_CGW453_R2COERXAX014_TPT3E_RSSI70_STA56_2GHZ_CH1_BW20_TCP_MIMOFD_SONFD_MESHFD_LPI_UL
const segments = filename.split(/[_\-]/);
const test_id = segments.find(s => /^R\d+[A-Z0-9]+$/.test(s)) || null;
const device = segments.length > 1 ? segments[1] : null; // Assuming device is the second segment
return { test_id, device };
}
/**
@@ -75,4 +84,13 @@ function parseElapsedTime(timeStr) {
return parseInt(h) * 3600 + parseInt(m) * 60 + parseInt(s) + parseFloat(`0.${frac}`);
}
module.exports = { parseFilename, parseTimestamp, parseElapsedTime };
function parseTputRssi(line) {
// Parse a line for station, tput, and RSSI info. Ex:
// [2026-05-18 15:09:46,167 INFO] STA56 over angles >> AVG IxChariot TPUT: 1464 Mbps, AVG DL RSSI: -42 dBm, AVG UL RSSI: -50 dBm, CHANNEL: 100
const match = line.match(/STA(\d+) over angles >> AVG IxChariot TPUT: (\d+) Mbps, AVG DL RSSI: (-?\d+) dBm, AVG UL RSSI: (-?\d+) dBm/);
if (!match) return null;
const [, station, tput, dlRssi, ulRssi] = match;
return { station: parseInt(station), tput: parseInt(tput), dlRssi: parseInt(dlRssi), ulRssi: parseInt(ulRssi) };
}
module.exports = { parseTargetFilename, parseResultFilename, parseTimestamp, parseElapsedTime, parseTputRssi };
+19
View File
@@ -58,4 +58,23 @@ router.post('/', async (req, res) => {
res.json({ ok: true, testCount: null, completedCount: null });
});
router.post('/rescan', async (req, res) => {
const targetDir = getConfig('target_dir');
const resultsDir = getConfig('results_dir');
if (!targetDir || !resultsDir) {
return res.status(400).json({ error: 'Directories not configured' });
}
try {
await fullScan(targetDir, resultsDir);
} catch (e) {
console.error('[config] rescan error:', e.message);
return res.status(500).json({ error: `Scan failed: ${e.message}` });
}
startWatching(targetDir, resultsDir);
const tests = getAllTests();
const completed = tests.filter(t => t.completed).length;
console.log(`[config] Rescan complete — ${tests.length} tests, ${completed} completed`);
res.json({ ok: true, testCount: tests.length, completedCount: completed });
});
module.exports = router;
+1 -1
View File
@@ -26,7 +26,7 @@ router.get('/', (req, res) => {
tests.sort((a, b) => {
const ia = a.interference || '';
const ib = b.interference || '';
return ia.localeCompare(ib) || (a.file_id || '').localeCompare(b.file_id || '');
return ia.localeCompare(ib) || (a.test_id || '').localeCompare(b.test_id || '');
});
res.json(tests);
+47 -49
View File
@@ -2,7 +2,7 @@
const fs = require('fs');
const path = require('path');
const readline = require('readline');
const { parseFilename, parseTimestamp, parseElapsedTime } = require('./parser');
const { parseTargetFilename, parseResultFilename, parseTimestamp, parseElapsedTime, parseTputRssi } = require('./parser');
const { upsertTest, markCompleted, clearTests } = require('./db');
/**
@@ -16,7 +16,13 @@ async function fullScan(targetDir, resultsDir) {
console.log(`[scanner] target dir : ${targetDir}`);
console.log(`[scanner] results dir: ${resultsDir}`);
// ── 1. Walk target directory ──────────────────────────────────────────────
await scanTargets(targetDir);
await scanResults(resultsDir);
}
async function scanTargets(targetDir) {
// Walk the target directory, find all TC_WIFI_*.ini files, parse their names,
// and upsert them into the DB. Ignore files without a valid test_id.
let parentEntries;
try {
parentEntries = fs.readdirSync(targetDir, { withFileTypes: true }).filter(d => d.isDirectory());
@@ -32,24 +38,24 @@ async function fullScan(targetDir, resultsDir) {
let files;
try {
files = fs.readdirSync(parentDirPath, { withFileTypes: true })
.filter(f => f.isFile() && f.name.startsWith('TC_WIFI_') && f.name.endsWith('.ini'));
.filter(f => f.isFile() && !f.name.startsWith('GLOBAL') && f.name.endsWith('.ini'));
} catch (e) {
console.error(`[scanner] Cannot read parent dir ${parentEntry.name}:`, e.message);
continue;
}
console.log(`[scanner] ${parentEntry.name}${files.length} TC_WIFI_*.ini file(s)`);
console.log(`[scanner] ${parentEntry.name}${files.length} target test file(s)`);
for (const file of files) {
const parsed = parseFilename(file.name, parentEntry.name);
if (!parsed.file_id) {
console.log(`[scanner] skip (no file_id): ${parentEntry.name}/${file.name}`);
const parsed = parseTargetFilename(file.name, parentEntry.name);
if (!parsed.test_id) {
console.log(`[scanner] skip (no test_id): ${parentEntry.name}/${file.name}`);
continue;
}
console.log(`[scanner] found: ${parentEntry.name}/${file.name}file_id=${parsed.file_id}`);
console.log(`[scanner] found: ${parentEntry.name}/${file.name}test_id=${parsed.test_id}`);
upsertTest({
id: parsed.id,
file_id: parsed.file_id,
test_id: parsed.test_id,
parent_dir: parentEntry.name,
filename: file.name,
interference: parsed.interference,
@@ -66,8 +72,6 @@ async function fullScan(targetDir, resultsDir) {
}
}
// ── 2. Walk results directory ─────────────────────────────────────────────
await scanResults(resultsDir);
}
/**
@@ -88,83 +92,77 @@ async function scanResults(resultsDir) {
}
/**
* Process one result directory: extract file_id, find the latest matching
* Process one result directory: extract test_id, find the latest matching
* log file, parse its elapsed-time line, and mark the test as completed.
*/
async function processResultDir(resultsDir, resultDirName) {
const segments = resultDirName.split(/[_\-]/);
const file_id = segments.find(s => /^R\d+[A-Z0-9]+$/i.test(s)) || null;
const device = segments.find(s => /^CGW\d+$/i.test(s)) || null;
console.log(`[scanner] result dir: ${resultDirName} → file_id=${file_id} device=${device}`);
if (!file_id) return;
const { test_id, device } = parseResultFilename(resultDirName);
//console.log(`[scanner] result dir: ${resultDirName} → test_id=${test_id} device=${device}`);
if (!test_id || !device) return;
const resultDirPath = path.join(resultsDir, resultDirName);
let logFiles;
try {
logFiles = fs.readdirSync(resultDirPath, { withFileTypes: true })
.filter(f => f.isFile() && f.name.endsWith('.txt') && f.name.includes(file_id))
.filter(f => f.isFile() && f.name.endsWith('.txt') && f.name.includes(test_id))
.map(f => f.name);
} catch {
} catch (e) {
console.error(`[scanner] Cannot read result dir ${resultDirName}:`, e.message);
return;
}
if (logFiles.length === 0) {
// Result directory exists but log not written yet — mark complete without timing
console.log(`[scanner] completed (no log yet): ${file_id}`);
markCompleted(file_id, device, null, null);
console.log(`[scanner] completed (no log yet): ${test_id}`);
markCompleted(test_id, device, null, null);
return;
}
// For re-runs, the latest log file has the latest timestamp in its name
const latestLog = logFiles.sort().at(-1);
const logPath = path.join(resultDirPath, latestLog);
const logPath = path.join(resultDirPath, latestLog);
const completed_at = parseTimestamp(latestLog);
const duration_seconds = await extractDuration(logPath, file_id);
const completed_at = parseTimestamp(latestLog);
const { duration_seconds, tputResults } = await extractLogData(logPath);
console.log(`[scanner] completed: ${file_id} device=${device} duration=${duration_seconds}s at=${completed_at}`);
markCompleted(file_id, device, completed_at, duration_seconds);
console.log(`[scanner] completed: ${test_id} device=${device} duration=${duration_seconds}s stations=${tputResults.length} at=${completed_at}`);
markCompleted(test_id, device, completed_at, duration_seconds, tputResults);
}
/**
* Stream-read a log file and return the elapsed time in seconds,
* or null if the expected line is not found.
* Single-pass log file reader: extracts elapsed time and all station tput/rssi entries.
*/
function extractDuration(logFilePath) {
console.log(`[scanner] extracting duration from log: ${logFilePath}`);
const REGEX = /\[.*?INFO\]\s+Elapsed time\s*:\s*([\d]+:[\d]{2}:[\d]{2}\.[\d]+)/;
function extractLogData(logFilePath) {
const ELAPSED_REGEX = /\[.*?INFO\]\s+Elapsed time\s*:\s*([\d]+:[\d]{2}:[\d]{2}\.[\d]+)/;
const TPUT_RSSI_REGEX = /\[.*?INFO\]\s+STA(\d+) over angles >> AVG IxChariot TPUT: (\d+) Mbps, AVG DL RSSI: (-?\d+) dBm, AVG UL RSSI: (-?\d+) dBm/
return new Promise((resolve) => {
const result = { duration_seconds: null, tputResults: [] };
let stream;
try {
stream = fs.createReadStream(logFilePath, { encoding: 'utf8' });
} catch {
return resolve(null);
} catch (e) {
console.error(`[scanner] Cannot read log file ${logFilePath}:`, e.message);
return resolve(result);
}
const rl = readline.createInterface({ input: stream, crlfDelay: Infinity });
let done = false;
function finish(value) {
if (done) return;
done = true;
resolve(value);
}
rl.on('line', (line) => {
if (done) return;
const match = line.match(REGEX);
if (match) {
finish(parseElapsedTime(match[1]));
rl.close();
stream.destroy();
const timeMatch = line.match(ELAPSED_REGEX);
const tputMatch = line.match(TPUT_RSSI_REGEX);
if (timeMatch) result.duration_seconds = parseElapsedTime(timeMatch[1]);
else if (tputMatch) {
parsed = parseTputRssi(line);
if (parsed) result.tputResults.push(parsed);
}
});
rl.on('close', () => finish(null));
rl.on('error', () => finish(null));
stream.on('error', () => finish(null));
rl.on('close', () => resolve(result));
rl.on('error', () => resolve(result));
stream.on('error', () => resolve(result));
});
}
+3 -3
View File
@@ -68,10 +68,10 @@ function startWatching(targetDir, resultsDir) {
if (path.normalize(dirPath) === path.normalize(resultsDir)) return;
const dirName = path.basename(dirPath);
const segs = dirName.split(/[_\-]/);
const file_id = segs.find(s => /^R\d+[A-Z0-9]+$/i.test(s)) || null;
const test_id = segs.find(s => /^R\d+[A-Z0-9]+$/i.test(s)) || null;
const device = segs.find(s => /^CGW\d+$/i.test(s)) || null;
if (file_id) {
resetByFileIdAndDevice(file_id, device);
if (test_id) {
resetByFileIdAndDevice(test_id, device);
broadcast({ type: 'update' });
}
});