diff --git a/.gitignore b/.gitignore index 0808c4a..6381ead 100644 --- a/.gitignore +++ b/.gitignore @@ -480,3 +480,4 @@ # Vim temporary swap files *.swp +.codex diff --git a/Program.cs b/Program.cs index 1bc95b9..a258a89 100644 --- a/Program.cs +++ b/Program.cs @@ -42,6 +42,7 @@ return 0; } + WritePayloadToVarLog(reading); Console.Error.WriteLine($"http {statusCode}: {body}"); return 2; } @@ -80,6 +81,27 @@ } /// + /// 将失败的上报 payload 写入 /var/log,便于后续排查与补发。 + /// + /// 失败时的传感器读数。 + private static void WritePayloadToVarLog(Jciebu2 payload) + { + try + { + var directory = "/var/log/sensorreadings"; + Directory.CreateDirectory(directory); + var fileName = $"{DateTimeOffset.UtcNow:yyyyMMddHHmmssfff}.json"; + var path = Path.Combine(directory, fileName); + var json = JsonSerializer.Serialize(payload, SensorReadingsJsonContext.Default.Jciebu2); + File.WriteAllText(path, json, Encoding.UTF8); + } + catch (Exception ex) + { + Console.Error.WriteLine($"error: failed to write payload log: {ex.Message}"); + } + } + + /// /// 从串口设备读取最新数据,并统一时间戳与位置字段。 /// /// 串口设备路径。 @@ -215,7 +237,8 @@ { if (data.Length < 56) { - throw new InvalidOperationException($"response too short: {data.Length} bytes (need >= 56)"); + Console.Error.WriteLine($"error: response too short: {data.Length} bytes (need >= 56)"); + Environment.Exit(2); } return new Jciebu2 diff --git a/ReadSensors.py b/ReadSensors.py deleted file mode 100644 index ff2bec3..0000000 --- a/ReadSensors.py +++ /dev/null @@ -1,117 +0,0 @@ -#!/usr/bin/env python3 -"""Read 2JCIE-BU once and POST to OData. - -Endpoint (given): http://10.16.1.100:8016/odata/SensorReadings - -No pip deps: uses Python stdlib only. - -Env vars: - SENSOR_PORT default /dev/ttyUSB0 - SENSOR_LOCATION default home_2f - ODATA_URL default http://10.16.1.100:8016/odata/SensorReadings - ODATA_TOKEN optional bearer token - ODATA_TIMEOUT_S default 10 -""" - -import json -import os -import sys -import urllib.request -import urllib.error - -import importlib.util -from pathlib import Path - -PORT = os.getenv("SENSOR_PORT", "/dev/ttyUSB0") -LOCATION = os.getenv("SENSOR_LOCATION", "home_2f") -ODATA_URL = os.getenv("ODATA_URL", "http://10.16.1.100:8016/odata/SensorReadings") -ODATA_TOKEN = os.getenv("ODATA_TOKEN", "").strip() -TIMEOUT_S = float(os.getenv("ODATA_TIMEOUT_S", "10")) - -# Load local sensor module (filename starts with digit) -_2JCIE_PATH = Path(__file__).with_name("2jciebu.py") -_spec = importlib.util.spec_from_file_location("_2jciebu", _2JCIE_PATH) -_mod = importlib.util.module_from_spec(_spec) -assert _spec and _spec.loader -_spec.loader.exec_module(_mod) -get_latest_reading = _mod.get_latest_reading - - -def _to_odata_payload(r: dict) -> dict: - """Map our snake_case reading dict to the OData entity property names. - - The OData service appears to use PascalCase keys (e.g. Ts, Location, TemperatureC...). - """ - ts = r.get("ts") - if ts is None: - raise ValueError("missing ts") - - # datetimeoffset(0) -> ISO8601 without fractional seconds, with Z - ts_str = ts.isoformat().replace("+00:00", "Z") - - payload = { - "Ts": ts_str, - "Location": r.get("location") or LOCATION, - "TemperatureC": r.get("temperature_c"), - "HumidityRh": r.get("humidity_rh"), - "AmbientLight": r.get("ambient_light"), - "PressureHpa": r.get("pressure_hpa"), - "NoiseDb": r.get("noise_db"), - "EtvocPpb": r.get("etvoc_ppb"), - "Eco2Ppm": r.get("eco2_ppm"), - "DiscomfortIndex": r.get("discomfort_index"), - "HeatstrokeIndex": r.get("heatstroke_index"), - "VibrationInfo": r.get("vibration_info"), - "SiValue": r.get("si_value"), - "PgaGal": r.get("pga_gal"), - "SeismicIntensity": r.get("seismic_intensity"), - "TemperatureFlag": r.get("temperature_flag"), - "HumidityFlag": r.get("humidity_flag"), - "AmbientLightFlag": r.get("ambient_light_flag"), - "PressureFlag": r.get("pressure_flag"), - "NoiseFlag": r.get("noise_flag"), - "EtvocFlag": r.get("etvoc_flag"), - "Eco2Flag": r.get("eco2_flag"), - "DiscomfortFlag": r.get("discomfort_flag"), - "HeatstrokeFlag": r.get("heatstroke_flag"), - "SiFlag": r.get("si_flag"), - "PgaFlag": r.get("pga_flag"), - "SeismicIntensityFlag": r.get("seismic_intensity_flag"), - } - - # Drop None values to avoid type issues on some OData backends - return {k: v for k, v in payload.items() if v is not None} - - -def post_odata(payload: dict) -> tuple[int, str]: - body = json.dumps(payload, ensure_ascii=False).encode("utf-8") - headers = { - "Content-Type": "application/json", - "Accept": "application/json", - } - if ODATA_TOKEN: - headers["Authorization"] = f"Bearer {ODATA_TOKEN}" - - req = urllib.request.Request(ODATA_URL, data=body, headers=headers, method="POST") - try: - with urllib.request.urlopen(req, timeout=TIMEOUT_S) as resp: - resp_body = resp.read().decode("utf-8", errors="replace") - return resp.status, resp_body - except urllib.error.HTTPError as e: - err_body = e.read().decode("utf-8", errors="replace") - return e.code, err_body - - -def main(): - r = get_latest_reading(port=PORT, location=LOCATION) - payload = _to_odata_payload(r) - code, body = post_odata(payload) - if 200 <= code < 300: - print("ok") - return - print(f"http {code}: {body}", file=sys.stderr) - sys.exit(2) - - -if __name__ == "__main__": - main()