Skip to content
14 changes: 14 additions & 0 deletions apps/backend/src/devices/devices.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import {
} from './dto/update-device.dto';
import { User } from 'src/decorators/user.decorator';
import { Public } from 'src/decorators/public';
import { UpdateDeviceSettingsDto } from './dto/update-device-settings.dto';
@Controller('devices')
export class DevicesController {
constructor(private readonly devicesService: DevicesService) {}
Expand Down Expand Up @@ -44,6 +45,19 @@ export class DevicesController {
return this.devicesService.updateStatus(deviceToken, updateDeviceStatusDto);
}

@Patch(':id/settings')
updateSettings(
@Param('id') id: string,
@User() user: any,
@Body() updateDeviceSettingsDto: UpdateDeviceSettingsDto,
) {
return this.devicesService.updateSettings(
id,
user,
updateDeviceSettingsDto,
);
}

@Patch(':id')
update(
@Param('id') id: string,
Expand Down
25 changes: 25 additions & 0 deletions apps/backend/src/devices/devices.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import { Device } from './entities/device.entity';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import * as crypto from 'crypto';
import { UpdateDeviceSettingsDto } from './dto/update-device-settings.dto';

@Injectable()
export class DevicesService {
Expand Down Expand Up @@ -74,6 +75,30 @@ export class DevicesService {
return updatedDevice;
}

async updateSettings(
id: string,
user: any,
updateDeviceSettingsDto: UpdateDeviceSettingsDto,
): Promise<Device> {
const device = await this.devicesRepository.findOneBy({
user: { id: user.id },
deviceId: id,
});

if (!device) {
throw new HttpException('Device does not exist', HttpStatus.NOT_FOUND);
}

const updatedDeviceSettings = {
...device,
...updateDeviceSettingsDto,
};

await this.devicesRepository.save(updatedDeviceSettings);

return updatedDeviceSettings;
}

async updateStatus(
deviceToken: string,
updateDeviceStatusDto: UpdateDeviceStatusDto,
Expand Down
5 changes: 5 additions & 0 deletions apps/backend/src/devices/dto/create-device-settings.dto.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
export class CreateDeviceSettingsDto {
samplingIntervalSec: number;
temperatureThreshold: number;
pm25Threshold: number;
}
6 changes: 6 additions & 0 deletions apps/backend/src/devices/dto/update-device-settings.dto.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import { CreateDeviceSettingsDto } from './create-device-settings.dto';
import { PartialType } from '@nestjs/mapped-types';

export class UpdateDeviceSettingsDto extends PartialType(
CreateDeviceSettingsDto,
) {}
6 changes: 6 additions & 0 deletions apps/backend/src/devices/entities/device.entity.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,4 +27,10 @@ export class Device {
user: User;
@Column({ unique: true })
deviceToken: string;
@Column({ type: 'int', default: 300 })
samplingIntervalSec: number;
@Column({ nullable: true, type: 'decimal' })
temperatureThreshold: number;
@Column({ nullable: true, type: 'decimal' })
pm25Threshold: number;
}
2 changes: 1 addition & 1 deletion apps/mobile/src/api/authService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ export const login = async (email: string, password: string) => {
});
return response.data;
} catch (error) {
console.log(error);
throw error;
}
};

Expand Down
2 changes: 1 addition & 1 deletion apps/mobile/src/api/axios.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import axios from "axios";
import { authStore } from "../stores/authStore";

export const instance = axios.create({
baseURL: "http://192.168.1.74:3000/",
baseURL: "http://192.168.5.105:3000/",
timeout: 10000,
});

Expand Down
17 changes: 17 additions & 0 deletions apps/mobile/src/api/deviceService.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import { instance } from "./axios";

export const patchDeviceSettings = async (
deviceId: string,
samplingInterval: number,
) => {
const samplingIntervalSec = samplingInterval * 60;

try {
const response = await instance.patch(`devices/${deviceId}/settings`, {
samplingIntervalSec: samplingIntervalSec,
});
return response.data;
} catch (error) {
console.log(error);
}
};
47 changes: 38 additions & 9 deletions apps/mobile/src/screens/dashboard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,9 @@ const RangeItem = ({ name, onPress, isSelected }: RangeProps) => (
]}
>
<View style={[styles.item, isSelected && styles.activeItem]}>
<Text>{name}</Text>
<Text style={isSelected ? styles.activeItemText : styles.itemText}>
{name}
</Text>
</View>
</Pressable>
);
Expand Down Expand Up @@ -209,7 +211,7 @@ export function DashboardScreen() {
<View
style={{ flex: 1, alignItems: "center", justifyContent: "flex-start" }}
>
<Text style={{ fontSize: 32, marginTop: 100 }}>Dashboard Screen</Text>
<Text style={styles.title}>Air Quality Dashboard</Text>
<View style={{ flexDirection: "row" }}>
{rangeList.map((r) => (
<RangeItem
Expand All @@ -220,12 +222,10 @@ export function DashboardScreen() {
></RangeItem>
))}
</View>
<Pressable
onPress={handleCSVExport}
style={{ backgroundColor: "#80aee1", padding: 10, borderRadius: 8 }}
>
<Text style={{ color: "white", fontWeight: "bold" }}>Download CSV</Text>
<Pressable onPress={handleCSVExport} style={styles.csvButton}>
<Text style={styles.csvButtonText}>Download CSV</Text>
</Pressable>
{!readings && <Text>Data could not be loaded</Text>}
<ScrollView>
{sensorList.map((s) => (
<SensorChart
Expand All @@ -241,20 +241,49 @@ export function DashboardScreen() {
);
}

const ACCENT_COLOR = "#0f766e";

const styles = StyleSheet.create({
title: {
fontSize: 32,
fontWeight: "600",
marginTop: 140,
color: "#1f2937",
},

item: {
backgroundColor: "#00ffc8fd",
backgroundColor: "#e5e7eb",
padding: 12,
borderRadius: 20,
alignSelf: "center",
paddingHorizontal: 20,
},

activeItem: {
backgroundColor: "red",
backgroundColor: ACCENT_COLOR,
padding: 12,
borderRadius: 20,
alignSelf: "center",
paddingHorizontal: 20,
},

itemText: {
color: "#374151",
},

activeItemText: {
color: "white",
fontWeight: "600",
},

csvButton: {
backgroundColor: ACCENT_COLOR,
padding: 10,
borderRadius: 20,
},

csvButtonText: {
color: "white",
fontWeight: "bold",
},
});
2 changes: 1 addition & 1 deletion apps/mobile/src/screens/devices.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -178,7 +178,7 @@ export function DevicesScreen() {

setSensorData((prev) => ({
...prev,
pressure_hpa: pressValue / 100,
pressure_hpa: pressValue,
}));
},
);
Expand Down
26 changes: 19 additions & 7 deletions apps/mobile/src/screens/login.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { View, Text, TextInput, StyleSheet, Button } from "react-native";
import { View, Text, TextInput, StyleSheet, Button, Alert } from "react-native";
import React from "react";
import { login } from "../api/authService";
import { authStore } from "../stores/authStore";
Expand All @@ -16,14 +16,26 @@ export function LoginScreen() {
const navigation = useNavigation();

const onClick = async () => {
const response = await login(email, password);
try {
const response = await login(email, password);

if (response) {
const { accessToken, refreshToken } = response;
if (response) {
const { accessToken, refreshToken } = response;

authStore.getState().login(accessToken, refreshToken);
save("accessToken", accessToken);
save("refreshToken", refreshToken);
authStore.getState().login(accessToken, refreshToken);
save("accessToken", accessToken);
save("refreshToken", refreshToken);
}
} catch (error) {
if (error.response) {
if (error.response.status == 401) {
Alert.alert("Error", "Authentication error");
} else {
Alert.alert("Error", "Something went wrong");
}
} else {
Alert.alert("Error", "Network error");
}
}
};

Expand Down
98 changes: 97 additions & 1 deletion apps/mobile/src/screens/settings.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
import { View, Text, Button } from "react-native";
import { View, Text, Button, TextInput, Alert, StyleSheet } from "react-native";
import { authStore } from "../stores/authStore";
import { useState } from "react";
import { patchDeviceSettings } from "../api/deviceService";
import { manager } from "../ble/bleManager";
import { deviceStore } from "../stores/deviceStore";
import { Buffer } from "buffer";

import * as SecureStore from "expo-secure-store";

Expand All @@ -10,6 +15,68 @@ export function SettingsScreen() {
SecureStore.deleteItemAsync("refreshToken");
};

const onUpdateSamplingInterval = async () => {
const intervalInMin = parseInt(samplingInterval, 10);

const intervalInSec = intervalInMin * 60;

const intervalInMs = intervalInSec * 1000;

const deviceId = deviceStore.getState().deviceId;

if (
intervalInMin < 1 ||
intervalInMin > 30 ||
Number.isNaN(intervalInMin)
) {
Alert.alert(
"Out of range",
"The number must be between 1 and 30 minutes",
);
return;
}

const buf = Buffer.alloc(4);

buf.writeUint32LE(intervalInMs, 0);

const base64Data = buf.toString("base64");

patchDeviceSettings(deviceId, intervalInSec);

try {
const services = await manager.servicesForDevice(deviceId);
const essService = services.find((s) => s.uuid.includes("181a"));
if (!essService) return;

const characteristics = await manager.characteristicsForDevice(
deviceId,
essService.uuid,
);

manager.writeCharacteristicWithResponseForDevice(
deviceId,
essService.uuid,
characteristics[6].uuid,
base64Data,
);
} catch (error) {
Alert.alert(
"Problem saving settings",
"Retry connecting to your device and try again",
);
}
};

const [samplingInterval, setSamplingInterval] = useState<string>("");

const handleChangeText = (inputText: string) => {
const cleanNumber = inputText.replace(/[^0-9]/g, "");

setSamplingInterval(cleanNumber);
};
console.log(samplingInterval);

return (
<View
style={{
Expand All @@ -20,7 +87,36 @@ export function SettingsScreen() {
}}
>
<Text style={{ fontSize: 30, fontWeight: "bold" }}>Settings Screen</Text>

<View>
<Text style={{ fontSize: 20 }}>Set sampling interval</Text>

<TextInput
style={styles.input}
onChangeText={handleChangeText}
value={samplingInterval}
placeholder="From 1 to 30 min"
placeholderTextColor="#100202"
keyboardType="numeric"
></TextInput>

<Button
onPress={onUpdateSamplingInterval}
title="Submit"
color="#841584"
></Button>
</View>

<Button onPress={onClick} title="Logout" color="#841584"></Button>
</View>
);
}

const styles = StyleSheet.create({
input: {
height: 40,
margin: 12,
borderWidth: 1,
padding: 10,
},
});
9 changes: 6 additions & 3 deletions apps/mobile/src/stores/authStore.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,9 +25,12 @@ export const authStore = create<authStoreTypes>((set) => ({
const deviceId = deviceStore.getState().deviceId;

if (deviceId) {
await manager.cancelDeviceConnection(deviceId);

console.log("Device disconnected");
try {
await manager.cancelDeviceConnection(deviceId);
console.log("Device disconnected");
} catch (error) {
console.error("Problem disconnecting device ", error);
}
}

set({
Expand Down
Loading