Repository navigation
D-Link DCS-2330L A: tested OV9712 profile, PWM2 IR control and Wi-Fi support findings #216
Description
Experimental D-Link DCS-2330L A board-support findings and reusable profile
Profile update — 29 October 2026: The optional daytime tuning and embedded dcs2330-image.c below now match the latest deployed settings. Daytime AE compensation was refined from 40 → 32 → 30, and automatic white-balance red/blue strengths from 128/128 to 80/48 using a phone photograph of the same scene as a visual reference. The latest night-only profile uses 100% IR, analog gain cap 16× and luminance 50; day mode restores 8× and luminance 25. The board-specific capture-mask fix, PWM2 routing and Wi-Fi findings are unchanged.
| Current optional setting | Day | Night |
|---|---|---|
| AE compensation | 30 | 64 |
| AWB red strength | 80 | 128 |
| AWB blue strength | 48 | 128 |
| Lens shading | Original geometry with the radial R/B correction described below | Original unmodified table |
| Analog / sensor digital / ISP gain caps | 8× / 1× / 1× | 16× / 1× / 1× |
| Image luminance | 25 | 50 (build default) |
| IR PWM2 duty | Off | 100% |
Automatic white balance remains enabled. The latest paired R/B adjustment reduced the blue/purple tint on neutral grey chairs and decking, but some cool tint remains. This is experimental tuning for one camera and scene, not colour-chart calibration or a universal OV9712 default. The exposure setting was accepted by the owner; the latest colour refinement has not yet received separate owner confirmation. The reference photograph and camera images are not shared here.
The updated helper compiled with -Wall -Wextra -Werror, and native parameter readback plus restarting the persistent image watcher verified AE 30, AWB 80/48, daytime 8×/1×/1× gain caps and the same lens table. IR was off in daytime, and the existing automatic fallback script's checksum was unchanged. These checks validate this refinement; they do not replace the earlier test limitations or constitute another complete night/boot/streaming test cycle. The updated source preserves night AE 64 and AWB 128/128 explicitly.
Additional night validation — 29 October 2026: Full IR output, a 16× analog gain limit and restoring the normal night luminance 50 made foreground chairs/decking clearer. The daytime luminance 25 had previously remained active at night. Three JPEG frames per controlled stage gave mean output luminance 5.54/255 before (IR60, gain8, luminance25), 9.15 with IR100, 12.31 after increasing the gain cap to 16×, and 24.86 with luminance50. These are scene-specific output pixel levels, not measurements of physical illumination. Exposure time stayed 33,168µs; digital and ISP gain remained 1×. Grain is more visible, and the distant garden remains dark. D-Link specifies about 5 m built-in IR range; this tuning does not establish performance beyond that range. No moving-subject/ghosting test was performed.
The updated watcher sets mode-specific gain and luminance through the loopback Majestic API, waits for native ISP gain readback, then applies the existing native exposure/colour helper. This avoids an asynchronous API/native-buffer race. It normalizes omitted default luminance to 50. The init service waits for the old watcher to exit before restarting. Verified day→night→automatic operation, native readback, video-service restart and watcher restart. Day restored gain8/luminance25/AE30/AWB80-48/IRoff; night restored gain16/luminance50/AE64/AWB128-128/original shading/IR100. The existing 600-second automatic fallback checksum remained unchanged. No firmware flash or full camera reboot was performed for this adjustment. These are optional scene settings, not new universal builder defaults.
Sharing the source and settings from a working conversion, in case these help a proper builder device overlay. This was tested on one hardware revision A camera. It is a board-support report for maintainer review, not a ready-to-flash image or a tested builder target. The investigation and implementation were AI-assisted; the findings below were checked on the physical camera.
Hardware and tested software
| Item | Observed |
|---|---|
| Camera | D-Link DCS-2330L, hardware A |
| SoC | Hi3518CV100 / ARMv5 |
| Sensor | OV9712, I2C, parallel digital-camera interface, 1280×720 |
| Memory | 128 MiB RAM, 32 MiB SPI NOR |
| Wi-Fi | RTL8188EU USB, ID 0bda:8179 |
| Vendor firmware initially installed | 1.14.03 |
| Running OpenIPC userspace | nightly 20261028-9a84dce |
| Kernel | Linux 3.0.8, custom build from OpenIPC/linux, hisilicon-hi3516cv100 |
| Wi-Fi driver | aircrack-ng/rtl8188eus, custom compatibility build |
| ISP ABI used by optional helper | Hi3518 MPP V1.0.B.0; do not assume compatibility with other SDK versions |
1. Board-specific OV9712 VI mask fixes green/purple highlight corruption
Use the existing ov9712_i2c_dc_720p.ini as the base, but create a DCS-2330L-specific copy with this change:
[vi_dev]
-Mask_0 =0x3FF00000
+Mask_0 =0xFFC00000Point isp.sensorConfig at that copy, e.g. /etc/sensors/dcs2330-ov9712.ini. Other sensor settings were retained: BGGR (Isp_Bayer=3), falling-edge clock (Clock_edge=1), 1280×720 and sensor rate 25. The encoded stream below uses 20 fps.
The mask selects bits 22–31 rather than 20–29. The D-Link avserver's selected 720p digital-camera configuration contains 0xffc00000; that supplied the hypothesis, and applying it removed the obvious green/purple highlight artifacts in snapshots and video. A different generic mask table also exists in avserver, so merely searching its binary for a mask is insufficient. Please keep this board-specific rather than changing all OV9712 cameras.
2. IR-cut, PWM2 and the missing board pin routing
- GPIO0 controls the IR-cut mode:
0is daytime,1is night. Majestic was configured withnightMode.irCutPin1: 0and automatic light monitoring. - The IR emitter is driven by PWM2, not PWM1. PWM1 is associated with the heater; the included controller never writes that channel.
- PWM2 alone did not illuminate the scene until the stock board routing was restored:
0x200f0134 = 0and0x200f0138 = 0. These correspond to GPIO0_5/SVB_PWM and GPIO0_6. The stock D-Link kernel's mux table has both entries zero; the bootloader left both at one in this installation. This association was inferred from stock code and confirmed by the optical response, not a schematic. - PWM base is
0x20130000; PWM2 period/duty/count/control are offsets0x40/0x44/0x48/0x4c. Period150at a 3 MHz PWM clock gives 20 kHz; count is10, control5enables and0disables. Disable/re-enable is needed to latch a new duty. Readback is at offsets0x50/0x54/0x58. - Initially tested operational setting: 35% at night, off in daytime. The current optional scene profile uses 100% at night, after an intermediate 60% setting; this is separate from a conservative builder default. The daemon follows GPIO0, rereads its config each second, and disables PWM2 on orderly stop. It leaves the stock-safe mux values zero on daemon stop; bounded
holdtests restore the saved mux and PWM state. Avoid running manualset/holdconcurrently with the daemon. control=0means the emitter is off even if the latched duty register still contains an earlier nonzero value.
Optical check after routing was corrected, with exposure otherwise under automatic control:
| PWM2 duty | Reported ISP digital gain | Reported luma |
|---|---|---|
| 0% | 4096 | 45 |
| 20% | 3784 | 52 |
| 50% | 3072 | 59 |
| 100% | 2232 | 64 |
This is evidence of lamp operation, not a brightness calibration. Before the mux correction, PWM changes did not produce this response. Source, config and the tested init script are included below.
3. Wi-Fi build findings
The generic image initially lacked the working USB Wi-Fi driver. A matching custom kernel plus 8188eu.ko restored Wi-Fi; a module built against a different kernel is not interchangeable. cfg80211 is required. In the driver revision above, rtw_cfg80211_create_vht_cap() and its 2.4 GHz call in rtw_cfg80211_init_wiphy() needed #if LINUX_VERSION_CODE >= KERNEL_VERSION(3, 6, 0) guards to compile against Linux 3.0.8. RTL8188EU is an 802.11n part; this avoids references to absent VHT API types/members.
Driver runtime options used:
options 8188eu rtw_power_mgnt=0 rtw_ips_mode=0 rtw_enusbss=0The driver was built with ARCH=arm, the OpenIPC arm-openipc-linux-musleabi- toolchain, CONFIG_RTL8188EU=m and CONFIG_CALIBRATE_TX_POWER_TO_MAX=y. This is a record of the tested custom build, not a complete upstream kernel/driver packaging patch. No Wi-Fi credentials are included.
4. Optional image tuning, separate from the hardware fixes
The attached Majestic YAML is a settings example to merge, not a replacement for someone else's entire configuration. The IQ profile retains the normal daytime 3DNR ladder and increases monochrome/night filtering. Analog gain is capped at 8× in day mode and 16× at night; sensor digital and ISP digital gain stay at 1×. Luminance is 25 by day and 50 at night, with saturation 40. The larger night gain cap trades increased grain for visibility. These values were judged against this camera's outdoor scene; they are not factory calibration for every unit.
In an eight-frame static-area comparison, adjacent-frame temporal RMS decreased from 13.73 to 5.13 (about 63%) with similar mean brightness. This was not a controlled moving-subject test; stronger temporal filtering can soften motion. The remaining low-light background noise is not eliminated.
The optional native image helper below applies daytime AE compensation 30 and nighttime 64, while retaining gain limits. Current daytime AWB red/blue strengths are 80/48; nighttime remains 128/128. The previous paired setting was 128/128. An earlier red-only reduction produced a blue cast; the current paired R/B adjustment was guided by neutral grey surfaces in the reference photograph. The latest settings and validation are summarized at the top of this issue. The helper's byte offsets are specific to the tested MPP ABI: AE compensation byte 48 of GetAEAttrEx, AWB strengths bytes 38/39 of GetAWBAttr. It preserves getter buffers and checks readback. The local mmap compatibility wrapper is required for these legacy 32-bit SDK libraries; without it SDK mapping calls failed with EINVAL.
A residual central red tint was still present with our IR emitter forced off. An optional daytime radial lens-shading adjustment reduced the red channel by up to 15% and blue by up to 6% at the center, tapering to zero at the edge; green was unchanged. This is experimental scene tuning, not a proven optical calibration or a recommended global sensor default. Night uses the unmodified original table. The native table has 397 little-endian uint16 values (794 bytes): six leading geometry values, 129 R coefficients, 129 G, 129 B, three trailing values, and node count 129. For node i=0..128, multiply R by 1-0.15*(1-i/128)^2 and B by 1-0.06*(1-i/128)^2, round to the nearest integer; retain all other fields.
The image helper expects the camera's own baseline table obtained via HI_MPI_ISP_GetShadingTable as /etc/sensors/iq/dcs2330-night-shading.bin, plus its derived daytime table as dcs2330-day-shading.bin. These unit-specific binary tables and SDK/vendor binaries are deliberately not redistributed. Do not run apply without obtaining and preserving that baseline. The helper/watch scripts are shared for review and adaptation, not as a universal installer. The watcher uses a fresh helper process after mode changes or a Majestic restart to avoid stale ISP mappings.
Recovery and validation limits
The tested installation retained its existing 600-second automatic fallback and the stock bootloader's dynamic recovery selector (loadbootaddr/loadbootsize). None of the profile changes modify flash partitions, bootloader environment or that fallback. The init scripts below check /etc/dcs2330l-committed, which is a marker from that custom installation; a proper builder port must deliberately integrate its own startup/recovery policy rather than blindly copying this dependency. The custom installation's flash layout is not a generic OpenIPC flashing layout, so no flashing commands or image are offered here. A fallback that restores network access is not evidence that every stock web/UI partition has been restored.
Tested on the camera: Wi-Fi connectivity, H.264 video, the capture-mask correction, IR lamp response, day/night switching, native-helper readback, and service recovery after Majestic restart. Audio, heater control and SD recording have not been validated as part of this profile. A complete builder target has not been built or tested. The C sources also compile with -Wall -Wextra -Werror using the matching ARM toolchain, and the shell scripts pass syntax checks.
No credentials, SSIDs, private addresses, MAC addresses, property images or firmware/flash dumps are included. The source below is the reusable part of the local implementation, provided here for upstream review. Please feel free to extract the board-specific settings into the appropriate overlay; the scene-dependent tuning should remain optional.
Source and configuration
The controller compiles with arm-openipc-linux-musleabi-gcc -Os -Wall -Wextra -Werror -o dcs2330-ir dcs2330-ir.c. The optional image helper additionally needs -Wl,--export-dynamic -ldl so its legacy mmap wrapper is visible to the loaded SDK libraries. Install paths referenced by the scripts are /usr/sbin/dcs2330-ir, /usr/sbin/dcs2330-image, /usr/sbin/dcs2330-image-watch, /etc/dcs2330-ir.conf, and /etc/init.d/S96dcs2330-ir / S97dcs2330-image. Preserve the installation's recovery policy when adapting these.
dcs2330-ir.c
#define _POSIX_C_SOURCE 200809L
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>
#include <fcntl.h>
#include <unistd.h>
#include <signal.h>
#include <sys/mman.h>
#include <sys/file.h>
#include <time.h>
/* DCS-2330L stock server uses PWM2, period 150 at 3 MHz. PWM1 is its heater. */
#define BASE 0x20130000
#define CFG "/etc/dcs2330-ir.conf"
static volatile uint32_t *p, *mux;
static uint32_t saved_mux5, saved_mux6;
static volatile sig_atomic_t stopping;
static void stop_signal(int s) { (void)s; stopping=1; }
static void delay_ms(long ms) { struct timespec t={ms/1000,(ms%1000)*1000000}; nanosleep(&t,0); }
static int integer(const char *s,int lo,int hi) { char *end;errno=0;long n=strtol(s,&end,10);return errno||!*s||*end||n<lo||n>hi?-1:(int)n; }
static int open_pwm(void) {
int fd=open("/dev/mem",O_RDWR|O_SYNC);if(fd<0){perror("/dev/mem");return -1;}
void *v=mmap(0,4096,PROT_READ|PROT_WRITE,MAP_SHARED,fd,BASE);close(fd);
if(v==MAP_FAILED){perror("mmap PWM");return -1;}p=v;
fd=open("/dev/mem",O_RDWR|O_SYNC);if(fd<0)return -1;
v=mmap(0,4096,PROT_READ|PROT_WRITE,MAP_SHARED,fd,0x200f0000);close(fd);
if(v==MAP_FAILED){perror("mmap mux");return -1;}mux=v;
saved_mux5=mux[0x134/4];saved_mux6=mux[0x138/4];return 0;
}
static void route_stock(void) {
/* Exact D-Link kernel mux table c03cea0c: index 77=0, index 78=0.
* PWM2 reaches the IR LED through the GPIO0_5/SVB_PWM pad.
* GPIO0_6 is an input in the same stock board GPIO table. */
mux[0x134/4]=0;mux[0x138/4]=0;__sync_synchronize();
}
static void restore_route(void) {mux[0x134/4]=saved_mux5;mux[0x138/4]=saved_mux6;__sync_synchronize();}
static int apply(int percent) {
/* Only PWM2 offsets 0x40..0x4c. The disable/re-enable latches duty changes. */
p[0x4c/4]=0;__sync_synchronize();delay_ms(2);
p[0x40/4]=150;p[0x44/4]=(150*percent+50)/100;p[0x48/4]=10;
__sync_synchronize();if(percent)p[0x4c/4]=5;__sync_synchronize();delay_ms(2);
if(percent&&(p[0x50/4]!=150||p[0x54/4]!=(uint32_t)((150*percent+50)/100)||(p[0x58/4]&0xc00)!=0xc00)) {
p[0x4c/4]=0;fprintf(stderr,"PWM2 did not latch requested duty; disabled\n");return -1;
}return 0;
}
static void status(void) { printf("{\"channel\":2,\"period\":%u,\"duty\":%u,\"control\":%u,\"active_period\":%u,\"active_duty\":%u,\"state\":%u}\n",p[16],p[17],p[19],p[20],p[21],p[22]); }
static void write_status(int percent) {
FILE *f=fopen("/tmp/dcs2330-ir-status.json.new","w");if(!f)return;
fprintf(f,"{\"channel\":2,\"brightness\":%d,\"control\":%u,\"active_period\":%u,\"active_duty\":%u,\"mux5\":%u,\"mux6\":%u}\n",percent,p[19],p[20],p[21],mux[77],mux[78]);fclose(f);rename("/tmp/dcs2330-ir-status.json.new","/tmp/dcs2330-ir-status.json");
}
static int read_config(int *level) {
FILE *f=fopen(CFG,"r");if(!f)return 0;char line[128];int enabled=0;*level=20;
while(fgets(line,sizeof(line),f)){int n;if(sscanf(line,"enabled=%d",&n)==1)enabled=n==1;else if(sscanf(line,"brightness=%d",&n)==1)*level=n>=0&&n<=100?n:0;}fclose(f);return enabled;
}
static int night(void) {
FILE *f=fopen("/sys/class/gpio/gpio0/value","r");if(!f)return 0;int c=fgetc(f);fclose(f);return c=='1';
}
int main(int argc,char **argv) {
if(argc<2){fprintf(stderr,"usage: dcs2330-ir status | set PERCENT | hold PERCENT SECONDS | run\n");return 2;}
int level=-1,seconds=0,daemon=!strcmp(argv[1],"run");
if(!strcmp(argv[1],"set")||!strcmp(argv[1],"hold")) {
if(argc<3||(level=integer(argv[2],0,100))<0)return 2;
if(!strcmp(argv[1],"hold")&&(argc!=4||(seconds=integer(argv[3],1,60))<0))return 2;
}else if(!daemon&&strcmp(argv[1],"status"))return 2;
if(open_pwm())return 1;
if(!strcmp(argv[1],"status")){status();return 0;}
signal(SIGTERM,stop_signal);signal(SIGINT,stop_signal);signal(SIGHUP,stop_signal);
if(daemon) {
int lock=open("/var/run/dcs2330-ir.lock",O_RDWR|O_CREAT,0600);if(lock<0||flock(lock,LOCK_EX|LOCK_NB)){fprintf(stderr,"IR controller already running or lock unavailable\n");return 1;}
route_stock();
int last=-1;
while(!stopping) {int wanted=read_config(&level)&&night()?level:0;
if(wanted!=last||p[16]!=150||p[17]!=(uint32_t)((150*wanted+50)/100)||p[19]!=(wanted?5u:0u)) {
if(apply(wanted))break;
last=wanted;write_status(last);
}delay_ms(1000);
}apply(0);write_status(0);close(lock);return 0;
}
uint32_t saved[4];for(int i=0;i<4;i++)saved[i]=p[16+i];
route_stock();
if(apply(level)){restore_route();return 1;}
status();fflush(stdout);
if(seconds){for(int i=0;i<seconds*10&&!stopping;i++)delay_ms(100);p[19]=0;__sync_synchronize();delay_ms(2);for(int i=0;i<3;i++)p[16+i]=saved[i];p[19]=saved[3];__sync_synchronize();restore_route();}
return 0;
}S96dcs2330-ir
#!/bin/sh
DAEMON=/usr/sbin/dcs2330-ir
PIDFILE=/var/run/dcs2330-ir.pid
case "$1" in
start)
[ -x "$DAEMON" ] || exit 1
[ -f /etc/dcs2330l-committed ] || exit 0
start-stop-daemon -S -q -b -m -p "$PIDFILE" -x "$DAEMON" -- run
;;
stop)
start-stop-daemon -K -q -p "$PIDFILE" -x "$DAEMON" 2>/dev/null
n=0
while [ -e "$PIDFILE" ] && kill -0 "$(cat "$PIDFILE")" 2>/dev/null && [ "$n" -lt 5 ]; do
sleep 1
n=$((n+1))
done
rm -f "$PIDFILE"
;;
restart)
"$0" stop
"$0" start
;;
status)
"$DAEMON" status
;;
*)
echo "Usage: $0 {start|stop|restart|status}"
exit 1
;;
esacdcs2330-ir.conf
# DCS-2330L A: follow the confirmed GPIO0 day/night filter control.
# Brightness is a percentage, 0..100. PWM carrier matches stock D-Link.
enabled=1
brightness=100dcs2330-night.ini
; DCS-2330L OV9712. Preserve default daylight processing.
[3dnr]
SF = 20, 20, 20, 20, 32, 48, 64, 128;
TF = 8, 8, 8, 8, 12, 16, 16, 32;
SF_M = 5, 6, 8, 16, 32, 48, 64, 128;
TF_M = 5, 6, 8, 8, 8, 12, 16, 32;
; Stronger night filtering, bounded by the stock profile's maximums.
[ir_3dnr]
SF = 96, 96, 96, 96, 96, 96, 112, 128;
TF = 32, 32, 32, 32, 32, 32, 32, 32;
SF_M = 64, 64, 64, 64, 64, 80, 96, 112;
TF_M = 24, 24, 24, 24, 24, 24, 24, 32;majestic-example.yaml
# Daytime starting example: the watcher changes aGain/luminance for each mode.
# Managed by majestic. Comments and formatting are removed when settings change
# via the web UI or API. Only settings that differ from this build's defaults
# are kept, so a default that changes in a later version still reaches you.
isp:
ispGain: 1
dGain: 1
aGain: 8
iqProfile: /etc/sensors/iq/dcs2330-night.ini
sensorConfig: /etc/sensors/dcs2330-ov9712.ini
image:
saturation: 40
luminance: 25
video0:
fps: 20
bitrate: 3072
jpeg:
qfactor: 85
osd: false
osd:
enabled: true
template: "%Y-%m-%d %H:%M:%S"
size: "0.8"
anchor: top-left
offsetX: "16"
offsetY: "16"
nightMode:
lightMonitor: true
autoNightGain: 4
irCutPin1: 0dcs2330-image.c
/* DCS-2330L / Hi3518CV100 MPP 1.0.B: daytime exposure and lens tint profile.
* API byte offsets were verified against this firmware's SDK and readback.
* Whole getter buffers are preserved so gain caps and metering stay intact. */
#include <stdio.h>
#include <stdint.h>
#include <stdlib.h>
#include <string.h>
#include <dlfcn.h>
#include <unistd.h>
#include <sys/syscall.h>
#include <errno.h>
#include <time.h>
void *mmap(void *a,size_t n,int p,int f,int fd,uint32_t o) {
/* Vendor libraries call the legacy six-word mmap ABI. */
if(o&4095){errno=EINVAL;return (void*)-1;}
return (void*)syscall(SYS_mmap2,a,n,p,f,fd,o>>12);
}
typedef int (*Api)(void *);
static Api symbol(void *lib,const char *name) {
Api fn=(Api)dlsym(lib,name);
if(!fn)fprintf(stderr,"Missing SDK function: %s\n",name);
return fn;
}
int main(int argc,char **argv) {
int apply=argc==3&&!strcmp(argv[1],"apply");
int night=apply&&!strcmp(argv[2],"night");
if(!(argc==2&&!strcmp(argv[1],"status"))&&!(apply&&(night||!strcmp(argv[2],"day"))))return 2;
void *mpi=dlopen("libmpi.so",RTLD_LAZY|RTLD_GLOBAL);
void *isp=dlopen("libisp.so",RTLD_LAZY|RTLD_GLOBAL);
void *ae=dlopen("lib_hiae.so",RTLD_LAZY|RTLD_GLOBAL);
void *awb=dlopen("lib_hiawb.so",RTLD_LAZY|RTLD_GLOBAL);
if(!mpi||!isp||!ae||!awb){fputs("SDK load failed\n",stderr);return 3;}
Api getae=symbol(ae,"HI_MPI_ISP_GetAEAttrEx"),setae=symbol(ae,"HI_MPI_ISP_SetAEAttrEx");
Api getwb=symbol(awb,"HI_MPI_ISP_GetAWBAttr"),setwb=symbol(awb,"HI_MPI_ISP_SetAWBAttr");
Api getsh=symbol(isp,"HI_MPI_ISP_GetShadingTable"),setsh=symbol(isp,"HI_MPI_ISP_SetShadingTable");
Api geten=symbol(isp,"HI_MPI_ISP_GetShadingAttr"),seten=symbol(isp,"HI_MPI_ISP_SetShadingAttr");
if(!getae||!setae||!getwb||!setwb||!getsh||!setsh||!geten||!seten)return 4;
uint32_t abuf[1024]={0},wbuf[1024]={0},sbuf[1024]={0};
uint8_t *a=(void*)abuf,*w=(void*)wbuf;uint16_t *sh=(void*)sbuf;int enabled=0;
if(getae(abuf)||getwb(wbuf)||getsh(sbuf)||geten(&enabled))return 5;
/* Refuse to modify an uninitialized ISP or an incompatible SDK. */
if(sh[396]!=129||sh[0]>1280||sh[1]>720||abuf[3]<1024||abuf[3]>32768||abuf[5]<1024||abuf[5]>32768||abuf[7]<1024||abuf[7]>32768)return 6;
if(apply) {
char path[128];snprintf(path,sizeof(path),"/etc/sensors/iq/dcs2330-%s-shading.bin",night?"night":"day");
FILE *f=fopen(path,"rb");if(!f)return 7;
uint32_t desired[1024]={0};size_t n=fread(desired,1,794,f);int extra=fgetc(f);fclose(f);
if(n!=794||extra!=EOF||((uint16_t*)desired)[396]!=129)return 8;
unsigned dg=abuf[3],ag=abuf[5],ig=abuf[7];
a[48]=night?64:30;w[38]=night?128:80;w[39]=night?128:48;
enabled=1;
if(setsh(desired)||seten(&enabled)||setae(abuf)||setwb(wbuf))return 9;
if(getae(abuf)||getwb(wbuf)||getsh(sbuf)||geten(&enabled))return 10;
if(a[48]!=(night?64:30)||w[38]!=(night?128:80)||w[39]!=(night?128:48)||!enabled||abuf[3]!=dg||abuf[5]!=ag||abuf[7]!=ig||memcmp(desired,sbuf,794))return 11;
}
char json[512];snprintf(json,sizeof(json),"{\"updated_epoch\":%ld,\"mode\":\"%s\",\"exposure_compensation\":%u,\"awb_red_strength\":%u,\"awb_blue_strength\":%u,\"shading_enabled\":%d,\"shading_center_rgb\":[%u,%u,%u],\"gain_caps\":[%u,%u,%u]}\n",(long)time(NULL),apply?(night?"night":"day"):"current",a[48],w[38],w[39],enabled,sh[6],sh[135],sh[264],abuf[5],abuf[3],abuf[7]);
fputs(json,stdout);
if(apply){FILE *f=fopen("/tmp/dcs2330-image-status.json.new","w");if(!f)return 12;fputs(json,f);if(fclose(f)||rename("/tmp/dcs2330-image-status.json.new","/tmp/dcs2330-image-status.json"))return 13;}
return 0;
}dcs2330-image-watch
#!/bin/sh
# Each application gets fresh mappings, including after Majestic recreates the ISP.
trap 'exit 0' TERM INT HUP
last=
apply_profile() {
case "$1" in
day) gain=8; luminance=25 ;;
night) gain=16; luminance=50 ;;
*) return 1 ;;
esac
# Keep the accepted daytime picture separate from the low-light settings.
# Only save changed values; a normal restart should not rewrite the flash.
current_gain=$(cli -g .isp.aGain 2>/dev/null)
current_luminance=$(cli -g .image.luminance 2>/dev/null)
# Majestic omits the default luminance (50) from its saved YAML.
case "$current_luminance" in ''|null) current_luminance=50 ;; esac
if [ "$current_gain" != "$gain" ]; then
curl --max-time 8 -fsS "http://127.0.0.1/api/v1/set?isp.aGain=$gain" >/dev/null || return 1
fi
if [ "$current_luminance" != "$luminance" ]; then
curl --max-time 8 -fsS "http://127.0.0.1/api/v1/set?image.luminance=$luminance" >/dev/null || return 1
fi
# Wait for the requested gain to reach the ISP before preserving its AE buffer.
# Otherwise an asynchronous settings update could race the native helper.
expected="\"gain_caps\":[$((gain * 1024)),1024,1024]"
n=0
while [ "$n" -lt 5 ]; do
status=$(/usr/sbin/dcs2330-image status) || status=
printf '%s\n' "$status" | grep -Fq "$expected" && break
n=$((n+1))
sleep 1
done
[ "$n" -lt 5 ] || return 1
/usr/sbin/dcs2330-image apply "$1"
}
while :; do
pid=$(pidof majestic)
night=$(cat /sys/class/gpio/gpio0/value 2>/dev/null)
case "$pid:$night" in
*' '*|:*) sleep 2; continue ;;
esac
case "$night" in 0) mode=day ;; 1) mode=night ;; *) sleep 2; continue ;; esac
key="$pid:$night"
if [ "$key" != "$last" ]; then
sleep 2
if [ "$(pidof majestic):$(cat /sys/class/gpio/gpio0/value 2>/dev/null)" = "$key" ] &&
apply_profile "$mode" > /tmp/dcs2330-image-apply.log 2>&1; then
last=$key
fi
fi
sleep 2
doneS97dcs2330-image
#!/bin/sh
PIDFILE=/var/run/dcs2330-image-watch.pid
WATCH=/usr/sbin/dcs2330-image-watch
case "$1" in
start)
[ -f /etc/dcs2330l-committed ] || exit 0
[ -x /usr/sbin/dcs2330-image ] && [ -x "$WATCH" ] || exit 1
start-stop-daemon -S -q -b -m -p "$PIDFILE" -x /bin/sh -- "$WATCH"
;;
stop)
if [ -r "$PIDFILE" ]; then
pid=$(cat "$PIDFILE")
if [ -r "/proc/$pid/cmdline" ] && tr '\000' ' ' < "/proc/$pid/cmdline" | grep -q "$WATCH"; then
kill "$pid"
n=0
while kill -0 "$pid" 2>/dev/null && [ "$n" -lt 10 ]; do
sleep 1
n=$((n+1))
done
# Do not start another watcher while an old settings request is running.
kill -0 "$pid" 2>/dev/null && exit 1
fi
rm -f "$PIDFILE"
fi
;;
restart) "$0" stop && "$0" start ;;
status) /usr/sbin/dcs2330-image status ;;
*) echo "Usage: $0 {start|stop|restart|status}"; exit 1 ;;
esac
Activity
openipc-ai commentedon Oct 29, 2026
Thanks for the detailed report. It's now a builder device: #217,
hi3518cv100_lite_dlink-dcs-2330l. It builds on CI, but nobody has flashed it on a camera yet, so we'll hold the merge until you've tried it.What went in from your findings:
>= 3.6guards. They're merged upstream as rtl8188eus-openipc: build against the 3.0 kernels firmware#2726, so a stock build now compiles it for 3.0.8. Your three power-saving options are passed when the driver is loaded.Mask_0 = 0xFFC00000. Other OV9712 cameras keep the stock file./etc/init.d/S96irled, a shell version of yourdcs2330-ir. It sets0x200f0134/0x200f0138to 0, then drives PWM2 at 20 kHz while GPIO0 is 1. Brightness defaults to 35% and can be changed withfw_setenv irled_duty N. PWM1 (the heater) is never touched.dcs2330-imagehelper and the scene-specific IQ/gain tuning. As you suggested, they stay optional local tweaks.PR builds don't publish images, so build it from the branch:
git checkout hi3518cv100-dlink-dcs-2330l && ./builder.sh hi3518cv100_lite_dlink-dcs-2330l. The image lands inarchive/hi3518cv100_lite_dlink-dcs-2330l/<timestamp>/. If that's a hassle, say so and we'll attach one here. If you can, please test these four things:loadbootaddrselector)? Please keep your recovery route ready first.wlandev rtl8188eu-dlink-dcs-2330l?fw_setenv irled_duty 20followed by/etc/init.d/S96irled restartshould visibly change the brightness.Your results on #217, including any problems, will decide whether it merges as is.
garnser commentedon Oct 29, 2026
Thanks for creating #217 and merging the Wi-Fi compatibility fix. I have posted hardware component-test results and the remaining brightness-validation fix. The new IR script passed 35%/20% restart, stop, and GPIO day/night tests on the existing installation; the original controller, automatic monitoring and fallback were restored afterwards.
The complete PR image has not been flashed. The reply documents the stock bootloader/custom partition layout and asks for the offered prebuilt kernel/rootfs components, checksums and source revisions so fallback-preserving testing can be planned. The component tests should not be taken as validation of a standard 8M installation path. No photos, credentials or private network details were posted.
openipc-ai commentedon Oct 29, 2026
Thanks. Answered on #217: #217 (comment)
duty()replacement is in as75f060d./initerases arootfs_datapartition it can't mount, and first boot writes the bootloader environment unless/etc/custom.okexists.The hardware-install status stays pending, as you asked.