重新整理物联网服务,分离业务逻辑。

This commit is contained in:
BBIT-Kai
2026-01-05 11:54:33 +08:00
parent e2982b141d
commit 892cb2494e
53 changed files with 558 additions and 26 deletions
+10
View File
@@ -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/
+8
View File
@@ -0,0 +1,8 @@
<component name="ProjectDictionaryState">
<dictionary name="project">
<words>
<w>linux</w>
<w>从海康sdk获取图片以及信息</w>
</words>
</dictionary>
</component>
+11
View File
@@ -0,0 +1,11 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="GoImports">
<option name="excludedPackages">
<array>
<option value="github.com/pkg/errors" />
<option value="golang.org/x/net/context" />
</array>
</option>
</component>
</project>
+8
View File
@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ProjectModuleManager">
<modules>
<module fileurl="file://$PROJECT_DIR$/.idea/sentinel.iml" filepath="$PROJECT_DIR$/.idea/sentinel.iml" />
</modules>
</component>
</project>
+15
View File
@@ -0,0 +1,15 @@
<?xml version="1.0" encoding="UTF-8"?>
<module type="WEB_MODULE" version="4">
<component name="Go" enabled="true">
<buildTags>
<option name="os" value="linux" />
<option name="arch" value="arm64" />
<option name="cgo" value="NO" />
</buildTags>
</component>
<component name="NewModuleRootManager">
<content url="file://$MODULE_DIR$" />
<orderEntry type="inheritedJdk" />
<orderEntry type="sourceFolder" forTests="false" />
</component>
</module>
+7
View File
@@ -0,0 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="VcsDirectoryMappings">
<mapping directory="$PROJECT_DIR$/../.." vcs="Git" />
<mapping directory="$PROJECT_DIR$/.." vcs="Git" />
</component>
</project>
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
+44
View File
@@ -0,0 +1,44 @@
# build_all.ps1
# 一键打包 main 和 updater,三平台: Windows 64, Linux amd64, Linux arm64
# -------------------------
# 项目根目录
$RootDir = Split-Path -Parent $MyInvocation.MyCommand.Definition
Set-Location $RootDir
# -------------------------
# 定义要打包的平台
$targets = @(
@{ OS="windows"; ARCH="amd64"; Dir="build/win"; MainOut="main.exe"; UpdaterOut="updater.exe" },
@{ OS="linux"; ARCH="amd64"; Dir="build/linux_amd64"; MainOut="main"; UpdaterOut="updater" },
@{ OS="linux"; ARCH="arm64"; Dir="build/linux_arm64"; MainOut="main"; UpdaterOut="updater" }
)
# -------------------------
# 循环打包
foreach ($t in $targets) {
Write-Host "=== Build $($t.OS)-$($t.ARCH) Version ==="
# 设置环境变量
$env:GOOS = $t.OS
$env:GOARCH = $t.ARCH
$env:CGO_ENABLED = "0"
# 创建输出目录
if (!(Test-Path $t.Dir)) {
New-Item -ItemType Directory -Path $t.Dir | Out-Null
}
# 编译 main
Write-Host "Building main..."
go build -ldflags="-s -w" -o "$($t.Dir)/$($t.MainOut)" ./main
# 编译 updater
Write-Host "Building updater..."
go build -ldflags="-s -w" -o "$($t.Dir)/$($t.UpdaterOut)" ./updater
Write-Host "$($t.OS)-$($t.ARCH) Build complete, Output dir: $($t.Dir)"
Write-Host ""
}
Write-Host "All builds finished."
+34
View File
@@ -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
)
+64
View File
@@ -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=
+209
View File
@@ -0,0 +1,209 @@
package main
import (
"encoding/json"
"os"
"os/exec"
"sentinel/pkg/config"
"sentinel/pkg/log"
model2 "sentinel/pkg/model"
"sentinel/pkg/utils"
"strconv"
"time"
)
type BusinessService struct {
mqtt *MQTTService
deviceID string
deptId string
cmdTopic string
deviceType string
subscriptions map[string]struct{} // 记录已订阅 topic
}
func NewBusinessService(m *MQTTService, deviceID string) *BusinessService {
return &BusinessService{
mqtt: m,
deviceID: deviceID,
subscriptions: make(map[string]struct{}),
}
}
// SubscribeTopic 订阅指定 topic,并记录可取消
func (b *BusinessService) SubscribeTopic(topic string, qos byte) error {
if err := b.mqtt.Subscribe(topic, qos); err != nil {
return err
}
b.subscriptions[topic] = struct{}{}
return nil
}
func getInitTopic(deviceID string) string {
return "+/+/+/" + deviceID + "/#"
}
func (b *BusinessService) getOwnTopic(deviceID string) string {
return b.deptId + "/cmd/" + b.deviceType + "/" + deviceID + "/#"
}
func (b *BusinessService) Start() error {
if err := b.SubscribeTopic(getInitTopic(b.deviceID), 1); err != nil {
return err
}
b.mqtt.SetMessageHandler(b.onMQTTMessage)
// 第一次连接就发送状态信息
b.SendStatusInfo()
return nil
}
// UnsubscribeTopic 取消订阅指定 topic
func (b *BusinessService) UnsubscribeTopic(topic string) error {
token := b.mqtt.client.Unsubscribe(topic)
if token.Wait() && token.Error() != nil {
return token.Error()
}
delete(b.subscriptions, topic)
return nil
}
// UnsubscribeAll 取消所有已订阅 topic
func (b *BusinessService) UnsubscribeAll() {
for topic := range b.subscriptions {
_ = b.mqtt.client.Unsubscribe(topic)
delete(b.subscriptions, topic)
}
}
// 消息处理
func (b *BusinessService) onMQTTMessage(topic string, payload []byte) {
model := model2.FromStringToMqttTopic(topic)
// 指令
if model.Domain == "cmd" && model.DeviceType == b.deviceType {
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)
}
} else if model.Domain == "status" && model.Resource == "receipt" {
b.deviceType = model.DeviceType
b.deptId = model.DeptId
// 取消订阅之前的初始化主题
if b.UnsubscribeTopic(getInitTopic(b.deviceID)) != nil {
log.Error("无法取消初始化主题")
return
}
// 新订阅属于自己的主题
if b.SubscribeTopic(b.getOwnTopic(b.deviceID), 1) != nil {
log.Error("无法定于属于自己的主题")
return
}
log.Println("设备初始化成功:所属项目:", model.DeptId, "\t设备类型:", model.DeviceType)
}
}
func (b *BusinessService) SendStatusInfo() {
info := map[string]interface{}{
"version": config.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 := "x/status/x/" + b.deviceID + "/info"
qos := byte(1)
retained := true
log.Println("发送消息:", topic)
if err := b.mqtt.Publish(topic, qos, retained, payload); err != nil {
log.Println("发送状态信息出错:", err)
} else {
log.Println("发送状态信息:", 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() {
args := []string{
"--version", strconv.Itoa(config.APP_VERSION),
}
launcher := newUpdaterLauncher()
if err := launcher.Start(args); err != nil {
log.Println("[BUS] failed to start updater:", err)
return
}
log.Println(
"[BUS] updater started, exiting main program",
)
time.Sleep(500 * time.Millisecond)
os.Exit(0)
}
// handleCheckUpdate 触发更新流程(主程序侧)
//func (b *BusinessService) handleCheckUpdate() {
//
// args := []string{
// "--version", strconv.Itoa(config.APP_VERSION),
// }
//
// cmd := exec.Command("./updater.exe", args...)
// cmd.Stdout = os.Stdout
// cmd.Stderr = os.Stderr
//
// // OS 级脱离父进程
// switch runtime.GOOS {
// case "windows":
// cmd.SysProcAttr = &syscall.SysProcAttr{
// CreationFlags: syscall.CREATE_NEW_PROCESS_GROUP,
// }
// }
//
// if err := cmd.Start(); err != nil {
// log.Println("[BUS] failed to start updater:", err)
// return
// }
//
// log.Println(
// "[BUS] updater started (pid=%d), exiting main program\n",
// cmd.Process.Pid,
// )
//
// // 给 updater 留出启动窗口(尤其是 systemd / docker 环境)
// time.Sleep(500 * time.Millisecond)
//
// os.Exit(0)
//}
+64
View File
@@ -0,0 +1,64 @@
package main
import (
"fmt"
"time"
"sentinel/pkg/config"
"sentinel/pkg/device"
"sentinel/pkg/log"
)
func main() {
banner := `
==========================================================================
██████ █████ █████ ██████ ██ █████ ██████ ██ ██ ██████
██ ██ ██ ██ ██ ████ ██ ██ ███ ██ ██
█████ ██ ██ ██ ██ █████ ██ ██ ██ ██ █████ ████ ██ ██
██ ██ ██ ██ ██ ██ ██████ ██ ██ ██ ██ ████ ██
██████ █████ █████ ██████ ██ ██ █████ ██████ ██ ██ ██
==========================================================================
`
fmt.Println(banner)
deviceID := device.GetDeviceID()
log.Init(config.Log_file_dic) // 初始化日志目录
log.Info("Device id: " + deviceID) // 第一次启动记录
log.Println("版本号: ", config.APP_VERSION) // 第一次启动记录
var mqttSvc *MQTTService
firstFail := true // 标记是否第一次失败
for {
mqttSvc = NewMQTTService(config.MQTT_BROKER, deviceID, deviceID, config.PASSWORD, 60)
err := mqttSvc.Connect()
if err != nil {
if firstFail {
log.Error("物联网服务连接失败,如未注册设备,请先注册: " + deviceID)
firstFail = false
}
time.Sleep(3 * time.Second) // 5秒后重试
continue
}
break
}
defer mqttSvc.Close()
biz := NewBusinessService(mqttSvc, 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
}
break
}
// 主线程循环,可做心跳或状态上报
for {
time.Sleep(10 * time.Second)
}
}
+7
View File
@@ -0,0 +1,7 @@
package main
import "sentinel/pkg/platform"
func newUpdaterLauncher() platform.UpdaterLauncher {
return &updaterLauncher{}
}
+20
View File
@@ -0,0 +1,20 @@
//go:build linux
// +build linux
package main
import (
"os"
"os/exec"
)
type updaterLauncher struct{}
func (u *updaterLauncher) Start(args []string) error {
cmd := exec.Command("./updater", args...)
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
// Linux 下先不做进程组处理,保证能跑
return cmd.Start()
}
+24
View File
@@ -0,0 +1,24 @@
//go:build windows
// +build windows
package main
import (
"os"
"os/exec"
"syscall"
)
type updaterLauncher struct{}
func (u *updaterLauncher) Start(args []string) error {
cmd := exec.Command("./updater.exe", args...)
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
cmd.SysProcAttr = &syscall.SysProcAttr{
CreationFlags: syscall.CREATE_NEW_PROCESS_GROUP,
}
return cmd.Start()
}
+73
View File
@@ -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)
}
}
+20
View File
@@ -0,0 +1,20 @@
package config
// 变动
// 常量
const (
// 版本号
APP_VERSION = 1
BASE_URL = "https://ai.ronsunny.cn:8090"
//BASE_URL = "http://127.0.0.1:13011"
Log_file_dic = "./logs"
MQTT_BROKER = "tls://ai.ronsunny.cn:8093"
PASSWORD = "123456"
)
var (
// DeviceType string
// DeptId string
)
+40
View File
@@ -0,0 +1,40 @@
package device
import (
"net"
"os"
"strings"
)
// GetDeviceID 返回本机原始唯一IDLinux /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 ""
}
+97
View File
@@ -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)
}
+7
View File
@@ -0,0 +1,7 @@
package model
type BaseResponse struct {
Status bool `json:"status"` // 是否成功
Message string `json:"message"` // 提示信息
Data interface{} `json:"data,omitempty"` // 泛型数据,用 interface{} 接收任意类型
}
+64
View File
@@ -0,0 +1,64 @@
package model
import (
"errors"
"strings"
)
type MqttTopic struct {
DeptId 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{
DeptId: 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.DeptId),
toVal(m.Domain),
toVal(m.DeviceType),
toVal(m.DeviceID),
toVal(m.Resource),
}, "/")
}
// 严格生成 topic,不允许 "+" 或空
func (m *MqttTopic) Build() (string, error) {
parts := []string{m.DeptId, 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, "#")
}
+7
View File
@@ -0,0 +1,7 @@
package model
type UpdateInfo struct {
Version int `json:"version"`
DownloadURL string `json:"url"`
Notes string `json:"notes"`
}
+22
View File
@@ -0,0 +1,22 @@
package api
import "sentinel/pkg/model"
const (
updateCheckURL = "/iot/common/update/check"
)
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
}
+101
View File
@@ -0,0 +1,101 @@
package api
import (
"bytes"
"encoding/json"
"fmt"
"net/http"
"net/url"
"sentinel/pkg/model"
"time"
"sentinel/pkg/config"
"sentinel/pkg/log"
)
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(config.BASE_URL + 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")
req.Header.Set("apikey", "NzusyzcLIUoZ22tflHN2sOjHrry3W7zJ")
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
}
@@ -0,0 +1,11 @@
package platform
type UpdaterLauncher interface {
Start(args []string) error
}
type MainProgramStarter interface {
Start(targetExe string) error
GetMainName() string
}
+100
View File
@@ -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)
}
+132
View File
@@ -0,0 +1,132 @@
package main
import (
"flag"
"fmt"
"io"
"net/http"
"net/url"
"os"
"path/filepath"
"sentinel/pkg/device"
"sentinel/pkg/log"
"sentinel/pkg/net"
"strconv"
"time"
)
func main() {
// 定义命令行参数
version := flag.String("version", "", "current version of main program")
flag.Parse()
if *version == "" {
// updater 视角:-1 表示“未知版本”,一定触发更新检测
*version = "0"
log.Println("[updater] --version not provided, fallback to -1")
fmt.Println("[updater] 主程序版本号:", *version)
}
deviceID := device.GetDeviceID()
fmt.Printf("[updater] 当前设备id: %s\n", deviceID)
versionInt, err := strconv.Atoi(*version)
if err != nil {
log.Println("[updater] invalid version:", *version)
versionInt = 0
}
if err := RunUpdate(deviceID, versionInt); err != nil {
log.Fatalf("[updater] 更新失败: %v", err)
}
fmt.Println("[updater] 更新程序结束")
}
func RunUpdate(deviceID string, version int) error {
// 1. 检查更新
info, err := api.CheckUpdate(deviceID)
if err != nil {
fmt.Println("[updater] 请求错误,请检查网络")
return err
}
fmt.Println("[updater] 新版本:", info.Version)
fmt.Println("[updater] 新内容:", info.Notes)
fmt.Println("[updater] 下载地址:", info.DownloadURL)
// 获取主程序路径
selfPath, err := os.Executable()
if err != nil {
return err
}
selfDir := filepath.Dir(selfPath)
starter := newMainProgramStarter()
targetExe := filepath.Join(selfDir, starter.GetMainName())
// 2. 对比版本号,没有新版本则直接启动原程序
if info.Version <= version {
fmt.Println("[updater] 暂未发现新版本,启动原程序")
if err := starter.Start(targetExe); err != nil {
return err
}
os.Exit(0)
}
// 3. 有新版本则先备份到 ./tmp/old_app/
backupDir := filepath.Join(selfDir, "tmp", "old_app")
_ = os.MkdirAll(backupDir, 0755)
backupFile := filepath.Join(backupDir, "main_"+strconv.Itoa(version)+".bak")
if err := os.Rename(targetExe, backupFile); err != nil {
fmt.Println("[updater] 备份失败,但继续更新:", err)
}
// 4. 下载新版本到 ./tmp
tmpDir := filepath.Join(selfDir, "tmp")
_ = os.MkdirAll(tmpDir, 0755)
u, err := url.Parse(info.DownloadURL)
if err != nil {
return err
}
base := filepath.Base(u.Path)
ext := filepath.Ext(base)
tmpFile, err := os.CreateTemp(tmpDir, "main_*"+ext)
if err != nil {
return err
}
defer tmpFile.Close()
client := &http.Client{Timeout: 30 * time.Second}
resp, err := client.Get(info.DownloadURL)
if err != nil {
return err
}
defer resp.Body.Close()
if _, err := io.Copy(tmpFile, resp.Body); err != nil {
return err
}
// 5. 重命名新文件到 ./main.exe
tmpFile.Close() // 关闭临时文件才能重命名
maxRetry := 20
for i := 0; i < maxRetry; i++ {
err := os.Rename(tmpFile.Name(), targetExe)
if err == nil {
break
}
fmt.Println("[updater] 文件被占用,等待 500ms 再尝试...")
time.Sleep(500 * time.Millisecond)
if i == maxRetry-1 {
return fmt.Errorf("替换失败: %w", err)
}
}
// 6. 启动主程序,同时完全退出自己
if err := starter.Start(targetExe); err != nil {
return err
}
fmt.Printf("[updater] 更新完成,新程序已启动,退出更新程序")
os.Exit(0)
return nil
}
+7
View File
@@ -0,0 +1,7 @@
package main
import "sentinel/pkg/platform"
func newMainProgramStarter() platform.MainProgramStarter {
return &mainProgramStarter{}
}
+27
View File
@@ -0,0 +1,27 @@
//go:build linux
// +build linux
package main
import (
"os"
"os/exec"
)
type mainProgramStarter struct{}
func (s *mainProgramStarter) GetMainName() string {
return "main"
}
func (s *mainProgramStarter) Start(targetExe string) error {
if err := os.Chmod(targetExe, 0755); err != nil {
return err
}
cmd := exec.Command(targetExe)
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
// Linux 下:先保持最简单,保证能跑
return cmd.Start()
}
+29
View File
@@ -0,0 +1,29 @@
//go:build windows
// +build windows
package main
import (
"os"
"os/exec"
"syscall"
)
type mainProgramStarter struct{}
func (s *mainProgramStarter) GetMainName() string {
return "main.exe"
}
func (s *mainProgramStarter) Start(targetExe string) error {
cmd := exec.Command(targetExe)
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
// Windows:新进程组,脱离 Ctrl+C / 父进程
cmd.SysProcAttr = &syscall.SysProcAttr{
CreationFlags: syscall.CREATE_NEW_PROCESS_GROUP,
}
return cmd.Start()
}
+10
View File
@@ -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/
+8
View File
@@ -0,0 +1,8 @@
<component name="ProjectDictionaryState">
<dictionary name="project">
<words>
<w>linux</w>
<w>从海康sdk获取图片以及信息</w>
</words>
</dictionary>
</component>
+11
View File
@@ -0,0 +1,11 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="GoImports">
<option name="excludedPackages">
<array>
<option value="github.com/pkg/errors" />
<option value="golang.org/x/net/context" />
</array>
</option>
</component>
</project>
+8
View File
@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ProjectModuleManager">
<modules>
<module fileurl="file://$PROJECT_DIR$/.idea/sentinel.iml" filepath="$PROJECT_DIR$/.idea/sentinel.iml" />
</modules>
</component>
</project>
+16
View File
@@ -0,0 +1,16 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="RemoteTargetsManager">
<targets>
<target name="WSL - Ubuntu-22.04" type="wsl" uuid="43159dba-0a6e-45fb-8b38-8e1c2ac28efa">
<config>
<option name="distributionMsId" value="Ubuntu-22.04" />
<option name="projectRootOnTarget" value="{exitCode=0, timeout=false, cancelled=false, stdout=/tmp/tmp.dVq8EjO5tG&#13;&#10;, stderr=}/Sentinel" />
</config>
<ContributedStateBase type="GoLanguageRuntime">
<config />
</ContributedStateBase>
</target>
</targets>
</component>
</project>
+15
View File
@@ -0,0 +1,15 @@
<?xml version="1.0" encoding="UTF-8"?>
<module type="WEB_MODULE" version="4">
<component name="Go" enabled="true">
<buildTags>
<option name="os" value="linux" />
<option name="arch" value="arm64" />
<option name="cgo" value="NO" />
</buildTags>
</component>
<component name="NewModuleRootManager">
<content url="file://$MODULE_DIR$" />
<orderEntry type="inheritedJdk" />
<orderEntry type="sourceFolder" forTests="false" />
</component>
</module>
+7
View File
@@ -0,0 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="VcsDirectoryMappings">
<mapping directory="$PROJECT_DIR$/../.." vcs="Git" />
<mapping directory="$PROJECT_DIR$/.." vcs="Git" />
</component>
</project>
+36
View File
@@ -0,0 +1,36 @@
# ==========================
# Stage 1: Build the Go binary
# ==========================
FROM golang:1.21-alpine AS builder
# 设置工作目录
WORKDIR /build
# 复制 go.mod 和 go.sum,先做依赖缓存
COPY go.mod go.sum ./
# 下载依赖(缓存层)
RUN go mod download
# 复制源代码
COPY app/ ./app
# 编译可执行文件
RUN go build -o sentinel ./app/main.go
# ==========================
# Stage 2: 生成最终运行镜像
# ==========================
FROM alpine:3.18
# 安装 ca-certificates,如果程序需要访问 HTTPS
RUN apk add --no-cache ca-certificates
# 设置工作目录
WORKDIR /app
# 复制从 builder 构建好的二进制
COPY --from=builder /build/sentinel .
# 设置默认执行命令
CMD ["./sentinel"]
+94
View File
@@ -0,0 +1,94 @@
package log
import (
"fmt"
"os"
"path/filepath"
"time"
)
var logDir = "./logs" // 日志目录,可根据需要修改
// 初始化日志目录
func Init() {
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)
}
+79
View File
@@ -0,0 +1,79 @@
package main
import (
"context"
"fmt"
"sentinel/app/log"
"sentinel/app/model"
"sentinel/app/net"
"sentinel/app/storage"
)
func main() {
banner := `
==========================================================================
_______ _______ _ __________________ _ _______ _
( ____ \( ____ \( ( /|\__ __/\__ __/( ( /|( ____ \( \
| (_____ | (__ | \ | | | | | | | \ | || (__ | |
(_____ )| __) | (\ \) | | | | | | (\ \) || __) | |
/\____) || (____/\| ) \ | | | ___) (___| ) \ || (____/\| (____/\
\_______)(_______/|/ )_) )_( \_______/|/ )_)(_______/(_______/
==========================================================================
`
deviceID := "6823316af0e540bea818ec1be25cc14a"
log.Init() // 初始化日志目录
log.Info("Device id: " + deviceID) // 第一次启动记录
log.Info(banner)
// 0. 从海康SDK获取图片以及信息
record := loadData(deviceID)
// 1. 上传图片到OSS
uploadFile(record.LicensePlateImage, record.VehicleImage)
// 2. 调用分析请求
analytics(record)
}
func loadData(deviceID string) model.Record {
return model.Record{
DeviceId: deviceID, //从环境变量取
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
}
}
+7
View File
@@ -0,0 +1,7 @@
package model
type BaseResponse struct {
Status bool `json:"status"` // 是否成功
Message string `json:"message"` // 提示信息
Data interface{} `json:"data,omitempty"` // 泛型数据,用 interface{} 接收任意类型
}
+9
View File
@@ -0,0 +1,9 @@
package model
type Record struct {
DeviceId string
LicensePlate string
LicensePlateImage string
VehicleType string
VehicleImage string
}
+13
View File
@@ -0,0 +1,13 @@
package api
import (
"sentinel/app/model"
)
const (
analyticsURL = "/api/public/sentinel-record-analytics"
)
func Analytics(req model.Record) error {
return Post(analyticsURL, req, nil)
}
+102
View File
@@ -0,0 +1,102 @@
package api
import (
"bytes"
"encoding/json"
"fmt"
"net/http"
"net/url"
"sentinel/app/log"
"sentinel/app/model"
"time"
)
// const baseURL = "http://127.0.0.1:13011"
const baseURL = "https://ai.ronsunny.cn:8090"
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")
req.Header.Set("apikey", "NzusyzcLIUoZ22tflHN2sOjHrry3W7zJ")
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
}
+62
View File
@@ -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
}
+100
View File
@@ -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)
}
+34
View File
@@ -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
)
+64
View File
@@ -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=