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:
@@ -7,10 +7,18 @@ yarn-error.log*
|
||||
pnpm-debug.log*
|
||||
lerna-debug.log*
|
||||
|
||||
# Dependencies
|
||||
node_modules
|
||||
|
||||
# Build output
|
||||
dist
|
||||
dist-ssr
|
||||
|
||||
# Local env files
|
||||
*.local
|
||||
.env
|
||||
.env.*
|
||||
!.env.example
|
||||
|
||||
# Editor directories and files
|
||||
.vscode/*
|
||||
@@ -22,3 +30,8 @@ dist-ssr
|
||||
*.njsproj
|
||||
*.sln
|
||||
*.sw?
|
||||
|
||||
# Server-specific
|
||||
server/config.json.bak
|
||||
server/*.pid
|
||||
server/test
|
||||
@@ -76,7 +76,7 @@ TP_WIFI_ATT_CDR_GRP1_ROT1_CGW452_WNC_REGRESSION/
|
||||
GLOBAL.ini ← skip
|
||||
TC_WIFI_COE_CGW452_R2COERXAC003_TPT3E_RSSI70_STA4_2GHZ_CH1_BW20_TCP_MIMOFD_SONFD_MESHFD_LPI_UL.ini
|
||||
↑
|
||||
file_id = R2COERXAC003
|
||||
test_id = R2COERXAC003
|
||||
```
|
||||
|
||||
Scanner filter: include only files where `filename.startsWith('TC_WIFI_') && filename.endsWith('.ini')`.
|
||||
@@ -86,17 +86,17 @@ Scanner filter: include only files where `filename.startsWith('TC_WIFI_') && fil
|
||||
```
|
||||
COE_CGW452_R2COERXAC003_TPT3E_RSSI70_STA4_2GHZ_CH1_BW20_TCP_MIMOFD_SONFD_MESHFD_LPI_UL/
|
||||
↑
|
||||
file_id = R2COERXAC003
|
||||
COE_CGW452_R2COERXAC003_..._2026-05-16-07-30-14 ← target log file (contains file_id in name)
|
||||
test_id = R2COERXAC003
|
||||
COE_CGW452_R2COERXAC003_..._2026-05-16-07-30-14 ← target log file (contains test_id in name)
|
||||
other_file.txt ← ignored
|
||||
...other files
|
||||
```
|
||||
|
||||
A test is **completed** when a result directory name contains the target's `file_id`.
|
||||
A test is **completed** when a result directory name contains the target's `test_id`.
|
||||
|
||||
**Duration per test**: the result directory may contain multiple `.txt` files. Only the `.txt` file(s) whose name includes the `file_id` are scanned. All other `.txt` files are ignored.
|
||||
**Duration per test**: the result directory may contain multiple `.txt` files. Only the `.txt` file(s) whose name includes the `test_id` are scanned. All other `.txt` files are ignored.
|
||||
|
||||
Filter: `filename.endsWith('.txt') && filename.includes(file_id)`
|
||||
Filter: `filename.endsWith('.txt') && filename.includes(test_id)`
|
||||
|
||||
Scan the matched file for the line:
|
||||
|
||||
@@ -108,7 +108,7 @@ Regex: `/\[.*?INFO\]\s+Elapsed time\s*:\s*([\d]+:[\d]{2}:[\d]{2}\.[\d]+)/`
|
||||
|
||||
The captured group (`1:51:00.660866`) is parsed as `H:MM:SS.microseconds` and converted to total seconds stored in `duration_seconds`. If no matching line is found, `duration_seconds` is left `NULL` and excluded from the elapsed sum and avg calculations.
|
||||
|
||||
**Re-runs**: if multiple `.txt` files match the `file_id` filter (re-run logs), use the one with the **latest** timestamp in its filename for both `completed_at` and `duration_seconds`.
|
||||
**Re-runs**: if multiple `.txt` files match the `test_id` filter (re-run logs), use the one with the **latest** timestamp in its filename for both `completed_at` and `duration_seconds`.
|
||||
|
||||
### 4.2 Tag Parsing
|
||||
|
||||
@@ -152,7 +152,7 @@ first segment = interference type
|
||||
-- Stores one row per target test file
|
||||
CREATE TABLE tests (
|
||||
id TEXT PRIMARY KEY, -- full derived ID (filename without prefix/ext)
|
||||
file_id TEXT NOT NULL, -- short test case code, e.g. R2COERXAC003
|
||||
test_id TEXT NOT NULL, -- short test case code, e.g. R2COERXAC003
|
||||
parent_dir TEXT NOT NULL, -- parent folder name (TP_WIFI_...)
|
||||
filename TEXT NOT NULL, -- original .ini filename
|
||||
completed INTEGER NOT NULL DEFAULT 0, -- 0 or 1
|
||||
@@ -1,16 +0,0 @@
|
||||
# React + Vite
|
||||
|
||||
This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules.
|
||||
|
||||
Currently, two official plugins are available:
|
||||
|
||||
- [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react) uses [Oxc](https://oxc.rs)
|
||||
- [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react-swc) uses [SWC](https://swc.rs/)
|
||||
|
||||
## React Compiler
|
||||
|
||||
The React Compiler is not enabled on this template because of its impact on dev & build performances. To add it, see [this documentation](https://react.dev/learn/react-compiler/installation).
|
||||
|
||||
## Expanding the ESLint configuration
|
||||
|
||||
If you are developing a production application, we recommend using TypeScript with type-aware lint rules enabled. Check out the [TS template](https://github.com/vitejs/vite/tree/main/packages/create-vite/template-react-ts) for information on how to integrate TypeScript and [`typescript-eslint`](https://typescript-eslint.io) in your project.
|
||||
@@ -36,11 +36,6 @@ export default function App() {
|
||||
})
|
||||
}, [allTests, filters])
|
||||
|
||||
function refresh() {
|
||||
queryClient.invalidateQueries({ queryKey: ['stats'] })
|
||||
queryClient.invalidateQueries({ queryKey: ['tests'] })
|
||||
}
|
||||
|
||||
const noConfig = !configLoading && !config?.target_dir && !config?.results_dir
|
||||
const configuredButEmpty = !configLoading && config?.target_dir && config?.results_dir &&
|
||||
!statsLoading && stats?.overall.total === 0
|
||||
@@ -49,7 +44,7 @@ export default function App() {
|
||||
<div className="min-h-screen bg-slate-950 text-slate-100 flex flex-col">
|
||||
{/* Top bar */}
|
||||
<header className="border-b border-slate-800 px-6 py-4 flex items-center justify-between">
|
||||
<h1 className="text-lg font-bold tracking-tight">Test Dashboard</h1>
|
||||
<h1 className="text-lg font-bold tracking-tight">CGW453 Test Dashboard</h1>
|
||||
<div className="flex items-center gap-2">
|
||||
<button
|
||||
onClick={() => setShowConfig(true)}
|
||||
|
||||
@@ -17,6 +17,9 @@ export default function ConfigModal({ onClose }) {
|
||||
const [scanResult, setScanResult] = useState(null) // { testCount, completedCount } | null
|
||||
const [saveError, setSaveError] = useState(null)
|
||||
|
||||
// Directories are locked once both are saved — restart server to change them
|
||||
const dirsLocked = !!(config?.target_dir && config?.results_dir)
|
||||
|
||||
useEffect(() => {
|
||||
if (config) {
|
||||
setForm({
|
||||
@@ -99,7 +102,12 @@ export default function ConfigModal({ onClose }) {
|
||||
<>
|
||||
{/* Directories */}
|
||||
<section>
|
||||
<h3 className="text-slate-300 text-xs uppercase tracking-widest mb-3">Directories</h3>
|
||||
<div className="flex items-center justify-between mb-3">
|
||||
<h3 className="text-slate-300 text-xs uppercase tracking-widest">Directories</h3>
|
||||
{dirsLocked && (
|
||||
<span className="text-xs text-slate-500">🔒 Restart server to change directories</span>
|
||||
)}
|
||||
</div>
|
||||
<div className="space-y-3">
|
||||
{[
|
||||
{ key: 'target_dir', label: 'Target Tests Directory' },
|
||||
@@ -108,6 +116,15 @@ export default function ConfigModal({ onClose }) {
|
||||
<div key={key}>
|
||||
<label className="text-slate-400 text-xs block mb-1">{label}</label>
|
||||
<div className="flex gap-2">
|
||||
{dirsLocked ? (
|
||||
<div
|
||||
title={form[key] ?? ''}
|
||||
className="flex-1 border border-slate-700/50 bg-slate-800/40 text-slate-500 text-sm rounded-lg px-3 py-2 truncate cursor-default select-all font-mono"
|
||||
>
|
||||
{(form[key] ?? '').replace(/^(.+[/\\])([^/\\]+[/\\][^/\\]*)$/, '…$2')}
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
<input
|
||||
type="text"
|
||||
value={form[key] ?? ''}
|
||||
@@ -121,6 +138,8 @@ export default function ConfigModal({ onClose }) {
|
||||
>
|
||||
Browse
|
||||
</button>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
|
||||
@@ -1,24 +1,14 @@
|
||||
import { useState } from 'react'
|
||||
import React, { useState } from 'react'
|
||||
import StatusBadge from './StatusBadge'
|
||||
|
||||
const COLS = [
|
||||
{ key: 'file_id', label: 'File ID' },
|
||||
{ key: 'interference', label: 'Type' },
|
||||
{ key: 'device', label: 'Device' },
|
||||
{ key: 'rotation', label: 'Rotation' },
|
||||
{ key: 'test_point', label: 'Test Point' },
|
||||
{ key: 'rssi', label: 'RSSI' },
|
||||
{ key: 'station', label: 'Station' },
|
||||
{ key: 'band', label: 'Band' },
|
||||
{ key: 'channel', label: 'Channel' },
|
||||
{ key: 'bandwidth', label: 'BW' },
|
||||
{ key: 'direction', label: 'Dir' },
|
||||
{ key: 'completed', label: 'Status' },
|
||||
{ key: 'duration_seconds', label: 'Duration' },
|
||||
]
|
||||
const TYPE_COLORS = {
|
||||
COE: 'bg-blue-900/50 text-blue-300',
|
||||
P2P: 'bg-purple-900/50 text-purple-300',
|
||||
P3P: 'bg-orange-900/50 text-orange-300',
|
||||
}
|
||||
|
||||
function fmtDuration(s) {
|
||||
if (s == null) return '—'
|
||||
if (s == null) return null
|
||||
const h = Math.floor(s / 3600)
|
||||
const m = Math.floor((s % 3600) / 60)
|
||||
const sec = Math.floor(s % 60)
|
||||
@@ -27,13 +17,46 @@ function fmtDuration(s) {
|
||||
return `${sec}s`
|
||||
}
|
||||
|
||||
function TagPill({ label, value, colorClass }) {
|
||||
if (value == null || value === '') return null
|
||||
return (
|
||||
<div className="flex flex-col gap-0.5 min-w-[60px]">
|
||||
<span className="text-[10px] text-slate-500 uppercase tracking-wide leading-none">{label}</span>
|
||||
<span className={`text-xs font-medium px-2 py-0.5 rounded whitespace-nowrap ${colorClass ?? 'bg-slate-800 text-slate-300'}`}>
|
||||
{value}
|
||||
</span>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function SortTh({ colKey, label, sort, onSort }) {
|
||||
return (
|
||||
<th
|
||||
onClick={() => onSort(colKey)}
|
||||
className="px-3 py-3 cursor-pointer select-none whitespace-nowrap hover:text-slate-200 transition-colors"
|
||||
>
|
||||
{label}
|
||||
{sort.key === colKey && <span className="ml-1">{sort.dir === 1 ? '↑' : '↓'}</span>}
|
||||
</th>
|
||||
)
|
||||
}
|
||||
|
||||
export default function TestTable({ tests = [], isLoading }) {
|
||||
const [sort, setSort] = useState({ key: 'file_id', dir: 1 })
|
||||
const [sort, setSort] = useState({ key: 'filename', dir: 1 })
|
||||
const [expanded, setExpanded] = useState(new Set())
|
||||
|
||||
function toggleSort(key) {
|
||||
setSort(s => ({ key, dir: s.key === key ? -s.dir : 1 }))
|
||||
}
|
||||
|
||||
function toggleExpand(id) {
|
||||
setExpanded(prev => {
|
||||
const next = new Set(prev)
|
||||
next.has(id) ? next.delete(id) : next.add(id)
|
||||
return next
|
||||
})
|
||||
}
|
||||
|
||||
const sorted = [...tests].sort((a, b) => {
|
||||
let av = a[sort.key] ?? ''
|
||||
let bv = b[sort.key] ?? ''
|
||||
@@ -58,59 +81,91 @@ export default function TestTable({ tests = [], isLoading }) {
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="overflow-x-auto rounded-xl border border-slate-800">
|
||||
<div className="rounded-xl border border-slate-800 overflow-hidden">
|
||||
<table className="w-full text-sm text-left text-slate-300 border-collapse">
|
||||
<thead className="bg-slate-800 text-slate-400 text-xs uppercase tracking-wider">
|
||||
<tr>
|
||||
{COLS.map(col => (
|
||||
<th
|
||||
key={col.key}
|
||||
onClick={() => toggleSort(col.key)}
|
||||
className="px-3 py-3 cursor-pointer select-none whitespace-nowrap hover:text-slate-200 transition-colors"
|
||||
>
|
||||
{col.label}
|
||||
{sort.key === col.key && (
|
||||
<span className="ml-1">{sort.dir === 1 ? '↑' : '↓'}</span>
|
||||
)}
|
||||
</th>
|
||||
))}
|
||||
<th className="px-3 py-3 w-6" />
|
||||
<SortTh colKey="filename" label="File" sort={sort} onSort={toggleSort} />
|
||||
<SortTh colKey="completed" label="Status" sort={sort} onSort={toggleSort} />
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{sorted.map((test, i) => (
|
||||
<tr
|
||||
key={test.id}
|
||||
className={`border-t border-slate-800 transition-colors ${
|
||||
test.completed
|
||||
{sorted.map((test, i) => {
|
||||
const isOpen = expanded.has(test.id)
|
||||
const rowBase = test.completed
|
||||
? 'bg-emerald-950/20 hover:bg-emerald-950/40'
|
||||
: i % 2 === 0 ? 'bg-slate-900 hover:bg-slate-800' : 'bg-slate-900/60 hover:bg-slate-800'
|
||||
}`}
|
||||
|
||||
return (
|
||||
<React.Fragment key={test.id}>
|
||||
<tr
|
||||
onClick={() => toggleExpand(test.id)}
|
||||
className={`border-t border-slate-800 cursor-pointer transition-colors ${rowBase}`}
|
||||
>
|
||||
<td className="px-3 py-2 font-mono text-xs text-slate-200 whitespace-nowrap">{test.file_id ?? '—'}</td>
|
||||
<td className="px-3 py-2 whitespace-nowrap">
|
||||
<span className={`text-xs font-semibold px-1.5 py-0.5 rounded ${
|
||||
test.interference === 'COE' ? 'bg-blue-900/50 text-blue-300' :
|
||||
test.interference === 'P2P' ? 'bg-purple-900/50 text-purple-300' :
|
||||
test.interference === 'P3P' ? 'bg-orange-900/50 text-orange-300' : ''
|
||||
}`}>
|
||||
{test.interference ?? '—'}
|
||||
</span>
|
||||
<td className="px-3 py-2 text-slate-500 text-xs select-none">
|
||||
{isOpen ? '▾' : '▸'}
|
||||
</td>
|
||||
<td className="px-3 py-2 whitespace-nowrap">{test.device ?? '—'}</td>
|
||||
<td className="px-3 py-2 whitespace-nowrap">{test.rotation ?? '—'}</td>
|
||||
<td className="px-3 py-2 whitespace-nowrap">{test.test_point ?? '—'}</td>
|
||||
<td className="px-3 py-2 whitespace-nowrap">{test.rssi ?? '—'}</td>
|
||||
<td className="px-3 py-2 whitespace-nowrap">{test.station ?? '—'}</td>
|
||||
<td className="px-3 py-2 whitespace-nowrap">{test.band ?? '—'}</td>
|
||||
<td className="px-3 py-2 whitespace-nowrap">{test.channel ?? '—'}</td>
|
||||
<td className="px-3 py-2 whitespace-nowrap">{test.bandwidth ?? '—'}</td>
|
||||
<td className="px-3 py-2 whitespace-nowrap">{test.direction ?? '—'}</td>
|
||||
<td className="px-3 py-2 whitespace-nowrap"><StatusBadge completed={test.completed} /></td>
|
||||
<td className="px-3 py-2 whitespace-nowrap text-slate-400">{fmtDuration(test.duration_seconds)}</td>
|
||||
<td className="px-3 py-2 text-xs text-slate-200 whitespace-nowrap">
|
||||
{test.filename ?? '—'}
|
||||
</td>
|
||||
<td className="px-3 py-2 whitespace-nowrap">
|
||||
<StatusBadge completed={test.completed} />
|
||||
</td>
|
||||
</tr>
|
||||
{isOpen && (
|
||||
<tr className={`border-t border-slate-700/60 ${test.completed ? 'bg-emerald-950/10' : 'bg-slate-900/80'}`}>
|
||||
<td colSpan={3} className="px-6 py-4">
|
||||
<div className="flex flex-wrap gap-3">
|
||||
<TagPill label="File ID" value={test.test_id} />
|
||||
<TagPill label="Type" value={test.interference} colorClass={TYPE_COLORS[test.interference]} />
|
||||
<TagPill label="Device" value={test.device} />
|
||||
<TagPill label="Rotation" value={test.rotation} />
|
||||
<TagPill label="Test Point" value={test.test_point} />
|
||||
<TagPill label="RSSI" value={test.rssi} />
|
||||
<TagPill label="Station" value={test.station} />
|
||||
<TagPill label="Band" value={test.band} />
|
||||
<TagPill label="Channel" value={test.channel} />
|
||||
<TagPill label="Bandwidth" value={test.bandwidth} />
|
||||
<TagPill label="Direction" value={test.direction} />
|
||||
<TagPill label="Elapsed Time" value={fmtDuration(test.duration_seconds)} />
|
||||
</div>
|
||||
{(() => {
|
||||
const rows = test.tput_results ? JSON.parse(test.tput_results) : []
|
||||
if (!test.completed || rows.length === 0) return null
|
||||
return (
|
||||
<div className="mt-3 pt-3 border-t border-slate-700/50">
|
||||
<table className="text-xs w-auto border-collapse">
|
||||
<thead>
|
||||
<tr className="text-slate-500 uppercase tracking-wide">
|
||||
<th className="pr-6 pb-1 text-left font-medium">Station</th>
|
||||
<th className="pr-6 pb-1 text-right font-medium">Throughput</th>
|
||||
<th className="pr-6 pb-1 text-right font-medium">DL RSSI</th>
|
||||
<th className="pb-1 text-right font-medium">UL RSSI</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{rows.map(r => (
|
||||
<tr key={r.station} className="text-slate-300">
|
||||
<td className="pr-6 py-0.5 text-cyan-400 font-medium">STA{r.station}</td>
|
||||
<td className="pr-6 py-0.5 text-right">{r.tput} Mbps</td>
|
||||
<td className="pr-6 py-0.5 text-right">{r.dlRssi} dBm</td>
|
||||
<td className="py-0.5 text-right">{r.ulRssi} dBm</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
)
|
||||
})()}
|
||||
</td>
|
||||
</tr>
|
||||
)}
|
||||
</React.Fragment>
|
||||
)
|
||||
})}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -6,6 +6,19 @@ function fmt(seconds) {
|
||||
return `${m}m`
|
||||
}
|
||||
|
||||
function fmtDays(seconds) {
|
||||
if (seconds == null) return null
|
||||
const days = seconds / 57600 // 16 hours per day
|
||||
return days < 1 ? `${(days * 24).toFixed(1)}h` : `${days.toFixed(1)}d (16h/day)`
|
||||
}
|
||||
|
||||
function fmtCompletionDate(seconds) {
|
||||
if (seconds == null) return null
|
||||
const days = seconds / 57600 // 16 hours per day
|
||||
const date = new Date(Date.now() + days * 24 * 3600 * 1000)
|
||||
return date.toLocaleDateString(undefined, { weekday: 'short', month: 'short', day: 'numeric', year: 'numeric' })
|
||||
}
|
||||
|
||||
export default function TimeDisplay({ elapsedSeconds, estimatedRemainingSeconds, byType }) {
|
||||
const missingTypes = byType
|
||||
? Object.entries(byType)
|
||||
@@ -13,9 +26,12 @@ export default function TimeDisplay({ elapsedSeconds, estimatedRemainingSeconds,
|
||||
.map(([t]) => t)
|
||||
: []
|
||||
|
||||
const days = fmtDays(estimatedRemainingSeconds)
|
||||
const completionDate = fmtCompletionDate(estimatedRemainingSeconds)
|
||||
|
||||
return (
|
||||
<div className="bg-slate-900 border border-slate-800 rounded-xl p-4 flex flex-col gap-3">
|
||||
<div className="flex gap-6">
|
||||
<div className="flex flex-wrap gap-6">
|
||||
<div>
|
||||
<p className="text-slate-400 text-xs uppercase tracking-widest">Time Elapsed</p>
|
||||
<p className="text-2xl font-bold text-slate-100 mt-0.5">
|
||||
@@ -23,11 +39,20 @@ export default function TimeDisplay({ elapsedSeconds, estimatedRemainingSeconds,
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-slate-400 text-xs uppercase tracking-widest">Est. Remaining</p>
|
||||
<p className="text-slate-400 text-xs uppercase tracking-widest">Est. Time Remaining</p>
|
||||
<p className={`text-2xl font-bold mt-0.5 ${estimatedRemainingSeconds != null ? 'text-slate-100' : 'text-amber-400'}`}>
|
||||
{estimatedRemainingSeconds != null ? fmt(estimatedRemainingSeconds) : '—'}
|
||||
</p>
|
||||
{days && (
|
||||
<p className="text-slate-400 text-xs mt-0.5">{days}</p>
|
||||
)}
|
||||
</div>
|
||||
{completionDate && (
|
||||
<div>
|
||||
<p className="text-slate-400 text-xs uppercase tracking-widest">Est. Completion</p>
|
||||
<p className="text-lg font-semibold text-slate-100 mt-0.5">{completionDate}</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{missingTypes.length > 0 && (
|
||||
|
||||
@@ -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.
+126
-43
@@ -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 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,
|
||||
};
|
||||
|
||||
+9
-4
@@ -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...');
|
||||
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] Startup scan complete — ${tests.length} tests found, ${tests.filter(t => t.completed).length} completed`);
|
||||
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
File diff suppressed because it is too large
Load Diff
Generated
+4883
File diff suppressed because it is too large
Load Diff
+6
-1
@@ -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
@@ -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 };
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
|
||||
+45
-47
@@ -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,31 +92,30 @@ 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;
|
||||
}
|
||||
|
||||
@@ -121,50 +124,45 @@ async function processResultDir(resultsDir, resultDirName) {
|
||||
const logPath = path.join(resultDirPath, latestLog);
|
||||
|
||||
const completed_at = parseTimestamp(latestLog);
|
||||
const duration_seconds = await extractDuration(logPath, file_id);
|
||||
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
@@ -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' });
|
||||
}
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user