feat: 新增all选项
This commit is contained in:
parent
49aee3a2ed
commit
bc4af6be86
2
.gitignore
vendored
2
.gitignore
vendored
@ -1 +1 @@
|
||||
logs/
|
||||
logs/
|
||||
|
||||
16
.idea/.gitignore
generated
vendored
16
.idea/.gitignore
generated
vendored
@ -1,8 +1,8 @@
|
||||
# Default ignored files
|
||||
/shelf/
|
||||
/workspace.xml
|
||||
# Editor-based HTTP Client requests
|
||||
/httpRequests/
|
||||
# Datasource local storage ignored files
|
||||
/dataSources/
|
||||
/dataSources.local.xml
|
||||
# Default ignored files
|
||||
/shelf/
|
||||
/workspace.xml
|
||||
# Editor-based HTTP Client requests
|
||||
/httpRequests/
|
||||
# Datasource local storage ignored files
|
||||
/dataSources/
|
||||
/dataSources.local.xml
|
||||
|
||||
16
.idea/angle_dtu.iml
generated
16
.idea/angle_dtu.iml
generated
@ -1,9 +1,9 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<module type="WEB_MODULE" version="4">
|
||||
<component name="Go" enabled="true" />
|
||||
<component name="NewModuleRootManager">
|
||||
<content url="file://$MODULE_DIR$" />
|
||||
<orderEntry type="inheritedJdk" />
|
||||
<orderEntry type="sourceFolder" forTests="false" />
|
||||
</component>
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<module type="WEB_MODULE" version="4">
|
||||
<component name="Go" enabled="true" />
|
||||
<component name="NewModuleRootManager">
|
||||
<content url="file://$MODULE_DIR$" />
|
||||
<orderEntry type="inheritedJdk" />
|
||||
<orderEntry type="sourceFolder" forTests="false" />
|
||||
</component>
|
||||
</module>
|
||||
14
.idea/modules.xml
generated
14
.idea/modules.xml
generated
@ -1,8 +1,8 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="ProjectModuleManager">
|
||||
<modules>
|
||||
<module fileurl="file://$PROJECT_DIR$/.idea/angle_dtu.iml" filepath="$PROJECT_DIR$/.idea/angle_dtu.iml" />
|
||||
</modules>
|
||||
</component>
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="ProjectModuleManager">
|
||||
<modules>
|
||||
<module fileurl="file://$PROJECT_DIR$/.idea/angle_dtu.iml" filepath="$PROJECT_DIR$/.idea/angle_dtu.iml" />
|
||||
</modules>
|
||||
</component>
|
||||
</project>
|
||||
10
.idea/vcs.xml
generated
10
.idea/vcs.xml
generated
@ -1,6 +1,6 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="VcsDirectoryMappings">
|
||||
<mapping directory="" vcs="Git" />
|
||||
</component>
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="VcsDirectoryMappings">
|
||||
<mapping directory="" vcs="Git" />
|
||||
</component>
|
||||
</project>
|
||||
132
angle.go.bak
132
angle.go.bak
@ -1,66 +1,66 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
"net"
|
||||
"time"
|
||||
)
|
||||
|
||||
func handleConnection(conn net.Conn) {
|
||||
defer conn.Close()
|
||||
|
||||
// 客户端地址信息
|
||||
remoteAddr := conn.RemoteAddr().String()
|
||||
fmt.Printf("客户端 %s 已连接\n", remoteAddr)
|
||||
|
||||
// 设置连接超时
|
||||
conn.SetReadDeadline(time.Now().Add(time.Hour * 24)) // 24小时超时,可以根据需要调整
|
||||
|
||||
buffer := make([]byte, 1024)
|
||||
for {
|
||||
// 读取客户端发送的数据
|
||||
n, err := conn.Read(buffer)
|
||||
|
||||
if err != nil {
|
||||
if err == io.EOF {
|
||||
fmt.Printf("客户端 %s 已断开连接\n", remoteAddr)
|
||||
} else {
|
||||
fmt.Printf("从客户端 %s 读取数据时出错: %v\n", remoteAddr, err)
|
||||
}
|
||||
break
|
||||
}
|
||||
|
||||
// 在终端显示接收到的数据
|
||||
receivedData := buffer[:n]
|
||||
fmt.Printf("从 %s 接收到数据: %s\n", remoteAddr, string(receivedData))
|
||||
|
||||
// 可选:回复客户端确认消息
|
||||
// conn.Write([]byte("服务器已收到消息"))
|
||||
}
|
||||
}
|
||||
|
||||
func main() {
|
||||
// 监听端口
|
||||
port := ":10002"
|
||||
listener, err := net.Listen("tcp", port)
|
||||
if err != nil {
|
||||
log.Fatalf("无法监听端口 %s: %v", port, err)
|
||||
}
|
||||
defer listener.Close()
|
||||
|
||||
fmt.Printf("TCP服务器已启动,正在监听端口 %s\n", port)
|
||||
|
||||
// 接受连接并处理
|
||||
for {
|
||||
conn, err := listener.Accept()
|
||||
if err != nil {
|
||||
fmt.Printf("接受连接失败: %v\n", err)
|
||||
continue
|
||||
}
|
||||
|
||||
// 为每个连接创建一个goroutine
|
||||
go handleConnection(conn)
|
||||
}
|
||||
}
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
"net"
|
||||
"time"
|
||||
)
|
||||
|
||||
func handleConnection(conn net.Conn) {
|
||||
defer conn.Close()
|
||||
|
||||
// 客户端地址信息
|
||||
remoteAddr := conn.RemoteAddr().String()
|
||||
fmt.Printf("客户端 %s 已连接\n", remoteAddr)
|
||||
|
||||
// 设置连接超时
|
||||
conn.SetReadDeadline(time.Now().Add(time.Hour * 24)) // 24小时超时,可以根据需要调整
|
||||
|
||||
buffer := make([]byte, 1024)
|
||||
for {
|
||||
// 读取客户端发送的数据
|
||||
n, err := conn.Read(buffer)
|
||||
|
||||
if err != nil {
|
||||
if err == io.EOF {
|
||||
fmt.Printf("客户端 %s 已断开连接\n", remoteAddr)
|
||||
} else {
|
||||
fmt.Printf("从客户端 %s 读取数据时出错: %v\n", remoteAddr, err)
|
||||
}
|
||||
break
|
||||
}
|
||||
|
||||
// 在终端显示接收到的数据
|
||||
receivedData := buffer[:n]
|
||||
fmt.Printf("从 %s 接收到数据: %s\n", remoteAddr, string(receivedData))
|
||||
|
||||
// 可选:回复客户端确认消息
|
||||
// conn.Write([]byte("服务器已收到消息"))
|
||||
}
|
||||
}
|
||||
|
||||
func main() {
|
||||
// 监听端口
|
||||
port := ":10002"
|
||||
listener, err := net.Listen("tcp", port)
|
||||
if err != nil {
|
||||
log.Fatalf("无法监听端口 %s: %v", port, err)
|
||||
}
|
||||
defer listener.Close()
|
||||
|
||||
fmt.Printf("TCP服务器已启动,正在监听端口 %s\n", port)
|
||||
|
||||
// 接受连接并处理
|
||||
for {
|
||||
conn, err := listener.Accept()
|
||||
if err != nil {
|
||||
fmt.Printf("接受连接失败: %v\n", err)
|
||||
continue
|
||||
}
|
||||
|
||||
// 为每个连接创建一个goroutine
|
||||
go handleConnection(conn)
|
||||
}
|
||||
}
|
||||
|
||||
18
db.go
18
db.go
@ -79,8 +79,13 @@ func GetSensorData(sensorID int, limit int, startDate time.Time, endDate time.Ti
|
||||
args = append(args, endDate)
|
||||
}
|
||||
|
||||
query += " ORDER BY timestamp DESC LIMIT ?"
|
||||
args = append(args, limit)
|
||||
query += " ORDER BY timestamp DESC"
|
||||
|
||||
// 只有当limit > 0时才添加LIMIT子句
|
||||
if limit > 0 {
|
||||
query += " LIMIT ?"
|
||||
args = append(args, limit)
|
||||
}
|
||||
|
||||
rows, err := db.Query(query, args...)
|
||||
if err != nil {
|
||||
@ -128,8 +133,13 @@ func GetAllSensorData(limit int, startDate time.Time, endDate time.Time) ([]Sens
|
||||
args = append(args, endDate)
|
||||
}
|
||||
|
||||
query += " ORDER BY timestamp DESC LIMIT ?"
|
||||
args = append(args, limit)
|
||||
query += " ORDER BY timestamp DESC"
|
||||
|
||||
// 只有当limit > 0时才添加LIMIT子句
|
||||
if limit > 0 {
|
||||
query += " LIMIT ?"
|
||||
args = append(args, limit)
|
||||
}
|
||||
|
||||
rows, err := db.Query(query, args...)
|
||||
if err != nil {
|
||||
|
||||
14
go.mod
14
go.mod
@ -1,7 +1,7 @@
|
||||
module probe-monitor
|
||||
|
||||
go 1.24.2
|
||||
|
||||
require github.com/go-sql-driver/mysql v1.9.2
|
||||
|
||||
require filippo.io/edwards25519 v1.1.0 // indirect
|
||||
module probe-monitor
|
||||
|
||||
go 1.24.2
|
||||
|
||||
require github.com/go-sql-driver/mysql v1.9.2
|
||||
|
||||
require filippo.io/edwards25519 v1.1.0 // indirect
|
||||
|
||||
8
go.sum
8
go.sum
@ -1,4 +1,4 @@
|
||||
filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA=
|
||||
filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4=
|
||||
github.com/go-sql-driver/mysql v1.9.2 h1:4cNKDYQ1I84SXslGddlsrMhc8k4LeDVj6Ad6WRjiHuU=
|
||||
github.com/go-sql-driver/mysql v1.9.2/go.mod h1:qn46aNg1333BRMNU69Lq93t8du/dwxI64Gl8i5p1WMU=
|
||||
filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA=
|
||||
filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4=
|
||||
github.com/go-sql-driver/mysql v1.9.2 h1:4cNKDYQ1I84SXslGddlsrMhc8k4LeDVj6Ad6WRjiHuU=
|
||||
github.com/go-sql-driver/mysql v1.9.2/go.mod h1:qn46aNg1333BRMNU69Lq93t8du/dwxI64Gl8i5p1WMU=
|
||||
|
||||
336
http_server.go
336
http_server.go
@ -1,168 +1,168 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"html/template"
|
||||
"log"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"time"
|
||||
)
|
||||
|
||||
// 启动HTTP服务器
|
||||
func StartHTTPServer(address string) error {
|
||||
http.HandleFunc("/", handleIndex)
|
||||
|
||||
http.HandleFunc("/api/data", handleGetData)
|
||||
http.HandleFunc("/api/sensors", handleGetSensors)
|
||||
http.HandleFunc("/api/clients", handleGetClients)
|
||||
fmt.Printf("HTTP服务器已启动,正在监听 %s\n", address)
|
||||
return http.ListenAndServe(address, nil)
|
||||
}
|
||||
|
||||
// 处理主页
|
||||
func handleIndex(w http.ResponseWriter, r *http.Request) {
|
||||
log.Printf("接收到主页请求: %s", r.URL.Path)
|
||||
|
||||
templatePath := "templates/index.html"
|
||||
absPath, _ := filepath.Abs(templatePath)
|
||||
|
||||
_, err := os.Stat(templatePath)
|
||||
if os.IsNotExist(err) {
|
||||
log.Printf("错误: 模板文件不存在: %s", absPath)
|
||||
http.Error(w, "模板文件不存在", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
tmpl, err := template.ParseFiles(templatePath)
|
||||
if err != nil {
|
||||
log.Printf("错误: 无法解析模板: %v", err)
|
||||
http.Error(w, "无法加载模板:"+err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
log.Printf("模板加载成功,开始渲染")
|
||||
err = tmpl.Execute(w, nil)
|
||||
if err != nil {
|
||||
log.Printf("错误: 渲染模板出错: %v", err)
|
||||
http.Error(w, "渲染模板出错:"+err.Error(), http.StatusInternalServerError)
|
||||
}
|
||||
log.Printf("模板渲染完成")
|
||||
}
|
||||
|
||||
// 处理获取传感器数据的API
|
||||
func handleGetData(w http.ResponseWriter, r *http.Request) {
|
||||
log.Printf("接收到获取数据请求: %s", r.URL.String())
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
|
||||
sensorIDStr := r.URL.Query().Get("sensor_id")
|
||||
limitStr := r.URL.Query().Get("limit")
|
||||
startDateStr := r.URL.Query().Get("start_date")
|
||||
endDateStr := r.URL.Query().Get("end_date")
|
||||
|
||||
limit := 500 // 默认限制为500条数据
|
||||
noLimit := false // 是否不限制数据条数
|
||||
|
||||
var sensorID int
|
||||
var err error
|
||||
if sensorIDStr != "" && sensorIDStr != "all" {
|
||||
sensorID, err = strconv.Atoi(sensorIDStr)
|
||||
if err != nil {
|
||||
log.Printf("错误: 无效的传感器ID: %s", sensorIDStr)
|
||||
http.Error(w, "无效的传感器ID", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if limitStr != "" {
|
||||
if limitStr == "all" {
|
||||
noLimit = true
|
||||
limit = 1000000 // 使用一个非常大的数值作为实际上的"无限制"
|
||||
} else {
|
||||
limit, err = strconv.Atoi(limitStr)
|
||||
if err != nil || limit <= 0 {
|
||||
log.Printf("错误: 无效的记录数限制: %s", limitStr)
|
||||
http.Error(w, "无效的记录数限制", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var startDate, endDate time.Time
|
||||
if startDateStr != "" {
|
||||
startDate, err = time.Parse("2006-01-02T15:04", startDateStr)
|
||||
if err != nil {
|
||||
log.Printf("错误: 无效的开始日期: %s, %v", startDateStr, err)
|
||||
http.Error(w, "无效的开始日期", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if endDateStr != "" {
|
||||
endDate, err = time.Parse("2006-01-02T15:04", endDateStr)
|
||||
if err != nil {
|
||||
log.Printf("错误: 无效的结束日期: %s, %v", endDateStr, err)
|
||||
http.Error(w, "无效的结束日期", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
var data []SensorData
|
||||
if sensorIDStr == "all" || sensorIDStr == "" {
|
||||
data, err = GetAllSensorData(limit, startDate, endDate)
|
||||
} else {
|
||||
data, err = GetSensorData(sensorID, limit, startDate, endDate)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
log.Printf("错误: 获取数据失败: %v", err)
|
||||
http.Error(w, "获取数据失败:"+err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
if noLimit {
|
||||
log.Printf("成功获取到所有数据记录(%d条)", len(data))
|
||||
} else {
|
||||
log.Printf("成功获取到 %d 条数据记录(限制:%d条)", len(data), limit)
|
||||
}
|
||||
|
||||
if err := json.NewEncoder(w).Encode(data); err != nil {
|
||||
log.Printf("错误: 编码JSON失败: %v", err)
|
||||
http.Error(w, "编码JSON失败:"+err.Error(), http.StatusInternalServerError)
|
||||
}
|
||||
}
|
||||
|
||||
// 处理获取所有传感器ID的API
|
||||
func handleGetSensors(w http.ResponseWriter, r *http.Request) {
|
||||
log.Printf("接收到获取传感器列表请求")
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
|
||||
sensorIDs, err := GetAllSensorIDs()
|
||||
if err != nil {
|
||||
log.Printf("错误: 获取传感器ID失败: %v", err)
|
||||
http.Error(w, "获取传感器ID失败:"+err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
log.Printf("成功获取到 %d 个传感器ID", len(sensorIDs))
|
||||
|
||||
if err := json.NewEncoder(w).Encode(sensorIDs); err != nil {
|
||||
log.Printf("错误: 编码JSON失败: %v", err)
|
||||
http.Error(w, "编码JSON失败:"+err.Error(), http.StatusInternalServerError)
|
||||
}
|
||||
}
|
||||
func handleGetClients(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
|
||||
clients := getAllClients()
|
||||
|
||||
if err := json.NewEncoder(w).Encode(clients); err != nil {
|
||||
log.Printf("错误: 编码客户端信息JSON失败: %v", err)
|
||||
http.Error(w, "编码JSON失败:"+err.Error(), http.StatusInternalServerError)
|
||||
}
|
||||
}
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"html/template"
|
||||
"log"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"time"
|
||||
)
|
||||
|
||||
// 启动HTTP服务器
|
||||
func StartHTTPServer(address string) error {
|
||||
http.HandleFunc("/", handleIndex)
|
||||
|
||||
http.HandleFunc("/api/data", handleGetData)
|
||||
http.HandleFunc("/api/sensors", handleGetSensors)
|
||||
http.HandleFunc("/api/clients", handleGetClients)
|
||||
fmt.Printf("HTTP服务器已启动,正在监听 %s\n", address)
|
||||
return http.ListenAndServe(address, nil)
|
||||
}
|
||||
|
||||
// 处理主页
|
||||
func handleIndex(w http.ResponseWriter, r *http.Request) {
|
||||
log.Printf("接收到主页请求: %s", r.URL.Path)
|
||||
|
||||
templatePath := "templates/index.html"
|
||||
absPath, _ := filepath.Abs(templatePath)
|
||||
|
||||
_, err := os.Stat(templatePath)
|
||||
if os.IsNotExist(err) {
|
||||
log.Printf("错误: 模板文件不存在: %s", absPath)
|
||||
http.Error(w, "模板文件不存在", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
tmpl, err := template.ParseFiles(templatePath)
|
||||
if err != nil {
|
||||
log.Printf("错误: 无法解析模板: %v", err)
|
||||
http.Error(w, "无法加载模板:"+err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
log.Printf("模板加载成功,开始渲染")
|
||||
err = tmpl.Execute(w, nil)
|
||||
if err != nil {
|
||||
log.Printf("错误: 渲染模板出错: %v", err)
|
||||
http.Error(w, "渲染模板出错:"+err.Error(), http.StatusInternalServerError)
|
||||
}
|
||||
log.Printf("模板渲染完成")
|
||||
}
|
||||
|
||||
// 处理获取传感器数据的API
|
||||
func handleGetData(w http.ResponseWriter, r *http.Request) {
|
||||
log.Printf("接收到获取数据请求: %s", r.URL.String())
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
|
||||
sensorIDStr := r.URL.Query().Get("sensor_id")
|
||||
limitStr := r.URL.Query().Get("limit")
|
||||
startDateStr := r.URL.Query().Get("start_date")
|
||||
endDateStr := r.URL.Query().Get("end_date")
|
||||
|
||||
limit := 500 // 默认限制为500条数据
|
||||
noLimit := false // 是否不限制数据条数
|
||||
|
||||
var sensorID int
|
||||
var err error
|
||||
if sensorIDStr != "" && sensorIDStr != "all" {
|
||||
sensorID, err = strconv.Atoi(sensorIDStr)
|
||||
if err != nil {
|
||||
log.Printf("错误: 无效的传感器ID: %s", sensorIDStr)
|
||||
http.Error(w, "无效的传感器ID", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if limitStr != "" {
|
||||
if limitStr == "all" {
|
||||
noLimit = true
|
||||
limit = 0 // 设置为0表示不使用LIMIT子句
|
||||
} else {
|
||||
limit, err = strconv.Atoi(limitStr)
|
||||
if err != nil || limit <= 0 {
|
||||
log.Printf("错误: 无效的记录数限制: %s", limitStr)
|
||||
http.Error(w, "无效的记录数限制", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var startDate, endDate time.Time
|
||||
if startDateStr != "" {
|
||||
startDate, err = time.Parse("2006-01-02T15:04", startDateStr)
|
||||
if err != nil {
|
||||
log.Printf("错误: 无效的开始日期: %s, %v", startDateStr, err)
|
||||
http.Error(w, "无效的开始日期", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if endDateStr != "" {
|
||||
endDate, err = time.Parse("2006-01-02T15:04", endDateStr)
|
||||
if err != nil {
|
||||
log.Printf("错误: 无效的结束日期: %s, %v", endDateStr, err)
|
||||
http.Error(w, "无效的结束日期", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
var data []SensorData
|
||||
if sensorIDStr == "all" || sensorIDStr == "" {
|
||||
data, err = GetAllSensorData(limit, startDate, endDate)
|
||||
} else {
|
||||
data, err = GetSensorData(sensorID, limit, startDate, endDate)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
log.Printf("错误: 获取数据失败: %v", err)
|
||||
http.Error(w, "获取数据失败:"+err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
if noLimit {
|
||||
log.Printf("成功获取到所有数据记录(%d条)", len(data))
|
||||
} else {
|
||||
log.Printf("成功获取到 %d 条数据记录(限制:%d条)", len(data), limit)
|
||||
}
|
||||
|
||||
if err := json.NewEncoder(w).Encode(data); err != nil {
|
||||
log.Printf("错误: 编码JSON失败: %v", err)
|
||||
http.Error(w, "编码JSON失败:"+err.Error(), http.StatusInternalServerError)
|
||||
}
|
||||
}
|
||||
|
||||
// 处理获取所有传感器ID的API
|
||||
func handleGetSensors(w http.ResponseWriter, r *http.Request) {
|
||||
log.Printf("接收到获取传感器列表请求")
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
|
||||
sensorIDs, err := GetAllSensorIDs()
|
||||
if err != nil {
|
||||
log.Printf("错误: 获取传感器ID失败: %v", err)
|
||||
http.Error(w, "获取传感器ID失败:"+err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
log.Printf("成功获取到 %d 个传感器ID", len(sensorIDs))
|
||||
|
||||
if err := json.NewEncoder(w).Encode(sensorIDs); err != nil {
|
||||
log.Printf("错误: 编码JSON失败: %v", err)
|
||||
http.Error(w, "编码JSON失败:"+err.Error(), http.StatusInternalServerError)
|
||||
}
|
||||
}
|
||||
func handleGetClients(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
|
||||
clients := getAllClients()
|
||||
|
||||
if err := json.NewEncoder(w).Encode(clients); err != nil {
|
||||
log.Printf("错误: 编码客户端信息JSON失败: %v", err)
|
||||
http.Error(w, "编码JSON失败:"+err.Error(), http.StatusInternalServerError)
|
||||
}
|
||||
}
|
||||
|
||||
158
logger.go
158
logger.go
@ -1,79 +1,79 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"time"
|
||||
)
|
||||
|
||||
var (
|
||||
logFile *os.File
|
||||
Logger *log.Logger // 导出Logger供其他包使用
|
||||
TCPDataLogger *log.Logger // 专门用于记录TCP数据的日志
|
||||
)
|
||||
|
||||
// 初始化日志系统
|
||||
func InitLogger() error {
|
||||
logsDir := "logs"
|
||||
if err := os.MkdirAll(logsDir, 0755); err != nil {
|
||||
return fmt.Errorf("创建日志目录失败: %v", err)
|
||||
}
|
||||
|
||||
today := time.Now().Format("2006-01-02")
|
||||
logFilePath := filepath.Join(logsDir, fmt.Sprintf("server_%s.log", today))
|
||||
|
||||
file, err := os.OpenFile(logFilePath, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
|
||||
if err != nil {
|
||||
return fmt.Errorf("打开日志文件失败: %v", err)
|
||||
}
|
||||
|
||||
logFile = file
|
||||
|
||||
multiWriter := io.MultiWriter(os.Stdout, file)
|
||||
Logger = log.New(multiWriter, "", log.Ldate|log.Ltime|log.Lshortfile)
|
||||
|
||||
tcpDataFilePath := filepath.Join(logsDir, fmt.Sprintf("tcp_data_%s.log", today))
|
||||
tcpDataFile, err := os.OpenFile(tcpDataFilePath, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
|
||||
if err != nil {
|
||||
return fmt.Errorf("打开TCP数据日志文件失败: %v", err)
|
||||
}
|
||||
|
||||
tcpDataMultiWriter := io.MultiWriter(os.Stdout, tcpDataFile)
|
||||
TCPDataLogger = log.New(tcpDataMultiWriter, "TCP_DATA: ", log.Ldate|log.Ltime)
|
||||
|
||||
log.SetOutput(multiWriter)
|
||||
log.SetFlags(log.Ldate | log.Ltime | log.Lshortfile)
|
||||
|
||||
Logger.Println("日志系统初始化完成")
|
||||
return nil
|
||||
}
|
||||
|
||||
// 关闭日志文件
|
||||
func CloseLogger() {
|
||||
if logFile != nil {
|
||||
logFile.Close()
|
||||
}
|
||||
}
|
||||
|
||||
// 日志轮转,每天创建新的日志文件
|
||||
func StartLogRotation() {
|
||||
go func() {
|
||||
for {
|
||||
now := time.Now()
|
||||
next := now.Add(24 * time.Hour)
|
||||
next = time.Date(next.Year(), next.Month(), next.Day(), 0, 0, 0, 0, next.Location())
|
||||
duration := next.Sub(now)
|
||||
|
||||
time.Sleep(duration)
|
||||
|
||||
Logger.Println("开始日志轮转...")
|
||||
CloseLogger()
|
||||
if err := InitLogger(); err != nil {
|
||||
log.Printf("日志轮转失败: %v", err)
|
||||
}
|
||||
}
|
||||
}()
|
||||
}
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"time"
|
||||
)
|
||||
|
||||
var (
|
||||
logFile *os.File
|
||||
Logger *log.Logger // 导出Logger供其他包使用
|
||||
TCPDataLogger *log.Logger // 专门用于记录TCP数据的日志
|
||||
)
|
||||
|
||||
// 初始化日志系统
|
||||
func InitLogger() error {
|
||||
logsDir := "logs"
|
||||
if err := os.MkdirAll(logsDir, 0755); err != nil {
|
||||
return fmt.Errorf("创建日志目录失败: %v", err)
|
||||
}
|
||||
|
||||
today := time.Now().Format("2006-01-02")
|
||||
logFilePath := filepath.Join(logsDir, fmt.Sprintf("server_%s.log", today))
|
||||
|
||||
file, err := os.OpenFile(logFilePath, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
|
||||
if err != nil {
|
||||
return fmt.Errorf("打开日志文件失败: %v", err)
|
||||
}
|
||||
|
||||
logFile = file
|
||||
|
||||
multiWriter := io.MultiWriter(os.Stdout, file)
|
||||
Logger = log.New(multiWriter, "", log.Ldate|log.Ltime|log.Lshortfile)
|
||||
|
||||
tcpDataFilePath := filepath.Join(logsDir, fmt.Sprintf("tcp_data_%s.log", today))
|
||||
tcpDataFile, err := os.OpenFile(tcpDataFilePath, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
|
||||
if err != nil {
|
||||
return fmt.Errorf("打开TCP数据日志文件失败: %v", err)
|
||||
}
|
||||
|
||||
tcpDataMultiWriter := io.MultiWriter(os.Stdout, tcpDataFile)
|
||||
TCPDataLogger = log.New(tcpDataMultiWriter, "TCP_DATA: ", log.Ldate|log.Ltime)
|
||||
|
||||
log.SetOutput(multiWriter)
|
||||
log.SetFlags(log.Ldate | log.Ltime | log.Lshortfile)
|
||||
|
||||
Logger.Println("日志系统初始化完成")
|
||||
return nil
|
||||
}
|
||||
|
||||
// 关闭日志文件
|
||||
func CloseLogger() {
|
||||
if logFile != nil {
|
||||
logFile.Close()
|
||||
}
|
||||
}
|
||||
|
||||
// 日志轮转,每天创建新的日志文件
|
||||
func StartLogRotation() {
|
||||
go func() {
|
||||
for {
|
||||
now := time.Now()
|
||||
next := now.Add(24 * time.Hour)
|
||||
next = time.Date(next.Year(), next.Month(), next.Day(), 0, 0, 0, 0, next.Location())
|
||||
duration := next.Sub(now)
|
||||
|
||||
time.Sleep(duration)
|
||||
|
||||
Logger.Println("开始日志轮转...")
|
||||
CloseLogger()
|
||||
if err := InitLogger(); err != nil {
|
||||
log.Printf("日志轮转失败: %v", err)
|
||||
}
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
@ -1,99 +1,99 @@
|
||||
/* 基本样式和Flex布局 */
|
||||
body {
|
||||
font-family: Arial, sans-serif;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.header {
|
||||
padding: 10px;
|
||||
text-align: center;
|
||||
border-bottom: 1px solid #ddd;
|
||||
}
|
||||
|
||||
.container {
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
padding: 15px;
|
||||
}
|
||||
|
||||
.controls {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 10px;
|
||||
margin-bottom: 20px;
|
||||
padding: 10px;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 5px;
|
||||
}
|
||||
|
||||
.control-group {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 5px;
|
||||
}
|
||||
|
||||
select, input, button {
|
||||
padding: 5px;
|
||||
}
|
||||
|
||||
button {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.chart-container {
|
||||
margin-bottom: 20px;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 5px;
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
canvas {
|
||||
width: 100%;
|
||||
max-height: 400px;
|
||||
}
|
||||
|
||||
.table-container {
|
||||
overflow-x: auto;
|
||||
margin-bottom: 20px;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 5px;
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
}
|
||||
|
||||
th, td {
|
||||
border: 1px solid #ddd;
|
||||
padding: 8px;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
th {
|
||||
background-color: #f8f8f8;
|
||||
}
|
||||
|
||||
tr:nth-child(even) {
|
||||
background-color: #f9f9f9;
|
||||
}
|
||||
|
||||
.footer {
|
||||
text-align: center;
|
||||
padding: 10px;
|
||||
border-top: 1px solid #ddd;
|
||||
}
|
||||
|
||||
/* 响应式设计 */
|
||||
@media (max-width: 768px) {
|
||||
.controls {
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
}
|
||||
|
||||
.control-group {
|
||||
justify-content: space-between;
|
||||
}
|
||||
}
|
||||
/* 基本样式和Flex布局 */
|
||||
body {
|
||||
font-family: Arial, sans-serif;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.header {
|
||||
padding: 10px;
|
||||
text-align: center;
|
||||
border-bottom: 1px solid #ddd;
|
||||
}
|
||||
|
||||
.container {
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
padding: 15px;
|
||||
}
|
||||
|
||||
.controls {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 10px;
|
||||
margin-bottom: 20px;
|
||||
padding: 10px;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 5px;
|
||||
}
|
||||
|
||||
.control-group {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 5px;
|
||||
}
|
||||
|
||||
select, input, button {
|
||||
padding: 5px;
|
||||
}
|
||||
|
||||
button {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.chart-container {
|
||||
margin-bottom: 20px;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 5px;
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
canvas {
|
||||
width: 100%;
|
||||
max-height: 400px;
|
||||
}
|
||||
|
||||
.table-container {
|
||||
overflow-x: auto;
|
||||
margin-bottom: 20px;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 5px;
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
}
|
||||
|
||||
th, td {
|
||||
border: 1px solid #ddd;
|
||||
padding: 8px;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
th {
|
||||
background-color: #f8f8f8;
|
||||
}
|
||||
|
||||
tr:nth-child(even) {
|
||||
background-color: #f9f9f9;
|
||||
}
|
||||
|
||||
.footer {
|
||||
text-align: center;
|
||||
padding: 10px;
|
||||
border-top: 1px solid #ddd;
|
||||
}
|
||||
|
||||
/* 响应式设计 */
|
||||
@media (max-width: 768px) {
|
||||
.controls {
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
}
|
||||
|
||||
.control-group {
|
||||
justify-content: space-between;
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,431 +1,431 @@
|
||||
let sensorChart = null;
|
||||
let refreshInterval = null;
|
||||
let allSensors = [];
|
||||
let currentSensorData = [];
|
||||
|
||||
// 页面加载完成后执行
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
// 初始化日期选择器为今天
|
||||
initializeDatePickers();
|
||||
|
||||
// 加载所有传感器ID
|
||||
loadSensors();
|
||||
|
||||
// 添加事件监听器
|
||||
setupEventListeners();
|
||||
|
||||
// 设置自动刷新
|
||||
setupAutoRefresh();
|
||||
});
|
||||
|
||||
// 初始化日期选择器
|
||||
function initializeDatePickers() {
|
||||
const now = new Date();
|
||||
const today = now.toISOString().split('T')[0];
|
||||
const time = now.toTimeString().split(' ')[0].substring(0, 5);
|
||||
|
||||
// 设置默认的开始时间为当天00:00
|
||||
document.getElementById('startDate').value = `${today}T00:00`;
|
||||
|
||||
// 设置默认的结束时间为当前时间
|
||||
document.getElementById('endDate').value = `${today}T${time}`;
|
||||
}
|
||||
|
||||
// 设置事件监听器
|
||||
function setupEventListeners() {
|
||||
// 查询按钮
|
||||
document.getElementById('queryBtn').addEventListener('click', function() {
|
||||
loadData();
|
||||
});
|
||||
|
||||
// 重置按钮
|
||||
document.getElementById('resetBtn').addEventListener('click', function() {
|
||||
resetFilters();
|
||||
});
|
||||
|
||||
// 传感器选择变化
|
||||
document.getElementById('sensorSelect').addEventListener('change', function() {
|
||||
loadData();
|
||||
});
|
||||
|
||||
// 记录数限制变化
|
||||
document.getElementById('limitSelect').addEventListener('change', function() {
|
||||
loadData();
|
||||
});
|
||||
|
||||
// 导出CSV按钮
|
||||
document.getElementById('exportBtn').addEventListener('click', function() {
|
||||
exportToCSV();
|
||||
});
|
||||
}
|
||||
|
||||
// 设置自动刷新
|
||||
function setupAutoRefresh() {
|
||||
const autoRefreshCheckbox = document.getElementById('autoRefresh');
|
||||
|
||||
// 初始化自动刷新
|
||||
if (autoRefreshCheckbox.checked) {
|
||||
startAutoRefresh();
|
||||
}
|
||||
|
||||
// 监听复选框变化
|
||||
autoRefreshCheckbox.addEventListener('change', function() {
|
||||
if (this.checked) {
|
||||
startAutoRefresh();
|
||||
} else {
|
||||
stopAutoRefresh();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// 开始自动刷新
|
||||
function startAutoRefresh() {
|
||||
if (refreshInterval) {
|
||||
clearInterval(refreshInterval);
|
||||
}
|
||||
|
||||
refreshInterval = setInterval(loadData, 10000); // 10秒刷新一次
|
||||
}
|
||||
|
||||
// 停止自动刷新
|
||||
function stopAutoRefresh() {
|
||||
if (refreshInterval) {
|
||||
clearInterval(refreshInterval);
|
||||
refreshInterval = null;
|
||||
}
|
||||
}
|
||||
|
||||
// 重置筛选条件
|
||||
function resetFilters() {
|
||||
initializeDatePickers();
|
||||
document.getElementById('sensorSelect').value = 'all';
|
||||
document.getElementById('limitSelect').value = '100';
|
||||
loadData();
|
||||
}
|
||||
|
||||
// 加载所有传感器ID
|
||||
function loadSensors() {
|
||||
fetch('/api/sensors')
|
||||
.then(response => {
|
||||
if (!response.ok) {
|
||||
throw new Error('获取传感器列表失败');
|
||||
}
|
||||
return response.json();
|
||||
})
|
||||
.then(data => {
|
||||
allSensors = data;
|
||||
updateSensorSelect(data);
|
||||
|
||||
// 加载数据
|
||||
loadData();
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('加载传感器列表出错:', error);
|
||||
alert('加载传感器列表出错: ' + error.message);
|
||||
});
|
||||
}
|
||||
|
||||
// 更新传感器选择下拉框
|
||||
function updateSensorSelect(sensors) {
|
||||
const select = document.getElementById('sensorSelect');
|
||||
|
||||
// 保留"所有传感器"选项
|
||||
const allOption = select.querySelector('option[value="all"]');
|
||||
select.innerHTML = '';
|
||||
select.appendChild(allOption);
|
||||
|
||||
if (sensors.length === 0) {
|
||||
const option = document.createElement('option');
|
||||
option.value = '';
|
||||
option.textContent = '没有可用的传感器';
|
||||
select.appendChild(option);
|
||||
return;
|
||||
}
|
||||
|
||||
sensors.forEach(id => {
|
||||
const option = document.createElement('option');
|
||||
option.value = id;
|
||||
option.textContent = `传感器 ${id}`;
|
||||
select.appendChild(option);
|
||||
});
|
||||
}
|
||||
|
||||
// 加载传感器数据
|
||||
function loadData() {
|
||||
const sensorID = document.getElementById('sensorSelect').value;
|
||||
const limit = document.getElementById('limitSelect').value;
|
||||
const startDate = document.getElementById('startDate').value;
|
||||
const endDate = document.getElementById('endDate').value;
|
||||
|
||||
let url = '/api/data?';
|
||||
let params = [];
|
||||
|
||||
// 添加查询参数
|
||||
if (sensorID !== 'all') {
|
||||
params.push(`sensor_id=${sensorID}`);
|
||||
}
|
||||
|
||||
if (limit) {
|
||||
params.push(`limit=${limit}`);
|
||||
}
|
||||
|
||||
if (startDate) {
|
||||
params.push(`start_date=${encodeURIComponent(startDate)}`);
|
||||
}
|
||||
|
||||
if (endDate) {
|
||||
params.push(`end_date=${encodeURIComponent(endDate)}`);
|
||||
}
|
||||
|
||||
url += params.join('&');
|
||||
|
||||
// 显示加载状态
|
||||
document.getElementById('queryBtn').textContent = '加载中...';
|
||||
|
||||
fetch(url)
|
||||
.then(response => {
|
||||
if (!response.ok) {
|
||||
throw new Error('获取传感器数据失败');
|
||||
}
|
||||
return response.json();
|
||||
})
|
||||
.then(data => {
|
||||
currentSensorData = data;
|
||||
updateTable(data);
|
||||
updateChart(data);
|
||||
document.getElementById('queryBtn').textContent = '查询数据';
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('加载数据出错:', error);
|
||||
alert('加载数据出错: ' + error.message);
|
||||
document.getElementById('queryBtn').textContent = '查询数据';
|
||||
});
|
||||
}
|
||||
|
||||
// 更新数据表格
|
||||
function updateTable(data) {
|
||||
const tableBody = document.getElementById('tableBody');
|
||||
tableBody.innerHTML = '';
|
||||
|
||||
if (data.length === 0) {
|
||||
const row = document.createElement('tr');
|
||||
row.innerHTML = '<td colspan="6" style="text-align: center;">没有数据</td>';
|
||||
tableBody.appendChild(row);
|
||||
return;
|
||||
}
|
||||
|
||||
data.forEach(item => {
|
||||
const row = document.createElement('tr');
|
||||
|
||||
// 解析时间并调整为中国时间(UTC+8)
|
||||
const date = new Date(item.timestamp);
|
||||
// 减去8小时,因为数据库时间似乎比实际时间早了8小时
|
||||
date.setHours(date.getHours() - 8);
|
||||
|
||||
// 格式化为中文日期时间格式
|
||||
const formattedDate =
|
||||
date.getFullYear() + '/' +
|
||||
(date.getMonth() + 1).toString().padStart(2, '0') + '/' +
|
||||
date.getDate().toString().padStart(2, '0') + ' ' +
|
||||
date.getHours().toString().padStart(2, '0') + ':' +
|
||||
date.getMinutes().toString().padStart(2, '0') + ':' +
|
||||
date.getSeconds().toString().padStart(2, '0');
|
||||
|
||||
row.innerHTML =
|
||||
'<td>' + item.id + '</td>' +
|
||||
'<td>' + item.sensor_id + '</td>' +
|
||||
'<td>' + item.x.toFixed(3) + '</td>' +
|
||||
'<td>' + item.y.toFixed(3) + '</td>' +
|
||||
'<td>' + item.z.toFixed(3) + '</td>' +
|
||||
'<td>' + formattedDate + '</td>';
|
||||
|
||||
tableBody.appendChild(row);
|
||||
});
|
||||
}
|
||||
|
||||
// 更新图表
|
||||
function updateChart(data) {
|
||||
// 准备图表数据
|
||||
const chartData = prepareChartData(data);
|
||||
|
||||
// 如果图表已经存在,销毁它
|
||||
if (sensorChart) {
|
||||
sensorChart.destroy();
|
||||
}
|
||||
|
||||
// 获取图表Canvas
|
||||
const ctx = document.getElementById('sensorChart').getContext('2d');
|
||||
|
||||
// 创建新图表
|
||||
sensorChart = new Chart(ctx, {
|
||||
type: 'line',
|
||||
data: chartData,
|
||||
options: {
|
||||
responsive: true,
|
||||
maintainAspectRatio: false,
|
||||
plugins: {
|
||||
title: {
|
||||
display: true,
|
||||
text: '传感器数据趋势'
|
||||
},
|
||||
tooltip: {
|
||||
callbacks: {
|
||||
label: function(context) {
|
||||
let label = context.dataset.label || '';
|
||||
if (label) {
|
||||
label += ': ';
|
||||
}
|
||||
if (context.parsed.y !== null) {
|
||||
label += context.parsed.y.toFixed(3);
|
||||
}
|
||||
return label;
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
scales: {
|
||||
x: {
|
||||
title: {
|
||||
display: true,
|
||||
text: '时间'
|
||||
}
|
||||
},
|
||||
y: {
|
||||
title: {
|
||||
display: true,
|
||||
text: '值'
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// 准备图表数据
|
||||
function prepareChartData(data) {
|
||||
// 如果没有数据,返回空数据集
|
||||
if (data.length === 0) {
|
||||
return {
|
||||
labels: [],
|
||||
datasets: []
|
||||
};
|
||||
}
|
||||
|
||||
// 反转数据以便按时间先后顺序显示
|
||||
const sortedData = [...data].sort((a, b) => {
|
||||
return new Date(a.timestamp) - new Date(b.timestamp);
|
||||
});
|
||||
|
||||
// 获取所有传感器ID
|
||||
let sensorIDs = [...new Set(sortedData.map(item => item.sensor_id))];
|
||||
|
||||
// 按传感器ID分组数据
|
||||
let datasets = [];
|
||||
let labels = [];
|
||||
|
||||
// 准备时间标签(使用第一个传感器的数据)
|
||||
if (sensorIDs.length > 0) {
|
||||
const firstSensorData = sortedData.filter(item => item.sensor_id === sensorIDs[0]);
|
||||
labels = firstSensorData.map(item => {
|
||||
const date = new Date(item.timestamp);
|
||||
date.setHours(date.getHours() - 8); // 调整时区
|
||||
return date.toLocaleString('zh-CN', {
|
||||
month: '2-digit',
|
||||
day: '2-digit',
|
||||
hour: '2-digit',
|
||||
minute: '2-digit'
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// 定义颜色
|
||||
const colors = [
|
||||
'rgb(75, 192, 192)',
|
||||
'rgb(255, 99, 132)',
|
||||
'rgb(54, 162, 235)',
|
||||
'rgb(255, 205, 86)',
|
||||
'rgb(153, 102, 255)',
|
||||
'rgb(255, 159, 64)'
|
||||
];
|
||||
|
||||
// 为X, Y, Z创建不同的数据集
|
||||
const dataTypes = [
|
||||
{ key: 'x', label: 'X值' },
|
||||
{ key: 'y', label: 'Y值' },
|
||||
{ key: 'z', label: 'Z值' }
|
||||
];
|
||||
|
||||
sensorIDs.forEach((sensorID, sensorIndex) => {
|
||||
const sensorData = sortedData.filter(item => item.sensor_id === sensorID);
|
||||
|
||||
dataTypes.forEach((type, typeIndex) => {
|
||||
const colorIndex = (sensorIndex * dataTypes.length + typeIndex) % colors.length;
|
||||
|
||||
datasets.push({
|
||||
label: `传感器${sensorID} - ${type.label}`,
|
||||
data: sensorData.map(item => item[type.key]),
|
||||
fill: false,
|
||||
borderColor: colors[colorIndex],
|
||||
tension: 0.1
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
return {
|
||||
labels: labels,
|
||||
datasets: datasets
|
||||
};
|
||||
}
|
||||
|
||||
// 导出到CSV文件
|
||||
function exportToCSV() {
|
||||
if (currentSensorData.length === 0) {
|
||||
alert('没有数据可导出');
|
||||
return;
|
||||
}
|
||||
|
||||
// 准备CSV内容
|
||||
let csvContent = "ID,传感器ID,X值,Y值,Z值,时间戳\n";
|
||||
|
||||
currentSensorData.forEach(item => {
|
||||
// 解析时间并调整为中国时间
|
||||
const date = new Date(item.timestamp);
|
||||
date.setHours(date.getHours() - 8);
|
||||
|
||||
// 格式化日期
|
||||
const formattedDate =
|
||||
date.getFullYear() + '/' +
|
||||
(date.getMonth() + 1).toString().padStart(2, '0') + '/' +
|
||||
date.getDate().toString().padStart(2, '0') + ' ' +
|
||||
date.getHours().toString().padStart(2, '0') + ':' +
|
||||
date.getMinutes().toString().padStart(2, '0') + ':' +
|
||||
date.getSeconds().toString().padStart(2, '0');
|
||||
|
||||
// 添加一行数据
|
||||
csvContent +=
|
||||
item.id + "," +
|
||||
item.sensor_id + "," +
|
||||
item.x.toFixed(3) + "," +
|
||||
item.y.toFixed(3) + "," +
|
||||
item.z.toFixed(3) + "," +
|
||||
formattedDate + "\n";
|
||||
});
|
||||
|
||||
// 创建Blob对象
|
||||
const blob = new Blob([csvContent], { type: 'text/csv;charset=utf-8;' });
|
||||
|
||||
// 创建下载链接
|
||||
const link = document.createElement("a");
|
||||
const url = URL.createObjectURL(blob);
|
||||
|
||||
// 设置下载属性
|
||||
link.setAttribute("href", url);
|
||||
link.setAttribute("download", "sensor_data.csv");
|
||||
|
||||
// 添加到文档并点击
|
||||
document.body.appendChild(link);
|
||||
link.click();
|
||||
|
||||
// 清理
|
||||
document.body.removeChild(link);
|
||||
}
|
||||
let sensorChart = null;
|
||||
let refreshInterval = null;
|
||||
let allSensors = [];
|
||||
let currentSensorData = [];
|
||||
|
||||
// 页面加载完成后执行
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
// 初始化日期选择器为今天
|
||||
initializeDatePickers();
|
||||
|
||||
// 加载所有传感器ID
|
||||
loadSensors();
|
||||
|
||||
// 添加事件监听器
|
||||
setupEventListeners();
|
||||
|
||||
// 设置自动刷新
|
||||
setupAutoRefresh();
|
||||
});
|
||||
|
||||
// 初始化日期选择器
|
||||
function initializeDatePickers() {
|
||||
const now = new Date();
|
||||
const today = now.toISOString().split('T')[0];
|
||||
const time = now.toTimeString().split(' ')[0].substring(0, 5);
|
||||
|
||||
// 设置默认的开始时间为当天00:00
|
||||
document.getElementById('startDate').value = `${today}T00:00`;
|
||||
|
||||
// 设置默认的结束时间为当前时间
|
||||
document.getElementById('endDate').value = `${today}T${time}`;
|
||||
}
|
||||
|
||||
// 设置事件监听器
|
||||
function setupEventListeners() {
|
||||
// 查询按钮
|
||||
document.getElementById('queryBtn').addEventListener('click', function() {
|
||||
loadData();
|
||||
});
|
||||
|
||||
// 重置按钮
|
||||
document.getElementById('resetBtn').addEventListener('click', function() {
|
||||
resetFilters();
|
||||
});
|
||||
|
||||
// 传感器选择变化
|
||||
document.getElementById('sensorSelect').addEventListener('change', function() {
|
||||
loadData();
|
||||
});
|
||||
|
||||
// 记录数限制变化
|
||||
document.getElementById('limitSelect').addEventListener('change', function() {
|
||||
loadData();
|
||||
});
|
||||
|
||||
// 导出CSV按钮
|
||||
document.getElementById('exportBtn').addEventListener('click', function() {
|
||||
exportToCSV();
|
||||
});
|
||||
}
|
||||
|
||||
// 设置自动刷新
|
||||
function setupAutoRefresh() {
|
||||
const autoRefreshCheckbox = document.getElementById('autoRefresh');
|
||||
|
||||
// 初始化自动刷新
|
||||
if (autoRefreshCheckbox.checked) {
|
||||
startAutoRefresh();
|
||||
}
|
||||
|
||||
// 监听复选框变化
|
||||
autoRefreshCheckbox.addEventListener('change', function() {
|
||||
if (this.checked) {
|
||||
startAutoRefresh();
|
||||
} else {
|
||||
stopAutoRefresh();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// 开始自动刷新
|
||||
function startAutoRefresh() {
|
||||
if (refreshInterval) {
|
||||
clearInterval(refreshInterval);
|
||||
}
|
||||
|
||||
refreshInterval = setInterval(loadData, 10000); // 10秒刷新一次
|
||||
}
|
||||
|
||||
// 停止自动刷新
|
||||
function stopAutoRefresh() {
|
||||
if (refreshInterval) {
|
||||
clearInterval(refreshInterval);
|
||||
refreshInterval = null;
|
||||
}
|
||||
}
|
||||
|
||||
// 重置筛选条件
|
||||
function resetFilters() {
|
||||
initializeDatePickers();
|
||||
document.getElementById('sensorSelect').value = 'all';
|
||||
document.getElementById('limitSelect').value = '100';
|
||||
loadData();
|
||||
}
|
||||
|
||||
// 加载所有传感器ID
|
||||
function loadSensors() {
|
||||
fetch('/api/sensors')
|
||||
.then(response => {
|
||||
if (!response.ok) {
|
||||
throw new Error('获取传感器列表失败');
|
||||
}
|
||||
return response.json();
|
||||
})
|
||||
.then(data => {
|
||||
allSensors = data;
|
||||
updateSensorSelect(data);
|
||||
|
||||
// 加载数据
|
||||
loadData();
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('加载传感器列表出错:', error);
|
||||
alert('加载传感器列表出错: ' + error.message);
|
||||
});
|
||||
}
|
||||
|
||||
// 更新传感器选择下拉框
|
||||
function updateSensorSelect(sensors) {
|
||||
const select = document.getElementById('sensorSelect');
|
||||
|
||||
// 保留"所有传感器"选项
|
||||
const allOption = select.querySelector('option[value="all"]');
|
||||
select.innerHTML = '';
|
||||
select.appendChild(allOption);
|
||||
|
||||
if (sensors.length === 0) {
|
||||
const option = document.createElement('option');
|
||||
option.value = '';
|
||||
option.textContent = '没有可用的传感器';
|
||||
select.appendChild(option);
|
||||
return;
|
||||
}
|
||||
|
||||
sensors.forEach(id => {
|
||||
const option = document.createElement('option');
|
||||
option.value = id;
|
||||
option.textContent = `传感器 ${id}`;
|
||||
select.appendChild(option);
|
||||
});
|
||||
}
|
||||
|
||||
// 加载传感器数据
|
||||
function loadData() {
|
||||
const sensorID = document.getElementById('sensorSelect').value;
|
||||
const limit = document.getElementById('limitSelect').value;
|
||||
const startDate = document.getElementById('startDate').value;
|
||||
const endDate = document.getElementById('endDate').value;
|
||||
|
||||
let url = '/api/data?';
|
||||
let params = [];
|
||||
|
||||
// 添加查询参数
|
||||
if (sensorID !== 'all') {
|
||||
params.push(`sensor_id=${sensorID}`);
|
||||
}
|
||||
|
||||
if (limit) {
|
||||
params.push(`limit=${limit}`);
|
||||
}
|
||||
|
||||
if (startDate) {
|
||||
params.push(`start_date=${encodeURIComponent(startDate)}`);
|
||||
}
|
||||
|
||||
if (endDate) {
|
||||
params.push(`end_date=${encodeURIComponent(endDate)}`);
|
||||
}
|
||||
|
||||
url += params.join('&');
|
||||
|
||||
// 显示加载状态
|
||||
document.getElementById('queryBtn').textContent = '加载中...';
|
||||
|
||||
fetch(url)
|
||||
.then(response => {
|
||||
if (!response.ok) {
|
||||
throw new Error('获取传感器数据失败');
|
||||
}
|
||||
return response.json();
|
||||
})
|
||||
.then(data => {
|
||||
currentSensorData = data;
|
||||
updateTable(data);
|
||||
updateChart(data);
|
||||
document.getElementById('queryBtn').textContent = '查询数据';
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('加载数据出错:', error);
|
||||
alert('加载数据出错: ' + error.message);
|
||||
document.getElementById('queryBtn').textContent = '查询数据';
|
||||
});
|
||||
}
|
||||
|
||||
// 更新数据表格
|
||||
function updateTable(data) {
|
||||
const tableBody = document.getElementById('tableBody');
|
||||
tableBody.innerHTML = '';
|
||||
|
||||
if (data.length === 0) {
|
||||
const row = document.createElement('tr');
|
||||
row.innerHTML = '<td colspan="6" style="text-align: center;">没有数据</td>';
|
||||
tableBody.appendChild(row);
|
||||
return;
|
||||
}
|
||||
|
||||
data.forEach(item => {
|
||||
const row = document.createElement('tr');
|
||||
|
||||
// 解析时间并调整为中国时间(UTC+8)
|
||||
const date = new Date(item.timestamp);
|
||||
// 减去8小时,因为数据库时间似乎比实际时间早了8小时
|
||||
date.setHours(date.getHours() - 8);
|
||||
|
||||
// 格式化为中文日期时间格式
|
||||
const formattedDate =
|
||||
date.getFullYear() + '/' +
|
||||
(date.getMonth() + 1).toString().padStart(2, '0') + '/' +
|
||||
date.getDate().toString().padStart(2, '0') + ' ' +
|
||||
date.getHours().toString().padStart(2, '0') + ':' +
|
||||
date.getMinutes().toString().padStart(2, '0') + ':' +
|
||||
date.getSeconds().toString().padStart(2, '0');
|
||||
|
||||
row.innerHTML =
|
||||
'<td>' + item.id + '</td>' +
|
||||
'<td>' + item.sensor_id + '</td>' +
|
||||
'<td>' + item.x.toFixed(3) + '</td>' +
|
||||
'<td>' + item.y.toFixed(3) + '</td>' +
|
||||
'<td>' + item.z.toFixed(3) + '</td>' +
|
||||
'<td>' + formattedDate + '</td>';
|
||||
|
||||
tableBody.appendChild(row);
|
||||
});
|
||||
}
|
||||
|
||||
// 更新图表
|
||||
function updateChart(data) {
|
||||
// 准备图表数据
|
||||
const chartData = prepareChartData(data);
|
||||
|
||||
// 如果图表已经存在,销毁它
|
||||
if (sensorChart) {
|
||||
sensorChart.destroy();
|
||||
}
|
||||
|
||||
// 获取图表Canvas
|
||||
const ctx = document.getElementById('sensorChart').getContext('2d');
|
||||
|
||||
// 创建新图表
|
||||
sensorChart = new Chart(ctx, {
|
||||
type: 'line',
|
||||
data: chartData,
|
||||
options: {
|
||||
responsive: true,
|
||||
maintainAspectRatio: false,
|
||||
plugins: {
|
||||
title: {
|
||||
display: true,
|
||||
text: '传感器数据趋势'
|
||||
},
|
||||
tooltip: {
|
||||
callbacks: {
|
||||
label: function(context) {
|
||||
let label = context.dataset.label || '';
|
||||
if (label) {
|
||||
label += ': ';
|
||||
}
|
||||
if (context.parsed.y !== null) {
|
||||
label += context.parsed.y.toFixed(3);
|
||||
}
|
||||
return label;
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
scales: {
|
||||
x: {
|
||||
title: {
|
||||
display: true,
|
||||
text: '时间'
|
||||
}
|
||||
},
|
||||
y: {
|
||||
title: {
|
||||
display: true,
|
||||
text: '值'
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// 准备图表数据
|
||||
function prepareChartData(data) {
|
||||
// 如果没有数据,返回空数据集
|
||||
if (data.length === 0) {
|
||||
return {
|
||||
labels: [],
|
||||
datasets: []
|
||||
};
|
||||
}
|
||||
|
||||
// 反转数据以便按时间先后顺序显示
|
||||
const sortedData = [...data].sort((a, b) => {
|
||||
return new Date(a.timestamp) - new Date(b.timestamp);
|
||||
});
|
||||
|
||||
// 获取所有传感器ID
|
||||
let sensorIDs = [...new Set(sortedData.map(item => item.sensor_id))];
|
||||
|
||||
// 按传感器ID分组数据
|
||||
let datasets = [];
|
||||
let labels = [];
|
||||
|
||||
// 准备时间标签(使用第一个传感器的数据)
|
||||
if (sensorIDs.length > 0) {
|
||||
const firstSensorData = sortedData.filter(item => item.sensor_id === sensorIDs[0]);
|
||||
labels = firstSensorData.map(item => {
|
||||
const date = new Date(item.timestamp);
|
||||
date.setHours(date.getHours() - 8); // 调整时区
|
||||
return date.toLocaleString('zh-CN', {
|
||||
month: '2-digit',
|
||||
day: '2-digit',
|
||||
hour: '2-digit',
|
||||
minute: '2-digit'
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// 定义颜色
|
||||
const colors = [
|
||||
'rgb(75, 192, 192)',
|
||||
'rgb(255, 99, 132)',
|
||||
'rgb(54, 162, 235)',
|
||||
'rgb(255, 205, 86)',
|
||||
'rgb(153, 102, 255)',
|
||||
'rgb(255, 159, 64)'
|
||||
];
|
||||
|
||||
// 为X, Y, Z创建不同的数据集
|
||||
const dataTypes = [
|
||||
{ key: 'x', label: 'X值' },
|
||||
{ key: 'y', label: 'Y值' },
|
||||
{ key: 'z', label: 'Z值' }
|
||||
];
|
||||
|
||||
sensorIDs.forEach((sensorID, sensorIndex) => {
|
||||
const sensorData = sortedData.filter(item => item.sensor_id === sensorID);
|
||||
|
||||
dataTypes.forEach((type, typeIndex) => {
|
||||
const colorIndex = (sensorIndex * dataTypes.length + typeIndex) % colors.length;
|
||||
|
||||
datasets.push({
|
||||
label: `传感器${sensorID} - ${type.label}`,
|
||||
data: sensorData.map(item => item[type.key]),
|
||||
fill: false,
|
||||
borderColor: colors[colorIndex],
|
||||
tension: 0.1
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
return {
|
||||
labels: labels,
|
||||
datasets: datasets
|
||||
};
|
||||
}
|
||||
|
||||
// 导出到CSV文件
|
||||
function exportToCSV() {
|
||||
if (currentSensorData.length === 0) {
|
||||
alert('没有数据可导出');
|
||||
return;
|
||||
}
|
||||
|
||||
// 准备CSV内容
|
||||
let csvContent = "ID,传感器ID,X值,Y值,Z值,时间戳\n";
|
||||
|
||||
currentSensorData.forEach(item => {
|
||||
// 解析时间并调整为中国时间
|
||||
const date = new Date(item.timestamp);
|
||||
date.setHours(date.getHours() - 8);
|
||||
|
||||
// 格式化日期
|
||||
const formattedDate =
|
||||
date.getFullYear() + '/' +
|
||||
(date.getMonth() + 1).toString().padStart(2, '0') + '/' +
|
||||
date.getDate().toString().padStart(2, '0') + ' ' +
|
||||
date.getHours().toString().padStart(2, '0') + ':' +
|
||||
date.getMinutes().toString().padStart(2, '0') + ':' +
|
||||
date.getSeconds().toString().padStart(2, '0');
|
||||
|
||||
// 添加一行数据
|
||||
csvContent +=
|
||||
item.id + "," +
|
||||
item.sensor_id + "," +
|
||||
item.x.toFixed(3) + "," +
|
||||
item.y.toFixed(3) + "," +
|
||||
item.z.toFixed(3) + "," +
|
||||
formattedDate + "\n";
|
||||
});
|
||||
|
||||
// 创建Blob对象
|
||||
const blob = new Blob([csvContent], { type: 'text/csv;charset=utf-8;' });
|
||||
|
||||
// 创建下载链接
|
||||
const link = document.createElement("a");
|
||||
const url = URL.createObjectURL(blob);
|
||||
|
||||
// 设置下载属性
|
||||
link.setAttribute("href", url);
|
||||
link.setAttribute("download", "sensor_data.csv");
|
||||
|
||||
// 添加到文档并点击
|
||||
document.body.appendChild(link);
|
||||
link.click();
|
||||
|
||||
// 清理
|
||||
document.body.removeChild(link);
|
||||
}
|
||||
|
||||
472
tcp_server.go
472
tcp_server.go
@ -1,236 +1,236 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"net"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
// 客户端信息结构
|
||||
type ClientInfo struct {
|
||||
IP string // IP地址
|
||||
Port string // 端口
|
||||
LastSeen time.Time // 最后活跃时间
|
||||
}
|
||||
|
||||
// 客户端列表(使用互斥锁保护的映射)
|
||||
var (
|
||||
clientsMutex sync.Mutex
|
||||
clients = make(map[string]*ClientInfo)
|
||||
)
|
||||
|
||||
// StartTCPServer 启动TCP服务器
|
||||
func StartTCPServer(address string) error {
|
||||
listener, err := net.Listen("tcp", address)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
startClientCleanup()
|
||||
|
||||
Logger.Printf("TCP服务器已启动,正在监听 %s\n", address)
|
||||
|
||||
for {
|
||||
conn, err := listener.Accept()
|
||||
if err != nil {
|
||||
Logger.Printf("接受连接失败: %v", err)
|
||||
continue
|
||||
}
|
||||
|
||||
go handleConnection(conn)
|
||||
}
|
||||
}
|
||||
|
||||
// handleConnection 处理客户端连接
|
||||
func handleConnection(conn net.Conn) {
|
||||
defer conn.Close()
|
||||
|
||||
remoteAddr := conn.RemoteAddr().String()
|
||||
Logger.Printf("新的客户端连接: %s", remoteAddr)
|
||||
|
||||
addClient(remoteAddr)
|
||||
|
||||
buffer := make([]byte, 1024)
|
||||
|
||||
for {
|
||||
n, err := conn.Read(buffer)
|
||||
if err != nil {
|
||||
if err != io.EOF {
|
||||
Logger.Printf("从客户端读取失败 %s: %v", remoteAddr, err)
|
||||
} else {
|
||||
Logger.Printf("客户端断开连接 %s", remoteAddr)
|
||||
}
|
||||
removeClient(remoteAddr)
|
||||
break
|
||||
}
|
||||
|
||||
rawData := string(buffer[:n])
|
||||
TCPDataLogger.Printf("从客户端 %s 接收到原始数据: %s", remoteAddr, rawData)
|
||||
|
||||
sensorID, x, y, z, err := parseData(rawData)
|
||||
|
||||
if err == nil {
|
||||
TCPDataLogger.Printf("解析成功 - 客户端: %s, 传感器ID: %d, 值: X=%.3f, Y=%.3f, Z=%.3f",
|
||||
remoteAddr, sensorID, x, y, z)
|
||||
|
||||
if err := SaveSensorData(sensorID, x, y, z); err != nil {
|
||||
Logger.Printf("保存传感器数据失败: %v", err)
|
||||
}
|
||||
} else {
|
||||
TCPDataLogger.Printf("无法解析从客户端 %s 接收到的数据: %s, 错误: %v", remoteAddr, rawData, err)
|
||||
}
|
||||
|
||||
resp := "OK\n"
|
||||
if _, err := conn.Write([]byte(resp)); err != nil {
|
||||
Logger.Printf("发送响应到客户端 %s 失败: %v", remoteAddr, err)
|
||||
removeClient(remoteAddr)
|
||||
break
|
||||
}
|
||||
|
||||
updateClientLastSeen(remoteAddr)
|
||||
}
|
||||
}
|
||||
|
||||
// parseData 使用正则表达式解析传感器数据
|
||||
func parseData(data string) (int, float64, float64, float64, error) {
|
||||
pattern := regexp.MustCompile(`(\d+):([-]?\d+\.\d+),\s*([-]?\d+\.\d+),\s*([-]?\d+\.\d+)`)
|
||||
matches := pattern.FindStringSubmatch(data)
|
||||
|
||||
if len(matches) != 5 {
|
||||
return 0, 0, 0, 0, fmt.Errorf("数据格式不正确: %s", data)
|
||||
}
|
||||
|
||||
sensorID, err := strconv.Atoi(matches[1])
|
||||
if err != nil {
|
||||
return 0, 0, 0, 0, fmt.Errorf("解析传感器ID失败: %v", err)
|
||||
}
|
||||
|
||||
x, err := strconv.ParseFloat(strings.TrimSpace(matches[2]), 64)
|
||||
if err != nil {
|
||||
return 0, 0, 0, 0, fmt.Errorf("解析X值失败: %v", err)
|
||||
}
|
||||
|
||||
y, err := strconv.ParseFloat(strings.TrimSpace(matches[3]), 64)
|
||||
if err != nil {
|
||||
return 0, 0, 0, 0, fmt.Errorf("解析Y值失败: %v", err)
|
||||
}
|
||||
|
||||
z, err := strconv.ParseFloat(strings.TrimSpace(matches[4]), 64)
|
||||
if err != nil {
|
||||
return 0, 0, 0, 0, fmt.Errorf("解析Z值失败: %v", err)
|
||||
}
|
||||
|
||||
return sensorID, x, y, z, nil
|
||||
}
|
||||
|
||||
// addClient 添加客户端
|
||||
func addClient(addr string) {
|
||||
clientsMutex.Lock()
|
||||
defer clientsMutex.Unlock()
|
||||
|
||||
host, port, err := net.SplitHostPort(addr)
|
||||
if err != nil {
|
||||
Logger.Printf("解析客户端地址失败 %s: %v", addr, err)
|
||||
host = addr
|
||||
port = "unknown"
|
||||
}
|
||||
|
||||
clients[addr] = &ClientInfo{
|
||||
IP: host,
|
||||
Port: port,
|
||||
LastSeen: time.Now(),
|
||||
}
|
||||
|
||||
Logger.Printf("添加新客户端: %s", addr)
|
||||
}
|
||||
|
||||
// updateClientLastSeen 更新客户端最后活跃时间
|
||||
func updateClientLastSeen(addr string) {
|
||||
clientsMutex.Lock()
|
||||
defer clientsMutex.Unlock()
|
||||
|
||||
if client, exists := clients[addr]; exists {
|
||||
client.LastSeen = time.Now()
|
||||
}
|
||||
}
|
||||
|
||||
// removeClient 移除客户端
|
||||
func removeClient(addr string) {
|
||||
clientsMutex.Lock()
|
||||
defer clientsMutex.Unlock()
|
||||
|
||||
if client, exists := clients[addr]; exists {
|
||||
client.LastSeen = time.Now()
|
||||
Logger.Printf("客户端标记为断开连接: %s", addr)
|
||||
}
|
||||
}
|
||||
|
||||
// getAllClients 获取所有客户端信息
|
||||
func getAllClients() []map[string]interface{} {
|
||||
clientsMutex.Lock()
|
||||
defer clientsMutex.Unlock()
|
||||
|
||||
now := time.Now()
|
||||
result := make([]map[string]interface{}, 0, len(clients))
|
||||
|
||||
for addr, client := range clients {
|
||||
lastSeenDuration := now.Sub(client.LastSeen)
|
||||
|
||||
if lastSeenDuration > 24*time.Hour {
|
||||
delete(clients, addr)
|
||||
continue
|
||||
}
|
||||
|
||||
isOnline := lastSeenDuration < 10*time.Minute
|
||||
|
||||
result = append(result, map[string]interface{}{
|
||||
"address": addr,
|
||||
"ip": client.IP,
|
||||
"port": client.Port,
|
||||
"lastSeen": client.LastSeen,
|
||||
"isOnline": isOnline,
|
||||
"lastSeenFormatted": formatDuration(lastSeenDuration),
|
||||
})
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
// formatDuration 格式化持续时间为友好的字符串
|
||||
func formatDuration(d time.Duration) string {
|
||||
if d < time.Minute {
|
||||
return "刚刚"
|
||||
} else if d < time.Hour {
|
||||
return fmt.Sprintf("%d分钟前", int(d.Minutes()))
|
||||
} else if d < 24*time.Hour {
|
||||
return fmt.Sprintf("%d小时前", int(d.Hours()))
|
||||
} else {
|
||||
return fmt.Sprintf("%d天前", int(d.Hours()/24))
|
||||
}
|
||||
}
|
||||
|
||||
// startClientCleanup 启动清理过期客户端的goroutine
|
||||
func startClientCleanup() {
|
||||
go func() {
|
||||
for {
|
||||
time.Sleep(1 * time.Hour) // 每小时检查一次
|
||||
|
||||
clientsMutex.Lock()
|
||||
now := time.Now()
|
||||
|
||||
for addr, client := range clients {
|
||||
if now.Sub(client.LastSeen) > 24*time.Hour {
|
||||
delete(clients, addr)
|
||||
Logger.Printf("移除过期客户端: %s", addr)
|
||||
}
|
||||
}
|
||||
|
||||
clientsMutex.Unlock()
|
||||
}
|
||||
}()
|
||||
}
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"net"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
// 客户端信息结构
|
||||
type ClientInfo struct {
|
||||
IP string // IP地址
|
||||
Port string // 端口
|
||||
LastSeen time.Time // 最后活跃时间
|
||||
}
|
||||
|
||||
// 客户端列表(使用互斥锁保护的映射)
|
||||
var (
|
||||
clientsMutex sync.Mutex
|
||||
clients = make(map[string]*ClientInfo)
|
||||
)
|
||||
|
||||
// StartTCPServer 启动TCP服务器
|
||||
func StartTCPServer(address string) error {
|
||||
listener, err := net.Listen("tcp", address)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
startClientCleanup()
|
||||
|
||||
Logger.Printf("TCP服务器已启动,正在监听 %s\n", address)
|
||||
|
||||
for {
|
||||
conn, err := listener.Accept()
|
||||
if err != nil {
|
||||
Logger.Printf("接受连接失败: %v", err)
|
||||
continue
|
||||
}
|
||||
|
||||
go handleConnection(conn)
|
||||
}
|
||||
}
|
||||
|
||||
// handleConnection 处理客户端连接
|
||||
func handleConnection(conn net.Conn) {
|
||||
defer conn.Close()
|
||||
|
||||
remoteAddr := conn.RemoteAddr().String()
|
||||
Logger.Printf("新的客户端连接: %s", remoteAddr)
|
||||
|
||||
addClient(remoteAddr)
|
||||
|
||||
buffer := make([]byte, 1024)
|
||||
|
||||
for {
|
||||
n, err := conn.Read(buffer)
|
||||
if err != nil {
|
||||
if err != io.EOF {
|
||||
Logger.Printf("从客户端读取失败 %s: %v", remoteAddr, err)
|
||||
} else {
|
||||
Logger.Printf("客户端断开连接 %s", remoteAddr)
|
||||
}
|
||||
removeClient(remoteAddr)
|
||||
break
|
||||
}
|
||||
|
||||
rawData := string(buffer[:n])
|
||||
TCPDataLogger.Printf("从客户端 %s 接收到原始数据: %s", remoteAddr, rawData)
|
||||
|
||||
sensorID, x, y, z, err := parseData(rawData)
|
||||
|
||||
if err == nil {
|
||||
TCPDataLogger.Printf("解析成功 - 客户端: %s, 传感器ID: %d, 值: X=%.3f, Y=%.3f, Z=%.3f",
|
||||
remoteAddr, sensorID, x, y, z)
|
||||
|
||||
if err := SaveSensorData(sensorID, x, y, z); err != nil {
|
||||
Logger.Printf("保存传感器数据失败: %v", err)
|
||||
}
|
||||
} else {
|
||||
TCPDataLogger.Printf("无法解析从客户端 %s 接收到的数据: %s, 错误: %v", remoteAddr, rawData, err)
|
||||
}
|
||||
|
||||
resp := "OK\n"
|
||||
if _, err := conn.Write([]byte(resp)); err != nil {
|
||||
Logger.Printf("发送响应到客户端 %s 失败: %v", remoteAddr, err)
|
||||
removeClient(remoteAddr)
|
||||
break
|
||||
}
|
||||
|
||||
updateClientLastSeen(remoteAddr)
|
||||
}
|
||||
}
|
||||
|
||||
// parseData 使用正则表达式解析传感器数据
|
||||
func parseData(data string) (int, float64, float64, float64, error) {
|
||||
pattern := regexp.MustCompile(`(\d+):([-]?\d+\.\d+),\s*([-]?\d+\.\d+),\s*([-]?\d+\.\d+)`)
|
||||
matches := pattern.FindStringSubmatch(data)
|
||||
|
||||
if len(matches) != 5 {
|
||||
return 0, 0, 0, 0, fmt.Errorf("数据格式不正确: %s", data)
|
||||
}
|
||||
|
||||
sensorID, err := strconv.Atoi(matches[1])
|
||||
if err != nil {
|
||||
return 0, 0, 0, 0, fmt.Errorf("解析传感器ID失败: %v", err)
|
||||
}
|
||||
|
||||
x, err := strconv.ParseFloat(strings.TrimSpace(matches[2]), 64)
|
||||
if err != nil {
|
||||
return 0, 0, 0, 0, fmt.Errorf("解析X值失败: %v", err)
|
||||
}
|
||||
|
||||
y, err := strconv.ParseFloat(strings.TrimSpace(matches[3]), 64)
|
||||
if err != nil {
|
||||
return 0, 0, 0, 0, fmt.Errorf("解析Y值失败: %v", err)
|
||||
}
|
||||
|
||||
z, err := strconv.ParseFloat(strings.TrimSpace(matches[4]), 64)
|
||||
if err != nil {
|
||||
return 0, 0, 0, 0, fmt.Errorf("解析Z值失败: %v", err)
|
||||
}
|
||||
|
||||
return sensorID, x, y, z, nil
|
||||
}
|
||||
|
||||
// addClient 添加客户端
|
||||
func addClient(addr string) {
|
||||
clientsMutex.Lock()
|
||||
defer clientsMutex.Unlock()
|
||||
|
||||
host, port, err := net.SplitHostPort(addr)
|
||||
if err != nil {
|
||||
Logger.Printf("解析客户端地址失败 %s: %v", addr, err)
|
||||
host = addr
|
||||
port = "unknown"
|
||||
}
|
||||
|
||||
clients[addr] = &ClientInfo{
|
||||
IP: host,
|
||||
Port: port,
|
||||
LastSeen: time.Now(),
|
||||
}
|
||||
|
||||
Logger.Printf("添加新客户端: %s", addr)
|
||||
}
|
||||
|
||||
// updateClientLastSeen 更新客户端最后活跃时间
|
||||
func updateClientLastSeen(addr string) {
|
||||
clientsMutex.Lock()
|
||||
defer clientsMutex.Unlock()
|
||||
|
||||
if client, exists := clients[addr]; exists {
|
||||
client.LastSeen = time.Now()
|
||||
}
|
||||
}
|
||||
|
||||
// removeClient 移除客户端
|
||||
func removeClient(addr string) {
|
||||
clientsMutex.Lock()
|
||||
defer clientsMutex.Unlock()
|
||||
|
||||
if client, exists := clients[addr]; exists {
|
||||
client.LastSeen = time.Now()
|
||||
Logger.Printf("客户端标记为断开连接: %s", addr)
|
||||
}
|
||||
}
|
||||
|
||||
// getAllClients 获取所有客户端信息
|
||||
func getAllClients() []map[string]interface{} {
|
||||
clientsMutex.Lock()
|
||||
defer clientsMutex.Unlock()
|
||||
|
||||
now := time.Now()
|
||||
result := make([]map[string]interface{}, 0, len(clients))
|
||||
|
||||
for addr, client := range clients {
|
||||
lastSeenDuration := now.Sub(client.LastSeen)
|
||||
|
||||
if lastSeenDuration > 24*time.Hour {
|
||||
delete(clients, addr)
|
||||
continue
|
||||
}
|
||||
|
||||
isOnline := lastSeenDuration < 10*time.Minute
|
||||
|
||||
result = append(result, map[string]interface{}{
|
||||
"address": addr,
|
||||
"ip": client.IP,
|
||||
"port": client.Port,
|
||||
"lastSeen": client.LastSeen,
|
||||
"isOnline": isOnline,
|
||||
"lastSeenFormatted": formatDuration(lastSeenDuration),
|
||||
})
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
// formatDuration 格式化持续时间为友好的字符串
|
||||
func formatDuration(d time.Duration) string {
|
||||
if d < time.Minute {
|
||||
return "刚刚"
|
||||
} else if d < time.Hour {
|
||||
return fmt.Sprintf("%d分钟前", int(d.Minutes()))
|
||||
} else if d < 24*time.Hour {
|
||||
return fmt.Sprintf("%d小时前", int(d.Hours()))
|
||||
} else {
|
||||
return fmt.Sprintf("%d天前", int(d.Hours()/24))
|
||||
}
|
||||
}
|
||||
|
||||
// startClientCleanup 启动清理过期客户端的goroutine
|
||||
func startClientCleanup() {
|
||||
go func() {
|
||||
for {
|
||||
time.Sleep(1 * time.Hour) // 每小时检查一次
|
||||
|
||||
clientsMutex.Lock()
|
||||
now := time.Now()
|
||||
|
||||
for addr, client := range clients {
|
||||
if now.Sub(client.LastSeen) > 24*time.Hour {
|
||||
delete(clients, addr)
|
||||
Logger.Printf("移除过期客户端: %s", addr)
|
||||
}
|
||||
}
|
||||
|
||||
clientsMutex.Unlock()
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
1331
templates/index.html
1331
templates/index.html
File diff suppressed because it is too large
Load Diff
Loading…
x
Reference in New Issue
Block a user