用Golang捣鼓YCC365摄像头角度调整,这事儿真没那么玄乎
- 单站
- 2026-06-29 14:00:14
- 212
最近家里装了个YCC365摄像头,想调个角度吧,还得用手机App一点点挪,麻烦不说,还不精确,我这人吧,就爱瞎折腾,心想能不能用Golang写点代码,直接控制摄像头角度?嘿,你还别说,真让我给整明白了,今天就跟大伙儿聊聊这事儿,保准你看完也能自己动手。
为啥非要跟摄像头过不去?
你想想啊,家里有个摄像头,今天想看看阳台花花草草,明天想瞄一眼门口快递,后天又得盯着厨房猫粮碗,每次都得掏出手机,打开App,点来点去调角度,烦不烦?关键是,有些App的调角度功能反应慢得跟树懒似的,调到一半还卡住。
很多时候你不在家,想远程调个角度,网络延迟+App响应慢,简直能把人逼疯,我当时就想,要是能写个小程序,命令行敲一下,摄像头就乖乖转到指定角度,多爽?Golang多线程处理、并发控制、网络请求这些功能,正好派上用场。
扒一扒YCC365的底细
在动手之前,咱得先搞清楚YCC365摄像头怎么工作的,这玩意儿不是靠魔法转动的,它内部有步进电机,通过控制电机脉冲来控制转动角度,水平方向一个电机,垂直方向一个电机,俩电机配合,就能让摄像头指哪儿看哪儿。
大多数YCC365摄像头支持ONVIF协议(开放网络视频接口论坛制定的标准),这个协议里规定了怎么控制摄像头的PTZ(Pan-Tilt-Zoom,即水平旋转、垂直旋转、变焦),还有一部分支持厂家的私有协议,不过没关系,咱们用Golang都能搞定。
我翻了好多资料,发现ONVIF协议是最通用的,而且Go语言有现成的库可以用,你猜怎么着?GitHub上有个叫use-go/onvif的项目,封装得很好,还有ivad rot、go-onvif这些库,咱挑顺手的用就行。
环境搭建:别怕,比装App还简单
说干就干,先得把Go环境搭起来,去Go官网下载安装包,一路“下一步”完事儿,然后设置好GOPATH和GOROOT,像这样:
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秒换个角度,用goroutine和time.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里没开放出来的功能,比如速度调节、预置位存储啥的。
行了,不啰嗦了,我得去调调我家阳台那个角度,最近鸽子总在那落脚。
