diff --git a/.idea/.gitignore b/.idea/.gitignore new file mode 100644 index 0000000..13566b8 --- /dev/null +++ b/.idea/.gitignore @@ -0,0 +1,8 @@ +# Default ignored files +/shelf/ +/workspace.xml +# Editor-based HTTP Client requests +/httpRequests/ +# Datasource local storage ignored files +/dataSources/ +/dataSources.local.xml diff --git a/config.yaml b/config.yaml index 14bf81b..c56989c 100644 --- a/config.yaml +++ b/config.yaml @@ -7,12 +7,4 @@ database: user: "weatheruser" password: "yourpassword" dbname: "weatherdb" - sslmode: "disable" - -heartbeat: - interval: 5 - message: "Hello" - -device_check: - interval: 5 - message: "Hello" \ No newline at end of file + sslmode: "disable" \ No newline at end of file diff --git a/config/config.go b/config/config.go index a6feb23..c4b313a 100644 --- a/config/config.go +++ b/config/config.go @@ -21,21 +21,9 @@ type DatabaseConfig struct { SSLMode string `yaml:"sslmode"` } -type HeartbeatConfig struct { - Interval int `yaml:"interval"` - Message string `yaml:"message"` -} - -type DeviceCheckConfig struct { - Interval int `yaml:"interval"` - Message string `yaml:"message"` -} - type Config struct { - Server ServerConfig `yaml:"server"` - Database DatabaseConfig `yaml:"database"` - Heartbeat HeartbeatConfig `yaml:"heartbeat"` - DeviceCheck DeviceCheckConfig `yaml:"device_check"` + Server ServerConfig `yaml:"server"` + Database DatabaseConfig `yaml:"database"` } var ( diff --git a/main.go b/main.go index b88ecbe..688eff9 100644 --- a/main.go +++ b/main.go @@ -156,53 +156,9 @@ func startUDP() { } } -func startDeviceCheck() { - cfg := config.GetConfig() - ticker := time.NewTicker(time.Duration(cfg.DeviceCheck.Interval) * time.Minute) - defer ticker.Stop() - - for range ticker.C { - devices := model.GetOnlineDevices() - log.Printf("当前在线设备数: %d", len(devices)) - - for _, device := range devices { - sendUDPMessage(device.IP, cfg.DeviceCheck.Message) - } - } -} - -func sendUDPMessage(ip string, message string) { - cfg := config.GetConfig() - addr, err := net.ResolveUDPAddr("udp", fmt.Sprintf("%s:%d", ip, cfg.Server.UDPPort)) - if err != nil { - log.Printf("解析UDP地址失败: %v", err) - return - } - - log.Printf("尝试向 %s 发送消息...", addr.String()) - - conn, err := net.DialUDP("udp", nil, addr) - if err != nil { - log.Printf("连接UDP失败: %v", err) - return - } - defer conn.Close() - - conn.SetWriteDeadline(time.Now().Add(5 * time.Second)) - - n, err := conn.Write([]byte(message)) - if err != nil { - log.Printf("发送UDP消息失败: %v", err) - return - } - - log.Printf("成功向 %s 发送 %d 字节消息: %s", ip, n, message) -} - func main() { setupLogger() go startUDP() - go startDeviceCheck() r := gin.Default() r.LoadHTMLGlob("templates/*") diff --git a/model/db.go b/model/db.go index 2ff127d..caf059f 100644 --- a/model/db.go +++ b/model/db.go @@ -3,6 +3,7 @@ package model import ( "database/sql" "fmt" + "net" "time" "weatherstation/config" @@ -12,6 +13,14 @@ import ( var db *sql.DB +// DBDevice 数据库中的设备信息 +type DBDevice struct { + StationID string + IP string + LastUpdate time.Time + DeviceType DeviceType +} + func InitDB() error { cfg := config.GetConfig() @@ -39,7 +48,7 @@ func CloseDB() { } } -func ensureStationExists(stationID, password string) error { +func ensureStationExists(stationID, password string, deviceType DeviceType) error { if db == nil { return fmt.Errorf("数据库未初始化") } @@ -51,13 +60,14 @@ func ensureStationExists(stationID, password string) error { } if count == 0 { - _, err = db.Exec("INSERT INTO stations (station_id, password) VALUES ($1, $2)", stationID, password) + _, err = db.Exec("INSERT INTO stations (station_id, password, device_type) VALUES ($1, $2, $3)", + stationID, password, deviceType) if err != nil { return fmt.Errorf("添加站点失败: %v", err) } } else { - _, err = db.Exec("UPDATE stations SET password = $1, last_update = $2 WHERE station_id = $3", - password, time.Now(), stationID) + _, err = db.Exec("UPDATE stations SET password = $1, last_update = $2, device_type = $3 WHERE station_id = $4", + password, time.Now(), deviceType, stationID) if err != nil { return fmt.Errorf("更新站点失败: %v", err) } @@ -66,12 +76,74 @@ func ensureStationExists(stationID, password string) error { return nil } +// RegisterDeviceInDB 在数据库中注册设备 +func RegisterDeviceInDB(stationID string, addr net.Addr) error { + if db == nil { + return fmt.Errorf("数据库未初始化") + } + + ipStr := addr.String() + if ipStr == "" { + return fmt.Errorf("无效的IP地址") + } + + ip, _, err := net.SplitHostPort(ipStr) + if err != nil { + return fmt.Errorf("解析IP地址失败: %v", err) + } + + _, err = db.Exec(` + INSERT INTO device_ips (station_id, ip_address, last_update) + VALUES ($1, $2, $3) + ON CONFLICT (station_id) + DO UPDATE SET ip_address = $2, last_update = $3 + `, stationID, ip, time.Now()) + + if err != nil { + return fmt.Errorf("注册设备IP失败: %v", err) + } + + return nil +} + +// GetOnlineDevicesFromDB 从数据库获取在线设备 +func GetOnlineDevicesFromDB() []DBDevice { + if db == nil { + return nil + } + + rows, err := db.Query(` + SELECT d.station_id, d.ip_address, d.last_update, s.device_type + FROM device_ips d + JOIN stations s ON d.station_id = s.station_id + WHERE d.last_update > $1 + ORDER BY d.last_update DESC + `, time.Now().Add(-24*time.Hour)) + + if err != nil { + return nil + } + defer rows.Close() + + var devices []DBDevice + for rows.Next() { + var d DBDevice + err := rows.Scan(&d.StationID, &d.IP, &d.LastUpdate, &d.DeviceType) + if err != nil { + continue + } + devices = append(devices, d) + } + + return devices +} + func SaveWeatherData(data *WeatherData, rawData string) error { if db == nil { return fmt.Errorf("数据库未初始化") } - err := ensureStationExists(data.StationID, data.Password) + err := ensureStationExists(data.StationID, data.Password, DeviceTypeEcowitt) if err != nil { return err } @@ -101,3 +173,42 @@ func SaveWeatherData(data *WeatherData, rawData string) error { return nil } + +func SaveWH65LPData(data *WH65LPData, rawData []byte) error { + if db == nil { + return fmt.Errorf("数据库未初始化") + } + + // 确保设备存在,WH65LP设备没有密码 + err := ensureStationExists(data.StationID, "", DeviceTypeWH65LP) + if err != nil { + return err + } + + // 插入数据 + _, err = db.Exec(` + INSERT INTO wh65lp_data ( + station_id, timestamp, temperature, humidity, wind_direction, + wind_speed, wind_gust, rain, uv_index, light, pressure, + low_battery, wsp_flag, raw_data + ) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14)`, + data.StationID, data.Timestamp, + int(data.Temperature*10), // 温度保存为整数,精确到0.1 + data.Humidity, + data.WindDirection, + int(data.WindSpeed*100), // 风速保存为整数,精确到0.01 + int(data.WindGust*100), // 阵风保存为整数,精确到0.01 + int(data.Rain*1000), // 降雨量保存为整数,精确到0.001 + data.UV, + int(data.Light*10), // 光照保存为整数,精确到0.1 + int(data.Pressure*100), // 气压保存为整数,精确到0.01 + data.LowBattery, + data.WSPFlag, + rawData) + + if err != nil { + return fmt.Errorf("保存WH65LP数据失败: %v", err) + } + + return nil +} diff --git a/model/device.go b/model/device.go index 009485b..94ba167 100644 --- a/model/device.go +++ b/model/device.go @@ -6,35 +6,49 @@ import ( "time" ) -type Device struct { - IP string - LastSeen time.Time - StationID string +// DeviceType 设备类型枚举 +type DeviceType string + +const ( + DeviceTypeEcowitt DeviceType = "ECOWITT" + DeviceTypeWH65LP DeviceType = "WH65LP" + DeviceTypeUnknown DeviceType = "UNKNOWN" +) + +// MemoryDevice 内存中的设备信息 +type MemoryDevice struct { + IP string + LastSeen time.Time + StationID string + DeviceType DeviceType } var ( - devices = make(map[string]*Device) + devices = make(map[string]*MemoryDevice) deviceMutex sync.RWMutex ) -func RegisterDevice(stationID string, addr net.Addr) { +// UpdateDeviceInMemory 更新内存中的设备信息 +func UpdateDeviceInMemory(stationID string, addr net.Addr, deviceType DeviceType) { ip, _, _ := net.SplitHostPort(addr.String()) deviceMutex.Lock() defer deviceMutex.Unlock() - devices[stationID] = &Device{ - IP: ip, - LastSeen: time.Now(), - StationID: stationID, + devices[stationID] = &MemoryDevice{ + IP: ip, + LastSeen: time.Now(), + StationID: stationID, + DeviceType: deviceType, } } -func GetOnlineDevices() []*Device { +// GetOnlineDevicesFromMemory 获取内存中的在线设备 +func GetOnlineDevicesFromMemory() []*MemoryDevice { deviceMutex.RLock() defer deviceMutex.RUnlock() - result := make([]*Device, 0, len(devices)) + result := make([]*MemoryDevice, 0, len(devices)) for _, device := range devices { if time.Since(device.LastSeen) < 10*time.Minute { result = append(result, device) @@ -42,3 +56,14 @@ func GetOnlineDevices() []*Device { } return result } + +// GetDeviceTypeFromMemory 从内存中获取设备类型 +func GetDeviceTypeFromMemory(stationID string) DeviceType { + deviceMutex.RLock() + defer deviceMutex.RUnlock() + + if device, ok := devices[stationID]; ok { + return device.DeviceType + } + return DeviceTypeUnknown +} diff --git a/model/weather_data.go b/model/weather_data.go index 0d8c580..8878087 100644 --- a/model/weather_data.go +++ b/model/weather_data.go @@ -3,8 +3,9 @@ package model import ( "fmt" "net/url" - "regexp" "strconv" + "strings" + "time" ) type WeatherData struct { @@ -37,16 +38,28 @@ type WeatherData struct { RTFreq int } -var urlRegex = regexp.MustCompile(`/weatherstation/updateweatherstation\.php\?([^&\s]+(&[^&\s]+)*)`) - func ParseWeatherData(data string) (*WeatherData, error) { - matches := urlRegex.FindStringSubmatch(data) - if len(matches) < 2 { - return nil, fmt.Errorf("无法找到有效的气象站数据URL") + if !strings.Contains(data, "GET /weatherstation/updateweatherstation.php") { + return nil, fmt.Errorf("不是气象站数据") } - queryString := matches[1] + urlStart := strings.Index(data, "GET ") + if urlStart == -1 { + return nil, fmt.Errorf("无法找到URL开始位置") + } + httpVersionEnd := strings.Index(data, " HTTP") + if httpVersionEnd == -1 { + return nil, fmt.Errorf("无法找到URL结束位置") + } + + urlString := data[urlStart+4 : httpVersionEnd] + queryStart := strings.Index(urlString, "?") + if queryStart == -1 { + return nil, fmt.Errorf("无法找到查询参数") + } + + queryString := urlString[queryStart+1:] values, err := url.ParseQuery(queryString) if err != nil { return nil, fmt.Errorf("解析查询参数失败: %v", err) @@ -161,12 +174,12 @@ func (w *WeatherData) String() string { 风向: %d° 风速: %.2f mph (%.2f km/h) 阵风: %.2f mph (%.2f km/h) -降雨量: %.3f 英寸 (%.2f mm) -日降雨量: %.3f 英寸 (%.2f mm) -周降雨量: %.3f 英寸 (%.2f mm) -月降雨量: %.3f 英寸 (%.2f mm) -年降雨量: %.3f 英寸 (%.2f mm) -总降雨量: %.3f 英寸 (%.2f mm) +降雨量: %.3f 英寸/小时 (%.2f mm/h) - 当前降雨率 +日降雨量: %.3f 英寸 (%.2f mm) - 过去24小时累计 +周降雨量: %.3f 英寸 (%.2f mm) - 本周累计 +月降雨量: %.3f 英寸 (%.2f mm) - 本月累计 +年降雨量: %.3f 英寸 (%.2f mm) - 本年累计 +总降雨量: %.3f 英寸 (%.2f mm) - 自设备安装以来累计 太阳辐射: %.2f W/m² 紫外线指数: %d 室内温度: %.1f°F (%.1f°C) @@ -175,7 +188,8 @@ func (w *WeatherData) String() string { 相对气压: %.3f 英寸汞柱 (%.2f hPa) 低电量: %v 软件类型: %s -日期UTC: %s`, +日期UTC: %s +`, w.StationID, w.TempF, (w.TempF-32)*5/9, w.Humidity, @@ -201,3 +215,242 @@ func (w *WeatherData) String() string { w.DateUTC, ) } + +type WH65LPData struct { + StationID string // 设备ID (24位) + Timestamp time.Time // 数据时间戳 + FamilyCode byte // 家族码 (0x24) + Temperature float64 // 温度 (°C) + Humidity int // 湿度 (%) + WindDirection int // 风向 (0-359°) + WindSpeed float64 // 风速 (m/s) + WindGust float64 // 阵风 (m/s) + Rain float64 // 降雨量 (mm) + UV int // 紫外线指数 (0-13) + Light float64 // 光照 (lux) + Pressure float64 // 气压 (hPa) + LowBattery bool // 低电量标志 + WSPFlag bool // 风速标志位 +} + +// ParseWH65LPData 解析WH65LP设备的25字节数据 +func ParseWH65LPData(data []byte) (*WH65LPData, error) { + if len(data) != 25 { + return nil, fmt.Errorf("数据长度错误:期望25字节,实际%d字节", len(data)) + } + + wd := &WH65LPData{ + Timestamp: time.Now(), + } + + // 1. 解析家族码 (第1字节,bits 0-7) + wd.FamilyCode = data[0] + if wd.FamilyCode != 0x24 { + return nil, fmt.Errorf("无效的家族码:0x%02X", wd.FamilyCode) + } + + // 2. 解析设备ID (第2字节 + 第22-23字节) + idLSB := data[1] + idMSB := uint32(data[22])<<8 | uint32(data[21]) + wd.StationID = fmt.Sprintf("%06X", (idMSB<<8)|uint32(idLSB)) + + // 3. 解析风向 (bits 16-24) + windDir := uint16(data[2]) | (uint16(data[3]&0x01) << 8) + wd.WindDirection = int(windDir) + + // 4. 解析风速标志和温度 + wd.WSPFlag = (data[3]>>1)&0x01 == 1 + wd.LowBattery = (data[3]>>3)&0x01 == 1 + + // 温度 (11位,bits 28-39) + tempRaw := uint16(data[4]) | (uint16(data[5]&0x07) << 8) + wd.Temperature = float64(tempRaw-400) / 10.0 + + // 5. 湿度 (bits 40-47) + wd.Humidity = int(data[5] >> 3) + + // 6. 风速 (bits 48-55 + WSP_9,WSP_8) + windSpeedRaw := uint16(data[6]) + if !wd.WSPFlag { + // 10位风速 + windSpeedRaw |= uint16(data[3]>>6) << 8 + } + wd.WindSpeed = float64(windSpeedRaw) / 8.0 * 0.51 + + // 7. 阵风 (bits 56-63) + wd.WindGust = float64(data[7]) * 0.51 + + // 8. 降雨量 (bits 64-79) + rainRaw := uint16(data[8]) | uint16(data[9])<<8 + wd.Rain = float64(rainRaw) * 0.254 + + // 9. 紫外线 (bits 80-95) + uvRaw := uint16(data[10]) | uint16(data[11])<<8 + wd.UV = getUVIndex(uvRaw) + + // 10. 光照 (bits 96-119) + lightRaw := uint32(data[12]) | uint32(data[13])<<8 | uint32(data[14])<<16 + wd.Light = float64(lightRaw) / 10.0 + + // 11. 气压 (bits 136-159) + pressureRaw := uint32(data[17]) | uint32(data[18])<<8 | uint32(data[19])<<16 + pressureRaw &= 0x1FFFF // 只取17位 + wd.Pressure = float64(pressureRaw) / 100.0 + + return wd, nil +} + +// getUVIndex 根据UV原始值获取UV指数 +func getUVIndex(uvRaw uint16) int { + switch { + case uvRaw <= 432: + return 0 + case uvRaw <= 851: + return 1 + case uvRaw <= 1210: + return 2 + case uvRaw <= 1570: + return 3 + case uvRaw <= 2017: + return 4 + case uvRaw <= 2450: + return 5 + case uvRaw <= 2761: + return 6 + case uvRaw <= 3100: + return 7 + case uvRaw <= 3512: + return 8 + case uvRaw <= 3918: + return 9 + case uvRaw <= 4277: + return 10 + case uvRaw <= 4650: + return 11 + case uvRaw <= 5029: + return 12 + default: + return 13 + } +} + +// String 格式化输出WH65LP数据 +func (w *WH65LPData) String() string { + return fmt.Sprintf(` +设备ID: %s +温度: %.1f°C +湿度: %d%% +风向: %d° +风速: %.2f m/s +阵风: %.2f m/s +降雨量: %.3f mm +UV指数: %d +光照: %.1f lux +气压: %.2f hPa +电池状态: %v +`, + w.StationID, + w.Temperature, + w.Humidity, + w.WindDirection, + w.WindSpeed, + w.WindGust, + w.Rain, + w.UV, + w.Light, + w.Pressure, + w.LowBattery, + ) +} + +// IsWH65LPData 检查数据是否是WH65LP的数据 +func IsWH65LPData(data []byte) bool { + // 1. 检查数据长度 + if len(data) != 25 { + return false + } + + // 2. 检查家族码 + if data[0] != 0x24 { + return false + } + + // 3. 验证CRC (第16字节,前15字节的CRC8) + if !ValidateCRC8(data[:15], data[15]) { + return false + } + + // 4. 验证气压校验和 (第21字节) + pressureSum := uint8(data[17] + data[18] + data[19]) + if pressureSum != data[20] { + return false + } + + // 5. 验证CRC2 (第24字节,前23字节的CRC8) + if !ValidateCRC8(data[:23], data[23]) { + return false + } + + // 6. 验证最终校验和 (第25字节) + var sum uint8 + for i := 0; i < 24; i++ { + sum += data[i] + } + if sum != data[24] { + return false + } + + return true +} + +// ValidateCRC8 验证CRC8校验 +// 多项式:0x31,初始值:0x00 +func ValidateCRC8(data []byte, crc uint8) bool { + return crc == CalculateCRC8(data) +} + +// CalculateCRC8 计算CRC8 +func CalculateCRC8(data []byte) uint8 { + var crc uint8 + for _, b := range data { + crc ^= b + for i := 0; i < 8; i++ { + if crc&0x80 != 0 { + crc = (crc << 1) ^ 0x31 + } else { + crc <<= 1 + } + } + } + return crc +} + +// ParseUDP10006Data 解析UDP 10006端口的数据 +func ParseUDP10006Data(data []byte) (*WH65LPData, error) { + // 1. 验证数据格式 + if !IsWH65LPData(data) { + return nil, fmt.Errorf("无效的WH65LP数据格式") + } + + // 2. 解析数据 + return ParseWH65LPData(data) +} + +// ConvertToMetric 将英制单位转换为公制单位 +func ConvertToMetric(data *WeatherData) *WH65LPData { + // 用于将旧格式数据转换为WH65LP格式 + return &WH65LPData{ + StationID: data.StationID, + Timestamp: time.Now(), + Temperature: (data.TempF - 32) * 5 / 9, + Humidity: data.Humidity, + WindDirection: data.WindDir, + WindSpeed: data.WindSpeedMph * 0.44704, // mph转m/s + WindGust: data.WindGustMph * 0.44704, // mph转m/s + Rain: data.RainIn * 25.4, // inch转mm + UV: data.UV, + Light: data.SolarRadiation * 126.7, // W/m²转lux (近似转换) + Pressure: data.BarometerIn * 33.8639, // inHg转hPa + LowBattery: data.LowBattery, + } +} diff --git a/server/udp_server.go b/server/udp_server.go new file mode 100644 index 0000000..105f1a9 --- /dev/null +++ b/server/udp_server.go @@ -0,0 +1,61 @@ +package server + +import ( + "log" + "net" + "weatherstation/model" +) + +func StartUDPServer(port string) error { + addr, err := net.ResolveUDPAddr("udp", ":"+port) + if err != nil { + return err + } + + conn, err := net.ListenUDP("udp", addr) + if err != nil { + return err + } + defer conn.Close() + + log.Printf("UDP服务器已启动,监听端口 %s", port) + + buffer := make([]byte, 1024) + for { + n, remoteAddr, err := conn.ReadFromUDP(buffer) + if err != nil { + log.Printf("读取UDP数据失败: %v", err) + continue + } + + // 处理接收到的数据 + go handleUDPData(buffer[:n], remoteAddr) + } +} + +func handleUDPData(data []byte, addr *net.UDPAddr) { + // 尝试解析为WH65LP数据 + weatherData, err := model.ParseUDP10006Data(data) + if err != nil { + log.Printf("解析数据失败: %v", err) + return + } + + // 更新内存中的设备信息 + model.UpdateDeviceInMemory(weatherData.StationID, addr, model.DeviceTypeWH65LP) + + // 注册设备到数据库 + err = model.RegisterDeviceInDB(weatherData.StationID, addr) + if err != nil { + log.Printf("注册设备失败: %v", err) + } + + // 保存数据 + err = model.SaveWH65LPData(weatherData, data) + if err != nil { + log.Printf("保存数据失败: %v", err) + return + } + + log.Printf("成功接收并保存来自 %s 的WH65LP数据:\n%s", addr.String(), weatherData.String()) +} diff --git a/参考页面.html b/参考页面.html new file mode 100644 index 0000000..5ab2f8a --- /dev/null +++ b/参考页面.html @@ -0,0 +1 @@ +Hello \ No newline at end of file