Skip to content

Commit

Permalink
Adding telemetry graphs (#464)
Browse files Browse the repository at this point in the history
* Adding telemetry graphs

* fixing the Lint rules for telemetry
  • Loading branch information
Thibault authored and GitHub committed Jul 11, 2025
1 parent 1964098 commit c74c0a0
Show file tree
Hide file tree
Showing 8 changed files with 468 additions and 75 deletions.
1 change: 1 addition & 0 deletions .github/workflows/autoredeploy.yml
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ jobs:
echo "SLACK_BOT_TOKEN=${{ secrets.SLACK_BOT_TOKEN }}" >> .env
echo "SLACK_CHANNEL_ID=${{ secrets.SLACK_CHANNEL_ID }}" >> .env
echo "SLACK_USER_TOKEN=${{ secrets.SLACK_USER_TOKEN }}" >> .env
echo "GRAFANA_BOT_TOKEN=${{ secrets.GRAFANA_BOT_TOKEN }}" >> .env
# Create .env.production for the frontend nextjs
echo "BACKEND_INTERNAL_URL=${{ vars.BACKEND_INTERNAL_URL }}" >> .env.production
Expand Down
Binary file modified backend/dbLocation.sqlite
Binary file not shown.
92 changes: 92 additions & 0 deletions backend/src/api/grafana/controllers/grafana.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
"use strict";
const fetch = require("node-fetch");

module.exports = {
fetchMetrics: async (ctx) => {
const { satSQL } = ctx.request.body; // Get satellite from request parameters
if (!satSQL) {
return ctx.badRequest("Satellite parameter is required");
}
try {
const grafanaToken = process.env.GRAFANA_BOT_TOKEN; // Grafana API token
const grafanaHost = "https://monitoring.hypso.space"; // Grafana URL
const datasourceId = 3; // Replace with your datasource UID

const fields = [
{ refId: "batteryVoltage", field: "vBatt", measurement: "eps" },
{ refId: "battCurr", field: "curBattIn", measurement: "eps" },
{ refId: "uptime", field: "uptimeInS", measurement: "eps" },
{
refId: "solarPanelTemp1",
field: "solarPanelTemp1",
measurement: "fc",
},
{
refId: "solarPanelTemp2",
field: "solarPanelTemp2",
measurement: "fc",
},
{
refId: "solarPanelTemp3",
field: "solarPanelTemp3",
measurement: "fc",
},
{
refId: "solarPanelTemp4",
field: "solarPanelTemp4",
measurement: "fc",
},
{
refId: "solarPanelTemp5",
field: "solarPanelTemp5",
measurement: "fc",
},
{
refId: "solarPanelTemp6",
field: "solarPanelTemp6",
measurement: "fc",
},
];

const queries = fields.map((item) => ({
refId: item.refId,
datasourceId: datasourceId,
resultFormat: "time_series",
rawQuery: true,
query: `SELECT "${item.field}" FROM "${satSQL}.${item.measurement}.GeneralTelemetry" WHERE time > now() - 7d`,
}));

const query = { queries };

// Make a POST request to Grafana's datasource proxy API
const response = await fetch(`${grafanaHost}/api/ds/query`, {
method: "POST",
headers: {
Authorization: `Bearer ${grafanaToken}`,
"Content-Type": "application/json",
},
body: JSON.stringify(query),
});

if (!response.ok) {
throw new Error(`Grafana query failed with status ${response.status}`);
}

const data = await response.json();
const values = fields.reduce((acc, item) => {
const result = data.results[item.refId];
if (result && result.frames && result.frames.length > 0) {
acc[item.refId] = result.frames[0].data.values; // Store as key-value pair
}
return acc;
}, {});
// Return the data to the client
ctx.send(values);
} catch (error) {
console.error("Error fetching metrics:", error);
return ctx.internalServerError(
"Failed to fetch metrics: " + error.message
);
}
},
};
14 changes: 14 additions & 0 deletions backend/src/api/grafana/routes/grafana.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
"use strict";

module.exports = {
routes: [
{
method: "POST",
path: "/grafana-metrics",
handler: "grafana.fetchMetrics",
config: {
auth: false,
},
},
],
};
16 changes: 13 additions & 3 deletions frontend/src/app/satellites/[satelliteSlug]/satImage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
import React, { useEffect, useState, useCallback } from "react";
import { useSatelliteStore } from "@/lib/store";
import { SatelliteNumber } from "@/lib/store";
import Image from "next/image";

export default function SatImage({
STRAPI_URL,
Expand Down Expand Up @@ -110,7 +111,15 @@ export default function SatImage({
}
}
fetchSlackImages();
}, [satImage, selectedSatellite]);
}, [
satImage,
selectedSatellite,
STRAPI_URL,
noradID,
satNumToEntry,
makeTheImagePublic,
createImageUrl,
]);

if (loading) {
return <div>Loading satellite image...</div>;
Expand All @@ -120,11 +129,12 @@ export default function SatImage({
}
return satImage ? (
<div className="flex h-full w-full items-center justify-center bg-black">
<img
<Image
key={satImage}
src={satImage}
alt="Satellite Image"
style={{ width: "100%", height: "100%" }}
width={1600} // Set according to the aspect ratio of the image
height={0}
className="max-h-[600px] max-w-[600px] object-contain p-2"
/>
</div>
Expand Down
40 changes: 2 additions & 38 deletions frontend/src/app/satellites/[satelliteSlug]/satInfo.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,5 @@
"use client";
import React, { useState, useEffect } from "react";
import Image from "next/image";
import Render3DMod from "../render3DMod";
import React from "react";
import SatTabs from "./satTabs";
import { SatAttributes } from "@/lib/utils";
import TabBar from "./tabBars";
Expand All @@ -16,22 +14,6 @@ export default function SatInfo({
STRAPI_URL: string | undefined;
BACKEND_INTERNAL_URL: string | undefined;
}) {
const [imageURL, setImageURL] = useState<string | undefined>(undefined);
const [is3DModel, setIs3DModel] = useState<boolean>(false);
useEffect(() => {
let satelliteImage =
satAttributes?.satelliteImage?.data?.attributes?.url;
if (BACKEND_INTERNAL_URL && satelliteImage) {
const fullImage = BACKEND_INTERNAL_URL + satelliteImage;
setImageURL(fullImage);
setIs3DModel(
satelliteImage.endsWith(".glb") ||
satelliteImage.endsWith(".gltf") ||
satelliteImage.endsWith(".glb?"),
);
}
}, [satAttributes, BACKEND_INTERNAL_URL]);

return (
<>
{" "}
Expand All @@ -45,26 +27,8 @@ export default function SatInfo({
<SatTabs
satAttributes={satAttributes}
STRAPI_URL={STRAPI_URL}
BACKEND_INTERNAL_URL={BACKEND_INTERNAL_URL}
/>

{/* Image container */}
<div className="w-full border-t-2 border-gray-600 xl:border-t-0">
<div className="flex h-full w-full items-center justify-center bg-black">
{imageURL ? (
is3DModel ? (
<Render3DMod url={imageURL} />
) : (
<Image
src={imageURL}
alt={satAttributes?.name ?? ""}
width={1600} // Set according to the aspect ratio of the image
height={0}
className="p-2"
/>
)
) : null}
</div>
</div>
</div>
</TabProvider>
</>
Expand Down
114 changes: 80 additions & 34 deletions frontend/src/app/satellites/[satelliteSlug]/satTabs.tsx
Original file line number Diff line number Diff line change
@@ -1,57 +1,103 @@
"use client";
import React from "react";
import React, { useEffect, useState } from "react";
import SatelliteDataHome from "@/components/satelliteData/SatelliteDataHome";
import { SatelliteNumber } from "@/lib/store";
import { SatAttributes } from "@/lib/utils";
import { useTabContext } from "../tabContext";
import SatImage from "./satImage";
import Render3DMod from "../render3DMod";
import Image from "next/image";
import dynamic from "next/dynamic";

const SatTelemetry = dynamic(() => import("./satTelemetry"), {
ssr: false,
});

export default function SatTabs({
satAttributes,
STRAPI_URL,
BACKEND_INTERNAL_URL,
}: {
satAttributes: SatAttributes;
STRAPI_URL: string | undefined;
BACKEND_INTERNAL_URL: string | undefined;
}) {
let noradId = Number(satAttributes?.catalogNumberNORAD) as SatelliteNumber;
const [imageURL, setImageURL] = useState<string | undefined>(undefined);
const [is3DModel, setIs3DModel] = useState<boolean>(false);
useEffect(() => {
let satelliteImage =
satAttributes?.satelliteImage?.data?.attributes?.url;
if (BACKEND_INTERNAL_URL && satelliteImage) {
const fullImage = BACKEND_INTERNAL_URL + satelliteImage;
setImageURL(fullImage);
setIs3DModel(
satelliteImage.endsWith(".glb") ||
satelliteImage.endsWith(".gltf") ||
satelliteImage.endsWith(".glb?"),
);
}
}, [satAttributes, BACKEND_INTERNAL_URL]);
const { selectedTab } = useTabContext();
return (
<div className="z-10 flex w-full flex-col border-gray-600 xl:border-r-2">
{selectedTab === "sat parameters" ? (
// Render the parameters of the satellite
<div className="z-10 flex w-full flex-col border-gray-600 xl:border-r-2">
<div className="border-b border-gray-600 bg-black p-5">
<div className="flex flex-row">
<p>NORAD ID: </p>
{noradId ? (
<a
href={`https://www.n2yo.com/satellite/?s=${noradId}`}
target="_blank"
className="ml-2 underline"
>
{noradId}
</a>
) : (
<span className="ml-2">
No NORAD ID has been assigned yet{" "}
</span>
)}
</div>
<div className="flex w-full flex-col border-2 border-gray-600 xl:flex-row">
<div className="z-10 flex w-full flex-col border-gray-600 xl:border-r-2">
{selectedTab === "sat parameters" ? (
// Render the parameters of the satellite
<div className="z-10 flex w-full flex-col border-gray-600 xl:border-r-2">
<div className="border-b border-gray-600 bg-black p-5">
<div className="flex flex-row">
<p>NORAD ID: </p>
{noradId ? (
<a
href={`https://www.n2yo.com/satellite/?s=${noradId}`}
target="_blank"
className="ml-2 underline"
>
{noradId}
</a>
) : (
<span className="ml-2">
No NORAD ID has been assigned yet{" "}
</span>
)}
</div>

<p className="text-gray-400">
{satAttributes?.massKg
? "Mass: " + satAttributes?.massKg + " kg"
: null}
</p>
</div>
{satAttributes?.missionStatus === "IN ORBIT" ? (
<div>
<SatelliteDataHome satelliteNum={noradId} />
<p className="text-gray-400">
{satAttributes?.massKg
? "Mass: " + satAttributes?.massKg + " kg"
: null}
</p>
</div>
) : null}
{satAttributes?.missionStatus === "IN ORBIT" ? (
<div>
<SatelliteDataHome satelliteNum={noradId} />
</div>
) : null}
</div>
) : selectedTab === "satellite image" ? (
<SatImage STRAPI_URL={STRAPI_URL} noradID={noradId} />
) : selectedTab === "satellite telemetry" ? (
<SatTelemetry STRAPI_URL={STRAPI_URL} noradID={noradId} />
) : null}
</div>
{/* Image container */}
{imageURL && selectedTab !== "satellite telemetry" ? (
<div className="w-full border-t-2 border-gray-600 xl:border-t-0">
<div className="flex h-full w-full items-center justify-center bg-black">
{is3DModel ? (
<Render3DMod url={imageURL} />
) : (
<Image
src={imageURL}
alt={satAttributes?.name ?? ""}
width={1600} // Set according to the aspect ratio of the image
height={0}
className="p-2"
/>
)}
</div>
</div>
) : selectedTab === "satellite image" ? (
<SatImage STRAPI_URL={STRAPI_URL} noradID={noradId} />
) : null}
</div>
);
Expand Down
Loading

0 comments on commit c74c0a0

Please sign in to comment.