diff --git a/Device.go b/Device.go index 9abe022..154fcbc 100644 --- a/Device.go +++ b/Device.go @@ -2,16 +2,17 @@ package goonvif import ( "encoding/xml" - "log" "fmt" "github.com/beevik/etree" "github.com/yakovlevdmv/gosoap" "strconv" - "reflect" + "net/http" + "io/ioutil" + "github.com/yakovlevdmv/WS-Discovery" "strings" "github.com/yakovlevdmv/goonvif/Device" - "github.com/yakovlevdmv/WS-Discovery" "errors" + "reflect" "github.com/yakovlevdmv/goonvif/networking" ) @@ -81,54 +82,112 @@ type device struct { } -func GetAvailableDevicesAtSpecificEthernetInterface(interfaceName string) { +func (dev *device)GetServices() map[string]string { + return dev.endpoints +} + +func readResponse(resp *http.Response) string { + b, err := ioutil.ReadAll(resp.Body) + if err != nil { + panic(err) + } + return string(b) +} + +func GetAvailableDevicesAtSpecificEthernetInterface(interfaceName string) []device { /* Call an WS-Discovery Probe Message to Discover NVT type Devices */ devices := WS_Discovery.SendProbe(interfaceName, nil, []string{"dn:"+NVT.String()}, map[string]string{"dn":"http://www.onvif.org/ver10/network/wsdl"}) + nvtDevices := make([]device, 0) + ////fmt.Println(devices) for _, j := range devices { - fmt.Println(j) + doc := etree.NewDocument() + if err := doc.ReadFromString(j); err != nil { + fmt.Errorf("%s", err.Error()) + return nil + } + ////fmt.Println(j) + endpoints := doc.Root().FindElements("./Body/ProbeMatches/ProbeMatch/XAddrs") + for _, xaddr := range endpoints { + //fmt.Println(xaddr.Tag,strings.Split(strings.Split(xaddr.Text(), " ")[0], "/")[2] ) + xaddr := strings.Split(strings.Split(xaddr.Text(), " ")[0], "/")[2] + fmt.Println(xaddr) + c := 0 + for c = 0; c < len(nvtDevices); c++ { + if nvtDevices[c].xaddr == xaddr { + fmt.Println(nvtDevices[c].xaddr, "==", xaddr) + break + } + } + if c < len(nvtDevices) { + continue + } + dev, err := NewDevice(strings.Split(xaddr, " ")[0]) + //fmt.Println(dev) + if err != nil { + fmt.Println("Error", xaddr) + fmt.Println(err) + continue + } else { + ////fmt.Println(dev) + nvtDevices = append(nvtDevices, *dev) + } + } + ////fmt.Println(j) + //nvtDevices[i] = NewDevice() } + return nvtDevices } -func (dev *device) getSupportedServices() { - resp, err := dev.CallMethod(Device.GetCapabilities{}) - if err != nil { - log.Println(err.Error()) - return - } else { +func (dev *device) getSupportedServices(resp *http.Response) { + //resp, err := dev.CallMethod(Device.GetCapabilities{Category:"All"}) + //if err != nil { + // log.Println(err.Error()) + //return + //} else { doc := etree.NewDocument() - if err := doc.ReadFromString(resp); err != nil { - log.Println(err.Error()) + + data, _ := ioutil.ReadAll(resp.Body) + + if err := doc.ReadFromBytes(data); err != nil { + //log.Println(err.Error()) return } services := doc.FindElements("./Envelope/Body/GetCapabilitiesResponse/Capabilities/*/XAddr") for _, j := range services{ - fmt.Println(j.Text()) - fmt.Println(j.Parent().Tag) + ////fmt.Println(j.Text()) + ////fmt.Println(j.Parent().Tag) dev.addEndpoint(j.Parent().Tag, j.Text()) } - } + //} } //NewDevice function construct a ONVIF Device entity -func NewDevice(xaddr string) *device { +func NewDevice(xaddr string) (*device, error) { dev := new(device) dev.xaddr = xaddr dev.endpoints = make(map[string]string) dev.addEndpoint("Device", "http://"+xaddr+"/onvif/device_service") - dev.getSupportedServices() - return dev + + getCapabilities := Device.GetCapabilities{Category: "All"} + + resp, err := dev.CallMethod(getCapabilities) + //fmt.Println(resp.Request.Host) + //fmt.Println(readResponse(resp)) + if err != nil || resp.StatusCode != http.StatusOK { + //panic(errors.New("camera is not available at " + xaddr + " or it does not support ONVIF services")) + return nil, errors.New("camera is not available at " + xaddr + " or it does not support ONVIF services") + } + + dev.getSupportedServices(resp) + return dev, nil } func (dev *device)addEndpoint(Key, Value string) { dev.endpoints[Key]=Value } -func newDeviceEntity() *device { - return &device{} -} - //Authenticate function authenticate client in the ONVIF Device. //Function takes and params. //You should use this function to allow authorized requests to the ONVIF Device @@ -146,6 +205,7 @@ func buildMethodSOAP(msg string) (gosoap.SoapMessage, error) { doc := etree.NewDocument() if err := doc.ReadFromString(msg); err != nil { //log.Println("Got error") + return "", err } element := doc.Root() @@ -162,7 +222,7 @@ func buildMethodSOAP(msg string) (gosoap.SoapMessage, error) { //CallMethod functions call an method, defined struct. //You should use Authenticate method to call authorized requests. -func (dev device) CallMethod(method interface{}) (string, error) { +func (dev device) CallMethod(method interface{}) (*http.Response, error) { pkgPath := strings.Split(reflect.TypeOf(method).PkgPath(),"/") pkg := pkgPath[len(pkgPath)-1] @@ -175,20 +235,30 @@ func (dev device) CallMethod(method interface{}) (string, error) { case "PTZ": endpoint = dev.endpoints["PTZ"] } - if len(endpoint) == 0 { - return "", errors.New("requested service is not implemented") - } - //TODO: Get endpoint automatically if dev.login != "" && dev.password != "" { - return dev.CallAuthorizedMethod(endpoint, method) + /*resp, err := dev.сallAuthorizedMethod(endpoint, method) + if err != nil { + panic(err) + return resp, err + } + + return resp, err*/ + return dev.callAuthorizedMethod(endpoint, method) } else { - return dev.CallNonAuthorizedMethod(endpoint, method) + /*resp, err := dev.сallAuthorizedMethod(endpoint, method) + if err != nil { + panic(err) + return resp, err + } + return resp, err*/ + return dev.callNonAuthorizedMethod(endpoint, method) + } } //CallNonAuthorizedMethod functions call an method, defined struct without authentication data -func (dev device) CallNonAuthorizedMethod(endpoint string, method interface{}) (string, error) { +func (dev device) callNonAuthorizedMethod(endpoint string, method interface{}) (*http.Response, error) { //TODO: Get endpoint automatically /* Converting struct to xml string representation @@ -196,7 +266,7 @@ func (dev device) CallNonAuthorizedMethod(endpoint string, method interface{}) ( output, err := xml.MarshalIndent(method, " ", " ") if err != nil { //log.Printf("error: %v\n", err.Error()) - return "", err + return nil, err } /* @@ -205,7 +275,7 @@ func (dev device) CallNonAuthorizedMethod(endpoint string, method interface{}) ( soap, err := buildMethodSOAP(string(output)) if err != nil { //log.Printf("error: %v\n", err) - return "", err + return nil, err } soap.AddRootNamespaces(Xlmns) @@ -217,14 +287,14 @@ func (dev device) CallNonAuthorizedMethod(endpoint string, method interface{}) ( } //CallMethod functions call an method, defined struct with authentication data -func (dev device) CallAuthorizedMethod(endpoint string, method interface{}) (string, error) { +func (dev device) callAuthorizedMethod(endpoint string, method interface{}) (*http.Response, error) { /* Converting struct to xml string representation */ output, err := xml.MarshalIndent(method, " ", " ") if err != nil { //log.Printf("error: %v\n", err.Error()) - return "", err + return nil, err } /* @@ -233,7 +303,7 @@ func (dev device) CallAuthorizedMethod(endpoint string, method interface{}) (str soap, err := buildMethodSOAP(string(output)) if err != nil { //log.Printf("error: %v\n", err.Error()) - return "", err + return nil, err } /* @@ -252,7 +322,7 @@ func (dev device) CallAuthorizedMethod(endpoint string, method interface{}) (str soapReq, err := xml.MarshalIndent(auth, "", " ") if err != nil { //log.Printf("error: %v\n", err.Error()) - return "", err + return nil, err } /* diff --git a/README.md b/README.md index 9f3e0a1..da40c9b 100644 --- a/README.md +++ b/README.md @@ -1 +1,171 @@ -# goonvif +# Goonvif +Библиотека **Goonvif** создана для упрощения взаимодействия с ONVIF устройствами. На данный момент в библиотеке реализована поддержка NVT(Network Video Transmitter) устройств, а именно следующих ONVIF сервисов: +- Core или DeviceManagement +- Media +- Imaging +- PTZ +- Analytics +# Dependencies +[etree](https://github.com/beevik/etree) +# Установка +Для установки библиотеки необходимо воспользоваться утилитой go get: +``` +go get github.com/yakovlevdmv/goonvif +``` + +# Начало работы +Чтобы начать работать с камерой, необходимо создать объект `device`. +Для этого необходимо воспользоваться функцией `func NewDevice(xaddr string) (*device, error)`, +которая принимает IP адрес ONVIF устройства и возвращает указатель на созданный объект либо ошибку. +Если камера недоступна, указан неверный адрес для ONVIF сервиса камеры (возможно находится по другому порту) или же камера вообще не поддерживает ONVIF +функция вернет error не являющимся `nil`, а в качестве указателя на объект устройства вернет `nil`. +### Пример подключения к камере +Пусть камера в сети находится по адресу 192.168.13.42, а ее ONVIF сервисы расположены на порте 1234. Тогда, +``` +dev, err := goonvif.NewDevice("192.168.13.42:1234") +``` +сработает успешно, а +``` +dev, err := goonvif.NewDevice("192.168.13.42:80") +``` +вернет нулевой объект камеры и ошибку: +> camera is not available at 192.168.13.42:80 or it does not support ONVIF services + +Модернизируем код, добавив обработку ошибки, и получим: +``` +dev, err := goonvif.NewDevice("192.168.13.42:80") +if err != nil { + panic(err) +} + +///Работа с камерой +``` +### Поддерживаемые ONVIF сервисы +Теперь, когда камера доступна, можно приступать к работе с ней. Однако стандарт ONVIF имеет множество сервисов, а также точку доступа (endpoint) которая не определена стандартом (кроме DeviceManagment: http://onvif_host/onvif/device_service). +Поэтому дальше встает вопрос о поддерживаемых камерой сервисах и определении их endpoint'ов. +Для получения поддерживаемых камерой сервисов необходимо вызвать метод GetCapabilities сервиса DeviceManagement. +Однако эта библиотека автоматизирует данный процесс, поэтому при создании объекта device при помощи `func NewDevice(xaddr string) (*device, error)` +библиотека одновременно обрабатывает поддерживаемые камерой сервисы. Таким образом есть два способа получения поддерживаемых устройством сервисов: +1. Вызвать метод GetCapabilities сервиса DeviceManagement(как это сделать будет рассмотрено дальше) и обработать ответ. +2. Довериться библиотеке и вызвать функцию `func (dev *device)GetServices() map[string]string`, которая вернет map[string]string, ключом которой является название сервиса, а значением - endpoint данного сервиса +### Работа с камерой +Для работы с различными сервисами камерами необходимо отправить корректный SOAP запрос, в теле которого находится вызываемый метод и принимаемые им функции. +**Goonvif** берет на себя работу по созданию корректного SOAP запроса и его отправке. В **Goonvif** определены структуры, для каждой функции каждого (поддерживаемого данной бибилиотекой) сервиса ONVIF: +- [DeviceManagement Service](Device/types.go) + +- [Media Service](Media/types.go) + +- [Imaging Service](Imaging/types.go) + +- [PTZ Service](PTZ/types.go) + +- [Analytics Service](Analytics/types.go) + +[Список всех сервисов стандарта (и документация к ним)](https://www.onvif.org/profiles/specifications/) + +Рассмторим, как организована отправка запросов в **Goonvif** на нескольких примерах. +1. Метод GetCapabilities сервиса DeviceManagement + +Все необходимые типы данных определены в пакете [Device](Device/types.go). +В файле (https://www.onvif.org/ver10/device/wsdl/devicemgmt.wsdl) можно увидеть: +![GetCapabilities](img/exmp_GetCapabilities.png) + +Таким образом, Функция GetCapabilities принимает в качестве аргумента перечисление: +`enum { 'All', 'Analytics', 'Device', 'Events', 'Imaging', 'Media', 'PTZ' }` +Чтобы вызвать данный метод создадим объект `Device.GetCapabilities`: +``` +capabilities := Device.GetCapabilities{Category:"All"} +``` +Для вызова данной функции воспользуемся методом `func (dev device) CallMethod(method interface{}) (string, error)`: +``` +resp, err := dev.CallMethod(capab) +if err != nil { + log.Println(err) +} else { + fmt.Println(resp) +} +``` +2. Создание пользователя методом CreateUsers сервиса DeviceManagement + +Все необходимые типы данных определены в пакете [Device](Device/types.go). +В файле (https://www.onvif.org/ver10/device/wsdl/devicemgmt.wsdl) можно увидеть структуру запроса: +![CreateUsers](img/exmp_CreateUsers.png) + +Создадим объект `Device.CreateUsers`: +``` +createUsers := Device.CreateUsers{User: onvif.User{Username:"korolev", Password:"qwerty", UserLevel:"User"}} +resp, err := dev.CallMethod(createUsers) +if err != nil { + log.Println(err) +} else { + fmt.Println(resp) +} +``` + +В данном примере можно наблюдать использование пакета onvif, в котором определено большинство типов, используемых в поддерживаемых библиотекой сервисах, поэтому при создании структур запросов необходимо это учитывать. + +##### ВАЖНО +Некоторые камеры работают специфично. Это означает, что в зависимости от модели камеры можно не получить ошибки при неправильном запросе. Поэтому советую проверять, точно ли выполнилась операция. Например, для метода CreateUsers надо вывести список всех пользователей и проверить добавился ли пользователь. + +3. Метод ContinuousMove сервиса PTZ + +Все необходимые типы данных определены в пакете [PTZ](PTZ/types.go). +В файле (https://www.onvif.org/ver20/ptz/wsdl/ptz.wsdl) можно увидеть структуру запроса: +![ContinuousMove](img/exmp_ContinuousMove.png) + +Так как данная команда определяется сервисом PTZ, необходимый тип находится в пакете [PTZ](PTZ/types.go). +Из файла [PTZ](PTZ/types.go) можно заметить, что : +> ProfileToken [ReferenceToken] +> A reference to the **MediaProfile** that indicate what should be stopped. + +Таким образом, для того, чтобы начать работать с PTZ сервисом для начала необходимо получить **ProfileToken** сервиса **Media**. Как это сделать смотрите в примере 4. Сейчас же предположим, что нам известен нужный токен. Пусть ProfileToken = "Profile_1". + +Создадим объект `PTZ.ContinuousMove`: +``` +move := PTZ.ContinuousMove{ + ProfileToken:"Profile_1", + Velocity:onvif.PTZSpeed{ + PanTilt:onvif.Vector2D{ + X: 1, + Y: 1, + }, + Zoom:onvif.Vector1D{X:0.5}, + }, +} +``` + +**Заметим**, что объекты Velocity, PanTilt и Zoom определены в пакете onvif. Такое применение свойственно для большинства встроенных в структуру типов. + +Для вызова данной функции воспользуемся методом `func (dev device) CallMethod(method interface{}) (string, error)`: +``` +resp, err := dev.CallMethod(capab) +if err != nil { + log.Println(err) +} else { + fmt.Println(resp) +} +``` + +4. Получение списков Media профилей + +Все необходимые типы данных определены в пакете [Media](Media/types.go). +В файле (https://www.onvif.org/ver10/media/wsdl/media.wsdl) можно увидеть структуру запроса: +![ContinuousMove](img/exmp_GetProfiles.png) + +Вот пример создания и вызова запроса GetProfiles +``` +resp, err := dev.CallMethod(Media.GetProfiles{}) +if err != nil { + panic (err) +} else { + fmt.Println(readResponse(resp)) +} +``` + +**Важно** Обработка response'ов камеры пока не реализована, поэтому данная задача ложится на **Ваши** плечи. Вы можете упростить обработку response'ов при помощи библиотеки [etree](https://github.com/beevik/etree) либо же воспользоваться сервисом (http://www.webtoolkitonline.com/xml-formatter.html) + +**Важно** Многие запросы требуют авторизованного доступа и для того, чтобы добавить аторизацию к конкретной камере, необходимо воспользоваться функцией `func (dev *device) Authenticate(username, password string)`. После применения данной функции все отправляемые камерой запросы будут авторизованными. +``` +device := onvif.NewDevice("192.168.13.42:1234") +device.Authenticate("username", "password") +``` diff --git a/img/exmp_ContinuousMove.png b/img/exmp_ContinuousMove.png new file mode 100644 index 0000000..6ba4847 Binary files /dev/null and b/img/exmp_ContinuousMove.png differ diff --git a/img/exmp_CreateUsers.png b/img/exmp_CreateUsers.png new file mode 100644 index 0000000..790f98a Binary files /dev/null and b/img/exmp_CreateUsers.png differ diff --git a/img/exmp_GetCapabilities.png b/img/exmp_GetCapabilities.png new file mode 100644 index 0000000..60843cf Binary files /dev/null and b/img/exmp_GetCapabilities.png differ diff --git a/img/exmp_GetProfiles.png b/img/exmp_GetProfiles.png new file mode 100644 index 0000000..6e99ceb Binary files /dev/null and b/img/exmp_GetProfiles.png differ diff --git a/networking/networking.go b/networking/networking.go index bd1a69d..ae3bbad 100644 --- a/networking/networking.go +++ b/networking/networking.go @@ -3,20 +3,26 @@ package networking import ( "net/http" "bytes" - "io/ioutil" ) -func SendSoap(endpoint, message string) (string, error) { +func SendSoap(endpoint, message string) (*http.Response, error) { httpClient := new(http.Client) resp, err := httpClient.Post(endpoint, "application/soap+xml; charset=utf-8", bytes.NewBufferString(message)) if err != nil { - return "", err - } - b, err := ioutil.ReadAll(resp.Body) - if err != nil { - return "", err + return resp, err } - return string(b),nil + /*if resp.StatusCode != http.StatusOK && resp.StatusCode != http.StatusBadRequest { + return "", errors.New("error: got HTTP response status " + strconv.Itoa(resp.StatusCode)) + }*/ + //b, err := ioutil.ReadAll(resp.Body) + //if err != nil { + // return resp, err + //} + //fmt.Println(endpoint) + //fmt.Println(string(b)) + //log.Println(resp.StatusCode) + + return resp,nil }