Initial commit
This commit is contained in:
commit
a9060efb87
3
.gitignore
vendored
Normal file
3
.gitignore
vendored
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
data/*.db
|
||||||
|
data/sensor.db
|
||||||
|
logs/
|
||||||
39
data/hourly_rain_schema.sql
Normal file
39
data/hourly_rain_schema.sql
Normal file
@ -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;
|
||||||
BIN
data/sensor.db.bak
Normal file
BIN
data/sensor.db.bak
Normal file
Binary file not shown.
BIN
data/sensor.db.bak2
Normal file
BIN
data/sensor.db.bak2
Normal file
Binary file not shown.
80
data_process.go
Normal file
80
data_process.go
Normal file
@ -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])
|
||||||
|
}
|
||||||
74
database.go
Normal file
74
database.go
Normal file
@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
18
go.mod
Normal file
18
go.mod
Normal file
@ -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
|
||||||
|
)
|
||||||
47
go.sum
Normal file
47
go.sum
Normal file
@ -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=
|
||||||
224
hourly_rainfall.go
Normal file
224
hourly_rainfall.go
Normal file
@ -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
|
||||||
|
}
|
||||||
5
imports.go
Normal file
5
imports.go
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
_ "modernc.org/sqlite"
|
||||||
|
)
|
||||||
174
main.go
Normal file
174
main.go
Normal file
@ -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)
|
||||||
|
}
|
||||||
128
sensor_comm.go
Normal file
128
sensor_comm.go
Normal file
@ -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)
|
||||||
|
}
|
||||||
BIN
tcp_server
Executable file
BIN
tcp_server
Executable file
Binary file not shown.
126
templates/hourly_rainfall.html
Normal file
126
templates/hourly_rainfall.html
Normal file
@ -0,0 +1,126 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>小时雨量统计</title>
|
||||||
|
<style>
|
||||||
|
body { font-family: Arial, sans-serif; margin: 20px; }
|
||||||
|
h1 { color: #333; }
|
||||||
|
.container { max-width: 1000px; margin: 0 auto; }
|
||||||
|
table { width: 100%; border-collapse: collapse; margin: 20px 0; }
|
||||||
|
th, td { padding: 10px; text-align: left; border-bottom: 1px solid #ddd; }
|
||||||
|
th { background-color: #4CAF50; color: white; }
|
||||||
|
tr:hover { background-color: #f5f5f5; }
|
||||||
|
.rainfall-bar {
|
||||||
|
height: 20px;
|
||||||
|
background-color: #2196F3;
|
||||||
|
display: inline-block;
|
||||||
|
min-width: 3px;
|
||||||
|
}
|
||||||
|
.summary {
|
||||||
|
background-color: #e8f5e9;
|
||||||
|
padding: 15px;
|
||||||
|
border-radius: 5px;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
.no-data { color: #999; }
|
||||||
|
.hours-selector { margin: 20px 0; }
|
||||||
|
.hours-selector a {
|
||||||
|
display: inline-block;
|
||||||
|
margin-right: 10px;
|
||||||
|
padding: 5px 10px;
|
||||||
|
text-decoration: none;
|
||||||
|
background-color: #e0e0e0;
|
||||||
|
color: #333;
|
||||||
|
border-radius: 3px;
|
||||||
|
}
|
||||||
|
.hours-selector a.active {
|
||||||
|
background-color: #4CAF50;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
.nav-links {
|
||||||
|
margin: 20px 0;
|
||||||
|
}
|
||||||
|
.nav-links a {
|
||||||
|
margin-right: 15px;
|
||||||
|
text-decoration: none;
|
||||||
|
color: #2196F3;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="container">
|
||||||
|
<h1>小时雨量统计</h1>
|
||||||
|
|
||||||
|
<div class="nav-links">
|
||||||
|
<a href="/">返回首页</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="hours-selector">
|
||||||
|
显示范围:
|
||||||
|
<a href="/hourly-rainfall?hours=24" class="{{if eq .Hours 24}}active{{end}}">24小时</a>
|
||||||
|
<a href="/hourly-rainfall?hours=48" class="{{if eq .Hours 48}}active{{end}}">48小时</a>
|
||||||
|
<a href="/hourly-rainfall?hours=72" class="{{if eq .Hours 72}}active{{end}}">3天</a>
|
||||||
|
<a href="/hourly-rainfall?hours=168" class="{{if eq .Hours 168}}active{{end}}">7天</a>
|
||||||
|
<a href="/hourly-rainfall?hours=720" class="{{if eq .Hours 720}}active{{end}}">30天</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="summary">
|
||||||
|
<h2>统计摘要</h2>
|
||||||
|
{{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}}
|
||||||
|
<p>
|
||||||
|
<strong>总降雨量:</strong> {{printf "%.1f" $sum}} mm<br>
|
||||||
|
<strong>最大小时降雨量:</strong> {{printf "%.1f" $max}} mm ({{$maxTime}})<br>
|
||||||
|
<strong>统计时段:</strong> {{len .HourlyData}} 小时
|
||||||
|
</p>
|
||||||
|
{{else}}
|
||||||
|
<p class="no-data">暂无降雨数据</p>
|
||||||
|
{{end}}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h2>小时降雨详情</h2>
|
||||||
|
{{if .HourlyData}}
|
||||||
|
<table>
|
||||||
|
<tr>
|
||||||
|
<th>时间</th>
|
||||||
|
<th>降雨量(mm)</th>
|
||||||
|
<th>数据采样数</th>
|
||||||
|
<th>雨量图示</th>
|
||||||
|
</tr>
|
||||||
|
{{range .HourlyData}}
|
||||||
|
<tr>
|
||||||
|
<td>{{formatTime .HourStart}}</td>
|
||||||
|
<td>{{printf "%.1f" (divInt64 .Rainfall 10)}}</td>
|
||||||
|
<td>{{.Samples}}</td>
|
||||||
|
<td>
|
||||||
|
<div class="rainfall-bar" style="width: {{mul (divInt64 .Rainfall 10) 5}}px"
|
||||||
|
title="{{printf "%.1f" (divInt64 .Rainfall 10)}} mm"></div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{{end}}
|
||||||
|
</table>
|
||||||
|
{{else}}
|
||||||
|
<p class="no-data">所选时间段内暂无降雨记录。</p>
|
||||||
|
{{end}}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
// 添加页面自动刷新功能
|
||||||
|
setTimeout(function() {
|
||||||
|
window.location.reload();
|
||||||
|
}, 5 * 60 * 1000); // 每5分钟刷新一次
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
112
templates/index.html
Normal file
112
templates/index.html
Normal file
@ -0,0 +1,112 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<title>雨量计 DTU 数据</title>
|
||||||
|
<style>
|
||||||
|
.connected {
|
||||||
|
color: green;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
.disconnected {
|
||||||
|
color: red;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<h1>雨量计数据</h1>
|
||||||
|
|
||||||
|
<div class="conn-info">
|
||||||
|
<p>
|
||||||
|
连接状态: <span class="{{if eq .ConnStatus "已连接"}}connected{{else}}disconnected{{end}}">{{.ConnStatus}}</span> |
|
||||||
|
客户端地址: {{.ClientAddr}}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button class="refresh-btn" onclick="refreshData()">查询最新数据</button>
|
||||||
|
|
||||||
|
{{if .Latest}}
|
||||||
|
<div>
|
||||||
|
<h2>最新数据 ({{.Latest.Timestamp.Format "2006-01-02 15:04:05"}})</h2>
|
||||||
|
<p>
|
||||||
|
<strong>温度:</strong> {{printf "%.1f" (div .Latest.Temperature 10)}}°C |
|
||||||
|
<strong>湿度:</strong> {{printf "%.1f" (div .Latest.Humidity 10)}}% |
|
||||||
|
<strong>风速:</strong> {{printf "%.2f" (div .Latest.WindSpeed 100)}}m/s |
|
||||||
|
<strong>风向:</strong> {{.Latest.WindDirection360}}° |
|
||||||
|
<strong>风力:</strong> {{.Latest.WindForce}}级
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
<strong>雨量:</strong> {{printf "%.1f" (div .Latest.OpticalRain 10)}}mm |
|
||||||
|
<strong>大气压:</strong> {{printf "%.1f" (div .Latest.AtmPressure 10)}}kPa |
|
||||||
|
<strong>太阳辐射:</strong> {{.Latest.SolarRadiation}}W/m²
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
{{end}}
|
||||||
|
|
||||||
|
<h2>历史数据</h2>
|
||||||
|
<table border="1">
|
||||||
|
<tr>
|
||||||
|
<th>时间</th>
|
||||||
|
<th>温度(°C)</th>
|
||||||
|
<th>湿度(%)</th>
|
||||||
|
<th>风速(m/s)</th>
|
||||||
|
<th>风向(°)</th>
|
||||||
|
<th>风力(级)</th>
|
||||||
|
<th>雨量(mm)</th>
|
||||||
|
<th>大气压(kPa)</th>
|
||||||
|
<th>太阳辐射(W/m²)</th>
|
||||||
|
</tr>
|
||||||
|
{{range .Records}}
|
||||||
|
<tr>
|
||||||
|
<td>{{.Timestamp.Format "2006-01-02 15:04:05"}}</td>
|
||||||
|
<td>{{printf "%.1f" (div .Temperature 10)}}</td>
|
||||||
|
<td>{{printf "%.1f" (div .Humidity 10)}}</td>
|
||||||
|
<td>{{printf "%.2f" (div .WindSpeed 100)}}</td>
|
||||||
|
<td>{{.WindDirection360}}</td>
|
||||||
|
<td>{{.WindForce}}</td>
|
||||||
|
<td>{{printf "%.1f" (div .OpticalRain 10)}}</td>
|
||||||
|
<td>{{printf "%.1f" (div .AtmPressure 10)}}</td>
|
||||||
|
<td>{{.SolarRadiation}}</td>
|
||||||
|
</tr>
|
||||||
|
{{end}}
|
||||||
|
</table>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
{{if gt .Page 1}}
|
||||||
|
<a href="/?page={{.PrevPage}}">上一页</a>
|
||||||
|
{{end}}
|
||||||
|
|
||||||
|
{{range .Pages}}
|
||||||
|
{{if eq . $.Page}}
|
||||||
|
<b>{{.}}</b>
|
||||||
|
{{else}}
|
||||||
|
<a href="/?page={{.}}">{{.}}</a>
|
||||||
|
{{end}}
|
||||||
|
{{end}}
|
||||||
|
|
||||||
|
{{if lt .Page .TotalPages}}
|
||||||
|
<a href="/?page={{.NextPage}}">下一页</a>
|
||||||
|
{{end}}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
function refreshData() {
|
||||||
|
fetch('/refresh-data', {
|
||||||
|
method: 'POST'
|
||||||
|
})
|
||||||
|
.then(response => {
|
||||||
|
if(response.ok) {
|
||||||
|
window.location.reload();
|
||||||
|
} else {
|
||||||
|
alert('获取数据失败,请稍后再试');
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
console.error('Error:', error);
|
||||||
|
alert('发生错误,请稍后再试');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
47
time_utils.go
Normal file
47
time_utils.go
Normal file
@ -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
|
||||||
|
}
|
||||||
206
web.go
Normal file
206
web.go
Normal file
@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
x
Reference in New Issue
Block a user