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, andOXYMETRY/<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 viaGET /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.cintoupload_http.c/.h— this is the seam
future backends plug into. uploader_sleephq.crefactored onto the shared layer; wire
behaviour unchanged.- New
supports_oxbackend 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_idsurvives 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_PARAMETERon every CREATE, signed or unsigned. smb_remote_base()now normalizes the configured path once —""
for the share root, elsedir/— 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 —
mkdirof theDATALOG
folder the uploader creates at every session start anyway (a fresh
create andOBJECT_NAME_COLLISIONboth 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
/SomnoTraceexample suggested a leading slash was required.
🔏 Adaptive SMB signing
- Uploads start unsigned as before; on the first
STATUS_INVALID_PARAMETERrejection 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
.sstfile 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
INSUFFICIENTstub (the old partial-source path persisted nothing),
and deterministic parse failures write anINSUFFICIENTmarker.
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
20000101000000and 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 beforemktime()— 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/configcould 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.binbuild clean, ~28% app partition free; 16/16 host
tests pass (incl. the newoximetry_timesuite).- 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.