From ea61e157ab038742c4bdcb8e0e58e9ad26ebfe75 Mon Sep 17 00:00:00 2001 From: Vasilisk9812 Date: Sat, 23 May 2026 13:04:06 +0900 Subject: [PATCH] added tracking feature(not tested) --- src/lib/api/index.ts | 1 + src/lib/api/telemetry.ts | 35 ++++++ .../features/tracking/TelemetryPanel.svelte | 106 ++++++++++++++---- src/lib/features/tracking/index.ts | 1 + .../tracking/telemetryStore.svelte.ts | 99 ++++++++++++++++ src/lib/i18n/locales/en.json | 13 +++ src/lib/i18n/locales/ru.json | 13 +++ src/routes/track/+page.svelte | 66 ++++++++++- 8 files changed, 309 insertions(+), 25 deletions(-) create mode 100644 src/lib/api/telemetry.ts create mode 100644 src/lib/features/tracking/telemetryStore.svelte.ts diff --git a/src/lib/api/index.ts b/src/lib/api/index.ts index 96fc068..29b43d2 100644 --- a/src/lib/api/index.ts +++ b/src/lib/api/index.ts @@ -1,4 +1,5 @@ export { api, ApiError, API_BASE_URL, setUnauthorizedHandler } from './client'; +export { telemetryApi, buildWsUrl, type RawTelemetryPacket } from './telemetry'; export { pointsApi } from './points'; export { profilesApi } from './profiles'; export { scenariosApi } from './scenarios'; diff --git a/src/lib/api/telemetry.ts b/src/lib/api/telemetry.ts new file mode 100644 index 0000000..ff1d1c4 --- /dev/null +++ b/src/lib/api/telemetry.ts @@ -0,0 +1,35 @@ +import { api, API_BASE_URL } from './client'; + +export interface RawTelemetryPacket { + id: string; + timestamp: number; // unix seconds + lat: number; + lon: number; + alt: number; + payload: Record; + raw_data: Record; +} + +/** Derives a WebSocket URL from the configured API base URL. */ +export function buildWsUrl(satelliteId: string): string { + let base = API_BASE_URL; + if (!base.startsWith('http')) { + base = `${window.location.protocol}//${window.location.host}${base}`; + } + const url = new URL(base); + url.protocol = url.protocol === 'https:' ? 'wss:' : 'ws:'; + return `${url.origin}${url.pathname}/ws/satellite/${satelliteId}/telemetry/`; +} + +export const telemetryApi = { + fetchHistory: async ( + satelliteId: string, + params?: { from?: number; till?: number }, + ): Promise => { + const res = await api.get( + `/${satelliteId}/telemetry/`, + { query: params }, + ); + return Array.isArray(res) ? res : res.results; + }, +}; diff --git a/src/lib/features/tracking/TelemetryPanel.svelte b/src/lib/features/tracking/TelemetryPanel.svelte index 3bb5428..c4de83c 100644 --- a/src/lib/features/tracking/TelemetryPanel.svelte +++ b/src/lib/features/tracking/TelemetryPanel.svelte @@ -1,36 +1,100 @@ - - - - + +
+ + {#if telemetryStore.status === 'idle'} + + {:else} + + {/if} +
- - - - +
+ + + {$t(`tracking.status_${telemetryStore.status}`)} + +
+ {#if telemetryStore.error} + {telemetryStore.error} + {/if}
- - - - - - + {#if telemetryStore.latest} + + + + + + + + + + + + + + + + + + + + + + + {$t('tracking.packetCount', { count: telemetryStore.points.length })} + + {:else if telemetryStore.status !== 'idle'} + {$t('tracking.waitingData')} + {/if}
diff --git a/src/lib/features/tracking/index.ts b/src/lib/features/tracking/index.ts index 17dbca6..2ee3723 100644 --- a/src/lib/features/tracking/index.ts +++ b/src/lib/features/tracking/index.ts @@ -1 +1,2 @@ export { default as TelemetryPanel } from './TelemetryPanel.svelte'; +export { telemetryStore, type TrackingStatus } from './telemetryStore.svelte'; diff --git a/src/lib/features/tracking/telemetryStore.svelte.ts b/src/lib/features/tracking/telemetryStore.svelte.ts new file mode 100644 index 0000000..2d0f2b9 --- /dev/null +++ b/src/lib/features/tracking/telemetryStore.svelte.ts @@ -0,0 +1,99 @@ +import { telemetryApi, buildWsUrl, type RawTelemetryPacket } from '$api/telemetry'; +import { parseTelemetry, type TelemetryPoint, type Telemetry } from '$domain'; + +export type TrackingStatus = 'idle' | 'connecting' | 'connected' | 'error'; + +function toPoint(p: RawTelemetryPacket): TelemetryPoint { + return { + latitude: p.lat, + longitude: p.lon, + altitude: p.alt, + datetime: new Date(p.timestamp * 1000).toISOString(), + payload: JSON.stringify(p.payload), + }; +} + +class TelemetryStore { + satelliteId = $state(''); + status = $state('idle'); + error = $state(null); + points = $state([]); + + #ws: WebSocket | null = null; + + get latest(): TelemetryPoint | null { + return this.points[this.points.length - 1] ?? null; + } + + get telemetry(): Telemetry | null { + if (this.points.length === 0) return null; + try { + return parseTelemetry(this.points); + } catch { + return null; + } + } + + async connect(id: string): Promise { + const UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i; + if (!UUID_RE.test(id)) { + this.status = 'error'; + this.error = `Invalid satellite ID — expected a UUID (e.g. 550e8400-e29b-41d4-a716-446655440000)`; + return; + } + + this.disconnect(); + this.satelliteId = id; + this.status = 'connecting'; + this.error = null; + this.points = []; + + // Load historical packets first — non-fatal if it fails + try { + const history = await telemetryApi.fetchHistory(id); + // API returns newest-first; reverse to chronological order + this.points = [...history].reverse().map(toPoint); + } catch (e) { + console.warn('[telemetry] history fetch failed:', e); + } + + const ws = new WebSocket(buildWsUrl(id)); + this.#ws = ws; + + ws.onopen = () => { + this.status = 'connected'; + }; + + ws.onmessage = ({ data }) => { + try { + const packet = JSON.parse(data) as { error?: string } & RawTelemetryPacket; + if (!packet.error) { + this.points = [...this.points, toPoint(packet)]; + } + } catch { + // ignore malformed frames + } + }; + + ws.onerror = () => { + this.status = 'error'; + this.error = 'WebSocket connection failed'; + }; + + ws.onclose = () => { + if (this.status !== 'idle') this.status = 'idle'; + this.#ws = null; + }; + } + + disconnect(): void { + this.#ws?.close(); + this.#ws = null; + this.status = 'idle'; + this.satelliteId = ''; + this.points = []; + this.error = null; + } +} + +export const telemetryStore = new TelemetryStore(); diff --git a/src/lib/i18n/locales/en.json b/src/lib/i18n/locales/en.json index fcd9967..6c8dd5a 100644 --- a/src/lib/i18n/locales/en.json +++ b/src/lib/i18n/locales/en.json @@ -163,6 +163,19 @@ "header": "Coordinate select mode", "body": "Click the map to pick coordinates" }, + "tracking": { + "satelliteId": "Satellite ID", + "satelliteIdPlaceholder": "Enter satellite UUID...", + "connect": "Connect", + "disconnect": "Disconnect", + "status": "Status", + "status_idle": "Idle", + "status_connecting": "Connecting", + "status_connected": "Connected", + "status_error": "Error", + "packetCount": "{count} packets received", + "waitingData": "Waiting for data..." + }, "forecast": { "success": "Forecast request", "successBody": "Forecast request successful!", diff --git a/src/lib/i18n/locales/ru.json b/src/lib/i18n/locales/ru.json index eeb2149..d86c1e8 100644 --- a/src/lib/i18n/locales/ru.json +++ b/src/lib/i18n/locales/ru.json @@ -163,6 +163,19 @@ "header": "Режим выбора координат", "body": "Кликните на карту, чтобы выбрать координаты" }, + "tracking": { + "satelliteId": "ID спутника", + "satelliteIdPlaceholder": "Введите UUID спутника...", + "connect": "Подключиться", + "disconnect": "Отключиться", + "status": "Статус", + "status_idle": "Ожидание", + "status_connecting": "Подключение", + "status_connected": "Подключено", + "status_error": "Ошибка", + "packetCount": "Получено пакетов: {count}", + "waitingData": "Ожидание данных..." + }, "forecast": { "success": "Запрос прогноза", "successBody": "Запрос прогноза успешно выполнен!", diff --git a/src/routes/track/+page.svelte b/src/routes/track/+page.svelte index e513ea3..008216a 100644 --- a/src/routes/track/+page.svelte +++ b/src/routes/track/+page.svelte @@ -1,20 +1,78 @@
- +