Skip to content

Commit

Permalink
Merge pull request #14 from GreenWizard2015/issues/11
Browse files Browse the repository at this point in the history
Issues/11
  • Loading branch information
GreenWizard2015 authored May 21, 2024
2 parents e9c38ef + 21540fe commit 263f88d
Show file tree
Hide file tree
Showing 10 changed files with 276 additions and 41 deletions.
1 change: 0 additions & 1 deletion ui/src/App.css
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,5 @@

.hold-to-pour-image {
object-fit: contain;
width: 25%;
height: auto;
}
12 changes: 10 additions & 2 deletions ui/src/App.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import React from 'react';
import './App.css';
import { Container, Form } from 'react-bootstrap';
import { Col, Container, Form, Row } from 'react-bootstrap';
import { connect } from 'react-redux';

import NotificationsArea from './components/NotificationsArea.js';
Expand All @@ -11,6 +11,7 @@ import SystemStatusArea from './components/SystemStatusArea.js';
import CurrentOperationInfoArea from './components/CurrentOperationInfoArea.js';
import HoldToPour from './components/HoldToPour.js';
import PowerLevel from './components/PowerLevel.js';
import TeaLevel from './components/TeaLevel.js';

function App({ isConnected }) {
return (
Expand All @@ -26,7 +27,14 @@ function App({ isConnected }) {
<PourTimeField />
<CurrentOperationInfoArea />
<SystemControls />
<HoldToPour />
<Row>
<Col>
<HoldToPour />
</Col>
<Col>
<TeaLevel />
</Col>
</Row>
</>
) : null}
</Form>
Expand Down
11 changes: 10 additions & 1 deletion ui/src/components/HoldToPour.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,20 @@ import { connect } from 'react-redux';
import { Container, Form } from 'react-bootstrap';
import { useWaterPumpAPI } from '../contexts/WaterPumpAPIContext';

export function HoldToPourComponent({ interval }) {
export function HoldToPourComponent({ interval, estimatedTeaLevel }) {
const { API }= useWaterPumpAPI();
const [isPouring, setIsPouring] = useState(false);
const [clickToPour, setClickToPour] = useState(false);
// continuously pour water while the button is pressed
const lastPouringTime = React.useRef(0);
// stop pouring if estimated level is greater than 100%
useEffect(() => {
if(!isPouring) return;
if(estimatedTeaLevel >= 100) {
setIsPouring(false);
}
}, [isPouring, estimatedTeaLevel]);

const onTick = React.useCallback(
async () => {
if(Date.now() < lastPouringTime.current) return;
Expand Down Expand Up @@ -77,6 +85,7 @@ function HoldToPourComponent_withExtras({ pouringTime, ...props }) {
export default connect(
state => ({
pouringTime: state.UI.pouringTime,
estimatedTeaLevel: state.Temp.estimatedTeaLevel,
}),
{ }
)(HoldToPourComponent_withExtras);
60 changes: 60 additions & 0 deletions ui/src/components/TeaLevel.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
.tea-glass {
height: 100%;
position: relative;
display: flex;
justify-content: center;
align-items: center;
user-select: none;
}

.tea-container {
position: relative;
height: 100%;
max-width: 100%;
width: fit-content;
display: flex;
padding: 1rem;
}

.cup-image {
max-height: 30vh;
max-width: 100%;
object-fit: contain;
position: relative;
z-index: 1;
cursor: pointer;
}

.tea-level {
position: absolute;
left: 0px;
right: 0px;
height: 3px;
background-color: #76b7b2;
z-index: 2;
color: #76b7b2;
}

.est-tea-level {
position: absolute;
left: 0px;
right: 0px;
height: 3px;
border-bottom: 3px dashed red;
z-index: 3;
text-align: right;
padding-bottom: 2rem;
color: red;
}

.prev-tea-level {
position: absolute;
left: 0px;
right: 0px;
height: 3px;
border-bottom: 3px dashed black;
z-index: 3;
text-align: center;
padding-bottom: 2rem;
color: black;
}
94 changes: 94 additions & 0 deletions ui/src/components/TeaLevel.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
import React, { useEffect, useState } from 'react';
import './TeaLevel.css';
import cup from './cup.jpg';
import { connect } from 'react-redux';
import { changeEstimatedTeaLevel, changeStartTeaLevel } from '../store/slices/Temp';
import { changeSpeed } from '../store/slices/UI';

const bottomLevel = 20; // where the tea starts (0%)
const maxLevel = 80; // where the tea ends (100%)

function toRealLevel(level) {
const H = maxLevel - bottomLevel;
return (level / 100 * H) + bottomLevel;
}

function toPercentage(realLevel) {
const H = maxLevel - bottomLevel;
return (realLevel - bottomLevel) / H * 100;
}

function TeaLevel({
lastOperationDuration, speed, startTeaLevel, estimatedTeaLevel,
changeStartTeaLevel, changeEstimatedTeaLevel, changeSpeed, lastTeaLevel,
}) {
const [calcSpeed, setCalcSpeed] = useState(speed);
// update the estimated level if speed or duration changes
useEffect(() => {
const estimatedLevel = startTeaLevel + speed * lastOperationDuration / 1000;
changeEstimatedTeaLevel(estimatedLevel);
}, [lastOperationDuration, speed, startTeaLevel, changeEstimatedTeaLevel]);

const handleCupClick = (e) => {
const { top, height } = e.target.getBoundingClientRect();
const clickY = e.clientY;
const clickedPosition = top - clickY + height;
const clickedPercentage = (clickedPosition / height) * 100;
const newLevel = toPercentage(clickedPercentage);
// limit the new level to the range [0, 100]
const level = Math.min(Math.max(newLevel, 0), 100);
changeStartTeaLevel( level );
// find speed
const newSpeed = (level - lastTeaLevel) / (lastOperationDuration / 1000);
setCalcSpeed(newSpeed);
};

function onSpeedSet(e) {
e.preventDefault();
changeSpeed(calcSpeed);
}

return (
<>
<div>
<div className="tea-glass">
<div className="tea-container">
<img src={cup} alt="Cup" className="cup-image" draggable="false"
onClick={handleCupClick}
/>
<div className="tea-level" style={{ bottom: `${toRealLevel(startTeaLevel)}%` }}>
{startTeaLevel.toFixed(0)}%
</div>

<div className="est-tea-level" style={{ bottom: `${toRealLevel(estimatedTeaLevel)}%` }}>
{estimatedTeaLevel.toFixed(0)}%
</div>

<div className="prev-tea-level" style={{ bottom: `${toRealLevel(lastTeaLevel)}%` }}>
{lastTeaLevel.toFixed(0)}%
</div>
</div>
</div>
</div>
<div>
<input
type="number" step="0.01"
value={calcSpeed.toFixed(2)}
onChange={(e) => setCalcSpeed(parseFloat(e.target.value))}
/>
<button onClick={onSpeedSet}>Set Speed</button>
</div>
</>
);
}

export default connect(
state => ({
lastOperationDuration: state.Temp.lastOperationDuration,
speed: state.UI.speed,
startTeaLevel: state.Temp.startTeaLevel,
estimatedTeaLevel: state.Temp.estimatedTeaLevel,
lastTeaLevel: state.Temp.prevTeaLevel,
}),
{ changeStartTeaLevel, changeEstimatedTeaLevel, changeSpeed }
)(TeaLevel);
Binary file added ui/src/components/cup.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
87 changes: 53 additions & 34 deletions ui/src/contexts/WaterPumpAPIContext.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import React from 'react';
import React, { useState, useEffect, useMemo, useRef, useCallback } from 'react';
import { connect } from 'react-redux';
import { CWaterPumpAPI } from '../api/CWaterPumpAPI.js';
import { updateSystemStatus } from '../store/slices/SystemStatus.js';
import { changeLastOperationDuration, pumpStartedEvent } from '../store/slices/Temp.js';

const WaterPumpAPIContext = React.createContext();

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

const FETCH_STATUS_INTERVAL = 5000;

function _publicWrapper({ apiObject, apiQueue, _pouringTime, _powerLevel }) {
if(null == apiObject) return { API: null };
function _publicWrapper({
apiObject, apiQueue, _pouringTime, _powerLevel, startTimeRef, onPumpStart
}) {
if (null == apiObject) return { API: null };
return {
API: {
stopPump: () => {
apiQueue.push({
action: async () => await apiObject.stop(),
action: async () => {
startTimeRef.current = null; // reset the start time
return await apiObject.stop();
},
failMessage: 'Failed to stop the pump'
});
},
startPump: () => {
apiQueue.push({
action: async () => await apiObject.start(
_pouringTime.current,
_powerLevel.current
),
action: async () => {
if (startTimeRef.current === null) {
startTimeRef.current = Date.now();
await onPumpStart();
}
return await apiObject.start(_pouringTime.current, _powerLevel.current);
},
failMessage: 'Failed to start the pump'
});
},
Expand All @@ -44,17 +53,17 @@ function _makeStatusAction(apiObject) {
async function _processQueue({ apiQueue, lastUpdateTime, statusAction, updateStatus }) {
const deltaTime = Date.now() - lastUpdateTime.current;
const hasTasks = (0 < apiQueue.length);
if((deltaTime < FETCH_STATUS_INTERVAL) && !hasTasks) return;
if ((deltaTime < FETCH_STATUS_INTERVAL) && !hasTasks) return;

const action = hasTasks ? apiQueue.shift() : statusAction;
const oldTime = lastUpdateTime.current;
lastUpdateTime.current = Number.MAX_SAFE_INTEGER; // prevent concurrent tasks, just in case
try {
await updateStatus(action);
lastUpdateTime.current = Date.now();
} catch(error) {
} catch (error) {
lastUpdateTime.current = oldTime;
if(hasTasks) { // re-queue the action if it failed
if (hasTasks) { // re-queue the action if it failed
apiQueue.unshift(action);
}
throw error;
Expand All @@ -65,40 +74,47 @@ function WaterPumpAPIProviderComponent({
children,
apiHost, pouringTime, powerLevel,
updateStatus,
changeLastOperationDuration,
onPumpStart,
}) {
// to prevent the callbacks from changing when the pouringTime or powerLevel changes
const _pouringTime = React.useRef(pouringTime);
React.useEffect(() => { _pouringTime.current = pouringTime; }, [pouringTime]);

const _powerLevel = React.useRef(powerLevel);
React.useEffect(() => { _powerLevel.current = powerLevel; }, [powerLevel]);

const { apiObject, apiQueue } = React.useMemo(
const _pouringTime = useRef(pouringTime);
useEffect(() => { _pouringTime.current = pouringTime; }, [pouringTime]);

const _powerLevel = useRef(powerLevel);
useEffect(() => { _powerLevel.current = powerLevel; }, [powerLevel]);

const startTimeRef = useRef(null);
const { apiObject, apiQueue } = useMemo(
() => ({
apiObject: new CWaterPumpAPI({ URL: apiHost }),
apiQueue: []
}),
[apiHost]
);
////////////////
const statusAction = React.useMemo(() => _makeStatusAction(apiObject), [apiObject]);
const lastUpdateTime = React.useRef(0);
const onTick = React.useCallback(
async () => _processQueue({ apiQueue, lastUpdateTime, statusAction, updateStatus }),
[apiQueue, lastUpdateTime, updateStatus, statusAction]

const statusAction = useMemo(() => _makeStatusAction(apiObject), [apiObject]);
const lastUpdateTime = useRef(0);
const onTick = useCallback(
async () => {
if(null != startTimeRef.current) { // update the total duration of the last operation
const T = Date.now() - startTimeRef.current;
changeLastOperationDuration(T);
}
_processQueue({ apiQueue, lastUpdateTime, statusAction, updateStatus });
},
[apiQueue, lastUpdateTime, statusAction, updateStatus, changeLastOperationDuration]
);

// Run the timer
React.useEffect(() => {
useEffect(() => {
const timer = setInterval(onTick, 100);
return () => { clearInterval(timer); };
}, [onTick]);

////////////////
const value = React.useMemo(
() => _publicWrapper({ apiObject, apiQueue, _pouringTime, _powerLevel }),
[apiObject, apiQueue, _pouringTime, _powerLevel]
const value = useMemo(
() => _publicWrapper({ apiObject, apiQueue, _pouringTime, _powerLevel, startTimeRef, onPumpStart }),
[apiObject, apiQueue, _pouringTime, _powerLevel, startTimeRef, onPumpStart]
);

return (
<WaterPumpAPIContext.Provider value={value}>
{children}
Expand All @@ -112,8 +128,11 @@ const WaterPumpAPIProvider = connect(
pouringTime: state.UI.pouringTime,
powerLevel: state.UI.powerLevelInPercents,
}),
{ updateStatus: updateSystemStatus }
{
updateStatus: updateSystemStatus, changeLastOperationDuration,
onPumpStart: pumpStartedEvent
}
)(WaterPumpAPIProviderComponent);

export default WaterPumpAPIProvider;
export { WaterPumpAPIProvider };
export { WaterPumpAPIProvider };
Loading

0 comments on commit 263f88d

Please sign in to comment.