Skip to content

Commit a99f851

Browse files
semi working version of the UI
1 parent c709d8c commit a99f851

File tree

5 files changed

+157
-51
lines changed

5 files changed

+157
-51
lines changed

ui/src/components/TeaLevel.js

Lines changed: 55 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
1-
import React, { useState } from 'react';
1+
import React, { useEffect, useState } from 'react';
22
import './TeaLevel.css';
33
import cup from './cup.jpg';
4+
import { connect } from 'react-redux';
5+
import { changeEstimatedTeaLevel, changeStartTeaLevel } from '../store/slices/Temp';
6+
import { changeSpeed } from '../store/slices/UI';
47

58
const bottomLevel = 20; // where the tea starts (0%)
69
const maxLevel = 80; // where the tea ends (100%)
@@ -15,9 +18,17 @@ function toPercentage(realLevel) {
1518
return (realLevel - bottomLevel) / H * 100;
1619
}
1720

18-
function TeaLevel({ initialLevel=null }) {
19-
const estimatedLevel = 50;
20-
const [currentLevel, setCurrentLevel] = useState(initialLevel || 70);
21+
function TeaLevel({
22+
lastOperationDuration, speed, startTeaLevel, estimatedTeaLevel,
23+
changeStartTeaLevel, changeEstimatedTeaLevel, changeSpeed, lastTeaLevel
24+
}) {
25+
const [calcSpeed, setCalcSpeed] = useState(speed);
26+
// update the estimated level if speed or duration changes
27+
useEffect(() => {
28+
const estimatedLevel = startTeaLevel + speed * lastOperationDuration / 1000;
29+
console.log(startTeaLevel, speed, lastOperationDuration, estimatedLevel);
30+
changeEstimatedTeaLevel(estimatedLevel);
31+
}, [lastOperationDuration, speed, startTeaLevel, changeEstimatedTeaLevel]);
2132

2233
const handleCupClick = (e) => {
2334
const { top, height } = e.target.getBoundingClientRect();
@@ -26,20 +37,50 @@ function TeaLevel({ initialLevel=null }) {
2637
const clickedPercentage = (clickedPosition / height) * 100;
2738
const newLevel = toPercentage(clickedPercentage);
2839
// limit the new level to the range [0, 100]
29-
setCurrentLevel( Math.min(Math.max(newLevel, 0), 100) );
40+
changeStartTeaLevel( Math.min(Math.max(newLevel, 0), 100) );
41+
// find speed
42+
const newSpeed = (newLevel - lastTeaLevel) / (lastOperationDuration / 1000);
43+
setCalcSpeed(newSpeed);
3044
};
3145

46+
function onSpeedSet(e) {
47+
e.preventDefault();
48+
changeSpeed(calcSpeed);
49+
}
50+
3251
return (
33-
<div className="tea-glass">
34-
<div className="tea-container">
35-
<img src={cup} alt="Cup" className="cup-image" draggable="false"
36-
onClick={handleCupClick}
37-
/>
38-
<div className="tea-level" style={{ bottom: `${toRealLevel(currentLevel)}%` }}></div>
39-
<div className="est-tea-level" style={{ bottom: `${toRealLevel(estimatedLevel)}%` }}></div>
52+
<>
53+
<div>
54+
<div className="tea-glass">
55+
<div className="tea-container">
56+
<img src={cup} alt="Cup" className="cup-image" draggable="false"
57+
onClick={handleCupClick}
58+
/>
59+
<div className="tea-level" style={{ bottom: `${toRealLevel(startTeaLevel)}%` }}></div>
60+
<div className="est-tea-level" style={{ bottom: `${toRealLevel(estimatedTeaLevel)}%` }}></div>
61+
</div>
62+
</div>
63+
</div>
64+
<div>
65+
<p>speed: {speed} %/s</p>
66+
<p>duration: {lastOperationDuration} ms</p>
67+
<p>last tea level: {lastTeaLevel.toFixed(2)}%</p>
68+
</div>
69+
<div>
70+
<input type="text" value={calcSpeed.toFixed(2)} readOnly />
71+
<button onClick={onSpeedSet}>Set Speed</button>
4072
</div>
41-
</div>
73+
</>
4274
);
4375
}
4476

45-
export default TeaLevel;
77+
export default connect(
78+
state => ({
79+
lastOperationDuration: state.Temp.lastOperationDuration,
80+
speed: state.UI.speed,
81+
startTeaLevel: state.Temp.startTeaLevel,
82+
estimatedTeaLevel: state.Temp.estimatedTeaLevel,
83+
lastTeaLevel: state.Temp.prevTeaLevel,
84+
}),
85+
{ changeStartTeaLevel, changeEstimatedTeaLevel, changeSpeed }
86+
)(TeaLevel);
Lines changed: 53 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
1-
import React from 'react';
1+
import React, { useState, useEffect, useMemo, useRef, useCallback } from 'react';
22
import { connect } from 'react-redux';
33
import { CWaterPumpAPI } from '../api/CWaterPumpAPI.js';
44
import { updateSystemStatus } from '../store/slices/SystemStatus.js';
5+
import { changeLastOperationDuration, pumpStartedEvent } from '../store/slices/Temp.js';
56

67
const WaterPumpAPIContext = React.createContext();
78

@@ -11,22 +12,30 @@ export function useWaterPumpAPI() {
1112

1213
const FETCH_STATUS_INTERVAL = 5000;
1314

14-
function _publicWrapper({ apiObject, apiQueue, _pouringTime, _powerLevel }) {
15-
if(null == apiObject) return { API: null };
15+
function _publicWrapper({
16+
apiObject, apiQueue, _pouringTime, _powerLevel, startTimeRef, onPumpStart
17+
}) {
18+
if (null == apiObject) return { API: null };
1619
return {
1720
API: {
1821
stopPump: () => {
1922
apiQueue.push({
20-
action: async () => await apiObject.stop(),
23+
action: async () => {
24+
startTimeRef.current = null; // reset the start time
25+
return await apiObject.stop();
26+
},
2127
failMessage: 'Failed to stop the pump'
2228
});
2329
},
2430
startPump: () => {
2531
apiQueue.push({
26-
action: async () => await apiObject.start(
27-
_pouringTime.current,
28-
_powerLevel.current
29-
),
32+
action: async () => {
33+
if (startTimeRef.current === null) {
34+
startTimeRef.current = Date.now();
35+
await onPumpStart();
36+
}
37+
return await apiObject.start(_pouringTime.current, _powerLevel.current);
38+
},
3039
failMessage: 'Failed to start the pump'
3140
});
3241
},
@@ -44,17 +53,17 @@ function _makeStatusAction(apiObject) {
4453
async function _processQueue({ apiQueue, lastUpdateTime, statusAction, updateStatus }) {
4554
const deltaTime = Date.now() - lastUpdateTime.current;
4655
const hasTasks = (0 < apiQueue.length);
47-
if((deltaTime < FETCH_STATUS_INTERVAL) && !hasTasks) return;
48-
56+
if ((deltaTime < FETCH_STATUS_INTERVAL) && !hasTasks) return;
57+
4958
const action = hasTasks ? apiQueue.shift() : statusAction;
5059
const oldTime = lastUpdateTime.current;
5160
lastUpdateTime.current = Number.MAX_SAFE_INTEGER; // prevent concurrent tasks, just in case
5261
try {
5362
await updateStatus(action);
5463
lastUpdateTime.current = Date.now();
55-
} catch(error) {
64+
} catch (error) {
5665
lastUpdateTime.current = oldTime;
57-
if(hasTasks) { // re-queue the action if it failed
66+
if (hasTasks) { // re-queue the action if it failed
5867
apiQueue.unshift(action);
5968
}
6069
throw error;
@@ -65,40 +74,47 @@ function WaterPumpAPIProviderComponent({
6574
children,
6675
apiHost, pouringTime, powerLevel,
6776
updateStatus,
77+
changeLastOperationDuration,
78+
onPumpStart,
6879
}) {
69-
// to prevent the callbacks from changing when the pouringTime or powerLevel changes
70-
const _pouringTime = React.useRef(pouringTime);
71-
React.useEffect(() => { _pouringTime.current = pouringTime; }, [pouringTime]);
72-
73-
const _powerLevel = React.useRef(powerLevel);
74-
React.useEffect(() => { _powerLevel.current = powerLevel; }, [powerLevel]);
75-
76-
const { apiObject, apiQueue } = React.useMemo(
80+
const _pouringTime = useRef(pouringTime);
81+
useEffect(() => { _pouringTime.current = pouringTime; }, [pouringTime]);
82+
83+
const _powerLevel = useRef(powerLevel);
84+
useEffect(() => { _powerLevel.current = powerLevel; }, [powerLevel]);
85+
86+
const startTimeRef = useRef(null);
87+
const { apiObject, apiQueue } = useMemo(
7788
() => ({
7889
apiObject: new CWaterPumpAPI({ URL: apiHost }),
7990
apiQueue: []
8091
}),
8192
[apiHost]
8293
);
83-
////////////////
84-
const statusAction = React.useMemo(() => _makeStatusAction(apiObject), [apiObject]);
85-
const lastUpdateTime = React.useRef(0);
86-
const onTick = React.useCallback(
87-
async () => _processQueue({ apiQueue, lastUpdateTime, statusAction, updateStatus }),
88-
[apiQueue, lastUpdateTime, updateStatus, statusAction]
94+
95+
const statusAction = useMemo(() => _makeStatusAction(apiObject), [apiObject]);
96+
const lastUpdateTime = useRef(0);
97+
const onTick = useCallback(
98+
async () => {
99+
if(null != startTimeRef.current) { // update the total duration of the last operation
100+
const T = Date.now() - startTimeRef.current;
101+
changeLastOperationDuration(T);
102+
}
103+
_processQueue({ apiQueue, lastUpdateTime, statusAction, updateStatus });
104+
},
105+
[apiQueue, lastUpdateTime, statusAction, updateStatus, changeLastOperationDuration]
89106
);
90107

91-
// Run the timer
92-
React.useEffect(() => {
108+
useEffect(() => {
93109
const timer = setInterval(onTick, 100);
94110
return () => { clearInterval(timer); };
95111
}, [onTick]);
96112

97-
////////////////
98-
const value = React.useMemo(
99-
() => _publicWrapper({ apiObject, apiQueue, _pouringTime, _powerLevel }),
100-
[apiObject, apiQueue, _pouringTime, _powerLevel]
113+
const value = useMemo(
114+
() => _publicWrapper({ apiObject, apiQueue, _pouringTime, _powerLevel, startTimeRef, onPumpStart }),
115+
[apiObject, apiQueue, _pouringTime, _powerLevel, startTimeRef, onPumpStart]
101116
);
117+
102118
return (
103119
<WaterPumpAPIContext.Provider value={value}>
104120
{children}
@@ -112,8 +128,11 @@ const WaterPumpAPIProvider = connect(
112128
pouringTime: state.UI.pouringTime,
113129
powerLevel: state.UI.powerLevelInPercents,
114130
}),
115-
{ updateStatus: updateSystemStatus }
131+
{
132+
updateStatus: updateSystemStatus, changeLastOperationDuration,
133+
onPumpStart: pumpStartedEvent
134+
}
116135
)(WaterPumpAPIProviderComponent);
117136

118137
export default WaterPumpAPIProvider;
119-
export { WaterPumpAPIProvider };
138+
export { WaterPumpAPIProvider };

ui/src/store/slices/Temp.js

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
import { createAsyncThunk, createSlice } from '@reduxjs/toolkit';
2+
3+
const initialState = {
4+
lastOperationDuration: 0,
5+
prevTeaLevel: 0,
6+
startTeaLevel: 0,
7+
estimatedTeaLevel: 50,
8+
}
9+
10+
export const TempSlice = createSlice({
11+
name: 'Temp',
12+
initialState: initialState,
13+
reducers: {
14+
changeLastOperationDuration: (state, action) => {
15+
state.lastOperationDuration = action.payload;
16+
},
17+
changeEstimatedTeaLevel: (state, action) => {
18+
state.estimatedTeaLevel = action.payload;
19+
},
20+
changePrevTeaLevel: (state, action) => {
21+
state.prevTeaLevel = action.payload;
22+
},
23+
pumpStartedEvent: (state, action) => {
24+
state.prevTeaLevel = state.startTeaLevel;
25+
},
26+
changeStartTeaLevel: (state, action) => {
27+
state.startTeaLevel = action.payload;
28+
state.estimatedTeaLevel = state.startTeaLevel;
29+
state.lastOperationDuration = 0;
30+
},
31+
}
32+
});
33+
34+
export const actions = TempSlice.actions;
35+
export const {
36+
changeLastOperationDuration,
37+
changeEstimatedTeaLevel,
38+
changePrevTeaLevel,
39+
pumpStartedEvent,
40+
changeStartTeaLevel
41+
} = actions;

ui/src/store/slices/UI.js

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ const INITIAL_STATE = {
1313
pouringTime: 1000,
1414
powerLevelInPercents: 100,
1515
apiHost: '',
16+
speed: 10,
1617
};
1718
// slice for system status
1819
export const UISlice = createSlice({
@@ -27,9 +28,12 @@ export const UISlice = createSlice({
2728
},
2829
updatePowerLevel(state, action) {
2930
state.powerLevelInPercents = validatePowerLevel(action.payload);
30-
}
31+
},
32+
changeSpeed(state, action) {
33+
state.speed = action.payload;
34+
},
3135
},
3236
});
3337

3438
export const actions = UISlice.actions;
35-
export const { updatePouringTime, updateAPIHost, updatePowerLevel } = actions;
39+
export const { updatePouringTime, updateAPIHost, updatePowerLevel, changeSpeed } = actions;

ui/src/store/slices/index.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
11
import { SystemStatusSlice } from "./SystemStatus";
22
import { UISlice } from "./UI";
33
import { NotificationsSlice } from "./Notifications";
4+
import { TempSlice } from "./Temp";
45

5-
const slices = [ SystemStatusSlice, UISlice, NotificationsSlice ];
6+
const slices = [ SystemStatusSlice, UISlice, NotificationsSlice, TempSlice ];
67
// export all slices as an object { [sliceName]: slice }
78
export const ALL_APP_SLICES = slices.reduce((acc, slice) => {
89
acc[slice.name] = slice;

0 commit comments

Comments
 (0)