From 14674363020ce0ddd815e0190b8dcb3824b94450 Mon Sep 17 00:00:00 2001 From: Felix Erdmann Date: Tue, 24 Oct 2023 11:56:11 +0200 Subject: [PATCH] feat: interval data upload --- ios/App/App/capacitor.config.json | 2 +- src/components/Device/SettingsDrawer.tsx | 49 +++----- src/components/Map/ControlBar.tsx | 10 +- src/components/Wizard/ConnectionSelection.tsx | 15 +-- src/components/Wizard/SelectDevice.tsx | 10 +- src/components/Wizard/WizardDrawer.tsx | 45 ++----- src/lib/api/openSenseMapClient.ts | 4 +- src/lib/store/useUploadStore.ts | 11 ++ src/lib/useSenseBox.ts | 34 +----- src/lib/useUploadToOpenSenseMap.ts | 111 ++++++++++++------ 10 files changed, 132 insertions(+), 159 deletions(-) create mode 100644 src/lib/store/useUploadStore.ts diff --git a/ios/App/App/capacitor.config.json b/ios/App/App/capacitor.config.json index 7b229ae..091c11c 100644 --- a/ios/App/App/capacitor.config.json +++ b/ios/App/App/capacitor.config.json @@ -3,6 +3,6 @@ "appName": "senseBox:Bike", "webDir": "out", "server": { - "url": "http://192.168.0.220:3000" + "url": "http://192.168.2.135:3000" } } diff --git a/src/components/Device/SettingsDrawer.tsx b/src/components/Device/SettingsDrawer.tsx index d2ad551..9860436 100644 --- a/src/components/Device/SettingsDrawer.tsx +++ b/src/components/Device/SettingsDrawer.tsx @@ -8,7 +8,13 @@ import { DialogTrigger, } from '@/components/ui/dialog' import { zodResolver } from '@hookform/resolvers/zod' -import { Cog, Settings2Icon, SettingsIcon, UserCog2 } from 'lucide-react' +import { + Cog, + ExternalLinkIcon, + Settings2Icon, + SettingsIcon, + UserCog2, +} from 'lucide-react' import { useForm } from 'react-hook-form' import * as z from 'zod' import { @@ -80,12 +86,15 @@ export default function SettingsDrawer() { return ( setOpen(false)}> - setOpen(true)}> + setOpen(true)} + className="focus:outline-none" + > - +
@@ -173,22 +182,7 @@ function SettingsDrawerFooter() { target="_blank" > openSenseMap - + re:edu - +
diff --git a/src/components/Map/ControlBar.tsx b/src/components/Map/ControlBar.tsx index 99504c1..47b5748 100644 --- a/src/components/Map/ControlBar.tsx +++ b/src/components/Map/ControlBar.tsx @@ -2,11 +2,11 @@ import { PauseIcon, PlayIcon } from '@heroicons/react/24/outline' import { Card } from '../ui/card' import useSenseBox from '@/lib/useSenseBox' import { BluetoothIcon, BluetoothOffIcon, Circle, Square } from 'lucide-react' +import useUploadToOpenSenseMap from '@/lib/useUploadToOpenSenseMap' export default function ControlBar() { - const { connect, isConnected, disconnect, uploadData } = useSenseBox() - - const isRecording = false + const { connect, isConnected, disconnect } = useSenseBox() + const { isRecording, start, stop } = useUploadToOpenSenseMap() return ( @@ -21,12 +21,12 @@ export default function ControlBar() { {isRecording ? ( disconnect()} + onClick={() => stop()} /> ) : ( uploadData()} + onClick={() => start()} /> )} diff --git a/src/components/Wizard/ConnectionSelection.tsx b/src/components/Wizard/ConnectionSelection.tsx index 70148b2..e2d3cda 100644 --- a/src/components/Wizard/ConnectionSelection.tsx +++ b/src/components/Wizard/ConnectionSelection.tsx @@ -4,7 +4,7 @@ import { Button } from '../ui/button' import Logo from '../../../public/bike.png' import Image from 'next/image' import Link from 'next/link' -import { ArrowLeft, ArrowRight, CheckCircle } from 'lucide-react' +import { ArrowLeft, ArrowRight, CheckCircle, CircleIcon } from 'lucide-react' import useSenseBox from '@/lib/useSenseBox' import { cn } from '@/lib/utils' import PreviewModal from '../Device/PreviewModal' @@ -30,16 +30,13 @@ export default function ConnectionSelection({ className="w-fit" onClick={() => swiper.slidePrev()} > - Zurück + Bearbeiten -

Verknüpfung erfolgreich

-
-

Verknüfte Box:

-
- - {selectedBox?.name} -
+

Verknüpfte Box

+
+ + {selectedBox?.name}
+

Box auswählen

{boxes && boxes.map(box => ( diff --git a/src/components/Wizard/WizardDrawer.tsx b/src/components/Wizard/WizardDrawer.tsx index 3e276f1..9b1ccde 100644 --- a/src/components/Wizard/WizardDrawer.tsx +++ b/src/components/Wizard/WizardDrawer.tsx @@ -1,5 +1,5 @@ import { Drawer } from 'vaul' -import { AlertOctagon, Check, UserCog2 } from 'lucide-react' +import { AlertOctagon, Check, ExternalLinkIcon, UserCog2 } from 'lucide-react' import ConnectionSelection from '@/components/Wizard/ConnectionSelection' import { Swiper, SwiperSlide } from 'swiper/react' import { Navigation } from 'swiper/modules' @@ -26,7 +26,10 @@ export default function WizardDrawer() { return ( setOpen(false)}> - setOpen(true)}> + setOpen(true)} + className="focus:outline-none" + >
{(!isLoggedIn || !selectedBox) && ( @@ -43,12 +46,12 @@ export default function WizardDrawer() { - +
void }) { target="_blank" > openSenseMap - + void }) { target="_blank" > re:edu - + {isLoggedIn && (

void +} + +export const useUploadStore = create()(set => ({ + lastUpload: undefined, + setLastUpload: lastUpload => set({ lastUpload }), +})) diff --git a/src/lib/useSenseBox.ts b/src/lib/useSenseBox.ts index 13fea66..dd665e2 100644 --- a/src/lib/useSenseBox.ts +++ b/src/lib/useSenseBox.ts @@ -31,6 +31,11 @@ const BackgroundGeolocation = registerPlugin( 'BackgroundGeolocation', ) +/** + * Parses the data received from the SenseBox and returns an array of values. + * @param data - The data received from the SenseBox as a DataView. + * @returns An array of values parsed from the data. + */ function parsePackages(data: DataView) { const packages = data.byteLength / 4 @@ -47,7 +52,6 @@ export default function useSenseBox(timestampInterval: number = 500) { namePrefix: 'senseBox', }) const { values, setValues } = useSenseBoxValuesStore() - const { selectedBox } = useAuthStore() const { useSenseBoxGPS } = useSettingsStore() const useSenseBoxGPSRef = useRef() useSenseBoxGPSRef.current = useSenseBoxGPS @@ -60,10 +64,6 @@ export default function useSenseBox(timestampInterval: number = 500) { const locationRef = useRef() locationRef.current = location - const [lastUploadTimestamp, setLastUploadTimestamp] = useState( - new Date('1970-01-01'), - ) - useEffect(() => { if (useSenseBoxGPS) { if (watcherId) @@ -103,10 +103,8 @@ export default function useSenseBox(timestampInterval: number = 500) { }) return () => { - console.log('in unmount') if (!watcherId) return - console.log('removing watcher', watcherId) BackgroundGeolocation.removeWatcher({ id: watcherId, }) @@ -223,27 +221,6 @@ export default function useSenseBox(timestampInterval: number = 500) { ]) } - const uploadValues = async () => { - if (!selectedBox) { - throw new Error('No box selected.') - } - - const data = values - .slice(-2500) - .flatMap(record => match(selectedBox, record)) - .map(record => ({ - ...record, - value: record.value.toFixed(2), - })) - - const latestTimestamp = Math.max( - ...data.map(e => new Date(e.createdAt).getTime()), - ) - setLastUploadTimestamp(new Date(latestTimestamp)) - - uploadData(selectedBox, data) - } - return { isConnected, connect, @@ -251,6 +228,5 @@ export default function useSenseBox(timestampInterval: number = 500) { disconnect, resetValues, send, - uploadData: uploadValues, } } diff --git a/src/lib/useUploadToOpenSenseMap.ts b/src/lib/useUploadToOpenSenseMap.ts index 150cbdb..865bc4b 100644 --- a/src/lib/useUploadToOpenSenseMap.ts +++ b/src/lib/useUploadToOpenSenseMap.ts @@ -1,48 +1,93 @@ -import { use, useState } from 'react' +import { useEffect, useRef, useState } from 'react' import { useAuthStore } from './store/useAuthStore' +import { + senseBoxDataRecord, + useSenseBoxValuesStore, +} from './store/useSenseBoxValuesStore' +import match from './senseBoxSensorIdMatcher' +import { uploadData } from './api/openSenseMapClient' +import { useUploadStore } from './store/useUploadStore' -interface UploadToOpenSenseMapProps { - sensorid: string - value: number - createdAt: string -} - -const useUploadToOpenSenseMap = () => { +const useUploadToOpenSenseMap = (interval: number = 10000) => { const [isLoading, setIsLoading] = useState(false) - const [error, setError] = useState(null) - const [success, setSuccess] = useState(false) - const boxId = useAuthStore(state => state.selectedBox) + const selectedBox = useAuthStore(state => state.selectedBox) + const values = useSenseBoxValuesStore(state => state.values) + const valuesRef = useRef() + valuesRef.current = values - const uploadToOpenSenseMap = async (data: UploadToOpenSenseMapProps) => { - setIsLoading(true) - try { - const response = await fetch( - `https://api.opensensemap.org/boxes/${boxId}/sensors/${data.sensorid}/data`, - { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - }, - body: JSON.stringify({ - value: data.value, // Deine Messwertdaten hier - createdAt: data.createdAt, - }), - }, + const [intervalId, setIntervalId] = useState() + const lastUpload = useUploadStore(state => state.lastUpload) + const lastUploadRef = useRef() + lastUploadRef.current = lastUpload + const setLastUpload = useUploadStore(state => state.setLastUpload) + + // useEffect(() => { + // return () => { + // console.log('in effect cleanup') + // clearInterval(intervalId) + // setIntervalId(undefined) + // } + // }, [intervalId]) + + function start() { + const intervalId = setInterval(() => { + uploadToOpenSenseMap() + }, interval) + setIntervalId(intervalId) + } + + function stop() { + clearInterval(intervalId) + setIntervalId(undefined) + } + + async function uploadToOpenSenseMap() { + if (!selectedBox) { + throw new Error('No box selected.') + } + if (!valuesRef.current) { + throw new Error('No values.') + } + + const data = valuesRef.current + .flatMap(record => match(selectedBox, record)) + .map(record => ({ + ...record, + value: record.value.toFixed(2), + })) + .slice(-2500) // max data to upload + + let filteredData = data + + if (lastUploadRef.current) { + filteredData = data.filter( + record => + new Date(record.createdAt).getTime() > + lastUploadRef.current!.getTime(), ) + } - if (!response.ok) { - throw new Error('Fehler beim Hochladen der Daten') - } + if (filteredData.length === 0) { + console.log('No new data to upload.') + return + } - setSuccess(true) - } catch (err: any) { - setError(err) + try { + setIsLoading(true) + await uploadData(selectedBox, filteredData) + const maxTimestamp = new Date( + Math.max(...data.map(record => new Date(record.createdAt).getTime())), + ) + console.log('maxTimestamp', maxTimestamp) + setLastUpload(maxTimestamp) + } catch (error) { + throw error } finally { setIsLoading(false) } } - return { isLoading, success, error, uploadToOpenSenseMap } + return { isRecording: intervalId !== undefined, isLoading, start, stop } } export default useUploadToOpenSenseMap