diff --git a/db/zm_create.sql.in b/db/zm_create.sql.in index cb9c5fed0..ef3befa91 100644 --- a/db/zm_create.sql.in +++ b/db/zm_create.sql.in @@ -132,6 +132,7 @@ CREATE TABLE `Controls` ( `MinWhiteSpeed` int(10) unsigned default NULL, `MaxWhiteSpeed` int(10) unsigned default NULL, `CanLight` tinyint(3) unsigned NOT NULL default '0', + `CanIndicatorLight` tinyint(3) unsigned NOT NULL default '0', `HasPresets` tinyint(3) unsigned NOT NULL default '0', `NumPresets` tinyint(3) unsigned NOT NULL default '0', `HasHomePreset` tinyint(3) unsigned NOT NULL default '0', @@ -1212,9 +1213,10 @@ INSERT INTO `Controls` VALUES (NULL,'PSIA','Remote','PSIA',0,0,0,0,1,0,0,1,0,0,0 INSERT INTO `Controls` VALUES (NULL,'Dahua','Ffmpeg','Dahua',0,0,1,1,1,0,0,1,0,0,0,0,0,0,0,0,1,0,0,1,0,0,0,0,0,0,0,0,1,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,20,1,1,1,1,0,0,1,0,1,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0); INSERT INTO `Controls` VALUES (NULL,'FOSCAMR2C','Libvlc','FOSCAMR2C',1,1,1,0,0,0,0,0,0,NULL,NULL,NULL,NULL,0,NULL,NULL,0,0,0,0,0,NULL,NULL,NULL,NULL,0,NULL,NULL,0,0,0,0,0,NULL,NULL,NULL,NULL,0,NULL,NULL,0,0,0,0,0,NULL,NULL,NULL,NULL,0,NULL,NULL,0,0,0,0,0,NULL,NULL,NULL,NULL,0,NULL,NULL,1,12,0,1,1,1,0,0,0,1,1,NULL,NULL,NULL,NULL,1,0,4,0,NULL,1,NULL,NULL,NULL,NULL,1,0,4,0,NULL,0,0); INSERT INTO `Controls` VALUES (NULL,'Amcrest HTTP API','Ffmpeg','Amcrest_HTTP',0,0,1,0,1,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,5,0,0,1,0,0,0,0,1,1,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,5); -INSERT INTO `Controls` (`Name`,`Type`,`Protocol`,`CanReset`,`CanReboot`,`CanZoom`,`CanZoomCon`,`HasPresets`,`NumPresets`,`HasHomePreset`,`CanSetPresets`,`CanMove`,`CanMoveDiag`,`CanMoveCon`,`CanPan`,`CanTilt`) VALUES ('Dahua/Amcrest RPC','Ffmpeg','Dahua_RPC',1,1,1,1,1,25,1,1,1,1,1,1,1); -INSERT INTO `Controls` (`Name`,`Type`,`Protocol`,`CanReset`,`CanReboot`,`CanZoom`,`CanZoomCon`,`HasPresets`,`NumPresets`,`HasHomePreset`,`CanSetPresets`,`CanMove`,`CanMoveDiag`,`CanMoveCon`,`CanPan`,`CanTilt`) VALUES ('Amcrest ASH21-B RPC','Ffmpeg','Dahua_RPC',1,1,0,0,0,0,0,0,1,0,1,1,1); -INSERT INTO `Controls` (`Name`,`Type`,`Protocol`,`CanReset`,`CanReboot`,`CanZoom`,`CanZoomCon`,`HasPresets`,`NumPresets`,`HasHomePreset`,`CanSetPresets`,`CanMove`,`CanMoveDiag`,`CanMoveCon`,`CanPan`,`CanTilt`,`CanLight`) VALUES ('Amcrest ADC2W RPC','Ffmpeg','Dahua_RPC',1,1,0,0,0,0,0,0,0,0,0,0,0,1); +INSERT INTO `Controls` (`Name`,`Type`,`Protocol`,`CanReset`,`CanReboot`,`CanZoom`,`CanZoomCon`,`HasPresets`,`NumPresets`,`HasHomePreset`,`CanSetPresets`,`CanMove`,`CanMoveDiag`,`CanMoveCon`,`CanPan`,`CanTilt`,`CanLight`,`CanIndicatorLight`) VALUES ('Dahua/Amcrest RPC','Ffmpeg','Dahua_RPC',1,1,1,1,1,25,1,1,1,1,1,1,1,1,1); +INSERT INTO `Controls` (`Name`,`Type`,`Protocol`,`CanReset`,`CanReboot`,`CanZoom`,`CanZoomCon`,`HasPresets`,`NumPresets`,`HasHomePreset`,`CanSetPresets`,`CanMove`,`CanMoveDiag`,`CanMoveCon`,`CanPan`,`CanTilt`,`CanIndicatorLight`) VALUES ('Amcrest ASH21-B RPC','Ffmpeg','Dahua_RPC',1,1,0,0,0,0,0,0,1,0,1,1,1,1); +INSERT INTO `Controls` (`Name`,`Type`,`Protocol`,`CanReset`,`CanReboot`,`CanZoom`,`CanZoomCon`,`HasPresets`,`NumPresets`,`HasHomePreset`,`CanSetPresets`,`CanMove`,`CanMoveDiag`,`CanMoveCon`,`CanPan`,`CanTilt`,`CanLight`,`CanIndicatorLight`) VALUES ('Amcrest ADC2W RPC','Ffmpeg','Dahua_RPC',1,1,0,0,0,0,0,0,0,0,0,0,0,1,1); +INSERT INTO `Controls` (`Name`,`Type`,`Protocol`,`CanReset`,`CanReboot`,`CanIndicatorLight`) VALUES ('Amcrest ASH42-B RPC','Ffmpeg','Dahua_RPC',1,1,1); INSERT INTO `Controls` VALUES (NULL,'ONVIF','Ffmpeg','ONVIF',0,0,1,1,1,0,0,0,1,NULL,NULL,NULL,NULL,0,NULL,NULL,0,0,0,0,0,NULL,NULL,NULL,NULL,0,NULL,NULL,1,0,1,0,0,0,100,1,1,0,NULL,NULL,0,0,0,0,0,NULL,NULL,NULL,NULL,0,NULL,NULL,1,0,1,0,0,0,100,1,1,0,NULL,NULL,1,20,1,1,1,1,1,0,1,1,1,NULL,NULL,NULL,NULL,0,NULL,NULL,0,NULL,1,NULL,NULL,NULL,NULL,0,NULL,NULL,0,NULL,0,0); -- diff --git a/db/zm_update-1.39.14.sql b/db/zm_update-1.39.14.sql new file mode 100644 index 000000000..e82249c88 --- /dev/null +++ b/db/zm_update-1.39.14.sql @@ -0,0 +1,39 @@ +-- +-- Add CanIndicatorLight capability column to Controls (camera indicator LED on/off). +-- +SET @s = (SELECT IF( + (SELECT COUNT(*) FROM INFORMATION_SCHEMA.COLUMNS + WHERE table_schema = DATABASE() + AND table_name = 'Controls' + AND column_name = 'CanIndicatorLight') > 0, + 'SELECT ''Column CanIndicatorLight already exists''', + 'ALTER TABLE `Controls` ADD COLUMN `CanIndicatorLight` tinyint(3) unsigned NOT NULL default ''0'' AFTER `CanLight`' +)); +PREPARE stmt FROM @s; +EXECUTE stmt; +DEALLOCATE PREPARE stmt; + +-- +-- Add model-specific Controls entry for the Amcrest ASH42-B. +-- This model has no PTZ; its indicator LED is controlled via LightGlobal config +-- (configManager.setConfig/getConfig, verified live). Reboot via magicBox.reboot. +-- +INSERT INTO `Controls` + (`Name`,`Type`,`Protocol`,`CanReset`,`CanReboot`,`CanIndicatorLight`) +SELECT 'Amcrest ASH42-B RPC','Ffmpeg','Dahua_RPC',1,1,1 + FROM DUAL + WHERE NOT EXISTS (SELECT 1 FROM `Controls` WHERE `Name`='Amcrest ASH42-B RPC'); + +-- +-- The ASH21-B and ADC2W also control their indicator LED via LightGlobal +-- (verified live on both models), so enable the capability on their entries. +-- +UPDATE `Controls` SET `CanIndicatorLight`=1 + WHERE `Name` IN ('Amcrest ASH21-B RPC','Amcrest ADC2W RPC'); + +-- +-- The generic entry keeps every capability enabled for testing new cameras. +-- (CanLight column was added by 1.39.12, CanIndicatorLight above.) +-- +UPDATE `Controls` SET `CanLight`=1, `CanIndicatorLight`=1 + WHERE `Name`='Dahua/Amcrest RPC'; diff --git a/scripts/ZoneMinder/lib/ZoneMinder/Control/Dahua_RPC.pm b/scripts/ZoneMinder/lib/ZoneMinder/Control/Dahua_RPC.pm index 736e2d1ea..0864a7455 100644 --- a/scripts/ZoneMinder/lib/ZoneMinder/Control/Dahua_RPC.pm +++ b/scripts/ZoneMinder/lib/ZoneMinder/Control/Dahua_RPC.pm @@ -67,7 +67,10 @@ sub open { $self->{state} = 'closed'; return undef; } - my $port = $self->{port} || 80; + # RPC2 is always on the camera's web port (default 80), not the stream port. + # parse_Path() derives {port} from the RTSP URL (typically 554), which is + # wrong for RPC. Only honour {port} if it looks like a web port. + my $port = ($self->{port} && $self->{port} != 554) ? $self->{port} : 80; # JSON-RPC auth is in-band; do not put userinfo in the URL. $self->{RPCBase} = 'http://'.$self->{host}.':'.$port.'/'; $self->{rpc_id} = 0; @@ -299,15 +302,51 @@ sub coaxial_white { sub lightOn { my $self = shift; $self->coaxial_white(1); } sub lightOff { my $self = shift; $self->coaxial_white(2); } +# Indicator LED (camera status light) via LightGlobal config. +# Used on models like the ASH42-B where LightGlobal.Enable controls the indicator. +sub indicatorLightOn { my $self = shift; $self->set_config({ name => 'LightGlobal', table => [ { Enable => JSON::MaybeXS::true } ] }); } +sub indicatorLightOff { my $self = shift; $self->set_config({ name => 'LightGlobal', table => [ { Enable => JSON::MaybeXS::false } ] }); } + +sub indicatorLightStatus { + my $self = shift; + my $r = $self->rpc_call('configManager.getConfig', { name => 'LightGlobal' }); + if (!$r || !$r->{result}) { + if ($r && $self->session_expired($r->{error})) { + return { Enable => undef } if !$self->login(); + $r = $self->rpc_call('configManager.getConfig', { name => 'LightGlobal' }); + } + } + my $enable = ($r and $r->{result} and $r->{params} and $r->{params}{table}) + ? $r->{params}{table}[0]{Enable} : undef; + return { Enable => (defined $enable ? ($enable ? 'On' : 'Off') : undef) }; +} + # Query the current white-light state. Returns { WhiteLight => 'On'|'Off'|undef }. # Used by the status-aware single-button toggle in the web UI. sub lightStatus { my $self = shift; my $r = $self->rpc_call('CoaxialControlIO.getStatus', { channel => 0 }); + if (!$r || !$r->{result}) { + if ($r && $self->session_expired($r->{error})) { + return { WhiteLight => undef } if !$self->login(); + $r = $self->rpc_call('CoaxialControlIO.getStatus', { channel => 0 }); + } + } my $status = ($r and ref($r) eq 'HASH' and $r->{params}) ? $r->{params}{status} : undef; return { WhiteLight => ($status ? $status->{WhiteLight} : undef) }; } +# Send a keepalive ping to prevent session expiry in long-running daemons. +# Call periodically (every ~30s) from the control daemon's idle loop. +sub keepAlive { + my $self = shift; + my $r = $self->rpc_call('global.keepAlive', { timeout => 20 }); + if (!$r || !$r->{result}) { + Debug('Dahua_RPC: keepAlive failed, re-logging in'); + $self->login(); + } +} + # --------------------------------------------------------------------------- # Config / probe interface # --------------------------------------------------------------------------- @@ -357,6 +396,18 @@ sub set_config { my ($self, $diff) = @_; return undef unless ref($diff) eq 'HASH' and $diff->{name} and exists $diff->{table}; my $r = $self->rpc_call('configManager.setConfig', $diff); + if (!$r || !$r->{result}) { + my $msg = $r ? ($r->{error}{message} // 'result=false') : 'no response'; + Error('Dahua_RPC: set_config('.$diff->{name}.') failed: '.$msg); + # Re-login on session expiry and retry once (same pattern as ptz_raw). + if ($r && $self->session_expired($r->{error})) { + Debug('Dahua_RPC: session expired during set_config, re-logging in'); + return undef if !$self->login(); + $r = $self->rpc_call('configManager.setConfig', $diff); + Error('Dahua_RPC: set_config('.$diff->{name}.') still failed after re-login') + if !$r || !$r->{result}; + } + } return $r ? $r->{result} : undef; } diff --git a/scripts/zmcontrol.pl.in b/scripts/zmcontrol.pl.in index 51b4dce6a..8dfafcb26 100644 --- a/scripts/zmcontrol.pl.in +++ b/scripts/zmcontrol.pl.in @@ -197,7 +197,9 @@ if ($options{command}) { vec($rin, fileno(SERVER), 1) = 1; my $win = $rin; my $ein = $win; - my $timeout = MAX_COMMAND_WAIT; + # Use a short timeout if the module supports keepAlive so the session + # does not expire between commands (Dahua cameras expire after ~60s idle). + my $timeout = $control->can('keepAlive') ? 30 : MAX_COMMAND_WAIT; while (!$zm_terminate) { my $nfound = select(my $rout = $rin, undef, undef, $timeout); if ( $nfound > 0 ) { @@ -246,6 +248,7 @@ if ($options{command}) { } } else { Debug('Select timed out'); + $control->keepAlive() if $control->can('keepAlive'); } } # end while !$zm_terminate Info("Control server $id/$protocol exiting"); diff --git a/version.txt b/version.txt index 4ca14640c..0c35b2617 100644 --- a/version.txt +++ b/version.txt @@ -1 +1 @@ -1.39.13 +1.39.14 diff --git a/web/includes/Control.php b/web/includes/Control.php index cca1d636b..0809a95f5 100644 --- a/web/includes/Control.php +++ b/web/includes/Control.php @@ -99,6 +99,7 @@ class Control extends ZM_Object { 'MinWhiteSpeed' => NULL, 'MaxWhiteSpeed' => NULL, 'CanLight' => 0, + 'CanIndicatorLight' => 0, 'HasPresets' => 0, 'NumPresets' => 0, 'HasHomePreset' => 0, @@ -125,6 +126,8 @@ class Control extends ZM_Object { $cmds['Reboot'] = 'reboot'; $cmds['LightOn'] = 'lightOn'; $cmds['LightOff'] = 'lightOff'; + $cmds['IndicatorLightOn'] = 'indicatorLightOn'; + $cmds['IndicatorLightOff'] = 'indicatorLightOff'; $cmds['PresetSet'] = 'presetSet'; $cmds['PresetGoto'] = 'presetGoto'; diff --git a/web/lang/en_gb.php b/web/lang/en_gb.php index d90490be2..22d5d33d4 100644 --- a/web/lang/en_gb.php +++ b/web/lang/en_gb.php @@ -205,6 +205,8 @@ $SLANG = array( 'CanWhite' => 'Can White Balance', 'CanWhiteCon' => 'Can White Bal. Continuous', 'CanLight' => 'Can Light', + 'CanIndicatorLight' => 'Can Indicator Light', + 'Indicator' => 'Indicator', 'CanWhiteRel' => 'Can White Bal. Relative', 'CanZoomAbs' => 'Can Zoom Absolute', 'CanZoom' => 'Can Zoom', diff --git a/web/skins/classic/css/base/views/control.css b/web/skins/classic/css/base/views/control.css index 2d2315f42..14418d564 100644 --- a/web/skins/classic/css/base/views/control.css +++ b/web/skins/classic/css/base/views/control.css @@ -45,7 +45,8 @@ } /* Lit/amber when the camera reports the light is on; clicking then turns it off. */ -.ptzControls .controlsPanel .lightToggleBtn.active { +.ptzControls .controlsPanel .lightToggleBtn.active, +.ptzControls .controlsPanel .indicatorLightToggleBtn.active { background-color: #ffc107; color: #000; } diff --git a/web/skins/classic/includes/control_functions.php b/web/skins/classic/includes/control_functions.php index 29dafa3a0..f6ed7eb0d 100644 --- a/web/skins/classic/includes/control_functions.php +++ b/web/skins/classic/includes/control_functions.php @@ -124,6 +124,16 @@ function controlLight($monitor, $cmds) { return ob_get_clean(); } +function controlIndicatorLight($monitor, $cmds) { + ob_start(); +?> +
+ +
+Control(); ob_start(); @@ -253,6 +263,8 @@ function ptzControls($monitor) { echo controlWhite($monitor, $cmds); if ( $control->CanLight() ) echo controlLight($monitor, $cmds); + if ( $control->CanIndicatorLight() ) + echo controlIndicatorLight($monitor, $cmds); if ( $control->CanMove() ) { ?>
diff --git a/web/skins/classic/views/_options_controlcaps.php b/web/skins/classic/views/_options_controlcaps.php index 6e277e784..aab6d2525 100644 --- a/web/skins/classic/views/_options_controlcaps.php +++ b/web/skins/classic/views/_options_controlcaps.php @@ -47,6 +47,7 @@ $controls = dbFetchAll('SELECT * FROM Controls ORDER BY Name'); + @@ -66,6 +67,7 @@ foreach( $controls as $control ) { + '', 'CanWhite' => '', 'CanLight' => '', + 'CanIndicatorLight' => '', 'CanAutoWhite' => '', 'CanWhiteAbs' => '', 'CanWhiteRel' => '', @@ -238,6 +239,10 @@ switch ( $name ) { checked="checked"/> + + + checked="checked"/> + checked="checked"/> diff --git a/web/skins/classic/views/js/watch.js b/web/skins/classic/views/js/watch.js index fc6c955dd..145f2855f 100644 --- a/web/skins/classic/views/js/watch.js +++ b/web/skins/classic/views/js/watch.js @@ -423,6 +423,26 @@ function updateLightButton(respObj) { // (un-highlighted, sends the on command) as a plain toggle. } +function indicatorLightStatusReq() { + if (!$j('.indicatorLightToggleBtn').length) return; + const data = {control: 'indicatorLightStatus', response: 1}; + if (auth_hash) data.auth = auth_hash; + $j.getJSON(monitorUrl + '?view=request&request=control&id='+monitorId, data) + .done(updateIndicatorLightButton) + .fail(logAjaxFail); +} + +function updateIndicatorLightButton(respObj) { + const btn = $j('.indicatorLightToggleBtn'); + if (!btn.length) return; + const state = (respObj && respObj.status) ? respObj.status.Enable : null; + if (state == 'On') { + btn.addClass('active').val(btn.attr('data-off-cmd')); + } else if (state == 'Off') { + btn.removeClass('active').val(btn.attr('data-on-cmd')); + } +} + function controlCmd(event) { button = event.target; @@ -1168,6 +1188,12 @@ function initPage() { setTimeout(lightStatusReq, 800); }); } + if ($j('.indicatorLightToggleBtn').length) { + indicatorLightStatusReq(); + $j(document).on('click', '.indicatorLightToggleBtn', function() { + setTimeout(indicatorLightStatusReq, 800); + }); + } } // initPage function watchFullscreen() {