From aad9e32268d1a5f2541092b4ca68cebd3d1be558 Mon Sep 17 00:00:00 2001 From: Jaro Marval Date: Mon, 28 Sep 2026 20:18:41 -0500 Subject: [PATCH 01/21] Adding a small host test harness --- .gitignore | 1 + test/Makefile | 11 +++++++++++ test/check.h | 25 +++++++++++++++++++++++++ 3 files changed, 37 insertions(+) create mode 100644 test/Makefile create mode 100644 test/check.h diff --git a/.gitignore b/.gitignore index b66e7014..725163d2 100644 --- a/.gitignore +++ b/.gitignore @@ -5,3 +5,4 @@ /toolchain compile_flags.txt divinus +/test/*_test diff --git a/test/Makefile b/test/Makefile new file mode 100644 index 00000000..7f18a080 --- /dev/null +++ b/test/Makefile @@ -0,0 +1,11 @@ +CFLAGS = -std=gnu99 -Wall -Wno-parentheses -I../src \ + -DCLOCK_MONOTONIC_COARSE=CLOCK_MONOTONIC -Dmmap64=mmap +TESTS = + +all: $(TESTS) + @for t in $(TESTS); do ./$$t || exit 1; done + +clean: + rm -f $(TESTS) + +.PHONY: all clean diff --git a/test/check.h b/test/check.h new file mode 100644 index 00000000..cddfb641 --- /dev/null +++ b/test/check.h @@ -0,0 +1,25 @@ +#pragma once + +#include +#include +#include + +static int checkFailures = 0; + +#define CHECK(cond) \ + do { \ + if (!(cond)) { \ + fprintf(stderr, "%s:%d: CHECK failed: %s\n", __FILE__, __LINE__, #cond); \ + checkFailures++; \ + } \ + } while (0) + +#define CHECK_DONE() \ + do { \ + if (checkFailures) { \ + fprintf(stderr, "%d check(s) failed\n", checkFailures); \ + return EXIT_FAILURE; \ + } \ + printf("%s: ok\n", __FILE__); \ + return EXIT_SUCCESS; \ + } while (0) From 8f66c80067a136ef70b521c93ff26be5a77f754f Mon Sep 17 00:00:00 2001 From: Jaro Marval Date: Mon, 28 Sep 2026 20:38:27 -0500 Subject: [PATCH 02/21] Adding namespace-agnostic SOAP helpers, fixing base64 padding --- src/hal/tools.c | 6 +- src/soap.c | 157 +++++++++++++++++++++++++++++++++++++++++++++++ src/soap.h | 17 +++++ test/Makefile | 5 +- test/soap_test.c | 74 ++++++++++++++++++++++ 5 files changed, 255 insertions(+), 4 deletions(-) create mode 100644 src/soap.c create mode 100644 src/soap.h create mode 100644 test/soap_test.c diff --git a/src/hal/tools.c b/src/hal/tools.c index 83ec44af..bf09f06c 100644 --- a/src/hal/tools.c +++ b/src/hal/tools.c @@ -35,9 +35,9 @@ int base64_decode(char *decoded, const char *string, int maxLen) { } } - if (buflen > 0 && i + buflen <= maxLen) { - memcpy(decoded + i, buf, buflen); - i += buflen; + if (buflen > 1 && i + buflen - 1 <= maxLen) { + memcpy(decoded + i, buf, buflen - 1); + i += buflen - 1; } return i; diff --git a/src/soap.c b/src/soap.c new file mode 100644 index 00000000..c0681316 --- /dev/null +++ b/src/soap.c @@ -0,0 +1,157 @@ +#include "soap.h" + +#include +#include +#include +#include +#include + +#include "hal/tools.h" + +static const char *soap_name_end(const char *name) { + while (*name && !isspace((unsigned char)*name) && *name != '>' && *name != '/') + name++; + return name; +} + +const char *soap_tag(const char *xml, const char *tag) { + size_t len = strlen(tag); + + for (const char *pos = strchr(xml, '<'); pos; pos = strchr(pos + 1, '<')) { + const char *name = pos + 1, *end; + if (*name == '/' || *name == '?' || *name == '!') continue; + end = soap_name_end(name); + const char *colon = memchr(name, ':', end - name); + if (colon) name = colon + 1; + if (end - name == len && !strncmp(name, tag, len)) + return pos; + } + + return NULL; +} + +bool soap_tag_text(const char *xml, const char *tag, char *text, size_t size) { + const char *start = soap_tag(xml, tag), *end; + + if (!start || !(start = strchr(start, '>'))) return false; + if (start[-1] == '/') { + *text = '\0'; + return true; + } + start++; + if (!(end = strstr(start, "= size) return false; + + memcpy(text, start, end - start); + text[end - start] = '\0'; + return true; +} + +bool soap_tag_attr_has(const char *xml, const char *tag, const char *needle) { + const char *start = soap_tag(xml, tag), *end, *hit; + + if (!start || !(end = strchr(start, '>'))) return false; + hit = strstr(start, needle); + return hit && hit < end; +} + +bool soap_action(const char *xml, char *action, size_t size) { + const char *pos = soap_tag(xml, "Body"), *end; + + if (!pos || !(pos = strchr(pos, '>'))) return false; + while (*++pos && isspace((unsigned char)*pos)); + if (*pos != '<') return false; + + end = soap_name_end(++pos); + const char *colon = memchr(pos, ':', end - pos); + if (colon) pos = colon + 1; + if (end == pos || end - pos >= size) return false; + + memcpy(action, pos, end - pos); + action[end - pos] = '\0'; + return true; +} + +int soap_duration(const char *text) { + long total = 0, value; + bool clock = false; + char *end; + + if (*text++ != 'P') return -1; + + while (*text) { + if (*text == 'T') { + clock = true; + text++; + continue; + } + value = strtol(text, &end, 10); + if (end == text || value < 0) return -1; + if (*end == '.') + while (isdigit((unsigned char)*++end)); + switch (*end) { + case 'D': if (clock) return -1; total += value * 86400; break; + case 'H': if (!clock) return -1; total += value * 3600; break; + case 'M': if (!clock) return -1; total += value * 60; break; + case 'S': if (!clock) return -1; total += value; break; + default: return -1; + } + if (total > INT_MAX) return -1; + text = end + 1; + } + + return total; +} + +time_t soap_datetime(const char *text) { + struct tm tm = {0}; + + if (sscanf(text, "%4d-%2d-%2dT%2d:%2d:%2d", &tm.tm_year, &tm.tm_mon, &tm.tm_mday, + &tm.tm_hour, &tm.tm_min, &tm.tm_sec) != 6) return -1; + tm.tm_year -= 1900; + tm.tm_mon -= 1; + return timegm(&tm); +} + +void soap_datetime_format(time_t when, char *text, size_t size) { + struct tm tm; + + gmtime_r(&when, &tm); + strftime(text, size, "%Y-%m-%dT%H:%M:%SZ", &tm); +} + +int soap_term_seconds(const char *xml, const char *tag, time_t now, int fallback, int max) { + char text[64]; + long seconds; + + if (!soap_tag_text(xml, tag, text, sizeof(text))) return fallback; + + if (*text == 'P') + seconds = soap_duration(text); + else { + time_t when = soap_datetime(text); + seconds = when < 0 ? -1 : when - now; + if (when >= 0 && seconds < 1) seconds = 1; + } + + if (seconds < 0) return fallback; + if (seconds < 1) return 1; + return seconds > max ? max : seconds; +} + +bool soap_digest_valid(const char *nonce, const char *created, const char *pass, const char *digest) { + char nonceDec[64], expected[32]; + unsigned char hash[SHA1_DIGEST_SIZE]; + sha1_context ctx; + + int nonceLen = base64_decode(nonceDec, nonce, sizeof(nonceDec)); + if (nonceLen <= 0) return false; + + sha1_init(&ctx); + sha1_update(&ctx, (unsigned char *)nonceDec, nonceLen); + sha1_update(&ctx, (unsigned char *)created, strlen(created)); + sha1_update(&ctx, (unsigned char *)pass, strlen(pass)); + sha1_final(hash, &ctx); + + base64_encode(expected, (char *)hash, SHA1_DIGEST_SIZE); + return !strcmp(digest, expected); +} diff --git a/src/soap.h b/src/soap.h new file mode 100644 index 00000000..9db2e4b6 --- /dev/null +++ b/src/soap.h @@ -0,0 +1,17 @@ +#pragma once + +#include +#include +#include + +const char *soap_tag(const char *xml, const char *tag); +bool soap_tag_text(const char *xml, const char *tag, char *text, size_t size); +bool soap_tag_attr_has(const char *xml, const char *tag, const char *needle); +bool soap_action(const char *xml, char *action, size_t size); + +int soap_duration(const char *text); +time_t soap_datetime(const char *text); +void soap_datetime_format(time_t when, char *text, size_t size); +int soap_term_seconds(const char *xml, const char *tag, time_t now, int fallback, int max); + +bool soap_digest_valid(const char *nonce, const char *created, const char *pass, const char *digest); diff --git a/test/Makefile b/test/Makefile index 7f18a080..48a0f7d6 100644 --- a/test/Makefile +++ b/test/Makefile @@ -1,6 +1,6 @@ CFLAGS = -std=gnu99 -Wall -Wno-parentheses -I../src \ -DCLOCK_MONOTONIC_COARSE=CLOCK_MONOTONIC -Dmmap64=mmap -TESTS = +TESTS = soap_test all: $(TESTS) @for t in $(TESTS); do ./$$t || exit 1; done @@ -8,4 +8,7 @@ all: $(TESTS) clean: rm -f $(TESTS) +soap_test: soap_test.c ../src/soap.c ../src/hal/tools.c + $(CC) $(CFLAGS) $^ -o $@ + .PHONY: all clean diff --git a/test/soap_test.c b/test/soap_test.c new file mode 100644 index 00000000..4057d839 --- /dev/null +++ b/test/soap_test.c @@ -0,0 +1,74 @@ +#include "check.h" +#include "hal/tools.h" +#include "soap.h" + +int main(void) { + char text[64]; + + CHECK(soap_action("", + text, sizeof(text))); + CHECK(!strcmp(text, "GetCapabilities")); + CHECK(soap_action("\n ", + text, sizeof(text))); + CHECK(!strcmp(text, "GetStreamUri")); + CHECK(!soap_action("
", text, sizeof(text))); + + CHECK(soap_tag_text("admin", "Username", text, sizeof(text))); + CHECK(!strcmp(text, "admin")); + CHECK(soap_tag_text("admin", "Username", text, sizeof(text))); + CHECK(!strcmp(text, "admin")); + CHECK(!soap_tag_text("", "Username", text, sizeof(text))); + CHECK(!soap_tag_text("0123456789", "Username", text, 8)); + CHECK(soap_tag_text("PT30S", "Timeout", text, sizeof(text))); + CHECK(!strcmp(text, "PT30S")); + CHECK(soap_tag_text("", "Filter", text, sizeof(text))); + CHECK(!strcmp(text, "")); + + CHECK(soap_tag_attr_has("x", + "Password", "PasswordDigest")); + CHECK(!soap_tag_attr_has("PasswordDigest", + "Password", "PasswordDigest")); + + CHECK(soap_duration("PT30S") == 30); + CHECK(soap_duration("PT24H") == 86400); + CHECK(soap_duration("PT1H30M") == 5400); + CHECK(soap_duration("P1DT1S") == 86401); + CHECK(soap_duration("PT0.5S") == 0); + CHECK(soap_duration("30") == -1); + CHECK(soap_duration("PT5X") == -1); + CHECK(soap_duration("P5H") == -1); + + CHECK(soap_datetime("2026-09-28T10:00:00Z") == 1790589600); + CHECK(soap_datetime("2026-09-28T10:00:00.123Z") == 1790589600); + CHECK(soap_datetime("yesterday") == -1); + soap_datetime_format(1790589600, text, sizeof(text)); + CHECK(!strcmp(text, "2026-09-28T10:00:00Z")); + + time_t now = 1790589600; + CHECK(soap_term_seconds("PT24H", + "InitialTerminationTime", now, 3600, 86400) == 86400); + CHECK(soap_term_seconds("PT48H", + "InitialTerminationTime", now, 3600, 86400) == 86400); + CHECK(soap_term_seconds("", "InitialTerminationTime", now, 3600, 86400) == 3600); + CHECK(soap_term_seconds("junk", + "TerminationTime", now, 3600, 86400) == 3600); + CHECK(soap_term_seconds("2026-09-28T10:01:00Z", + "TerminationTime", now, 3600, 86400) == 60); + CHECK(soap_term_seconds("2026-09-28T09:00:00Z", + "TerminationTime", now, 3600, 86400) == 1); + + char decoded[64]; + CHECK(base64_decode(decoded, "AAECAwQFBgcICQoLDA0ODw==", sizeof(decoded)) == 16); + CHECK(base64_decode(decoded, "AAECAwQFBgcICQoLDA0O", sizeof(decoded)) == 15); + CHECK(base64_decode(decoded, "QUI=", sizeof(decoded)) == 2 && !memcmp(decoded, "AB", 2)); + + // Vectors computed with Python's base64 and hashlib + CHECK(soap_digest_valid("AAECAwQFBgcICQoLDA0ODw==", "2026-09-28T10:00:00Z", "secret", + "ULiogxchk2lxIjGaG/ZQGvrxNZo=")); + CHECK(soap_digest_valid("AAECAwQFBgcICQoLDA0O", "2026-09-28T10:00:00Z", "secret", + "TeXki4uPUCX4dC42CRxGIPV4Nbo=")); + CHECK(!soap_digest_valid("AAECAwQFBgcICQoLDA0ODw==", "2026-09-28T10:00:00Z", "wrong", + "ULiogxchk2lxIjGaG/ZQGvrxNZo=")); + + CHECK_DONE(); +} From 5728858d55b7b7866107887e581da2259343f678 Mon Sep 17 00:00:00 2001 From: Jaro Marval Date: Mon, 28 Sep 2026 20:40:09 -0500 Subject: [PATCH 03/21] Parsing ONVIF actions and credentials by local names --- src/onvif.c | 103 ++++++++++----------------------------------------- src/onvif.h | 1 + src/server.c | 4 ++ 3 files changed, 24 insertions(+), 84 deletions(-) diff --git a/src/onvif.c b/src/onvif.c index 10fd5b54..7896a6fb 100644 --- a/src/onvif.c +++ b/src/onvif.c @@ -137,31 +137,8 @@ void *onvif_thread(void) { char* onvif_extract_soap_action(const char* soap_data) { static char action[128]; - char *action_start = NULL; - char *body_start = strstr(soap_data, "Body"); - if (!body_start) return NULL; - - body_start = strchr(body_start, '>'); - if (!body_start) return NULL; - body_start++; - - while (*body_start && isspace(*body_start)) body_start++; - - if (*body_start != '<') return NULL; - body_start++; - - char *action_end = strchr(body_start, ' '); - if (!action_end) action_end = strchr(body_start, '>'); - if (!action_end) return NULL; - - int action_len = action_end - body_start; - if (action_len >= sizeof(action)) action_len = sizeof(action) - 1; - - strncpy(action, body_start, action_len); - action[action_len] = '\0'; - - return action; + return soap_action(soap_data, action, sizeof(action)) ? action : NULL; } bool onvif_is_preauth_action(const char *action) { @@ -176,73 +153,31 @@ bool onvif_is_preauth_action(const char *action) { } bool onvif_validate_soap_auth(const char *soap_data) { - const char *created_tag = "Created", *digest_tag = "PasswordDigest", *nonce_tag = ""))) return false; - memcpy(user, start, end - start); - user[end - start] = '\0'; + if (!token || + !soap_tag_text(token, "Username", user, sizeof(user)) || + !soap_tag_text(token, "Password", pass, sizeof(pass))) return false; if (!EQUALS(user, app_config.onvif_auth_user)) { HAL_WARNING("onvif", "Invalid username: %s\n", user); return false; } - if (!(start = strstr(soap_data, pass_tag)) || - !(start += strlen(pass_tag))) return false; - if ((pos = strstr(start, type_attr)) < (start = strchr(start, '>')) && - (pos += strlen(type_attr)) && strstr(pos, digest_tag)) digest = 1; - if (!(end = strstr(start, ""))) return false; - memcpy(pass, ++start, end - start); - pass[end - start] = '\0'; - - if (digest) { - char digest_comp[SHA1_DIGEST_SIZE] = {0}, nonce_dec[64], pass_dec[64]; - sha1_context ctx; - - if (!(start = strstr(soap_data, nonce_tag)) || - !(start = strchr(start, '>'))) return false; - if (!(end = strstr(++start, ""))) return false; - memcpy(nonce, start, end - start); - nonce[end - start] = '\0'; - - if (!(start = strstr(soap_data, created_tag)) || - !(start = strchr(start, '>'))) return false; - if (!(end = strstr(++start, ""))) return false; - memcpy(created, start, end - start); - created[end - start] = '\0'; - - int nonce_len = base64_decode(nonce_dec, nonce, sizeof(nonce_dec)); - if (nonce_len < 0) return false; - - sha1_init(&ctx); - sha1_update(&ctx, (unsigned char *)nonce_dec, nonce_len - 1); - sha1_update(&ctx, (unsigned char *)created, strlen(created)); - sha1_update(&ctx, (unsigned char *)app_config.onvif_auth_pass, strlen(app_config.onvif_auth_pass)); - sha1_final(digest_comp, &ctx); - - int pass_len = base64_encode(pass_dec, digest_comp, SHA1_DIGEST_SIZE); - if (pass_len < 0) return false; - pass_dec[pass_len] = '\0'; - - bool valid = !memcmp(pass, pass_dec, pass_len); - if (valid) - HAL_INFO("onvif", "Valid password digest!\n"); - else - HAL_WARNING("onvif", "Invalid password digest!\n"); - return valid; - } else { - bool valid = EQUALS(pass, app_config.onvif_auth_pass); - if (valid) - HAL_INFO("onvif", "Valid password provided!\n"); - else - HAL_WARNING("onvif", "Invalid password provided!\n"); - return valid; - } + bool valid; + if (soap_tag_attr_has(token, "Password", "PasswordDigest")) + valid = soap_tag_text(token, "Nonce", nonce, sizeof(nonce)) && + soap_tag_text(token, "Created", created, sizeof(created)) && + soap_digest_valid(nonce, created, app_config.onvif_auth_pass, pass); + else + valid = EQUALS(pass, app_config.onvif_auth_pass); + + if (valid) + HAL_INFO("onvif", "Valid credentials provided!\n"); + else + HAL_WARNING("onvif", "Invalid credentials provided!\n"); + return valid; } void onvif_respond_capabilities(char *response, int *respLen) { diff --git a/src/onvif.h b/src/onvif.h index 2aadf692..dfe19bfe 100644 --- a/src/onvif.h +++ b/src/onvif.h @@ -7,6 +7,7 @@ #include "app_config.h" #include "network.h" +#include "soap.h" int start_onvif(void); void stop_onvif(void); diff --git a/src/server.c b/src/server.c index 56c06497..c7c636db 100644 --- a/src/server.c +++ b/src/server.c @@ -619,6 +619,10 @@ void respond_request(http_request_t *req) { } char *action = onvif_extract_soap_action(req->payload); + if (!action) { + send_http_error(req->clntFd, 400); + return; + } HAL_INFO("onvif", "\x1b[32mAction: %s\x1b[0m\n", action); respLen = sizeof(response); From 6d62338dd382df38cb4599f514e09f12fef6bce2 Mon Sep 17 00:00:00 2001 From: Jaro Marval Date: Mon, 28 Sep 2026 20:43:06 -0500 Subject: [PATCH 04/21] Answering unknown ONVIF actions with SOAP faults, reporting real response lengths --- res/onvif/fault.xml | 16 +++++++++ src/onvif.c | 87 ++++++++++++++++++--------------------------- src/onvif.h | 4 +++ src/server.c | 13 ++++--- 4 files changed, 63 insertions(+), 57 deletions(-) create mode 100644 res/onvif/fault.xml diff --git a/res/onvif/fault.xml b/res/onvif/fault.xml new file mode 100644 index 00000000..e90e3a53 --- /dev/null +++ b/res/onvif/fault.xml @@ -0,0 +1,16 @@ + + + + + + SOAP-ENV:%s + + %s + + + + %s + + + + diff --git a/src/onvif.c b/src/onvif.c index 7896a6fb..1b106a9f 100644 --- a/src/onvif.c +++ b/src/onvif.c @@ -4,6 +4,8 @@ IMPORT_STR(.rodata, "../res/onvif/capabilities.xml", capabilitiesxml); extern const char capabilitiesxml[]; IMPORT_STR(.rodata, "../res/onvif/deviceinfo.xml", deviceinfoxml); extern const char deviceinfoxml[]; +IMPORT_STR(.rodata, "../res/onvif/fault.xml", faultxml); +extern const char faultxml[]; IMPORT_STR(.rodata, "../res/onvif/discovery.xml", discoveryxml); extern const char discoveryxml[]; IMPORT_STR(.rodata, "../res/onvif/mediaprofile.xml", mediaprofilexml); @@ -19,10 +21,31 @@ extern const char systemtimexml[]; IMPORT_STR(.rodata, "../res/onvif/videosources.xml", videosourcesxml); extern const char videosourcesxml[]; -const char onvifgood[] = "HTTP/1.1 200 OK\r\n" \ - "Content-Type: application/soap+xml; charset=utf-8\r\n" \ - "Connection: close\r\n" \ - "\r\n"; +static void onvif_reply(char *response, int *respLen, const char *status, const char *xml, ...) { + int maxLen = *respLen; + int headerLen = snprintf(response, maxLen, + "HTTP/1.1 %s\r\n" + "Content-Type: application/soap+xml; charset=utf-8\r\n" + "Connection: close\r\n" + "\r\n", status); + + va_list args; + va_start(args, xml); + int bodyLen = vsnprintf(response + headerLen, maxLen - headerLen, xml, args); + va_end(args); + + if (bodyLen >= maxLen - headerLen) { + HAL_WARNING("onvif", "Response truncated to %d bytes!\n", maxLen - 1); + bodyLen = maxLen - headerLen - 1; + } + *respLen = headerLen + bodyLen; +} + +void onvif_respond_fault(char *response, int *respLen, bool sender, + const char *subcode, const char *reason) { + onvif_reply(response, respLen, sender ? "400 Bad Request" : "500 Internal Server Error", + faultxml, sender ? "Sender" : "Receiver", subcode, reason); +} extern NetInfo netinfo; pthread_t onvifPid = 0; @@ -183,13 +206,7 @@ bool onvif_validate_soap_auth(const char *soap_data) { void onvif_respond_capabilities(char *response, int *respLen) { if (!response || !respLen) return; - int maxLen = *respLen; - int headerLen = strlen(onvifgood); - memcpy(response, onvifgood, headerLen); - *respLen = headerLen; - - *respLen += snprintf(response + headerLen, maxLen - headerLen, - capabilitiesxml, + onvif_reply(response, respLen, "200 OK", capabilitiesxml, netinfo.ipaddr[0], app_config.web_port, // Analytics netinfo.ipaddr[0], app_config.web_port, // Device netinfo.ipaddr[0], app_config.web_port, // Events @@ -201,13 +218,7 @@ void onvif_respond_capabilities(char *response, int *respLen) { void onvif_respond_deviceinfo(char *response, int *respLen) { if (!response || !respLen) return; - int maxLen = *respLen; - int headerLen = strlen(onvifgood); - memcpy(response, onvifgood, headerLen); - *respLen = headerLen; - - *respLen += snprintf(response + headerLen, maxLen - headerLen, - deviceinfoxml, + onvif_reply(response, respLen, "200 OK", deviceinfoxml, "OpenIPC", "IP Camera", "1.0", "To be replaced", chip); } @@ -241,13 +252,7 @@ void onvif_respond_mediaprofiles(char *response, int *respLen) { profileCnt++; } - int maxLen = *respLen; - int headerLen = strlen(onvifgood); - memcpy(response, onvifgood, headerLen); - *respLen = headerLen; - - *respLen += snprintf(response + headerLen, maxLen - headerLen, - mediaprofilesxml, + onvif_reply(response, respLen, "200 OK", mediaprofilesxml, profile); } @@ -267,13 +272,7 @@ void onvif_respond_snapshot(char *response, int *respLen) { snprintf(snapshot_url, sizeof(snapshot_url), "http://%s:%d/image.jpg", netinfo.ipaddr[0], app_config.web_port); - int maxLen = *respLen; - int headerLen = strlen(onvifgood); - memcpy(response, onvifgood, headerLen); - *respLen = headerLen; - - *respLen += snprintf(response + headerLen, maxLen - headerLen, - snapshotxml, + onvif_reply(response, respLen, "200 OK", snapshotxml, snapshot_url); } @@ -293,13 +292,7 @@ void onvif_respond_stream(char *response, int *respLen) { snprintf(stream_url, sizeof(stream_url), "rtsp://%s:%d/", netinfo.ipaddr[0], app_config.rtsp_port); - int maxLen = *respLen; - int headerLen = strlen(onvifgood); - memcpy(response, onvifgood, headerLen); - *respLen = headerLen; - - *respLen += snprintf(response + headerLen, maxLen - headerLen, - streamxml, + onvif_reply(response, respLen, "200 OK", streamxml, stream_url); } @@ -312,13 +305,7 @@ void onvif_respond_systemtime(char *response, int *respLen) { time(&now); tm_info = gmtime(&now); - int maxLen = *respLen; - int headerLen = strlen(onvifgood); - memcpy(response, onvifgood, headerLen); - *respLen = headerLen; - - *respLen += snprintf(response + headerLen, maxLen - headerLen, - systemtimexml, + onvif_reply(response, respLen, "200 OK", systemtimexml, tm_info->tm_hour, tm_info->tm_min, tm_info->tm_sec, tm_info->tm_year + 1900, tm_info->tm_mon + 1, tm_info->tm_mday); } @@ -333,12 +320,6 @@ void onvif_respond_videosources(char *response, int *respLen) { int framerate = app_config.mp4_enable ? app_config.mp4_fps : app_config.mjpeg_fps; - int maxLen = *respLen; - int headerLen = strlen(onvifgood); - memcpy(response, onvifgood, headerLen); - *respLen = headerLen; - - *respLen += snprintf(response + headerLen, maxLen - headerLen, - videosourcesxml, + onvif_reply(response, respLen, "200 OK", videosourcesxml, framerate, width, height); } \ No newline at end of file diff --git a/src/onvif.h b/src/onvif.h index dfe19bfe..a60afd96 100644 --- a/src/onvif.h +++ b/src/onvif.h @@ -1,6 +1,7 @@ #pragma once #include +#include #include #include #include @@ -18,6 +19,9 @@ char* onvif_extract_soap_action(const char* soap_data); bool onvif_is_preauth_action(const char *action); bool onvif_validate_soap_auth(const char *soap_data); +void onvif_respond_fault(char *response, int *respLen, bool sender, + const char *subcode, const char *reason); + void onvif_respond_capabilities(char *response, int *respLen); void onvif_respond_deviceinfo(char *response, int *respLen); void onvif_respond_mediaprofiles(char *response, int *respLen); diff --git a/src/server.c b/src/server.c index c7c636db..1597e315 100644 --- a/src/server.c +++ b/src/server.c @@ -620,7 +620,10 @@ void respond_request(http_request_t *req) { char *action = onvif_extract_soap_action(req->payload); if (!action) { - send_http_error(req->clntFd, 400); + respLen = sizeof(response); + onvif_respond_fault(response, &respLen, true, "ter:InvalidArgVal", + "No action found in the SOAP body"); + send_and_close(req->clntFd, response, respLen); return; } HAL_INFO("onvif", "\x1b[32mAction: %s\x1b[0m\n", action); @@ -672,9 +675,11 @@ void respond_request(http_request_t *req) { } } - if (!EMPTY(action)) - HAL_WARNING("server", "Unknown ONVIF request: %s->%s\n", path, action); - send_http_error(req->clntFd, 501); + HAL_WARNING("server", "Unknown ONVIF request: %s->%s\n", path, action); + respLen = sizeof(response); + onvif_respond_fault(response, &respLen, false, "ter:ActionNotSupported", + "The requested action is not supported"); + send_and_close(req->clntFd, response, respLen); return; } From c379aa76adededde29caca860543b23996b6a4ca Mon Sep 17 00:00:00 2001 From: Jaro Marval Date: Mon, 28 Sep 2026 20:44:05 -0500 Subject: [PATCH 05/21] Adding a PullPoint subscription store for ONVIF events --- src/onvif_event.c | 141 ++++++++++++++++++++++++++++++++++++++++ src/onvif_event.h | 23 +++++++ test/Makefile | 5 +- test/onvif_event_test.c | 85 ++++++++++++++++++++++++ 4 files changed, 253 insertions(+), 1 deletion(-) create mode 100644 src/onvif_event.c create mode 100644 src/onvif_event.h create mode 100644 test/onvif_event_test.c diff --git a/src/onvif_event.c b/src/onvif_event.c new file mode 100644 index 00000000..b1db2bdf --- /dev/null +++ b/src/onvif_event.c @@ -0,0 +1,141 @@ +#include "onvif_event.h" + +#include +#include +#include + +typedef struct { + int id; + time_t expires; + unsigned int generation; + int head, count; + onvif_event_msg queue[ONVIF_EVENT_QUEUE]; +} onvif_event_sub; + +static onvif_event_sub subs[ONVIF_EVENT_MAX_SUBS]; +static pthread_mutex_t eventMtx = PTHREAD_MUTEX_INITIALIZER; +static pthread_cond_t eventCond = PTHREAD_COND_INITIALIZER; +static bool motionState = false; +static int lastId = 0, waits = 0; + +static onvif_event_sub *find_sub(int id) { + for (int i = 0; i < ONVIF_EVENT_MAX_SUBS; i++) + if (id > 0 && subs[i].id == id) return &subs[i]; + return NULL; +} + +static void push_msg(onvif_event_sub *sub, onvif_event_msg msg) { + if (sub->count == ONVIF_EVENT_QUEUE) { + sub->head = (sub->head + 1) % ONVIF_EVENT_QUEUE; + sub->count--; + } + sub->queue[(sub->head + sub->count) % ONVIF_EVENT_QUEUE] = msg; + sub->count++; +} + +int onvif_event_subscribe(time_t now, int seconds, time_t *expires) { + int id = -1; + + pthread_mutex_lock(&eventMtx); + for (int i = 0; i < ONVIF_EVENT_MAX_SUBS; i++) { + if (subs[i].id) continue; + memset(&subs[i], 0, sizeof(subs[i])); + subs[i].id = id = ++lastId; + subs[i].expires = *expires = now + seconds; + push_msg(&subs[i], (onvif_event_msg){ .time = now, .initial = true, .state = motionState }); + break; + } + pthread_mutex_unlock(&eventMtx); + + return id; +} + +bool onvif_event_renew(int id, time_t now, int seconds, time_t *expires) { + pthread_mutex_lock(&eventMtx); + onvif_event_sub *sub = find_sub(id); + if (sub) sub->expires = *expires = now + seconds; + pthread_mutex_unlock(&eventMtx); + + return sub; +} + +bool onvif_event_unsubscribe(int id) { + pthread_mutex_lock(&eventMtx); + onvif_event_sub *sub = find_sub(id); + if (sub) { + sub->id = 0; + pthread_cond_broadcast(&eventCond); + } + pthread_mutex_unlock(&eventMtx); + + return sub; +} + +bool onvif_event_sync(int id, time_t now) { + pthread_mutex_lock(&eventMtx); + onvif_event_sub *sub = find_sub(id); + if (sub) { + push_msg(sub, (onvif_event_msg){ .time = now, .initial = true, .state = motionState }); + pthread_cond_broadcast(&eventCond); + } + pthread_mutex_unlock(&eventMtx); + + return sub; +} + +int onvif_event_pull(int id, int timeout_s, int limit, onvif_event_msg *msgs, time_t *expires) { + struct timespec deadline; + clock_gettime(CLOCK_REALTIME, &deadline); + deadline.tv_sec += timeout_s; + + pthread_mutex_lock(&eventMtx); + onvif_event_sub *sub = find_sub(id); + unsigned int generation = 0; + + // A newer pull on the same subscription releases the one already waiting + if (sub) { + generation = ++sub->generation; + pthread_cond_broadcast(&eventCond); + } + + if (sub && !sub->count && timeout_s > 0 && waits < ONVIF_EVENT_MAX_WAITS) { + waits++; + while ((sub = find_sub(id)) && !sub->count && sub->generation == generation && + pthread_cond_timedwait(&eventCond, &eventMtx, &deadline) != ETIMEDOUT); + waits--; + sub = find_sub(id); + } + + int count = -1; + if (sub) { + for (count = 0; sub->count && count < limit; count++) { + msgs[count] = sub->queue[sub->head]; + sub->head = (sub->head + 1) % ONVIF_EVENT_QUEUE; + sub->count--; + } + *expires = sub->expires; + } + pthread_mutex_unlock(&eventMtx); + + return count; +} + +void onvif_event_expire(time_t now) { + pthread_mutex_lock(&eventMtx); + for (int i = 0; i < ONVIF_EVENT_MAX_SUBS; i++) + if (subs[i].id && subs[i].expires <= now) { + subs[i].id = 0; + pthread_cond_broadcast(&eventCond); + } + pthread_mutex_unlock(&eventMtx); +} + +void onvif_motion_notify(bool state, time_t when) { + pthread_mutex_lock(&eventMtx); + motionState = state; + for (int i = 0; i < ONVIF_EVENT_MAX_SUBS; i++) + if (subs[i].id) + push_msg(&subs[i], (onvif_event_msg){ .time = when, .state = state }); + pthread_cond_broadcast(&eventCond); + pthread_mutex_unlock(&eventMtx); +} diff --git a/src/onvif_event.h b/src/onvif_event.h new file mode 100644 index 00000000..5237fa1c --- /dev/null +++ b/src/onvif_event.h @@ -0,0 +1,23 @@ +#pragma once + +#include +#include + +#define ONVIF_EVENT_MAX_SUBS 4 +#define ONVIF_EVENT_MAX_WAITS 4 +#define ONVIF_EVENT_QUEUE 8 + +typedef struct { + time_t time; + bool initial; + bool state; +} onvif_event_msg; + +int onvif_event_subscribe(time_t now, int seconds, time_t *expires); +bool onvif_event_renew(int id, time_t now, int seconds, time_t *expires); +bool onvif_event_unsubscribe(int id); +bool onvif_event_sync(int id, time_t now); +int onvif_event_pull(int id, int timeout_s, int limit, + onvif_event_msg *msgs, time_t *expires); +void onvif_event_expire(time_t now); +void onvif_motion_notify(bool state, time_t when); diff --git a/test/Makefile b/test/Makefile index 48a0f7d6..37f2695a 100644 --- a/test/Makefile +++ b/test/Makefile @@ -1,6 +1,6 @@ CFLAGS = -std=gnu99 -Wall -Wno-parentheses -I../src \ -DCLOCK_MONOTONIC_COARSE=CLOCK_MONOTONIC -Dmmap64=mmap -TESTS = soap_test +TESTS = soap_test onvif_event_test all: $(TESTS) @for t in $(TESTS); do ./$$t || exit 1; done @@ -11,4 +11,7 @@ clean: soap_test: soap_test.c ../src/soap.c ../src/hal/tools.c $(CC) $(CFLAGS) $^ -o $@ +onvif_event_test: onvif_event_test.c ../src/onvif_event.c + $(CC) $(CFLAGS) $^ -lpthread -o $@ + .PHONY: all clean diff --git a/test/onvif_event_test.c b/test/onvif_event_test.c new file mode 100644 index 00000000..0eb04f04 --- /dev/null +++ b/test/onvif_event_test.c @@ -0,0 +1,85 @@ +#include +#include + +#include "check.h" +#include "onvif_event.h" + +static void *second_pull(void *arg) { + onvif_event_msg msgs[ONVIF_EVENT_QUEUE]; + time_t expires; + + usleep(200000); + onvif_event_pull(*(int *)arg, 0, 8, msgs, &expires); + return NULL; +} + +static void *late_notify(void *arg) { + usleep(200000); + onvif_motion_notify(true, 1010); + return NULL; +} + +int main(void) { + onvif_event_msg msgs[ONVIF_EVENT_QUEUE]; + time_t expires, now = 1000, start; + pthread_t thread; + + int id = onvif_event_subscribe(now, 60, &expires); + CHECK(id > 0 && expires == 1060); + + // The first pull gets the Initialized message with the current state + CHECK(onvif_event_pull(id, 0, 8, msgs, &expires) == 1); + CHECK(msgs[0].initial && !msgs[0].state && msgs[0].time == 1000); + + onvif_motion_notify(true, 1001); + onvif_motion_notify(false, 1002); + CHECK(onvif_event_pull(id, 0, 1, msgs, &expires) == 1); + CHECK(!msgs[0].initial && msgs[0].state && msgs[0].time == 1001); + CHECK(onvif_event_pull(id, 0, 8, msgs, &expires) == 1); + CHECK(!msgs[0].state && msgs[0].time == 1002); + + // A full queue drops the oldest message + for (int i = 0; i < ONVIF_EVENT_QUEUE + 2; i++) + onvif_motion_notify(i % 2 == 0, 2000 + i); + CHECK(onvif_event_pull(id, 0, 8, msgs, &expires) == ONVIF_EVENT_QUEUE); + CHECK(msgs[0].time == 2002 && msgs[ONVIF_EVENT_QUEUE - 1].time == 2009); + + // An empty pull waits for its timeout + start = time(NULL); + CHECK(onvif_event_pull(id, 1, 8, msgs, &expires) == 0); + CHECK(time(NULL) - start >= 1); + + // A notification wakes a waiting pull right away + pthread_create(&thread, NULL, late_notify, NULL); + start = time(NULL); + CHECK(onvif_event_pull(id, 5, 8, msgs, &expires) == 1 && msgs[0].time == 1010); + CHECK(time(NULL) - start < 2); + pthread_join(thread, NULL); + + // A second pull on the same subscription releases the first one empty + pthread_create(&thread, NULL, second_pull, &id); + start = time(NULL); + CHECK(onvif_event_pull(id, 5, 8, msgs, &expires) == 0); + CHECK(time(NULL) - start < 2); + pthread_join(thread, NULL); + + // Renew, sync, unsubscribe + CHECK(onvif_event_renew(id, 1100, 30, &expires) && expires == 1130); + CHECK(onvif_event_sync(id, 1101)); + CHECK(onvif_event_pull(id, 0, 8, msgs, &expires) == 1 && msgs[0].initial && msgs[0].state); + CHECK(onvif_event_unsubscribe(id)); + CHECK(onvif_event_pull(id, 0, 8, msgs, &expires) == -1); + CHECK(!onvif_event_renew(id, 1100, 30, &expires)); + + // At most four subscriptions; expired ones free their slot + int ids[ONVIF_EVENT_MAX_SUBS]; + for (int i = 0; i < ONVIF_EVENT_MAX_SUBS; i++) + CHECK((ids[i] = onvif_event_subscribe(3000, 10 + i, &expires)) > 0); + CHECK(onvif_event_subscribe(3000, 10, &expires) == -1); + onvif_event_expire(3010); + CHECK(onvif_event_pull(ids[0], 0, 8, msgs, &expires) == -1); + CHECK(onvif_event_pull(ids[1], 0, 8, msgs, &expires) == 1); + CHECK(onvif_event_subscribe(3010, 10, &expires) > 0); + + CHECK_DONE(); +} From 1a5b53046fc0c21f1c07315c7fc77cafb330dff3 Mon Sep 17 00:00:00 2001 From: Jaro Marval Date: Mon, 28 Sep 2026 20:47:58 -0500 Subject: [PATCH 06/21] Adding the ONVIF event service responses and GetServices --- res/onvif/capabilities.xml | 2 +- res/onvif/event.xml | 9 ++ res/onvif/eventcaps.xml | 3 + res/onvif/eventprops.xml | 22 +++++ res/onvif/notification.xml | 13 +++ res/onvif/pullmessages.xml | 4 + res/onvif/pullpoint.xml | 7 ++ res/onvif/renew.xml | 4 + res/onvif/services.xml | 22 +++++ res/onvif/syncpoint.xml | 1 + res/onvif/unsubscribe.xml | 1 + src/onvif.c | 181 ++++++++++++++++++++++++++++++++++++- src/onvif.h | 18 +++- 13 files changed, 284 insertions(+), 3 deletions(-) create mode 100644 res/onvif/event.xml create mode 100644 res/onvif/eventcaps.xml create mode 100644 res/onvif/eventprops.xml create mode 100644 res/onvif/notification.xml create mode 100644 res/onvif/pullmessages.xml create mode 100644 res/onvif/pullpoint.xml create mode 100644 res/onvif/renew.xml create mode 100644 res/onvif/services.xml create mode 100644 res/onvif/syncpoint.xml create mode 100644 res/onvif/unsubscribe.xml diff --git a/res/onvif/capabilities.xml b/res/onvif/capabilities.xml index 6601faa3..09e6ada1 100644 --- a/res/onvif/capabilities.xml +++ b/res/onvif/capabilities.xml @@ -49,7 +49,7 @@ http://%s:%d/onvif/event_service false - false + true false diff --git a/res/onvif/event.xml b/res/onvif/event.xml new file mode 100644 index 00000000..0660fc5a --- /dev/null +++ b/res/onvif/event.xml @@ -0,0 +1,9 @@ + + + + %s%s + + +%s + + diff --git a/res/onvif/eventcaps.xml b/res/onvif/eventcaps.xml new file mode 100644 index 00000000..7ac0c6da --- /dev/null +++ b/res/onvif/eventcaps.xml @@ -0,0 +1,3 @@ + + + diff --git a/res/onvif/eventprops.xml b/res/onvif/eventprops.xml new file mode 100644 index 00000000..bd941878 --- /dev/null +++ b/res/onvif/eventprops.xml @@ -0,0 +1,22 @@ + + http://www.onvif.org/onvif/ver10/topics/topicns.xml + true + + + + + + + + + + + + + + + http://www.onvif.org/ver10/tev/topicExpression/ConcreteSet + http://docs.oasis-open.org/wsn/t-1/TopicExpression/Concrete + http://www.onvif.org/ver10/tev/messageContentFilter/ItemFilter + http://www.onvif.org/onvif/ver10/schema/onvif.xsd + diff --git a/res/onvif/notification.xml b/res/onvif/notification.xml new file mode 100644 index 00000000..b6071462 --- /dev/null +++ b/res/onvif/notification.xml @@ -0,0 +1,13 @@ + + tns1:VideoSource/MotionAlarm + + + + + + + + + + + diff --git a/res/onvif/pullmessages.xml b/res/onvif/pullmessages.xml new file mode 100644 index 00000000..e34eb74a --- /dev/null +++ b/res/onvif/pullmessages.xml @@ -0,0 +1,4 @@ + + %s + %s%s + diff --git a/res/onvif/pullpoint.xml b/res/onvif/pullpoint.xml new file mode 100644 index 00000000..ff676450 --- /dev/null +++ b/res/onvif/pullpoint.xml @@ -0,0 +1,7 @@ + + + http://%s:%d/onvif/subscription?id=%d + + %s + %s + diff --git a/res/onvif/renew.xml b/res/onvif/renew.xml new file mode 100644 index 00000000..78eb7036 --- /dev/null +++ b/res/onvif/renew.xml @@ -0,0 +1,4 @@ + + %s + %s + diff --git a/res/onvif/services.xml b/res/onvif/services.xml new file mode 100644 index 00000000..a399b3af --- /dev/null +++ b/res/onvif/services.xml @@ -0,0 +1,22 @@ + + + + + + http://www.onvif.org/ver10/device/wsdl + http://%s:%d/onvif/device_service + 240 + + + http://www.onvif.org/ver10/media/wsdl + http://%s:%d/onvif/media_service + 240 + + + http://www.onvif.org/ver10/events/wsdl + http://%s:%d/onvif/event_service + 240 + + + + diff --git a/res/onvif/syncpoint.xml b/res/onvif/syncpoint.xml new file mode 100644 index 00000000..658e33c6 --- /dev/null +++ b/res/onvif/syncpoint.xml @@ -0,0 +1 @@ + diff --git a/res/onvif/unsubscribe.xml b/res/onvif/unsubscribe.xml new file mode 100644 index 00000000..b298fe9b --- /dev/null +++ b/res/onvif/unsubscribe.xml @@ -0,0 +1 @@ + diff --git a/src/onvif.c b/src/onvif.c index 1b106a9f..0a814d55 100644 --- a/src/onvif.c +++ b/src/onvif.c @@ -20,6 +20,26 @@ IMPORT_STR(.rodata, "../res/onvif/systemtime.xml", systemtimexml); extern const char systemtimexml[]; IMPORT_STR(.rodata, "../res/onvif/videosources.xml", videosourcesxml); extern const char videosourcesxml[]; +IMPORT_STR(.rodata, "../res/onvif/event.xml", eventxml); +extern const char eventxml[]; +IMPORT_STR(.rodata, "../res/onvif/eventcaps.xml", eventcapsxml); +extern const char eventcapsxml[]; +IMPORT_STR(.rodata, "../res/onvif/eventprops.xml", eventpropsxml); +extern const char eventpropsxml[]; +IMPORT_STR(.rodata, "../res/onvif/notification.xml", notificationxml); +extern const char notificationxml[]; +IMPORT_STR(.rodata, "../res/onvif/pullmessages.xml", pullmessagesxml); +extern const char pullmessagesxml[]; +IMPORT_STR(.rodata, "../res/onvif/pullpoint.xml", pullpointxml); +extern const char pullpointxml[]; +IMPORT_STR(.rodata, "../res/onvif/renew.xml", renewxml); +extern const char renewxml[]; +IMPORT_STR(.rodata, "../res/onvif/services.xml", servicesxml); +extern const char servicesxml[]; +IMPORT_STR(.rodata, "../res/onvif/syncpoint.xml", syncpointxml); +extern const char syncpointxml[]; +IMPORT_STR(.rodata, "../res/onvif/unsubscribe.xml", unsubscribexml); +extern const char unsubscribexml[]; static void onvif_reply(char *response, int *respLen, const char *status, const char *xml, ...) { int maxLen = *respLen; @@ -322,4 +342,163 @@ void onvif_respond_videosources(char *response, int *respLen) { onvif_reply(response, respLen, "200 OK", videosourcesxml, framerate, width, height); -} \ No newline at end of file +} + +#define ONVIF_EVENTS_WSDL "http://www.onvif.org/ver10/events/wsdl/" +#define ONVIF_WSN_BW2 "http://docs.oasis-open.org/wsn/bw-2/" +#define ONVIF_SOURCE_TOKEN "VideoSource_1" +#define ONVIF_SUB_DEFAULT_S 3600 +#define ONVIF_SUB_MAX_S 86400 +#define ONVIF_PULL_MAX_S 60 + +static void onvif_message_id(const char *request, char *messageId, size_t size) { + if (!soap_tag_text(request, "MessageID", messageId, size)) + *messageId = '\0'; +} + +static void onvif_reply_event(char *response, int *respLen, const char *messageId, + const char *action, const char *body) { + char relates[160] = ""; + + if (*messageId) + snprintf(relates, sizeof(relates), "\n %s", messageId); + onvif_reply(response, respLen, "200 OK", eventxml, action, relates, body); +} + +static void onvif_respond_unknown_sub(char *response, int *respLen) { + onvif_respond_fault(response, respLen, true, "wsrf-rw:ResourceUnknownFault", + "Unknown or expired subscription"); +} + +void onvif_respond_services(char *response, int *respLen) { + onvif_reply(response, respLen, "200 OK", servicesxml, + netinfo.ipaddr[0], app_config.web_port, + netinfo.ipaddr[0], app_config.web_port, + netinfo.ipaddr[0], app_config.web_port); +} + +void onvif_respond_eventcaps(char *response, int *respLen, const char *request) { + char messageId[128]; + + onvif_message_id(request, messageId, sizeof(messageId)); + onvif_reply_event(response, respLen, messageId, + ONVIF_EVENTS_WSDL "EventPortType/GetServiceCapabilitiesResponse", eventcapsxml); +} + +void onvif_respond_eventprops(char *response, int *respLen, const char *request) { + char messageId[128]; + + onvif_message_id(request, messageId, sizeof(messageId)); + onvif_reply_event(response, respLen, messageId, + ONVIF_EVENTS_WSDL "EventPortType/GetEventPropertiesResponse", eventpropsxml); +} + +void onvif_respond_pullpoint(char *response, int *respLen, const char *request) { + char body[1024], messageId[128], current[32], termination[32]; + time_t now = time(NULL), expires; + int seconds = soap_term_seconds(request, "InitialTerminationTime", now, + ONVIF_SUB_DEFAULT_S, ONVIF_SUB_MAX_S); + + int id = onvif_event_subscribe(now, seconds, &expires); + if (id < 0) { + onvif_respond_fault(response, respLen, false, "ter:OutofMemory", + "Too many subscriptions"); + return; + } + HAL_INFO("onvif", "Subscription #%d created for %d seconds\n", id, seconds); + + soap_datetime_format(now, current, sizeof(current)); + soap_datetime_format(expires, termination, sizeof(termination)); + snprintf(body, sizeof(body), pullpointxml, + netinfo.ipaddr[0], app_config.web_port, id, current, termination); + onvif_message_id(request, messageId, sizeof(messageId)); + onvif_reply_event(response, respLen, messageId, + ONVIF_EVENTS_WSDL "EventPortType/CreatePullPointSubscriptionResponse", body); +} + +void onvif_pull_parse(const char *request, int id, onvif_pull_req *pull) { + char text[32]; + + pull->id = id; + pull->timeout = soap_tag_text(request, "Timeout", text, sizeof(text)) ? + soap_duration(text) : 0; + if (pull->timeout < 0) pull->timeout = 0; + if (pull->timeout > ONVIF_PULL_MAX_S) pull->timeout = ONVIF_PULL_MAX_S; + pull->limit = soap_tag_text(request, "MessageLimit", text, sizeof(text)) ? + atoi(text) : ONVIF_EVENT_QUEUE; + if (pull->limit < 1) pull->limit = 1; + if (pull->limit > ONVIF_EVENT_QUEUE) pull->limit = ONVIF_EVENT_QUEUE; + onvif_message_id(request, pull->messageId, sizeof(pull->messageId)); +} + +void onvif_respond_pullmessages(char *response, int *respLen, const onvif_pull_req *pull) { + onvif_event_msg msgs[ONVIF_EVENT_QUEUE]; + char body[6656], notes[5632] = "", current[32], termination[32], when[32]; + time_t expires; + int notesLen = 0; + + int count = onvif_event_pull(pull->id, pull->timeout, pull->limit, msgs, &expires); + if (count < 0) { + onvif_respond_unknown_sub(response, respLen); + return; + } + + for (int i = 0; i < count && notesLen < sizeof(notes); i++) { + soap_datetime_format(msgs[i].time, when, sizeof(when)); + notesLen += snprintf(notes + notesLen, sizeof(notes) - notesLen, notificationxml, + when, msgs[i].initial ? "Initialized" : "Changed", ONVIF_SOURCE_TOKEN, + msgs[i].state ? "true" : "false"); + } + + soap_datetime_format(time(NULL), current, sizeof(current)); + soap_datetime_format(expires, termination, sizeof(termination)); + snprintf(body, sizeof(body), pullmessagesxml, current, termination, notes); + onvif_reply_event(response, respLen, pull->messageId, + ONVIF_EVENTS_WSDL "PullPointSubscription/PullMessagesResponse", body); +} + +void onvif_respond_renew(char *response, int *respLen, int id, const char *request) { + char body[512], messageId[128], current[32], termination[32]; + time_t now = time(NULL), expires; + int seconds = soap_term_seconds(request, "TerminationTime", now, + ONVIF_SUB_DEFAULT_S, ONVIF_SUB_MAX_S); + + if (!onvif_event_renew(id, now, seconds, &expires)) { + onvif_respond_unknown_sub(response, respLen); + return; + } + + soap_datetime_format(now, current, sizeof(current)); + soap_datetime_format(expires, termination, sizeof(termination)); + snprintf(body, sizeof(body), renewxml, termination, current); + onvif_message_id(request, messageId, sizeof(messageId)); + onvif_reply_event(response, respLen, messageId, + ONVIF_WSN_BW2 "SubscriptionManager/RenewResponse", body); +} + +void onvif_respond_unsubscribe(char *response, int *respLen, int id, const char *request) { + char messageId[128]; + + if (!onvif_event_unsubscribe(id)) { + onvif_respond_unknown_sub(response, respLen); + return; + } + HAL_INFO("onvif", "Subscription #%d removed\n", id); + + onvif_message_id(request, messageId, sizeof(messageId)); + onvif_reply_event(response, respLen, messageId, + ONVIF_WSN_BW2 "SubscriptionManager/UnsubscribeResponse", unsubscribexml); +} + +void onvif_respond_syncpoint(char *response, int *respLen, int id, const char *request) { + char messageId[128]; + + if (!onvif_event_sync(id, time(NULL))) { + onvif_respond_unknown_sub(response, respLen); + return; + } + + onvif_message_id(request, messageId, sizeof(messageId)); + onvif_reply_event(response, respLen, messageId, + ONVIF_EVENTS_WSDL "PullPointSubscription/SetSynchronizationPointResponse", syncpointxml); +} diff --git a/src/onvif.h b/src/onvif.h index a60afd96..48c6925d 100644 --- a/src/onvif.h +++ b/src/onvif.h @@ -8,6 +8,7 @@ #include "app_config.h" #include "network.h" +#include "onvif_event.h" #include "soap.h" int start_onvif(void); @@ -28,4 +29,19 @@ void onvif_respond_mediaprofiles(char *response, int *respLen); void onvif_respond_snapshot(char *response, int *respLen); void onvif_respond_stream(char *response, int *respLen); void onvif_respond_systemtime(char *response, int *respLen); -void onvif_respond_videosources(char *response, int *respLen); \ No newline at end of file +void onvif_respond_videosources(char *response, int *respLen); + +typedef struct { + int id, timeout, limit; + char messageId[128]; +} onvif_pull_req; + +void onvif_respond_services(char *response, int *respLen); +void onvif_respond_eventcaps(char *response, int *respLen, const char *request); +void onvif_respond_eventprops(char *response, int *respLen, const char *request); +void onvif_respond_pullpoint(char *response, int *respLen, const char *request); +void onvif_pull_parse(const char *request, int id, onvif_pull_req *pull); +void onvif_respond_pullmessages(char *response, int *respLen, const onvif_pull_req *pull); +void onvif_respond_renew(char *response, int *respLen, int id, const char *request); +void onvif_respond_unsubscribe(char *response, int *respLen, int id, const char *request); +void onvif_respond_syncpoint(char *response, int *respLen, int id, const char *request); From faa824515ff5de5e09b5588d2e042e353897e6e2 Mon Sep 17 00:00:00 2001 From: Jaro Marval Date: Mon, 28 Sep 2026 20:47:58 -0500 Subject: [PATCH 07/21] Serving ONVIF PullPoint subscriptions with long polling --- src/main.c | 2 ++ src/server.c | 73 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 75 insertions(+) diff --git a/src/main.c b/src/main.c index 6c027acf..1e367aba 100644 --- a/src/main.c +++ b/src/main.c @@ -4,6 +4,7 @@ #include "media.h" #include "network.h" #include "night.h" +#include "onvif_event.h" #include "rtsp/rtsp_server.h" #include "server.h" #include "watchdog.h" @@ -105,6 +106,7 @@ int main(int argc, char *argv[]) { while (keepRunning) { if (app_config.rtsp_enable) rtsp_tick(rtspHandle); + if (app_config.onvif_enable) onvif_event_expire(time(NULL)); watchdog_reset(); sleep(1); } diff --git a/src/server.c b/src/server.c index 1597e315..4d79d99d 100644 --- a/src/server.c +++ b/src/server.c @@ -453,6 +453,42 @@ void *send_jpeg_thread(void *vargp) { return NULL; } +struct pulltask { + int client_fd; + onvif_pull_req pull; +}; + +void *send_pullmessages_thread(void *vargp) { + struct pulltask *task = (struct pulltask *)vargp; + char response[8192]; + int respLen = sizeof(response); + + onvif_respond_pullmessages(response, &respLen, &task->pull); + send_and_close(task->client_fd, response, respLen); + free(task); + return NULL; +} + +static void start_pullmessages(int client_fd, int id, const char *payload) { + struct pulltask *task = malloc(sizeof(struct pulltask)); + task->client_fd = client_fd; + onvif_pull_parse(payload, id, &task->pull); + + pthread_t thread_id; + pthread_attr_t thread_attr; + pthread_attr_init(&thread_attr); + size_t new_stacksize = 32 * 1024; + if (pthread_attr_setstacksize(&thread_attr, new_stacksize)) + HAL_DANGER("onvif", "Can't set stack size %zu\n", new_stacksize); + if (pthread_create(&thread_id, &thread_attr, send_pullmessages_thread, (void *)task)) { + HAL_DANGER("onvif", "Can't create thread\n"); + send_http_error(client_fd, 500); + free(task); + } else + pthread_detach(thread_id); + pthread_attr_destroy(&thread_attr); +} + int send_file(const int client_fd, const char *path) { if (!access(path, F_OK)) { const char *mime = (path); @@ -654,6 +690,10 @@ void respond_request(http_request_t *req) { onvif_respond_systemtime((char*)response, &respLen); send_and_close(req->clntFd, response, respLen); return; + } else if (EQUALS(action, "GetServices")) { + onvif_respond_services((char*)response, &respLen); + send_and_close(req->clntFd, response, respLen); + return; } } else if (EQUALS(path, "media_service")) { if (EQUALS(action, "GetProfiles")) { @@ -673,6 +713,39 @@ void respond_request(http_request_t *req) { send_and_close(req->clntFd, response, respLen); return; } + } else if (EQUALS(path, "event_service")) { + if (EQUALS(action, "GetServiceCapabilities")) { + onvif_respond_eventcaps((char*)response, &respLen, req->payload); + send_and_close(req->clntFd, response, respLen); + return; + } else if (EQUALS(action, "GetEventProperties")) { + onvif_respond_eventprops((char*)response, &respLen, req->payload); + send_and_close(req->clntFd, response, respLen); + return; + } else if (EQUALS(action, "CreatePullPointSubscription")) { + onvif_respond_pullpoint((char*)response, &respLen, req->payload); + send_and_close(req->clntFd, response, respLen); + return; + } + } else if (EQUALS(path, "subscription")) { + int id = req->query && STARTS_WITH(req->query, "id=") ? + atoi(req->query + 3) : 0; + if (EQUALS(action, "PullMessages")) { + start_pullmessages(req->clntFd, id, req->payload); + return; + } else if (EQUALS(action, "Renew")) { + onvif_respond_renew((char*)response, &respLen, id, req->payload); + send_and_close(req->clntFd, response, respLen); + return; + } else if (EQUALS(action, "Unsubscribe")) { + onvif_respond_unsubscribe((char*)response, &respLen, id, req->payload); + send_and_close(req->clntFd, response, respLen); + return; + } else if (EQUALS(action, "SetSynchronizationPoint")) { + onvif_respond_syncpoint((char*)response, &respLen, id, req->payload); + send_and_close(req->clntFd, response, respLen); + return; + } } HAL_WARNING("server", "Unknown ONVIF request: %s->%s\n", path, action); From 892be4a2ef87f269bff7806a4a8e0fa3c7cb0d9a Mon Sep 17 00:00:00 2001 From: Jaro Marval Date: Mon, 28 Sep 2026 21:10:37 -0500 Subject: [PATCH 08/21] Replacing the longest idle ONVIF subscription when all slots are taken --- src/onvif.c | 8 ++------ src/onvif_event.c | 32 ++++++++++++++++++++------------ src/onvif_event.h | 2 +- test/onvif_event_test.c | 39 ++++++++++++++++++++++++--------------- 4 files changed, 47 insertions(+), 34 deletions(-) diff --git a/src/onvif.c b/src/onvif.c index 0a814d55..3cf966bb 100644 --- a/src/onvif.c +++ b/src/onvif.c @@ -400,11 +400,6 @@ void onvif_respond_pullpoint(char *response, int *respLen, const char *request) ONVIF_SUB_DEFAULT_S, ONVIF_SUB_MAX_S); int id = onvif_event_subscribe(now, seconds, &expires); - if (id < 0) { - onvif_respond_fault(response, respLen, false, "ter:OutofMemory", - "Too many subscriptions"); - return; - } HAL_INFO("onvif", "Subscription #%d created for %d seconds\n", id, seconds); soap_datetime_format(now, current, sizeof(current)); @@ -437,7 +432,8 @@ void onvif_respond_pullmessages(char *response, int *respLen, const onvif_pull_r time_t expires; int notesLen = 0; - int count = onvif_event_pull(pull->id, pull->timeout, pull->limit, msgs, &expires); + int count = onvif_event_pull(pull->id, time(NULL), pull->timeout, pull->limit, + msgs, &expires); if (count < 0) { onvif_respond_unknown_sub(response, respLen); return; diff --git a/src/onvif_event.c b/src/onvif_event.c index b1db2bdf..28a41416 100644 --- a/src/onvif_event.c +++ b/src/onvif_event.c @@ -6,7 +6,7 @@ typedef struct { int id; - time_t expires; + time_t expires, lastSeen; unsigned int generation; int head, count; onvif_event_msg queue[ONVIF_EVENT_QUEUE]; @@ -34,17 +34,20 @@ static void push_msg(onvif_event_sub *sub, onvif_event_msg msg) { } int onvif_event_subscribe(time_t now, int seconds, time_t *expires) { - int id = -1; + onvif_event_sub *sub = &subs[0]; + // A free slot, or else the subscription idle for the longest time: + // clients that reconnect rarely unsubscribe first pthread_mutex_lock(&eventMtx); - for (int i = 0; i < ONVIF_EVENT_MAX_SUBS; i++) { - if (subs[i].id) continue; - memset(&subs[i], 0, sizeof(subs[i])); - subs[i].id = id = ++lastId; - subs[i].expires = *expires = now + seconds; - push_msg(&subs[i], (onvif_event_msg){ .time = now, .initial = true, .state = motionState }); - break; - } + for (int i = 0; i < ONVIF_EVENT_MAX_SUBS && sub->id; i++) + if (!subs[i].id || subs[i].lastSeen < sub->lastSeen) sub = &subs[i]; + if (sub->id) pthread_cond_broadcast(&eventCond); + + memset(sub, 0, sizeof(*sub)); + int id = sub->id = ++lastId; + sub->expires = *expires = now + seconds; + sub->lastSeen = now; + push_msg(sub, (onvif_event_msg){ .time = now, .initial = true, .state = motionState }); pthread_mutex_unlock(&eventMtx); return id; @@ -53,7 +56,10 @@ int onvif_event_subscribe(time_t now, int seconds, time_t *expires) { bool onvif_event_renew(int id, time_t now, int seconds, time_t *expires) { pthread_mutex_lock(&eventMtx); onvif_event_sub *sub = find_sub(id); - if (sub) sub->expires = *expires = now + seconds; + if (sub) { + sub->expires = *expires = now + seconds; + sub->lastSeen = now; + } pthread_mutex_unlock(&eventMtx); return sub; @@ -83,7 +89,8 @@ bool onvif_event_sync(int id, time_t now) { return sub; } -int onvif_event_pull(int id, int timeout_s, int limit, onvif_event_msg *msgs, time_t *expires) { +int onvif_event_pull(int id, time_t now, int timeout_s, int limit, + onvif_event_msg *msgs, time_t *expires) { struct timespec deadline; clock_gettime(CLOCK_REALTIME, &deadline); deadline.tv_sec += timeout_s; @@ -94,6 +101,7 @@ int onvif_event_pull(int id, int timeout_s, int limit, onvif_event_msg *msgs, ti // A newer pull on the same subscription releases the one already waiting if (sub) { + sub->lastSeen = now; generation = ++sub->generation; pthread_cond_broadcast(&eventCond); } diff --git a/src/onvif_event.h b/src/onvif_event.h index 5237fa1c..70d33f9b 100644 --- a/src/onvif_event.h +++ b/src/onvif_event.h @@ -17,7 +17,7 @@ int onvif_event_subscribe(time_t now, int seconds, time_t *expires); bool onvif_event_renew(int id, time_t now, int seconds, time_t *expires); bool onvif_event_unsubscribe(int id); bool onvif_event_sync(int id, time_t now); -int onvif_event_pull(int id, int timeout_s, int limit, +int onvif_event_pull(int id, time_t now, int timeout_s, int limit, onvif_event_msg *msgs, time_t *expires); void onvif_event_expire(time_t now); void onvif_motion_notify(bool state, time_t when); diff --git a/test/onvif_event_test.c b/test/onvif_event_test.c index 0eb04f04..bd97b7ba 100644 --- a/test/onvif_event_test.c +++ b/test/onvif_event_test.c @@ -9,7 +9,7 @@ static void *second_pull(void *arg) { time_t expires; usleep(200000); - onvif_event_pull(*(int *)arg, 0, 8, msgs, &expires); + onvif_event_pull(*(int *)arg, 1000, 0, 8, msgs, &expires); return NULL; } @@ -28,58 +28,67 @@ int main(void) { CHECK(id > 0 && expires == 1060); // The first pull gets the Initialized message with the current state - CHECK(onvif_event_pull(id, 0, 8, msgs, &expires) == 1); + CHECK(onvif_event_pull(id, 1000, 0, 8, msgs, &expires) == 1); CHECK(msgs[0].initial && !msgs[0].state && msgs[0].time == 1000); onvif_motion_notify(true, 1001); onvif_motion_notify(false, 1002); - CHECK(onvif_event_pull(id, 0, 1, msgs, &expires) == 1); + CHECK(onvif_event_pull(id, 1000, 0, 1, msgs, &expires) == 1); CHECK(!msgs[0].initial && msgs[0].state && msgs[0].time == 1001); - CHECK(onvif_event_pull(id, 0, 8, msgs, &expires) == 1); + CHECK(onvif_event_pull(id, 1000, 0, 8, msgs, &expires) == 1); CHECK(!msgs[0].state && msgs[0].time == 1002); // A full queue drops the oldest message for (int i = 0; i < ONVIF_EVENT_QUEUE + 2; i++) onvif_motion_notify(i % 2 == 0, 2000 + i); - CHECK(onvif_event_pull(id, 0, 8, msgs, &expires) == ONVIF_EVENT_QUEUE); + CHECK(onvif_event_pull(id, 1000, 0, 8, msgs, &expires) == ONVIF_EVENT_QUEUE); CHECK(msgs[0].time == 2002 && msgs[ONVIF_EVENT_QUEUE - 1].time == 2009); // An empty pull waits for its timeout start = time(NULL); - CHECK(onvif_event_pull(id, 1, 8, msgs, &expires) == 0); + CHECK(onvif_event_pull(id, 1000, 1, 8, msgs, &expires) == 0); CHECK(time(NULL) - start >= 1); // A notification wakes a waiting pull right away pthread_create(&thread, NULL, late_notify, NULL); start = time(NULL); - CHECK(onvif_event_pull(id, 5, 8, msgs, &expires) == 1 && msgs[0].time == 1010); + CHECK(onvif_event_pull(id, 1000, 5, 8, msgs, &expires) == 1 && msgs[0].time == 1010); CHECK(time(NULL) - start < 2); pthread_join(thread, NULL); // A second pull on the same subscription releases the first one empty pthread_create(&thread, NULL, second_pull, &id); start = time(NULL); - CHECK(onvif_event_pull(id, 5, 8, msgs, &expires) == 0); + CHECK(onvif_event_pull(id, 1000, 5, 8, msgs, &expires) == 0); CHECK(time(NULL) - start < 2); pthread_join(thread, NULL); // Renew, sync, unsubscribe CHECK(onvif_event_renew(id, 1100, 30, &expires) && expires == 1130); CHECK(onvif_event_sync(id, 1101)); - CHECK(onvif_event_pull(id, 0, 8, msgs, &expires) == 1 && msgs[0].initial && msgs[0].state); + CHECK(onvif_event_pull(id, 1000, 0, 8, msgs, &expires) == 1 && msgs[0].initial && msgs[0].state); CHECK(onvif_event_unsubscribe(id)); - CHECK(onvif_event_pull(id, 0, 8, msgs, &expires) == -1); + CHECK(onvif_event_pull(id, 1000, 0, 8, msgs, &expires) == -1); CHECK(!onvif_event_renew(id, 1100, 30, &expires)); - // At most four subscriptions; expired ones free their slot + // Expired subscriptions free their slot int ids[ONVIF_EVENT_MAX_SUBS]; for (int i = 0; i < ONVIF_EVENT_MAX_SUBS; i++) CHECK((ids[i] = onvif_event_subscribe(3000, 10 + i, &expires)) > 0); - CHECK(onvif_event_subscribe(3000, 10, &expires) == -1); onvif_event_expire(3010); - CHECK(onvif_event_pull(ids[0], 0, 8, msgs, &expires) == -1); - CHECK(onvif_event_pull(ids[1], 0, 8, msgs, &expires) == 1); - CHECK(onvif_event_subscribe(3010, 10, &expires) > 0); + CHECK(onvif_event_pull(ids[0], 3010, 0, 8, msgs, &expires) == -1); + CHECK(onvif_event_pull(ids[1], 3010, 0, 8, msgs, &expires) == 1); + + // When full, a new subscription replaces the one idle for the longest time + CHECK((ids[0] = onvif_event_subscribe(3010, 60, &expires)) > 0); + CHECK(onvif_event_pull(ids[2], 3011, 0, 8, msgs, &expires) == 1); + CHECK(onvif_event_pull(ids[3], 3012, 0, 8, msgs, &expires) == 1); + CHECK(onvif_event_pull(ids[0], 3013, 0, 8, msgs, &expires) == 1); + int newest = onvif_event_subscribe(3014, 60, &expires); + CHECK(newest > 0); + CHECK(onvif_event_pull(ids[1], 3014, 0, 8, msgs, &expires) == -1); + CHECK(onvif_event_pull(ids[2], 3014, 0, 8, msgs, &expires) == 0); + CHECK(onvif_event_pull(newest, 3014, 0, 8, msgs, &expires) == 1); CHECK_DONE(); } From 6c98581be320eecc2bb803c016240b95ac8d48a1 Mon Sep 17 00:00:00 2001 From: Jaro Marval Date: Tue, 29 Sep 2026 08:35:21 -0500 Subject: [PATCH 09/21] Timing ONVIF subscriptions on a monotonic clock --- src/main.c | 2 +- src/onvif.c | 15 ++++++++------- src/onvif_event.c | 10 ++++++++-- src/onvif_event.h | 5 ++++- test/onvif_event_test.c | 8 ++++---- 5 files changed, 25 insertions(+), 15 deletions(-) diff --git a/src/main.c b/src/main.c index 1e367aba..5b9b72db 100644 --- a/src/main.c +++ b/src/main.c @@ -106,7 +106,7 @@ int main(int argc, char *argv[]) { while (keepRunning) { if (app_config.rtsp_enable) rtsp_tick(rtspHandle); - if (app_config.onvif_enable) onvif_event_expire(time(NULL)); + if (app_config.onvif_enable) onvif_event_expire(onvif_event_clock()); watchdog_reset(); sleep(1); } diff --git a/src/onvif.c b/src/onvif.c index 3cf966bb..44617a76 100644 --- a/src/onvif.c +++ b/src/onvif.c @@ -399,11 +399,11 @@ void onvif_respond_pullpoint(char *response, int *respLen, const char *request) int seconds = soap_term_seconds(request, "InitialTerminationTime", now, ONVIF_SUB_DEFAULT_S, ONVIF_SUB_MAX_S); - int id = onvif_event_subscribe(now, seconds, &expires); + int id = onvif_event_subscribe(onvif_event_clock(), now, seconds, &expires); HAL_INFO("onvif", "Subscription #%d created for %d seconds\n", id, seconds); soap_datetime_format(now, current, sizeof(current)); - soap_datetime_format(expires, termination, sizeof(termination)); + soap_datetime_format(now + seconds, termination, sizeof(termination)); snprintf(body, sizeof(body), pullpointxml, netinfo.ipaddr[0], app_config.web_port, id, current, termination); onvif_message_id(request, messageId, sizeof(messageId)); @@ -432,7 +432,7 @@ void onvif_respond_pullmessages(char *response, int *respLen, const onvif_pull_r time_t expires; int notesLen = 0; - int count = onvif_event_pull(pull->id, time(NULL), pull->timeout, pull->limit, + int count = onvif_event_pull(pull->id, onvif_event_clock(), pull->timeout, pull->limit, msgs, &expires); if (count < 0) { onvif_respond_unknown_sub(response, respLen); @@ -446,8 +446,9 @@ void onvif_respond_pullmessages(char *response, int *respLen, const onvif_pull_r msgs[i].state ? "true" : "false"); } - soap_datetime_format(time(NULL), current, sizeof(current)); - soap_datetime_format(expires, termination, sizeof(termination)); + time_t now = time(NULL); + soap_datetime_format(now, current, sizeof(current)); + soap_datetime_format(now + (expires - onvif_event_clock()), termination, sizeof(termination)); snprintf(body, sizeof(body), pullmessagesxml, current, termination, notes); onvif_reply_event(response, respLen, pull->messageId, ONVIF_EVENTS_WSDL "PullPointSubscription/PullMessagesResponse", body); @@ -459,13 +460,13 @@ void onvif_respond_renew(char *response, int *respLen, int id, const char *reque int seconds = soap_term_seconds(request, "TerminationTime", now, ONVIF_SUB_DEFAULT_S, ONVIF_SUB_MAX_S); - if (!onvif_event_renew(id, now, seconds, &expires)) { + if (!onvif_event_renew(id, onvif_event_clock(), seconds, &expires)) { onvif_respond_unknown_sub(response, respLen); return; } soap_datetime_format(now, current, sizeof(current)); - soap_datetime_format(expires, termination, sizeof(termination)); + soap_datetime_format(now + seconds, termination, sizeof(termination)); snprintf(body, sizeof(body), renewxml, termination, current); onvif_message_id(request, messageId, sizeof(messageId)); onvif_reply_event(response, respLen, messageId, diff --git a/src/onvif_event.c b/src/onvif_event.c index 28a41416..8201824d 100644 --- a/src/onvif_event.c +++ b/src/onvif_event.c @@ -18,6 +18,12 @@ static pthread_cond_t eventCond = PTHREAD_COND_INITIALIZER; static bool motionState = false; static int lastId = 0, waits = 0; +time_t onvif_event_clock(void) { + struct timespec now; + clock_gettime(CLOCK_MONOTONIC, &now); + return now.tv_sec; +} + static onvif_event_sub *find_sub(int id) { for (int i = 0; i < ONVIF_EVENT_MAX_SUBS; i++) if (id > 0 && subs[i].id == id) return &subs[i]; @@ -33,7 +39,7 @@ static void push_msg(onvif_event_sub *sub, onvif_event_msg msg) { sub->count++; } -int onvif_event_subscribe(time_t now, int seconds, time_t *expires) { +int onvif_event_subscribe(time_t now, time_t when, int seconds, time_t *expires) { onvif_event_sub *sub = &subs[0]; // A free slot, or else the subscription idle for the longest time: @@ -47,7 +53,7 @@ int onvif_event_subscribe(time_t now, int seconds, time_t *expires) { int id = sub->id = ++lastId; sub->expires = *expires = now + seconds; sub->lastSeen = now; - push_msg(sub, (onvif_event_msg){ .time = now, .initial = true, .state = motionState }); + push_msg(sub, (onvif_event_msg){ .time = when, .initial = true, .state = motionState }); pthread_mutex_unlock(&eventMtx); return id; diff --git a/src/onvif_event.h b/src/onvif_event.h index 70d33f9b..a67f74f2 100644 --- a/src/onvif_event.h +++ b/src/onvif_event.h @@ -13,7 +13,10 @@ typedef struct { bool state; } onvif_event_msg; -int onvif_event_subscribe(time_t now, int seconds, time_t *expires); +// Subscription lifetimes use this monotonic clock, immune to NTP setting the date +time_t onvif_event_clock(void); + +int onvif_event_subscribe(time_t now, time_t when, int seconds, time_t *expires); bool onvif_event_renew(int id, time_t now, int seconds, time_t *expires); bool onvif_event_unsubscribe(int id); bool onvif_event_sync(int id, time_t now); diff --git a/test/onvif_event_test.c b/test/onvif_event_test.c index bd97b7ba..fcfbd1fc 100644 --- a/test/onvif_event_test.c +++ b/test/onvif_event_test.c @@ -24,7 +24,7 @@ int main(void) { time_t expires, now = 1000, start; pthread_t thread; - int id = onvif_event_subscribe(now, 60, &expires); + int id = onvif_event_subscribe(now, now, 60, &expires); CHECK(id > 0 && expires == 1060); // The first pull gets the Initialized message with the current state @@ -74,17 +74,17 @@ int main(void) { // Expired subscriptions free their slot int ids[ONVIF_EVENT_MAX_SUBS]; for (int i = 0; i < ONVIF_EVENT_MAX_SUBS; i++) - CHECK((ids[i] = onvif_event_subscribe(3000, 10 + i, &expires)) > 0); + CHECK((ids[i] = onvif_event_subscribe(3000, 3000, 10 + i, &expires)) > 0); onvif_event_expire(3010); CHECK(onvif_event_pull(ids[0], 3010, 0, 8, msgs, &expires) == -1); CHECK(onvif_event_pull(ids[1], 3010, 0, 8, msgs, &expires) == 1); // When full, a new subscription replaces the one idle for the longest time - CHECK((ids[0] = onvif_event_subscribe(3010, 60, &expires)) > 0); + CHECK((ids[0] = onvif_event_subscribe(3010, 3010, 60, &expires)) > 0); CHECK(onvif_event_pull(ids[2], 3011, 0, 8, msgs, &expires) == 1); CHECK(onvif_event_pull(ids[3], 3012, 0, 8, msgs, &expires) == 1); CHECK(onvif_event_pull(ids[0], 3013, 0, 8, msgs, &expires) == 1); - int newest = onvif_event_subscribe(3014, 60, &expires); + int newest = onvif_event_subscribe(3014, 3014, 60, &expires); CHECK(newest > 0); CHECK(onvif_event_pull(ids[1], 3014, 0, 8, msgs, &expires) == -1); CHECK(onvif_event_pull(ids[2], 3014, 0, 8, msgs, &expires) == 0); From c2c323fc0219a18a5e64053bda6a600ca0ba03e4 Mon Sep 17 00:00:00 2001 From: Jaro Marval Date: Mon, 28 Sep 2026 21:30:27 -0500 Subject: [PATCH 10/21] Hardening the ONVIF SOAP parsing and PullMessages allocation - ISO 8601 durations are parsed in 64 bits - A failed PullMessages allocation is answered with an error - RelatesTo has room for the longest message ID --- src/onvif.c | 2 +- src/server.c | 4 ++++ src/soap.c | 4 ++-- test/soap_test.c | 2 ++ 4 files changed, 9 insertions(+), 3 deletions(-) diff --git a/src/onvif.c b/src/onvif.c index 44617a76..51e077e0 100644 --- a/src/onvif.c +++ b/src/onvif.c @@ -358,7 +358,7 @@ static void onvif_message_id(const char *request, char *messageId, size_t size) static void onvif_reply_event(char *response, int *respLen, const char *messageId, const char *action, const char *body) { - char relates[160] = ""; + char relates[176] = ""; if (*messageId) snprintf(relates, sizeof(relates), "\n %s", messageId); diff --git a/src/server.c b/src/server.c index 4d79d99d..90cd3ac2 100644 --- a/src/server.c +++ b/src/server.c @@ -471,6 +471,10 @@ void *send_pullmessages_thread(void *vargp) { static void start_pullmessages(int client_fd, int id, const char *payload) { struct pulltask *task = malloc(sizeof(struct pulltask)); + if (!task) { + send_http_error(client_fd, 500); + return; + } task->client_fd = client_fd; onvif_pull_parse(payload, id, &task->pull); diff --git a/src/soap.c b/src/soap.c index c0681316..268cb7cc 100644 --- a/src/soap.c +++ b/src/soap.c @@ -72,7 +72,7 @@ bool soap_action(const char *xml, char *action, size_t size) { } int soap_duration(const char *text) { - long total = 0, value; + long long total = 0, value; bool clock = false; char *end; @@ -84,7 +84,7 @@ int soap_duration(const char *text) { text++; continue; } - value = strtol(text, &end, 10); + value = strtoll(text, &end, 10); if (end == text || value < 0) return -1; if (*end == '.') while (isdigit((unsigned char)*++end)); diff --git a/test/soap_test.c b/test/soap_test.c index 4057d839..37f91782 100644 --- a/test/soap_test.c +++ b/test/soap_test.c @@ -37,6 +37,8 @@ int main(void) { CHECK(soap_duration("30") == -1); CHECK(soap_duration("PT5X") == -1); CHECK(soap_duration("P5H") == -1); + CHECK(soap_duration("P30000D") == -1); + CHECK(soap_duration("PT9999999999S") == -1); CHECK(soap_datetime("2026-09-28T10:00:00Z") == 1790589600); CHECK(soap_datetime("2026-09-28T10:00:00.123Z") == 1790589600); From 2cab9c311d710c43bd1682a0465aba8d7b033677 Mon Sep 17 00:00:00 2001 From: Jaro Marval Date: Wed, 30 Sep 2026 16:41:29 -0500 Subject: [PATCH 11/21] Rejecting ONVIF durations that overflow before scaling them --- src/soap.c | 14 ++++++++------ test/soap_test.c | 3 +++ 2 files changed, 11 insertions(+), 6 deletions(-) diff --git a/src/soap.c b/src/soap.c index 268cb7cc..30ae7a5e 100644 --- a/src/soap.c +++ b/src/soap.c @@ -72,7 +72,7 @@ bool soap_action(const char *xml, char *action, size_t size) { } int soap_duration(const char *text) { - long long total = 0, value; + long long total = 0, value, unit; bool clock = false; char *end; @@ -89,13 +89,15 @@ int soap_duration(const char *text) { if (*end == '.') while (isdigit((unsigned char)*++end)); switch (*end) { - case 'D': if (clock) return -1; total += value * 86400; break; - case 'H': if (!clock) return -1; total += value * 3600; break; - case 'M': if (!clock) return -1; total += value * 60; break; - case 'S': if (!clock) return -1; total += value; break; + case 'D': if (clock) return -1; unit = 86400; break; + case 'H': if (!clock) return -1; unit = 3600; break; + case 'M': if (!clock) return -1; unit = 60; break; + case 'S': if (!clock) return -1; unit = 1; break; default: return -1; } - if (total > INT_MAX) return -1; + // Checked before multiplying so a huge value cannot wrap around + if (value > (INT_MAX - total) / unit) return -1; + total += value * unit; text = end + 1; } diff --git a/test/soap_test.c b/test/soap_test.c index 37f91782..6344ab2b 100644 --- a/test/soap_test.c +++ b/test/soap_test.c @@ -39,6 +39,9 @@ int main(void) { CHECK(soap_duration("P5H") == -1); CHECK(soap_duration("P30000D") == -1); CHECK(soap_duration("PT9999999999S") == -1); + // 94368760191893771 * 86400 wraps to 128 in 64 bits + CHECK(soap_duration("P94368760191893771D") == -1); + CHECK(soap_duration("PT99999999999999999999H") == -1); CHECK(soap_datetime("2026-09-28T10:00:00Z") == 1790589600); CHECK(soap_datetime("2026-09-28T10:00:00.123Z") == 1790589600); From eadbde3a6a5fc9d110a2a17aa0c18d0b0c4a61e9 Mon Sep 17 00:00:00 2001 From: Jaro Marval Date: Wed, 30 Sep 2026 16:42:04 -0500 Subject: [PATCH 12/21] Applying time zone offsets to ONVIF termination times --- src/soap.c | 26 +++++++++++++++++++++++--- test/soap_test.c | 7 +++++++ 2 files changed, 30 insertions(+), 3 deletions(-) diff --git a/src/soap.c b/src/soap.c index 30ae7a5e..44bd5cd6 100644 --- a/src/soap.c +++ b/src/soap.c @@ -106,12 +106,32 @@ int soap_duration(const char *text) { time_t soap_datetime(const char *text) { struct tm tm = {0}; + int len, offset = 0, hours, minutes; + + if (sscanf(text, "%4d-%2d-%2dT%2d:%2d:%2d%n", &tm.tm_year, &tm.tm_mon, &tm.tm_mday, + &tm.tm_hour, &tm.tm_min, &tm.tm_sec, &len) != 6) return -1; + text += len; + if (*text == '.') + while (isdigit((unsigned char)*++text)); + + // xs:dateTime ends in Z, a +hh:mm/-hh:mm offset, or nothing (taken as UTC) + if (*text == 'Z') + text++; + else if (*text == '+' || *text == '-') { + for (int i = 1; i <= 5; i++) + if (i == 3 ? text[i] != ':' : !isdigit((unsigned char)text[i])) return -1; + hours = (text[1] - '0') * 10 + text[2] - '0'; + minutes = (text[4] - '0') * 10 + text[5] - '0'; + if (hours > 14 || minutes > 59) return -1; + offset = (hours * 3600 + minutes * 60) * (*text == '-' ? -1 : 1); + text += 6; + } + while (isspace((unsigned char)*text)) text++; + if (*text) return -1; - if (sscanf(text, "%4d-%2d-%2dT%2d:%2d:%2d", &tm.tm_year, &tm.tm_mon, &tm.tm_mday, - &tm.tm_hour, &tm.tm_min, &tm.tm_sec) != 6) return -1; tm.tm_year -= 1900; tm.tm_mon -= 1; - return timegm(&tm); + return timegm(&tm) - offset; } void soap_datetime_format(time_t when, char *text, size_t size) { diff --git a/test/soap_test.c b/test/soap_test.c index 6344ab2b..ad7fa8ff 100644 --- a/test/soap_test.c +++ b/test/soap_test.c @@ -45,6 +45,13 @@ int main(void) { CHECK(soap_datetime("2026-09-28T10:00:00Z") == 1790589600); CHECK(soap_datetime("2026-09-28T10:00:00.123Z") == 1790589600); + CHECK(soap_datetime("2026-09-28T10:00:00") == 1790589600); + CHECK(soap_datetime("2026-09-28T12:00:00+02:00") == 1790589600); + CHECK(soap_datetime("2026-09-28T04:30:00.5-05:30") == 1790589600); + CHECK(soap_datetime("2026-09-28T10:00:00+02") == -1); + CHECK(soap_datetime("2026-09-28T10:00:00+-2:00") == -1); + CHECK(soap_datetime("2026-09-28T10:00:00+02:-5") == -1); + CHECK(soap_datetime("2026-09-28T10:00:00Zjunk") == -1); CHECK(soap_datetime("yesterday") == -1); soap_datetime_format(1790589600, text, sizeof(text)); CHECK(!strcmp(text, "2026-09-28T10:00:00Z")); From 18dfbdd0ac87f121a448c90091b517a0c8ba37dc Mon Sep 17 00:00:00 2001 From: Jaro Marval Date: Wed, 30 Sep 2026 16:42:52 -0500 Subject: [PATCH 13/21] Keeping ONVIF subscriptions with a waiting pull out of replacement --- src/onvif_event.c | 12 ++++++++---- test/onvif_event_test.c | 23 +++++++++++++++++++++++ 2 files changed, 31 insertions(+), 4 deletions(-) diff --git a/src/onvif_event.c b/src/onvif_event.c index 8201824d..a579e5c0 100644 --- a/src/onvif_event.c +++ b/src/onvif_event.c @@ -8,7 +8,7 @@ typedef struct { int id; time_t expires, lastSeen; unsigned int generation; - int head, count; + int head, count, waiting; onvif_event_msg queue[ONVIF_EVENT_QUEUE]; } onvif_event_sub; @@ -43,10 +43,13 @@ int onvif_event_subscribe(time_t now, time_t when, int seconds, time_t *expires) onvif_event_sub *sub = &subs[0]; // A free slot, or else the subscription idle for the longest time: - // clients that reconnect rarely unsubscribe first + // clients that reconnect rarely unsubscribe first, and one with a pull + // waiting is in use however long ago that pull started pthread_mutex_lock(&eventMtx); for (int i = 0; i < ONVIF_EVENT_MAX_SUBS && sub->id; i++) - if (!subs[i].id || subs[i].lastSeen < sub->lastSeen) sub = &subs[i]; + if (!subs[i].id || (!subs[i].waiting && sub->waiting) || + (!subs[i].waiting == !sub->waiting && subs[i].lastSeen < sub->lastSeen)) + sub = &subs[i]; if (sub->id) pthread_cond_broadcast(&eventCond); memset(sub, 0, sizeof(*sub)); @@ -114,10 +117,11 @@ int onvif_event_pull(int id, time_t now, int timeout_s, int limit, if (sub && !sub->count && timeout_s > 0 && waits < ONVIF_EVENT_MAX_WAITS) { waits++; + sub->waiting++; while ((sub = find_sub(id)) && !sub->count && sub->generation == generation && pthread_cond_timedwait(&eventCond, &eventMtx, &deadline) != ETIMEDOUT); waits--; - sub = find_sub(id); + if ((sub = find_sub(id))) sub->waiting--; } int count = -1; diff --git a/test/onvif_event_test.c b/test/onvif_event_test.c index fcfbd1fc..c0b96bfc 100644 --- a/test/onvif_event_test.c +++ b/test/onvif_event_test.c @@ -19,6 +19,14 @@ static void *late_notify(void *arg) { return NULL; } +static void *waiting_pull(void *arg) { + onvif_event_msg msgs[ONVIF_EVENT_QUEUE]; + time_t expires; + + *(int *)arg = onvif_event_pull(*(int *)arg, 4001, 1, 8, msgs, &expires); + return NULL; +} + int main(void) { onvif_event_msg msgs[ONVIF_EVENT_QUEUE]; time_t expires, now = 1000, start; @@ -90,5 +98,20 @@ int main(void) { CHECK(onvif_event_pull(ids[2], 3014, 0, 8, msgs, &expires) == 0); CHECK(onvif_event_pull(newest, 3014, 0, 8, msgs, &expires) == 1); + // A subscription with a pull waiting is not idle, even if seen the longest ago + for (int i = 0; i < ONVIF_EVENT_MAX_SUBS; i++) { + CHECK((ids[i] = onvif_event_subscribe(4000, 4000, 60, &expires)) > 0); + CHECK(onvif_event_pull(ids[i], 4000, 0, 8, msgs, &expires) == 1); + } + int waiter = ids[0]; + pthread_create(&thread, NULL, waiting_pull, &waiter); + usleep(200000); + for (int i = 1; i < ONVIF_EVENT_MAX_SUBS; i++) + CHECK(onvif_event_pull(ids[i], 4002, 0, 8, msgs, &expires) == 0); + CHECK(onvif_event_subscribe(4003, 4003, 60, &expires) > 0); + pthread_join(thread, NULL); + CHECK(waiter == 0); + CHECK(onvif_event_pull(ids[1], 4003, 0, 8, msgs, &expires) == -1); + CHECK_DONE(); } From de493b104a236094e6a037e1020afab0281ae823 Mon Sep 17 00:00:00 2001 From: Jaro Marval Date: Wed, 30 Sep 2026 16:44:00 -0500 Subject: [PATCH 14/21] Timing PullMessages waits on the monotonic clock --- src/onvif_event.c | 44 ++++++++++++++++++++++++++++++++++++-------- 1 file changed, 36 insertions(+), 8 deletions(-) diff --git a/src/onvif_event.c b/src/onvif_event.c index a579e5c0..a5f481f1 100644 --- a/src/onvif_event.c +++ b/src/onvif_event.c @@ -3,6 +3,7 @@ #include #include #include +#include typedef struct { int id; @@ -18,6 +19,33 @@ static pthread_cond_t eventCond = PTHREAD_COND_INITIALIZER; static bool motionState = false; static int lastId = 0, waits = 0; +// Pull deadlines follow the monotonic clock where the condition variable can +// (not on macOS, where the tests also run), so setting the date cannot stretch them +#if defined(_POSIX_CLOCK_SELECTION) && _POSIX_CLOCK_SELECTION >= 0 +#define EVENT_WAIT_CLOCK CLOCK_MONOTONIC +static pthread_once_t eventOnce = PTHREAD_ONCE_INIT; + +static void event_cond_init(void) { + pthread_condattr_t attr; + + pthread_condattr_init(&attr); + pthread_condattr_setclock(&attr, CLOCK_MONOTONIC); + pthread_cond_init(&eventCond, &attr); + pthread_condattr_destroy(&attr); +} + +static void event_lock(void) { + pthread_once(&eventOnce, event_cond_init); + pthread_mutex_lock(&eventMtx); +} +#else +#define EVENT_WAIT_CLOCK CLOCK_REALTIME + +static void event_lock(void) { + pthread_mutex_lock(&eventMtx); +} +#endif + time_t onvif_event_clock(void) { struct timespec now; clock_gettime(CLOCK_MONOTONIC, &now); @@ -45,7 +73,7 @@ int onvif_event_subscribe(time_t now, time_t when, int seconds, time_t *expires) // A free slot, or else the subscription idle for the longest time: // clients that reconnect rarely unsubscribe first, and one with a pull // waiting is in use however long ago that pull started - pthread_mutex_lock(&eventMtx); + event_lock(); for (int i = 0; i < ONVIF_EVENT_MAX_SUBS && sub->id; i++) if (!subs[i].id || (!subs[i].waiting && sub->waiting) || (!subs[i].waiting == !sub->waiting && subs[i].lastSeen < sub->lastSeen)) @@ -63,7 +91,7 @@ int onvif_event_subscribe(time_t now, time_t when, int seconds, time_t *expires) } bool onvif_event_renew(int id, time_t now, int seconds, time_t *expires) { - pthread_mutex_lock(&eventMtx); + event_lock(); onvif_event_sub *sub = find_sub(id); if (sub) { sub->expires = *expires = now + seconds; @@ -75,7 +103,7 @@ bool onvif_event_renew(int id, time_t now, int seconds, time_t *expires) { } bool onvif_event_unsubscribe(int id) { - pthread_mutex_lock(&eventMtx); + event_lock(); onvif_event_sub *sub = find_sub(id); if (sub) { sub->id = 0; @@ -87,7 +115,7 @@ bool onvif_event_unsubscribe(int id) { } bool onvif_event_sync(int id, time_t now) { - pthread_mutex_lock(&eventMtx); + event_lock(); onvif_event_sub *sub = find_sub(id); if (sub) { push_msg(sub, (onvif_event_msg){ .time = now, .initial = true, .state = motionState }); @@ -101,10 +129,10 @@ bool onvif_event_sync(int id, time_t now) { int onvif_event_pull(int id, time_t now, int timeout_s, int limit, onvif_event_msg *msgs, time_t *expires) { struct timespec deadline; - clock_gettime(CLOCK_REALTIME, &deadline); + clock_gettime(EVENT_WAIT_CLOCK, &deadline); deadline.tv_sec += timeout_s; - pthread_mutex_lock(&eventMtx); + event_lock(); onvif_event_sub *sub = find_sub(id); unsigned int generation = 0; @@ -139,7 +167,7 @@ int onvif_event_pull(int id, time_t now, int timeout_s, int limit, } void onvif_event_expire(time_t now) { - pthread_mutex_lock(&eventMtx); + event_lock(); for (int i = 0; i < ONVIF_EVENT_MAX_SUBS; i++) if (subs[i].id && subs[i].expires <= now) { subs[i].id = 0; @@ -149,7 +177,7 @@ void onvif_event_expire(time_t now) { } void onvif_motion_notify(bool state, time_t when) { - pthread_mutex_lock(&eventMtx); + event_lock(); motionState = state; for (int i = 0; i < ONVIF_EVENT_MAX_SUBS; i++) if (subs[i].id) From e3eb96751ca0e6bbc39e9b482418cc589e34aaaa Mon Sep 17 00:00:00 2001 From: Jaro Marval Date: Wed, 30 Sep 2026 16:44:09 -0500 Subject: [PATCH 15/21] Allowing GetServices and GetServiceCapabilities before ONVIF authentication --- src/onvif.c | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/onvif.c b/src/onvif.c index 51e077e0..f35ddd66 100644 --- a/src/onvif.c +++ b/src/onvif.c @@ -188,7 +188,10 @@ bool onvif_is_preauth_action(const char *action) { if (!action || EMPTY(action)) return false; + // PRE_AUTH access class in ONVIF Core if (EQUALS(action, "GetCapabilities") || + EQUALS(action, "GetServiceCapabilities") || + EQUALS(action, "GetServices") || EQUALS(action, "GetSystemDateAndTime")) return true; From d226bd74b717cd0c153106879d928fca871cd802 Mon Sep 17 00:00:00 2001 From: Jaro Marval Date: Mon, 28 Sep 2026 20:48:51 -0500 Subject: [PATCH 16/21] Adding a block-based motion detector core --- src/motion_core.c | 114 ++++++++++++++++++++++++++++++++++++++++ src/motion_core.h | 23 ++++++++ test/Makefile | 5 +- test/motion_core_test.c | 101 +++++++++++++++++++++++++++++++++++ 4 files changed, 242 insertions(+), 1 deletion(-) create mode 100644 src/motion_core.c create mode 100644 src/motion_core.h create mode 100644 test/motion_core_test.c diff --git a/src/motion_core.c b/src/motion_core.c new file mode 100644 index 00000000..1fa7eb5e --- /dev/null +++ b/src/motion_core.c @@ -0,0 +1,114 @@ +#include "motion_core.h" + +#include +#include + +#define BLOCK 16 +#define BG_SHIFT 4 +#define ENTER_FRAMES 2 +#define SETTLE_FRAMES 10 + +int motion_core_init(motion_core *core, int width, int height) { + memset(core, 0, sizeof(*core)); + if (width % BLOCK || width > MOTION_MAX_WIDTH || height < BLOCK) + return EXIT_FAILURE; + + core->width = width; + core->height = height / BLOCK * BLOCK; + if (!(core->background = malloc(core->width * core->height * sizeof(*core->background)))) + return EXIT_FAILURE; + + motion_core_tune(core, 5, 25); + return EXIT_SUCCESS; +} + +void motion_core_free(motion_core *core) { + free(core->background); + core->background = NULL; +} + +void motion_core_tune(motion_core *core, int sensitivity, int holdFrames) { + core->threshold = 40 - 3 * sensitivity; + core->minBlocks = 11 - sensitivity; + core->holdFrames = holdFrames; +} + +void motion_core_pause(motion_core *core, int frames) { + core->settle = frames; +} + +bool motion_core_reset(motion_core *core) { + bool state = core->state; + + core->primed = core->state = false; + core->streak = core->quiet = core->settle = 0; + return state; +} + +static int motion_core_step(motion_core *core, bool moving) { + if (moving) { + core->quiet = 0; + if (!core->state && ++core->streak >= ENTER_FRAMES) { + core->state = true; + core->streak = 0; + return MOTION_START; + } + } else { + core->streak = 0; + if (core->state && ++core->quiet >= core->holdFrames) { + core->state = false; + core->quiet = 0; + return MOTION_END; + } + } + + return MOTION_NONE; +} + +int motion_core_feed(motion_core *core, const uint8_t *luma, int stride) { + int blocksX = core->width / BLOCK, blocksY = core->height / BLOCK; + int changed = 0, active = 0; + + if (core->settle) { + core->settle--; + core->primed = false; + return motion_core_step(core, false); + } + + // Background in 8.8 fixed point, alpha 1/16: about 3 s of memory at 5 fps + if (!core->primed) { + for (int y = 0; y < core->height; y++) + for (int x = 0; x < core->width; x++) + core->background[y * core->width + x] = luma[y * stride + x] << 8; + core->primed = true; + return motion_core_step(core, false); + } + + for (int by = 0; by < blocksY; by++) { + int counts[MOTION_MAX_WIDTH / BLOCK] = {0}; + + for (int y = by * BLOCK; y < (by + 1) * BLOCK; y++) { + const uint8_t *row = luma + y * stride; + uint16_t *bg = core->background + y * core->width; + for (int x = 0; x < core->width; x++) { + int diff = row[x] - (bg[x] >> 8); + if (diff > core->threshold || -diff > core->threshold) { + counts[x / BLOCK]++; + changed++; + } + bg[x] += (((int)row[x] << 8) - bg[x]) >> BG_SHIFT; + } + } + + for (int bx = 0; bx < blocksX; bx++) + if (counts[bx] > BLOCK * BLOCK / 4) active++; + } + + // Lights switching or the IR cut filter moving: rebuild the background + if (changed > core->width * core->height / 2) { + core->settle = SETTLE_FRAMES; + return motion_core_step(core, false); + } + + return motion_core_step(core, active >= core->minBlocks); +} diff --git a/src/motion_core.h b/src/motion_core.h new file mode 100644 index 00000000..e32e77a9 --- /dev/null +++ b/src/motion_core.h @@ -0,0 +1,23 @@ +#pragma once + +#include +#include + +#define MOTION_MAX_WIDTH 640 + +enum { MOTION_NONE, MOTION_START, MOTION_END }; + +typedef struct { + int width, height; + int threshold, minBlocks, holdFrames; + uint16_t *background; + bool primed, state; + int streak, quiet, settle; +} motion_core; + +int motion_core_init(motion_core *core, int width, int height); +void motion_core_free(motion_core *core); +void motion_core_tune(motion_core *core, int sensitivity, int holdFrames); +void motion_core_pause(motion_core *core, int frames); +bool motion_core_reset(motion_core *core); +int motion_core_feed(motion_core *core, const uint8_t *luma, int stride); diff --git a/test/Makefile b/test/Makefile index 37f2695a..c94083aa 100644 --- a/test/Makefile +++ b/test/Makefile @@ -1,6 +1,6 @@ CFLAGS = -std=gnu99 -Wall -Wno-parentheses -I../src \ -DCLOCK_MONOTONIC_COARSE=CLOCK_MONOTONIC -Dmmap64=mmap -TESTS = soap_test onvif_event_test +TESTS = soap_test onvif_event_test motion_core_test all: $(TESTS) @for t in $(TESTS); do ./$$t || exit 1; done @@ -14,4 +14,7 @@ soap_test: soap_test.c ../src/soap.c ../src/hal/tools.c onvif_event_test: onvif_event_test.c ../src/onvif_event.c $(CC) $(CFLAGS) $^ -lpthread -o $@ +motion_core_test: motion_core_test.c ../src/motion_core.c + $(CC) $(CFLAGS) $^ -o $@ + .PHONY: all clean diff --git a/test/motion_core_test.c b/test/motion_core_test.c new file mode 100644 index 00000000..48352839 --- /dev/null +++ b/test/motion_core_test.c @@ -0,0 +1,101 @@ +#include + +#include "check.h" +#include "motion_core.h" + +#define W 320 +#define H 180 +#define STRIDE 384 + +static uint8_t frame[STRIDE * H]; + +static void fill(uint8_t value) { + memset(frame, value, sizeof(frame)); +} + +static void square(int x0, int y0, int size, uint8_t value) { + for (int y = y0; y < y0 + size && y < H; y++) + for (int x = x0; x < x0 + size && x < W; x++) + frame[y * STRIDE + x] = value; +} + +static void noise(int amplitude, unsigned int *seed) { + for (int i = 0; i < sizeof(frame); i++) { + *seed = *seed * 1103515245 + 12345; + frame[i] = 100 + (int)((*seed >> 16) % (2 * amplitude + 1)) - amplitude; + } +} + +int main(void) { + motion_core core; + int events; + + CHECK(motion_core_init(&core, W, H) == EXIT_SUCCESS); + motion_core_tune(&core, 5, 5); + + // A still scene never reports motion + events = 0; + for (int i = 0; i < 50; i++) { + fill(80); + events += motion_core_feed(&core, frame, STRIDE) != MOTION_NONE; + } + CHECK(events == 0); + + // A moving 64x64 square starts motion on the second analysed frame + int started = -1; + for (int i = 0; i < 10; i++) { + fill(80); + square(16 + i * 16, 48, 64, 220); + if (motion_core_feed(&core, frame, STRIDE) == MOTION_START && started < 0) started = i; + } + CHECK(started == 1); + CHECK(core.state); + + // Motion ends after hold frames without movement + int ended = -1; + for (int i = 0; i < 30; i++) { + fill(80); + if (motion_core_feed(&core, frame, STRIDE) == MOTION_END) ended = i; + } + CHECK(ended >= 4 && ended <= 15); + CHECK(!core.state); + + // A global change (lights on) is ignored and the background is rebuilt + events = 0; + for (int i = 0; i < 30; i++) { + fill(200); + events += motion_core_feed(&core, frame, STRIDE) != MOTION_NONE; + } + CHECK(events == 0); + + // Sensor noise below the threshold never reports motion + unsigned int seed = 1; + events = 0; + for (int i = 0; i < 50; i++) { + noise(10, &seed); + events += motion_core_feed(&core, frame, STRIDE) != MOTION_NONE; + } + CHECK(events == 0); + + // A pause hides movement until the background is rebuilt + motion_core_pause(&core, 10); + events = 0; + for (int i = 0; i < 10; i++) { + fill(80); + square(16 + i * 16, 48, 64, 220); + events += motion_core_feed(&core, frame, STRIDE) == MOTION_START; + } + CHECK(events == 0); + + // Reset returns the previous state + for (int i = 0; i < 5; i++) { + fill(80); + square(16 + i * 16, 48, 64, 220); + motion_core_feed(&core, frame, STRIDE); + } + CHECK(motion_core_reset(&core) == true); + CHECK(motion_core_reset(&core) == false); + + motion_core_free(&core); + CHECK_DONE(); +} From 74c58b90fe8422fba5dc9597282a085e8d70e3ae Mon Sep 17 00:00:00 2001 From: Jaro Marval Date: Mon, 28 Sep 2026 21:00:18 -0500 Subject: [PATCH 17/21] Adding the motion detection settings --- divinus.yaml | 5 +++++ doc/config.md | 9 +++++++++ src/app_config.c | 17 +++++++++++++++++ src/app_config.h | 5 +++++ 4 files changed, 36 insertions(+) diff --git a/divinus.yaml b/divinus.yaml index cf30373e..ccb6b077 100644 --- a/divinus.yaml +++ b/divinus.yaml @@ -61,6 +61,11 @@ night_mode: adc_device: /dev/sar_adc_drv adc_threshold: 128 +motion_detect: + enable: false + sensitivity: 5 + hold_s: 5 + http_post: enable: false host: diff --git a/doc/config.md b/doc/config.md index cab0b10b..ffd94e7e 100644 --- a/doc/config.md +++ b/doc/config.md @@ -30,6 +30,15 @@ This document describes the fields that can be found within a configuration file - **adc_device**: Path to the ADC device used for night mode. - **adc_threshold**: Threshold raw value to trigger night mode, depends on the bitness of the given ADC device. +## Motion detection section + +Software motion detection, published as ONVIF `tns1:VideoSource/MotionAlarm` events +(PullPoint). Only available on infinity6 for now. + +- **enable**: Boolean to activate motion detection (default: `false`). +- **sensitivity**: From 1 (only large movements) to 10 (small movements) (default: `5`). +- **hold_s**: Seconds without movement before the motion state goes back to false (default: `5`). + ## ISP section - **mirror**: Boolean to turn on image mirroring (default: `false`). diff --git a/src/app_config.c b/src/app_config.c index ed828771..bb5664d3 100644 --- a/src/app_config.c +++ b/src/app_config.c @@ -105,6 +105,11 @@ int app_config_save(void) { fprintf(file, " adc_device: %s\n", app_config.adc_device); fprintf(file, " adc_threshold: %d\n", app_config.adc_threshold); + fprintf(file, "motion_detect:\n"); + fprintf(file, " enable: %s\n", app_config.motion_detect_enable ? "true" : "false"); + fprintf(file, " sensitivity: %d\n", app_config.motion_detect_sensitivity); + fprintf(file, " hold_s: %d\n", app_config.motion_detect_hold_s); + fprintf(file, "isp:\n"); fprintf(file, " mirror: %s\n", app_config.mirror ? "true" : "false"); fprintf(file, " flip: %s\n", app_config.flip ? "true" : "false"); @@ -281,6 +286,10 @@ enum ConfigError app_config_parse(void) { app_config.adc_device[0] = 0; app_config.adc_threshold = 128; + app_config.motion_detect_enable = false; + app_config.motion_detect_sensitivity = 5; + app_config.motion_detect_hold_s = 5; + struct IniConfig ini; memset(&ini, 0, sizeof(struct IniConfig)); @@ -370,6 +379,14 @@ enum ConfigError app_config_parse(void) { &app_config.adc_threshold); } + parse_bool(&ini, "motion_detect", "enable", &app_config.motion_detect_enable); + if (app_config.motion_detect_enable) { + parse_int(&ini, "motion_detect", "sensitivity", 1, 10, + &app_config.motion_detect_sensitivity); + parse_int(&ini, "motion_detect", "hold_s", 1, 300, + &app_config.motion_detect_hold_s); + } + err = parse_bool(&ini, "isp", "mirror", &app_config.mirror); if (err != CONFIG_OK) goto RET_ERR; diff --git a/src/app_config.h b/src/app_config.h index 029e0d48..69ecfabb 100644 --- a/src/app_config.h +++ b/src/app_config.h @@ -37,6 +37,11 @@ struct AppConfig { char adc_device[128]; int adc_threshold; + // [motion_detect] + bool motion_detect_enable; + int motion_detect_sensitivity; + int motion_detect_hold_s; + // [isp] bool mirror; bool flip; From 478a9be78c948a8cfbda91e5cbe33d44e224dc38 Mon Sep 17 00:00:00 2001 From: Jaro Marval Date: Mon, 28 Sep 2026 21:00:18 -0500 Subject: [PATCH 18/21] Reading raw frames from a free VPE port on infinity6 Measured on SSC323: port 2 gives 320x180 NV12 frames with a 320 stride. Port 3 returns buffers with fresh timestamps whose memory is never written, even through an uncached mapping. --- src/hal/star/i6_hal.c | 66 +++++++++++++++++++++++++++++++++++++++++++ src/hal/star/i6_hal.h | 5 ++++ src/hal/star/i6_sys.h | 38 +++++++++++++++++++++++++ src/hal/types.h | 7 +++++ src/media.c | 36 +++++++++++++++++++++++ src/media.h | 5 ++++ 6 files changed, 157 insertions(+) diff --git a/src/hal/star/i6_hal.c b/src/hal/star/i6_hal.c index 1c48fd0c..5da3fa37 100644 --- a/src/hal/star/i6_hal.c +++ b/src/hal/star/i6_hal.c @@ -379,6 +379,72 @@ void i6_pipeline_destroy(void) i6_snr.fnDisable(_i6_snr_index); } +// Port 3 of the SSC323 VPE hands out buffers that are never written +#define I6_RAW_PORT 2 + +int i6_raw_create(short width, short height) +{ + int ret; + + if (!i6_sys.fnGetBuf || !i6_sys.fnPutBuf) + HAL_ERROR("i6_raw", "libmi_sys does not export the buffer functions!\n"); + if (i6_state[I6_RAW_PORT].enable) + HAL_ERROR("i6_raw", "VPE port %d is already used by an encoder!\n", I6_RAW_PORT); + + i6_vpe_port port; + port.output.width = width; + port.output.height = height; + port.mirror = 0; + port.flip = 0; + port.compress = I6_COMPR_NONE; + port.pixFmt = I6_PIXFMT_YUV420SP; + if (ret = i6_vpe.fnSetPortConfig(_i6_vpe_chn, I6_RAW_PORT, &port)) + return ret; + + i6_sys_bind bind = { .module = I6_SYS_MOD_VPE, + .device = _i6_vpe_dev, .channel = _i6_vpe_chn, .port = I6_RAW_PORT }; + if (ret = i6_sys.fnSetOutputDepth(&bind, 1, 3)) + return ret; + + return i6_vpe.fnEnablePort(_i6_vpe_chn, I6_RAW_PORT); +} + +int i6_raw_get(hal_rawframe *frame) +{ + i6_sys_bind bind = { .module = I6_SYS_MOD_VPE, + .device = _i6_vpe_dev, .channel = _i6_vpe_chn, .port = I6_RAW_PORT }; + i6_sys_bufinfo info; + int handle; + + if (i6_sys.fnGetBuf(&bind, &info, &handle)) + return EXIT_FAILURE; + + if (info.frame.pixFmt != I6_PIXFMT_YUV420SP) { + i6_sys.fnPutBuf(handle); + HAL_ERROR("i6_raw", "Unexpected frame format %d!\n", info.frame.pixFmt); + } + + if (i6_sys.fnFlushInvCache) + i6_sys.fnFlushInvCache(info.frame.virtAddr[0], info.frame.stride[0] * info.frame.height); + + frame->luma = info.frame.virtAddr[0]; + frame->stride = info.frame.stride[0]; + frame->width = info.frame.width; + frame->height = info.frame.height; + frame->handle = handle; + return EXIT_SUCCESS; +} + +int i6_raw_release(hal_rawframe *frame) +{ + return i6_sys.fnPutBuf(frame->handle); +} + +void i6_raw_destroy(void) +{ + i6_vpe.fnDisablePort(_i6_vpe_chn, I6_RAW_PORT); +} + int i6_region_create(char handle, hal_rect rect, short opacity) { int ret; diff --git a/src/hal/star/i6_hal.h b/src/hal/star/i6_hal.h index 75e83c48..3ca8a175 100644 --- a/src/hal/star/i6_hal.h +++ b/src/hal/star/i6_hal.h @@ -38,6 +38,11 @@ int i6_config_load(char *path); int i6_pipeline_create(char index, short width, short height, char mirror, char flip, char framerate); void i6_pipeline_destroy(void); +int i6_raw_create(short width, short height); +int i6_raw_get(hal_rawframe *frame); +int i6_raw_release(hal_rawframe *frame); +void i6_raw_destroy(void); + int i6_region_create(char handle, hal_rect rect, short opacity); void i6_region_deinit(void); void i6_region_destroy(char handle); diff --git a/src/hal/star/i6_sys.h b/src/hal/star/i6_sys.h index 88447f2e..ef93c686 100644 --- a/src/hal/star/i6_sys.h +++ b/src/hal/star/i6_sys.h @@ -68,6 +68,31 @@ typedef struct { unsigned char version[128]; } i6_sys_ver; +typedef struct { + unsigned long long pts; + int type; + unsigned int flags; + union { + // Layout measured on SSC323 (infinity6), the first five words are unknown + struct { + int tileMode; + unsigned int unknown[4]; + i6_common_pixfmt pixFmt; + i6_common_compr compress; + int scanMode; + int fieldType; + int ispInfoType; + unsigned short width; + unsigned short height; + void *virtAddr[3]; + unsigned long long physAddr[3]; + unsigned int stride[3]; + unsigned int bufSize; + } frame; + unsigned char reserved[112]; + }; +} i6_sys_bufinfo; + typedef struct { void *handle, *handleCamOsWrapper; @@ -81,6 +106,10 @@ typedef struct { unsigned int dstFps, i6_sys_link link, unsigned int linkParam); int (*fnSetOutputDepth)(i6_sys_bind *bind, unsigned int usrDepth, unsigned int bufDepth); int (*fnUnbind)(i6_sys_bind *source, i6_sys_bind *dest); + + int (*fnFlushInvCache)(void *address, unsigned int length); + int (*fnGetBuf)(i6_sys_bind *bind, i6_sys_bufinfo *info, int *handle); + int (*fnPutBuf)(int handle); } i6_sys_impl; static int i6_sys_load(i6_sys_impl *sys_lib) { @@ -119,6 +148,15 @@ static int i6_sys_load(i6_sys_impl *sys_lib) { hal_symbol_load("i6_sys", sys_lib->handle, "MI_SYS_UnBindChnPort"))) return EXIT_FAILURE; + sys_lib->fnFlushInvCache = (int(*)(void *address, unsigned int length)) + hal_symbol_load("i6_sys", sys_lib->handle, "MI_SYS_FlushInvCache"); + + sys_lib->fnGetBuf = (int(*)(i6_sys_bind *bind, i6_sys_bufinfo *info, int *handle)) + hal_symbol_load("i6_sys", sys_lib->handle, "MI_SYS_ChnOutputPortGetBuf"); + + sys_lib->fnPutBuf = (int(*)(int handle)) + hal_symbol_load("i6_sys", sys_lib->handle, "MI_SYS_ChnOutputPortPutBuf"); + return EXIT_SUCCESS; } diff --git a/src/hal/types.h b/src/hal/types.h index 4b0714b8..85703697 100644 --- a/src/hal/types.h +++ b/src/hal/types.h @@ -70,6 +70,13 @@ typedef struct { hal_vidcodec payload; } hal_chnstate; +typedef struct { + unsigned char *luma; + int stride; + short width, height; + int handle; +} hal_rawframe; + typedef struct { unsigned short width, height; } hal_dim; diff --git a/src/media.c b/src/media.c index e9cb510c..2a2a0112 100644 --- a/src/media.c +++ b/src/media.c @@ -371,6 +371,42 @@ void set_grayscale(bool active) { pthread_mutex_unlock(&chnMtx); } +int raw_create(short width, short height) { + switch (plat) { +#if defined(__ARM_PCS_VFP) + case HAL_PLATFORM_I6: return i6_raw_create(width, height); +#endif + } + HAL_WARNING("media", "Raw frames are not supported on this platform!\n"); + return EXIT_FAILURE; +} + +int raw_get(hal_rawframe *frame) { + switch (plat) { +#if defined(__ARM_PCS_VFP) + case HAL_PLATFORM_I6: return i6_raw_get(frame); +#endif + } + return EXIT_FAILURE; +} + +int raw_release(hal_rawframe *frame) { + switch (plat) { +#if defined(__ARM_PCS_VFP) + case HAL_PLATFORM_I6: return i6_raw_release(frame); +#endif + } + return EXIT_FAILURE; +} + +void raw_destroy(void) { + switch (plat) { +#if defined(__ARM_PCS_VFP) + case HAL_PLATFORM_I6: i6_raw_destroy(); break; +#endif + } +} + int take_next_free_channel(bool mainLoop) { pthread_mutex_lock(&chnMtx); for (int i = 0; i < chnCount; i++) { diff --git a/src/media.h b/src/media.h index f7172e4b..6a406ff4 100644 --- a/src/media.h +++ b/src/media.h @@ -34,6 +34,11 @@ void media_stop(void); void request_idr(void); void set_grayscale(bool active); + +int raw_create(short width, short height); +int raw_get(hal_rawframe *frame); +int raw_release(hal_rawframe *frame); +void raw_destroy(void); int take_next_free_channel(bool mainLoop); int create_channel(char index, short width, short height, char framerate, char jpeg); From 1e691c69ba68abd94a00469c92f04355aa270341 Mon Sep 17 00:00:00 2001 From: Jaro Marval Date: Mon, 28 Sep 2026 21:12:30 -0500 Subject: [PATCH 19/21] Detecting motion from raw VPE frames, raising ONVIF events --- src/main.c | 7 ++++ src/motion.c | 105 +++++++++++++++++++++++++++++++++++++++++++++++++++ src/motion.h | 16 ++++++++ 3 files changed, 128 insertions(+) create mode 100644 src/motion.c create mode 100644 src/motion.h diff --git a/src/main.c b/src/main.c index 5b9b72db..e9d40413 100644 --- a/src/main.c +++ b/src/main.c @@ -2,6 +2,7 @@ #include "hal/macros.h" #include "http_post.h" #include "media.h" +#include "motion.h" #include "network.h" #include "night.h" #include "onvif_event.h" @@ -95,6 +96,9 @@ int main(int argc, char *argv[]) { if (app_config.night_mode_enable) night_enable(); + if (app_config.motion_detect_enable) + motion_start(); + if (app_config.http_post_enable) http_post_start(); @@ -123,6 +127,9 @@ int main(int argc, char *argv[]) { if (app_config.night_mode_enable) night_disable(); + if (app_config.motion_detect_enable) + motion_stop(); + sdk_stop(); if (app_config.stream_enable) diff --git a/src/motion.c b/src/motion.c new file mode 100644 index 00000000..23c8421d --- /dev/null +++ b/src/motion.c @@ -0,0 +1,105 @@ +#include "motion.h" + +#define MOTION_WIDTH 320 +#define MOTION_HEIGHT 180 +#define MOTION_FPS 5 +#define MOTION_STALL_S 10 + +static char motionOn = 0; +static pthread_t motionPid = 0; +static pthread_mutex_t pauseMtx = PTHREAD_MUTEX_INITIALIZER; +static int pauseFrames = 0; + +void motion_pause(int ms) { + pthread_mutex_lock(&pauseMtx); + pauseFrames = ms * MOTION_FPS / 1000; + pthread_mutex_unlock(&pauseMtx); +} + +static void motion_report(int change) { + if (change == MOTION_NONE) return; + HAL_INFO("motion", "Motion %s\n", change == MOTION_START ? "started" : "ended"); + onvif_motion_notify(change == MOTION_START, time(NULL)); +} + +static void *motion_thread(void) { + motion_core core; + hal_rawframe frame; + int missed = 0; + bool reopened = false; + + if (motion_core_init(&core, MOTION_WIDTH, MOTION_HEIGHT)) { + HAL_DANGER("motion", "Can't allocate the detector!\n"); + return NULL; + } + motion_core_tune(&core, app_config.motion_detect_sensitivity, + app_config.motion_detect_hold_s * MOTION_FPS); + + while (keepRunning && motionOn) { + usleep(1000000 / MOTION_FPS); + + pthread_mutex_lock(&pauseMtx); + if (pauseFrames) { + motion_core_pause(&core, pauseFrames); + pauseFrames = 0; + } + pthread_mutex_unlock(&pauseMtx); + + if (raw_get(&frame)) { + if (++missed < MOTION_STALL_S * MOTION_FPS) continue; + HAL_WARNING("motion", "No frames for %d seconds!\n", MOTION_STALL_S); + if (motion_core_reset(&core)) motion_report(MOTION_END); + if (reopened) { + HAL_DANGER("motion", "Frames did not come back, detection stopped!\n"); + break; + } + raw_destroy(); + if (raw_create(MOTION_WIDTH, MOTION_HEIGHT)) break; + reopened = true; + missed = 0; + continue; + } + missed = 0; + + int change = motion_core_feed(&core, frame.luma, frame.stride); + raw_release(&frame); + motion_report(change); + } + + if (motion_core_reset(&core)) motion_report(MOTION_END); + motion_core_free(&core); + HAL_INFO("motion", "Motion detection thread is closing...\n"); + return NULL; +} + +int motion_start(void) { + if (motionOn) return EXIT_SUCCESS; + + if (raw_create(MOTION_WIDTH, MOTION_HEIGHT)) { + HAL_WARNING("motion", "Motion detection is disabled!\n"); + return EXIT_FAILURE; + } + + motionOn = 1; + pthread_attr_t thread_attr; + pthread_attr_init(&thread_attr); + size_t new_stacksize = 16 * 1024; + if (pthread_attr_setstacksize(&thread_attr, new_stacksize)) + HAL_DANGER("motion", "Can't set stack size %zu\n", new_stacksize); + if (pthread_create(&motionPid, &thread_attr, (void *(*)(void *))motion_thread, NULL)) { + HAL_DANGER("motion", "Can't create thread\n"); + motionOn = 0; + raw_destroy(); + } + pthread_attr_destroy(&thread_attr); + + return motionOn ? EXIT_SUCCESS : EXIT_FAILURE; +} + +void motion_stop(void) { + if (!motionOn) return; + + motionOn = 0; + pthread_join(motionPid, NULL); + raw_destroy(); +} diff --git a/src/motion.h b/src/motion.h new file mode 100644 index 00000000..4a610631 --- /dev/null +++ b/src/motion.h @@ -0,0 +1,16 @@ +#pragma once + +#include +#include +#include +#include + +#include "app_config.h" +#include "hal/macros.h" +#include "media.h" +#include "motion_core.h" +#include "onvif_event.h" + +int motion_start(void); +void motion_stop(void); +void motion_pause(int ms); From 163dc83fea1beceaf82a780a469dd761be283794 Mon Sep 17 00:00:00 2001 From: Jaro Marval Date: Mon, 28 Sep 2026 21:30:27 -0500 Subject: [PATCH 20/21] Reserving the raw VPE port, hardening the motion detector input - Encoders no longer take the port the motion detector reads - Frames smaller than the detector input are skipped - More headroom for MI_SYS_BufInfo_t --- src/hal/star/i6_hal.c | 3 --- src/hal/star/i6_hal.h | 3 +++ src/hal/star/i6_sys.h | 2 +- src/media.c | 22 ++++++++++++++++++---- src/motion.c | 6 ++++++ 5 files changed, 28 insertions(+), 8 deletions(-) diff --git a/src/hal/star/i6_hal.c b/src/hal/star/i6_hal.c index 5da3fa37..3dc3e902 100644 --- a/src/hal/star/i6_hal.c +++ b/src/hal/star/i6_hal.c @@ -379,9 +379,6 @@ void i6_pipeline_destroy(void) i6_snr.fnDisable(_i6_snr_index); } -// Port 3 of the SSC323 VPE hands out buffers that are never written -#define I6_RAW_PORT 2 - int i6_raw_create(short width, short height) { int ret; diff --git a/src/hal/star/i6_hal.h b/src/hal/star/i6_hal.h index 3ca8a175..ae589f84 100644 --- a/src/hal/star/i6_hal.h +++ b/src/hal/star/i6_hal.h @@ -38,6 +38,9 @@ int i6_config_load(char *path); int i6_pipeline_create(char index, short width, short height, char mirror, char flip, char framerate); void i6_pipeline_destroy(void); +// Port 3 of the SSC323 VPE hands out buffers that are never written +#define I6_RAW_PORT 2 + int i6_raw_create(short width, short height); int i6_raw_get(hal_rawframe *frame); int i6_raw_release(hal_rawframe *frame); diff --git a/src/hal/star/i6_sys.h b/src/hal/star/i6_sys.h index ef93c686..c5ef97e7 100644 --- a/src/hal/star/i6_sys.h +++ b/src/hal/star/i6_sys.h @@ -89,7 +89,7 @@ typedef struct { unsigned int stride[3]; unsigned int bufSize; } frame; - unsigned char reserved[112]; + unsigned char reserved[240]; }; } i6_sys_bufinfo; diff --git a/src/media.c b/src/media.c index 2a2a0112..656cca27 100644 --- a/src/media.c +++ b/src/media.c @@ -371,14 +371,25 @@ void set_grayscale(bool active) { pthread_mutex_unlock(&chnMtx); } +static int rawChannel = -1; + int raw_create(short width, short height) { + int ret = EXIT_FAILURE; + + pthread_mutex_lock(&chnMtx); switch (plat) { #if defined(__ARM_PCS_VFP) - case HAL_PLATFORM_I6: return i6_raw_create(width, height); + case HAL_PLATFORM_I6: + if (!(ret = i6_raw_create(width, height))) + rawChannel = I6_RAW_PORT; + break; #endif + default: + HAL_WARNING("media", "Raw frames are not supported on this platform!\n"); } - HAL_WARNING("media", "Raw frames are not supported on this platform!\n"); - return EXIT_FAILURE; + pthread_mutex_unlock(&chnMtx); + + return ret; } int raw_get(hal_rawframe *frame) { @@ -400,17 +411,20 @@ int raw_release(hal_rawframe *frame) { } void raw_destroy(void) { + pthread_mutex_lock(&chnMtx); switch (plat) { #if defined(__ARM_PCS_VFP) case HAL_PLATFORM_I6: i6_raw_destroy(); break; #endif } + rawChannel = -1; + pthread_mutex_unlock(&chnMtx); } int take_next_free_channel(bool mainLoop) { pthread_mutex_lock(&chnMtx); for (int i = 0; i < chnCount; i++) { - if (chnState[i].enable) continue; + if (chnState[i].enable || i == rawChannel) continue; chnState[i].enable = true; chnState[i].mainLoop = mainLoop; pthread_mutex_unlock(&chnMtx); diff --git a/src/motion.c b/src/motion.c index 23c8421d..6bc20adf 100644 --- a/src/motion.c +++ b/src/motion.c @@ -61,6 +61,12 @@ static void *motion_thread(void) { } missed = 0; + if (frame.width < MOTION_WIDTH || frame.height < MOTION_HEIGHT || + frame.stride < MOTION_WIDTH) { + raw_release(&frame); + continue; + } + int change = motion_core_feed(&core, frame.luma, frame.stride); raw_release(&frame); motion_report(change); From 7109f2c55cddd3820476ba84cd48f5e3e7298218 Mon Sep 17 00:00:00 2001 From: Jaro Marval Date: Mon, 5 Oct 2026 11:07:37 -0500 Subject: [PATCH 21/21] Reopening the raw port on every frame stall, as a second stall stopped the detector --- src/media.c | 18 ++++++++++++++++++ src/media.h | 1 + src/motion.c | 18 +++++++++--------- 3 files changed, 28 insertions(+), 9 deletions(-) diff --git a/src/media.c b/src/media.c index 656cca27..753640a6 100644 --- a/src/media.c +++ b/src/media.c @@ -392,6 +392,24 @@ int raw_create(short width, short height) { return ret; } +// Keeps the port reserved, so no encoder can take it while it is closed +int raw_reopen(short width, short height) { + int ret = EXIT_FAILURE; + + pthread_mutex_lock(&chnMtx); + switch (plat) { +#if defined(__ARM_PCS_VFP) + case HAL_PLATFORM_I6: + i6_raw_destroy(); + ret = i6_raw_create(width, height); + break; +#endif + } + pthread_mutex_unlock(&chnMtx); + + return ret; +} + int raw_get(hal_rawframe *frame) { switch (plat) { #if defined(__ARM_PCS_VFP) diff --git a/src/media.h b/src/media.h index 6a406ff4..8521d5f6 100644 --- a/src/media.h +++ b/src/media.h @@ -36,6 +36,7 @@ void request_idr(void); void set_grayscale(bool active); int raw_create(short width, short height); +int raw_reopen(short width, short height); int raw_get(hal_rawframe *frame); int raw_release(hal_rawframe *frame); void raw_destroy(void); diff --git a/src/motion.c b/src/motion.c index 6bc20adf..470c9fdf 100644 --- a/src/motion.c +++ b/src/motion.c @@ -26,7 +26,7 @@ static void *motion_thread(void) { motion_core core; hal_rawframe frame; int missed = 0; - bool reopened = false; + bool stalled = false; if (motion_core_init(&core, MOTION_WIDTH, MOTION_HEIGHT)) { HAL_DANGER("motion", "Can't allocate the detector!\n"); @@ -47,18 +47,18 @@ static void *motion_thread(void) { if (raw_get(&frame)) { if (++missed < MOTION_STALL_S * MOTION_FPS) continue; - HAL_WARNING("motion", "No frames for %d seconds!\n", MOTION_STALL_S); + if (!stalled) + HAL_WARNING("motion", "No frames for %d seconds, reopening the port...\n", MOTION_STALL_S); if (motion_core_reset(&core)) motion_report(MOTION_END); - if (reopened) { - HAL_DANGER("motion", "Frames did not come back, detection stopped!\n"); - break; - } - raw_destroy(); - if (raw_create(MOTION_WIDTH, MOTION_HEIGHT)) break; - reopened = true; + if (raw_reopen(MOTION_WIDTH, MOTION_HEIGHT) && !stalled) + HAL_DANGER("motion", "Could not reopen the port, retrying...\n"); + stalled = true; missed = 0; continue; } + if (stalled) + HAL_INFO("motion", "Frames are back\n"); + stalled = false; missed = 0; if (frame.width < MOTION_WIDTH || frame.height < MOTION_HEIGHT ||