commit a9060efb87acaa3a9e24622447cbf6aaeec67710 Author: fengyarnom Date: Thu Apr 24 14:44:30 2025 +0800 Initial commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..dfd1625 --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +data/*.db +data/sensor.db +logs/ diff --git a/data/hourly_rain_schema.sql b/data/hourly_rain_schema.sql new file mode 100644 index 0000000..698c5a7 --- /dev/null +++ b/data/hourly_rain_schema.sql @@ -0,0 +1,39 @@ +-- 创建小时雨量统计表 +CREATE TABLE IF NOT EXISTS hourly_rainfall ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + hour_start DATETIME NOT NULL, -- 小时开始时间(例如:2025-04-21 18:00:00) + hour_end DATETIME NOT NULL, -- 小时结束时间(例如:2025-04-21 19:00:00) + rainfall INTEGER NOT NULL, -- 该小时的雨量(整数,实际值需除以10) + min_value INTEGER, -- 该小时内记录的最小累积值 + max_value INTEGER, -- 该小时内记录的最大累积值 + samples INTEGER, -- 该小时内的采样数量 + created_at DATETIME NOT NULL, -- 记录创建时间 + UNIQUE(hour_start) -- 确保每小时只有一条记录 +); + +-- 创建用于计算历史数据的临时视图 +CREATE TEMP VIEW IF NOT EXISTS hourly_data AS +SELECT + datetime(strftime('%Y-%m-%d %H:00:00', timestamp)) as hour_start, + datetime(strftime('%Y-%m-%d %H:59:59', timestamp)) as hour_end, + MIN(optical_rain) as min_value, + MAX(optical_rain) as max_value, + COUNT(*) as samples, + MAX(optical_rain) - MIN(optical_rain) as rainfall +FROM sensor_data +WHERE timestamp >= '2025-04-21 18:00:00' +GROUP BY strftime('%Y-%m-%d %H', timestamp) +ORDER BY hour_start; + +-- 将历史数据插入小时雨量表 +INSERT OR IGNORE INTO hourly_rainfall + (hour_start, hour_end, rainfall, min_value, max_value, samples, created_at) +SELECT + hour_start, + hour_end, + rainfall, + min_value, + max_value, + samples, + datetime('now') +FROM hourly_data; diff --git a/data/sensor.db.bak b/data/sensor.db.bak new file mode 100644 index 0000000..4d1ef6b Binary files /dev/null and b/data/sensor.db.bak differ diff --git a/data/sensor.db.bak2 b/data/sensor.db.bak2 new file mode 100644 index 0000000..511d156 Binary files /dev/null and b/data/sensor.db.bak2 differ diff --git a/data_process.go b/data_process.go new file mode 100644 index 0000000..8587976 --- /dev/null +++ b/data_process.go @@ -0,0 +1,80 @@ +package main + +import ( + "time" +) + +// 传感器数据结构 +type SensorData struct { + WindSpeed int + WindForce int + WindDirection8 int + WindDirection360 int + Humidity int + Temperature int + Noise int + PM25 int + PM10 int + AtmPressure int + Lux20WH int + Lux20WL int + Light20W int + OpticalRain int + CompassAngle int + SolarRadiation int + Timestamp time.Time +} + +// 解析传感器数据 +func parseData(data []byte) *SensorData { + // 检查响应格式是否正确 + if len(data) < 37 || data[0] != 0x01 || data[1] != 0x03 || data[2] != 0x20 { + logger.Println("响应格式无效") + return nil + } + + // 提取数据部分 + dataBytes := data[3:35] // 跳过地址码、功能码、长度,不包括CRC + + sensorData := SensorData{ + WindSpeed: bytesToInt(dataBytes[0:2]), + WindForce: bytesToInt(dataBytes[2:4]), + WindDirection8: bytesToInt(dataBytes[4:6]), + WindDirection360: bytesToInt(dataBytes[6:8]), + Humidity: bytesToInt(dataBytes[8:10]), + Temperature: bytesToInt(dataBytes[10:12]), + Noise: bytesToInt(dataBytes[12:14]), + PM25: bytesToInt(dataBytes[14:16]), + PM10: bytesToInt(dataBytes[16:18]), + AtmPressure: bytesToInt(dataBytes[18:20]), + Lux20WH: bytesToInt(dataBytes[20:22]), + Lux20WL: bytesToInt(dataBytes[22:24]), + Light20W: bytesToInt(dataBytes[24:26]), + OpticalRain: bytesToInt(dataBytes[26:28]), + CompassAngle: bytesToInt(dataBytes[28:30]), + SolarRadiation: bytesToInt(dataBytes[30:32]), + Timestamp: time.Now(), + } + + return &sensorData +} + +// 记录传感器数据到日志 +func logSensorData(data SensorData) { + logger.Printf("[传感器] 时间: %s, 温度: %.1f°C, 湿度: %.1f%%, 风速: %.2fm/s (%d°), 雨量: %.1fmm, PM2.5: %dμg/m³", + data.Timestamp.Format("2006-01-02 15:04:05"), + float64(data.Temperature)/10.0, + float64(data.Humidity)/10.0, + float64(data.WindSpeed)/100.0, + data.WindDirection360, + float64(data.OpticalRain)/10.0, + data.PM25) +} + +// 将两个字节转换为整数 +func bytesToInt(bytes []byte) int { + if len(bytes) != 2 { + return 0 + } + return int(bytes[0])<<8 | int(bytes[1]) +} diff --git a/database.go b/database.go new file mode 100644 index 0000000..9633fca --- /dev/null +++ b/database.go @@ -0,0 +1,74 @@ +package main + +import ( + "database/sql" + "os" +) + +var db *sql.DB + +// 初始化数据库 +func initDB() { + var err error + if err := os.MkdirAll("data", 0755); err != nil { + logger.Fatalf("创建数据目录失败: %v", err) + } + + db, err = sql.Open("sqlite", "data/sensor.db") + if err != nil { + logger.Fatalf("打开数据库错误: %v", err) + } + + // 创建数据表 + createTableSQL := `CREATE TABLE IF NOT EXISTS sensor_data ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + timestamp DATETIME NOT NULL, + wind_speed INTEGER, + wind_force INTEGER, + wind_direction8 INTEGER, + wind_direction360 INTEGER, + humidity INTEGER, + temperature INTEGER, + noise INTEGER, + pm25 INTEGER, + pm10 INTEGER, + atm_pressure INTEGER, + lux20wh INTEGER, + lux20wl INTEGER, + light20w INTEGER, + optical_rain INTEGER, + compass_angle INTEGER, + solar_radiation INTEGER + );` + + _, err = db.Exec(createTableSQL) + if err != nil { + logger.Fatalf("创建表错误: %v", err) + } +} + +// 保存传感器数据到数据库 +func saveSensorData(data SensorData) { + stmt, err := db.Prepare(`INSERT INTO sensor_data ( + timestamp, wind_speed, wind_force, wind_direction8, wind_direction360, + humidity, temperature, noise, pm25, pm10, atm_pressure, + lux20wh, lux20wl, light20w, optical_rain, compass_angle, solar_radiation + ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`) + + if err != nil { + logger.Printf("准备SQL语句错误: %v", err) + return + } + defer stmt.Close() + + _, err = stmt.Exec( + data.Timestamp, data.WindSpeed, data.WindForce, data.WindDirection8, data.WindDirection360, + data.Humidity, data.Temperature, data.Noise, data.PM25, data.PM10, data.AtmPressure, + data.Lux20WH, data.Lux20WL, data.Light20W, data.OpticalRain, data.CompassAngle, data.SolarRadiation, + ) + + if err != nil { + logger.Printf("保存数据错误: %v", err) + return + } +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..b3cf273 --- /dev/null +++ b/go.mod @@ -0,0 +1,18 @@ +module tcp_server + +go 1.24.2 + +require modernc.org/sqlite v1.37.0 + +require ( + github.com/dustin/go-humanize v1.0.1 // indirect + github.com/google/uuid v1.6.0 // indirect + github.com/mattn/go-isatty v0.0.20 // indirect + github.com/ncruces/go-strftime v0.1.9 // indirect + github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect + golang.org/x/exp v0.0.0-20250305212735-054e65f0b394 // indirect + golang.org/x/sys v0.31.0 // indirect + modernc.org/libc v1.62.1 // indirect + modernc.org/mathutil v1.7.1 // indirect + modernc.org/memory v1.9.1 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..e944b01 --- /dev/null +++ b/go.sum @@ -0,0 +1,47 @@ +github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= +github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= +github.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e h1:ijClszYn+mADRFY17kjQEVQ1XRhq2/JR1M3sGqeJoxs= +github.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e/go.mod h1:boTsfXsheKC2y+lKOCMpSfarhxDeIzfZG1jqGcPl3cA= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= +github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/ncruces/go-strftime v0.1.9 h1:bY0MQC28UADQmHmaF5dgpLmImcShSi2kHU9XLdhx/f4= +github.com/ncruces/go-strftime v0.1.9/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls= +github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE= +github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo= +golang.org/x/exp v0.0.0-20250305212735-054e65f0b394 h1:nDVHiLt8aIbd/VzvPWN6kSOPE7+F/fNFDSXLVYkE/Iw= +golang.org/x/exp v0.0.0-20250305212735-054e65f0b394/go.mod h1:sIifuuw/Yco/y6yb6+bDNfyeQ/MdPUy/hKEMYQV17cM= +golang.org/x/mod v0.24.0 h1:ZfthKaKaT4NrhGVZHO1/WDTwGES4De8KtWO0SIbNJMU= +golang.org/x/mod v0.24.0/go.mod h1:IXM97Txy2VM4PJ3gI61r1YEk/gAj6zAHN3AdZt6S9Ww= +golang.org/x/sync v0.12.0 h1:MHc5BpPuC30uJk597Ri8TV3CNZcTLu6B6z4lJy+g6Jw= +golang.org/x/sync v0.12.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= +golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.31.0 h1:ioabZlmFYtWhL+TRYpcnNlLwhyxaM9kWTDEmfnprqik= +golang.org/x/sys v0.31.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= +golang.org/x/tools v0.31.0 h1:0EedkvKDbh+qistFTd0Bcwe/YLh4vHwWEkiI0toFIBU= +golang.org/x/tools v0.31.0/go.mod h1:naFTU+Cev749tSJRXJlna0T3WxKvb1kWEx15xA4SdmQ= +modernc.org/cc/v4 v4.25.2 h1:T2oH7sZdGvTaie0BRNFbIYsabzCxUQg8nLqCdQ2i0ic= +modernc.org/cc/v4 v4.25.2/go.mod h1:uVtb5OGqUKpoLWhqwNQo/8LwvoiEBLvZXIQ/SmO6mL0= +modernc.org/ccgo/v4 v4.25.1 h1:TFSzPrAGmDsdnhT9X2UrcPMI3N/mJ9/X9ykKXwLhDsU= +modernc.org/ccgo/v4 v4.25.1/go.mod h1:njjuAYiPflywOOrm3B7kCB444ONP5pAVr8PIEoE0uDw= +modernc.org/fileutil v1.3.0 h1:gQ5SIzK3H9kdfai/5x41oQiKValumqNTDXMvKo62HvE= +modernc.org/fileutil v1.3.0/go.mod h1:XatxS8fZi3pS8/hKG2GH/ArUogfxjpEKs3Ku3aK4JyQ= +modernc.org/gc/v2 v2.6.5 h1:nyqdV8q46KvTpZlsw66kWqwXRHdjIlJOhG6kxiV/9xI= +modernc.org/gc/v2 v2.6.5/go.mod h1:YgIahr1ypgfe7chRuJi2gD7DBQiKSLMPgBQe9oIiito= +modernc.org/libc v1.62.1 h1:s0+fv5E3FymN8eJVmnk0llBe6rOxCu/DEU+XygRbS8s= +modernc.org/libc v1.62.1/go.mod h1:iXhATfJQLjG3NWy56a6WVU73lWOcdYVxsvwCgoPljuo= +modernc.org/mathutil v1.7.1 h1:GCZVGXdaN8gTqB1Mf/usp1Y/hSqgI2vAGGP4jZMCxOU= +modernc.org/mathutil v1.7.1/go.mod h1:4p5IwJITfppl0G4sUEDtCr4DthTaT47/N3aT6MhfgJg= +modernc.org/memory v1.9.1 h1:V/Z1solwAVmMW1yttq3nDdZPJqV1rM05Ccq6KMSZ34g= +modernc.org/memory v1.9.1/go.mod h1:/JP4VbVC+K5sU2wZi9bHoq2MAkCnrt2r98UGeSK7Mjw= +modernc.org/opt v0.1.4 h1:2kNGMRiUjrp4LcaPuLY2PzUfqM/w9N23quVwhKt5Qm8= +modernc.org/opt v0.1.4/go.mod h1:03fq9lsNfvkYSfxrfUhZCWPk1lm4cq4N+Bh//bEtgns= +modernc.org/sortutil v1.2.1 h1:+xyoGf15mM3NMlPDnFqrteY07klSFxLElE2PVuWIJ7w= +modernc.org/sortutil v1.2.1/go.mod h1:7ZI3a3REbai7gzCLcotuw9AC4VZVpYMjDzETGsSMqJE= +modernc.org/sqlite v1.37.0 h1:s1TMe7T3Q3ovQiK2Ouz4Jwh7dw4ZDqbebSDTlSJdfjI= +modernc.org/sqlite v1.37.0/go.mod h1:5YiWv+YviqGMuGw4V+PNplcyaJ5v+vQd7TQOgkACoJM= +modernc.org/strutil v1.2.1 h1:UneZBkQA+DX2Rp35KcM69cSsNES9ly8mQWD71HKlOA0= +modernc.org/strutil v1.2.1/go.mod h1:EHkiggD70koQxjVdSBM3JKM7k6L0FbGE5eymy9i3B9A= +modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y= +modernc.org/token v1.1.0/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM= diff --git a/hourly_rainfall.go b/hourly_rainfall.go new file mode 100644 index 0000000..b292b64 --- /dev/null +++ b/hourly_rainfall.go @@ -0,0 +1,224 @@ +package main + +import ( + "database/sql" + "fmt" + "time" +) + +// 定期更新小时雨量统计 +func startHourlyRainfallUpdater() { + // 确保小时雨量表存在 + ensureHourlyRainfallTable() + + // 更新历史数据 + updateHistoricalHourlyRainfall() + + // 启动定时更新 + go periodicHourlyRainfallUpdate() +} + +// 确保小时雨量表存在 +func ensureHourlyRainfallTable() { + _, err := db.Exec(` + CREATE TABLE IF NOT EXISTS hourly_rainfall ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + hour_start DATETIME NOT NULL, + hour_end DATETIME NOT NULL, + rainfall INTEGER NOT NULL, + min_value INTEGER, + max_value INTEGER, + samples INTEGER, + created_at DATETIME NOT NULL, + UNIQUE(hour_start) + ) + `) + + if err != nil { + logger.Printf("创建小时雨量表错误: %v", err) + } +} + +// 更新历史小时雨量数据(从2025-04-21 18:00:00开始) +func updateHistoricalHourlyRainfall() { + logger.Printf("开始更新历史小时雨量数据...") + + // 查询现有的小时数据 + rows, err := db.Query(` + SELECT + datetime(strftime('%Y-%m-%d %H:00:00', timestamp)) as hour_start, + datetime(strftime('%Y-%m-%d %H:59:59', timestamp)) as hour_end, + MIN(optical_rain) as min_value, + MAX(optical_rain) as max_value, + COUNT(*) as samples, + MAX(optical_rain) - MIN(optical_rain) as rainfall + FROM sensor_data + WHERE timestamp >= '2025-04-21 18:00:00' + GROUP BY strftime('%Y-%m-%d %H', timestamp) + ORDER BY hour_start + `) + + if err != nil { + logger.Printf("查询历史数据错误: %v", err) + return + } + defer rows.Close() + + // 准备插入语句 + stmt, err := db.Prepare(` + INSERT OR IGNORE INTO hourly_rainfall + (hour_start, hour_end, rainfall, min_value, max_value, samples, created_at) + VALUES (?, ?, ?, ?, ?, ?, datetime('now')) + `) + + if err != nil { + logger.Printf("准备插入语句错误: %v", err) + return + } + defer stmt.Close() + + // 插入数据 + count := 0 + for rows.Next() { + var hourStart, hourEnd string + var minValue, maxValue, samples, rainfall int + + err := rows.Scan(&hourStart, &hourEnd, &minValue, &maxValue, &samples, &rainfall) + if err != nil { + logger.Printf("读取行数据错误: %v", err) + continue + } + + _, err = stmt.Exec(hourStart, hourEnd, rainfall, minValue, maxValue, samples) + if err != nil { + logger.Printf("插入数据错误: %v", err) + continue + } + + count++ + } + + logger.Printf("历史小时雨量数据更新完成,共插入 %d 条记录", count) +} + +// 定期更新小时雨量 +func periodicHourlyRainfallUpdate() { + // 计算下一个整点后5分钟的时间(给足够时间收集整点数据) + now := time.Now() + nextHour := time.Date(now.Year(), now.Month(), now.Day(), now.Hour()+1, 5, 0, 0, now.Location()) + delay := nextHour.Sub(now) + + // 首次等待到下一个整点后5分钟 + time.Sleep(delay) + + // 然后每小时更新一次 + ticker := time.NewTicker(time.Hour) + for range ticker.C { + updateLastHourRainfall() + } +} + +// 更新上一个小时的雨量数据 +func updateLastHourRainfall() { + // 计算上一个小时的时间范围 + now := time.Now() + lastHourEnd := time.Date(now.Year(), now.Month(), now.Day(), now.Hour(), 0, 0, 0, now.Location()) + lastHourStart := lastHourEnd.Add(-time.Hour) + + // 查询这个小时的数据 + var minValue, maxValue, samples, rainfall sql.NullInt64 + + err := db.QueryRow(` + SELECT + MIN(optical_rain), + MAX(optical_rain), + COUNT(*), + MAX(optical_rain) - MIN(optical_rain) + FROM sensor_data + WHERE timestamp >= ? AND timestamp < ? + `, lastHourStart.Format("2006-01-02 15:04:05"), lastHourEnd.Format("2006-01-02 15:04:05")).Scan( + &minValue, &maxValue, &samples, &rainfall) + + if err != nil { + logger.Printf("查询上一小时数据错误: %v", err) + return + } + + // 检查是否有足够的数据 + if !samples.Valid || samples.Int64 < 2 { + logger.Printf("上一小时 (%s) 数据样本不足,跳过更新", lastHourStart.Format("2006-01-02 15:04")) + return + } + + // 插入或更新小时雨量记录 + _, err = db.Exec(` + INSERT OR REPLACE INTO hourly_rainfall + (hour_start, hour_end, rainfall, min_value, max_value, samples, created_at) + VALUES (?, ?, ?, ?, ?, ?, datetime('now')) + `, + lastHourStart.Format("2006-01-02 15:04:05"), + lastHourEnd.Format("2006-01-02 15:04:05"), + rainfall.Int64, + minValue.Int64, + maxValue.Int64, + samples.Int64) + + if err != nil { + logger.Printf("更新小时雨量记录错误: %v", err) + return + } + + logger.Printf("已更新 %s 小时的雨量数据: %.1fmm (%d 个样本)", + lastHourStart.Format("2006-01-02 15:04"), + float64(rainfall.Int64)/10.0, + samples.Int64) +} + +// 获取最近24小时的降雨数据 +func getRecentHourlyRainfall(hours int) ([]HourlyRainfall, error) { + if hours <= 0 { + hours = 24 + } + + rows, err := db.Query(` + SELECT hour_start, rainfall, samples + FROM hourly_rainfall + ORDER BY hour_start DESC + LIMIT ? + `, hours) + + if err != nil { + return nil, fmt.Errorf("查询小时雨量错误: %v", err) + } + defer rows.Close() + + var result []HourlyRainfall + for rows.Next() { + var hr HourlyRainfall + var hourStart string + + err := rows.Scan(&hourStart, &hr.Rainfall, &hr.Samples) + if err != nil { + return nil, fmt.Errorf("读取小时雨量错误: %v", err) + } + + // 解析时间 + hr.HourStart, _ = time.Parse("2006-01-02 15:04:05", hourStart) + + result = append(result, hr) + } + + // 反转数组,使其按时间顺序排列 + for i, j := 0, len(result)-1; i < j; i, j = i+1, j-1 { + result[i], result[j] = result[j], result[i] + } + + return result, nil +} + +// 小时雨量数据结构 +type HourlyRainfall struct { + HourStart time.Time + Rainfall int64 + Samples int64 +} diff --git a/imports.go b/imports.go new file mode 100644 index 0000000..bdedb62 --- /dev/null +++ b/imports.go @@ -0,0 +1,5 @@ +package main + +import ( + _ "modernc.org/sqlite" +) diff --git a/main.go b/main.go new file mode 100644 index 0000000..c510ccd --- /dev/null +++ b/main.go @@ -0,0 +1,174 @@ +package main + +import ( + "log" + "net" + "os" + "sync" + "time" +) + +var ( + logger *log.Logger + logFile *os.File + activeConn net.Conn + activeSensor *SensorComm + activeConnMutex sync.Mutex + clientAddress string +) + +func main() { + setupLogging() + defer logFile.Close() + + initDB() + defer db.Close() + + go startWebServer() + + addr := "0.0.0.0:10002" + listener, err := net.Listen("tcp", addr) + if err != nil { + logger.Fatalf("监听端口错误: %v", err) + } + defer listener.Close() + + logger.Printf("服务器已启动: %s", addr) + + for { + conn, err := listener.Accept() + if err != nil { + logger.Printf("接受连接错误: %v", err) + continue + } + + activeConnMutex.Lock() + // 关闭旧连接 + if activeConn != nil { + oldConn := activeConn + oldSensor := activeSensor + activeConn = nil + activeSensor = nil + activeConnMutex.Unlock() + + if oldSensor != nil { + oldSensor.close() + } else if oldConn != nil { + oldConn.Close() + } + + logger.Printf("关闭旧连接,接入新连接: %s", conn.RemoteAddr().String()) + } else { + activeConnMutex.Unlock() + logger.Printf("新连接: %s", conn.RemoteAddr().String()) + } + + sensorComm := newSensorComm(conn) + + activeConnMutex.Lock() + activeConn = conn + activeSensor = sensorComm + clientAddress = conn.RemoteAddr().String() + activeConnMutex.Unlock() + + go handleConnection(sensorComm) + } +} + +func handleConnection(sensor *SensorComm) { + defer func() { + activeConnMutex.Lock() + if activeConn == sensor.conn { + activeConn = nil + activeSensor = nil + clientAddress = "" + } + activeConnMutex.Unlock() + + sensor.close() + }() + + logger.Printf("处理连接: %s", sensor.address) + + nextQuery := getNextQueryTime() + nextReset := getNextHourTime() + + var nextEvent time.Time + isQueryEvent := true + + if nextQuery.Before(nextReset) { + nextEvent = nextQuery + isQueryEvent = true + } else { + nextEvent = nextReset + isQueryEvent = false + } + + timer := time.NewTimer(time.Until(nextEvent)) + sensor.sendQuery() + + buffer := make([]byte, 1024) + done := make(chan bool) + + go func() { + for { + n, err := sensor.conn.Read(buffer) + if err != nil { + logger.Printf("客户端断开: %v", err) + done <- true + return + } + + if n >= 37 { + sensorData := sensor.handleData(buffer[:n]) + if sensorData != nil { + saveSensorData(*sensorData) + logSensorData(*sensorData) + } + } + + logger.Printf("接收数据长度: %d", n) + } + }() + + for { + select { + case <-timer.C: + if isQueryEvent { + sensor.sendQuery() + nextQuery = getNextQueryTime() + } else { + sensor.resetHourly() + nextReset = getNextHourTime() + } + + if nextQuery.Before(nextReset) { + nextEvent = nextQuery + isQueryEvent = true + } else { + nextEvent = nextReset + isQueryEvent = false + } + + timer.Reset(time.Until(nextEvent)) + + case <-done: + return + } + } +} + +func setupLogging() { + if err := os.MkdirAll("logs", 0755); err != nil { + log.Fatalf("创建日志目录失败: %v", err) + } + + logFileName := "logs/sensor_" + time.Now().Format("2006-01-02") + ".log" + var err error + logFile, err = os.OpenFile(logFileName, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644) + if err != nil { + log.Fatalf("打开日志文件失败: %v", err) + } + + logger = log.New(logFile, "", log.LstdFlags) +} diff --git a/sensor.db b/sensor.db new file mode 100644 index 0000000..5b44e59 Binary files /dev/null and b/sensor.db differ diff --git a/sensor_comm.go b/sensor_comm.go new file mode 100644 index 0000000..9c5c87b --- /dev/null +++ b/sensor_comm.go @@ -0,0 +1,128 @@ +package main + +import ( + "encoding/hex" + "net" + "time" +) + +// 传感器通信相关结构和函数 +type SensorComm struct { + conn net.Conn + address string + lastQueryTime time.Time + lastResetTime time.Time + querySuccess bool + responseRecv chan bool + queryCmd []byte + resetCmd []byte +} + +// 创建新的传感器通信实例 +func newSensorComm(conn net.Conn) *SensorComm { + sc := &SensorComm{ + conn: conn, + address: conn.RemoteAddr().String(), + lastQueryTime: time.Time{}, + lastResetTime: time.Time{}, + querySuccess: false, + responseRecv: make(chan bool, 1), // 带缓冲的通道,避免阻塞 + queryCmd: prepareQueryCommand(), + resetCmd: prepareResetCommand(), + } + return sc +} + +// 准备查询命令 +func prepareQueryCommand() []byte { + queryHexData := "01 03 01 F4 00 10 04 08" + queryHexData = removeSpaces(queryHexData) + queryData, err := hex.DecodeString(queryHexData) + if err != nil { + logger.Printf("解析查询命令错误: %v", err) + return nil + } + return queryData +} + +// 准备重置命令 +func prepareResetCommand() []byte { + resetHexData := "01 06 60 02 00 5A B6 31" + resetHexData = removeSpaces(resetHexData) + resetData, err := hex.DecodeString(resetHexData) + if err != nil { + logger.Printf("解析重置命令错误: %v", err) + return nil + } + return resetData +} + +// 发送查询命令 +func (sc *SensorComm) sendQuery() bool { + sc.querySuccess = false + _, err := sc.conn.Write(sc.queryCmd) + if err != nil { + logger.Printf("发送查询命令错误: %v", err) + return false + } + + sc.lastQueryTime = time.Now() + logger.Printf("发送查询命令: %s", time.Now().Format("15:04:05")) + return true +} + +// 发送整点重置命令,在查询成功后执行 +func (sc *SensorComm) resetHourly() bool { + // 检查是否在过去5分钟内已经重置过 + if time.Since(sc.lastResetTime) < 5*time.Minute { + logger.Printf("最近5分钟内已重置过雨量,跳过本次重置") + return false + } + + // 直接发送重置命令 + _, err := sc.conn.Write(sc.resetCmd) + if err != nil { + logger.Printf("发送重置命令错误: %v", err) + return false + } + + logger.Printf("发送雨量重置命令: %s", time.Now().Format("15:04:05")) + sc.lastResetTime = time.Now() + return true +} +// 处理接收到的数据 +func (sc *SensorComm) handleData(data []byte) *SensorData { + sensorData := parseData(data) + if sensorData != nil { + sc.querySuccess = true + + // 通知已收到响应 + select { + case sc.responseRecv <- true: + default: + // 通道已满或无接收者,这里不阻塞 + } + + return sensorData + } + return nil +} + +// 关闭连接 +func (sc *SensorComm) close() { + if sc.conn != nil { + sc.conn.Close() + } + close(sc.responseRecv) +} + +// 移除字符串中的空格 +func removeSpaces(s string) string { + var result []rune + for _, r := range s { + if r != ' ' { + result = append(result, r) + } + } + return string(result) +} diff --git a/tcp_server b/tcp_server new file mode 100755 index 0000000..e427d62 Binary files /dev/null and b/tcp_server differ diff --git a/templates/hourly_rainfall.html b/templates/hourly_rainfall.html new file mode 100644 index 0000000..e4e3fc5 --- /dev/null +++ b/templates/hourly_rainfall.html @@ -0,0 +1,126 @@ + + + + + + 小时雨量统计 + + + +
+

小时雨量统计

+ + + +
+ 显示范围: + 24小时 + 48小时 + 3天 + 7天 + 30天 +
+ +
+

统计摘要

+ {{if .HourlyData}} + {{$sum := 0.0}} + {{$max := 0.0}} + {{$maxTime := ""}} + {{range .HourlyData}} + {{$rainfall := divInt64 .Rainfall 10}} + {{$sum = add $sum $rainfall}} + {{if gt $rainfall $max}} + {{$max = $rainfall}} + {{$maxTime = formatTime .HourStart}} + {{end}} + {{end}} +

+ 总降雨量: {{printf "%.1f" $sum}} mm
+ 最大小时降雨量: {{printf "%.1f" $max}} mm ({{$maxTime}})
+ 统计时段: {{len .HourlyData}} 小时 +

+ {{else}} +

暂无降雨数据

+ {{end}} +
+ +

小时降雨详情

+ {{if .HourlyData}} + + + + + + + + {{range .HourlyData}} + + + + + + + {{end}} +
时间降雨量(mm)数据采样数雨量图示
{{formatTime .HourStart}}{{printf "%.1f" (divInt64 .Rainfall 10)}}{{.Samples}} +
+
+ {{else}} +

所选时间段内暂无降雨记录。

+ {{end}} +
+ + + + diff --git a/templates/index.html b/templates/index.html new file mode 100644 index 0000000..6ee32c8 --- /dev/null +++ b/templates/index.html @@ -0,0 +1,112 @@ + + + + + 雨量计 DTU 数据 + + + +

雨量计数据

+ +
+

+ 连接状态: {{.ConnStatus}} | + 客户端地址: {{.ClientAddr}} +

+
+ + + + {{if .Latest}} +
+

最新数据 ({{.Latest.Timestamp.Format "2006-01-02 15:04:05"}})

+

+ 温度: {{printf "%.1f" (div .Latest.Temperature 10)}}°C | + 湿度: {{printf "%.1f" (div .Latest.Humidity 10)}}% | + 风速: {{printf "%.2f" (div .Latest.WindSpeed 100)}}m/s | + 风向: {{.Latest.WindDirection360}}° | + 风力: {{.Latest.WindForce}}级 +

+

+ 雨量: {{printf "%.1f" (div .Latest.OpticalRain 10)}}mm | + 大气压: {{printf "%.1f" (div .Latest.AtmPressure 10)}}kPa | + 太阳辐射: {{.Latest.SolarRadiation}}W/m² +

+
+ {{end}} + +

历史数据

+ + + + + + + + + + + + + {{range .Records}} + + + + + + + + + + + + {{end}} +
时间温度(°C)湿度(%)风速(m/s)风向(°)风力(级)雨量(mm)大气压(kPa)太阳辐射(W/m²)
{{.Timestamp.Format "2006-01-02 15:04:05"}}{{printf "%.1f" (div .Temperature 10)}}{{printf "%.1f" (div .Humidity 10)}}{{printf "%.2f" (div .WindSpeed 100)}}{{.WindDirection360}}{{.WindForce}}{{printf "%.1f" (div .OpticalRain 10)}}{{printf "%.1f" (div .AtmPressure 10)}}{{.SolarRadiation}}
+ +
+ {{if gt .Page 1}} + 上一页 + {{end}} + + {{range .Pages}} + {{if eq . $.Page}} + {{.}} + {{else}} + {{.}} + {{end}} + {{end}} + + {{if lt .Page .TotalPages}} + 下一页 + {{end}} +
+ + + + diff --git a/time_utils.go b/time_utils.go new file mode 100644 index 0000000..32aff36 --- /dev/null +++ b/time_utils.go @@ -0,0 +1,47 @@ +package main + +import ( + "time" +) + +// 获取5分钟为间隔的下一个查询时间 +func getNextQueryTime() time.Time { + now := time.Now() + minute := now.Minute() + nextMinute := ((minute / 5) + 1) * 5 + + var nextHour int + if nextMinute >= 60 { + nextMinute = 0 + nextHour = now.Hour() + 1 + if nextHour >= 24 { + nextHour = 0 + } + } else { + nextHour = now.Hour() + } + + return time.Date(now.Year(), now.Month(), now.Day(), nextHour, nextMinute, 0, 0, now.Location()) +} + +// 获取下一个整点时间 +func getNextHourTime() time.Time { + now := time.Now() + nextHour := now.Hour() + 1 + if nextHour >= 24 { + nextHour = 0 + // 如果是23点,则下一小时是次日的0点 + if now.Hour() == 23 { + tomorrow := now.AddDate(0, 0, 1) + return time.Date(tomorrow.Year(), tomorrow.Month(), tomorrow.Day(), 0, 0, 0, 0, now.Location()) + } + } + return time.Date(now.Year(), now.Month(), now.Day(), nextHour, 0, 0, 0, now.Location()) +} + +// 判断是否接近整点(前后2分钟内) +func isNearHour() bool { + now := time.Now() + minute := now.Minute() + return minute >= 58 || minute <= 2 +} diff --git a/web.go b/web.go new file mode 100644 index 0000000..60b736d --- /dev/null +++ b/web.go @@ -0,0 +1,206 @@ +package main + +import ( + "html/template" + "net/http" + "os" + "path/filepath" + "strconv" + "time" +) + +func startWebServer() { + if err := os.MkdirAll("templates", 0755); err != nil { + logger.Fatalf("创建模板目录失败: %v", err) + } + + indexPath := filepath.Join("templates", "index.html") + if _, err := os.Stat(indexPath); os.IsNotExist(err) { + logger.Printf("警告: %s 不存在", indexPath) + } + http.HandleFunc("/", handleIndex) + http.HandleFunc("/refresh-data", handleRefresh) + + logger.Printf("启动Web服务器: http://0.0.0.0:10001") + if err := http.ListenAndServe(":10001", nil); err != nil { + logger.Fatalf("启动Web服务器失败: %v", err) + } +} + +func handleRefresh(w http.ResponseWriter, r *http.Request) { + if r.Method != "POST" { + http.Error(w, "只支持POST请求", http.StatusMethodNotAllowed) + return + } + + activeConnMutex.Lock() + sensor := activeSensor + activeConnMutex.Unlock() + + if sensor == nil { + http.Error(w, "无活动的传感器连接", http.StatusServiceUnavailable) + return + } + + if time.Since(sensor.lastQueryTime) < 3*time.Second { + w.WriteHeader(http.StatusOK) + return + } + + if sensor.sendQuery() { + logger.Printf("用户触发查询命令") + w.WriteHeader(http.StatusOK) + } else { + http.Error(w, "发送查询命令失败", http.StatusInternalServerError) + } +} + +func handleIndex(w http.ResponseWriter, r *http.Request) { + page := 1 + pageSize := 20 + + if pageParam := r.URL.Query().Get("page"); pageParam != "" { + if p, err := strconv.Atoi(pageParam); err == nil && p > 0 { + page = p + } + } + + offset := (page - 1) * pageSize + + var total int + err := db.QueryRow("SELECT COUNT(*) FROM sensor_data").Scan(&total) + if err != nil { + http.Error(w, "数据库错误", http.StatusInternalServerError) + logger.Printf("统计记录错误: %v", err) + return + } + + totalPages := (total + pageSize - 1) / pageSize + + pages := []int{} + startPage := page - 2 + if startPage < 1 { + startPage = 1 + } + endPage := startPage + 4 + if endPage > totalPages { + endPage = totalPages + startPage = endPage - 4 + if startPage < 1 { + startPage = 1 + } + } + for i := startPage; i <= endPage; i++ { + pages = append(pages, i) + } + + rows, err := db.Query(` + SELECT id, timestamp, wind_speed, wind_force, wind_direction8, wind_direction360, + humidity, temperature, noise, pm25, pm10, atm_pressure, + lux20wh, lux20wl, light20w, optical_rain, compass_angle, solar_radiation + FROM sensor_data + ORDER BY timestamp DESC + LIMIT ? OFFSET ? + `, pageSize, offset) + + if err != nil { + http.Error(w, "数据库错误", http.StatusInternalServerError) + logger.Printf("查询记录错误: %v", err) + return + } + defer rows.Close() + var records []SensorData + for rows.Next() { + var id int + var data SensorData + err := rows.Scan( + &id, &data.Timestamp, &data.WindSpeed, &data.WindForce, &data.WindDirection8, &data.WindDirection360, + &data.Humidity, &data.Temperature, &data.Noise, &data.PM25, &data.PM10, &data.AtmPressure, + &data.Lux20WH, &data.Lux20WL, &data.Light20W, &data.OpticalRain, &data.CompassAngle, &data.SolarRadiation, + ) + if err != nil { + http.Error(w, "数据库错误", http.StatusInternalServerError) + logger.Printf("扫描记录错误: %v", err) + return + } + records = append(records, data) + } + + var latest SensorData + var id int + err = db.QueryRow(` + SELECT id, timestamp, wind_speed, wind_force, wind_direction8, wind_direction360, + humidity, temperature, noise, pm25, pm10, atm_pressure, + lux20wh, lux20wl, light20w, optical_rain, compass_angle, solar_radiation + FROM sensor_data + ORDER BY timestamp DESC + LIMIT 1 + `).Scan( + &id, &latest.Timestamp, &latest.WindSpeed, &latest.WindForce, &latest.WindDirection8, &latest.WindDirection360, + &latest.Humidity, &latest.Temperature, &latest.Noise, &latest.PM25, &latest.PM10, &latest.AtmPressure, + &latest.Lux20WH, &latest.Lux20WL, &latest.Light20W, &latest.OpticalRain, &latest.CompassAngle, &latest.SolarRadiation, + ) + + var latestPtr *SensorData + if err == nil { + latestPtr = &latest + } + + activeConnMutex.Lock() + connStatus := "未连接" + clientAddr := clientAddress + var lastReset time.Time + + if activeSensor != nil { + connStatus = "已连接" + lastReset = activeSensor.lastResetTime + } + activeConnMutex.Unlock() + + lastResetStr := "无记录" + if !lastReset.IsZero() { + lastResetStr = lastReset.Format("2006-01-02 15:04:05") + } + data := struct { + Records []SensorData + Latest *SensorData + Page int + PrevPage int + NextPage int + TotalPages int + Pages []int + ConnStatus string + ClientAddr string + LastReset string + }{ + Records: records, + Latest: latestPtr, + Page: page, + PrevPage: page - 1, + NextPage: page + 1, + TotalPages: totalPages, + Pages: pages, + ConnStatus: connStatus, + ClientAddr: clientAddr, + LastReset: lastResetStr, + } + + funcMap := template.FuncMap{ + "div": func(a, b int) float64 { + return float64(a) / float64(b) + }, + } + + tmpl, err := template.New("index.html").Funcs(funcMap).ParseFiles("templates/index.html") + if err != nil { + http.Error(w, "模板错误", http.StatusInternalServerError) + logger.Printf("解析模板错误: %v", err) + return + } + + if err := tmpl.Execute(w, data); err != nil { + http.Error(w, "模板错误", http.StatusInternalServerError) + logger.Printf("执行模板错误: %v", err) + return + } +}