diff --git a/app/dashboard/experimental/antenna-insights/page.tsx b/app/dashboard/experimental/antenna-insights/page.tsx new file mode 100644 index 0000000..e5728a5 --- /dev/null +++ b/app/dashboard/experimental/antenna-insights/page.tsx @@ -0,0 +1,18 @@ +import SignalQualityComponent from '@/components/pages/signal-quality' +import React from 'react' + +const AntennaInsightsPage = () => { + return ( +
+
+

Antenna Signal Quality

+

+ Use this to reference your individual antenna connection's signal quality. +

+
+ +
+ ) +} + +export default AntennaInsightsPage \ No newline at end of file diff --git a/app/dashboard/experimental/layout.tsx b/app/dashboard/experimental/layout.tsx index e59e9c2..0e6ee62 100644 --- a/app/dashboard/experimental/layout.tsx +++ b/app/dashboard/experimental/layout.tsx @@ -102,6 +102,16 @@ const ExperimentalLayout = ({ children }: ExperimentalLayoutProps) => { > Keep Alive + + Antenna Insights + {children} diff --git a/components/pages/signal-quality.tsx b/components/pages/signal-quality.tsx new file mode 100644 index 0000000..56ea87e --- /dev/null +++ b/components/pages/signal-quality.tsx @@ -0,0 +1,423 @@ +"use client"; + +import { useEffect, useState, useRef } from "react"; +import { + Card, + CardContent, + CardFooter, + CardHeader, + CardTitle, +} from "@/components/ui/card"; +import { Progress } from "@/components/ui/progress"; +import { calculateSignalPercentage } from "@/utils/signalMetrics"; + +import { useAuth } from "@/hooks/auth"; +import { AnimatedThemeToggler } from "../ui/animated-theme-toggler"; + +interface ChartDataItem { + activity: string; + value: number; + label: string; + fill: string; +} + +interface AllSignals { + input: Array; +} + +interface SignalData { + rsrp: number | null; + rsrq: number | null; + sinr: number | null; + networkType: string; + bands: string | null; + networkName: string; +} + +interface AntennaSignalData { + antenna: number; + rsrp: number | null; + rsrq: number | null; + sinr: number | null; + bandType: string; +} + +interface AtCommandResponse { + command: string; + response: string; + status: string; +} + +const processSignalValues = (matches: string[] | null): number | null => { + if (!matches) return null; + const validValues = matches + .map(Number) + .filter((val) => val !== -32768 && val !== -140); + if (validValues.length === 0) return null; + const sum = validValues.reduce((acc, curr) => acc + curr, 0); + return Math.round(sum / validValues.length); +}; + +const parseAntennaSignalData = (atResponses: AtCommandResponse[]): AntennaSignalData[] => { + // Filter to only include signal measurement commands + const signalCommands = atResponses.filter(cmd => + ['AT+QRSRP', 'AT+QRSRQ', 'AT+QSINR'].includes(cmd.command) + ); + + // Extract values and band type from each command + const signalData: { [key: string]: { values: (number | null)[], bandType: string } } = {}; + + signalCommands.forEach(cmd => { + const responseLines = cmd.response.split('\n'); + const dataLine = responseLines.find(line => line.startsWith('+Q')); + + if (dataLine) { + // Extract the data part after the colon + const dataPart = dataLine.split(': ')[1]; + if (dataPart) { + const parts = dataPart.split(','); + + // Extract the 4 antenna values (indices 0-3) + const antennaValues = parts.slice(0, 4).map(val => { + const num = parseInt(val.trim()); + return (num === -32768 || num === -140) ? null : num; + }) as (number | null)[]; + + // Extract band type (last part) + const bandType = parts[parts.length - 1]?.trim() || 'Unknown'; + + signalData[cmd.command] = { values: antennaValues, bandType }; + } + } + }); + + // Create array of antenna objects (always 4 antennas) + const antennaArray: AntennaSignalData[] = []; + + for (let i = 0; i < 4; i++) { + antennaArray.push({ + antenna: i, + rsrp: signalData['AT+QRSRP']?.values[i] ?? null, + rsrq: signalData['AT+QRSRQ']?.values[i] ?? null, + sinr: signalData['AT+QSINR']?.values[i] ?? null, + bandType: signalData['AT+QRSRP']?.bandType ?? + signalData['AT+QRSRQ']?.bandType ?? + signalData['AT+QSINR']?.bandType ?? 'Unknown' + }); + } + + return antennaArray; +}; + +// Signal strength bar component +const SignalBar = ({ + label, + value, + type, + unit, + bandType +}: { + label: string; + value: number | null; + type: 'rsrp' | 'rsrq' | 'sinr'; + unit: string; + bandType?: string; +}) => { + if (value === null) { + return ( +
+
+ {label} + N/A +
+ +
+ ); + } + + const percentage = calculateSignalPercentage(type, value, bandType); + const getBarColor = (percentage: number) => { + if (percentage >= 95) return "bg-blue-600"; // Excellent - Blue + if (percentage >= 85) return "bg-blue-500"; // Very Good - Light Blue + if (percentage >= 75) return "bg-green-600"; // Good - Dark Green + if (percentage >= 65) return "bg-green-500"; // Good - Green + if (percentage >= 55) return "bg-green-400"; // Fair - Light Green + if (percentage >= 45) return "bg-yellow-500"; // Fair - Yellow + if (percentage >= 35) return "bg-orange-500"; // Poor - Orange + if (percentage >= 25) return "bg-orange-600"; // Poor - Dark Orange + if (percentage >= 15) return "bg-red-500"; // Bad - Red + return "bg-red-700"; // Very Bad - Dark Red + }; + + return ( +
+
+ {label} + {value}{unit} +
+
+
+
+
{percentage.toFixed(0)}%
+
+ ); +}; + +// Create a standardized array of 4 antennas (0-3) +const getStandardizedAntennaArray = (antennaData: AntennaSignalData[]): AntennaSignalData[] => { + const standardizedArray: AntennaSignalData[] = []; + + for (let i = 0; i < 4; i++) { + const existingAntenna = antennaData.find(ant => ant.antenna === i); + standardizedArray.push(existingAntenna || { + antenna: i, + rsrp: null, + rsrq: null, + sinr: null, + bandType: antennaData[0]?.bandType || 'Unknown' + }); + } + + return standardizedArray; +}; + +// Get antenna display name +const getAntennaName = (antennaNumber: number): string => { + switch (antennaNumber) { + case 0: return "Main Antenna"; + case 1: return "Diverse Antenna"; + case 2: return "MIMO 1"; + case 3: return "MIMO 2"; + default: return `Antenna ${antennaNumber}`; + } +}; + +export default function ChartPreviewSignal() { + const [signalData, setSignalData] = useState({ + rsrp: null, + rsrq: null, + sinr: null, + networkType: "", + bands: null, + networkName: "", + }); + const [antennaData, setAntennaData] = useState([]); + const [initialLoading, setInitialLoading] = useState(true); + const previousData = useRef(null); + const { logout } = useAuth(); + useEffect(() => { + const fetchStats = async () => { + try { + const response = await fetch( + "/cgi-bin/quecmanager/at_cmd/fetch_data.sh?set=5" + ); + const data: AtCommandResponse[] = await response.json(); + console.log(data); + + // Parse antenna-specific signal data + const parsedAntennaData = parseAntennaSignalData(data); + setAntennaData(parsedAntennaData); + console.log("Parsed antenna data:", parsedAntennaData); + + const allSignals: AllSignals = { input: [] }; + if (data) { + // Keep existing averaging logic for backward compatibility + const rsrpResponse = data.find(cmd => cmd.command === 'AT+QRSRP'); + const rsrqResponse = data.find(cmd => cmd.command === 'AT+QRSRQ'); + const sinrResponse = data.find(cmd => cmd.command === 'AT+QSINR'); + const caResponse = data.find(cmd => cmd.command === 'AT+QCAINFO'); + const spnResponse = data.find(cmd => cmd.command === 'AT+QSPN'); + + const newData: SignalData = { + rsrp: rsrpResponse ? processSignalValues(rsrpResponse.response.match(/-?\d+/g)) : null, + rsrq: rsrqResponse ? processSignalValues(rsrqResponse.response.match(/-?\d+/g)) : null, + sinr: sinrResponse ? processSignalValues(sinrResponse.response.match(/-?\d+/g)) : null, + networkType: "", + bands: null, + networkName: "", + }; + + console.log(newData); + + const bands = caResponse ? caResponse.response.match( + /"LTE BAND \d+"|"NR5G BAND \d+"/g + ) : null; + + const hasLTE = bands?.some((band) => band.includes("LTE")); + const hasNR5G = bands?.some((band) => band.includes("NR5G")); + + newData.networkType = + hasLTE && hasNR5G + ? "NR5G-NSA" + : hasLTE + ? "LTE" + : hasNR5G + ? "NR5G-SA" + : "No Signal"; + + const parsedBands = bands?.map((band) => { + if (band.includes("LTE")) { + return `B${band.match(/\d+/)}`; + } else if (band.includes("NR5G")) { + return `N${band.split(" ")[2].replace(/"/g, "").trim()}`; + } + }); + + newData.bands = parsedBands ? parsedBands.join(", ") : "No Signal"; + newData.networkName = spnResponse ? + spnResponse.response + .split("\n")[1] + ?.split(":")[1] + ?.split(",")[1] + ?.replace(/"/g, "") + ?.trim() || "No Signal" : "No Signal"; + + setSignalData(newData); + previousData.current = newData; + } + } catch (error) { + console.error("Error fetching stats:", error); + } finally { + if (initialLoading) { + setInitialLoading(false); + } + } + }; + + const intervalId = setInterval(fetchStats, 2000); + return () => clearInterval(intervalId); + }, [initialLoading, logout]); + + const chartData: ChartDataItem[] = [ + { + activity: "rsrp", + value: + signalData.rsrp !== null + ? calculateSignalPercentage("rsrp", signalData.rsrp, antennaData[0]?.bandType) + : 0, + label: + signalData.rsrp !== null + ? `${signalData.rsrp.toFixed(1)} dBm` + : "No Signal", + fill: "hsl(var(--chart-1))", + }, + { + activity: "rsrq", + value: + signalData.rsrq !== null + ? calculateSignalPercentage("rsrq", signalData.rsrq, antennaData[0]?.bandType) + : 0, + label: + signalData.rsrq !== null + ? `${signalData.rsrq.toFixed(1)} dB` + : "No Signal", + fill: "hsl(var(--chart-2))", + }, + { + activity: "sinr", + value: + signalData.sinr !== null + ? calculateSignalPercentage("sinr", signalData.sinr, antennaData[0]?.bandType) + : 0, + label: + signalData.sinr !== null + ? `${signalData.sinr.toFixed(1)} dB` + : "No Signal", + fill: "hsl(var(--chart-3))", + }, + ]; + + return ( +
+ + +
+ QuecManager Signal Quality Stats + +
+
+ + {/* Individual Antenna Data Display */} +
+

+ Individual Antenna Signals ({antennaData[0]?.bandType || 'Not Available'}) +

+
+ {getStandardizedAntennaArray(antennaData).map((antenna) => ( +
+
+ {getAntennaName(antenna.antenna)} +
+
+ + + +
+
+ ))} +
+
+
+ +
+
+ Antenna Alignment Strategy +
+
+
1
+
+ Start with RSRP +

Rotate antenna to maximize this value first (target: -70 to -80 dBm)

+
+
+
+
2
+
+ Verify RSRQ +

Ensure it stays above -15 dB (indicates low interference)

+
+
+
+
3
+
+ Check SINR +

Should improve as RSRP increases and interference decreases (target: ≥13 dB)

+
+
+
+
4
+
+ Fine-tune +

Small adjustments focusing on the best balance of all three parameters

+
+
+
+
+
+
+
+
+ + ); +} diff --git a/next.config.mjs b/next.config.mjs index 33ed38a..b4f08b3 100644 --- a/next.config.mjs +++ b/next.config.mjs @@ -5,15 +5,15 @@ const nextConfig = { trailingSlash: true, // uncomment for development - // async rewrites() { - // return [ - // { - // source: '/cgi-bin/:path*', - // destination: 'http://192.168.224.1/cgi-bin/:path*', - // basePath: false, - // }, - // ]; - // }, + async rewrites() { + return [ + { + source: '/cgi-bin/:path*', + destination: 'http://192.168.228.1/cgi-bin/:path*', + basePath: false, + }, + ]; + } }; export default nextConfig; diff --git a/utils/signalMetrics.ts b/utils/signalMetrics.ts index 1bd04b6..2b3df2e 100644 --- a/utils/signalMetrics.ts +++ b/utils/signalMetrics.ts @@ -4,25 +4,35 @@ interface SignalRanges { min: number; // poorest value max: number; // ideal value } - + const SIGNAL_RANGES: Record = { - rsrp: { min: -140, max: -70 }, // -140 (poor) to -70 (ideal) - rsrq: { min: -20, max: -10 }, // -20 (poor) to -10 (ideal) - sinr: { min: 0, max: 20 } // 0 (poor) to 20 (ideal) + rsrp: { min: -140, max: -40 }, // -140 (poor) to -40 (ideal) + rsrq: { min: -20, max: -3 }, // -20 (poor) to -3 (ideal) + sinr_lte: { min: -20, max: 30 }, // -20 (poor) to 30 (ideal) for LTE + sinr_5g: { min: -23, max: 40 } // -23 (poor) to 40 (ideal) for 5G }; - + export const calculateSignalPercentage = ( type: 'rsrp' | 'rsrq' | 'sinr', - value: number + value: number, + bandType?: string ): number => { - const range = SIGNAL_RANGES[type]; - + let range: SignalRanges; + + // Handle SINR with band type differentiation + if (type === 'sinr') { + const is5G = bandType?.includes('NR5G') || bandType?.includes('5G'); + range = is5G ? SIGNAL_RANGES['sinr_5g'] : SIGNAL_RANGES['sinr_lte']; + } else { + range = SIGNAL_RANGES[type]; + } + // Ensure value stays within bounds const clampedValue = Math.max(Math.min(value, range.max), range.min); - + // Calculate percentage const percentage = ((clampedValue - range.min) / (range.max - range.min)) * 100; - + // Round to 1 decimal place and ensure it's between 0 and 100 return Math.min(Math.max(Math.round(percentage * 10) / 10, 0), 100); }; \ No newline at end of file