Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ jobs:
steps:
- uses: actions/checkout@v4
with:
path: src/hikmicro_ptz_driver
path: src/hikmicro_driver # holds hikmicro_msgs and hikmicro_driver
- name: Dependencies
run: |
deps() {
Expand Down Expand Up @@ -86,7 +86,7 @@ jobs:
source /opt/ros/noetic/setup.bash
source devel/setup.bash
result=0
bash src/hikmicro_ptz_driver/test/smoke_test.sh > smoke.txt 2>&1 || result=$?
bash src/hikmicro_driver/hikmicro_driver/test/smoke_test.sh > smoke.txt 2>&1 || result=$?
cat smoke.txt
echo "::notice title=Smoke test::$(grep -c '^ok' smoke.txt) ok, $(grep -c '^FAIL' smoke.txt) failed"
if [ "$result" != 0 ]; then
Expand Down
56 changes: 31 additions & 25 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
<div align="center">

# hikmicro_ptz_driver
# hikmicro_driver

**ROS Noetic driver for the HIKMICRO HM-TD5567T bi-spectrum (thermal + visible) PTZ camera
over ISAPI: precise pan/tilt/zoom that tells you when it has arrived, presets, light and wiper,
Expand All @@ -23,6 +23,10 @@ Deep Robotics X30.**

---

**Docs:** [ISAPI summary](docs/isapi.md) · [network setup](docs/network.md) ·
[first run on the camera](docs/first_run.md) · [troubleshooting](docs/troubleshooting.md) ·
[messages](hikmicro_msgs/README.md)

Everything goes over the LAN cable: the camera is controlled with **ISAPI** (HTTP with digest
authentication, *ISAPI Thermography thermal cameras PT series*, which lists the HM-TD5567T-7/15/25
W models) and streams its two sensors over **RTSP**. The ISAPI client, the multipart/thermometry
Expand Down Expand Up @@ -61,11 +65,11 @@ All names under the launch namespace (`/hikmicro` by default).

| Topic | Type | |
|---|---|---|
| `ptz/state` | `hikmicro_ptz_driver/PtzState` | connected, pan, tilt, zoom, moving (`state_rate`, 10 Hz) |
| `ptz/state` | `hikmicro_msgs/PtzState` | connected, pan, tilt, zoom, moving (`state_rate`, 10 Hz) |
| `joint_states` | `sensor_msgs/JointState` | pan / tilt joints for `robot_state_publisher` |
| `ptz/cmd_vel` (in) | `geometry_msgs/Twist` | manual: `angular.z` pan, `angular.y` tilt, `linear.z` zoom, −1..1; stops 0.5 s after the last message |
| `thermal/temperature` | `sensor_msgs/Image` | **32FC1, °C per pixel** (pixel-to-pixel thermometry, `radiometric_rate`) |
| `thermal/stats` | `hikmicro_ptz_driver/ThermalStats` | full-frame max / min / mean and where |
| `thermal/stats` | `hikmicro_msgs/ThermalStats` | full-frame max / min / mean and where |
| `visible/image_raw` | `sensor_msgs/Image` | RGB stream (channel 1) |
| `thermal/image_raw` | `sensor_msgs/Image` | thermal video (channel 2, palette as set on the camera) |
| `*/image_raw/compressed` | `sensor_msgs/CompressedImage` | via image_transport |
Expand Down Expand Up @@ -136,7 +140,7 @@ catkin_make && source devel/setup.bash # or: catkin build

# 3. Launch (the password comes from the environment, so roslaunch never prints it)
export HIKMICRO_PASSWORD='<camera password>'
roslaunch hikmicro_ptz_driver hikmicro.launch host:=192.168.1.64
roslaunch hikmicro_driver hikmicro.launch host:=192.168.1.64
# cameras:=false for control only

# 4. Second terminal
Expand All @@ -150,7 +154,7 @@ rosrun rqt_image_view rqt_image_view /hikmicro/visible/image_raw

```bash
rosrun actionlib_tools axclient.py /hikmicro/ptz/move_to # GUI
rostopic pub -1 /hikmicro/ptz/move_to/goal hikmicro_ptz_driver/MoveToActionGoal \
rostopic pub -1 /hikmicro/ptz/move_to/goal hikmicro_msgs/MoveToActionGoal \
"{goal: {pan: 123.4, tilt: -12.5, zoom: 8.0}}"
rostopic echo -n 1 /hikmicro/ptz/move_to/result # state: "reached"
```
Expand All @@ -160,7 +164,7 @@ rostopic echo -n 1 /hikmicro/ptz/move_to/result # state: "reache
```bash
rosservice call /hikmicro/ptz/set_preset "{id: 3, name: 'trafo 1 bushing'}"
rosservice call /hikmicro/ptz/get_presets
rostopic pub -1 /hikmicro/ptz/goto_preset/goal hikmicro_ptz_driver/GotoPresetActionGoal \
rostopic pub -1 /hikmicro/ptz/goto_preset/goal hikmicro_msgs/GotoPresetActionGoal \
"{goal: {id: 3}}"
```

Expand Down Expand Up @@ -202,8 +206,8 @@ rosservice call /hikmicro/isapi/request "{method: GET, path: /ISAPI/PTZCtrl/chan
#!/usr/bin/env python3
import actionlib
import rospy
from hikmicro_ptz_driver.msg import MoveToAction, MoveToGoal
from hikmicro_ptz_driver.srv import GetTemperature, Snapshot
from hikmicro_msgs.msg import MoveToAction, MoveToGoal
from hikmicro_msgs.srv import GetTemperature, Snapshot
from std_srvs.srv import Trigger

POINTS = [ # name, pan, tilt, zoom, box in the thermal image (x1, y1, x2, y2)
Expand Down Expand Up @@ -233,7 +237,7 @@ for name, pan, tilt, zoom, (x1, y1, x2, y2) in POINTS:

## Parameters

[config/hikmicro.yaml](config/hikmicro.yaml) and [config/camera.yaml](config/camera.yaml), every
[hikmicro.yaml](hikmicro_driver/config/hikmicro.yaml) and [camera.yaml](hikmicro_driver/config/camera.yaml), every
one commented.

**`hikmicro_ptz_node`**
Expand Down Expand Up @@ -287,32 +291,34 @@ one commented.

```bash
catkin_make run_tests && catkin_test_results build/test_results --verbose
bash src/hikmicro_ptz_driver/test/smoke_test.sh # with the workspace sourced
bash src/hikmicro_ptz_driver/hikmicro_driver/test/smoke_test.sh # with the workspace sourced
```

- `test_isapi_xml`: builders and parsers against the document's example messages
- `test_radiometric`: multipart parsing, float32 and 16-bit Kelvin matrices, region statistics
- `test_motion`: arrival detection (reached, overshoot, stall, timeout, wrap around 0°)
- `test_camera_fake`: the ISAPI client end to end against [test/fake_camera.py](test/fake_camera.py)
- `test_camera_fake`: the ISAPI client end to end against [fake_camera.py](hikmicro_driver/test/fake_camera.py)
(digest auth, a PTZ that moves at finite speed, presets, aux, IR-cut, snapshots, thermometry)
- `test_rtsp`: GStreamer pipelines, decoder order, the capture loop on a local video
- `smoke_test.sh`: `hikmicro.launch` against the fake camera — actions, services, topics; CI runs
all of it in a `ros:noetic` container

```
hikmicro_ptz_driver/
├── include/hikmicro_ptz_driver/
│ ├── http_client.hpp libcurl, digest auth (no ROS)
│ ├── isapi_xml.hpp ISAPI messages (no ROS)
│ ├── radiometric.hpp jpegPicWithAppendData, temperature statistics (no ROS)
│ ├── motion.hpp arrival detection (no ROS)
│ ├── camera.hpp HikCamera: the whole camera API (no ROS)
│ ├── rtsp_capture.hpp RTSP capture thread (OpenCV, no ROS)
│ └── *_interface.hpp ROS: PTZ, aux, thermal
├── src/ hikmicro_node.cpp, camera_node.cpp, ...
├── msg/ srv/ action/
├── config/ launch/
└── test/
hikmicro_ptz_driver/ repo
├── hikmicro_msgs/ PtzState, PtzPreset, ThermalStats, 7 services, MoveTo, GotoPreset
├── hikmicro_driver/
│ ├── include/hikmicro_driver/
│ │ ├── http_client.hpp libcurl, digest auth (no ROS)
│ │ ├── isapi_xml.hpp ISAPI messages (no ROS)
│ │ ├── radiometric.hpp jpegPicWithAppendData, temperature statistics (no ROS)
│ │ ├── motion.hpp arrival detection (no ROS)
│ │ ├── camera.hpp HikCamera: the whole camera API (no ROS)
│ │ ├── rtsp_capture.hpp RTSP capture thread (OpenCV, no ROS)
│ │ └── *_interface.hpp ROS: PTZ, aux, thermal
│ ├── src/ hikmicro_node.cpp, camera_node.cpp, ...
│ ├── config/ launch/
│ └── test/
└── docs/ guides
```

hikmicro_ptz_driver is released under the [Apache License 2.0](LICENSE).
hikmicro_driver is released under the [Apache License 2.0](LICENSE).
33 changes: 33 additions & 0 deletions docs/first_run.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
# First run on the camera

The driver is tested against a simulated camera
([fake_camera.py](../hikmicro_driver/test/fake_camera.py)). Fill in the **Result** column on
the real camera; it decides the defaults that go into `hikmicro.yaml`.

```bash
export HIKMICRO_PASSWORD='<camera password>'
roslaunch hikmicro_driver hikmicro.launch host:=192.168.1.64
```

| # | Check | Command | Expected | Result |
|---|---|---|---|---|
| 1 | Connection | `rostopic echo -n 1 /hikmicro/ptz/state` | `connected: True`; the log shows model and firmware | |
| 2 | Precision | same | `high_precision: True` (absoluteEx, 0.001°) | |
| 3 | Pan direction | `move_to` pan 10, then pan 20 | turns which way? → `pan_sign`, `invert_pan` | |
| 4 | Tilt direction | `move_to` tilt 0, then tilt 10 | tilts which way? → `tilt_sign`, `invert_tilt` | |
| 5 | Arrival | `rostopic pub -1 /hikmicro/ptz/move_to/goal hikmicro_msgs/MoveToActionGoal "{goal: {pan: 120, tilt: -10, zoom: 4}}"` | result `state: "reached"` within tolerance | |
| 6 | Limit | `move_to` beyond the tilt limit | `stalled` or `error` | |
| 7 | Aux ids | `rosservice call /hikmicro/isapi/request "{method: GET, path: /ISAPI/PTZCtrl/channels/1/auxcontrols}"` | ids of LIGHT / WIPER | light: · wiper: |
| 8 | Light / wiper | `rosservice call /hikmicro/aux/light "data: true"`, `aux/wiper` | the right device switches | |
| 9 | Night | `rosservice call /hikmicro/image/day_night "mode: 'night'"` | B/W image with IR light | |
| 10 | Thermometry | `rosservice call /hikmicro/thermal/get_temperature "{mode: 0, normalized: true, x: 0.5, y: 0.5}"` on a hand | 30–36 °C | |
| 11 | Streams | `rostopic hz /hikmicro/visible/image_raw /hikmicro/thermal/image_raw` | stream frame rates | codec: |

## If a check fails

- **2 `False`:** the firmware has no `absoluteEx`; moves use `/status` with 0.1° steps.
- **7 / 8:** set `light_aux_id` / `wiper_aux_id` in [hikmicro.yaml](../hikmicro_driver/config/hikmicro.yaml).
- **10 `notSupport`:** no pixel-to-pixel thermometry on this firmware.
- **11 with `decoder: jetson`:** set `codec` to what `ffprobe` shows (H.264 / H.265).

See also [troubleshooting.md](troubleshooting.md).
55 changes: 55 additions & 0 deletions docs/isapi.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
# ISAPI summary

What the driver uses from *ISAPI Thermography thermal cameras PT series* (Hikvision / HIKMICRO).
All requests are HTTP with **digest authentication** on port 80; `<ch>` is 1 for the visible /
PTZ channel and 2 for the thermal one.

## PTZ

| Method, path | Body / answer | Driver |
|---|---|---|
| `GET /ISAPI/PTZCtrl/channels/1/absoluteEx` | `elevation`, `azimuth` (degrees, 0.001), `absoluteZoom` (ratio) | `ptz/state` (high precision) |
| `PUT /ISAPI/PTZCtrl/channels/1/absoluteEx` | same fields, optional `horizontalSpeed` / `verticalSpeed` | `ptz/move_to` |
| `GET /ISAPI/PTZCtrl/channels/1/status` | `AbsoluteHigh`: elevation −900..2700, azimuth 0..3600 (tenths), `absoluteZoom` ×10 | fallback without `absoluteEx` |
| `PUT /ISAPI/PTZCtrl/channels/1/absolute` | `AbsoluteHigh` | fallback move |
| `PUT /ISAPI/PTZCtrl/channels/1/continuous` | `pan`, `tilt`, `zoom` −100..100 (0 0 0 stops) | `ptz/cmd_vel`, `ptz/stop` |
| `GET /ISAPI/PTZCtrl/channels/1/presets` | id, name, position | `ptz/get_presets` |
| `PUT` / `DELETE /ISAPI/PTZCtrl/channels/1/presets/<id>` | save the current position / delete | `ptz/set_preset`, `ptz/remove_preset` |
| `PUT /ISAPI/PTZCtrl/channels/1/presets/<id>/goto` | — | `ptz/goto_preset` |
| `PUT /ISAPI/PTZCtrl/channels/1/onepushfoucs/start` | (the camera's own spelling) | `ptz/focus` |

The camera only acknowledges a move; "arrived" is decided from the polled position (in
tolerance and still for `settle_time`).

## Auxiliaries and image

| Method, path | Body / answer | Driver |
|---|---|---|
| `GET /ISAPI/PTZCtrl/channels/1/auxcontrols` | list of id + type (`LIGHT`, `WIPER`, `FAN`, `HEATER`) | ids for the aux services |
| `PUT /ISAPI/PTZCtrl/channels/1/auxcontrols/<id>` | `PTZAux`: type, `status` on / off | `aux/light`, `aux/wiper`, `aux/set` |
| `GET` / `PUT /ISAPI/Image/channels/1/ircutFilter` | `IrcutFilterType` auto / day / night | `image/day_night` |
| `GET /ISAPI/Streaming/channels/<ch>01/picture` | JPEG | `snapshot` |
| `GET /ISAPI/System/deviceInfo` | model, serial, firmware | log at start |

## Thermometry

`GET /ISAPI/Thermal/channels/2/thermometry/jpegPicWithAppendData?format=json` answers
`multipart/form-data`:

1. JSON `JpegPictureWithAppendData`: `jpegPicWidth`, `jpegPicHeight`,
`temperatureDataLength`, `scale`, `offset`
2. the thermal JPEG
3. the temperature matrix: float32 °C, or 16-bit where `K = raw / scale + offset`
4. on bi-spectrum models, the visible JPEG

The driver publishes it as `thermal/temperature` (32FC1 °C) and answers
`thermal/get_temperature` from it.

## Errors

`ResponseStatus` (XML) or JSON with `statusCode`, `subStatusCode` (e.g. `notSupport`), and
`errorMsg`; the driver puts them in the service / action messages.

## RTSP

`rtsp://<ip>:554/Streaming/Channels/101` visible, `/201` thermal (`102` / `202` sub streams).
45 changes: 45 additions & 0 deletions docs/network.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
# Network setup

The camera ships with `192.168.1.64/24`. The computer running the driver needs an address in the
same subnet, or the camera gets a new one (SADP tool or the camera's web page).

## An address in the camera's subnet

Quick test, lost on reboot:

```bash
sudo ip addr add 192.168.1.10/24 dev eth0 # your interface: ip -br link
ping -c 3 192.168.1.64
```

Permanent, with netplan (`/etc/netplan/60-hikmicro.yaml`):

```yaml
network:
version: 2
ethernets:
eth0:
addresses:
- 192.168.1.10/24
```

```bash
sudo netplan apply
```

## Ports

| Port | Use |
|---|---|
| 80 | ISAPI (HTTP, digest authentication), `port:=` |
| 554 | RTSP, `/Streaming/Channels/101` visible, `/201` thermal |

## Check before launching

```bash
curl --digest -u admin:<password> http://192.168.1.64/ISAPI/System/deviceInfo
ffprobe -v error -show_streams "rtsp://admin:<password>@192.168.1.64:554/Streaming/Channels/101" | grep -E 'codec_name|width'
```

A 401 means a wrong password; no answer means the address or the subnet is wrong. Several
failed logins lock the account for a while (camera setting).
22 changes: 22 additions & 0 deletions docs/troubleshooting.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
# Troubleshooting

| Symptom | Cause | Fix |
|---|---|---|
| `deviceInfo: ... Couldn't connect` | address or subnet | [network.md](network.md); `ping 192.168.1.64` |
| `HTTP 401 ... wrong user or password` | password, or the account is locked after failed logins | `export HIKMICRO_PASSWORD=...`; wait for the lock to expire |
| `no password ... the camera will answer 401` | `HIKMICRO_PASSWORD` not set | set it, or `password:=` (printed by roslaunch) |
| `ptz/state` `high_precision: False` | no `absoluteEx` on this firmware | works with 0.1° via `/status` |
| `move_to` → `stalled` | end stop, limit or obstruction | target inside the camera's range |
| `move_to` → `timeout` | slow move, or tolerance too tight | raise `move_timeout` / `pan_tolerance` / `tilt_tolerance` |
| `move_to` → `superseded` | a newer goal, `cmd_vel` or `ptz/stop` took over | |
| arrived but the image still shakes | settles slower than `settle_time` | raise `settle_time` |
| `aux/light` switches the wrong device or `missing` | aux ids differ | `light_aux_id`, `wiper_aux_id` (first run check 7) |
| `get_temperature` `notSupport` | no pixel-to-pixel thermometry | |
| `radiometric frame: ...` warnings every 30 s | thermometry failing; the node backs off | see the message (`notSupport`, timeout) |
| `cannot open rtsp://...` repeating | port 554, credentials or decoder | `ffprobe` the URL; `decoder: ffmpeg` |
| `cmd_vel` keeps moving | publisher stopped without a zero command | stops by itself after `cmd_vel_timeout` (0.5 s) |

```bash
roslaunch hikmicro_driver hikmicro.launch host:=<ip> 2>&1 | tee hikmicro.log
rosservice call /hikmicro/isapi/request "{method: GET, path: /ISAPI/PTZCtrl/channels/1/capabilities}"
```
10 changes: 10 additions & 0 deletions hikmicro_driver/CHANGELOG.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
Changelog for package hikmicro_driver
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

0.1.0 (2026-10-18)
------------------
* ISAPI client: digest authentication, PTZ (absoluteEx / status), presets, auxiliaries, IR-cut, snapshots, pixel-to-pixel thermometry
* hikmicro_ptz_node: move_to / goto_preset with arrival detection, cmd_vel, aux, day/night, temperatures
* hikmicro_camera_node: visible and thermal RTSP streams as images
* Unit tests, fake camera and launch smoke test on Noetic
28 changes: 4 additions & 24 deletions CMakeLists.txt → hikmicro_driver/CMakeLists.txt
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
cmake_minimum_required(VERSION 3.0.2)
project(hikmicro_ptz_driver)
project(hikmicro_driver)

set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
Expand All @@ -15,7 +15,7 @@ find_package(catkin REQUIRED COMPONENTS
geometry_msgs
actionlib
actionlib_msgs
message_generation
hikmicro_msgs
cv_bridge
image_transport
)
Expand All @@ -26,31 +26,11 @@ find_package(PkgConfig REQUIRED)
pkg_check_modules(TINYXML2 REQUIRED tinyxml2)
find_package(nlohmann_json 3 REQUIRED)

add_message_files(FILES
PtzPreset.msg
PtzState.msg
ThermalStats.msg
)
add_service_files(FILES
GetPresets.srv
GetTemperature.srv
RawRequest.srv
SetAux.srv
SetDayNight.srv
SetPreset.srv
Snapshot.srv
)
add_action_files(FILES
GotoPreset.action
MoveTo.action
)
generate_messages(DEPENDENCIES std_msgs sensor_msgs actionlib_msgs)

catkin_package(
INCLUDE_DIRS include
LIBRARIES hikmicro_isapi hikmicro_rtsp
CATKIN_DEPENDS roscpp std_msgs std_srvs sensor_msgs geometry_msgs actionlib actionlib_msgs
message_runtime cv_bridge image_transport
cv_bridge image_transport hikmicro_msgs
DEPENDS CURL
)

Expand All @@ -75,7 +55,7 @@ add_executable(hikmicro_ptz_node
)
include_directories(include ${catkin_INCLUDE_DIRS})
target_link_libraries(hikmicro_ptz_node hikmicro_isapi ${catkin_LIBRARIES})
add_dependencies(hikmicro_ptz_node ${${PROJECT_NAME}_EXPORTED_TARGETS} ${catkin_EXPORTED_TARGETS})
add_dependencies(hikmicro_ptz_node ${catkin_EXPORTED_TARGETS})

# RTSP capture (OpenCV, no ROS) and the camera node: one per stream, visible / thermal
add_library(hikmicro_rtsp src/rtsp_capture.cpp)
Expand Down
File renamed without changes.
File renamed without changes.
Loading
Loading