elsampsa / valkka-core

Valkka - Create video surveillance, management and analysis programs with PyQt
GNU Lesser General Public License v3.0
181 stars 35 forks source link

onvif SendReceiveSerialData #36

Closed ekptwtos closed 1 year ago

ekptwtos commented 1 year ago

The problem

I have recently acquired an Active Silicon IP-camera and I have been trying to control it using your library. I tried previously using python-onvif-zeep. The camera implements the ONVIF Profile S standard. I would like to send zoom in / our, focus and other basic VISCA commands to the camera using the SendReceiveSerialData service as described in the DeviceIO wsdl, however I can not seem to get it to work. My knowledge in ONVIF, SOAP, and zeep is quite limited so please forgive me if its something too obvious!

Please note : camera does NOT support PTZ service!

Minimal reproducible example and needed changes

MRE

Here is the python code I have:

#! /usr/bin/env python3

from valkka.onvif import OnVif, DeviceManagement, Media, DeviceIO, PTZ, getWSDLPath
from zeep import Client
import zeep.helpers
from zeep.wsse.username import UsernameToken
import time

try:
    device_service = DeviceManagement(
        ip="10.0.0.250",
        port=8000,
        user="",
        password=""
    )
except Exception as e:
    print(e)

cap = device_service.ws_client.GetCapabilities()
print("CAPABILITIES: \n{}".format(cap))
srv = device_service.ws_client.GetServices(True)
print("SERVICES: \n{}".format(srv))

try:
    deviceIO_service = DeviceIO(
        ip="10.0.0.250",
        port=8000,
        user="",
        password=""
    )
except Exception as e:
    print(e)

# element = deviceIO_service.zeep_client.get_element('ns10:SendReceiveSerialCommand')
# print(element)

ports = deviceIO_service.ws_client.GetSerialPorts()
# print (ports)
serial_token = ports[0].token
# print(serial_token)

zoomin = bytes.fromhex('81 01 04 07 02 FF')
#zoomout = bytes.fromhex('81 01 04 07 03 FF')
#zoomin = b'\x81\x01\x04\x07\x02\xff'

data = {
    'SerialData': zoomin
}

ack = deviceIO_service.ws_client.SendReceiveSerialCommand(serial_token, data)
print(ack)
time.sleep(3)

The first issue I encountered was that the SendReceiveSerialCommand service was not working and the following error occurred:

raise ConnectionError(err, request=request)
requests.exceptions.ConnectionError: ('Connection aborted.', RemoteDisconnected('Remote end closed connection without response'))
Needed changes

The reason behind that is the deviceIO.wsdl file of the library. It does not contain the "Token" element, unlike DeviceIO wsdl,therefore I had to manually add it under the SendReceiveSerialCommand element:

    <xs:element name="SendReceiveSerialCommand">
        <xs:annotation>
            <xs:documentation>Transmitting arbitrary data to the connected serial device and then receiving its response data.</xs:documentation>
        </xs:annotation>
        <xs:complexType>
            <xs:sequence>
                <xs:element name="Token" type="tt:ReferenceToken" minOccurs="0">
                    <xs:annotation>
                                <xs:documentation>The physical serial port reference to be used when this request is invoked.</xs:documentation>
                        </xs:annotation>
                    </xs:element>
                <xs:element name="SerialData" type="tmd:SerialData" minOccurs="0">
                    <xs:annotation>
                        <xs:documentation>The serial port data.</xs:documentation>
                    </xs:annotation>
                </xs:element>
                <xs:element name="TimeOut" type="xs:duration" minOccurs="0">
                    <xs:annotation>
                        <xs:documentation>Indicates that the command should be responded back within the specified period of time.</xs:documentation>
                    </xs:annotation>
                </xs:element>
                <xs:element name="DataLength" type="xs:integer" minOccurs="0">
                    <xs:annotation>
                        <xs:documentation>This element may be put in the case that data length returned from the connected serial device is already determined as some fixed bytes length. It indicates the length of received data which can be regarded as available.</xs:documentation>
                    </xs:annotation>
                </xs:element>
                <xs:element name="Delimiter" type="xs:string" minOccurs="0">
                    <xs:annotation>
                        <xs:documentation>This element may be put in the case that the delimiter codes returned from the connected serial device is already known. It indicates the termination data sequence of the responded data. In case the string has more than one character a device shall interpret the whole string as a single delimiter. Furthermore a device shall return the delimiter character(s) to the client.</xs:documentation>
                    </xs:annotation>
                </xs:element>
            </xs:sequence>
        </xs:complexType>
    </xs:element>
    <xs:element name="SendReceiveSerialCommandResponse">
        <xs:annotation>
            <xs:documentation>Receiving the response data.</xs:documentation>
        </xs:annotation>
        <xs:complexType>
            <xs:sequence>
                <xs:element name="SerialData" type="tmd:SerialData" minOccurs="0"/>
            </xs:sequence>
        </xs:complexType>
    </xs:element>

After this part was fixed the errors go away, but the camera does not zoom in/out ( depending on which VISCA command is being passed). Additionally, the acknowledgement comes back as "None".

Nothing changes if I fill the dictionary with all the fields from the SendReceiveSerialCommand service like so:

serial_data = {
    'SerialData': zoomin,
    'TimeOut': "PT0M0.1S",
    'DataLength': "100",
    'Delimiter': "",
}

The setup

I am running this python script on Ubuntu 20.04 with python 3.8. The setup is a network between the camera and the laptop with static IP addresses assigned.

Please note that the camera is working as I can successfully send commands to the camera over the web interface and when running the C# example (can be found under downloads->software) it comes with, on a Windows machine.

Thank you in advance for your time and effort to help me out!

------ Original question posted in StackOverflow ------

ekptwtos commented 1 year ago

As per the answer:

The data was not passed in the correct format to the function, so the SOAP message was incomplete.

Check the zeep documentation for details about passing SOAP datastructures.

deviceio_type_factory = deviceIO_service.zeep_client.type_factory("http://www.onvif.org/ver10/deviceIO/wsdl")
serial_data = deviceio_type_factory.SerialData(Binary=zoomin)

ack = deviceIO_service.ws_client.SendReceiveSerialCommand(Token= ports[0].token, SerialData=serial_data, TimeOut='PT0M6S', DataLength='100', Delimiter='')

visca_ack_comp = ack['Binary'].hex()
print(visca_ack_comp )
elsampsa commented 1 year ago

Heey! Great that you were able to find a solution to your problem!

The onvif classes in libValkka are just convenience-wrappers that are using zeep under-the-hood, so if there is a problem with "valkka onvif" it's basically just a problem of zeep / onvif / soap / xml, not libValkka. For soap problems it's better to ask elsewhere, like stack overflow, just as you did. :)

So the correct way to do this is to use the official onvif wsdl file & that's it?

ekptwtos commented 1 year ago

Hello!

Yes the problem was basically two fold! 1) The onvif that was stored under

/usr/lib/python3/dist-packages/valkka/onvif/wsdl/deviceio.wsdl

was missing the "Token" element, I decided to add that myself, but another way would be to just download the latest onvif file and replace the existing one.

2) I was sending the data incorrectly. That was the way described in the python-onvif-zeep under the section "configure ( control ) your camera ". The correct way is as described in my comment above!

Please feel free to add this to your documentation! This could save someone else's time and frustration when trying to control his camera!