diff --git a/README.md b/README.md index e1859df..46f6999 100644 --- a/README.md +++ b/README.md @@ -64,7 +64,7 @@ It is designed as a complete end to end IoT product, from embedded firmware to c | Firmware | C · Zephyr RTOS · nRF Connect SDK | | Protocols | BLE 5.0 · I2C | | Mobile | React Native · TypeScript · Kotlin (native modules) | -| Backend | NestJS · PostgreSQL · InfluxDB · Redis | +| Backend | NestJS · PostgreSQL · InfluxDB | | Infrastructure | Fly.io · GitHub Actions · Cloudflare | --- diff --git a/apps/backend/src/readings/readings.service.ts b/apps/backend/src/readings/readings.service.ts index 13e73a2..ba04080 100644 --- a/apps/backend/src/readings/readings.service.ts +++ b/apps/backend/src/readings/readings.service.ts @@ -41,14 +41,14 @@ export class ReadingsService { deviceId: deviceId, createdAt: Between(new Date(from), new Date(to)), }, - take: limit, + ...(limit ? { take: limit } : {}), }); } else { deviceReadings = await this.readingsRepository.find({ where: { deviceId: deviceId, }, - take: limit, + ...(limit ? { take: limit } : {}), }); } diff --git a/apps/mobile/package-lock.json b/apps/mobile/package-lock.json index 20dc64c..55efd06 100644 --- a/apps/mobile/package-lock.json +++ b/apps/mobile/package-lock.json @@ -35,6 +35,8 @@ "react-native": "0.81.5", "react-native-ble-plx": "^3.5.1", "react-native-gesture-handler": "~2.28.0", + "react-native-gifted-charts": "^1.4.77", + "react-native-linear-gradient": "^2.8.3", "react-native-reanimated": "~4.1.1", "react-native-safe-area-context": "~5.6.0", "react-native-screens": "~4.16.0", @@ -4592,6 +4594,13 @@ "node": ">=0.6" } }, + "node_modules/boolbase": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz", + "integrity": "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==", + "license": "ISC", + "peer": true + }, "node_modules/bplist-creator": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/bplist-creator/-/bplist-creator-0.1.0.tgz", @@ -5126,6 +5135,60 @@ "hyphenate-style-name": "^1.0.3" } }, + "node_modules/css-select": { + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/css-select/-/css-select-5.2.2.tgz", + "integrity": "sha512-TizTzUddG/xYLA3NXodFM0fSbNizXjOKhqiQQwvhlspadZokn1KDy0NZFS0wuEubIYAV5/c1/lAr0TaaFXEXzw==", + "license": "BSD-2-Clause", + "peer": true, + "dependencies": { + "boolbase": "^1.0.0", + "css-what": "^6.1.0", + "domhandler": "^5.0.2", + "domutils": "^3.0.1", + "nth-check": "^2.0.1" + }, + "funding": { + "url": "https://github.com/sponsors/fb55" + } + }, + "node_modules/css-tree": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-1.1.3.tgz", + "integrity": "sha512-tRpdppF7TRazZrjJ6v3stzv93qxRcSsFmW6cX0Zm2NVKpxE1WV1HblnghVv9TreireHkqI/VDEsfolRF1p6y7Q==", + "license": "MIT", + "peer": true, + "dependencies": { + "mdn-data": "2.0.14", + "source-map": "^0.6.1" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/css-tree/node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "license": "BSD-3-Clause", + "peer": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/css-what": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/css-what/-/css-what-6.2.2.tgz", + "integrity": "sha512-u/O3vwbptzhMs3L1fQE82ZSLHQQfto5gyZzwteVIEyeaY5Fc7R4dapF/BvRoSYFeqfBk4m0V1Vafq5Pjv25wvA==", + "license": "BSD-2-Clause", + "peer": true, + "engines": { + "node": ">= 6" + }, + "funding": { + "url": "https://github.com/sponsors/fb55" + } + }, "node_modules/csstype": { "version": "3.2.3", "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", @@ -5351,6 +5414,65 @@ "node": ">=0.10.0" } }, + "node_modules/dom-serializer": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-2.0.0.tgz", + "integrity": "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==", + "license": "MIT", + "peer": true, + "dependencies": { + "domelementtype": "^2.3.0", + "domhandler": "^5.0.2", + "entities": "^4.2.0" + }, + "funding": { + "url": "https://github.com/cheeriojs/dom-serializer?sponsor=1" + } + }, + "node_modules/domelementtype": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.3.0.tgz", + "integrity": "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fb55" + } + ], + "license": "BSD-2-Clause", + "peer": true + }, + "node_modules/domhandler": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-5.0.3.tgz", + "integrity": "sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==", + "license": "BSD-2-Clause", + "peer": true, + "dependencies": { + "domelementtype": "^2.3.0" + }, + "engines": { + "node": ">= 4" + }, + "funding": { + "url": "https://github.com/fb55/domhandler?sponsor=1" + } + }, + "node_modules/domutils": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/domutils/-/domutils-3.2.2.tgz", + "integrity": "sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw==", + "license": "BSD-2-Clause", + "peer": true, + "dependencies": { + "dom-serializer": "^2.0.0", + "domelementtype": "^2.3.0", + "domhandler": "^5.0.3" + }, + "funding": { + "url": "https://github.com/fb55/domutils?sponsor=1" + } + }, "node_modules/dotenv": { "version": "16.4.7", "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.4.7.tgz", @@ -5419,6 +5541,19 @@ "node": ">= 0.8" } }, + "node_modules/entities": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", + "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", + "license": "BSD-2-Clause", + "peer": true, + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, "node_modules/env-editor": { "version": "0.4.2", "resolved": "https://registry.npmjs.org/env-editor/-/env-editor-0.4.2.tgz", @@ -7318,6 +7453,17 @@ "node": ">=6" } }, + "node_modules/gifted-charts-core": { + "version": "0.1.81", + "resolved": "https://registry.npmjs.org/gifted-charts-core/-/gifted-charts-core-0.1.81.tgz", + "integrity": "sha512-plgJSbKB0Lxp2KQ/Fvj1qbOhiy6wxPiZ0Av60iFHpSSu6YlJjYhwczx5w2/iJQdZYb851OFMHgN/pgTNVLv6dA==", + "license": "MIT", + "peerDependencies": { + "react": "*", + "react-native": "*", + "react-native-svg": "*" + } + }, "node_modules/glob": { "version": "13.0.6", "resolved": "https://registry.npmjs.org/glob/-/glob-13.0.6.tgz", @@ -9023,6 +9169,13 @@ "node": ">= 0.4" } }, + "node_modules/mdn-data": { + "version": "2.0.14", + "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.0.14.tgz", + "integrity": "sha512-dn6wd0uw5GsdswPFfsgMp5NSB0/aDe6fK94YJV/AJDYXL6HVLWBsxeq7js7Ad+mU2K9LAlwpk6kN2D5mwCPVow==", + "license": "CC0-1.0", + "peer": true + }, "node_modules/memoize-one": { "version": "5.2.1", "resolved": "https://registry.npmjs.org/memoize-one/-/memoize-one-5.2.1.tgz", @@ -9612,6 +9765,19 @@ "node": ">=10" } }, + "node_modules/nth-check": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/nth-check/-/nth-check-2.1.1.tgz", + "integrity": "sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==", + "license": "BSD-2-Clause", + "peer": true, + "dependencies": { + "boolbase": "^1.0.0" + }, + "funding": { + "url": "https://github.com/fb55/nth-check?sponsor=1" + } + }, "node_modules/nullthrows": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/nullthrows/-/nullthrows-1.1.1.tgz", @@ -10520,6 +10686,30 @@ "react-native": "*" } }, + "node_modules/react-native-gifted-charts": { + "version": "1.4.77", + "resolved": "https://registry.npmjs.org/react-native-gifted-charts/-/react-native-gifted-charts-1.4.77.tgz", + "integrity": "sha512-Ul4juHO0Gicng139i61AzQ3h4kLM25dzid1rU+d3d7PuHsI4UypgeChz/luaHMZaWgXkALjq6DLqaM2Y2AEhrA==", + "license": "MIT", + "dependencies": { + "gifted-charts-core": "0.1.81" + }, + "peerDependencies": { + "expo-linear-gradient": "*", + "react": "*", + "react-native": "*", + "react-native-linear-gradient": "*", + "react-native-svg": "*" + }, + "peerDependenciesMeta": { + "expo-linear-gradient": { + "optional": true + }, + "react-native-linear-gradient": { + "optional": true + } + } + }, "node_modules/react-native-is-edge-to-edge": { "version": "1.3.1", "resolved": "https://registry.npmjs.org/react-native-is-edge-to-edge/-/react-native-is-edge-to-edge-1.3.1.tgz", @@ -10530,6 +10720,16 @@ "react-native": "*" } }, + "node_modules/react-native-linear-gradient": { + "version": "2.8.3", + "resolved": "https://registry.npmjs.org/react-native-linear-gradient/-/react-native-linear-gradient-2.8.3.tgz", + "integrity": "sha512-KflAXZcEg54PXkLyflaSZQ3PJp4uC4whM7nT/Uot9m0e/qxFV3p6uor1983D1YOBJbJN7rrWdqIjq0T42jOJyA==", + "license": "MIT", + "peerDependencies": { + "react": "*", + "react-native": "*" + } + }, "node_modules/react-native-reanimated": { "version": "4.1.7", "resolved": "https://registry.npmjs.org/react-native-reanimated/-/react-native-reanimated-4.1.7.tgz", @@ -10582,6 +10782,21 @@ "react-native": "*" } }, + "node_modules/react-native-svg": { + "version": "15.15.5", + "resolved": "https://registry.npmjs.org/react-native-svg/-/react-native-svg-15.15.5.tgz", + "integrity": "sha512-L4go5jA+GWutdJ/JucuN20cjAbMg1HmMtAP+wZ+3JLCf6Jd0bhXQHxciRP/AQm/FlrIEZwkMcHNZP+FXAiic0w==", + "license": "MIT", + "peer": true, + "dependencies": { + "css-select": "^5.1.0", + "css-tree": "^1.1.3" + }, + "peerDependencies": { + "react": "*", + "react-native": "*" + } + }, "node_modules/react-native-web": { "version": "0.21.2", "resolved": "https://registry.npmjs.org/react-native-web/-/react-native-web-0.21.2.tgz", diff --git a/apps/mobile/package.json b/apps/mobile/package.json index b990d0f..eba3597 100644 --- a/apps/mobile/package.json +++ b/apps/mobile/package.json @@ -38,6 +38,8 @@ "react-native": "0.81.5", "react-native-ble-plx": "^3.5.1", "react-native-gesture-handler": "~2.28.0", + "react-native-gifted-charts": "^1.4.77", + "react-native-linear-gradient": "^2.8.3", "react-native-reanimated": "~4.1.1", "react-native-safe-area-context": "~5.6.0", "react-native-screens": "~4.16.0", diff --git a/apps/mobile/src/api/axios.ts b/apps/mobile/src/api/axios.ts index 66dc20b..2ab7f4c 100644 --- a/apps/mobile/src/api/axios.ts +++ b/apps/mobile/src/api/axios.ts @@ -2,7 +2,7 @@ import axios from "axios"; import { authStore } from "../stores/authStore"; export const instance = axios.create({ - baseURL: "http://192.168.1.71:3000/", + baseURL: "http://192.168.1.74:3000/", timeout: 10000, }); diff --git a/apps/mobile/src/api/readingsService.ts b/apps/mobile/src/api/readingsService.ts index fe27639..8ce03dc 100644 --- a/apps/mobile/src/api/readingsService.ts +++ b/apps/mobile/src/api/readingsService.ts @@ -24,3 +24,25 @@ export const postReading = async ( console.log(error); } }; + +export const getReadings = async ( + deviceId: string, + from: string, + to: string, +) => { + try { + console.log("device id", deviceId); + + const response = await instance.get("readings", { + params: { + deviceId: deviceId, + from: from, + to: to, + }, + }); + console.log("responses", response.data); + return response.data; + } catch (error) { + console.log(error); + } +}; diff --git a/apps/mobile/src/screens/dashboard.tsx b/apps/mobile/src/screens/dashboard.tsx index 8b715e3..ed42bc8 100644 --- a/apps/mobile/src/screens/dashboard.tsx +++ b/apps/mobile/src/screens/dashboard.tsx @@ -1,20 +1,108 @@ import { View, Text } from "react-native"; import Ionicons from "@expo/vector-icons/MaterialIcons"; +import { getReadings } from "../api/readingsService"; +import { deviceStore } from "../stores/deviceStore"; +import { useEffect, useState } from "react"; +import { LineChart } from "react-native-gifted-charts"; +import { useFocusEffect } from "@react-navigation/native"; +import React, { useCallback } from "react"; + +type sensorReading = { + id: number | null; + deviceId: string | null; + createdAt: Date | null; + temperature_c: number | null; + humidity_pct: number | null; + pressure_hpa: number | null; + pm1_0_ugm3: number | null; + pm2_5_ugm3: number | null; + pm10_ugm3: number | null; +}; export function DashboardScreen() { + const [connectedDevice, setConnectedDevice] = useState( + () => deviceStore.getState().deviceId, + ); + const [readings, setReadings] = useState(null); + + console.log(readings); + + useEffect(() => { + return deviceStore.subscribe((state) => { + setConnectedDevice(state.deviceId); + }); + }, []); + + useFocusEffect( + useCallback(() => { + (async () => { + const to = Date.now(); + + const from = Date.now() - 24 * 60 * 60 * 1000; + + const readings = await getReadings( + connectedDevice, + new Date(from).toISOString(), + new Date(to).toISOString(), + ); + + setReadings(readings); + })(); + }, [connectedDevice]), + ); + + const chartData = readings?.map((r, index) => ({ + value: r.temperature_c ?? 0, + label: + index % 4 == 0 && r.createdAt + ? new Date(r.createdAt).toLocaleTimeString([], { + hour: "2-digit", + minute: "2-digit", + }) + : "", + })); return ( - - Dashboard Screen + + Dashboard Screen - Under construction - + + Temp (°C) + + + + Hour + ); diff --git a/apps/mobile/src/screens/devices.tsx b/apps/mobile/src/screens/devices.tsx index f2d66a5..21dcab7 100644 --- a/apps/mobile/src/screens/devices.tsx +++ b/apps/mobile/src/screens/devices.tsx @@ -13,6 +13,7 @@ import { useState, useRef, useEffect } from "react"; import { Device } from "react-native-ble-plx"; import { Buffer } from "buffer"; import { postReading } from "../api/readingsService"; +import { deviceStore } from "../stores/deviceStore"; type senseorReading = { temperature_c: number | null; @@ -23,6 +24,26 @@ type senseorReading = { pm10_ugm3: number | null; }; +type DeviceProps = { name: string; onPress: () => void }; + +const DeviceItem = ({ name, onPress }: DeviceProps) => ( + [ + { + opacity: pressed ? 0.6 : 1, + padding: 15, + borderRadius: 8, + marginBottom: 10, + }, + ]} + > + + {name} + + +); + export function DevicesScreen() { const [devices, setDevices] = useState([]); const [sensorData, setSensorData] = useState({ @@ -62,26 +83,6 @@ export function DevicesScreen() { } }, [sensorData]); - type DeviceProps = { name: string; onPress: () => void }; - - const DeviceItem = ({ name, onPress }: DeviceProps) => ( - [ - { - opacity: pressed ? 0.6 : 1, - padding: 15, - borderRadius: 8, - marginBottom: 10, - }, - ]} - > - - {name} - - - ); - const onClick = async () => { const bluetoothPermission = await requestBluetoothPermission(); @@ -127,6 +128,8 @@ export function DevicesScreen() { const essService = services.find((s) => s.uuid.includes("181a")); if (!essService) return; + deviceStore.getState().setConnectedDevice(itemId); + const characteristics = await manager.characteristicsForDevice( itemId, essService.uuid, diff --git a/apps/mobile/src/stores/deviceStore.ts b/apps/mobile/src/stores/deviceStore.ts index f512eb7..2cf9132 100644 --- a/apps/mobile/src/stores/deviceStore.ts +++ b/apps/mobile/src/stores/deviceStore.ts @@ -14,13 +14,16 @@ type Device = { type deviceStoreTypes = { devices: Device[]; + deviceId: string; setDevices: (devices: Device[]) => void; addDevice: (device: Device) => void; removeDevice: (deviceId: string) => void; + setConnectedDevice: (deviceId: string) => void; }; export const deviceStore = create((set) => ({ devices: [], + deviceId: "", setDevices: (devices: Device[]) => set((state) => ({ @@ -36,4 +39,10 @@ export const deviceStore = create((set) => ({ set((state) => ({ devices: state.devices.filter((device) => device.deviceId !== deviceId), })), + + setConnectedDevice: (deviceId: string) => { + set((state) => ({ + deviceId: deviceId, + })); + }, }));