diff --git a/.gitignore b/.gitignore index 333c1e9..9abe0e7 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1 @@ -logs/ +logs/ diff --git a/.idea/.gitignore b/.idea/.gitignore index 13566b8..1c2fda5 100644 --- a/.idea/.gitignore +++ b/.idea/.gitignore @@ -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 diff --git a/.idea/angle_dtu.iml b/.idea/angle_dtu.iml index 5e764c4..338a266 100644 --- a/.idea/angle_dtu.iml +++ b/.idea/angle_dtu.iml @@ -1,9 +1,9 @@ - - - - - - - - + + + + + + + + \ No newline at end of file diff --git a/.idea/modules.xml b/.idea/modules.xml index d8a6140..22e21da 100644 --- a/.idea/modules.xml +++ b/.idea/modules.xml @@ -1,8 +1,8 @@ - - - - - - - + + + + + + + \ No newline at end of file diff --git a/.idea/vcs.xml b/.idea/vcs.xml index 35eb1dd..c8397c9 100644 --- a/.idea/vcs.xml +++ b/.idea/vcs.xml @@ -1,6 +1,6 @@ - - - - - + + + + + \ No newline at end of file diff --git a/angle.go.bak b/angle.go.bak index 5e329fb..ce16b35 100644 --- a/angle.go.bak +++ b/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) + } +} diff --git a/db.go b/db.go index 8af8768..0652e0a 100644 --- a/db.go +++ b/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 { diff --git a/go.mod b/go.mod index d738fc6..95dd8b2 100644 --- a/go.mod +++ b/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 diff --git a/go.sum b/go.sum index 0bbe40c..ca075bf 100644 --- a/go.sum +++ b/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= diff --git a/http_server.go b/http_server.go index 598b76f..8671b01 100644 --- a/http_server.go +++ b/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) + } +} diff --git a/logger.go b/logger.go index 1323815..1ba7c62 100644 --- a/logger.go +++ b/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) + } + } + }() +} diff --git a/static/css/main.css b/static/css/main.css index 8b0e6a2..051076e 100644 --- a/static/css/main.css +++ b/static/css/main.css @@ -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; + } +} diff --git a/static/js/main.js b/static/js/main.js index 0a1f8cb..5500b83 100644 --- a/static/js/main.js +++ b/static/js/main.js @@ -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 = '没有数据'; - 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 = - '' + item.id + '' + - '' + item.sensor_id + '' + - '' + item.x.toFixed(3) + '' + - '' + item.y.toFixed(3) + '' + - '' + item.z.toFixed(3) + '' + - '' + formattedDate + ''; - - 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 = '没有数据'; + 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 = + '' + item.id + '' + + '' + item.sensor_id + '' + + '' + item.x.toFixed(3) + '' + + '' + item.y.toFixed(3) + '' + + '' + item.z.toFixed(3) + '' + + '' + formattedDate + ''; + + 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); +} diff --git a/tcp_server.go b/tcp_server.go index 975c04e..5a9aaa4 100644 --- a/tcp_server.go +++ b/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() + } + }() +} diff --git a/templates/index.html b/templates/index.html index 391572f..5bf2e24 100644 --- a/templates/index.html +++ b/templates/index.html @@ -1,665 +1,666 @@ - - - - - - 测斜仪数据 - - - - - -
-

测斜仪数据

-
- -
-
-
- - -
- -
- - -
- -
- - -
- -
- - -
- -
- - -
- -
- -
-
- - -
-

传感器数据图表

- -
- - -
-

传感器数据表格

- - - - - - - - - - - - - - - -
数据编号探头地址时间XYZ
-
-
- - - - - + + + + + + 测斜仪数据 + + + + + +
+

测斜仪数据

+
+ +
+
+
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ +
+
+ + +
+

传感器数据图表

+ +
+ + +
+

传感器数据表格

+ + + + + + + + + + + + + + + +
数据编号探头地址时间XYZ
+
+
+ + + + +