-
Notifications
You must be signed in to change notification settings - Fork 0
API: Server-Side Event Stream #24
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
05c954a
9561e4d
7b39ed3
a3de9be
bec76e7
97c5eb3
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,85 @@ | ||
| # SPDX-License-Identifier: BSD-3-Clause | ||
| # Copyright (c) 2025 Sun Devil Rocketry | ||
|
|
||
| import threading | ||
| import time | ||
| import json | ||
|
|
||
| from typing import Dict, Any | ||
| from util import make_safe_number | ||
|
|
||
| # CONSTANTS | ||
| ORIGIN_STRING = "SDEC-API" | ||
| STREAM_VERSION_STRING = "1.0" | ||
| RATE_LIMITER_HZ = 30 | ||
| RATE_LIMITER_PERIOD = 1.0 / RATE_LIMITER_HZ | ||
|
|
||
|
|
||
| class Stream: | ||
| def __init__(self) -> None: | ||
| self.stream_cond = threading.Condition() | ||
| self.sequence_number = 0 | ||
| self.terminate = False | ||
| self.data = None | ||
|
coderabbitai[bot] marked this conversation as resolved.
|
||
|
|
||
| def sse_put_callback(self, messageType: str, timestamp: float, msg: dict[str, Any]): | ||
| """ | ||
| Set up a message for all clients to broadcast. | ||
| """ | ||
|
|
||
| with self.stream_cond: | ||
| response = { | ||
| "origin": ORIGIN_STRING, | ||
| "version": STREAM_VERSION_STRING, | ||
| "timestamp": timestamp, | ||
| "messageType": messageType, | ||
| "payload": msg | ||
| } | ||
|
Comment on lines
+31
to
+37
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. JSON structure |
||
| self.data = f"data: {json.dumps(response)}\n\n".encode("utf-8") | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Probably move it from callback into
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Also, Optional, just a note.
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. it's not 500 hz, it's like 60-ish for the serial path and 25-ish for LoRa. Given that your rate is upwards of 500 hz though this should def be considered. I'm definitely willing to drop packets since my priority is just making sure the data I'm feeding is the most up to date and I have very little latency. The deque sounds great for you if you're trying to preserve every packet, just make sure you're not flooding the client at more than 60 hz. I personally recommend 30 for the clients' sake, since their refresh rate won't go very high. The one concern I have about this rate limiting is on the rolling pressure chart. |
||
| self.sequence_number += 1 | ||
| self.stream_cond.notify_all() | ||
|
|
||
|
|
||
| def sse_broadcast_callback(self): | ||
| """ | ||
| Yields messages for the client with this request open. | ||
| """ | ||
| last_seq_num = 0 # scoped to this instance | ||
| last_time = time.monotonic() | ||
|
|
||
| # Send a message immediately on connect if one exists | ||
| with self.stream_cond: | ||
| if self.data is not None: | ||
| last_seq_num = self.sequence_number | ||
| data_to_yield = self.data | ||
| else: | ||
| data_to_yield = None | ||
|
|
||
| if data_to_yield is not None: | ||
| yield data_to_yield | ||
|
|
||
| # Wait for new messages to be put | ||
| while not self.terminate: | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Just random thing, but may a Inno, just a neat thing instead of waiting time outs for disconnection.
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The API itself doesn't track client state atm. This is handled behind some layers of abstraction by Flask & Werkzeug. The background processing layer has no idea how many clients are in the foreground. We may change this eventually if we need to throttle too many client connections, but I'm planning for ~4 simultaneous clients right now. |
||
| with self.stream_cond: | ||
| # Wait for sequence_number to change to avoid spurious wakeups | ||
| while self.sequence_number == last_seq_num and not self.terminate: | ||
| self.stream_cond.wait() | ||
| # delay the broadcast (yielding to other threads) if rate limiter | ||
| # indicates that messages are coming in too fast | ||
| curr_time = time.monotonic() | ||
| if curr_time < RATE_LIMITER_PERIOD + last_time: | ||
| # Release the lock if we have to wait for the timeout, | ||
| # then reacquire once we're back on the target rate. | ||
| self.stream_cond.release() | ||
| try: | ||
| time.sleep( RATE_LIMITER_PERIOD + last_time - curr_time ) | ||
| finally: | ||
| self.stream_cond.acquire() | ||
| # end the broadcast entirely if the termination flag is set | ||
| if self.terminate: | ||
| break | ||
| last_seq_num = self.sequence_number | ||
| data_to_yield = self.data | ||
| last_time = time.monotonic() | ||
|
|
||
| yield data_to_yield | ||
|
ETSells marked this conversation as resolved.
|
||
Uh oh!
There was an error while loading. Please reload this page.