Skip to content

Commit 263f88d

Browse files
Merge pull request psmgeelen#14 from GreenWizard2015/issues/11
Issues/11
2 parents e9c38ef + 21540fe commit 263f88d

File tree

10 files changed

+276
-41
lines changed

10 files changed

+276
-41
lines changed

ui/src/App.css

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,5 @@
1010

1111
.hold-to-pour-image {
1212
object-fit: contain;
13-
width: 25%;
1413
height: auto;
1514
}

ui/src/App.js

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import React from 'react';
22
import './App.css';
3-
import { Container, Form } from 'react-bootstrap';
3+
import { Col, Container, Form, Row } from 'react-bootstrap';
44
import { connect } from 'react-redux';
55

66
import NotificationsArea from './components/NotificationsArea.js';
@@ -11,6 +11,7 @@ import SystemStatusArea from './components/SystemStatusArea.js';
1111
import CurrentOperationInfoArea from './components/CurrentOperationInfoArea.js';
1212
import HoldToPour from './components/HoldToPour.js';
1313
import PowerLevel from './components/PowerLevel.js';
14+
import TeaLevel from './components/TeaLevel.js';
1415

1516
function App({ isConnected }) {
1617
return (
@@ -26,7 +27,14 @@ function App({ isConnected }) {
2627
<PourTimeField />
2728
<CurrentOperationInfoArea />
2829
<SystemControls />
29-
<HoldToPour />
30+
<Row>
31+
<Col>
32+
<HoldToPour />
33+
</Col>
34+
<Col>
35+
<TeaLevel />
36+
</Col>
37+
</Row>
3038
</>
3139
) : null}
3240
</Form>

ui/src/components/HoldToPour.js

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,20 @@ import { connect } from 'react-redux';
33
import { Container, Form } from 'react-bootstrap';
44
import { useWaterPumpAPI } from '../contexts/WaterPumpAPIContext';
55

6-
export function HoldToPourComponent({ interval }) {
6+
export function HoldToPourComponent({ interval, estimatedTeaLevel }) {
77
const { API }= useWaterPumpAPI();
88
const [isPouring, setIsPouring] = useState(false);
99
const [clickToPour, setClickToPour] = useState(false);
1010
// continuously pour water while the button is pressed
1111
const lastPouringTime = React.useRef(0);
12+
// stop pouring if estimated level is greater than 100%
13+
useEffect(() => {
14+
if(!isPouring) return;
15+
if(estimatedTeaLevel >= 100) {
16+
setIsPouring(false);
17+
}
18+
}, [isPouring, estimatedTeaLevel]);
19+
1220
const onTick = React.useCallback(
1321
async () => {
1422
if(Date.now() < lastPouringTime.current) return;
@@ -77,6 +85,7 @@ function HoldToPourComponent_withExtras({ pouringTime, ...props }) {
7785
export default connect(
7886
state => ({
7987
pouringTime: state.UI.pouringTime,
88+
estimatedTeaLevel: state.Temp.estimatedTeaLevel,
8089
}),
8190
{ }
8291
)(HoldToPourComponent_withExtras);

ui/src/components/TeaLevel.css

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
.tea-glass {
2+
height: 100%;
3+
position: relative;
4+
display: flex;
5+
justify-content: center;
6+
align-items: center;
7+
user-select: none;
8+
}
9+
10+
.tea-container {
11+
position: relative;
12+
height: 100%;
13+
max-width: 100%;
14+
width: fit-content;
15+
display: flex;
16+
padding: 1rem;
17+
}
18+
19+
.cup-image {
20+
max-height: 30vh;
21+
max-width: 100%;
22+
object-fit: contain;
23+
position: relative;
24+
z-index: 1;
25+
cursor: pointer;
26+
}
27+
28+
.tea-level {
29+
position: absolute;
30+
left: 0px;
31+
right: 0px;
32+
height: 3px;
33+
background-color: #76b7b2;
34+
z-index: 2;
35+
color: #76b7b2;
36+
}
37+
38+
.est-tea-level {
39+
position: absolute;
40+
left: 0px;
41+
right: 0px;
42+
height: 3px;
43+
border-bottom: 3px dashed red;
44+
z-index: 3;
45+
text-align: right;
46+
padding-bottom: 2rem;
47+
color: red;
48+
}
49+
50+
.prev-tea-level {
51+
position: absolute;
52+
left: 0px;
53+
right: 0px;
54+
height: 3px;
55+
border-bottom: 3px dashed black;
56+
z-index: 3;
57+
text-align: center;
58+
padding-bottom: 2rem;
59+
color: black;
60+
}

ui/src/components/TeaLevel.js

Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
import React, { useEffect, useState } from 'react';
2+
import './TeaLevel.css';
3+
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';
7+
8+
const bottomLevel = 20; // where the tea starts (0%)
9+
const maxLevel = 80; // where the tea ends (100%)
10+
11+
function toRealLevel(level) {
12+
const H = maxLevel - bottomLevel;
13+
return (level / 100 * H) + bottomLevel;
14+
}
15+
16+
function toPercentage(realLevel) {
17+
const H = maxLevel - bottomLevel;
18+
return (realLevel - bottomLevel) / H * 100;
19+
}
20+
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+
changeEstimatedTeaLevel(estimatedLevel);
30+
}, [lastOperationDuration, speed, startTeaLevel, changeEstimatedTeaLevel]);
31+
32+
const handleCupClick = (e) => {
33+
const { top, height } = e.target.getBoundingClientRect();
34+
const clickY = e.clientY;
35+
const clickedPosition = top - clickY + height;
36+
const clickedPercentage = (clickedPosition / height) * 100;
37+
const newLevel = toPercentage(clickedPercentage);
38+
// limit the new level to the range [0, 100]
39+
const level = Math.min(Math.max(newLevel, 0), 100);
40+
changeStartTeaLevel( level );
41+
// find speed
42+
const newSpeed = (level - lastTeaLevel) / (lastOperationDuration / 1000);
43+
setCalcSpeed(newSpeed);
44+
};
45+
46+
function onSpeedSet(e) {
47+
e.preventDefault();
48+
changeSpeed(calcSpeed);
49+
}
50+
51+
return (
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)}%` }}>
60+
{startTeaLevel.toFixed(0)}%
61+
</div>
62+
63+
<div className="est-tea-level" style={{ bottom: `${toRealLevel(estimatedTeaLevel)}%` }}>
64+
{estimatedTeaLevel.toFixed(0)}%
65+
</div>
66+
67+
<div className="prev-tea-level" style={{ bottom: `${toRealLevel(lastTeaLevel)}%` }}>
68+
{lastTeaLevel.toFixed(0)}%
69+
</div>
70+
</div>
71+
</div>
72+
</div>
73+
<div>
74+
<input
75+
type="number" step="0.01"
76+
value={calcSpeed.toFixed(2)}
77+
onChange={(e) => setCalcSpeed(parseFloat(e.target.value))}
78+
/>
79+
<button onClick={onSpeedSet}>Set Speed</button>
80+
</div>
81+
</>
82+
);
83+
}
84+
85+
export default connect(
86+
state => ({
87+
lastOperationDuration: state.Temp.lastOperationDuration,
88+
speed: state.UI.speed,
89+
startTeaLevel: state.Temp.startTeaLevel,
90+
estimatedTeaLevel: state.Temp.estimatedTeaLevel,
91+
lastTeaLevel: state.Temp.prevTeaLevel,
92+
}),
93+
{ changeStartTeaLevel, changeEstimatedTeaLevel, changeSpeed }
94+
)(TeaLevel);

ui/src/components/cup.jpg

191 KB
Loading
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 };

0 commit comments

Comments
 (0)