Add SIM868 cellular fallback for tracking uploads (untested, no SIM card)
modules/app_cellular.py: uses the SIM868's built-in AT-command HTTP(S) client (AT+HTTPINIT/HTTPPARA/HTTPACTION/HTTPREAD) instead of a real ppp0 interface, since the module's GNSS (polled continuously by app_gps.py) and its GSM/GPRS radio share the same physical UART (/dev/serial0) - a pppd/chat dial-up would take over that port entirely and starve GNSS polling. Staying in AT-command mode lets cellular HTTP calls interleave safely through the same serial lock. Only carries small JSON tracking batches, never dashcam video - cellular is far too slow/limited for that, and the modem's HTTP AT interface has too small an internal buffer anyway. Dashcam uploads remain WiFi/Ethernet-only, unchanged. modules/app_upload.py: tracking-batch upload always tries WiFi/ Ethernet first; only on a connection-level failure (not an HTTP error status, which means the server was reached) does it retry over cellular. Exposes last_network_path/cellular_fallback_configured in /api/upload/status; UI shows which path the last batch used. Feature-flagged off by default via new YARIS_CELLULAR_ENABLED/ YARIS_CELLULAR_APN/_APN_USER/_APN_PASS keys in db/upload_creds.env (all blank by default) - has zero effect on the existing WiFi upload path unless explicitly configured. Regression-tested against the real deployed server that normal WiFi uploads are unaffected while cellular stays unconfigured. *** UNTESTED against real cellular hardware - no SIM card was *** available. See docstrings in app_cellular.py and PLAN.md for *** exactly what needs validation once a SIM card is available. Co-authored-by: Copilot <[email protected]>
This commit is contained in:
+62
-11
@@ -6,6 +6,20 @@ paused independently via a persisted toggle (settings table in the same
|
||||
SQLite db used for tracking) so a user can stop shipping large video files
|
||||
over a slow/metered connection while trip logs keep flowing.
|
||||
|
||||
Network path: normal WiFi/Ethernet (via `requests`) is always tried first for
|
||||
everything. If a tracking-batch upload fails with a connection-level error
|
||||
(no route/DNS/timeout - not an HTTP error status, which means the server WAS
|
||||
reached) and the SIM868 cellular fallback is configured, the same small JSON
|
||||
batch is retried over the modem's AT-command HTTP client (see
|
||||
modules/app_cellular.py). Dashcam uploads never use cellular - only
|
||||
WiFi/Ethernet - since video files are far too large/slow for a GPRS/3G link;
|
||||
they simply wait for connectivity to return, same as before.
|
||||
|
||||
*** The cellular fallback path is UNTESTED (no SIM card available) - see
|
||||
*** modules/app_cellular.py's docstring. It's opt-in/feature-flagged off by
|
||||
*** default (YARIS_CELLULAR_ENABLED unset) so this has zero effect on the
|
||||
*** already-verified WiFi upload path unless explicitly configured.
|
||||
|
||||
Credentials/server URL are loaded from db/upload_creds.env (gitignored,
|
||||
never commit real values - mirrors the db/creds.env pattern already used by
|
||||
app_db.py for MongoDB).
|
||||
@@ -18,7 +32,7 @@ from threading import Thread
|
||||
import requests
|
||||
from dotenv import load_dotenv
|
||||
|
||||
from modules import app_sql, app_dash
|
||||
from modules import app_sql, app_dash, app_cellular
|
||||
|
||||
_BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||
load_dotenv(os.path.join(_BASE_DIR, "db", "upload_creds.env"))
|
||||
@@ -38,6 +52,7 @@ DASHCAM_UPLOAD_SETTING_KEY = "dashcam_upload_enabled"
|
||||
_upload_thread = None
|
||||
_last_error = None
|
||||
_last_success_at = None
|
||||
_last_network_path = None # "wifi" or "cellular" - last path a batch actually went out on
|
||||
|
||||
|
||||
def _headers():
|
||||
@@ -65,6 +80,8 @@ def get_status():
|
||||
"pending_tracking_rows": app_sql.count_unsent_tracking_rows(),
|
||||
"last_error": _last_error,
|
||||
"last_success_at": _last_success_at,
|
||||
"last_network_path": _last_network_path,
|
||||
"cellular_fallback_configured": app_cellular.is_configured(),
|
||||
}
|
||||
|
||||
|
||||
@@ -80,7 +97,16 @@ def _sha256_file(path):
|
||||
|
||||
|
||||
def _upload_tracking_batch():
|
||||
"""Send up to TRACKING_BATCH_SIZE not-yet-uploaded rows. Returns count sent."""
|
||||
"""Send up to TRACKING_BATCH_SIZE not-yet-uploaded rows. Returns count sent.
|
||||
|
||||
Tries WiFi/Ethernet (`requests`) first. If that fails at the connection
|
||||
level (no route, DNS failure, timeout - as opposed to the server
|
||||
responding with an HTTP error, which means it WAS reached) and the
|
||||
SIM868 cellular fallback is configured, retries the same batch over the
|
||||
modem's AT-command HTTP client before giving up.
|
||||
"""
|
||||
global _last_network_path
|
||||
|
||||
rows = app_sql.get_unsent_tracking_rows(limit=TRACKING_BATCH_SIZE)
|
||||
if not rows:
|
||||
return 0
|
||||
@@ -97,15 +123,40 @@ def _upload_tracking_batch():
|
||||
}
|
||||
for row in rows
|
||||
]
|
||||
row_ids = [row["id"] for row in rows]
|
||||
|
||||
resp = requests.post(
|
||||
f"{SERVER_URL}/api/v1/tracking/batch",
|
||||
json={"points": points},
|
||||
headers=_headers(),
|
||||
timeout=15,
|
||||
)
|
||||
resp.raise_for_status()
|
||||
app_sql.mark_tracking_rows_uploaded([row["id"] for row in rows])
|
||||
try:
|
||||
resp = requests.post(
|
||||
f"{SERVER_URL}/api/v1/tracking/batch",
|
||||
json={"points": points},
|
||||
headers=_headers(),
|
||||
timeout=15,
|
||||
)
|
||||
resp.raise_for_status()
|
||||
_last_network_path = "wifi"
|
||||
except requests.exceptions.ConnectionError as wifi_err:
|
||||
if not app_cellular.is_configured():
|
||||
raise
|
||||
print(f"WiFi/Ethernet upload unreachable ({wifi_err}), trying cellular fallback...")
|
||||
import json as _json
|
||||
|
||||
body_bytes = _json.dumps({"points": points}).encode()
|
||||
try:
|
||||
status_code, _body = app_cellular.http_post_json(
|
||||
f"{SERVER_URL}/api/v1/tracking/batch", body_bytes, _headers()
|
||||
)
|
||||
except app_cellular.CellularError as cell_err:
|
||||
# Neither path worked - raise the original WiFi error so the
|
||||
# normal backoff/retry logic in _upload_loop handles it.
|
||||
print(f"Cellular fallback also failed: {cell_err}")
|
||||
raise wifi_err
|
||||
if status_code >= 400:
|
||||
raise app_cellular.CellularError(
|
||||
f"cellular upload got HTTP {status_code}"
|
||||
)
|
||||
_last_network_path = "cellular"
|
||||
|
||||
app_sql.mark_tracking_rows_uploaded(row_ids)
|
||||
return len(rows)
|
||||
|
||||
|
||||
@@ -172,7 +223,7 @@ def _upload_loop():
|
||||
time.sleep(CATCHUP_INTERVAL)
|
||||
else:
|
||||
time.sleep(UPLOAD_INTERVAL)
|
||||
except requests.RequestException as e:
|
||||
except (requests.RequestException, app_cellular.CellularError) as e:
|
||||
_last_error = str(e)
|
||||
print(f"Upload failed ({e}), retrying in {backoff}s")
|
||||
time.sleep(backoff)
|
||||
|
||||
Reference in New Issue
Block a user