Files
zoneminder/scripts/ZoneMinder/CMakeLists.txt
T
Isaac ConnorandClaude Opus 5 210d10da1d feat: add ZoneMinder::Control::AMLink for the AMLINK AL5M white light
These cameras have a white light that ONVIF does not expose at all: imaging
offers only Brightness/ColorSaturation/Contrast/Sharpness, GetRelayOutputs is
empty with RelayOutputs="0", and there is no PTZ service and so no auxiliary
commands. The light lives behind a vendor JSON-RPC tunnel.

The vocabulary turns out to be Dahua's -- CoaxialControlIO for the light,
magicBox for reboot -- so this subclasses Dahua_RPC and replaces only open,
rpc_call and login. lightOn, lightOff, lightStatus and reboot are inherited
unchanged, and Dahua_RPC's lightStatus already parses the
params.status.WhiteLight these cameras return.

The transport is the whole of the work, and none of it is discoverable by
probing:

  * The endpoint is /Onvif/device_service with a capital O. The lowercase
    /onvif/device_service is the real ONVIF SOAP service; the capitalised
    spelling is a separate vendor tunnel sharing the path. /RPC2, /RPC3,
    /OutsideCmd and every /cgi-bin/* return 404 on this model, which is why
    the API looked absent.
  * Requests are <body><cmdType>T</cmdType><cmd>P</cmd></body>. Without
    cmdType the camera drops the connection rather than returning an error.
  * P is base64 of the JSON-RPC object, XOR-masked with a session key once one
    exists. Masking is mandatory: an unmasked Request is refused even on a
    freshly authenticated session, so the key exchange is not optional.
  * That key needs a three step bootstrap -- log in, fetch the device RSA
    public key over the unauthenticated OutsideCmd channel, then send an AES
    key encrypted under it and decrypt the mask key from the reply. The salt
    doubles as the AES key and must be 32 decimal digits; 16 is accepted by
    the AES step and then rejected by getGeneralKey.

IO polarity matches Dahua_RPC: IO 1 is on, IO 2 is off. Worth stating because
the camera's own web UI is a toggle that does not reveal which is which, and
because a floodlight cannot be verified optically in daylight -- an RTSP
brightness comparison appeared to say the opposite and was exposure drift.
CoaxialControlIO.getStatus is the reliable witness.

Tested: 26 assertions over the pure transport helpers -- mask symmetry and
cycling, NUL bytes surviving the mask, the envelope, stripping the CRLF the
device pads its replies with, and the channel routing, which fails silently on
the wire if wrong. Verified live against both units: open negotiates a 32 byte
mask key and lightStatus reports Off -> On -> Off around lightOn/lightOff. Perl
suite 15 files / 249 assertions pass.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UvTCzCbvGt8xKQRNCSA7o8
2026-09-11 20:13:02 -05:00

90 lines
3.4 KiB
CMake

# CMakeLists.txt for the ZoneMinder Perl modules.
# Create configured .pm files from .pm.in templates
configure_file(lib/ZoneMinder/Base.pm.in
"${CMAKE_CURRENT_BINARY_DIR}/lib/ZoneMinder/Base.pm" @ONLY)
configure_file(lib/ZoneMinder/Config.pm.in
"${CMAKE_CURRENT_BINARY_DIR}/lib/ZoneMinder/Config.pm" @ONLY)
configure_file(lib/ZoneMinder/Memory.pm.in
"${CMAKE_CURRENT_BINARY_DIR}/lib/ZoneMinder/Memory.pm" @ONLY)
configure_file(lib/ZoneMinder/ConfigData.pm.in
"${CMAKE_CURRENT_BINARY_DIR}/lib/ZoneMinder/ConfigData.pm" @ONLY)
configure_file(lib/ZoneMinder/ONVIF.pm.in
"${CMAKE_CURRENT_BINARY_DIR}/lib/ZoneMinder/ONVIF.pm" @ONLY)
# Install static .pm files from source tree
install(DIRECTORY "${CMAKE_CURRENT_SOURCE_DIR}/lib/"
DESTINATION "${ZM_PERL_INSTALL_PATH}"
FILES_MATCHING PATTERN "*.pm")
# Install configured .pm files from build tree
install(DIRECTORY "${CMAKE_CURRENT_BINARY_DIR}/lib/"
DESTINATION "${ZM_PERL_INSTALL_PATH}"
FILES_MATCHING PATTERN "*.pm")
# Generate man3 pages for modules with verified documentation.
# Only modules whose POD was audited and confirmed accurate are listed.
# Memory.pm is generated from .pm.in so its source is in the build tree.
if(BUILD_MAN)
find_program(POD2MAN pod2man)
find_program(GZIP gzip)
if(POD2MAN AND GZIP)
set(_man3_modules
ZoneMinder/Memory.pm
ZoneMinder/Logger.pm
ZoneMinder/Control/Dahua.pm
ZoneMinder/Control/Reolink_HTTP.pm
ZoneMinder/Control/Amcrest_HTTP.pm
ZoneMinder/Control/Trendnet.pm
ZoneMinder/Control/Instar720p.pm
ZoneMinder/Control/FoscamHD.pm
ZoneMinder/Control/IPSpeaker.pm
ZoneMinder/Control/AMLink.pm)
# Build output uses "/" (safe for GNU make); installed name uses "::" (Perl convention).
# GNU make treats "::" as double-colon rule syntax, so build-time filenames must avoid it.
set(_man3_outputs)
foreach(_pm_rel IN LISTS _man3_modules)
# Derive Perl man page name: ZoneMinder/Control/Dahua.pm -> ZoneMinder::Control::Dahua
string(REPLACE "/" "::" _man_name "${_pm_rel}")
string(REPLACE ".pm" "" _man_name "${_man_name}")
# Build-time filename uses "/" separators (make-safe)
string(REPLACE ".pm" "" _build_stem "${_pm_rel}")
# Memory.pm is generated by configure_file; others are static source files
if(EXISTS "${CMAKE_CURRENT_SOURCE_DIR}/lib/${_pm_rel}")
set(_pm_src "${CMAKE_CURRENT_SOURCE_DIR}/lib/${_pm_rel}")
else()
set(_pm_src "${CMAKE_CURRENT_BINARY_DIR}/lib/${_pm_rel}")
endif()
set(_man_dir "${CMAKE_CURRENT_BINARY_DIR}/man3")
set(_man_out "${_man_dir}/${_build_stem}.3pm")
set(_man_gz "${_man_out}.gz")
get_filename_component(_man_out_dir "${_man_out}" DIRECTORY)
file(MAKE_DIRECTORY "${_man_out_dir}")
add_custom_command(OUTPUT "${_man_out}"
DEPENDS "${_pm_src}"
COMMAND ${POD2MAN} --section 3pm --center "${CMAKE_PROJECT_NAME}"
--release --stderr --name "${_man_name}"
"${_pm_src}" > "${_man_out}")
add_custom_command(OUTPUT "${_man_gz}"
DEPENDS "${_man_out}"
COMMAND ${GZIP} -n --best -c "${_man_out}" > "${_man_gz}")
list(APPEND _man3_outputs "${_man_gz}")
# Install with Perl :: convention in filename
install(FILES "${_man_gz}"
DESTINATION "${ZM_MANPAGE_DEST_PREFIX}/man3"
RENAME "${_man_name}.3pm.gz")
endforeach()
add_custom_target(perl-man3 ALL DEPENDS ${_man3_outputs})
endif()
endif()