feat: Add PD data visualization and Instant utility class

This commit is contained in:
2025-02-11 15:56:04 +08:00
parent e5bb316873
commit 6407307976
2 changed files with 105 additions and 13 deletions

70
app/utils/__init__.py Normal file
View File

@ -0,0 +1,70 @@
import time
from datetime import timedelta
class Instant:
"""A measurement of a monotonically nondecreasing clock."""
_time: float
@staticmethod
def clock() -> float:
"""Get current clock time in microseconds."""
return time.monotonic()
def __init__(self):
"""Initialize with current clock time."""
self._time = self.clock()
@classmethod
def now(cls) -> "Instant":
"""Create new Instant with current time."""
return cls()
def elapsed(self) -> timedelta:
"""Get elapsed time as timedelta."""
now = self.clock()
diff = now - self._time
return timedelta(seconds=diff)
def elapsed_ms(self) -> int:
"""Get elapsed time in milliseconds."""
return int(self.elapsed().total_seconds() * 1000)
def has_elapsed_ms(self, ms: int) -> bool:
"""Check if specified milliseconds have elapsed."""
return self.elapsed_ms() >= ms
def mut_every_ms(self, ms: int) -> bool:
"""Check if time has elapsed and reset if true."""
if self.elapsed_ms() >= ms:
self.mut_reset()
return True
return False
def has_elapsed(self, duration: timedelta) -> bool:
"""Check if specified duration has elapsed."""
return self.elapsed() >= duration
def mut_every(self, duration: timedelta) -> bool:
"""Check if duration has elapsed and reset if true."""
if self.has_elapsed(duration):
self.mut_reset()
return True
return False
def mut_reset(self) -> None:
"""Reset the timer to current time."""
self._time = self.clock()
def mut_elapsed_and_reset(self) -> timedelta:
"""Get elapsed time and reset timer."""
now = self.clock()
diff = now - self._time
duration = timedelta(microseconds=diff)
self._time = now
return duration
def count(self) -> float:
"""Get the internal time counter value."""
return self._time

48
main.py
View File

@ -24,11 +24,13 @@ from anyio.streams.memory import MemoryObjectSendStream, MemoryObjectReceiveStre
from threading import Thread from threading import Thread
from time import sleep from time import sleep
from pydantic import BaseModel, computed_field from pydantic import BaseModel, computed_field
from datetime import datetime from datetime import datetime, timedelta
import awkward as ak import awkward as ak
from awkward import Array as AwkwardArray, Record as AwkwardRecord from awkward import Array as AwkwardArray, Record as AwkwardRecord
from app.model import AlgoReport from app.model import AlgoReport
from app.utils import Instant
from collections import deque from collections import deque
from dataclasses import dataclass
class AppHistory(TypedDict): class AppHistory(TypedDict):
@ -38,6 +40,7 @@ class AppHistory(TypedDict):
accel_x_data: deque[int] accel_x_data: deque[int]
accel_y_data: deque[int] accel_y_data: deque[int]
accel_z_data: deque[int] accel_z_data: deque[int]
pd_data: deque[int]
# https://handmadesoftware.medium.com/streamlit-asyncio-and-mongodb-f85f77aea825 # https://handmadesoftware.medium.com/streamlit-asyncio-and-mongodb-f85f77aea825
@ -46,6 +49,7 @@ class AppState(TypedDict):
message_queue: MemoryObjectReceiveStream[bytes] message_queue: MemoryObjectReceiveStream[bytes]
task_group: TaskGroup task_group: TaskGroup
history: AppHistory history: AppHistory
refresh_inst: Instant
UDP_SERVER_HOST: Final[str] = "localhost" UDP_SERVER_HOST: Final[str] = "localhost"
@ -96,7 +100,9 @@ def resource(params: Any = None):
"accel_x_data": deque(maxlen=MAX_LENGTH), "accel_x_data": deque(maxlen=MAX_LENGTH),
"accel_y_data": deque(maxlen=MAX_LENGTH), "accel_y_data": deque(maxlen=MAX_LENGTH),
"accel_z_data": deque(maxlen=MAX_LENGTH), "accel_z_data": deque(maxlen=MAX_LENGTH),
"pd_data": deque(maxlen=MAX_LENGTH),
}, },
"refresh_inst": Instant(),
} }
logger.info("Resource created") logger.info("Resource created")
return state return state
@ -140,32 +146,35 @@ def main():
) )
placeholder = st.empty() placeholder = st.empty()
md_placeholder = st.empty()
while True: while True:
try: try:
message = state["message_queue"].receive_nowait() message = state["message_queue"].receive_nowait()
except anyio.WouldBlock: except anyio.WouldBlock:
continue continue
report = AlgoReport.unmarshal(message)
if state["refresh_inst"].mut_every_ms(500):
md_placeholder.markdown(
f"""
- HR: {report.data.hr_f}bpm
- HR CONF: {report.data.hr_conf}%
- ACTIVITY: {report.data.activity_class.name}
- SCD: {report.data.scd_contact_state.name}
"""
)
with placeholder.container(): with placeholder.container():
report = AlgoReport.unmarshal(message)
history["timescape"].append(datetime.now()) history["timescape"].append(datetime.now())
history["hr_data"].append(report.data.hr_f) history["hr_data"].append(report.data.hr_f)
history["hr_conf"].append(report.data.hr_conf) history["hr_conf"].append(report.data.hr_conf)
history["accel_x_data"].append(report.accel_x) history["accel_x_data"].append(report.accel_x)
history["accel_y_data"].append(report.accel_y) history["accel_y_data"].append(report.accel_y)
history["accel_z_data"].append(report.accel_z) history["accel_z_data"].append(report.accel_z)
history["pd_data"].append(report.led_2)
# with st.container(): fig_hr, fig_accel, fig_pd = st.tabs(["Heart Rate", "Accelerometer", "PD"])
# c1, c2 = st.columns(2)
# with c1:
# c1.write(f"HR: {report.data.hr_f}")
# with c2:
# c2.write(f"HR Confidence: {report.data.hr_conf}")
fig_hr, fig_accel = st.tabs(["Heart Rate", "Accelerometer"])
with fig_hr: with fig_hr:
fig_hr.plotly_chart( st.plotly_chart(
go.Figure( go.Figure(
data=[ data=[
go.Scatter( go.Scatter(
@ -184,7 +193,7 @@ def main():
) )
) )
with fig_accel: with fig_accel:
fig_accel.plotly_chart( st.plotly_chart(
go.Figure( go.Figure(
data=[ data=[
go.Scatter( go.Scatter(
@ -208,6 +217,19 @@ def main():
] ]
) )
) )
with fig_pd:
st.plotly_chart(
go.Figure(
data=[
go.Scatter(
x=list(history["timescape"]),
y=list(history["pd_data"]),
mode="lines",
name="PD",
)
]
)
)
if __name__ == "__main__": if __name__ == "__main__":