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 (
+
+ );
+ }
+
+ 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