| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498 |
- package reporter
- import "C"
- import (
- "context"
- "encoding/json"
- "fmt"
- "io/fs"
- "os"
- "os/exec"
- "path/filepath"
- "strconv"
- "strings"
- "sync/atomic"
- "time"
- mqtt "github.com/eclipse/paho.mqtt.golang"
- gps "hnyfkj.com.cn/rtu/bxs-sy/air530z"
- modem "hnyfkj.com.cn/rtu/bxs-sy/air720u"
- "hnyfkj.com.cn/rtu/bxs-sy/baseapp"
- mcu "hnyfkj.com.cn/rtu/bxs-sy/mcu_ctrl_board"
- )
- const (
- MODULE_NAME = "Reporter"
- sendPingReqInterval = 10 * time.Second
- waitPingRespTimeout = 5 * time.Second
- )
- var (
- Reporter *MQTTReporter
- )
- type MQTTReporter struct {
- client mqtt.Client
- ctx context.Context
- cancel context.CancelFunc
- msgQos byte // MQTT数据消息传输的质量等级(统一)
- isLogin atomic.Bool // 标记是否已成功登录MQTT后端服务器
- dui string // 登录成功后服务端返回的设备唯一ID
- inheritDUI string // 继承的历史ID, 可选可为空(换板时)
- // 主动上报的后台任务, 登录成功时用于照片补录和上报通知类消息
- uploadPhotosTask *SingleTask
- reportMCUTask *SingleTask
- reportLocTask *SingleTask
- }
- func (r *MQTTReporter) IsLogin() bool {
- return r.isLogin.Load()
- }
- func (r *MQTTReporter) onLogin(client mqtt.Client, msg mqtt.Message) {
- imei, dui, err := parseLoginResp(msg.Payload())
- if err != nil {
- baseapp.Logger.Errorf("[%s] 登录失败: %v!!", MODULE_NAME, err)
- return
- }
- if imei != modem.GetIMEI() { // 判断是否我的应答
- return
- }
- if len(r.inheritDUI) > 0 && r.inheritDUI == dui {
- _ = os.Remove(filepath.Join(baseapp.VAR_DIR, "inheritDUI.txt"))
- } else if len(r.inheritDUI) > 0 && r.inheritDUI != dui {
- baseapp.Logger.Errorf("[%s] 登录失败: 要继承的DUI(%s)与服务器返回的DUI(%s)不匹配!!", MODULE_NAME, r.inheritDUI, dui)
- return
- }
- r.dui = dui
- r.isLogin.Store(true)
- r.client.Unsubscribe("/yfkj/bxs-sy/server/rpc/response") // 登录成功后, 取消对登录应答的订阅(不关心是否成功)
- baseapp.Logger.Infof("[%s] 登录成功, 设备DUI: %s", MODULE_NAME, r.dui)
- r.uploadPhotosTask.Run(r.uploadPendingPhotos, true) // 补录上传历史遗存照片, 单实例运行
- r.reportLocTask.Run(r.reportLocation, true) // 定位成功通知当前位置, 单实例运行
- r.reportMCUTask.Run(func() { r.reportMCUParams(mcu.CfgParams) }, true) // 上报控制板的运行参数, 单实例运行
- }
- func (r *MQTTReporter) uploadPendingPhotos() {
- baseapp.Logger.Infof("[%s] 拍照补录上传任务开始", MODULE_NAME)
- nums := 0
- filepath.WalkDir(baseapp.IMG_DIR, func(path string, d fs.DirEntry, walkErr error) error {
- if walkErr != nil || d.IsDir() || filepath.Ext(path) != ".jpg" {
- return nil
- } else if !r.IsLogin() || r.ctx.Err() != nil {
- return context.Canceled
- }
- fileUploader.uploadLock.Lock() // 上传锁定(多个上传任务并发时, 保证串行执行) 2025-10-17
- defer fileUploader.uploadLock.Unlock()
- mcu.MCBSetCamStateBit(mcu.PhotoUploading)
- defer mcu.MCBSetCamStateBit(mcu.Idle)
- _, err := uploadFileToFtp(r.ctx, path, CfgServers.Img2Ftp.Address, CfgServers.Img2Ftp.Username, CfgServers.Img2Ftp.Password, defaultUploadTimeout)
- if err == nil {
- baseapp.Logger.Infof("[%s] 拍照补录上传成功, 本地文件: %q已删除", MODULE_NAME, path)
- os.Remove(path)
- nums++
- }
- return nil
- })
- baseapp.Logger.Infof("[%s] 拍照补录上传任务结束, 本次共上传%d张照片", MODULE_NAME, nums)
- }
- func (r *MQTTReporter) reportMCUParams(setParams *mcu.Config) {
- baseapp.Logger.Infof("[%s] 主动上报MCU参数任务开始", MODULE_NAME)
- for {
- if !r.IsLogin() || r.ctx.Err() != nil {
- return
- }
- msg := fmt.Sprintf(`{"imei": "%s", "version": "%s", "ctrlMode": "%d", "lightDuration": "%d", "startHour": "%d", "endHour": "%d", "takePhotoIntervalMinutes": "%d"}`,
- modem.GetIMEI(), setParams.Version, setParams.CtrlMode, setParams.LightDuration, setParams.StartHour, setParams.EndHour, setParams.TakePhotoInterval)
- token := r.client.Publish(fmt.Sprintf("/yfkj/bxs-sy/notify/%s/mcuParams", r.dui), r.msgQos, false, msg)
- select {
- case <-r.ctx.Done():
- return
- case <-token.Done():
- }
- if token.Error() != nil {
- continue
- }
- baseapp.Logger.Infof("[%s] 主动上报MCU参数任务结束(%q)", MODULE_NAME, msg)
- return
- }
- }
- func (r *MQTTReporter) reportLocation() {
- baseapp.Logger.Infof("[%s] 主动上报当前GPS位置信息任务开始", MODULE_NAME)
- for {
- if !r.IsLogin() || r.ctx.Err() != nil {
- return
- }
- lat, lon, err := gps.Get2DPosition()
- if err != nil {
- time.Sleep(defaultRtyInterval)
- continue
- }
- msg := fmt.Sprintf(`{"imei": "%s", "lat": "%s", "lon": "%s"}`, modem.GetIMEI(), lat, lon)
- token := r.client.Publish(fmt.Sprintf("/yfkj/bxs-sy/notify/%s/location", r.dui), r.msgQos, false, msg)
- select {
- case <-r.ctx.Done():
- return
- case <-token.Done():
- }
- if token.Error() != nil {
- continue
- }
- baseapp.Logger.Infof("[%s] 主动上报当前GPS位置信息任务结束(%q)", MODULE_NAME, msg)
- return
- }
- }
- func (r *MQTTReporter) handleRequest(client mqtt.Client, msg mqtt.Message) {
- go r.execRequest(msg)
- }
- func (r *MQTTReporter) execRequest(msg mqtt.Message) {
- var req rpcRequest
- reqStr := string(msg.Payload())
- baseapp.Logger.Debugf("[%s] 收到一个RPC请求: %s", MODULE_NAME, reqStr)
- var resp rpcResponse = rpcResponse{
- JSONRPC: "2.0",
- }
- if err := json.Unmarshal(msg.Payload(), &req); err != nil {
- resp.Error = &rpcError{Code: -32700, Message: "Parse error"}
- goto send_p
- } else {
- resp.ID = req.ID
- }
- switch req.Method {
- // Call-1: 设置MCU控制板运行参数, 保存到文件
- case "setMCUParams":
- params, err := parseMCUParams(msg.Payload())
- if err != nil {
- resp.Error = makeCustomError(err)
- goto send_p
- }
- var cfgParams mcu.Config
- cfgParams.Version = params.Version
- cfgParams.CtrlMode, _ = strconv.Atoi(params.CtrlMode)
- cfgParams.LightDuration, _ = strconv.Atoi(params.LightDuration)
- cfgParams.StartHour, _ = strconv.Atoi(params.StartHour)
- cfgParams.EndHour, _ = strconv.Atoi(params.EndHour)
- cfgParams.TakePhotoInterval, _ = strconv.Atoi(params.PhotoIntervalMinutes)
- err = mcu.SaveCfgParams(&cfgParams)
- if err != nil {
- resp.Error = makeCustomError(err)
- } else {
- resp.Result, _ = json.Marshal("Success")
- go r.reportMCUParams(&cfgParams)
- }
- // Call-2: 获取MCU控制板运行参数, 从文件读取
- case "getMCUParams":
- var params mcuParams
- params.Version = mcu.CfgParams.Version
- params.CtrlMode = fmt.Sprintf("%d", mcu.CfgParams.CtrlMode)
- params.LightDuration = fmt.Sprintf("%d", mcu.CfgParams.LightDuration)
- params.StartHour = fmt.Sprintf("%d", mcu.CfgParams.StartHour)
- params.EndHour = fmt.Sprintf("%d", mcu.CfgParams.EndHour)
- params.PhotoIntervalMinutes = fmt.Sprintf("%d", mcu.CfgParams.TakePhotoInterval)
- resp.Result, _ = json.Marshal(params)
- // Call-3, 异步请求MCU获取当前实时的环境数据
- case "getCurrentEnvData":
- err := mcu.MCBReqEnvCurData()
- if err != nil {
- resp.Error = makeCustomError(err)
- } else {
- resp.Result, _ = json.Marshal("Success")
- }
- // Call-4: 同步执行相机拍照, 比较耗时, 需等待
- case "takephoto":
- remoteFile, err := TakePhotoAndUpToFtp(r.ctx)
- if err != nil {
- resp.Error = makeCustomError(err)
- } else {
- resp.Result, _ = json.Marshal(map[string]string{"filename": remoteFile})
- }
- // Call-5:重启, 服务端不一定能收到成功的应答
- case "reboot":
- cmd := exec.Command("reboot")
- if err := cmd.Run(); err != nil {
- resp.Error = &rpcError{Code: -32700, Message: "Failed"}
- } else {
- resp.Result, _ = json.Marshal("Success")
- }
- // Call-6: 获取RTU当前的位置信息(2D经纬度坐标)
- case "getLocation":
- lat, lon, err := gps.Get2DPosition()
- if err != nil {
- resp.Error = makeCustomError(err)
- } else {
- resp.Result, _ = json.Marshal(map[string]string{"lat": lat, "lon": lon})
- }
- // Call-7: 升级RTU固件, 升级后程序会自动退出, 靠守护进程重新拉起, 替换新程序
- case "upgradeApp":
- serverAddr, loginUser, loginPass, remoteFile, md5val1, err := parseUpgradeAppResp(msg.Payload())
- if err != nil {
- resp.Error = makeCustomError(err)
- goto send_p
- }
- mcu.MCBSetMNTStateBit(mcu.AppUpgrading)
- localFile, err := downloadFileFromFtp(r.ctx, serverAddr, loginUser, loginPass, remoteFile, defaultDownloadTimeout)
- if err != nil {
- resp.Error = makeCustomError(err)
- mcu.MCBSetMNTStateBit(mcu.Idle)
- goto send_p
- }
- md5val2, err := fileMD5(localFile)
- if err != nil {
- resp.Error = makeCustomError(err)
- mcu.MCBSetMNTStateBit(mcu.Idle)
- goto send_p
- }
- if !strings.EqualFold(md5val1, md5val2) {
- resp.Error = &rpcError{Code: -32001, Message: fmt.Sprintf("MD5校验失败(%s)", md5val2)}
- mcu.MCBSetMNTStateBit(mcu.Idle)
- goto send_p
- }
- // 打包: tar czf rtu_bxs_seyou_1.0.0.1.tar.gz ./rtu_bxs_seyou.out
- // 解包: tar xzf rtu_bxs_seyou_1.0.0.1.tar.gz -C baseapp.EXEC_DIR
- cmdStr := "tar xzf " + localFile + " -C " + baseapp.EXEC_DIR
- cmd := exec.Command("sh", "-c", cmdStr)
- cmd.Stdout = os.Stdout
- cmd.Stderr = os.Stderr
- err = cmd.Run()
- os.Remove(localFile)
- if err != nil {
- resp.Error = &rpcError{Code: -32001, Message: "解压升级包失败"}
- mcu.MCBSetMNTStateBit(mcu.Idle)
- goto send_p
- }
- resp.Result, _ = json.Marshal("Success")
- mcu.MCBSetMNTStateBit(mcu.Idle)
- default: // 返回不支持的方法调用(不支持的请求)
- resp.Error = &rpcError{Code: -32601, Message: "Method not found"}
- }
- send_p:
- payload, _ := json.Marshal(resp)
- token := r.client.Publish(fmt.Sprintf("/yfkj/bxs-sy/device/rpc/%s/response", r.dui), r.msgQos, false, payload)
- select {
- case <-r.ctx.Done():
- return
- case <-token.Done():
- }
- if token.Error() == nil {
- baseapp.Logger.Debugf("[%s] 发送RPC应答成功: %s", MODULE_NAME, string(payload))
- } else {
- baseapp.Logger.Errorf("[%s] 发送RPC应答失败: %v!!", MODULE_NAME, token.Error())
- }
- }
- func (r *MQTTReporter) runLoop() {
- logoutMsg, _ := makeLogoutMsg(modem.GetIMEI())
- opts := mqtt.NewClientOptions().
- AddBroker(CfgServers.MQTTSrv.Address).SetUsername(CfgServers.MQTTSrv.Username).SetPassword(CfgServers.MQTTSrv.Password).
- SetConnectRetry(false).SetAutoReconnect(false).SetCleanSession(true).SetKeepAlive(sendPingReqInterval).SetPingTimeout(waitPingRespTimeout).
- SetOrderMatters(false).SetWill("/yfkj/bxs-sy/notify/logout", string(logoutMsg), r.msgQos, false)
- opts.OnConnect = func(c mqtt.Client) {
- baseapp.Logger.Infof("[%s] 服务器连接成功", MODULE_NAME)
- }
- opts.OnConnectionLost = func(c mqtt.Client, err error) {
- if r.isLogin.Swap(false) {
- baseapp.Logger.Warnf("[%s] 服务器连接丢失: %v!", MODULE_NAME, err)
- }
- }
- r.client = mqtt.NewClient(opts)
- ticker := time.NewTicker(1 * time.Second)
- defer ticker.Stop()
- for {
- if r.IsLogin() {
- select {
- case <-r.ctx.Done():
- return
- case <-ticker.C: // 唤醒定期检测
- continue
- }
- }
- wStep1: // 1, 连接到服务器
- if !r.client.IsConnected() && !r.client.IsConnectionOpen() {
- token := r.client.Connect()
- select {
- case <-r.ctx.Done():
- return
- case <-token.Done():
- }
- if token.Error() != nil {
- baseapp.Logger.Errorf("[%s] 服务器连接失败: %v!!", MODULE_NAME, token.Error())
- select {
- case <-r.ctx.Done():
- return
- case <-time.After(3 * time.Second):
- }
- goto wStep1
- }
- }
- wStep2: // 2, 订阅登录应答
- token := r.client.Subscribe("/yfkj/bxs-sy/server/rpc/response", r.msgQos, r.onLogin)
- select {
- case <-r.ctx.Done():
- return
- case <-token.Done():
- }
- if token.Error() != nil {
- baseapp.Logger.Errorf("[%s] 登录应答订阅失败: %v!!", MODULE_NAME, token.Error())
- select {
- case <-r.ctx.Done():
- return
- case <-time.After(3 * time.Second):
- }
- if !r.client.IsConnected() && !r.client.IsConnectionOpen() {
- goto wStep1
- } else {
- goto wStep2
- }
- }
- wStep3: // 3, 发送登录请求
- loginReq, _ := makeLoginReq(modem.GetIMEI(), modem.GetSimICCID(), modem.GetRSSI(), baseapp.Version, r.inheritDUI)
- token = r.client.Publish("/yfkj/bxs-sy/server/rpc/request", r.msgQos, false, loginReq)
- select {
- case <-r.ctx.Done():
- return
- case <-token.Done():
- }
- if token.Error() != nil {
- baseapp.Logger.Errorf("[%s] 登录请求发送失败: %v!!", MODULE_NAME, token.Error())
- select {
- case <-r.ctx.Done():
- return
- case <-time.After(3 * time.Second):
- }
- if !r.client.IsConnected() && !r.client.IsConnectionOpen() {
- goto wStep1
- } else {
- goto wStep3
- }
- }
- tc := time.NewTicker(500 * time.Millisecond)
- tt := time.NewTimer(5 * time.Second) // 超时
- for !r.IsLogin() {
- select {
- case <-r.ctx.Done():
- tc.Stop()
- tt.Stop()
- return
- case <-tc.C: // 唤醒定期检测
- case <-tt.C:
- baseapp.Logger.Warnf("[%s] 登录超时,重新发送登录请求...", MODULE_NAME)
- if !r.client.IsConnected() && !r.client.IsConnectionOpen() {
- goto wStep1
- } else {
- goto wStep3
- }
- }
- }
- tc.Stop()
- tt.Stop()
- wStep4: // 4, 注册本地方法
- token = r.client.Subscribe(fmt.Sprintf("/yfkj/bxs-sy/device/rpc/%s/request", r.dui), r.msgQos, r.handleRequest)
- select {
- case <-r.ctx.Done():
- return
- case <-token.Done():
- }
- if token.Error() != nil {
- baseapp.Logger.Errorf("[%s] 注册本地RPC方法失败: %v!!", MODULE_NAME, token.Error())
- select {
- case <-r.ctx.Done():
- return
- case <-time.After(3 * time.Second):
- }
- if !r.client.IsConnected() && !r.client.IsConnectionOpen() {
- goto wStep1
- } else {
- goto wStep4
- }
- }
- } // for end
- }
- func ModuleInit() bool {
- err := loadCfgServers()
- if err != nil {
- baseapp.Logger.Errorf("[%s] 加载服务器配置项失败: %v!!", MODULE_NAME, err)
- return false
- }
- inheritDUI := "" // 换RTU板卡时, 继承历史ID, 可选可为空
- if data, err := os.ReadFile(filepath.Join(baseapp.VAR_DIR, "inheritDUI.txt")); err == nil {
- inheritDUI = strings.TrimSpace(string(data))
- if len(inheritDUI) != 14 || !isDecimal(inheritDUI) {
- baseapp.Logger.Errorf("[%s] 文件中的DUI: %q 无效, 无法继承历史台账数据!!", MODULE_NAME, inheritDUI)
- os.Exit(1)
- }
- }
- ctx, cancel := context.WithCancel(context.Background())
- Reporter = &MQTTReporter{ctx: ctx, cancel: cancel, msgQos: 1, inheritDUI: inheritDUI,
- uploadPhotosTask: &SingleTask{}, reportMCUTask: &SingleTask{}, reportLocTask: &SingleTask{}}
- go Reporter.runLoop()
- go LoopTakePhoto(Reporter.ctx)
- go LoopRecvEnvDataGrp(Reporter.ctx)
- go LoopRecvEnvDataOne(Reporter.ctx)
- return true
- }
- func ModuleExit() {
- if Reporter != nil {
- Reporter.cancel()
- }
- }
- //export RTU_IsLoginMqServer
- func RTU_IsLoginMqServer() C.int {
- if Reporter != nil && Reporter.IsLogin() {
- return 1
- }
- return 0
- }
|