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/divinus.yaml b/divinus.yaml
index cf30373e..7c5d5e50 100644
--- a/divinus.yaml
+++ b/divinus.yaml
@@ -60,6 +60,15 @@ night_mode:
pin_switch_delay_us: 150
adc_device: /dev/sar_adc_drv
adc_threshold: 128
+ night_gain: 8
+ day_gain: 2
+ night_hold_s: 15
+ day_hold_s: 60
+
+motion_detect:
+ enable: false
+ sensitivity: 5
+ hold_s: 5
http_post:
enable: false
diff --git a/doc/config.md b/doc/config.md
index cab0b10b..96b8568c 100644
--- a/doc/config.md
+++ b/doc/config.md
@@ -29,6 +29,19 @@ This document describes the fields that can be found within a configuration file
- **pin_switch_delay_us**: Delay in microseconds before switching GPIO pins, must be used to protect cut filter coils from burning.
- **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.
+- **night_gain**: Without a light sensor or ADC, total ISP gain (×) that switches to night mode (default: `8`).
+- **day_gain**: Without a light sensor or ADC, total ISP gain (×) below which night mode switches back to day; keep it under the gain reached with the IR LED on (default: `2`).
+- **night_hold_s**: Seconds the gain must stay above `night_gain` before switching to night (default: `15`).
+- **day_hold_s**: Seconds the gain must stay below `day_gain` before switching back to day (default: `60`).
+
+## 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
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/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/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/app_config.c b/src/app_config.c
index ed828771..cce8233a 100644
--- a/src/app_config.c
+++ b/src/app_config.c
@@ -104,6 +104,15 @@ int app_config_save(void) {
fprintf(file, " pin_switch_delay_us: %d\n", app_config.pin_switch_delay_us);
fprintf(file, " adc_device: %s\n", app_config.adc_device);
fprintf(file, " adc_threshold: %d\n", app_config.adc_threshold);
+ fprintf(file, " night_gain: %d\n", app_config.night_gain);
+ fprintf(file, " day_gain: %d\n", app_config.day_gain);
+ fprintf(file, " night_hold_s: %d\n", app_config.night_hold_s);
+ fprintf(file, " day_hold_s: %d\n", app_config.day_hold_s);
+
+ 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");
@@ -280,6 +289,14 @@ enum ConfigError app_config_parse(void) {
app_config.check_interval_s = 10;
app_config.adc_device[0] = 0;
app_config.adc_threshold = 128;
+ app_config.night_gain = 8;
+ app_config.day_gain = 2;
+ app_config.night_hold_s = 15;
+ app_config.day_hold_s = 60;
+
+ 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));
@@ -368,6 +385,24 @@ enum ConfigError app_config_parse(void) {
parse_int(
&ini, "night_mode", "adc_threshold", INT_MIN, INT_MAX,
&app_config.adc_threshold);
+ parse_int(&ini, "night_mode", "night_gain", 1, 1024, &app_config.night_gain);
+ parse_int(&ini, "night_mode", "day_gain", 1, 1024, &app_config.day_gain);
+ parse_int(&ini, "night_mode", "night_hold_s", 1, 3600, &app_config.night_hold_s);
+ parse_int(&ini, "night_mode", "day_hold_s", 1, 3600, &app_config.day_hold_s);
+ if (!app_config.adc_device[0] && app_config.ir_sensor_pin == 999 &&
+ app_config.day_gain >= app_config.night_gain) {
+ HAL_DANGER("app_config", "night_mode.day_gain must be lower than night_gain!\n");
+ err = CONFIG_PARAM_ISNT_IN_RANGE;
+ goto RET_ERR;
+ }
+ }
+
+ 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);
diff --git a/src/app_config.h b/src/app_config.h
index 029e0d48..ca0f966b 100644
--- a/src/app_config.h
+++ b/src/app_config.h
@@ -36,6 +36,15 @@ struct AppConfig {
unsigned int pin_switch_delay_us;
char adc_device[128];
int adc_threshold;
+ unsigned int night_gain;
+ unsigned int day_gain;
+ unsigned int night_hold_s;
+ unsigned int day_hold_s;
+
+ // [motion_detect]
+ bool motion_detect_enable;
+ int motion_detect_sensitivity;
+ int motion_detect_hold_s;
// [isp]
bool mirror;
diff --git a/src/gpio.c b/src/gpio.c
index cb19919d..cd71b595 100644
--- a/src/gpio.c
+++ b/src/gpio.c
@@ -118,15 +118,13 @@ int gpio_read(char pin, bool *value) {
char val = 0;
lseek(fd, 0, SEEK_SET);
- read(fd, &val, 0);
- if (!val) {
+ if (read(fd, &val, 1) != 1) {
close(fd);
HAL_ERROR("gpio", "Unable to read from GPIO pin %d!\n", pin);
}
*value = val - 0x30;
close(fd);
- if (gpio_direction(pin, "out")) return EXIT_FAILURE;
if (gpio_export(pin, false)) return EXIT_FAILURE;
return EXIT_SUCCESS;
diff --git a/src/hal/star/i6_hal.c b/src/hal/star/i6_hal.c
index 1c48fd0c..93d1d774 100644
--- a/src/hal/star/i6_hal.c
+++ b/src/hal/star/i6_hal.c
@@ -183,7 +183,8 @@ int i6_channel_create(char index, short width, short height, char jpeg)
int i6_channel_grayscale(char enable)
{
- return i6_isp.fnSetColorToGray(0, &enable);
+ int active = enable;
+ return i6_isp.fnSetColorToGray(0, &active);
}
int i6_channel_unbind(char index)
@@ -379,6 +380,69 @@ void i6_pipeline_destroy(void)
i6_snr.fnDisable(_i6_snr_index);
}
+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;
@@ -492,6 +556,20 @@ int i6_sensor_exposure(unsigned int micros)
return ret;
}
+int i6_isp_gain(unsigned int *gain)
+{
+ i6_isp_expinfo exp;
+ int ret;
+
+ if (!i6_isp.fnQueryExposureInfo)
+ return EXIT_FAILURE;
+ if (ret = i6_isp.fnQueryExposureInfo(_i6_isp_chn, &exp))
+ return ret;
+
+ *gain = (unsigned long long)exp.info.longExp.sensorGain * exp.info.longExp.ispGain / 1024;
+ return EXIT_SUCCESS;
+}
+
int i6_video_create(char index, hal_vidconfig *config)
{
int ret;
diff --git a/src/hal/star/i6_hal.h b/src/hal/star/i6_hal.h
index 75e83c48..43713a1c 100644
--- a/src/hal/star/i6_hal.h
+++ b/src/hal/star/i6_hal.h
@@ -38,12 +38,21 @@ 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);
+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);
void i6_region_init(void);
int i6_region_setbitmap(int handle, hal_bitmap *bitmap);
+int i6_isp_gain(unsigned int *gain);
int i6_sensor_exposure(unsigned int micros);
int i6_video_create(char index, hal_vidconfig *config);
diff --git a/src/hal/star/i6_isp.h b/src/hal/star/i6_isp.h
index 04a5c142..fb4fe0e8 100644
--- a/src/hal/star/i6_isp.h
+++ b/src/hal/star/i6_isp.h
@@ -17,15 +17,39 @@ typedef struct {
int params[13];
} i6_isp_p3a;
+typedef struct {
+ unsigned int fNx10;
+ unsigned int sensorGain;
+ unsigned int ispGain;
+ unsigned int shutterUs;
+} i6_isp_expval;
+
+typedef union {
+ struct {
+ unsigned int stable;
+ unsigned int reachBoundary;
+ i6_isp_expval longExp;
+ i6_isp_expval shortExp;
+ unsigned int lumY;
+ unsigned int avgY;
+ unsigned int hits[128];
+ unsigned int lvX10;
+ int bv;
+ unsigned int sceneTarget;
+ } info;
+ unsigned char reserved[1024];
+} i6_isp_expinfo;
+
typedef struct {
void *handle, *handleCus3a, *handleIspAlgo;
int (*fnDisableUserspace3A)(int channel);
int (*fnEnableUserspace3A)(int channel, i6_isp_p3a *params);
int (*fnLoadChannelConfig)(int channel, char *path, unsigned int key);
- int (*fnSetColorToGray)(int channel, char *enable);
+ int (*fnSetColorToGray)(int channel, int *enable);
int (*fnGetExposureLimit)(int channel, i6_isp_exp *config);
int (*fnSetExposureLimit)(int channel, i6_isp_exp *config);
+ int (*fnQueryExposureInfo)(int channel, i6_isp_expinfo *info);
} i6_isp_impl;
static int i6_isp_load(i6_isp_impl *isp_lib) {
@@ -48,7 +72,7 @@ static int i6_isp_load(i6_isp_impl *isp_lib) {
hal_symbol_load("i6_isp", isp_lib->handle, "MI_ISP_API_CmdLoadBinFile")))
return EXIT_FAILURE;
- if (!(isp_lib->fnSetColorToGray = (int(*)(int channel, char *enable))
+ if (!(isp_lib->fnSetColorToGray = (int(*)(int channel, int *enable))
hal_symbol_load("i6_isp", isp_lib->handle, "MI_ISP_IQ_SetColorToGray")))
return EXIT_FAILURE;
@@ -60,6 +84,9 @@ static int i6_isp_load(i6_isp_impl *isp_lib) {
hal_symbol_load("i6_isp", isp_lib->handle, "MI_ISP_AE_SetExposureLimit")))
return EXIT_FAILURE;
+ isp_lib->fnQueryExposureInfo = (int(*)(int channel, i6_isp_expinfo *info))
+ hal_symbol_load("i6_isp", isp_lib->handle, "MI_ISP_AE_QueryExposureInfo");
+
return EXIT_SUCCESS;
}
diff --git a/src/hal/star/i6_sys.h b/src/hal/star/i6_sys.h
index 88447f2e..c5ef97e7 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[240];
+ };
+} 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/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/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/main.c b/src/main.c
index 6c027acf..e9d40413 100644
--- a/src/main.c
+++ b/src/main.c
@@ -2,8 +2,10 @@
#include "hal/macros.h"
#include "http_post.h"
#include "media.h"
+#include "motion.h"
#include "network.h"
#include "night.h"
+#include "onvif_event.h"
#include "rtsp/rtsp_server.h"
#include "server.h"
#include "watchdog.h"
@@ -94,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();
@@ -105,6 +110,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(onvif_event_clock());
watchdog_reset();
sleep(1);
}
@@ -121,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/media.c b/src/media.c
index e9cb510c..33756ba9 100644
--- a/src/media.c
+++ b/src/media.c
@@ -371,10 +371,87 @@ 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:
+ 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");
+ }
+ pthread_mutex_unlock(&chnMtx);
+
+ 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)
+ 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) {
+ 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 get_isp_gain(unsigned int *gain) {
+ switch (plat) {
+#if defined(__ARM_PCS_VFP)
+ case HAL_PLATFORM_I6: return i6_isp_gain(gain);
+#endif
+ }
+ return EXIT_FAILURE;
+}
+
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/media.h b/src/media.h
index f7172e4b..56d22a69 100644
--- a/src/media.h
+++ b/src/media.h
@@ -34,6 +34,14 @@ void media_stop(void);
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);
+
+int get_isp_gain(unsigned int *gain);
int take_next_free_channel(bool mainLoop);
int create_channel(char index, short width, short height, char framerate, char jpeg);
diff --git a/src/motion.c b/src/motion.c
new file mode 100644
index 00000000..470c9fdf
--- /dev/null
+++ b/src/motion.c
@@ -0,0 +1,111 @@
+#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 stalled = 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;
+ 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 (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 ||
+ frame.stride < MOTION_WIDTH) {
+ raw_release(&frame);
+ continue;
+ }
+
+ 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);
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/src/night.c b/src/night.c
index a0c8c44c..96965861 100644
--- a/src/night.c
+++ b/src/night.c
@@ -43,10 +43,12 @@ void night_mode(bool enable) {
}
void *night_thread(void) {
+ unsigned int gain;
+
gpio_init();
usleep(10000);
- night_mode(night_mode_on());
+ if (!manual) night_mode(night_mode_on());
if (app_config.adc_device[0]) {
int adc_fd = -1;
@@ -71,22 +73,61 @@ void *night_thread(void) {
usleep(app_config.check_interval_s * 1000000 / 12);
}
if (adc_fd) close(adc_fd);
+ } else if (app_config.ir_sensor_pin == 999 && get_isp_gain(&gain)) {
+ HAL_WARNING("night", "No ISP gain on this platform, automatic switching disabled!\n");
} else if (app_config.ir_sensor_pin == 999) {
- while (keepRunning) sleep(1);
+ bool night = night_mode_on(), gain_lost = false;
+ unsigned int held = 0, since_pulse = 0;
+
+ while (keepRunning && nightOn) {
+ sleep(1);
+ if (manual) {
+ held = 0;
+ continue;
+ }
+ // A missed pulse leaves the filter out by day, which lowers the gain
+ // so no crossing ever corrects it: pulse the current position again
+ if (++since_pulse >= 600) {
+ night_ircut(!night);
+ since_pulse = 0;
+ }
+ if (get_isp_gain(&gain)) {
+ if (!gain_lost)
+ HAL_WARNING("night", "Could not read the ISP gain, retrying...\n");
+ gain_lost = true;
+ held = 0;
+ continue;
+ }
+ gain_lost = false;
+
+ bool crossing = night ?
+ gain < app_config.day_gain * 1024 : gain >= app_config.night_gain * 1024;
+ if (!crossing) {
+ held = 0;
+ continue;
+ }
+ if (++held < (night ? app_config.day_hold_s : app_config.night_hold_s))
+ continue;
+
+ night = !night;
+ held = 0;
+ motion_pause(3000);
+ night_mode(night);
+ }
} else {
- while (keepRunning) {
+ while (keepRunning && nightOn) {
bool state = false;
if (!gpio_read(app_config.ir_sensor_pin, &state))
if (!manual) night_mode(state);
- sleep(app_config.check_interval_s);
+ for (int i = 0; i < app_config.check_interval_s && keepRunning && nightOn; i++)
+ sleep(1);
}
}
usleep(10000);
gpio_deinit();
HAL_INFO("night", "Night mode thread is closing...\n");
- nightOn = 0;
}
int night_enable(void) {
@@ -101,13 +142,16 @@ int night_enable(void) {
size_t new_stacksize = 16 * 1024;
if (pthread_attr_setstacksize(&thread_attr, new_stacksize))
HAL_DANGER("night", "Error: Can't set stack size %zu\n", new_stacksize);
- pthread_create(&nightPid, &thread_attr, (void *(*)(void *))night_thread, NULL);
+ nightOn = 1;
+ if (pthread_create(&nightPid, &thread_attr, (void *(*)(void *))night_thread, NULL)) {
+ HAL_DANGER("night", "Can't create thread\n");
+ nightOn = 0;
+ ret = EXIT_FAILURE;
+ }
if (pthread_attr_setstacksize(&thread_attr, stacksize))
HAL_DANGER("night", "Error: Can't set stack size %zu\n", stacksize);
pthread_attr_destroy(&thread_attr);
- nightOn = 1;
-
return ret;
}
diff --git a/src/night.h b/src/night.h
index 0a029ec3..5bae755f 100644
--- a/src/night.h
+++ b/src/night.h
@@ -11,6 +11,7 @@
#include "gpio.h"
#include "hal/macros.h"
#include "media.h"
+#include "motion.h"
bool night_grayscale_on(void);
bool night_ircut_on(void);
diff --git a/src/onvif.c b/src/onvif.c
index 10fd5b54..f35ddd66 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);
@@ -18,11 +20,52 @@ 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;
+ 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;
+}
-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";
+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;
@@ -137,38 +180,18 @@ 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) {
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;
@@ -176,85 +199,37 @@ 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 = "";
- char *pos, *end, *start;
- char digest = 0, created[64], nonce[64], pass[64], user[64];
+ char created[64], nonce[64], pass[64], user[64];
+ const char *token = soap_tag(soap_data, "UsernameToken");
- if (!(start = strstr(soap_data, user_tag)) ||
- !(start += strlen(user_tag))) return false;
- if (!(end = strstr(start, ""))) 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) {
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
@@ -266,13 +241,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);
}
@@ -306,13 +275,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);
}
@@ -332,13 +295,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);
}
@@ -358,13 +315,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);
}
@@ -377,13 +328,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);
}
@@ -398,12 +343,162 @@ 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
+}
+
+#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[176] = "";
+
+ 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(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(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));
+ 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, onvif_event_clock(), 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");
+ }
+
+ 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);
+}
+
+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, onvif_event_clock(), seconds, &expires)) {
+ onvif_respond_unknown_sub(response, respLen);
+ return;
+ }
+
+ soap_datetime_format(now, current, sizeof(current));
+ 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,
+ 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 2aadf692..48c6925d 100644
--- a/src/onvif.h
+++ b/src/onvif.h
@@ -1,12 +1,15 @@
#pragma once
#include
+#include
#include
#include
#include
#include "app_config.h"
#include "network.h"
+#include "onvif_event.h"
+#include "soap.h"
int start_onvif(void);
void stop_onvif(void);
@@ -17,10 +20,28 @@ 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);
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);
diff --git a/src/onvif_event.c b/src/onvif_event.c
new file mode 100644
index 00000000..a5f481f1
--- /dev/null
+++ b/src/onvif_event.c
@@ -0,0 +1,187 @@
+#include "onvif_event.h"
+
+#include
+#include
+#include
+#include
+
+typedef struct {
+ int id;
+ time_t expires, lastSeen;
+ unsigned int generation;
+ int head, count, waiting;
+ 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;
+
+// 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);
+ 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];
+ 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, 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, and one with a pull
+ // waiting is in use however long ago that pull started
+ 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))
+ 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 = when, .initial = true, .state = motionState });
+ pthread_mutex_unlock(&eventMtx);
+
+ return id;
+}
+
+bool onvif_event_renew(int id, time_t now, int seconds, time_t *expires) {
+ event_lock();
+ onvif_event_sub *sub = find_sub(id);
+ if (sub) {
+ sub->expires = *expires = now + seconds;
+ sub->lastSeen = now;
+ }
+ pthread_mutex_unlock(&eventMtx);
+
+ return sub;
+}
+
+bool onvif_event_unsubscribe(int id) {
+ event_lock();
+ 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) {
+ event_lock();
+ 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, time_t now, int timeout_s, int limit,
+ onvif_event_msg *msgs, time_t *expires) {
+ struct timespec deadline;
+ clock_gettime(EVENT_WAIT_CLOCK, &deadline);
+ deadline.tv_sec += timeout_s;
+
+ event_lock();
+ 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) {
+ sub->lastSeen = now;
+ generation = ++sub->generation;
+ pthread_cond_broadcast(&eventCond);
+ }
+
+ 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--;
+ if ((sub = find_sub(id))) sub->waiting--;
+ }
+
+ 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) {
+ event_lock();
+ 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) {
+ event_lock();
+ 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..a67f74f2
--- /dev/null
+++ b/src/onvif_event.h
@@ -0,0 +1,26 @@
+#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;
+
+// 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);
+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/src/server.c b/src/server.c
index 56c06497..90cd3ac2 100644
--- a/src/server.c
+++ b/src/server.c
@@ -453,6 +453,46 @@ 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));
+ if (!task) {
+ send_http_error(client_fd, 500);
+ return;
+ }
+ 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);
@@ -619,6 +659,13 @@ void respond_request(http_request_t *req) {
}
char *action = onvif_extract_soap_action(req->payload);
+ if (!action) {
+ 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);
respLen = sizeof(response);
@@ -647,6 +694,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")) {
@@ -666,11 +717,46 @@ 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;
+ }
}
- 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;
}
diff --git a/src/soap.c b/src/soap.c
new file mode 100644
index 00000000..44bd5cd6
--- /dev/null
+++ b/src/soap.c
@@ -0,0 +1,179 @@
+#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, "")) || end - 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 long total = 0, value, unit;
+ bool clock = false;
+ char *end;
+
+ if (*text++ != 'P') return -1;
+
+ while (*text) {
+ if (*text == 'T') {
+ clock = true;
+ text++;
+ continue;
+ }
+ value = strtoll(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; 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;
+ }
+ // Checked before multiplying so a huge value cannot wrap around
+ if (value > (INT_MAX - total) / unit) return -1;
+ total += value * unit;
+ text = end + 1;
+ }
+
+ return total;
+}
+
+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;
+
+ tm.tm_year -= 1900;
+ tm.tm_mon -= 1;
+ return timegm(&tm) - offset;
+}
+
+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
new file mode 100644
index 00000000..c94083aa
--- /dev/null
+++ b/test/Makefile
@@ -0,0 +1,20 @@
+CFLAGS = -std=gnu99 -Wall -Wno-parentheses -I../src \
+ -DCLOCK_MONOTONIC_COARSE=CLOCK_MONOTONIC -Dmmap64=mmap
+TESTS = soap_test onvif_event_test motion_core_test
+
+all: $(TESTS)
+ @for t in $(TESTS); do ./$$t || exit 1; done
+
+clean:
+ rm -f $(TESTS)
+
+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 $@
+
+motion_core_test: motion_core_test.c ../src/motion_core.c
+ $(CC) $(CFLAGS) $^ -o $@
+
+.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)
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();
+}
diff --git a/test/onvif_event_test.c b/test/onvif_event_test.c
new file mode 100644
index 00000000..c0b96bfc
--- /dev/null
+++ b/test/onvif_event_test.c
@@ -0,0 +1,117 @@
+#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, 1000, 0, 8, msgs, &expires);
+ return NULL;
+}
+
+static void *late_notify(void *arg) {
+ usleep(200000);
+ onvif_motion_notify(true, 1010);
+ 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;
+ pthread_t thread;
+
+ 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
+ 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, 1000, 0, 1, msgs, &expires) == 1);
+ CHECK(!msgs[0].initial && msgs[0].state && msgs[0].time == 1001);
+ 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, 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, 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, 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, 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, 1000, 0, 8, msgs, &expires) == 1 && msgs[0].initial && msgs[0].state);
+ CHECK(onvif_event_unsubscribe(id));
+ CHECK(onvif_event_pull(id, 1000, 0, 8, msgs, &expires) == -1);
+ CHECK(!onvif_event_renew(id, 1100, 30, &expires));
+
+ // 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, 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, 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, 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);
+
+ // 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();
+}
diff --git a/test/soap_test.c b/test/soap_test.c
new file mode 100644
index 00000000..ad7fa8ff
--- /dev/null
+++ b/test/soap_test.c
@@ -0,0 +1,86 @@
+#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_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);
+ 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"));
+
+ 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();
+}