Initial commit

This commit is contained in:
fengyarnom 2025-04-24 14:44:30 +08:00
commit a9060efb87
18 changed files with 1283 additions and 0 deletions

3
.gitignore vendored Normal file
View File

@ -0,0 +1,3 @@
data/*.db
data/sensor.db
logs/

View 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

Binary file not shown.

BIN
data/sensor.db.bak2 Normal file

Binary file not shown.

80
data_process.go Normal file
View 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
View 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
View 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
View 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
View 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
View File

@ -0,0 +1,5 @@
package main
import (
_ "modernc.org/sqlite"
)

174
main.go Normal file
View 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)
}

BIN
sensor.db Normal file

Binary file not shown.

128
sensor_comm.go Normal file
View 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

Binary file not shown.

View 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
View 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
View 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
View 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
}
}