diff --git a/sentinel/.idea/.gitignore b/sentinel/.idea/.gitignore new file mode 100644 index 0000000..ab1f416 --- /dev/null +++ b/sentinel/.idea/.gitignore @@ -0,0 +1,10 @@ +# Default ignored files +/shelf/ +/workspace.xml +# Ignored default folder with query files +/queries/ +# Datasource local storage ignored files +/dataSources/ +/dataSources.local.xml +# Editor-based HTTP Client requests +/httpRequests/ diff --git a/sentinel/.idea/dictionaries/project.xml b/sentinel/.idea/dictionaries/project.xml new file mode 100644 index 0000000..0cad634 --- /dev/null +++ b/sentinel/.idea/dictionaries/project.xml @@ -0,0 +1,7 @@ + + + + 从海康sdk获取图片以及信息 + + + \ No newline at end of file diff --git a/sentinel/.idea/go.imports.xml b/sentinel/.idea/go.imports.xml new file mode 100644 index 0000000..d7202f0 --- /dev/null +++ b/sentinel/.idea/go.imports.xml @@ -0,0 +1,11 @@ + + + + + + \ No newline at end of file diff --git a/sentinel/.idea/modules.xml b/sentinel/.idea/modules.xml new file mode 100644 index 0000000..f5c9a13 --- /dev/null +++ b/sentinel/.idea/modules.xml @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/sentinel/.idea/sentinel.iml b/sentinel/.idea/sentinel.iml new file mode 100644 index 0000000..5e764c4 --- /dev/null +++ b/sentinel/.idea/sentinel.iml @@ -0,0 +1,9 @@ + + + + + + + + + \ No newline at end of file diff --git a/sentinel/.idea/vcs.xml b/sentinel/.idea/vcs.xml new file mode 100644 index 0000000..6c0b863 --- /dev/null +++ b/sentinel/.idea/vcs.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/sentinel/bin/main.exe b/sentinel/bin/main.exe new file mode 100644 index 0000000..cafcb95 Binary files /dev/null and b/sentinel/bin/main.exe differ diff --git a/sentinel/bin/tmp/licensePlateImage_test1.jpg b/sentinel/bin/tmp/licensePlateImage_test1.jpg new file mode 100644 index 0000000..aa0fd72 Binary files /dev/null and b/sentinel/bin/tmp/licensePlateImage_test1.jpg differ diff --git a/sentinel/bin/tmp/vehicleImage_test1.jpg b/sentinel/bin/tmp/vehicleImage_test1.jpg new file mode 100644 index 0000000..7b7046a Binary files /dev/null and b/sentinel/bin/tmp/vehicleImage_test1.jpg differ diff --git a/sentinel/bin/updater.exe b/sentinel/bin/updater.exe new file mode 100644 index 0000000..365b4ab Binary files /dev/null and b/sentinel/bin/updater.exe differ diff --git a/sentinel/go.mod b/sentinel/go.mod new file mode 100644 index 0000000..587373a --- /dev/null +++ b/sentinel/go.mod @@ -0,0 +1,34 @@ +module sentinel + +go 1.25.5 + +require ( + github.com/dustin/go-humanize v1.0.1 // indirect + github.com/eclipse/paho.mqtt.golang v1.5.1 // indirect + github.com/go-ini/ini v1.67.0 // indirect + github.com/go-ole/go-ole v1.2.6 // indirect + github.com/google/uuid v1.6.0 // indirect + github.com/gorilla/websocket v1.5.3 // indirect + github.com/klauspost/compress v1.18.0 // indirect + github.com/klauspost/cpuid/v2 v2.2.11 // indirect + github.com/klauspost/crc32 v1.3.0 // indirect + github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 // indirect + github.com/minio/crc64nvme v1.1.0 // indirect + github.com/minio/md5-simd v1.1.2 // indirect + github.com/minio/minio-go/v7 v7.0.97 // indirect + github.com/philhofer/fwd v1.2.0 // indirect + github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c // indirect + github.com/rs/xid v1.6.0 // indirect + github.com/shirou/gopsutil/v3 v3.24.5 // indirect + github.com/shoenig/go-m1cpu v0.1.6 // indirect + github.com/tinylib/msgp v1.3.0 // indirect + github.com/tklauser/go-sysconf v0.3.12 // indirect + github.com/tklauser/numcpus v0.6.1 // indirect + github.com/yusufpapurcu/wmi v1.2.4 // indirect + golang.org/x/crypto v0.42.0 // indirect + golang.org/x/net v0.44.0 // indirect + golang.org/x/sync v0.17.0 // indirect + golang.org/x/sys v0.36.0 // indirect + golang.org/x/text v0.29.0 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect +) diff --git a/sentinel/go.sum b/sentinel/go.sum new file mode 100644 index 0000000..4fc4b51 --- /dev/null +++ b/sentinel/go.sum @@ -0,0 +1,64 @@ +github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= +github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= +github.com/eclipse/paho.mqtt.golang v1.5.1 h1:/VSOv3oDLlpqR2Epjn1Q7b2bSTplJIeV2ISgCl2W7nE= +github.com/eclipse/paho.mqtt.golang v1.5.1/go.mod h1:1/yJCneuyOoCOzKSsOTUc0AJfpsItBGWvYpBLimhArU= +github.com/go-ini/ini v1.67.0 h1:z6ZrTEZqSWOTyH2FlglNbNgARyHG8oLW9gMELqKr06A= +github.com/go-ini/ini v1.67.0/go.mod h1:ByCAeIL28uOIIG0E3PJtZPDL8WnHpFKFOtgjp+3Ies8= +github.com/go-ole/go-ole v1.2.6 h1:/Fpf6oFPoeFik9ty7siob0G6Ke8QvQEuVcuChpwXzpY= +github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0= +github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg= +github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= +github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo= +github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ= +github.com/klauspost/cpuid/v2 v2.0.1/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= +github.com/klauspost/cpuid/v2 v2.2.11 h1:0OwqZRYI2rFrjS4kvkDnqJkKHdHaRnCm68/DY4OxRzU= +github.com/klauspost/cpuid/v2 v2.2.11/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0= +github.com/klauspost/crc32 v1.3.0 h1:sSmTt3gUt81RP655XGZPElI0PelVTZ6YwCRnPSupoFM= +github.com/klauspost/crc32 v1.3.0/go.mod h1:D7kQaZhnkX/Y0tstFGf8VUzv2UofNGqCjnC3zdHB0Hw= +github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 h1:6E+4a0GO5zZEnZ81pIr0yLvtUWk2if982qA3F3QD6H4= +github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0/go.mod h1:zJYVVT2jmtg6P3p1VtQj7WsuWi/y4VnjVBn7F8KPB3I= +github.com/minio/crc64nvme v1.1.0 h1:e/tAguZ+4cw32D+IO/8GSf5UVr9y+3eJcxZI2WOO/7Q= +github.com/minio/crc64nvme v1.1.0/go.mod h1:eVfm2fAzLlxMdUGc0EEBGSMmPwmXD5XiNRpnu9J3bvg= +github.com/minio/md5-simd v1.1.2 h1:Gdi1DZK69+ZVMoNHRXJyNcxrMA4dSxoYHZSQbirFg34= +github.com/minio/md5-simd v1.1.2/go.mod h1:MzdKDxYpY2BT9XQFocsiZf/NKVtR7nkE4RoEpN+20RM= +github.com/minio/minio-go/v7 v7.0.97 h1:lqhREPyfgHTB/ciX8k2r8k0D93WaFqxbJX36UZq5occ= +github.com/minio/minio-go/v7 v7.0.97/go.mod h1:re5VXuo0pwEtoNLsNuSr0RrLfT/MBtohwdaSmPPSRSk= +github.com/philhofer/fwd v1.2.0 h1:e6DnBTl7vGY+Gz322/ASL4Gyp1FspeMvx1RNDoToZuM= +github.com/philhofer/fwd v1.2.0/go.mod h1:RqIHx9QI14HlwKwm98g9Re5prTQ6LdeRQn+gXJFxsJM= +github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c h1:ncq/mPwQF4JjgDlrVEn3C11VoGHZN7m8qihwgMEtzYw= +github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE= +github.com/rs/xid v1.6.0 h1:fV591PaemRlL6JfRxGDEPl69wICngIQ3shQtzfy2gxU= +github.com/rs/xid v1.6.0/go.mod h1:7XoLgs4eV+QndskICGsho+ADou8ySMSjJKDIan90Nz0= +github.com/shirou/gopsutil/v3 v3.24.5 h1:i0t8kL+kQTvpAYToeuiVk3TgDeKOFioZO3Ztz/iZ9pI= +github.com/shirou/gopsutil/v3 v3.24.5/go.mod h1:bsoOS1aStSs9ErQ1WWfxllSeS1K5D+U30r2NfcubMVk= +github.com/shoenig/go-m1cpu v0.1.6 h1:nxdKQNcEB6vzgA2E2bvzKIYRuNj7XNJ4S/aRSwKzFtM= +github.com/shoenig/go-m1cpu v0.1.6/go.mod h1:1JJMcUBvfNwpq05QDQVAnx3gUHr9IYF7GNg9SUEw2VQ= +github.com/tinylib/msgp v1.3.0 h1:ULuf7GPooDaIlbyvgAxBV/FI7ynli6LZ1/nVUNu+0ww= +github.com/tinylib/msgp v1.3.0/go.mod h1:ykjzy2wzgrlvpDCRc4LA8UXy6D8bzMSuAF3WD57Gok0= +github.com/tklauser/go-sysconf v0.3.12 h1:0QaGUFOdQaIVdPgfITYzaTegZvdCjmYO52cSFAEVmqU= +github.com/tklauser/go-sysconf v0.3.12/go.mod h1:Ho14jnntGE1fpdOqQEEaiKRpvIavV0hSfmBq8nJbHYI= +github.com/tklauser/numcpus v0.6.1 h1:ng9scYS7az0Bk4OZLvrNXNSAO2Pxr1XXRAPyjhIx+Fk= +github.com/tklauser/numcpus v0.6.1/go.mod h1:1XfjsgE2zo8GVw7POkMbHENHzVg3GzmoZ9fESEdAacY= +github.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo0= +github.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0= +golang.org/x/crypto v0.42.0 h1:chiH31gIWm57EkTXpwnqf8qeuMUi0yekh6mT2AvFlqI= +golang.org/x/crypto v0.42.0/go.mod h1:4+rDnOTJhQCx2q7/j6rAN5XDw8kPjeaXEUR2eL94ix8= +golang.org/x/net v0.44.0 h1:evd8IRDyfNBMBTTY5XRF1vaZlD+EmWx6x8PkhR04H/I= +golang.org/x/net v0.44.0/go.mod h1:ECOoLqd5U3Lhyeyo/QDCEVQ4sNgYsqvCZ722XogGieY= +golang.org/x/sync v0.17.0 h1:l60nONMj9l5drqw6jlhIELNv9I0A4OFgRsG9k2oT9Ug= +golang.org/x/sync v0.17.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= +golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201204225414-ed752295db88/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.36.0 h1:KVRy2GtZBrk1cBYA7MKu5bEZFxQk4NIDV6RLVcC8o0k= +golang.org/x/sys v0.36.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= +golang.org/x/text v0.29.0 h1:1neNs90w9YzJ9BocxfsQNHKuAT4pkghyXc4nhZ6sJvk= +golang.org/x/text v0.29.0/go.mod h1:7MhJOA9CD2qZyOKYazxdYMF85OwPdEr9jTtBpO7ydH4= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/sentinel/main/business_service.go b/sentinel/main/business_service.go new file mode 100644 index 0000000..156e9b4 --- /dev/null +++ b/sentinel/main/business_service.go @@ -0,0 +1,131 @@ +package main + +import ( + "encoding/json" + "os" + "os/exec" + "path/filepath" + "sentinel/pkg/log" + model2 "sentinel/pkg/model" + "sentinel/pkg/utils" + "time" +) + +type BusinessService struct { + mqtt *MQTTService + deviceID string + project string + cmdTopic string + deviceType string +} + +func NewBusinessService(m *MQTTService, project, deviceType, deviceID string) *BusinessService { + // 根据统一规则生成 topic + cmdTopic := project + "/cmd/" + deviceType + "/" + deviceID + "/#" + return &BusinessService{ + mqtt: m, + project: project, + deviceID: deviceID, + cmdTopic: cmdTopic, + deviceType: deviceType, + } +} + +func (b *BusinessService) Start() error { + // 订阅 cmd topic + if err := b.mqtt.Subscribe(b.cmdTopic, 1); err != nil { + return err + } + b.mqtt.SetMessageHandler(b.onMQTTMessage) + // 第一次连接就发送状态信息 + b.SendStatusInfo() + + return nil +} + +// 消息处理 +func (b *BusinessService) onMQTTMessage(topic string, payload []byte) { + model := model2.FromStringToMqttTopic(topic) + // 是指令 + if model.Domain == "cmd" { + log.Println("收到指令:", model.Resource) + switch model.Resource { + case "ping": + log.Println("pong") + case "shutdown": + b.handleShutdown() + case "restart": + b.handleRestart() + case "check_update": + b.handleCheckUpdate() + default: + log.Println("未知的命令:", model.Resource) + } + } +} + +func (b *BusinessService) SendStatusInfo() { + info := map[string]interface{}{ + "project": utils.PROJECT, + "deviceType": utils.DEVICE_TPYE, + "version": utils.APP_VERSION, + "online": true, + "ip": utils.GetLocalIP(), + "hostname": utils.GetHostname(), + "mac": utils.GetMacAddress(), + "os": utils.GetOSInfo(), + "cpu": utils.GetCPUInfo(), + "memory_total": utils.GetMemory(), + "disk_total": utils.GetDisk(), + "last_seen": time.Now().UTC().Format(time.RFC3339), + } + + payload, _ := json.Marshal(info) + topic := b.project + "/status/" + b.deviceType + "/" + b.deviceID + "/info" + qos := byte(1) + retained := true + + if err := b.mqtt.Publish(topic, qos, retained, payload); err != nil { + log.Println("[BUS] failed to send status info:", err) + } else { + log.Println("[BUS] status info sent:", string(payload)) + } +} + +// 关闭程序(立即退出) +func (b *BusinessService) handleShutdown() { + os.Exit(0) +} + +// 重启程序 +func (b *BusinessService) handleRestart() { + exe, _ := os.Executable() + cmd := exec.Command(exe) + cmd.Stdout = os.Stdout + cmd.Stderr = os.Stderr + _ = cmd.Start() + os.Exit(0) +} + +// 更新程序 +func (b *BusinessService) handleCheckUpdate() { + exe, _ := os.Executable() + updaterPath := filepath.Join(filepath.Dir(exe), "updater") + if _, err := os.Stat(updaterPath); os.IsNotExist(err) { + if _, err2 := os.Stat(updaterPath + ".exe"); err2 == nil { + updaterPath = updaterPath + ".exe" + } else { + log.Println("[BUS] updater not found") + return + } + } + cmd := exec.Command(updaterPath, "--target", exe) + cmd.Stdout = os.Stdout + cmd.Stderr = os.Stderr + if err := cmd.Start(); err != nil { + log.Println("[BUS] failed to start updater:", err) + return + } + log.Println("[BUS] exiting main program for update") + os.Exit(0) +} diff --git a/sentinel/main/func.go b/sentinel/main/func.go new file mode 100644 index 0000000..db5cc84 --- /dev/null +++ b/sentinel/main/func.go @@ -0,0 +1,66 @@ +package main + +import ( + "context" + "fmt" + "sentinel/pkg/device" + "sentinel/pkg/log" + "sentinel/pkg/model" + api "sentinel/pkg/net" + "sentinel/pkg/storage" +) + +func test() { + // 0. 从海康SDK获取图片以及信息 + record := loadData() + // 1. 上传图片到OSS + uploadFile(record.LicensePlateImage, record.VehicleImage) + // 2. 调用分析请求 + analytics(record) +} + +func loadData() model.Record { + return model.Record{ + DeviceId: device.GetDeviceID(), + LicensePlate: "晋A-888888", + LicensePlateImage: "licensePlateImage_test1.jpg", + VehicleType: "大型货车", + VehicleImage: "vehicleImage_test1.jpg", + } +} + +func uploadFile(licensePlateImage string, vehicleImage string) { + if err := storage.Init(); err != nil { + log.Fatal(err) + } + // todo 需要压缩图片至1~3MB + size, err := storage.UploadFile( + context.Background(), + "sentinel", + "license_plate/"+licensePlateImage, + "tmp/"+licensePlateImage, + "image/jpeg", + ) + if err != nil { + log.Fatal(err) + } + log.Println(fmt.Sprintf("车牌照已上传完毕, 大小: = %d KB", size/1024)) + size, err = storage.UploadFile( + context.Background(), + "sentinel", + "vehicle_image/"+vehicleImage, + "tmp/"+vehicleImage, + "image/jpeg", + ) + if err != nil { + log.Fatal(err) + } + log.Println(fmt.Sprintf("车身照已上传完毕, 大小: = %d KB", size/1024)) +} + +func analytics(record model.Record) { + err := api.Analytics(record) + if err != nil { + return + } +} diff --git a/sentinel/main/main.go b/sentinel/main/main.go new file mode 100644 index 0000000..43b3c1a --- /dev/null +++ b/sentinel/main/main.go @@ -0,0 +1,58 @@ +package main + +import ( + "fmt" + "sentinel/pkg/utils" + "time" + + "sentinel/pkg/device" + "sentinel/pkg/log" +) + +func main() { + deviceID := device.GetDeviceID() + log.Init(utils.Log_file_dic) // 初始化日志目录 + log.Info("Device id: " + deviceID) // 第一次启动记录 + + broker := fmt.Sprintf("tls://%s:%d", utils.MQTT_HOST, utils.MQTT_PORT) + username := deviceID + password := utils.PASSWORD + + var mqttSvc *MQTTService + firstFail := true // 标记是否第一次失败 + for { + mqttSvc = NewMQTTService(broker, username, username, password, 60) + err := mqttSvc.Connect() + if err != nil { + if firstFail { + log.Error("物联网服务连接失败,请先注册设备. DeviceID: " + deviceID + " ") + firstFail = false + } + time.Sleep(5 * time.Second) // 5秒后重试 + continue + } + log.Info("物联网服务已启动") + break + } + defer mqttSvc.Close() + + biz := NewBusinessService(mqttSvc, utils.PROJECT, utils.DEVICE_TPYE, deviceID) + for { + // MQTT业务 + err := biz.Start() + if err != nil { + log.Error("business service start failed: " + err.Error()) + fmt.Println("业务启动失败,5秒后重试...") + time.Sleep(5 * time.Second) + continue + } + // 个人业务 + test() + break + } + + // 主线程循环,可做心跳或状态上报 + for { + time.Sleep(10 * time.Second) + } +} diff --git a/sentinel/main/mqtt_service.go b/sentinel/main/mqtt_service.go new file mode 100644 index 0000000..c80e1d6 --- /dev/null +++ b/sentinel/main/mqtt_service.go @@ -0,0 +1,73 @@ +package main + +import ( + "crypto/tls" + "sentinel/pkg/log" + "time" + + mqtt "github.com/eclipse/paho.mqtt.golang" +) + +type MQTTService struct { + client mqtt.Client + handler func(topic string, payload []byte) +} + +func NewMQTTService(broker, clientID, username, password string, keepalive int) *MQTTService { + opts := mqtt.NewClientOptions() + opts.AddBroker(broker) + opts.SetClientID(clientID) + opts.SetUsername(username) + opts.SetPassword(password) + opts.SetKeepAlive(time.Duration(keepalive) * time.Second) + opts.SetTLSConfig(&tls.Config{InsecureSkipVerify: true}) + + ms := &MQTTService{} + opts.SetDefaultPublishHandler(func(c mqtt.Client, m mqtt.Message) { + if ms.handler != nil { + ms.handler(m.Topic(), m.Payload()) + } + }) + opts.OnConnect = func(c mqtt.Client) { + log.Println("物联网服务已连接") + } + opts.OnConnectionLost = func(c mqtt.Client, err error) { + log.Println("物联网服务已断开:", err) + } + + ms.client = mqtt.NewClient(opts) + return ms +} + +func (m *MQTTService) Connect() error { + token := m.client.Connect() + if token.Wait() && token.Error() != nil { + return token.Error() + } + return nil +} + +func (m *MQTTService) Subscribe(topic string, qos byte) error { + token := m.client.Subscribe(topic, qos, nil) + if token.Wait() && token.Error() != nil { + return token.Error() + } + log.Println("物联网服务消息订阅:", topic) + return nil +} + +func (m *MQTTService) Publish(topic string, qos byte, retained bool, payload interface{}) error { + token := m.client.Publish(topic, qos, retained, payload) + token.Wait() + return token.Error() +} + +func (m *MQTTService) SetMessageHandler(h func(topic string, payload []byte)) { + m.handler = h +} + +func (m *MQTTService) Close() { + if m.client != nil { + m.client.Disconnect(250) + } +} diff --git a/sentinel/pkg/device/deviceid.go b/sentinel/pkg/device/deviceid.go new file mode 100644 index 0000000..b75849d --- /dev/null +++ b/sentinel/pkg/device/deviceid.go @@ -0,0 +1,40 @@ +package device + +import ( + "net" + "os" + "strings" +) + +// GetDeviceID 返回本机原始唯一ID(Linux /etc/machine-id 或 hostname+MAC) +func GetDeviceID() string { + // 尝试读取 Linux /etc/machine-id + if data, err := os.ReadFile("/etc/machine-id"); err == nil { + s := strings.TrimSpace(string(data)) + if s != "" { + return s + } + } + + // fallback: hostname + first non零MAC + hn, _ := os.Hostname() + mac := getFirstMac() + return hn + "|" + mac +} + +func getFirstMac() string { + ifaces, err := net.Interfaces() + if err != nil { + return "" + } + for _, it := range ifaces { + if len(it.HardwareAddr) == 0 { + continue + } + mac := it.HardwareAddr.String() + if mac != "" && mac != "00:00:00:00:00:00" { + return mac + } + } + return "" +} diff --git a/sentinel/pkg/log/logger.go b/sentinel/pkg/log/logger.go new file mode 100644 index 0000000..a807394 --- /dev/null +++ b/sentinel/pkg/log/logger.go @@ -0,0 +1,97 @@ +package log + +import ( + "fmt" + "os" + "path/filepath" + "time" +) + +var logDir = "./logs" // 日志目录,可根据需要修改 + +// 初始化日志目录 +func Init(dir string) { + if dir != "" { + logDir = dir + } + if err := os.MkdirAll(logDir, 0755); err != nil { + fmt.Println("create log dir failed:", err) + } + cleanupOldLogs() +} + +// Cleanup 删除超过7天的日志文件 +func cleanupOldLogs() { + files, err := os.ReadDir(logDir) + if err != nil { + return + } + cutoff := time.Now().AddDate(0, 0, -7) + for _, f := range files { + if f.IsDir() { + continue + } + info, err := f.Info() + if err != nil { + continue + } + if info.ModTime().Before(cutoff) { + _ = os.Remove(filepath.Join(logDir, f.Name())) + } + } +} + +// log 内部写文件 +func logToFile(level, msg string) { + fmt.Println(msg) + t := time.Now() + // 确保日志目录存在 + if err := os.MkdirAll(logDir, 0755); err != nil { + fmt.Println("create log dir failed:", err) + return + } + + filename := filepath.Join(logDir, t.Format("2006-01-02")+".log") + f, err := os.OpenFile(filename, os.O_CREATE|os.O_APPEND|os.O_WRONLY, 0644) + if err != nil { + fmt.Println("open log file failed:", err) + return + } + defer f.Close() + + line := fmt.Sprintf("%s [%s] %s\n", t.Format("2006-01-02 15:04:05"), level, msg) + _, _ = f.WriteString(line) +} + +// 对外接口 +func Info(msg string) { + logToFile("INFO", msg) +} + +// Println 支持多个参数拼接,写 INFO 日志 +func Println(v ...interface{}) { + msg := fmt.Sprint(v...) + logToFile("INFO", msg) +} +func Warn(msg string) { + logToFile("WARN", msg) +} + +func Error(msg string) { + logToFile("ERROR", msg) +} +func Fatal(err error) { + if err == nil { + return + } + logToFile("ERROR", err.Error()) +} + +// Fatal 打印错误日志并退出程序 +func Fatalf(msg string, args ...interface{}) { + if len(args) > 0 { + msg = fmt.Sprintf(msg, args...) + } + logToFile("FATAL", msg) + os.Exit(1) +} diff --git a/sentinel/pkg/model/BaseResponse.go b/sentinel/pkg/model/BaseResponse.go new file mode 100644 index 0000000..7b33824 --- /dev/null +++ b/sentinel/pkg/model/BaseResponse.go @@ -0,0 +1,7 @@ +package model + +type BaseResponse struct { + Status bool `json:"status"` // 是否成功 + Message string `json:"message"` // 提示信息 + Data interface{} `json:"data,omitempty"` // 泛型数据,用 interface{} 接收任意类型 +} diff --git a/sentinel/pkg/model/MqttTopic.go b/sentinel/pkg/model/MqttTopic.go new file mode 100644 index 0000000..7187e5a --- /dev/null +++ b/sentinel/pkg/model/MqttTopic.go @@ -0,0 +1,64 @@ +package model + +import ( + "errors" + "strings" +) + +type MqttTopic struct { + Project string + Domain string + DeviceType string + DeviceID string + Resource string +} + +// 从字符串解析成 MqttTopic +func FromStringToMqttTopic(topic string) *MqttTopic { + parts := strings.Split(topic, "/") + // 补齐不足的部分 + for len(parts) < 5 { + parts = append(parts, "") + } + return &MqttTopic{ + Project: parts[0], + Domain: parts[1], + DeviceType: parts[2], + DeviceID: parts[3], + Resource: parts[4], + } +} + +// 从结构体生成 topic 字符串,可用 "+" 表示通配符 +func (m *MqttTopic) ToString() string { + toVal := func(s string) string { + if s == "" { + return "+" + } + return s + } + return strings.Join([]string{ + toVal(m.Project), + toVal(m.Domain), + toVal(m.DeviceType), + toVal(m.DeviceID), + toVal(m.Resource), + }, "/") +} + +// 严格生成 topic,不允许 "+" 或空 +func (m *MqttTopic) Build() (string, error) { + parts := []string{m.Project, m.Domain, m.DeviceType, m.DeviceID, m.Resource} + for _, p := range parts { + if p == "" || p == "+" { + return "", errors.New("cannot build strict topic, wildcard exists") + } + } + return strings.Join(parts, "/"), nil +} + +// 判断是否为通配符 topic +func (m *MqttTopic) IsWildcard() bool { + topic := m.ToString() + return strings.Contains(topic, "+") || strings.Contains(topic, "#") +} diff --git a/sentinel/pkg/model/Record.go b/sentinel/pkg/model/Record.go new file mode 100644 index 0000000..041089f --- /dev/null +++ b/sentinel/pkg/model/Record.go @@ -0,0 +1,9 @@ +package model + +type Record struct { + DeviceId string + LicensePlate string + LicensePlateImage string + VehicleType string + VehicleImage string +} diff --git a/sentinel/pkg/model/UpdateInfo.go b/sentinel/pkg/model/UpdateInfo.go new file mode 100644 index 0000000..0a0fd5a --- /dev/null +++ b/sentinel/pkg/model/UpdateInfo.go @@ -0,0 +1,7 @@ +package model + +type UpdateInfo struct { + Version int `json:"version"` + DownloadURL string `json:"url"` + Notes bool `json:"notes"` +} diff --git a/sentinel/pkg/net/api.go b/sentinel/pkg/net/api.go new file mode 100644 index 0000000..00472fd --- /dev/null +++ b/sentinel/pkg/net/api.go @@ -0,0 +1,27 @@ +package api + +import "sentinel/pkg/model" + +const ( + updateCheckURL = "/iot/common/update/check" + analyticsURL = "/api/public/sentinel-record-analytics" +) + +func CheckUpdate(deviceID string) (*model.UpdateInfo, error) { + var resp model.UpdateInfo + err := Get( + updateCheckURL, + map[string]string{ + "deviceID": deviceID, + }, + &resp, + ) + if err != nil { + return nil, err + } + return &resp, nil +} + +func Analytics(req model.Record) error { + return Post(analyticsURL, req, nil) +} diff --git a/sentinel/pkg/net/httpclient.go b/sentinel/pkg/net/httpclient.go new file mode 100644 index 0000000..2af72c4 --- /dev/null +++ b/sentinel/pkg/net/httpclient.go @@ -0,0 +1,101 @@ +package api + +import ( + "bytes" + "encoding/json" + "fmt" + "net/http" + "net/url" + "sentinel/pkg/model" + "time" + + "sentinel/pkg/log" +) + +const baseURL = "http://127.0.0.1:13011" + +var client = &http.Client{ + Timeout: 5 * time.Second, +} + +func Get(path string, query map[string]string, out any) error { + return do(http.MethodGet, path, query, nil, out) +} + +func Post(path string, body any, out any) error { + return do(http.MethodPost, path, nil, body, out) +} + +func do(method, path string, query map[string]string, body any, out any) error { + u, err := url.Parse(baseURL + path) + if err != nil { + log.Error("parse url failed: " + err.Error()) + return err + } + + if len(query) > 0 { + q := u.Query() + for k, v := range query { + q.Set(k, v) + } + u.RawQuery = q.Encode() + } + + var reqBody *bytes.Reader + if body != nil { + b, err := json.Marshal(body) + if err != nil { + log.Error("marshal body failed: " + err.Error()) + return err + } + reqBody = bytes.NewReader(b) + } else { + reqBody = bytes.NewReader(nil) + } + + req, err := http.NewRequest(method, u.String(), reqBody) + if err != nil { + log.Error("create request failed: " + err.Error()) + return err + } + req.Header.Set("Content-Type", "application/json") + + resp, err := client.Do(req) + if err != nil { + log.Error("request failed: " + err.Error()) + return err + } + defer resp.Body.Close() + + if resp.StatusCode < 200 || resp.StatusCode >= 300 { + log.Error("http status error: " + resp.Status) + return fmt.Errorf("http status %d", resp.StatusCode) + } + + // 解析成 BaseResponse + var baseResp model.BaseResponse + if err := json.NewDecoder(resp.Body).Decode(&baseResp); err != nil { + log.Error("decode base response failed: " + err.Error()) + return err + } + + if !baseResp.Status { + log.Error("server returned error: " + baseResp.Message) + return fmt.Errorf(baseResp.Message) + } + + if out != nil && baseResp.Data != nil { + // 将 Data 转成业务类型 + b, err := json.Marshal(baseResp.Data) + if err != nil { + log.Error("marshal data failed: " + err.Error()) + return err + } + if err := json.Unmarshal(b, out); err != nil { + log.Error("unmarshal data to out failed: " + err.Error()) + return err + } + } + + return nil +} diff --git a/sentinel/pkg/storage/minio.go b/sentinel/pkg/storage/minio.go new file mode 100644 index 0000000..1c915b8 --- /dev/null +++ b/sentinel/pkg/storage/minio.go @@ -0,0 +1,62 @@ +package storage + +import ( + "context" + "errors" + + "github.com/minio/minio-go/v7" + "github.com/minio/minio-go/v7/pkg/credentials" +) + +const ( + minioEndpoint = "ai.ronsunny.cn:9000" + minioAccessKey = "minioadmin" + minioSecretKey = "minioadmin" + useSSL = true +) + +var client *minio.Client + +func Init() error { + if client != nil { + return nil + } + + c, err := minio.New(minioEndpoint, &minio.Options{ + Creds: credentials.NewStaticV4(minioAccessKey, minioSecretKey, ""), + Secure: useSSL, + }) + if err != nil { + return err + } + + client = c + return nil +} +func UploadFile( + ctx context.Context, + bucketName string, + objectName string, + filePath string, + contentType string, +) (int64, error) { + + if client == nil { + return 0, errors.New("minio client not initialized") + } + + info, err := client.FPutObject( + ctx, + bucketName, + objectName, + filePath, + minio.PutObjectOptions{ + ContentType: contentType, + }, + ) + if err != nil { + return 0, err + } + + return info.Size, nil +} diff --git a/sentinel/pkg/utils/Global.go b/sentinel/pkg/utils/Global.go new file mode 100644 index 0000000..a62dcc7 --- /dev/null +++ b/sentinel/pkg/utils/Global.go @@ -0,0 +1,16 @@ +package utils + +// 变动 + +// 常量 +const ( + // 版本号 + APP_VERSION = 0 + + Log_file_dic = "./logs" + MQTT_HOST = "ai.ronsunny.cn" + MQTT_PORT = 8093 + PASSWORD = "123456" + PROJECT = "sentinel" + DEVICE_TPYE = "edge" +) diff --git a/sentinel/pkg/utils/utils.go b/sentinel/pkg/utils/utils.go new file mode 100644 index 0000000..5c28daf --- /dev/null +++ b/sentinel/pkg/utils/utils.go @@ -0,0 +1,100 @@ +package utils + +import ( + "fmt" + "net" + "os" + "runtime" + + "github.com/shirou/gopsutil/v3/cpu" + "github.com/shirou/gopsutil/v3/disk" + "github.com/shirou/gopsutil/v3/mem" +) + +func GetLocalIP() string { + ifaces, err := net.Interfaces() + if err != nil { + return "" + } + for _, iface := range ifaces { + if iface.Flags&net.FlagUp == 0 { + continue // 接口未启用 + } + if iface.Flags&net.FlagLoopback != 0 { + continue // 忽略回环 + } + addrs, err := iface.Addrs() + if err != nil { + continue + } + for _, addr := range addrs { + var ip net.IP + switch v := addr.(type) { + case *net.IPNet: + ip = v.IP + case *net.IPAddr: + ip = v.IP + } + if ip == nil || ip.IsLoopback() { + continue + } + ip = ip.To4() + if ip == nil { + continue + } + return ip.String() + } + } + return "" +} + +func GetHostname() string { + name, err := os.Hostname() + if err != nil { + return "" + } + return name +} +func GetMacAddress() string { + ifaces, err := net.Interfaces() + if err != nil { + return "" + } + for _, iface := range ifaces { + if iface.Flags&net.FlagUp == 0 || iface.Flags&net.FlagLoopback != 0 { + continue + } + mac := iface.HardwareAddr.String() + if mac != "" { + return mac + } + } + return "" +} + +func GetOSInfo() string { + return runtime.GOOS // windows, linux, darwin +} +func GetCPUInfo() string { + info, err := cpu.Info() + if err != nil || len(info) == 0 { + return "" + } + return info[0].ModelName +} + +func GetMemory() string { + v, err := mem.VirtualMemory() + if err != nil { + return "" + } + return fmt.Sprintf("%dMB", v.Total/1024/1024) +} + +func GetDisk() string { + usage, err := disk.Usage("/") + if err != nil { + return "" + } + return fmt.Sprintf("%dGB", usage.Total/1024/1024/1024) +} diff --git a/sentinel/tmp/licensePlateImage_test1.jpg b/sentinel/tmp/licensePlateImage_test1.jpg new file mode 100644 index 0000000..aa0fd72 Binary files /dev/null and b/sentinel/tmp/licensePlateImage_test1.jpg differ diff --git a/sentinel/tmp/vehicleImage_test1.jpg b/sentinel/tmp/vehicleImage_test1.jpg new file mode 100644 index 0000000..7b7046a Binary files /dev/null and b/sentinel/tmp/vehicleImage_test1.jpg differ diff --git a/sentinel/updater/update.go b/sentinel/updater/update.go new file mode 100644 index 0000000..a13f731 --- /dev/null +++ b/sentinel/updater/update.go @@ -0,0 +1,83 @@ +package main + +import ( + "crypto/sha256" + "fmt" + "io" + "net/http" + "os" + "os/exec" + "path/filepath" + "sentinel/pkg/log" + "sentinel/pkg/utils" + + "sentinel/pkg/device" + "sentinel/pkg/net" +) + +func main() { + deviceID := device.GetDeviceID() + fmt.Printf("[updater] device id: %s\n", deviceID) + + exeDir, _ := os.Executable() + target := filepath.Join(filepath.Dir(exeDir), "main_program_binary_name") // TODO: 替换 + + if err := RunUpdate(deviceID, target); err != nil { + log.Fatalf("[updater] update failed: %v", err) + } + fmt.Println("[updater] update finished") +} + +// RunUpdate 检查更新、下载、替换主程序并启动新程序 +func RunUpdate(deviceID string, targetExe string) error { + info, err := api.CheckUpdate(deviceID) + if err != nil { + return err + } + // 2. 比对本地版本 + if info.Version <= utils.APP_VERSION { + fmt.Println("[updater] already latest version:", utils.APP_VERSION) + return nil + } + fmt.Println("[updater] updating to version:", info.Version, "notes:", info.Notes) + + // 3. 下载新版本到临时目录 + tmpFile := filepath.Join(os.TempDir(), "new_program_tmp") + out, err := os.Create(tmpFile) + if err != nil { + return fmt.Errorf("create temp file failed: %w", err) + } + defer out.Close() + + resp2, err := http.Get(info.DownloadURL) + if err != nil { + return fmt.Errorf("download failed: %w", err) + } + defer resp2.Body.Close() + + h := sha256.New() + mw := io.MultiWriter(out, h) + if _, err := io.Copy(mw, resp2.Body); err != nil { + return fmt.Errorf("write temp file failed: %w", err) + } + + // 4. 替换 targetExe + backup := targetExe + ".bak" + _ = os.Remove(backup) + _ = os.Rename(targetExe, backup) // 备份旧版本 + if err := os.Rename(tmpFile, targetExe); err != nil { + return fmt.Errorf("replace main program failed: %w", err) + } + fmt.Println("[updater] replaced main program") + + // 5. 启动新主程序 + cmd := exec.Command(targetExe) + cmd.Stdout = os.Stdout + cmd.Stderr = os.Stderr + if err := cmd.Start(); err != nil { + return fmt.Errorf("start new program failed: %w", err) + } + + fmt.Println("[updater] new program started successfully") + return nil +}