From 87f3849fe74545d61cf4ea6d895ae92c50ed17bd Mon Sep 17 00:00:00 2001 From: bleeth Date: Fri, 2 Aug 2024 16:58:41 +0800 Subject: [PATCH] =?UTF-8?q?feat=EF=BC=9A=E9=A2=84=E7=BD=AE=E4=BD=8D?= =?UTF-8?q?=E6=93=8D=E4=BD=9C=E6=8E=A5=E5=8F=A3=E5=92=8C=E5=88=97=E8=A1=A8?= =?UTF-8?q?=E6=9F=A5=E8=AF=A2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 21 ++++++++++++++++++ channel.go | 41 ++++++++++++++++++++++++++++++++++++ handle.go | 2 ++ manscdp.go | 25 ++++++++++++++++++++++ ptz.go | 62 +++++++++++++++++++++++++++++++++++++++++++++++++++++- restful.go | 41 ++++++++++++++++++++++++++++++++++++ 6 files changed, 191 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 44961e5..581c085 100644 --- a/README.md +++ b/README.md @@ -139,3 +139,24 @@ http 200 表示成功,404流不存在 | id | 是 | 设备ID | | expires | 是 | 订阅周期(秒) | | interval | 是 | 订阅间隔(秒) | + +### 预置位列表查询 + +`/gb28181/api/preset/list` + +| 参数名 | 必传 | 含义 | +| -------- | ---- | -------------- | +| id | 是 | 设备ID | +| channel | 是 | 通道编号 | + + +### 预置位操作 + +`/gb28181/api/preset/control` + +| 参数名 | 必传 | 含义 | +| -------- | ---- |---------------------| +| id | 是 | 设备ID | +| channel | 是 | 通道编号 | +| cmd | 是 | 操作指令 0=新增,1=删除,2=调用 | +| point | 是 | 预置点位1-255 | diff --git a/channel.go b/channel.go index a69ebd2..466e67c 100644 --- a/channel.go +++ b/channel.go @@ -31,6 +31,7 @@ func (p *PullStream) CreateRequest(method sip.RequestMethod) (req sip.Request) { req = p.channel.CreateRequst(method) from, _ := res.From() to, _ := res.To() + callId, _ := res.CallID() req.ReplaceHeaders(from.Name(), []sip.Header{from}) req.ReplaceHeaders(to.Name(), []sip.Header{to}) @@ -120,6 +121,11 @@ type Channel struct { ChannelInfo } +type PresetInfo struct { + PresetID int `json:"-" yaml:"-"` // + PresetName string `json:"-" yaml:"-"` // +} + func (c *Channel) MarshalJSON() ([]byte, error) { m := map[string]any{ "DeviceID": c.DeviceID, @@ -264,6 +270,41 @@ func (channel *Channel) QueryRecord(startTime, endTime string) ([]*Record, error return r.list, r.err } +func (channel *Channel) QueryPresetList() (sip.Response, error) { + d := channel.Device + request := d.CreateRequest(sip.MESSAGE) + contentType := sip.ContentType("Application/MANSCDP+xml") + request.AppendHeader(&contentType) + + body := BuildPresetListXML(100, channel.DeviceID) + request.SetBody(body, true) + + resp, err := d.SipRequestForResponse(request) + if err != nil { + return nil, fmt.Errorf("query error: %s", err) + } + if resp.StatusCode() != http.StatusOK { + return nil, fmt.Errorf("query error, status=%d", resp.StatusCode()) + } + return resp, nil +} + +func (channel *Channel) PresetControl(ptzCode int, point byte) int { + cmd := byte(PresetSet) + switch ptzCode { + case PresetAddPoint: + cmd = PresetSet + case PresetDelPoint: + cmd = PresetDel + case PresetCallPoint: + cmd = PresetCall + default: + + } + PTZCmd := Pack(cmd, point) + return channel.Control(PTZCmd) +} + func (channel *Channel) Control(PTZCmd string) int { d := channel.Device request := d.CreateRequest(sip.MESSAGE) diff --git a/handle.go b/handle.go index 59d3340..47ad4dd 100644 --- a/handle.go +++ b/handle.go @@ -274,6 +274,8 @@ func (c *GB28181Config) OnMessage(req sip.Request, tx sip.ServerTransaction) { body = BuildAlarmResponseXML(d.ID) case "Broadcast": GB28181Plugin.Info("broadcast message", zap.String("body", req.Body())) + case "PresetQuery": + GB28181Plugin.Info("PresetQuery message", zap.String("body", req.Body())) default: d.Warn("Not supported CmdType", zap.String("CmdType", temp.CmdType), zap.String("body", req.Body())) response := sip.NewResponseFromRequest("", req, http.StatusBadRequest, "", "") diff --git a/manscdp.go b/manscdp.go index 94b156c..11287a2 100644 --- a/manscdp.go +++ b/manscdp.go @@ -1,12 +1,23 @@ package gb28181 import ( + "encoding/xml" "fmt" "strconv" "time" ) var ( + // 获取预置位列表 + PresentListXML = ` + + + PresetQuery + %d + %s + +` + // CatalogXML 获取设备列表xml样式 CatalogXML = ` Catalog @@ -65,6 +76,10 @@ func BuildCatalogXML(sn int, id string) string { return fmt.Sprintf(CatalogXML, sn, id) } +func BuildPresetListXML(sn int, id string) string { + return fmt.Sprintf(PresentListXML, sn, id) +} + // BuildRecordInfoXML 获取录像文件列表指令 func BuildRecordInfoXML(sn int, id string, start, end int64) string { return fmt.Sprintf(RecordInfoXML, sn, id, intTotime(start).Format("2006-01-02T15:04:05"), intTotime(end).Format("2006-01-02T15:04:05")) @@ -90,3 +105,13 @@ var ( func BuildAlarmResponseXML(id string) string { return fmt.Sprintf(AlarmResponseXML, id) } + +func XmlEncode(v interface{}) (string, error) { + xmlData, err := xml.MarshalIndent(v, "", " ") + if err != nil { + return "", err + } + xml := string(xmlData) + xml = `` + "\n" + xml + "\n" + return xml, err +} diff --git a/ptz.go b/ptz.go index 128eebf..e4abbf5 100644 --- a/ptz.go +++ b/ptz.go @@ -1,6 +1,10 @@ package gb28181 -import "fmt" +import ( + "encoding/hex" + "encoding/xml" + "fmt" +) var ( name2code = map[string]uint8{ @@ -18,6 +22,35 @@ var ( } ) +type PresetCmd byte + +const ( + PresetAddPoint = 0 + PresetDelPoint = 1 + PresetCallPoint = 2 +) + +const DeviceControl = "DeviceControl" +const PTZFirstByte = 0xA5 +const ( + PresetSet = 0x81 + PresetCall = 0x82 + PresetDel = 0x83 +) + +type MessagePtz struct { + XMLName xml.Name `xml:"Control"` + CmdType string `xml:"CmdType"` + SN int `xml:"SN"` + DeviceID string `xml:"DeviceID"` + PTZCmd string `xml:"PTZCmd"` +} + +type Preset struct { + CMD byte + Point byte +} + func toPtzStrByCmdName(cmdName string, horizontalSpeed, verticalSpeed, zoomSpeed uint8) (string, error) { c, err := toPtzCode(cmdName) if err != nil { @@ -45,3 +78,30 @@ func toPtzCode(cmd string) (uint8, error) { return 0, fmt.Errorf("invalid ptz cmd %q", cmd) } } + +func getVerificationCode(ptz []byte) { + sum := uint8(0) + for i := 0; i < len(ptz)-1; i++ { + sum += ptz[i] + } + ptz[len(ptz)-1] = sum +} + +func getAssembleCode() uint8 { + return (PTZFirstByte>>4 + PTZFirstByte&0xF + 0) % 16 +} + +func Pack(cmd, point byte) string { + buf := make([]byte, 8) + buf[0] = PTZFirstByte + buf[1] = getAssembleCode() + buf[2] = 0 + + buf[3] = cmd + + buf[4] = 0 + buf[5] = point + buf[6] = 0 + getVerificationCode(buf) + return hex.EncodeToString(buf) +} diff --git a/restful.go b/restful.go index 26a90ca..6bcf7d8 100644 --- a/restful.go +++ b/restful.go @@ -230,6 +230,47 @@ func (c *GB28181Config) API_position(w http.ResponseWriter, r *http.Request) { } } +func (c *GB28181Config) API_preset_list(w http.ResponseWriter, r *http.Request) { + query := r.URL.Query() + //设备id + id := query.Get("id") + //获取通道 + channel := query.Get("channel") + if c := FindChannel(id, channel); c != nil { + res, err := c.QueryPresetList() + if err == nil { + util.ReturnValue(res, w, r) + } else { + util.ReturnError(util.APIErrorInternal, err.Error(), w, r) + } + } else { + util.ReturnError(util.APIErrorNotFound, fmt.Sprintf("device %q channel %q not found", id, channel), w, r) + } + +} + +func (c *GB28181Config) API_preset_control(w http.ResponseWriter, r *http.Request) { + //CORS(w, r) + query := r.URL.Query() + //设备id + id := query.Get("id") + //获取通道 + channel := query.Get("channel") + //获取cmd + ptzCmd := query.Get("cmd") + //获取点 + point := query.Get("point") + + if c := FindChannel(id, channel); c != nil { + _ptzCmd, _ := strconv.ParseInt(ptzCmd, 10, 16) + _point, _ := strconv.ParseInt(point, 10, 8) + code := c.PresetControl(int(_ptzCmd), byte(_point)) + util.ReturnError(code, "device received", w, r) + } else { + util.ReturnError(util.APIErrorNotFound, fmt.Sprintf("device %q channel %q not found", id, channel), w, r) + } +} + type DevicePosition struct { ID string GpsTime time.Time //gps时间