Add local settings management
Add cellular-first ntfy delivery with WiFi fallback and safe SIM868 HTTP header framing. Co-authored-by: Copilot <[email protected]>
This commit is contained in:
+70
-15
@@ -28,15 +28,11 @@ AT interface has a small internal buffer unsuited to multi-MB bodies.
|
||||
import os
|
||||
import time
|
||||
|
||||
from modules import app_gps
|
||||
|
||||
APN = os.getenv("YARIS_CELLULAR_APN", "")
|
||||
APN_USER = os.getenv("YARIS_CELLULAR_APN_USER", "")
|
||||
APN_PASS = os.getenv("YARIS_CELLULAR_APN_PASS", "")
|
||||
ENABLED = os.getenv("YARIS_CELLULAR_ENABLED", "") == "1"
|
||||
from modules import app_gps, app_sql
|
||||
|
||||
GPRS_BRINGUP_TIMEOUT = 30 # seconds to wait for AT+CIICR / AT+CIFSR
|
||||
HTTP_ACTION_TIMEOUT = 60 # seconds to wait for the +HTTPACTION URC (2G/3G can be slow)
|
||||
HTTP_USERDATA_MAX_BYTES = 1024
|
||||
|
||||
_gprs_context_up = False
|
||||
|
||||
@@ -48,9 +44,19 @@ class CellularError(Exception):
|
||||
|
||||
def is_configured():
|
||||
"""True once the user has filled in an APN and flipped the enabled flag
|
||||
in db/upload_creds.env. Off by default - nothing cellular-related runs
|
||||
unless explicitly opted into."""
|
||||
return ENABLED and bool(APN)
|
||||
in the Settings page or legacy environment configuration. Off by default -
|
||||
nothing cellular-related runs unless explicitly opted into."""
|
||||
enabled = app_sql.get_setting(
|
||||
"cellular_enabled",
|
||||
"1" if os.getenv("YARIS_CELLULAR_ENABLED", "") == "1" else "0",
|
||||
)
|
||||
return enabled == "1" and bool(get_apn())
|
||||
|
||||
|
||||
def get_apn():
|
||||
return app_sql.get_setting(
|
||||
"cellular_apn", os.getenv("YARIS_CELLULAR_APN", "")
|
||||
)
|
||||
|
||||
|
||||
def _read_until(ser, terminators, timeout):
|
||||
@@ -104,7 +110,16 @@ def _ensure_gprs_context():
|
||||
|
||||
_at(ser, "AT+CGATT=1", timeout=10)
|
||||
|
||||
apn_cmd = f'AT+CSTT="{APN}","{APN_USER}","{APN_PASS}"'
|
||||
apn = _validate_http_parameter(get_apn(), "APN")
|
||||
apn_user = _validate_http_parameter(
|
||||
os.getenv("YARIS_CELLULAR_APN_USER", ""),
|
||||
"APN username",
|
||||
)
|
||||
apn_password = _validate_http_parameter(
|
||||
os.getenv("YARIS_CELLULAR_APN_PASS", ""),
|
||||
"APN password",
|
||||
)
|
||||
apn_cmd = f'AT+CSTT="{apn}","{apn_user}","{apn_password}"'
|
||||
_at(ser, apn_cmd, timeout=10)
|
||||
|
||||
_at(ser, "AT+CIICR", timeout=GPRS_BRINGUP_TIMEOUT)
|
||||
@@ -116,8 +131,22 @@ def _ensure_gprs_context():
|
||||
_gprs_context_up = True
|
||||
|
||||
|
||||
def http_post_json(url, body_bytes, extra_headers, timeout=HTTP_ACTION_TIMEOUT):
|
||||
"""POST a small JSON body to `url` via the modem's built-in HTTP client.
|
||||
def _validate_http_parameter(value, label):
|
||||
if not isinstance(value, str) or any(
|
||||
char in value for char in ('"', "\r", "\n")
|
||||
):
|
||||
raise CellularError(f"{label} contains unsupported characters")
|
||||
return value
|
||||
|
||||
|
||||
def http_post(
|
||||
url,
|
||||
body_bytes,
|
||||
extra_headers,
|
||||
content_type="application/json",
|
||||
timeout=HTTP_ACTION_TIMEOUT,
|
||||
):
|
||||
"""POST a small body to `url` via the modem's built-in HTTP client.
|
||||
|
||||
Returns (status_code, response_body_bytes). Raises CellularError on any
|
||||
failure (modem not responding, no network attach, HTTP-layer failure,
|
||||
@@ -126,6 +155,18 @@ def http_post_json(url, body_bytes, extra_headers, timeout=HTTP_ACTION_TIMEOUT):
|
||||
"""
|
||||
if not is_configured():
|
||||
raise CellularError("cellular fallback not configured (APN/enabled flag not set)")
|
||||
_validate_http_parameter(url, "URL")
|
||||
_validate_http_parameter(content_type, "Content type")
|
||||
for name, value in extra_headers.items():
|
||||
_validate_http_parameter(name, "Header name")
|
||||
_validate_http_parameter(value, f"{name} header")
|
||||
header_lines = r"\r\n".join(
|
||||
f"{name}: {value}" for name, value in extra_headers.items()
|
||||
)
|
||||
if header_lines:
|
||||
header_lines += r"\r\n"
|
||||
if len(header_lines.encode("utf-8")) > HTTP_USERDATA_MAX_BYTES:
|
||||
raise CellularError("HTTP headers exceed the modem's 1024-byte limit")
|
||||
|
||||
_ensure_gprs_context()
|
||||
|
||||
@@ -153,10 +194,14 @@ def http_post_json(url, body_bytes, extra_headers, timeout=HTTP_ACTION_TIMEOUT):
|
||||
"cellular is unavailable on this firmware. Consider a "
|
||||
"plain-HTTP router port-forward path for cellular use."
|
||||
)
|
||||
_at(ser, 'AT+HTTPPARA="CONTENT","application/json"', timeout=5)
|
||||
_at(ser, f'AT+HTTPPARA="CONTENT","{content_type}"', timeout=5)
|
||||
|
||||
header_lines = "".join(f"{k}: {v}\r\n" for k, v in extra_headers.items())
|
||||
_at(ser, f'AT+HTTPPARA="USERDATA","{header_lines}"', timeout=5)
|
||||
if header_lines:
|
||||
_at(
|
||||
ser,
|
||||
f'AT+HTTPPARA="USERDATA","{header_lines}"',
|
||||
timeout=5,
|
||||
)
|
||||
|
||||
_at(
|
||||
ser,
|
||||
@@ -192,3 +237,13 @@ def http_post_json(url, body_bytes, extra_headers, timeout=HTTP_ACTION_TIMEOUT):
|
||||
_at(ser, "AT+HTTPTERM", timeout=5)
|
||||
except CellularError:
|
||||
pass
|
||||
|
||||
|
||||
def http_post_json(url, body_bytes, extra_headers, timeout=HTTP_ACTION_TIMEOUT):
|
||||
return http_post(
|
||||
url,
|
||||
body_bytes,
|
||||
extra_headers,
|
||||
content_type="application/json",
|
||||
timeout=timeout,
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user