Compare commits

...

18 Commits

Author SHA1 Message Date
a1dc088a23 Fix repeating notifications in Accident.js 2021-01-06 13:55:56 +05:30
d4a19060e8 Add notifications to pet mode 2021-01-06 13:55:22 +05:30
fe9f7e81e9 Add Smart Lights to SmartHome 2021-01-05 22:28:57 +05:30
6e07c2be85 Add marketplace 2021-01-04 23:53:59 +05:30
03db7575fd Changes in theming 2021-01-04 23:53:20 +05:30
df7470f147 Add Maps plugin 2021-01-04 14:54:45 +05:30
17a3436017 Fix typo 2021-01-04 12:53:20 +05:30
4bdcd49e1f Add SmartHome and relevant config 2021-01-04 12:18:38 +05:30
031c1ede21 Add Voice Wave Animation for recording and show recorded text 2021-01-04 03:45:09 +05:30
08e5c2e3d2 Add socket.io event handlers for voice:* messages 2021-01-04 00:59:02 +05:30
2c80c4b279 Add voiceSlice redux slice for voice icon state 2021-01-04 00:58:07 +05:30
66d688291c Add locking mechanism around sendNotification
Workaround for multiple messages being sent
2021-01-03 12:51:50 +05:30
49430114fe Use standardized colors instead of color codes 2021-01-03 12:25:49 +05:30
cfd0056197 Add PetMode plugin and integrate it into home page 2021-01-02 04:05:03 +05:30
facd8d912f Integrate telegram with accident plugin 2021-01-01 15:58:29 +05:30
b743cbc9a5 Add telegram util to send telegram messages and location 2021-01-01 15:58:02 +05:30
ace77028c4 Rename plugins to fit conventions 2021-01-01 14:05:39 +05:30
dd215a8acc Create Accident plugin 2021-01-01 13:58:34 +05:30
20 changed files with 501 additions and 17 deletions

View File

@@ -7,6 +7,7 @@
"@testing-library/jest-dom": "^4.2.4",
"@testing-library/react": "^9.3.2",
"@testing-library/user-event": "^7.1.2",
"axios": "^0.21.1",
"grommet": "^2.16.2",
"grommet-icons": "^4.5.0",
"polished": "^4.0.5",

View File

@@ -1,20 +1,28 @@
import React from "react";
import { useSelector, useDispatch } from "react-redux";
import { Box, Button, Clock } from "grommet";
import { Box, Button, Clock, Text } from "grommet";
import { setPlugin, selectCore } from "./coreSlice";
import { selectVoice } from "./voiceSlice";
import VoiceBars from "./components/VoiceBars";
import plugins from "./plugins";
function Core() {
const coreState = useSelector(selectCore);
const voiceState = useSelector(selectVoice);
const dispatch = useDispatch();
const plugin = plugins[coreState.plugin];
const props = {
data: coreState.data,
close: () => dispatch(setPlugin(false))
};
const dummyButtons = ["Maps", "Home Devices", "Phone"];
return <>
<Box height="5vh" background="light-1" justify="center" direction="row">
{voiceState.recording ? <VoiceBars style={{height: "100%"}}/>:
voiceState.text === null ? null:
<Text style={{paddingTop: "1em"}}>{voiceState.text}</Text>
}
</Box>
{plugin ? React.createElement(plugin, props, null) : (
<Box
align="center"
@@ -23,16 +31,19 @@ function Core() {
justify="center"
direction="column"
alignContent="center"
height="100vh"
height="95vh"
>
<Clock size="xxlarge" />
<Clock size="xxlarge" type="digital" />
<Box direction="row" margin="1.5em" wrap={true}>
<Button onClick={() => dispatch(setPlugin("sideMirrors"))} primary
label="Side Mirrors" />
{dummyButtons.map(i =>
<Button primary label={i} key={i} margin="0 0 0 1em" />
)}
<Button onClick={() => dispatch(setPlugin("petMode"))} primary
label="Pet Mode" />
<Button onClick={() => dispatch(setPlugin("smartHome"))} primary
label="Smart Home" margin="0 0 0 1em" />
<Button onClick={() => dispatch(setPlugin("maps"))} primary
label="Maps" margin="0 0 0 1em" />
<Button onClick={() => dispatch(setPlugin("marketplace"))} primary
label="Marketplace" margin="0 0 0 1em" />
</Box>
</Box>)
}

View File

@@ -4,6 +4,7 @@ import io from "socket.io-client";
import { WS_BASE } from "./config";
import { useDispatch } from "react-redux";
import { setData, setPlugin } from "./coreSlice";
import { setRecording, setText } from "./voiceSlice";
const WebSocketContext = createContext(null);
@@ -26,6 +27,20 @@ function WebSocketProvider({ children }) {
}, payload.time);
}
});
socket.on("voice:wakeword", () => {
dispatch(setRecording(true));
dispatch(setText(null));
});
socket.on("voice:record_end", () => {
dispatch(setRecording(false));
dispatch(setText(null));
});
socket.on("voice:utterance", payload => {
dispatch(setRecording(false));
dispatch(setText(payload.text));
setTimeout(() => dispatch(setText(null)), 3000);
});
}
return (

View File

@@ -1,8 +1,10 @@
import { configureStore } from "@reduxjs/toolkit";
import coreReducer from "../coreSlice";
import voiceReducer from "../voiceSlice";
export default configureStore({
reducer: {
core: coreReducer,
voice: voiceReducer,
},
});

View File

@@ -0,0 +1,79 @@
import React from "react";
function VoiceBars() {
return (
<div className="waves" style={{transform: "rotate(180deg)", marginTop: "130px", transition: "1s"}}>
<svg width="100vw" fill="none" version="1.1"
xmlns="http://www.w3.org/2000/svg">
<linearGradient id="grad1" x1="0%" y1="0%" x2="100%" y2="0%">
<stop offset="0%" stopColor="#00B4DB" />
<stop offset="50%" stopColor="#224488" />
<stop offset="100%" stopColor="#0083B0" />
</linearGradient>
<path
fill="url(#grad1)"
d="
M0 67
C 273,183
822,-40
1920.00,106
V 359
H 0
V 67
Z">
<animate
repeatCount="indefinite"
fill="url(#grad1)"
attributeName="d"
dur="5s"
attributeType="XML"
values="
M0 77
C 473,283
822,-40
1920,116
V 359
H 0
V 67
Z;
M0 77
C 473,-40
1222,283
1920,136
V 359
H 0
V 67
Z;
M0 77
C 973,260
1722,-53
1920,120
V 359
H 0
V 67
Z;
M0 77
C 473,283
822,-40
1920,116
V 359
H 0
V 67
Z
">
</animate>
</path>
</svg>
</div>
);
}
export default VoiceBars;

85
src/plugins/Accident.js Normal file
View File

@@ -0,0 +1,85 @@
import React, { useState, useEffect, useRef } from "react";
import PropTypes from "prop-types";
import { Box, Button, Heading, Text } from "grommet";
import { Aid, Impact } from "grommet-icons";
import Telegram from "../utils/telegram";
import { TG_API, TG_USERID } from "../config";
function sendNotification(close) {
if (window.sendingAccidentAlert)
return;
window.sendingAccidentAlert = true;
const bot = new Telegram(TG_API);
bot.sendMessage(TG_USERID, "User detected in an accident. Location has been attached below.");
navigator.geolocation.getCurrentPosition(
position => {
window.sendingAccidentAlert = false;
bot.sendLocation( TG_USERID,
position.coords.latitude, position.coords.longitude);
},
() => {
window.sendingAccidentAlert = false;
bot.sendMessage(TG_USERID, "Error retrieving location");
}
);
setTimeout(close, 2000);
}
function Accident(props) {
const threshold = 20;
const [ time, setTime ] = useState(threshold);
const countRef = useRef(null);
function decrementTime(time) {
if (time === 0) {
clearInterval(countRef.current);
sendNotification(props.close);
return 0;
}
return time - 1;
}
useEffect(() => {
countRef.current = setInterval(() => setTime(decrementTime), 1000);
return () => clearInterval(countRef.current);
}, []);
function dismiss() {
clearTimeout(countRef.current);
props.close();
}
return (
<Box
align="center"
background="light-1"
width="100vw"
justify="center"
direction="column"
alignContent="center"
height="95vh"
>
{
time === 0 ?
<Aid color="plain" size="xlarge" />
:<Impact color="status-critical" size="xlarge" />
}
<Heading>{
time === 0 ?
"Calling Ambulance!"
:"Accident Detected!"
}</Heading>
<Text>{
time === 0 ? "Calling Ambulance":
`To cancel calling the ambulance, press the button below. (${time})`
}</Text>
<Button primary size="large" onClick={dismiss}
label="Dismiss" margin="1.5em" />
</Box>
);
}
Accident.propTypes = {
close: PropTypes.func
};
export default Accident;

View File

@@ -11,11 +11,14 @@ function GenericPageWithIcon(props) {
justify="center"
direction="column"
alignContent="center"
height="100vh"
height="95vh"
style={{
padding: "0 3em",
}}
>
{props.icon}
<Heading>{props.title}</Heading>
<Text>{props.description}</Text>
<Text style={{ textAlign: "center" }}>{props.description}</Text>
<Button primary size="large" onClick={props.close}
label="Dismiss" margin="1.5em" />
</Box>

53
src/plugins/Maps.js Normal file
View File

@@ -0,0 +1,53 @@
import React, { useState, useEffect } from "react";
import PropTypes from "prop-types";
import { Box, Button } from "grommet";
import { MAPS_API } from "../config";
function Maps(props) {
const [ location, setLocation ] = useState([null, null]);
useEffect(() => {
navigator.geolocation.getCurrentPosition(
position => setLocation([position.coords.latitude, position.coords.longitude]),
() => {}
);
});
const mapsEmbedUrl = new URL("https://www.google.com/maps/embed/v1/view");
mapsEmbedUrl.searchParams.append("key", MAPS_API);
mapsEmbedUrl.searchParams.append("center", `${location[0]},${location[1]}`);
mapsEmbedUrl.searchParams.append("zoom", 14);
return (
<Box
align="center"
background="light-1"
width="100vw"
justify="center"
direction="column"
alignContent="center"
height="95vh"
>
{location[0] === null ?
"Loading Location":
<iframe
width="650"
height="300"
frameBorder="0"
style={{ border: 0 }}
src={mapsEmbedUrl.href}>
</iframe>
}
<Button primary size="large" onClick={props.close}
label="Dismiss" margin="1.5em" />
</Box>
);
}
Maps.propTypes = {
data: PropTypes.object,
close: PropTypes.func
};
Maps.pluginName = "Maps";
export default Maps;

View File

@@ -0,0 +1,67 @@
import React from "react";
import PropTypes from "prop-types";
import { Box, Button, Card, CardBody, CardFooter, CardHeader, Heading } from "grommet";
import { InstallOption, Trash } from "grommet-icons";
function Marketplace(props) {
const plugins = [
{
name: "Maps",
description: "Google Maps is a mapping service. It offers satellite imagery, aerial photography, street maps, 360° interactive panoramic views of streets, real-time traffic conditions, and route planning for traveling.",
installed: true
},
{
name: "Smart Home",
description: "Smart Home Plugin lets you integrate with various well known smart devices allowing you to stream video from various devices.",
installed: true
},
{
name: "Spotify",
description: "With Spotify, you have access to a world of music. You can listen to artists and albums, or create your own playlist of your favourite songs.",
installed: false
},
];
return (
<Box
align="center"
background="light-1"
width="100vw"
justify="center"
direction="column"
alignContent="center"
height="95vh"
>
<Box wrap="wrap" direction="row">
{
plugins.map(i =>
<Card key={i.name} style={{margin: "1em", width: "30%"}}>
<CardHeader pad="medium"><Heading>{i.name}</Heading></CardHeader>
<CardBody pad="medium">{i.description}</CardBody>
<CardFooter pad="medium" justify="center">
<Button
label={i.installed ? "Uninstall": "Install"}
icon={i.installed ?
<Trash color="status-error" /> :
<InstallOption color="status-ok" />
}
/>
</CardFooter>
</Card>
)
}
</Box>
<Button primary size="large" onClick={props.close}
label="Dismiss" margin="1.5em" />
</Box>
);
}
Marketplace.propTypes = {
data: PropTypes.object,
close: PropTypes.func
};
Marketplace.pluginName = "Marketplace";
export default Marketplace;

59
src/plugins/PetMode.js Normal file
View File

@@ -0,0 +1,59 @@
import React, { useState, useEffect } from "react";
import PropTypes from "prop-types";
import GenericPageWithIcon from "./GenericPageWithIcon";
import { Sun } from "grommet-icons";
import axios from "axios";
import Telegram from "../utils/telegram";
import { CAR_API, TG_API, TG_USERID } from "../config";
function sendNotification(temp) {
if (window.sendingPetAlert)
return;
window.sendingPetAlert = true;
const bot = new Telegram(TG_API);
bot.sendMessage(TG_USERID, `The temperature in the car is ${temp}°C. Please check your child/pet.`);
window.sendingPetAlert = false;
}
function PetMode(props) {
const [ temp, setTemp ] = useState(null);
const [ , setNotified ] = useState(false);
useEffect(() => {
axios.get(`${CAR_API}data/InsideTemperature`)
.then(resp => setTemp(resp.data.value));
const id = setInterval(
() => axios.get(`${CAR_API}data/InsideTemperature`)
.then(resp => {
setTemp(resp.data.value);
if (resp.data.value > 35 || resp.data.value < 5) {
setNotified(notified => {
if (notified)
return true;
sendNotification(resp.data.value);
return true;
});
}
}),
1000
);
return () => clearInterval(id);
}, []);
if (temp === null)
return <></>;
return (
<GenericPageWithIcon
title={temp === null ? "Fetching temperature": `${temp}°C`}
description={`The temperature inside is ${temp}°C. The pet / child is ${temp > 35 ? "un": ""}safe.`}
icon={<Sun color={temp > 35 ? "status-critical": "plain"} size="xlarge" />}
close={props.close}
/>);
}
PetMode.propTypes = {
close: PropTypes.func
};
PetMode.pluginName = "Temperature";
export default PetMode;

50
src/plugins/SmartHome.js Normal file
View File

@@ -0,0 +1,50 @@
import React, {useState, useEffect} from "react";
import PropTypes from "prop-types";
import { Box, Button } from "grommet";
import axios from "axios";
import { CAMERA_URL, LIGHTS_URL } from "../config";
function SmartHome(props) {
const [ idx, setIdx ] = useState(0);
useEffect(() => {
const id = setInterval(() => setIdx(i => i + 1), 20);
return () => clearInterval(id);
}, []);
function turnOn() {
axios.get(`${LIGHTS_URL}/on`);
}
function turnOff() {
axios.get(`${LIGHTS_URL}/off`);
}
return (
<Box
align="center"
background="light-1"
width="100vw"
justify="center"
direction="column"
alignContent="center"
height="95vh"
>
<img src={`${CAMERA_URL}?id=${idx}`} />
<Box direction="row" margin="1em 0">
<Button primary size="large" onClick={turnOn}
label="Turn On Lights" margin="0 0 0 0" />
<Button primary size="large" onClick={turnOff}
label="Turn Off Lights" margin="0 0 0 1em" />
</Box>
<Button primary size="large" onClick={props.close}
label="Dismiss" margin="0" />
</Box>
);
}
SmartHome.propTypes = {
data: PropTypes.object,
close: PropTypes.func
};
SmartHome.pluginName = "Smart Home";
export default SmartHome;

View File

@@ -8,7 +8,7 @@ function Warning(props) {
<GenericPageWithIcon
title={props.data.title}
description={props.data.description}
icon={<Alert color="#D0C100" size="xlarge" />}
icon={<Alert color="status-critical" size="xlarge" />}
close={props.close}
/>);
}

View File

@@ -1,9 +1,19 @@
import Warning from "./warning";
import Manual from "./manual";
import Temperature from "./temperature";
import Warning from "./Warning";
import Manual from "./Manual";
import Temperature from "./Temperature";
import Accident from "./Accident";
import PetMode from "./PetMode";
import SmartHome from "./SmartHome";
import Maps from "./Maps";
import Marketplace from "./Marketplace";
export default {
warning: Warning,
manual: Manual,
temperature: Temperature
temperature: Temperature,
accident: Accident,
petMode: PetMode,
smartHome: SmartHome,
maps: Maps,
marketplace: Marketplace,
};

View File

@@ -1,3 +1,9 @@
const WS_BASE = "http://localhost:5050/";
const TG_API = "xxxxxxxxx:xxxxxxxxxxxxxxxxxxxxxx";
const TG_USERID = 123456789;
const CAR_API = "http://localhost:5000/";
const CAMERA_URL = "http://path.to/still/image.jpg";
const MAPS_API = "XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX";
const LIGHTS_URL = "http://192.168.1.50:5000/";
export { WS_BASE };
export { WS_BASE, TG_API, TG_USERID, CAR_API, CAMERA_URL, MAPS_API, LIGHTS_URL };

0
src/utils/index.js Normal file
View File

19
src/utils/telegram.js Normal file
View File

@@ -0,0 +1,19 @@
import axios from "axios";
class Telegram {
constructor(token) {
this.axios = axios.create({
baseURL: `https://api.telegram.org/bot${token}`
});
}
sendMessage(chat_id, text) {
this.axios.post("/sendMessage", {chat_id, text});
}
sendLocation(chat_id, latitude, longitude) {
this.axios.post("/sendLocation", {chat_id, latitude, longitude});
}
}
export default Telegram;

17
src/voiceSlice.js Normal file
View File

@@ -0,0 +1,17 @@
import { createSlice } from "@reduxjs/toolkit";
export const voiceSlice = createSlice({
name: "voice",
initialState: {
recording: false,
text: null
},
reducers: {
setRecording: (state, action) => ({...state, recording: action.payload}),
setText: (state, action) => ({...state, text: action.payload}),
}
});
export const { setRecording, setText } = voiceSlice.actions;
export const selectVoice = state => state.voice;
export default voiceSlice.reducer;

View File

@@ -2590,6 +2590,13 @@ axe-core@^4.0.2:
resolved "https://registry.npmjs.org/axe-core/-/axe-core-4.1.1.tgz#70a7855888e287f7add66002211a423937063eaf"
integrity sha512-5Kgy8Cz6LPC9DJcNb3yjAXTu3XihQgEdnIg50c//zOC/MyLP0Clg+Y8Sh9ZjjnvBrDZU4DgXS9C3T9r4/scGZQ==
axios@^0.21.1:
version "0.21.1"
resolved "https://registry.npmjs.org/axios/-/axios-0.21.1.tgz#22563481962f4d6bde9a76d516ef0e5d3c09b2b8"
integrity sha512-dKQiRHxGD9PPRIUNIWvZhPTPpl1rf/OxTYKsqKUDjBwYylTvV7SjSHJb9ratfyzM6wCdLCOYLzs73qpg5c4iGA==
dependencies:
follow-redirects "^1.10.0"
axobject-query@^2.2.0:
version "2.2.0"
resolved "https://registry.npmjs.org/axobject-query/-/axobject-query-2.2.0.tgz#943d47e10c0b704aa42275e20edf3722648989be"
@@ -5204,7 +5211,7 @@ flush-write-stream@^1.0.0:
inherits "^2.0.3"
readable-stream "^2.3.6"
follow-redirects@^1.0.0:
follow-redirects@^1.0.0, follow-redirects@^1.10.0:
version "1.13.1"
resolved "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.13.1.tgz#5f69b813376cee4fd0474a3aba835df04ab763b7"
integrity sha512-SSG5xmZh1mkPGyKzjZP8zLjltIfpW32Y5QpdNJyjcfGxK3qo3NDDkZOZSFiGn1A6SclQxY9GzEwAHQ3dmYRWpg==