Как я решил скрасить весну и купил PTZ камеру
На днях я купил PTZ аналоговую камеру неизвестного вендора и буквально с прилавка что висела там пару лет)).

На фото камера смонтирована на табуретке что я купил вечером.
Проверив ее своим HD CCTV TESTERом я понял что камера работает и передает видеоизображение и работает по протоколу PELCO-P. Хмм, задумался я, а почему бы не сделать управление с компьютера. Давайте расскажу что такое Pelco-P и Pelco-D.
Pelco-D/P — это протоколы связи, в основном используемые для управления поворотными камерами.
Они очень похожи друг на друга. Зачастую устройства снабжены переключателями, которые позволяют пользователю выбирать тип протокола в рамках одного и того же оборудования. Они могут быть реализованы, среди прочего, в управляющих клавиатурах, рекордерах или реализованы в программном обеспечении, например, на компьютере. Они позволяют вращать камеру,регулировать фокусировку (focus), масштабировать (zoom) и т.д.
Однако эти протоколы используются все реже и реже. C появлением аналоговых систем высокой четкости, таких как HD-CVi, AHD, HD-TVI, их создатели представили свои собственные решения для управления повортными камерами. При управлении через Pelco необходимо использовать дополнительную пару проводов.
Обычно передача сигнала осуществляется через RS-485. Обе системы имеют адресное поле 1 байт, поэтому они могут контролировать до 256 устройств, подключенных к шине. Нет определенной скорости передачи данных. Обычно в контроллерах есть несколько вариантов: 1200, 2400, 4800, 9600 бод.
Моя камера назовем ее “Циклоп” работает по протоколу Pelco-P со скоростью 2400 бод и имеет адрес 01 с завода.
Для удобства я набросал небольшой GUI с использованием QT.

Port: — Вбиваете название порта своего USB-RS485 адаптера. Я использую вот такой.
Baud rate: — Скорость для передачи команд
Address: — Адрес камеры
И обязательно поставьте галочку какой протокол вы используйте.
Особо я не делал никаких защит от дурака поэтому, но в дальнейшем допишу как будет вечерок. Программа доступна на моем Github
https://github.com/sdivcom-dotcom/pelco-remoted
Для этого нужно поставить:
pip install PyQt5
pip install pyserial
sudo pip install PyQt5
sudo pip install pyserial
sudo python3 gui.py
Также для удобства своей работы я сделал консольную утилиту что доступна там же. Там достаточно простой код.
sudo python3 console.py -h
usage: console.py [-h] [-p PORT] [-b BAUD] [-a ADDRESS] [-prot PROTOCOL] [-c COMMAND]
options:
-h, — help show this help message and exit
-p PORT, — Port adapter
-b BAUD, — baud rate camera
-a ADDRESS, — address camera
-prot PROTOCOL, — protocol camera
-c COMMAND, — command camera
Я даже приведу код всей программы


from commands import *
import argparse


parser = argparse.ArgumentParser(description="")

parser.add_argument('-p', '--port',
                    dest='port',
                    help='Port CH341a',
                    default='/dev/ttyUSB0',
                    type=str)

parser.add_argument('-b', '--baud',
                    dest='baud',
                    help='baud rate camera',
                    default=2400,
                    type=int)

parser.add_argument('-a', '--address',
                    dest='address',
                    help='address camera',
                    default='01',
                    type=str)

parser.add_argument('-prot', '--protocol',
                    dest='protocol',
                    help='protocol camera',
                    default="p",
                    type=str)

parser.add_argument('-c', '--command',
                    dest='command',
                    help='command camera',
                    default='stop',
                    type=str)

args = parser.parse_args()
command = args.command
port = args.port
baud = args.baud
address = args.address
protocol = args.protocol

if command == "up":
    up(port, baud, address, protocol)

elif command == "down":
    down(port, baud, address, protocol)

elif command == "right":
    right(port, baud, address, protocol)

elif command == "left":
    left(port, baud, address, protocol)

elif command == "zoom_plus":
    zoom_plus(port, baud, address, protocol)

elif command == "zoom_minus":
    zoom_minus(port, baud, address, protocol)

elif command == "focus_plus":
    focus_plus(port, baud, address, protocol)

elif command == "focus_minus":
    focus_minus(port, baud, address, protocol)

else:
    print("Not command")

Думаю проблем с ним не возникнет. И да программа написана на скорую руку.
Давайте расскажу подробнее о протоколе и как я реализовал управление. Заглянем в файл commands.py

import serial
import time

#commands PELCO-P
const_stop_p = "00000000"
const_up_p = "00080020"
const_down_p = "00100020"
const_left_p = "00042000"
const_right_p = "00022000"
const_zoom_plus_p = "00200000"
const_zoom_minus_p = "00400000"
const_focus_plus_p = "00800000"
const_focus_minus_p = "01000000"

#commands PELCO-D
const_stop_d = "00000000"
const_up_d = "00083F3F"
const_down_d = "00103F3F"
const_left_d = "00043F3F"
const_right_d = "00023F3F"
const_focus_plus_d = "00200000"
const_focus_minus_d = "00400000"
const_zoom_plus_d = "00800000"
const_zoom_minus_d = "01000000"

delay_run = 0.37
delay_optic = 0.05

def pelco_p_data(address, command):
    address = str(address)
    command = str(command)
    start_byte = "A0"
    af_byte = "AF"
    checksum = "00"
    data = start_byte + address + command + af_byte + checksum
    #print(data)
    return data


def pelco_d_data(address, command):
    address = str(address)
    command = str(command)
    start_byte = "FF"
    checksum = "00"
    data = start_byte + address + command + checksum
    return data


def write_command(port, baud, data, data_stop, delay):
    ser = serial.Serial(port=port, baudrate=baud)
    data_hex = bytes.fromhex(data)
    data_stop_hex = bytes.fromhex(data_stop)
    ser.write(data_hex)
    time.sleep(delay)
    ser.write(data_stop_hex)
    ser.close()


def up(port, baud, address, procotol):
    delay = delay_run
    if procotol == "p":
        command_stop = const_stop_p
        command_up = const_up_p
        data_stop = pelco_p_data(address, command_stop)
        data = pelco_p_data(address, command_up)
    elif procotol == "d":
        command_stop = const_stop_d
        command_up = const_up_d
        data_stop = pelco_d_data(address, command_stop)
        data = pelco_d_data(address, command_up)
    else:
        pass
    write_command(port, baud, data, data_stop, delay)


def down(port, baud, address, procotol):
    delay = delay_run
    if procotol == "p":
        command_stop = const_stop_p
        command_down = const_down_p
        data_stop = pelco_p_data(address, command_stop)
        data = pelco_p_data(address, command_down)
    elif procotol == "d":
        command_stop = const_stop_d
        command_down = const_down_d
        data_stop = pelco_d_data(address, command_stop)
        data = pelco_d_data(address, command_down)
    else:
        pass
    write_command(port, baud, data, data_stop, delay)


def left(port, baud, address, procotol):
    delay = delay_run
    if procotol == "p":
        command_stop = const_stop_p
        command_left = const_left_p
        data_stop = pelco_p_data(address, command_stop)
        data = pelco_p_data(address, command_left)
    elif procotol == "d":
        command_stop = const_stop_d
        command_left = const_left_d
        data_stop = pelco_d_data(address, command_stop)
        data = pelco_d_data(address, command_left)
    else:
        pass
    write_command(port, baud, data, data_stop, delay)


def right(port, baud, address, procotol):
    delay = delay_run
    if procotol == "p":
        command_stop = const_stop_p
        command_right = const_right_p
        data_stop = pelco_p_data(address, command_stop)
        data = pelco_p_data(address, command_right)
    elif procotol == "d":
        command_stop = const_stop_d
        command_right = const_right_d
        data_stop = pelco_d_data(address, command_stop)
        data = pelco_d_data(address, command_right)
    else:
        pass
    write_command(port, baud, data, data_stop, delay)


def zoom_plus(port, baud, address, procotol):
    delay = delay_optic
    if procotol == "p":
        command_stop = const_stop_p
        command_zoom_plus = const_zoom_plus_p
        data_stop = pelco_p_data(address, command_stop)
        data = pelco_p_data(address, command_zoom_plus)
    elif procotol == "d":
        command_stop = const_stop_d
        command_zoom_plus = const_zoom_plus_d
        data_stop = pelco_d_data(address, command_stop)
        data = pelco_d_data(address, command_zoom_plus)
    else:
        pass
    write_command(port, baud, data, data_stop, delay)


def zoom_minus(port, baud, address, procotol):
    delay = delay_optic
    if procotol == "p":
        command_stop = const_stop_p
        command_zoom_minus = const_zoom_minus_p
        data_stop = pelco_p_data(address, command_stop)
        data = pelco_p_data(address, command_zoom_minus)
    elif procotol == "d":
        command_stop = const_stop_d
        command_zoom_minus = const_zoom_minus_d
        data_stop = pelco_d_data(address, command_stop)
        data = pelco_d_data(address, command_zoom_minus)
    else:
        pass
    write_command(port, baud, data, data_stop, delay)


def focus_plus(port, baud, address, procotol):
    delay = delay_optic
    if procotol == "p":
        command_stop = const_stop_p
        command_focus_plus = const_focus_plus_p
        data_stop = pelco_p_data(address, command_stop)
        data = pelco_p_data(address, command_focus_plus)
    elif procotol == "d":
        command_stop = const_stop_d
        command_focus_plus = const_focus_plus_d
        data_stop = pelco_d_data(address, command_stop)
        data = pelco_d_data(address, command_zoom_plus)
    else:
        pass
    write_command(port, baud, data, data_stop, delay)


def focus_minus(port, baud, address, procotol):
    delay = delay_optic
    if procotol == "p":
        command_stop = const_stop_p
        command_focus_minus = const_focus_minus_p
        data_stop = pelco_p_data(address, command_stop)
        data = pelco_p_data(address, command_focus_minus)
    elif procotol == "d":
        command_stop = const_stop_d
        command_focus_minus = const_focus_minus_d
        data_stop = pelco_d_data(address, command_stop)
        data = pelco_d_data(address, command_focus_minus)
    else:
        pass
    write_command(port, baud, data, data_stop, delay)


def random_command(port, baud, address, protocol, command):
    delay = 0.5
    if procotol == "p":
        command_stop = const_stop_p
        data_stop = pelco_p_data(address, command_stop)
        data = pelco_p_data(address, command)
    elif procotol == "d":
        command_stop = const_stop_d
        data_stop = pelco_d_data(address, command_stop)
        data = pelco_d_data(address, command)
    else:
        pass
    write_command(port, baud, data, data_stop, delay)
Самым главной функцией тут является pelco_p_data для PELCO-P и pelco_d_data для PELCO-D
Рассмотрим для PELCO-P

def pelco_p_data(address, command):
    address = str(address)
    command = str(command)
    start_byte = "A0"
    af_byte = "AF"
    checksum = "00"
    data = start_byte + address + command + af_byte + checksum
    #print(data)
    return data
Что же такое start_byte это то с чего начинается прием команды камерой. Это всегда A0. Далее идет байт адреса. После идет команда в 4 байта. Я сделал их константами ибо мне нужно было всего несколько из них.
Думаю в дальнейшем я реализую протокол полностью хоть это и избыточно для моей задачи. “af_byte” это окончание команды и это всегда AF. Далее идет чек-сумма, но как выяснилось камере они по боку. Поэтому пока я сделал их 00.
Далее важным участком является write_command.


def write_command(port, baud, data, data_stop, delay):
    ser = serial.Serial(port=port, baudrate=baud)
    data_hex = bytes.fromhex(data)
    data_stop_hex = bytes.fromhex(data_stop)
    ser.write(data_hex)
    time.sleep(delay)
    ser.write(data_stop_hex)
    ser.close()
Как мы видим это функция для передачи данных. В ней ничего нет примечательного кроме того что отправка команд всегда идет с командой “stop” ибо команда будет бесконечно выполнять одну команду. И да команда каждый раз открывает и закрывает порт для безопасности и экономии. Далее я решил улучшить код и сделал полноценную библиотеку с примерами. Она доступна https://github.com/sdivcom-dotcom/python-pelco-lib
Перед вами моя небольшая библиотека для упрощения работы с протоколом PELCO-P и PELCO-D. Опишу ее основные функции:
Касается всех функций!
Для корректной работы требуется каждой передать адрес камеры в формате "00" где нули это числа адреса камеры на шине.
til_speed - это скорость по оси наклона - tilt и она должна быть от 00 до 63. Желательно передавать число как int.
pan_speed - это скорость по оси вращения влево или вправо и она должна быть от 00 до 63. Желательно передавать число как int.
Ниже приведены функции для протокола PELCO-P. файл commands_pelco_p.py
Ниже приведены функции для протокола PELCO-D. файл commands_pelco_d.py
1. pelco_p_stop(address) - остановка движения камеры.
2. pelco_p_tilt_up(address, til_speed) - движение камеры вверх.
3. pelco_p_tilt_down(address, til_speed) - движение камеры вниз.
4. pelco_p_tilt(address, up, down, til_speed) - движение камеры вниз и вверх.
Если передать up = 1 то камера будет двигаться наверх.
Если передать down = 1 то камера будет двигаться вниз.
5. pelco_p_pan_left(address, pan_speed) - движение камеры влево.
6. pelco_p_pan_right(address, pan_speed) - движение камеры вправо.
7. pelco_p_pan(address, right, left, pan_speed) - движение камеры влево или вправо.
Если передать right = 1 то камера будет двигаться вправо.
Если передать left = 1 то камера будет двигаться влево.
8. pelco_p_upleft(address, pan_speed, til_speed) - движение камеры вверх и влево.
9. pelco_p_upright(address, pan_speed, til_speed) - движение камеры вверх и вправо.
10. pelco_p_down_left(address, pan_speed, til_speed) - движение камеры вниз влево.
11. pelco_p_down_right(address, pan_speed, til_speed) - движение камеры вниз и вправо.
12. pelco_p_zoom_in(address) - зум объектива движется вперед.
13. pelco_p_zoom_out(address) - зум объектива движется назад.
14. pelco_p_focus_far(address) - фокус объектива движется вперед.
15. pelco_p_focus_near(address) - фокус объектива движется назад.
16. pelco_d_stop(address) - остановка движения камеры.
17. pelco_d_tilt_up(address, til_speed) - движение камеры вверх.
18. pelco_d_tilt_down(address, til_speed) - движение камеры вниз.
19. pelco_d_tilt(address, up, down, til_speed) - движение камеры вниз и вверх.
Если передать up = 1 то камера будет двигаться наверх.
Если передать down = 1 то камера будет двигаться вниз.
20. pelco_d_pan_left(address, pan_speed) - движение камеры влево.
21. pelco_d_pan_right(address, pan_speed) - движение камеры вправо.
22. pelco_d_pan(address, right, left, pan_speed) - движение камеры влево или вправо.
Если передать right = 1 то камера будет двигаться вправо.
Если передать left = 1 то камера будет двигаться влево.
23. pelco_d_upleft(address, pan_speed, til_speed) - движение камеры вверх и влево.
24. pelco_d_upright(address, pan_speed, til_speed) - движение камеры вверх и вправо.
25. pelco_d_down_left(address, pan_speed, til_speed) - движение камеры вниз влево.
26. pelco_d_down_right(address, pan_speed, til_speed) - движение камеры вниз и вправо.
27. pelco_d_zoom_in(address) - зум объектива движется вперед.
28. pelco_d_zoom_out(address) - зум объектива движется назад.
29. pelco_d_focus_far(address) - фокус объектива движется вперед.
30. pelco_d_focus_near(address) - фокус объектива движется назад.
Также у нас есть транспортный модуль где есть две функции для работы с COM портом. Для работы с ним нужно добавить библиотеку pyserial
Для обоих функций есть параметры.
port - сюда мы передаем номер COM порта из системы. Виртуальный com порт что создается в системе когда вы подключаете ваш usb-rs485 адаптер.
baud - параметр baudrate для протокола что устанавливается настройками камеры. Обычно он равен(1200, 1800, 2400, 4800, 9600).
data - команда которую мы хотим отправить.
data_stop сюда мы передаем строку что сформировала stop команда. Пример приведен ниже.
delay - задежка между командами в секундах.
31. write_com_command(port, baud, data, data_stop, delay) отправляет команду и ждем заданый промежуток и отправляет стоп команду. Удобна для небольшого шага при движении.
32. write_com_action(port, baud, data) просто отправляет данные.

Стандартный пример работы с библиотекой выглядит так.
Добавляем необходимое в наш файл с программой.


from commands_pelco_d import pelco_d_stop, pelco_d_tilt_up
from pelco_transport import write_com_action, write_com_command
port = "/dev/ttyUSB1"
baud = "2400"
address = "01"
delay = 1
til_speed = "32"
data = pelco_d_tilt_up(address, til_speed)
data_stop = pelco_d_stop(address)
write_com_command(port, baud, data, data_stop, delay)
Вот у нас вышел код что отправляет по COM порту доступному по адресу "/dev/ttyUSB1" c baudrate 2400 команду по протоколу PELCO-D для того чтобы камера по ардесу "01" одну секунду поднимала объектив наверх и после остановилась.

Made on
Tilda