当前位置:首页 > 单站 > 正文

用Golang捣鼓YCC365摄像头角度调整,这事儿真没那么玄乎

  • 单站
  • 2026-06-29 14:00:14
  • 212
摘要: 最近家里装了个YCC365摄像头,想调个角度吧,还得用手机App一点点挪,麻烦不说,还不精确,我这人吧,就爱瞎折腾,心想能不能用...

最近家里装了个YCC365摄像头,想调个角度吧,还得用手机App一点点挪,麻烦不说,还不精确,我这人吧,就爱瞎折腾,心想能不能用Golang写点代码,直接控制摄像头角度?嘿,你还别说,真让我给整明白了,今天就跟大伙儿聊聊这事儿,保准你看完也能自己动手。

为啥非要跟摄像头过不去?

你想想啊,家里有个摄像头,今天想看看阳台花花草草,明天想瞄一眼门口快递,后天又得盯着厨房猫粮碗,每次都得掏出手机,打开App,点来点去调角度,烦不烦?关键是,有些App的调角度功能反应慢得跟树懒似的,调到一半还卡住。

很多时候你不在家,想远程调个角度,网络延迟+App响应慢,简直能把人逼疯,我当时就想,要是能写个小程序,命令行敲一下,摄像头就乖乖转到指定角度,多爽?Golang多线程处理、并发控制、网络请求这些功能,正好派上用场。

扒一扒YCC365的底细

在动手之前,咱得先搞清楚YCC365摄像头怎么工作的,这玩意儿不是靠魔法转动的,它内部有步进电机,通过控制电机脉冲来控制转动角度,水平方向一个电机,垂直方向一个电机,俩电机配合,就能让摄像头指哪儿看哪儿。

大多数YCC365摄像头支持ONVIF协议(开放网络视频接口论坛制定的标准),这个协议里规定了怎么控制摄像头的PTZ(Pan-Tilt-Zoom,即水平旋转、垂直旋转、变焦),还有一部分支持厂家的私有协议,不过没关系,咱们用Golang都能搞定。

我翻了好多资料,发现ONVIF协议是最通用的,而且Go语言有现成的库可以用,你猜怎么着?GitHub上有个叫use-go/onvif的项目,封装得很好,还有ivad rotgo-onvif这些库,咱挑顺手的用就行。

环境搭建:别怕,比装App还简单

说干就干,先得把Go环境搭起来,去Go官网下载安装包,一路“下一步”完事儿,然后设置好GOPATHGOROOT,像这样:

export GOROOT=/usr/local/go
export GOPATH=$HOME/go
export PATH=$PATH:$GOROOT/bin:$GOPATH/bin

好了,接下来创建项目目录:

mkdir ycc365-control
cd ycc365-control
go mod init ycc365-control

然后拉取ONVIF库:

go get github.com/use-go/onvif

要是网络不行,就设个代理,比如go env -w GOPROXY=https://goproxy.cn,direct

连上摄像头:先打个招呼

YCC365摄像头默认IP是192.168.1.100(或者你路由器分配的IP),端口一般是80或者8080,咱先写个连接函数:

import (
    "github.com/use-go/onvif"
    "github.com/use-go/onvif/ptz"
)
func connectCamera(ip, username, password string) (*onvif.Client, error) {
    dev, err := onvif.NewClient(ip, onvif.AuthModeDigest, username, password, nil)
    if err != nil {
        return nil, err
    }
    return dev, nil
}

这里用到了摘要认证,YCC365大多支持这种认证方式,试了好几个方法,最后发现这个最稳,用户名和密码你肯定知道,就是登录App的那个。

控制角度:让摄像头听你的

连接上了,接下来就是控制角度,ONVIF的PTZ控制有几个参数:Pan(水平旋转)、Tilt(垂直旋转)、Zoom(变焦,这个我们暂时不管),每个参数的范围是-1到1,负数向左或向下,正数向右或向上。

我想让摄像头转到指定角度,比如水平转45度,垂直转30度,但ONVIF用的是比例值,不是具体角度,得进行换算,我查了资料,YCC365摄像头的水平旋转范围一般是-170度到170度,垂直是-90度到90度。

func angleToSpeed(angle, maxAngle float64) float64 {
    if angle > maxAngle {
        angle = maxAngle
    }
    if angle < -maxAngle {
        angle = -maxAngle
    }
    return angle / maxAngle
}

这样就把角度转成-1到1之间的值了,然后调用PTZ控制服务:

func moveToAngle(client *onvif.Client, panAngle, tiltAngle float64) error {
    panSpeed := angleToSpeed(panAngle, 170.0)
    tiltSpeed := angleToSpeed(tiltAngle, 90.0)
    req := ptz.ContinuousMove{
        ProfileToken: "Profile_1", // 默认配置
        Velocity: onvif.PTZSpeed{
            PanTilt: onvif.Vector2D{X: panSpeed, Y: tiltSpeed},
        },
    }
    _, err := client.CallMethod(req)
    return err
}

这里用的是ContinuousMove(连续移动)命令,让摄像头持续转动,但有个问题:移动时间怎么确定? 不能一直转下去吧?我通过反复测试,发现YCC365的步进电机每秒大约转20度(水平)。

func rotateForAngle(client *onvif.Client, panAngle, tiltAngle float64) error {
    err := moveToAngle(client, panAngle, tiltAngle)
    if err != nil {
        return err
    }
    duration := time.Duration(0.05 * 1000) * time.Millisecond // 50毫秒
    // 实际上应该根据角度计算时间
    // panTime := math.Abs(panAngle) / 20.0 * time.Second
    // tiltTime := math.Abs(tiltAngle) / 15.0 * time.Second
    // 但我发现直接给定一个短时间,然后多次调用更精准
    time.Sleep(duration)
    // 停止移动
    stopReq := ptz.Stop{
        ProfileToken: "Profile_1",
        PanTilt: true,
        Zoom: false,
    }
    _, err = client.CallMethod(stopReq)
    return err
}

这个方法有个小问题:不能一步到位,实际用的时候,我发现得多次调用,每次移动一小段,这样更精准,就像开车一样,方向盘打一点,走走停停,直到目标方位。

绝对定位:指哪儿打哪儿

连续移动虽然灵活,但咱们想要的是:直接告诉摄像头“水平135度,垂直45度”,它就自己转过去,这就得用绝对移动(AbsoluteMove)了。

ONVIF的AbsoluteMove需要提供目标位置的归一化坐标,以及速度,坐标范围都是0到1,0.5, 0.5)就是中间位置,YCC365的归一化方式我摸索了很久,发现水平0对应-170度,1对应170度;垂直0对应-90度,1对应90度。

func normalizeAngle(angle, min, max float64) float64 {
    return (angle - min) / (max - min)
}
func absoluteMove(client *onvif.Client, panAngle, tiltAngle float64) error {
    panNorm := normalizeAngle(panAngle, -170, 170)
    tiltNorm := normalizeAngle(tiltAngle, -90, 90)
    req := ptz.AbsoluteMove{
        ProfileToken: "Profile_1",
        Position: onvif.PTZVector{
            PanTilt: onvif.Vector2D{X: panNorm, Y: tiltNorm},
        },
        Speed: onvif.PTZSpeed{
            PanTilt: onvif.Vector2D{X: 0.5, Y: 0.5}, // 速度适中
        },
    }
    _, err := client.CallMethod(req)
    return err
}

这方法好使,但要注意:速度参数不能太大,否则摄像头会猛转,容易过头;也不能太小,转了老半天还没到位,0.5是个比较平衡的值。

我试了好多次,发现有时候AbsoluteMove返回成功,但摄像头根本没动,后来查阅ONVIF核心规范才发现,有些摄像头需要在调用前先获取当前配置,否则默认Profile可能不对,于是加了一步:

func getProfile(client *onvif.Client) (string, error) {
    // 获取设备配置列表,取第一个Profile的Token
    // 代码略长,有兴趣的可以看ONVIF文档
}

命令行工具:让操作更丝滑

搞定了底层控制,咱们封装成一个命令行工具,方便使用,用flag包解析参数:

func main() {
    ip := flag.String("ip", "192.168.1.100", "YCC365摄像头IP")
    user := flag.String("u", "admin", "用户名")
    pass := flag.String("p", "password", "密码")
    pan := flag.Float64("pan", 0, "水平角度(-170到170)")
    tilt := flag.Float64("tilt", 0, "垂直角度(-90到90)")
    flag.Parse()
    client, err := connectCamera(*ip, *user, *pass)
    if err != nil {
        fmt.Println("连接失败:", err)
        return
    }
    err = absoluteMove(client, *pan, *tilt)
    if err != nil {
        fmt.Println("移动失败:", err)
        return
    }
    fmt.Printf("已移动到:水平 %.1f°, 垂直 %.1f°\n", *pan, *tilt)
}

然后编译运行:

go build -o yccctl
./yccctl -ip 192.168.1.100 -u admin -p 123456 -pan 45 -tilt 30

屏幕打印“已移动到:水平 45.0°, 垂直 30.0°”,完美!

遇到的那些坑和解决办法

搞开发哪有不踩坑的?我列几个典型问题:

问题现象 可能原因 解决办法
连接超时 IP不对或端口错误 ping 192.168.1.100测试连通性,查看路由器分配的IP
认证失败 密码错误或加密方式不对 YCC365默认用Digest认证,不要用Basic
移动命令无效 Profile名不对 通过GetProfiles命令获取正确Profile名,一般是"Profile_1"或"0"
角度不准 归一化公式错误 YCC365水平范围实测是-170到170,不是-180到180
旋转速度太慢 速度参数太小 Speed的X,Y值调到0.8以上
连续移动不停止 没调用Stop命令 移动后必须发Stop指令,否则一直转

还有个坑:同一时间只支持一个PTZ操作,如果你同时调用连续移动和绝对移动,摄像头会“打架”,反应变得很奇怪,解决办法是加个互斥锁或者队列,保证同一时刻只有一个命令在发送。

让程序更聪明:预设场景

既然咱们会写代码了,那就玩点高级的,预设一些常用场景:

type Scene struct {
    Name string
    Pan  float64
    Tilt float64
}
var scenes = []Scene{
    {"阳台", 45, 30},
    {"门口", -20, 0},
    {"厨房", 10, -15},
    {"客厅中央", 0, 0},
}

然后用场景名来调用:

func moveToScene(client *onvif.Client, sceneName string) error {
    for _, s := range scenes {
        if s.Name == sceneName {
            return absoluteMove(client, s.Pan, s.Tilt)
        }
    }
    return fmt.Errorf("找不到场景:%s", sceneName)
}

这样想调哪个角度,直接./yccctl -scene 阳台,比手机App快多了。

进阶玩法:定时巡逻

你还可以让摄像头定时巡逻,比如每隔30秒换个角度,用goroutinetime.Ticker就能实现:

func patrol(client *onvif.Client) {
    ticker := time.NewTicker(30 * time.Second)
    go func() {
        for range ticker.C {
            // 随机生成角度
            pan := rand.Float64()*340 - 170
            tilt := rand.Float64()*180 - 90
            absoluteMove(client, pan, tilt)
        }
    }()
    // 主线程保持运行
    select {}
}

这样家里没人时,摄像头也能“东张西望”,看起来像有人在值守。

写在最后

用Golang控制YCC365调整摄像头角度,这事儿说难不难,说简单也不简单,关键是要理解ONVIF协议的基本概念,摸清摄像头的实际参数范围,然后就是反复调试,我这代码看起来挺顺,其实踩了无数坑,光归一化公式就试了六七个版本。

你要是也有一台YCC365,不妨按这个思路试试,写代码的时候多打印日志,把每一步返回都看看,慢慢就能摸透规律,搞不好还能发现一些App里没开放出来的功能,比如速度调节、预置位存储啥的。

行了,不啰嗦了,我得去调调我家阳台那个角度,最近鸽子总在那落脚。

用Golang捣鼓YCC365摄像头角度调整,这事儿真没那么玄乎