Skip to content
Open
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1,401 changes: 1,361 additions & 40 deletions RocketControlUnitGUI/package-lock.json

Large diffs are not rendered by default.

15 changes: 14 additions & 1 deletion RocketControlUnitGUI/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
"@sveltejs/adapter-node": "^4.0.1",
"@tailwindcss/forms": "0.5.7",
"@tailwindcss/typography": "0.5.10",
"@types/leaflet": "^1.9.12",
"@types/node": "20.10.0",
"@typescript-eslint/eslint-plugin": "^6.0.0",
"@typescript-eslint/parser": "^6.0.0",
Expand All @@ -40,6 +41,18 @@
"type": "module",
"dependencies": {
"@floating-ui/dom": "1.5.3",
"pocketbase": "^0.20.3"
"@theatre/core": "^0.7.2",
"@theatre/studio": "^0.7.2",
"@threlte/core": "^7.3.1",
"@threlte/extras": "^8.11.5",
"@threlte/flex": "^1.0.3",
"@threlte/theatre": "^2.1.8",
"@types/three": "^0.169.0",
"chart": "^0.1.2",
"chart.js": "^4.4.4",
"chartjs": "^0.3.24",
"leaflet": "^1.9.4",
"pocketbase": "^0.20.3",
"three": "^0.169.0"
}
}
110 changes: 110 additions & 0 deletions RocketControlUnitGUI/src/data.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
// data.ts
import PocketBase from 'pocketbase';
import dotenv from "dotenv";



const ADMIN_EMAIL: string = import.meta.env.VITE_EMAIL;
const ADMIN_PASSWORD: string = import.meta.env.VITE_PASSWORD;

console.log(ADMIN_EMAIL);

const pb = new PocketBase('http://127.0.0.1:8090');
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This needs to change back to the PI IP

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Okay, will do!

pb.autoCancellation(false);

let isAuthenticated = false;



// Function to authenticate the admin user
export async function authenticate() {
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Authentication as well as subscribing is handled in usePocketbase hook (which may need some additions) in master so refactor to use that. Also, when using the hook we should make sure to pass around a global instance so that we are not opening duplicate pb connections like in this file.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I have to refactor my code to incorporate the usePocketbase hook now will I will be implementing soon!

if (!isAuthenticated) {
await pb.admins.authWithPassword(ADMIN_EMAIL, ADMIN_PASSWORD);
isAuthenticated = true;
}
}

type RecordData = { [key: string]: any };
export type AllData = { [collectionName: string]: RecordData[] };

// Fetch paginated data with existing logic
export async function fetchPaginatedData(
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should be extracted into its own hook that depends on Return<typeof usePocketbase>. Perhaps make a chart hook that encapsulates chart related logic to avoid global state for the chart system (this is not a big deal though, it would just be cleaner).

collectionName: string,
sendToChart: (data: RecordData[]) => void,
batchSize: number = 10
) {
console.log(`Fetching data from collection: ${collectionName}`);
let page = 1;
let hasMoreData = true;

try {
await authenticate(); // Authenticate once before fetching data
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I believe we only need to authenticate once, not every time we want to read/write to/from pocketbase.


while (hasMoreData) {
console.log('Fetching data from collection:', collectionName, 'Page:', page);
const records = await pb.collection(collectionName).getList(page, batchSize);
console.log('Fetched records:', records.items);

if (records.items.length === 0) {
console.warn(`No records found for ${collectionName} on page ${page}.`);
break;
}

const dynamicKeys = Object.keys(records.items[0]).filter(key => key !== 'id' && key !== 'created');

const transformedBatch: RecordData[] = records.items.map((record: RecordData) => {
const transformedRecord: RecordData = {};
dynamicKeys.forEach(key => {
transformedRecord[key] = record[key];
});
return transformedRecord;
});

console.log('Transformed batch:', transformedBatch);
sendToChart(transformedBatch);

if (records.items.length < batchSize) {
hasMoreData = false;
} else {
page += 1;
}
}
console.log('All data fetched for collection:', collectionName);
} catch (error) {
console.error('Error fetching paginated data for collection:', collectionName, error);
}
}

// Real-time subscription to a collection
export async function subscribeToCollection(
collectionName: string,
handleDataUpdate: (data: RecordData) => void
) {
try {
await authenticate(); // Ensure we're authenticated before subscribing

// Subscribe to the collection for any changes
pb.collection(collectionName).subscribe('*', function (e) {
console.log(`Received real-time update for collection ${collectionName}:`, e.record);
handleDataUpdate(e.record);
});

console.log(`Subscribed to real-time updates for collection: ${collectionName}`);
} catch (error) {
console.error(`Error subscribing to collection ${collectionName}:`, error);
}
}

// Function to unsubscribe from a collection (optional)
export async function unsubscribeFromCollection(collectionName: string) {
try {
await authenticate(); // Ensure we're authenticated before unsubscribing

// Unsubscribe from the collection
pb.collection(collectionName).unsubscribe('*');

console.log(`Unsubscribed from real-time updates for collection: ${collectionName}`);
} catch (error) {
console.error(`Error unsubscribing from collection ${collectionName}:`, error);
}
}
46 changes: 37 additions & 9 deletions RocketControlUnitGUI/src/routes/data/+page.svelte
Original file line number Diff line number Diff line change
@@ -1,11 +1,39 @@

<script lang="ts">
import LineChart from './LineChart.svelte';
import MapChart from './MapChart.svelte';
import RocketChart from './RocketChart.svelte';
import {Canvas} from '@threlte/core';
</script>

<main>
<!-- <div style="display: flex; flex-direction: column; align-items: center;">
<LineChart collection="Rcupressure" />
<LineChart collection="RcuTemperature" />
<LineChart collection="Imu" />
</div> -->
<!--
<MapChart /> -->
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

should this be commented out?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yeah, need to tweak that page cause I need to use divs for css, so that everything looks in its proper place

<div class="canvas-container">
<Canvas>
<RocketChart />
</Canvas>
</div>


</main>

<style>
main {
display: flex;
flex-direction: row;
align-items: center;
justify-content: center;
padding: 20px;
}


</script>

<svelte:head></svelte:head>

<main>
<p>DATA</p>
</main>
.canvas-container{
width:800px;
height:600px;
}
</style>

191 changes: 191 additions & 0 deletions RocketControlUnitGUI/src/routes/data/LineChart.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,191 @@
<script lang="ts">
import { onMount, onDestroy } from 'svelte';
import { fetchPaginatedData, subscribeToCollection, unsubscribeFromCollection } from '../../data';
import { Chart, LineController, LineElement, PointElement, LinearScale, Title, CategoryScale } from 'chart.js';

Chart.register(LineController, LineElement, PointElement, LinearScale, Title, CategoryScale);

interface RecordData {
[key: string]: any;
}

export let collection: string;
export let fields: string[] = [];

let chartData: RecordData[] = [];
let chart: Chart | null = null;
let canvasContainer: HTMLCanvasElement | null = null;
const MAX_DATA_POINTS = 10;
let isUpdating = false;
let dynamicFields: string[] = [];


async function getInitialData() {
try {
await fetchPaginatedData(collection, handleFirstBatch, 1);
} catch (error) {
console.error(`Error fetching initial data for ${collection}:`, error);
}
}

function sendToChart(batchData: RecordData[]) {
chartData = [...chartData, ...batchData];

if (chartData.length > MAX_DATA_POINTS) {
chartData = chartData.slice(-MAX_DATA_POINTS);
}

if (!isUpdating) {
isUpdating = true;
requestAnimationFrame(() => {
createOrUpdateChart();
isUpdating = false;
});
}
}

async function handleFirstBatch(batchData: RecordData[]) {
console.log('Fetched batch data for', collection, ':', batchData);
if (batchData.length > 0) {
determineFields(batchData[0]);
const transformedBatch = batchData.map(transformData);
console.log('Transformed batch data for', collection, ':', transformedBatch);
sendToChart(transformedBatch);
} else {
console.warn('No records fetched for', collection);
}
}

function determineFields(record: RecordData) {
if (fields.length === 0) {
dynamicFields = Object.keys(record).filter((key) => key !== 'id' && key !== 'created');
}
}

function transformData(record: RecordData): RecordData {
const dataFields = fields.length ? fields : dynamicFields;
const transformed: RecordData = {};
dataFields.forEach(field => {
transformed[field] = record[field];
});
return transformed;
}

function createOrUpdateChart() {
if (!canvasContainer) return;

const dataFields = fields.length ? fields : dynamicFields;
if (dataFields.length === 0) {
console.warn(`No fields available for chart creation for ${collection}.`);
return;
}

const chartConfigData = {
labels: chartData.map((_, index) => index.toString()),
datasets: dataFields.map((field) => ({
label: field,
data: chartData.map((d) => d[field] !== undefined ? d[field] : 0),
borderColor: generateColorForField(field),
fill: false,
})),
};

console.log('Chart configuration data for', collection, ':', chartConfigData);

if (chart) {
chart.data = chartConfigData;
chart.update();
} else {
chart = new Chart(canvasContainer, {
type: 'line',
data: chartConfigData,
options: {
responsive: true,
maintainAspectRatio: false,
scales: {
x: {
title: { display: true, text: 'Data Points', color: '#777' },
grid: { color: 'rgba(200, 200, 200, 0.1)' },
ticks: { color: '#777' }
},
y: {
title: { display: true, text: 'Value', color: '#777' },
beginAtZero: true,
grid: { color: 'rgba(200, 200, 200, 0.1)' },
ticks: { color: '#777' }
},
},
plugins: {
legend: {
display: true,
position: 'top',
labels: { color: '#333', font: { size: 12, weight: 500 } }
},
tooltip: {
backgroundColor: 'rgba(0,0,0,0.7)',
titleColor: '#FFF',
bodyColor: '#FFF',
cornerRadius: 4,
}
},
elements: {
line: { borderWidth: 2 },
point: { radius: 3, hoverRadius: 5, backgroundColor: '#FFF', borderWidth: 1.5 },
}
},
});
}
}

function generateColorForField(field: string): string {
const colors = ['#FF0000', '#00FF00', '#0000FF', '#FFFF00', '#FF00FF', '#00FFFF'];
const index = fields.length ? fields.indexOf(field) : dynamicFields.indexOf(field);
return colors[index % colors.length];
}

// Function to handle real-time updates
function handleRealTimeUpdate(record: RecordData) {
const transformedRecord = transformData(record);
sendToChart([transformedRecord]);
}

// On mount, fetch initial data and start real-time updates
onMount(() => {
getInitialData(); // Fetch initial data
subscribeToCollection(collection, handleRealTimeUpdate); // Subscribe to real-time updates
});

// On destroy, clean up subscriptions
onDestroy(() => {
unsubscribeFromCollection(collection); // Unsubscribe from real-time updates when component is destroyed
});
</script>

<div class="chart-container">
<h1>{collection} Graph</h1>
<canvas bind:this={canvasContainer}></canvas>
</div>

<style>
.chart-container {
display: flex;
flex-direction: column;
width: 400px;
height: 300px;
position: relative;
background: linear-gradient(135deg, #495a8f 0%, #495f9f 100%);
border-radius: 12px;
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
padding: 20px;
margin: 20px;
justify-content: space-evenly;
align-items: center;
}

canvas {
width: 100%;
height: 100%;
display: block;
}
</style>

Loading