-
Notifications
You must be signed in to change notification settings - Fork 19
obd: stop posting "adapter asleep or car off" on every boot (#56, #31) #65
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
Changes from all commits
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 |
|---|---|---|
|
|
@@ -52,6 +52,30 @@ | |
| # cycles (~3 min); the payload carries ts so the UI can show data age. | ||
| OBD_ALL_CACHE = os.path.expanduser("~/.carwatch/obd-all.json") | ||
| FULL_SWEEP_EVERY = 3 | ||
| # The "no engine data yet" epoch marker, PERSISTED for the same reason | ||
| # DEEP_STAMP above is: an in-memory flag dies with the process. obdwatch | ||
| # restarts on every boot and every rfcomm rebind, so the flag reset and the | ||
| # identical "adapter asleep or car off" line went to petrus's phone again | ||
| # (#56: 12:58Z and 13:06Z on 15 Sep, car parked at home; #31 is the same | ||
| # shape on service restart). State belongs on disk, not in a local. | ||
| NO_DATA_STAMP = os.path.expanduser("~/.carwatch/no-data-posted.stamp") | ||
|
|
||
|
|
||
| def _no_data_posted() -> bool: | ||
| return os.path.exists(NO_DATA_STAMP) | ||
|
|
||
|
|
||
| def _set_no_data_posted(on: bool) -> None: | ||
| try: | ||
| if on: | ||
| os.makedirs(os.path.dirname(NO_DATA_STAMP), exist_ok=True) | ||
| with open(NO_DATA_STAMP, "w") as f: | ||
| f.write(str(int(time.time()))) | ||
| elif os.path.exists(NO_DATA_STAMP): | ||
| os.remove(NO_DATA_STAMP) | ||
| except OSError as e: | ||
| # Never let bookkeeping kill the read loop; worst case we repost once. | ||
| print(f"obdwatch: no-data stamp {e}", flush=True) | ||
|
|
||
|
|
||
| # Map the 8 basic PIDs (the ones the room reads use and that never errno-5) | ||
|
|
@@ -252,7 +276,12 @@ def run() -> None: | |
| # off the BT adapter power-flaps, each flap > RECONNECT_GAP_S reset the | ||
| # post-state and re-posted the same no-data line into petrus's phone | ||
| # (4+ times on Aug 27, twice within 34s). State, not reconnects, decides. | ||
| no_data_posted = False | ||
| no_data_posted = _no_data_posted() # survives restarts (#31, #56) | ||
| seen_data = False # has a real reading been posted since we started? | ||
| # "asleep or car off" is only worth saying as a | ||
| # data -> lost TRANSITION. On a cold boot with the | ||
| # car parked it is not news, it is the normal | ||
| # state, and it was arriving as a notification. | ||
| last_dtc_key = None # last stored-DTC set, to post only on a CHANGE | ||
| last_posted_batt = None # hybrid-SoC at the last post; a high-water mark | ||
| # that follows charge UP silently and posts on a | ||
|
|
@@ -352,6 +381,8 @@ def run() -> None: | |
| post(f"Engine read (live from my OBD port): {line}") | ||
| last_post_readings = line | ||
| no_data_posted = False | ||
| seen_data = True | ||
| _set_no_data_posted(False) | ||
| if batt is not None: | ||
| last_posted_batt = batt # reset baseline on post | ||
| last_dtc_key = dtc_key | ||
|
|
@@ -457,12 +488,21 @@ def run() -> None: | |
| # faster read cadence does NOT re-spam the room (was 60s, which | ||
| # made the "LIVE 29s" badge grow to a minute between updates). | ||
| else: | ||
| if not last_post_readings and not no_data_posted: | ||
| if not last_post_readings and not no_data_posted and seen_data: | ||
| # `seen_data` is the #56 gate: post this ONLY as a | ||
| # data -> lost transition. Booting next to a parked car is | ||
| # not an event. The fact still shows in the presence and | ||
| # preflight tiles, which is where a resting state belongs. | ||
| hint = (result.get("summary", "no data") | ||
| if port else failure_hint(result)) | ||
| post(f"OBD: adapter/link present but no engine data yet - {hint}") | ||
| last_post_readings = "(failed)" | ||
| no_data_posted = True | ||
| _set_no_data_posted(True) | ||
| elif not last_post_readings and not seen_data: | ||
| print("obdwatch: no engine data and none seen this session " | ||
| "- not posting (car is simply off)", flush=True) | ||
| last_post_readings = "(failed)" | ||
|
Comment on lines
+502
to
+505
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.
On the targeted cold-boot path, an asleep car reaches this branch and stores the truthy Useful? React with 👍 / 👎. |
||
| next_try = time.time() + RETRY_COOLDOWN_S | ||
| # Live steering fills what was idle sleep time: sample the wheel angle | ||
| # off the passive CAN broadcast (id 0x0500 byte 0) and cache it, so | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,60 @@ | ||
| """#56 / #31: the car posted "adapter asleep or car off" on every boot. | ||
|
|
||
| @eclass sent petrus the identical line at 12:58Z and 13:06Z on 15 Sep with the | ||
| car parked at home. `no_data_posted` was a local variable, so every boot and | ||
| every rfcomm rebind started a fresh process with the flag cleared. The same | ||
| file already learned this lesson once - DEEP_STAMP is persisted because the | ||
| in-memory `deep_done` reset on rfcomm flaps - and it was not applied here. | ||
| """ | ||
| import importlib | ||
| import os | ||
| import sys | ||
| import tempfile | ||
| import unittest | ||
|
|
||
| sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) | ||
|
|
||
|
|
||
| class NoDataEpochMarker(unittest.TestCase): | ||
|
|
||
| def setUp(self): | ||
| self.tmp = tempfile.TemporaryDirectory() | ||
| self.addCleanup(self.tmp.cleanup) | ||
| from carwatch import obdwatch | ||
| self.ow = importlib.reload(obdwatch) | ||
| self.ow.NO_DATA_STAMP = os.path.join(self.tmp.name, "no-data-posted.stamp") | ||
|
|
||
| def test_marker_survives_a_restart(self): | ||
| # A fresh process must be able to see that the line was already sent. | ||
| self.assertFalse(self.ow._no_data_posted()) | ||
| self.ow._set_no_data_posted(True) | ||
| self.assertTrue(self.ow._no_data_posted(), | ||
| "the epoch marker did not persist; a restart reposts") | ||
|
|
||
| def test_a_successful_reading_clears_the_epoch(self): | ||
| self.ow._set_no_data_posted(True) | ||
| self.ow._set_no_data_posted(False) | ||
| self.assertFalse(self.ow._no_data_posted()) | ||
|
|
||
| def test_clearing_an_absent_marker_is_not_an_error(self): | ||
| self.ow._set_no_data_posted(False) # must not raise | ||
| self.assertFalse(self.ow._no_data_posted()) | ||
|
|
||
| def test_bookkeeping_failure_never_raises(self): | ||
| # A read-only or missing state dir must not kill the read loop. | ||
| self.ow.NO_DATA_STAMP = "/proc/definitely/not/writable/stamp" | ||
| self.ow._set_no_data_posted(True) # must not raise | ||
| self.assertFalse(self.ow._no_data_posted()) | ||
|
|
||
| def test_post_is_gated_on_having_seen_data(self): | ||
| # The #56 rule lives in the read loop; assert the source states it, so | ||
| # the gate cannot be dropped silently in a refactor. | ||
| import inspect | ||
| src = inspect.getsource(self.ow.run) | ||
| self.assertIn("and seen_data", src, | ||
| "the no-data post is no longer gated on a data -> lost " | ||
| "transition; it will fire on a cold boot again") | ||
|
|
||
|
|
||
| if __name__ == "__main__": | ||
| unittest.main() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
When a successful poll is followed by
result["ok"] == Falsewhile the adapter path remains present,last_post_readingsstill contains the successful line, so this condition is false regardless ofseen_data. Because the persistent rfcomm service can leave/dev/rfcomm0bound while the gateway stops answering, the intended data-to-lost transition can occur without any reconnect clearing this value, and the new alert is never posted.Useful? React with 👍 / 👎.