Compare commits

...

3 Commits

Author SHA1 Message Date
Cedric Verstraeten
ebd655ac73 Allow remote configuration through MQTT + restructure config method 2023-09-12 10:50:36 +02:00
Cedric Verstraeten
6325e37aae empty presets caused hub connection failing 2023-09-07 08:16:46 +02:00
Cedric Verstraeten
ecabc47847 integrate ondevice configurated presets 2023-08-30 14:12:07 +02:00
16 changed files with 607 additions and 41 deletions

View File

@@ -54,6 +54,35 @@ const docTemplate = `{
}
}
},
"/api/camera/onvif/gotopreset": {
"post": {
"description": "Will activate the desired ONVIF preset.",
"tags": [
"camera"
],
"summary": "Will activate the desired ONVIF preset.",
"operationId": "camera-onvif-gotopreset",
"parameters": [
{
"description": "OnvifPreset",
"name": "config",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/models.OnvifPreset"
}
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/models.APIResponse"
}
}
}
}
},
"/api/camera/onvif/login": {
"post": {
"description": "Try to login into ONVIF supported camera.",
@@ -112,6 +141,35 @@ const docTemplate = `{
}
}
},
"/api/camera/onvif/presets": {
"post": {
"description": "Will return the ONVIF presets for the specific camera.",
"tags": [
"camera"
],
"summary": "Will return the ONVIF presets for the specific camera.",
"operationId": "camera-onvif-presets",
"parameters": [
{
"description": "OnvifCredentials",
"name": "config",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/models.OnvifCredentials"
}
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/models.APIResponse"
}
}
}
}
},
"/api/camera/onvif/zoom": {
"post": {
"description": "Zooming in or out the camera.",
@@ -317,8 +375,15 @@ const docTemplate = `{
"models.APIResponse": {
"type": "object",
"properties": {
"can_pan_tilt": {
"type": "boolean"
},
"can_zoom": {
"type": "boolean"
},
"data": {},
"message": {}
"message": {},
"ptz_functions": {}
}
},
"models.Authentication": {
@@ -621,6 +686,17 @@ const docTemplate = `{
}
}
},
"models.OnvifPreset": {
"type": "object",
"properties": {
"onvif_credentials": {
"$ref": "#/definitions/models.OnvifCredentials"
},
"preset": {
"type": "string"
}
}
},
"models.OnvifZoom": {
"type": "object",
"properties": {

View File

@@ -46,6 +46,35 @@
}
}
},
"/api/camera/onvif/gotopreset": {
"post": {
"description": "Will activate the desired ONVIF preset.",
"tags": [
"camera"
],
"summary": "Will activate the desired ONVIF preset.",
"operationId": "camera-onvif-gotopreset",
"parameters": [
{
"description": "OnvifPreset",
"name": "config",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/models.OnvifPreset"
}
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/models.APIResponse"
}
}
}
}
},
"/api/camera/onvif/login": {
"post": {
"description": "Try to login into ONVIF supported camera.",
@@ -104,6 +133,35 @@
}
}
},
"/api/camera/onvif/presets": {
"post": {
"description": "Will return the ONVIF presets for the specific camera.",
"tags": [
"camera"
],
"summary": "Will return the ONVIF presets for the specific camera.",
"operationId": "camera-onvif-presets",
"parameters": [
{
"description": "OnvifCredentials",
"name": "config",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/models.OnvifCredentials"
}
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/models.APIResponse"
}
}
}
}
},
"/api/camera/onvif/zoom": {
"post": {
"description": "Zooming in or out the camera.",
@@ -309,8 +367,15 @@
"models.APIResponse": {
"type": "object",
"properties": {
"can_pan_tilt": {
"type": "boolean"
},
"can_zoom": {
"type": "boolean"
},
"data": {},
"message": {}
"message": {},
"ptz_functions": {}
}
},
"models.Authentication": {
@@ -613,6 +678,17 @@
}
}
},
"models.OnvifPreset": {
"type": "object",
"properties": {
"onvif_credentials": {
"$ref": "#/definitions/models.OnvifCredentials"
},
"preset": {
"type": "string"
}
}
},
"models.OnvifZoom": {
"type": "object",
"properties": {

View File

@@ -2,8 +2,13 @@ basePath: /
definitions:
models.APIResponse:
properties:
can_pan_tilt:
type: boolean
can_zoom:
type: boolean
data: {}
message: {}
ptz_functions: {}
type: object
models.Authentication:
properties:
@@ -202,6 +207,13 @@ definitions:
tilt:
type: number
type: object
models.OnvifPreset:
properties:
onvif_credentials:
$ref: '#/definitions/models.OnvifCredentials'
preset:
type: string
type: object
models.OnvifZoom:
properties:
onvif_credentials:
@@ -310,6 +322,25 @@ paths:
summary: Will return the ONVIF capabilities for the specific camera.
tags:
- camera
/api/camera/onvif/gotopreset:
post:
description: Will activate the desired ONVIF preset.
operationId: camera-onvif-gotopreset
parameters:
- description: OnvifPreset
in: body
name: config
required: true
schema:
$ref: '#/definitions/models.OnvifPreset'
responses:
"200":
description: OK
schema:
$ref: '#/definitions/models.APIResponse'
summary: Will activate the desired ONVIF preset.
tags:
- camera
/api/camera/onvif/login:
post:
description: Try to login into ONVIF supported camera.
@@ -348,6 +379,25 @@ paths:
summary: Panning or/and tilting the camera.
tags:
- camera
/api/camera/onvif/presets:
post:
description: Will return the ONVIF presets for the specific camera.
operationId: camera-onvif-presets
parameters:
- description: OnvifCredentials
in: body
name: config
required: true
schema:
$ref: '#/definitions/models.OnvifCredentials'
responses:
"200":
description: OK
schema:
$ref: '#/definitions/models.APIResponse'
summary: Will return the ONVIF presets for the specific camera.
tags:
- camera
/api/camera/onvif/zoom:
post:
description: Zooming in or out the camera.

View File

@@ -3,7 +3,7 @@ module github.com/kerberos-io/agent/machinery
go 1.19
// replace github.com/kerberos-io/joy4 v1.0.57 => ../../../../github.com/kerberos-io/joy4
// replace github.com/kerberos-io/onvif v0.0.5 => ../../../../github.com/kerberos-io/onvif
// replace github.com/kerberos-io/onvif v0.0.6 => ../../../../github.com/kerberos-io/onvif
require (
github.com/InVisionApp/conjungo v1.1.0
@@ -25,7 +25,7 @@ require (
github.com/gorilla/websocket v1.5.0
github.com/kellydunn/golang-geo v0.7.0
github.com/kerberos-io/joy4 v1.0.58
github.com/kerberos-io/onvif v0.0.6
github.com/kerberos-io/onvif v0.0.7
github.com/minio/minio-go/v6 v6.0.57
github.com/nsmith5/mjpeg v0.0.0-20200913181537-54b8ada0e53e
github.com/op/go-logging v0.0.0-20160315200505-970db520ece7

View File

@@ -266,8 +266,8 @@ github.com/kellydunn/golang-geo v0.7.0 h1:A5j0/BvNgGwY6Yb6inXQxzYwlPHc6WVZR+Mrar
github.com/kellydunn/golang-geo v0.7.0/go.mod h1:YYlQPJ+DPEzrHx8kT3oPHC/NjyvCCXE+IuKGKdrjrcU=
github.com/kerberos-io/joy4 v1.0.58 h1:R8EECSF+bG7o2yHC6cX/lF77Z+bDVGl6OioLZ3+5MN4=
github.com/kerberos-io/joy4 v1.0.58/go.mod h1:nZp4AjvKvTOXRrmDyAIOw+Da+JA5OcSo/JundGfOlFU=
github.com/kerberos-io/onvif v0.0.6 h1:+nvDuxGzQgHjc7V7kiYxUIcw1bO6R9esAMcxWRiKcwA=
github.com/kerberos-io/onvif v0.0.6/go.mod h1:Hr2dJOH2LM5SpYKk17gYZ1CMjhGhUl+QlT5kwYogrW0=
github.com/kerberos-io/onvif v0.0.7 h1:LIrXjTH7G2W9DN69xZeJSB0uS3W1+C3huFO8kTqx7/A=
github.com/kerberos-io/onvif v0.0.7/go.mod h1:Hr2dJOH2LM5SpYKk17gYZ1CMjhGhUl+QlT5kwYogrW0=
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
github.com/klauspost/compress v1.13.6/go.mod h1:/3/Vjq9QcHkK5uEr5lBEmyoZ1iFhe47etQ6QUkpK6sk=
github.com/klauspost/compress v1.15.0 h1:xqfchp4whNFxn5A4XFyyYtitiWI8Hy5EW59jEwcyL6U=

View File

@@ -9,6 +9,8 @@ import (
"github.com/kerberos-io/agent/machinery/src/components"
"github.com/kerberos-io/agent/machinery/src/log"
"github.com/kerberos-io/agent/machinery/src/models"
configService "github.com/kerberos-io/agent/machinery/src/config"
"github.com/kerberos-io/agent/machinery/src/routers"
"github.com/kerberos-io/agent/machinery/src/utils"
"gopkg.in/DataDog/dd-trace-go.v1/ddtrace/tracer"
@@ -92,10 +94,10 @@ func main() {
configuration.Port = port
// Open this configuration either from Kerberos Agent or Kerberos Factory.
components.OpenConfig(configDirectory, &configuration)
configService.OpenConfig(configDirectory, &configuration)
// We will override the configuration with the environment variables
components.OverrideWithEnvironmentVariables(&configuration)
configService.OverrideWithEnvironmentVariables(&configuration)
// Printing final configuration
utils.PrintConfiguration(&configuration)
@@ -113,7 +115,7 @@ func main() {
if configuration.Config.Key == "" {
key := utils.RandStringBytesMaskImpr(30)
configuration.Config.Key = key
err := components.StoreConfig(configDirectory, configuration.Config)
err := configService.StoreConfig(configDirectory, configuration.Config)
if err == nil {
log.Log.Info("Main: updated unique key for agent to: " + key)
} else {

View File

@@ -279,6 +279,8 @@ loop:
onvifEnabled := "false"
onvifZoom := "false"
onvifPanTilt := "false"
onvifPresets := "false"
var onvifPresetsList []byte
if config.Capture.IPCamera.ONVIFXAddr != "" {
cameraConfiguration := configuration.Config.Capture.IPCamera
device, err := onvif.ConnectToOnvifDevice(&cameraConfiguration)
@@ -293,8 +295,34 @@ loop:
if canPanTilt {
onvifPanTilt = "true"
}
// Try to read out presets
presets, err := onvif.GetPresetsFromDevice(device)
if err == nil && len(presets) > 0 {
onvifPresets = "true"
onvifPresetsList, err = json.Marshal(presets)
if err != nil {
log.Log.Error("HandleHeartBeat: error while marshalling presets: " + err.Error())
onvifPresetsList = []byte("[]")
}
} else {
if err != nil {
log.Log.Error("HandleHeartBeat: error while getting presets: " + err.Error())
} else {
log.Log.Debug("HandleHeartBeat: no presets found.")
}
onvifPresetsList = []byte("[]")
}
} else {
log.Log.Error("HandleHeartBeat: error while getting PTZ configurations: " + err.Error())
onvifPresetsList = []byte("[]")
}
} else {
log.Log.Error("HandleHeartBeat: error while connecting to ONVIF device: " + err.Error())
onvifPresetsList = []byte("[]")
}
} else {
log.Log.Debug("HandleHeartBeat: ONVIF is not enabled.")
onvifPresetsList = []byte("[]")
}
// Check if the agent is running inside a cluster (Kerberos Factory) or as
@@ -339,6 +367,8 @@ loop:
"onvif" : "%s",
"onvif_zoom" : "%s",
"onvif_pantilt" : "%s",
"onvif_presets": "%s",
"onvif_presets_list": %s,
"cameraConnected": "%s",
"numberoffiles" : "33",
"timestamp" : 1564747908,
@@ -346,7 +376,7 @@ loop:
"docker" : true,
"kios" : false,
"raspberrypi" : false
}`, config.Key, system.Version, system.CPUId, username, key, name, isEnterprise, system.Hostname, system.Architecture, system.TotalMemory, system.UsedMemory, system.FreeMemory, system.ProcessUsedMemory, macs, ips, "0", "0", "0", uptimeString, boottimeString, config.HubSite, onvifEnabled, onvifZoom, onvifPanTilt, cameraConnected)
}`, config.Key, system.Version, system.CPUId, username, key, name, isEnterprise, system.Hostname, system.Architecture, system.TotalMemory, system.UsedMemory, system.FreeMemory, system.ProcessUsedMemory, macs, ips, "0", "0", "0", uptimeString, boottimeString, config.HubSite, onvifEnabled, onvifZoom, onvifPanTilt, onvifPresets, onvifPresetsList, cameraConnected)
var jsonStr = []byte(object)
buffy := bytes.NewBuffer(jsonStr)

View File

@@ -14,6 +14,7 @@ import (
"github.com/kerberos-io/agent/machinery/src/capture"
"github.com/kerberos-io/agent/machinery/src/cloud"
"github.com/kerberos-io/agent/machinery/src/computervision"
configService "github.com/kerberos-io/agent/machinery/src/config"
"github.com/kerberos-io/agent/machinery/src/log"
"github.com/kerberos-io/agent/machinery/src/models"
"github.com/kerberos-io/agent/machinery/src/onvif"
@@ -72,7 +73,7 @@ func Bootstrap(configDirectory string, configuration *models.Configuration, comm
// We'll create a MQTT handler, which will be used to communicate with Kerberos Hub.
// Configure a MQTT client which helps for a bi-directional communication
mqttClient := routers.ConfigureMQTT(configuration, communication)
mqttClient := routers.ConfigureMQTT(configDirectory, configuration, communication)
// Run the agent and fire up all the other
// goroutines which do image capture, motion detection, onvif, etc.
@@ -87,15 +88,15 @@ func Bootstrap(configDirectory string, configuration *models.Configuration, comm
if status == "not started" {
// We will re open the configuration, might have changed :O!
OpenConfig(configDirectory, configuration)
configService.OpenConfig(configDirectory, configuration)
// We will override the configuration with the environment variables
OverrideWithEnvironmentVariables(configuration)
configService.OverrideWithEnvironmentVariables(configuration)
}
// Reset the MQTT client, might have provided new information, so we need to reconnect.
if routers.HasMQTTClientModified(configuration) {
routers.DisconnectMQTT(mqttClient, &configuration.Config)
mqttClient = routers.ConfigureMQTT(configuration, communication)
mqttClient = routers.ConfigureMQTT(configDirectory, configuration, communication)
}
// We will create a new cancelable context, which will be used to cancel and restart.
@@ -134,6 +135,10 @@ func RunAgent(configDirectory string, configuration *models.Configuration, commu
width := videoStream.(av.VideoCodecData).Width()
height := videoStream.(av.VideoCodecData).Height()
// Set config values as well
configuration.Config.Capture.IPCamera.Width = width
configuration.Config.Capture.IPCamera.Height = height
var queue *pubsub.Queue
var subQueue *pubsub.Queue
@@ -162,6 +167,13 @@ func RunAgent(configDirectory string, configuration *models.Configuration, commu
time.Sleep(time.Second * 3)
return status
}
width := videoStream.(av.VideoCodecData).Width()
height := videoStream.(av.VideoCodecData).Height()
// Set config values as well
configuration.Config.Capture.IPCamera.Width = width
configuration.Config.Capture.IPCamera.Height = height
}
if cameraSettings.RTSP != rtspUrl || cameraSettings.SubRTSP != subRtspUrl || cameraSettings.Width != width || cameraSettings.Height != height || cameraSettings.Num != num || cameraSettings.Denum != denum || cameraSettings.Codec != videoStream.(av.VideoCodecData).Type() {
@@ -190,6 +202,7 @@ func RunAgent(configDirectory string, configuration *models.Configuration, commu
cameraSettings.Denum = denum
cameraSettings.Codec = videoStream.(av.VideoCodecData).Type()
cameraSettings.Initialized = true
} else {
log.Log.Info("RunAgent: camera settings did not change, keeping decoder")
}
@@ -285,10 +298,10 @@ func RunAgent(configDirectory string, configuration *models.Configuration, commu
(*communication.CancelContext)()
// We will re open the configuration, might have changed :O!
OpenConfig(configDirectory, configuration)
configService.OpenConfig(configDirectory, configuration)
// We will override the configuration with the environment variables
OverrideWithEnvironmentVariables(configuration)
configService.OverrideWithEnvironmentVariables(configuration)
// Here we are cleaning up everything!
if configuration.Config.Offline != "true" {

View File

@@ -1,4 +1,4 @@
package components
package config
import (
"context"
@@ -210,7 +210,7 @@ func OverrideWithEnvironmentVariables(configuration *models.Configuration) {
configuration.Config.Key = value
break
case "AGENT_NAME":
configuration.Config.Name = value
configuration.Config.FriendlyName = value
break
case "AGENT_TIMEZONE":
configuration.Config.Timezone = value

View File

@@ -29,3 +29,8 @@ type OnvifZoom struct {
OnvifCredentials OnvifCredentials `json:"onvif_credentials,omitempty" bson:"onvif_credentials"`
Zoom float64 `json:"zoom,omitempty" bson:"zoom"`
}
type OnvifPreset struct {
OnvifCredentials OnvifCredentials `json:"onvif_credentials,omitempty" bson:"onvif_credentials"`
Preset string `json:"preset,omitempty" bson:"preset"`
}

View File

@@ -70,9 +70,11 @@ type Capture struct {
// IPCamera configuration, such as the RTSP url of the IPCamera and the FPS.
// Also includes ONVIF integration
type IPCamera struct {
Width int `json:"width"`
Height int `json:"height"`
FPS string `json:"fps"`
RTSP string `json:"rtsp"`
SubRTSP string `json:"sub_rtsp"`
FPS string `json:"fps"`
ONVIF string `json:"onvif,omitempty" bson:"onvif"`
ONVIFXAddr string `json:"onvif_xaddr,omitempty" bson:"onvif_xaddr"`
ONVIFUsername string `json:"onvif_username,omitempty" bson:"onvif_username"`

View File

@@ -15,4 +15,10 @@ type OnvifActionPTZ struct {
X float64 `json:"x" bson:"x"`
Y float64 `json:"y" bson:"y"`
Z float64 `json:"z" bson:"z"`
Preset string `json:"preset" bson:"preset"`
}
type OnvifActionPreset struct {
Name string `json:"name" bson:"name"`
Token string `json:"token" bson:"token"`
}

View File

@@ -78,6 +78,8 @@ func HandleONVIFActions(configuration *models.Configuration, communication *mode
err = AbsolutePanTiltMoveFake(device, configurations, token, x, y, z)
if err != nil {
log.Log.Error("HandleONVIFActions (AbsolutePanTitleMoveFake): " + err.Error())
} else {
log.Log.Info("HandleONVIFActions (AbsolutePanTitleMoveFake): successfully moved camera")
}
/*if canAbsoluteMove {
@@ -92,6 +94,17 @@ func HandleONVIFActions(configuration *models.Configuration, communication *mode
}
}*/
} else if onvifAction.Action == "preset" {
// Execute the preset
preset := ptzAction.Preset
err := GoToPresetFromDevice(device, preset)
if err != nil {
log.Log.Error("HandleONVIFActions (GotoPreset): " + err.Error())
} else {
log.Log.Info("HandleONVIFActions (GotoPreset): successfully moved camera")
}
} else if onvifAction.Action == "ptz" {
if err == nil {
@@ -311,7 +324,7 @@ func AbsolutePanTiltMove(device *onvif.Device, configuration ptz.GetConfiguratio
}
bs, _ := ioutil.ReadAll(res.Body)
log.Log.Debug("AbsoluteMove: " + string(bs))
log.Log.Info("AbsoluteMove: " + string(bs))
return err
}
@@ -330,18 +343,17 @@ func AbsolutePanTiltMoveFake(device *onvif.Device, configuration ptz.GetConfigur
speed := 0.6
wait := 100 * time.Millisecond
err := ZoomOutCompletely(device, configuration, token)
// We'll move quickly to the position (might be inaccurate)
err = PanUntilPosition(device, configuration, token, pan, speed, wait)
err = TiltUntilPosition(device, configuration, token, tilt, speed, wait)
err = ZoomOutCompletely(device, configuration, token)
err = PanUntilPosition(device, configuration, token, pan, zoom, speed, wait)
err = TiltUntilPosition(device, configuration, token, tilt, zoom, speed, wait)
// Now we'll move a bit slower to make sure we are ok (will be more accurate)
speed = 0.2
speed = 0.1
wait = 200 * time.Millisecond
err = PanUntilPosition(device, configuration, token, pan, speed, wait)
err = TiltUntilPosition(device, configuration, token, tilt, speed, wait)
err = PanUntilPosition(device, configuration, token, pan, zoom, speed, wait)
err = TiltUntilPosition(device, configuration, token, tilt, zoom, speed, wait)
err = ZoomUntilPosition(device, configuration, token, zoom, speed, wait)
return err
@@ -376,10 +388,10 @@ func ZoomOutCompletely(device *onvif.Device, configuration ptz.GetConfigurations
return err
}
func PanUntilPosition(device *onvif.Device, configuration ptz.GetConfigurationsResponse, token xsd.ReferenceToken, pan float64, speed float64, wait time.Duration) error {
func PanUntilPosition(device *onvif.Device, configuration ptz.GetConfigurationsResponse, token xsd.ReferenceToken, pan float64, zoom float64, speed float64, wait time.Duration) error {
position, err := GetPosition(device, token)
if position.PanTilt.X >= pan-0.01 && position.PanTilt.X <= pan+0.01 {
if position.PanTilt.X >= pan-0.005 && position.PanTilt.X <= pan+0.005 {
} else {
@@ -422,6 +434,7 @@ func PanUntilPosition(device *onvif.Device, configuration ptz.GetConfigurationsR
_, errStop := device.CallMethod(ptz.Stop{
ProfileToken: token,
PanTilt: true,
Zoom: true,
})
if errStop != nil {
@@ -431,10 +444,10 @@ func PanUntilPosition(device *onvif.Device, configuration ptz.GetConfigurationsR
return err
}
func TiltUntilPosition(device *onvif.Device, configuration ptz.GetConfigurationsResponse, token xsd.ReferenceToken, tilt float64, speed float64, wait time.Duration) error {
func TiltUntilPosition(device *onvif.Device, configuration ptz.GetConfigurationsResponse, token xsd.ReferenceToken, tilt float64, zoom float64, speed float64, wait time.Duration) error {
position, err := GetPosition(device, token)
if position.PanTilt.Y >= tilt-0.01 && position.PanTilt.Y <= tilt+0.01 {
if position.PanTilt.Y >= tilt-0.005 && position.PanTilt.Y <= tilt+0.005 {
} else {
@@ -477,6 +490,7 @@ func TiltUntilPosition(device *onvif.Device, configuration ptz.GetConfigurations
_, errStop := device.CallMethod(ptz.Stop{
ProfileToken: token,
PanTilt: true,
Zoom: true,
})
if errStop != nil {
@@ -489,7 +503,7 @@ func TiltUntilPosition(device *onvif.Device, configuration ptz.GetConfigurations
func ZoomUntilPosition(device *onvif.Device, configuration ptz.GetConfigurationsResponse, token xsd.ReferenceToken, zoom float64, speed float64, wait time.Duration) error {
position, err := GetPosition(device, token)
if position.Zoom.X >= zoom-0.01 && position.Zoom.X <= zoom+0.01 {
if position.Zoom.X >= zoom-0.005 && position.Zoom.X <= zoom+0.005 {
} else {
@@ -530,6 +544,7 @@ func ZoomUntilPosition(device *onvif.Device, configuration ptz.GetConfigurations
_, errStop := device.CallMethod(ptz.Stop{
ProfileToken: token,
PanTilt: true,
Zoom: true,
})
@@ -635,6 +650,89 @@ func GetCapabilitiesFromDevice(device *onvif.Device) []string {
return capabilities
}
func GetPresetsFromDevice(device *onvif.Device) ([]models.OnvifActionPreset, error) {
var presets []models.OnvifActionPreset
var presetsResponse ptz.GetPresetsResponse
// Get token from the first profile
token, err := GetTokenFromProfile(device, 0)
if err == nil {
resp, err := device.CallMethod(ptz.GetPresets{
ProfileToken: token,
})
defer resp.Body.Close()
b, err := io.ReadAll(resp.Body)
if err == nil {
stringBody := string(b)
decodedXML, et, err := getXMLNode(stringBody, "GetPresetsResponse")
if err != nil {
log.Log.Error("GetPresetsFromDevice: " + err.Error())
return presets, err
} else {
if err := decodedXML.DecodeElement(&presetsResponse, et); err != nil {
log.Log.Error("GetPresetsFromDevice: " + err.Error())
return presets, err
}
for _, preset := range presetsResponse.Preset {
p := models.OnvifActionPreset{
Name: string(preset.Name),
Token: string(preset.Token),
}
presets = append(presets, p)
}
return presets, err
}
} else {
log.Log.Error("GetPresetsFromDevice: " + err.Error())
}
} else {
log.Log.Error("GetPresetsFromDevice: " + err.Error())
}
return presets, err
}
func GoToPresetFromDevice(device *onvif.Device, presetName string) error {
var goToPresetResponse ptz.GotoPresetResponse
// Get token from the first profile
token, err := GetTokenFromProfile(device, 0)
if err == nil {
resp, err := device.CallMethod(ptz.GotoPreset{
ProfileToken: token,
PresetToken: xsd.ReferenceToken(presetName),
})
defer resp.Body.Close()
b, err := io.ReadAll(resp.Body)
if err == nil {
stringBody := string(b)
decodedXML, et, err := getXMLNode(stringBody, "GotoPresetResponses")
if err != nil {
log.Log.Error("GoToPresetFromDevice: " + err.Error())
return err
} else {
if err := decodedXML.DecodeElement(&goToPresetResponse, et); err != nil {
log.Log.Error("GoToPresetFromDevice: " + err.Error())
return err
}
return err
}
} else {
log.Log.Error("GoToPresetFromDevice: " + err.Error())
}
} else {
log.Log.Error("GoToPresetFromDevice: " + err.Error())
}
return err
}
func getXMLNode(xmlBody string, nodeName string) (*xml.Decoder, *xml.StartElement, error) {
xmlBytes := bytes.NewBufferString(xmlBody)
decodedXML := xml.NewDecoder(xmlBytes)

View File

@@ -250,3 +250,105 @@ func DoOnvifZoom(c *gin.Context) {
})
}
}
// GetOnvifPresets godoc
// @Router /api/camera/onvif/presets [post]
// @ID camera-onvif-presets
// @Tags camera
// @Param config body models.OnvifCredentials true "OnvifCredentials"
// @Summary Will return the ONVIF presets for the specific camera.
// @Description Will return the ONVIF presets for the specific camera.
// @Success 200 {object} models.APIResponse
func GetOnvifPresets(c *gin.Context) {
var onvifCredentials models.OnvifCredentials
err := c.BindJSON(&onvifCredentials)
if err == nil && onvifCredentials.ONVIFXAddr != "" {
configuration := &models.Configuration{
Config: models.Config{
Capture: models.Capture{
IPCamera: models.IPCamera{
ONVIFXAddr: onvifCredentials.ONVIFXAddr,
ONVIFUsername: onvifCredentials.ONVIFUsername,
ONVIFPassword: onvifCredentials.ONVIFPassword,
},
},
},
}
cameraConfiguration := configuration.Config.Capture.IPCamera
device, err := onvif.ConnectToOnvifDevice(&cameraConfiguration)
if err == nil {
presets, err := onvif.GetPresetsFromDevice(device)
if err == nil {
c.JSON(200, gin.H{
"presets": presets,
})
} else {
c.JSON(400, gin.H{
"data": "Something went wrong: " + err.Error(),
})
}
} else {
c.JSON(400, gin.H{
"data": "Something went wrong: " + err.Error(),
})
}
} else {
c.JSON(400, gin.H{
"data": "Something went wrong: " + err.Error(),
})
}
}
// GoToOnvifPReset godoc
// @Router /api/camera/onvif/gotopreset [post]
// @ID camera-onvif-gotopreset
// @Tags camera
// @Param config body models.OnvifPreset true "OnvifPreset"
// @Summary Will activate the desired ONVIF preset.
// @Description Will activate the desired ONVIF preset.
// @Success 200 {object} models.APIResponse
func GoToOnvifPreset(c *gin.Context) {
var onvifPreset models.OnvifPreset
err := c.BindJSON(&onvifPreset)
if err == nil && onvifPreset.OnvifCredentials.ONVIFXAddr != "" {
configuration := &models.Configuration{
Config: models.Config{
Capture: models.Capture{
IPCamera: models.IPCamera{
ONVIFXAddr: onvifPreset.OnvifCredentials.ONVIFXAddr,
ONVIFUsername: onvifPreset.OnvifCredentials.ONVIFUsername,
ONVIFPassword: onvifPreset.OnvifCredentials.ONVIFPassword,
},
},
},
}
cameraConfiguration := configuration.Config.Capture.IPCamera
device, err := onvif.ConnectToOnvifDevice(&cameraConfiguration)
if err == nil {
err := onvif.GoToPresetFromDevice(device, onvifPreset.Preset)
if err == nil {
c.JSON(200, gin.H{
"data": "Camera preset activated: " + onvifPreset.Preset,
})
} else {
c.JSON(400, gin.H{
"data": "Something went wrong: " + err.Error(),
})
}
} else {
c.JSON(400, gin.H{
"data": "Something went wrong: " + err.Error(),
})
}
} else {
c.JSON(400, gin.H{
"data": "Something went wrong: " + err.Error(),
})
}
}

View File

@@ -12,6 +12,7 @@ import (
"github.com/kerberos-io/agent/machinery/src/cloud"
"github.com/kerberos-io/agent/machinery/src/components"
configService "github.com/kerberos-io/agent/machinery/src/config"
"github.com/kerberos-io/agent/machinery/src/log"
"github.com/kerberos-io/agent/machinery/src/models"
"github.com/kerberos-io/agent/machinery/src/utils"
@@ -40,7 +41,7 @@ func AddRoutes(r *gin.Engine, authMiddleware *jwt.GinJWTMiddleware, configDirect
var config models.Config
err := c.BindJSON(&config)
if err == nil {
err := components.SaveConfig(configDirectory, config, configuration, communication)
err := configService.SaveConfig(configDirectory, config, configuration, communication)
if err == nil {
c.JSON(200, gin.H{
"data": "☄ Reconfiguring",
@@ -165,7 +166,7 @@ func AddRoutes(r *gin.Engine, authMiddleware *jwt.GinJWTMiddleware, configDirect
var config models.Config
err := c.BindJSON(&config)
if err == nil {
err := components.SaveConfig(configDirectory, config, configuration, communication)
err := configService.SaveConfig(configDirectory, config, configuration, communication)
if err == nil {
c.JSON(200, gin.H{
"data": "☄ Reconfiguring",
@@ -215,7 +216,7 @@ func AddRoutes(r *gin.Engine, authMiddleware *jwt.GinJWTMiddleware, configDirect
// We will only send an image once per second.
time.Sleep(time.Second * 1)
log.Log.Info("AddRoutes (/stream): reading from MJPEG stream")
img, err := components.GetImageFromFilePath(configDirectory)
img, err := configService.GetImageFromFilePath(configDirectory)
return img, err
}
h := components.StartMotionJPEG(imageFunction, 80)
@@ -227,6 +228,8 @@ func AddRoutes(r *gin.Engine, authMiddleware *jwt.GinJWTMiddleware, configDirect
// the camera.
api.POST("/camera/onvif/login", LoginToOnvif)
api.POST("/camera/onvif/capabilities", GetOnvifCapabilities)
api.POST("/camera/onvif/presets", GetOnvifPresets)
api.POST("/camera/onvif/gotopreset", GoToOnvifPreset)
api.POST("/camera/onvif/pantilt", DoOnvifPanTilt)
api.POST("/camera/onvif/zoom", DoOnvifZoom)
api.POST("/camera/verify/:streamType", capture.VerifyCamera)

View File

@@ -8,6 +8,8 @@ import (
"time"
mqtt "github.com/eclipse/paho.mqtt.golang"
"github.com/gofrs/uuid"
configService "github.com/kerberos-io/agent/machinery/src/config"
"github.com/kerberos-io/agent/machinery/src/log"
"github.com/kerberos-io/agent/machinery/src/models"
"github.com/kerberos-io/agent/machinery/src/onvif"
@@ -55,12 +57,22 @@ func HasMQTTClientModified(configuration *models.Configuration) bool {
}
func PackageMQTTMessage(msg Message) ([]byte, error) {
// We'll generate an unique id, and encrypt it using the private key.
msg.Mid = "0123456789+1"
// Create a Version 4 UUID.
u2, err := uuid.NewV4()
if err != nil {
log.Log.Error("failed to generate UUID: " + err.Error())
}
// We'll generate an unique id, and encrypt / decrypt it using the private key if available.
msg.Mid = u2.String()
msg.Timestamp = time.Now().Unix()
// At the moment we don't do the encryption part, but we'll implement it
// once the legacy methods (subscriptions are moved).
msg.Encrypted = false
msg.PublicKey = ""
msg.Fingerprint = ""
payload, err := json.Marshal(msg)
return payload, err
}
@@ -98,7 +110,7 @@ func PackageMQTTMessage(msg Message) ([]byte, error) {
// - {devicekey}/{sessionid}/answer: once a WebRTC request is received through (kerberos/register), we'll draft an answer and send it back to the remote WebRTC client.
// - kerberos/{hubkey}/device/{devicekey}/motion: a motion signal
func ConfigureMQTT(configuration *models.Configuration, communication *models.Communication) mqtt.Client {
func ConfigureMQTT(configDirectory string, configuration *models.Configuration, communication *models.Communication) mqtt.Client {
config := configuration.Config
@@ -174,8 +186,9 @@ func ConfigureMQTT(configuration *models.Configuration, communication *models.Co
log.Log.Info("ConfigureMQTT: " + mqttClientID + " connected to " + mqttURL)
// Create a susbcription for listen and reply
MQTTListenerHandler(c, hubKey, configuration, communication)
MQTTListenerHandler(c, hubKey, configDirectory, configuration, communication)
// Legacy methods below -> should be converted to the above method.
// Create a subscription to know if send out a livestream or not.
MQTTListenerHandleLiveSD(c, hubKey, configuration, communication)
@@ -207,7 +220,7 @@ func ConfigureMQTT(configuration *models.Configuration, communication *models.Co
return nil
}
func MQTTListenerHandler(mqttClient mqtt.Client, hubKey string, configuration *models.Configuration, communication *models.Communication) {
func MQTTListenerHandler(mqttClient mqtt.Client, hubKey string, configDirectory string, configuration *models.Configuration, communication *models.Communication) {
if hubKey == "" {
log.Log.Info("MQTTListenerHandler: no hub key provided, not subscribing to kerberos/hub/{hubkey}")
} else {
@@ -225,6 +238,7 @@ func MQTTListenerHandler(mqttClient mqtt.Client, hubKey string, configuration *m
var message Message
json.Unmarshal(msg.Payload(), &message)
if message.Mid != "" && message.Timestamp != 0 {
// Messages might be encrypted, if so we'll
// need to decrypt them.
@@ -249,6 +263,10 @@ func MQTTListenerHandler(mqttClient mqtt.Client, hubKey string, configuration *m
HandleGetPTZPosition(mqttClient, hubKey, payload, configuration, communication)
case "update-ptz-position":
HandleUpdatePTZPosition(mqttClient, hubKey, payload, configuration, communication)
case "request-config":
HandleRequestConfig(mqttClient, hubKey, payload, configuration, communication)
case "update-config":
HandleUpdateConfig(mqttClient, hubKey, payload, configDirectory, configuration, communication)
}
}
}
@@ -336,10 +354,95 @@ func HandleUpdatePTZPosition(mqttClient mqtt.Client, hubKey string, payload Payl
}
}
// We received a request config request, we'll fetch the current config and send it back.
type RequestConfigPayload struct {
Timestamp int64 `json:"timestamp"` // timestamp of the preset request.
}
func HandleRequestConfig(mqttClient mqtt.Client, hubKey string, payload Payload, configuration *models.Configuration, communication *models.Communication) {
value := payload.Value
// Convert map[string]interface{} to RequestConfigPayload
jsonData, _ := json.Marshal(value)
var configPayload RequestConfigPayload
json.Unmarshal(jsonData, &configPayload)
if configPayload.Timestamp != 0 {
// Get Config from the device
key := configuration.Config.Key
name := configuration.Config.Name
if key != "" && name != "" {
var configMap map[string]interface{}
inrec, _ := json.Marshal(configuration.Config)
json.Unmarshal(inrec, &configMap)
message := Message{
Payload: Payload{
Action: "receive-config",
DeviceId: configuration.Config.Key,
Value: configMap,
},
}
payload, err := PackageMQTTMessage(message)
if err == nil {
mqttClient.Publish("kerberos/hub/"+hubKey, 0, false, payload)
} else {
log.Log.Info("HandleRequestConfig: something went wrong while sending config to hub: " + string(payload))
}
} else {
log.Log.Info("HandleRequestConfig: no config available")
}
log.Log.Info("HandleRequestConfig: Received a request for the config")
}
}
// We received a update config request, we'll update the current config and send a confirmation back.
type UpdateConfigPayload struct {
Timestamp int64 `json:"timestamp"` // timestamp of the preset request.
Config models.Config `json:"config"`
}
func HandleUpdateConfig(mqttClient mqtt.Client, hubKey string, payload Payload, configDirectory string, configuration *models.Configuration, communication *models.Communication) {
value := payload.Value
// Convert map[string]interface{} to UpdateConfigPayload
jsonData, _ := json.Marshal(value)
var configPayload UpdateConfigPayload
json.Unmarshal(jsonData, &configPayload)
if configPayload.Timestamp != 0 {
config := configPayload.Config
err := configService.SaveConfig(configDirectory, config, configuration, communication)
if err == nil {
log.Log.Info("HandleUpdateConfig: Config updated")
message := Message{
Payload: Payload{
Action: "acknowledge-update-config",
DeviceId: configuration.Config.Key,
},
}
payload, err := PackageMQTTMessage(message)
if err == nil {
mqttClient.Publish("kerberos/hub/"+hubKey, 0, false, payload)
} else {
log.Log.Info("HandleRequestConfig: something went wrong while sending acknowledge config to hub: " + string(payload))
}
} else {
log.Log.Info("HandleUpdateConfig: Config update failed")
}
}
}
func DisconnectMQTT(mqttClient mqtt.Client, config *models.Config) {
if mqttClient != nil {
// Cleanup all subscriptions
// New methods
mqttClient.Unsubscribe("kerberos/agent/" + PREV_HubKey)