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
+}