github ilyakruchinin/SomnoTrace v3.1.0

3 hours ago

v3.1.0 — changes since v3.0.3

☁️ New: Aerivue backend (aerivue.app)

  • Full upload backend for Aerivue, on par with the
    SleepHQ integration: per-day imports → multipart file posts →
    process → poll to completion.
  • Uploads the complete file set: STR.edf, Identification.json/.crc,
    SETTINGS/, DATALOG/<day>/ session groups, and OXYMETRY/<day>/
    Wellue/O2 Ring recordings — oximetry imports verified server-side
    (77 recordings, "imported" with reading counts).
  • Web portal: Aerivue card with enable toggle, masked API-key field,
    "Test connection" button (validates the key via GET /api/v1/me
    without saving), and dirty-state tracking. Settings persist in NVS
    (aer_en, aer_key); secrets are masked (***) in API responses.
  • New endpoint: POST /api/uploads/test-aerivue.
  • Upload state tracking, retries, cooldowns and the progress-bar card
    work identically to the existing backends — Aerivue appears as a
    third row automatically.
  • Error mapping: 401/403 → permanent, 404 (expired import) / 5xx /
    content_hash_mismatch → transient with automatic recovery;
    import-result details (sessions imported/skipped, nights imported,
    per-recording oximetry results) are logged.

🧩 Modular upload architecture

  • Extracted the shared TLS/HTTP/multipart plumbing (~560 lines) from
    uploader_sleephq.c into upload_http.c/.h — this is the seam
    future backends plug into.
  • uploader_sleephq.c refactored onto the shared layer; wire
    behaviour unchanged.
  • New supports_ox backend capability flag — the scheduler skips
    oximetry units for backends that can't take them.

🔌 Resilience: keep-alive connection handling

  • Long backfills could die mid-session when the remote edge closed the
    keep-alive connection (observed at ~1,000 requests / ~12 min on one
    socket — consistent with a Cloudflare edge request cap; the failure
    cascaded across groups until the day rolled back).
  • The shared HTTP layer now detects Connection: close, peer closes
    and TLS transport errors, marks the socket dead, reconnects
    transparently and retries the interrupted request once.
  • s_import_id survives socket reconnects — a day that loses its
    connection mid-flight continues into the same server-side import
    (48 h TTL) instead of orphaning it.

🪟 Fix: SMB uploads failing with STATUS_INVALID_PARAMETER on Windows

  • Root cause (verified byte-for-byte from a packet capture): when
    Remote Path was left empty (share root), the remote-path join
    produced /DATALOG/… — on the wire \DATALOG\…, a name with a
    leading separator. SMB2 CREATE names must be relative to the tree
    connect; Windows rejects a leading separator with
    STATUS_INVALID_PARAMETER on every CREATE, signed or unsigned.
  • smb_remote_base() now normalizes the configured path once — ""
    for the share root, else dir/ — and every join produces a strictly
    share-relative name. Affected configs (empty Remote Path + Windows
    server) failed 100% of uploads; non-empty paths and Samba servers
    were unaffected, which is why most setups never saw it.
  • "Test connection" now runs a write probe — mkdir of the DATALOG
    folder the uploader creates at every session start anyway (a fresh
    create and OBJECT_NAME_COLLISION both count as success, so nothing
    is created that uploads would not create). It catches read-only
    shares, permission problems and name-shape rejections — all of which
    previously passed connect+stat while every upload failed — and
    mirrors the adaptive-signing retry below.
  • Portal: the Remote Path placeholder now shows a bare folder name and
    the label notes "(leave empty for share root)" — the old
    /SomnoTrace example suggested a leading slash was required.

🔏 Adaptive SMB signing

  • Uploads start unsigned as before; on the first
    STATUS_INVALID_PARAMETER rejection the connection is rebuilt with
    signing forced and the operation retried — gated to SMB 3.1.1
    sessions with credentials.
  • Learned preference (s_sign_learned, RAM-only): subsequent sessions
    in the same boot connect signed directly; "Test connection" mirrors
    it. A learned-signed connect that fails falls back to unsigned — the
    preference can never wedge the uploader.
  • Guest/no-password sessions are skipped — they have no session key to
    sign with.
  • Kept for genuinely signing-enforced servers (per-share
    RequireSigning, hardened inbound policy). Note: the reported user
    failure turned out to be the share-root path bug above — signing was
    a wire-level correlation, not the cause.

😴 Fix: sleep-stage graphs could silently go missing until reboot (#299)

  • Every live scoring enqueue now records the .sst file it should
    produce; the worker stats that exact path ~3 minutes later and
    re-enqueues when it's absent (up to 3 attempts). A dropped job now
    heals in minutes instead of waiting for the next boot — previously
    every enqueue/score failure was a silent one-way drop whose only
    backstop was the boot reconcile.
  • Queue-full enqueue refusals are now WARN-logged instead of vanishing.
  • Recordings that can never score no longer churn the queue: a
    complete file with <30 valid epochs always persists an
    INSUFFICIENT stub (the old partial-source path persisted nothing),
    and deterministic parse failures write an INSUFFICIENT marker.
    Stubs self-correct — if the source later grows, its mtime overtakes
    the stub and the file re-scores.
  • Bounded periodic reconcile inside the worker: a 14-day window every
    30 min plus a full pass every 24 h; the boot-time full scan is
    unchanged. Per-pass cost scales with the window, not the recording
    history.

🕐 Fix: oximetry recording-name time validation (thanks @Plantucha, #297)

  • The OxyII driver carried its own filename-time parser with no range
    checks — a ring whose clock was never set produced names like
    20000101000000 and got filed under year 2000. Both drivers now
    share one parser (oximetry_time.c).
  • The canonical civil-time check could not fail: mktime() normalizes
    the struct in place (30 Feb → 2 Mar), so the round-trip compared
    normalized fields against themselves. Calendar fields are now
    validated before mktime() — impossible dates (30 Feb, 29 Feb in a
    non-leap year, month 13) are refused, and the 2015–2099 range applies
    to OxyII names too.
  • A start timestamp of 0 (the parsers' "not a time") is now refused
    before conversion — it used to label recordings 1970-01-01.
  • Covered by a dedicated host test (independently computed expected
    values, TZ-pinned incl. a DST boundary) plus three entries in the
    mutation suite.

🛠️ Smaller fixes

  • Stale uploader settings served to the portal (#313) —
    /api/uploads/config could return zeroed/stale fields when queried
    before uploader init on fast page loads. Now serializes a fresh NVS
    read instead of the cached copy.
  • Portal confirm dialog showed a literal "\n\n" (#310) — the
    Format SD confirmation now gets real line breaks.

✅ Verified

  • somnotrace.bin build clean, ~28% app partition free; 16/16 host
    tests pass (incl. the new oximetry_time suite).
  • SMB path fix verified against the reported Windows 11 setup —
    confirmed on the wire (CREATE name now relative) and by the reporter,
    whose share-root config uploads after the workaround; this release
    makes the workaround unnecessary.
  • First Aerivue production sync: 45/45 days (148 sessions) and 77/77
    oximetry recordings landed, none refused.

Don't miss a new SomnoTrace release

NewReleases is sending notifications on new releases.