Compare commits
13 Commits
develop-an
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 47a290207c | |||
| 03e88da686 | |||
| 9a0117f84f | |||
| 017d6489f1 | |||
| eb69a2be61 | |||
| 5f0aed7258 | |||
| 686cf8847e | |||
| 67fe281c0a | |||
| 53cea255ee | |||
| d4bd14a91f | |||
| 43e71e4dd6 | |||
| 9a7d2b5f79 | |||
| c09b3e789a |
8
.idea/.gitignore
generated
vendored
Normal file
8
.idea/.gitignore
generated
vendored
Normal file
@ -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
|
||||
@ -8,11 +8,3 @@ database:
|
||||
password: "yourpassword"
|
||||
dbname: "weatherdb"
|
||||
sslmode: "disable"
|
||||
|
||||
heartbeat:
|
||||
interval: 5
|
||||
message: "Hello"
|
||||
|
||||
device_check:
|
||||
interval: 5
|
||||
message: "Hello"
|
||||
@ -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 (
|
||||
|
||||
100
main.go
100
main.go
@ -16,8 +16,6 @@ import (
|
||||
|
||||
"weatherstation/config"
|
||||
"weatherstation/model"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
type UTF8Writer struct {
|
||||
@ -123,7 +121,6 @@ func startUDP() {
|
||||
continue
|
||||
}
|
||||
rawData := buffer[:n]
|
||||
data := string(rawData)
|
||||
log.Printf("从 %s 接收到 %d 字节数据", addr.String(), n)
|
||||
|
||||
hexDump := hexDump(rawData)
|
||||
@ -131,17 +128,54 @@ func startUDP() {
|
||||
asciiDump := asciiDump(rawData)
|
||||
log.Printf("ASCII码:\n%s", asciiDump)
|
||||
|
||||
// 首先尝试解析为WH65LP数据
|
||||
if len(rawData) == 25 && rawData[0] == 0x24 {
|
||||
wh65lpData, err := model.ParseWH65LPData(rawData)
|
||||
if err != nil {
|
||||
log.Printf("解析WH65LP数据失败: %v", err)
|
||||
} else {
|
||||
log.Println("成功解析WH65LP气象站数据:")
|
||||
log.Println(wh65lpData)
|
||||
|
||||
// 更新内存中的设备信息
|
||||
model.UpdateDeviceInMemory(wh65lpData.StationID, addr, model.DeviceTypeWH65LP)
|
||||
// 注册设备到数据库
|
||||
err = model.RegisterDeviceInDB(wh65lpData.StationID, addr)
|
||||
if err != nil {
|
||||
log.Printf("注册设备失败: %v", err)
|
||||
}
|
||||
log.Printf("设备 %s 已注册,IP: %s", wh65lpData.StationID, addr.String())
|
||||
|
||||
// 保存数据
|
||||
err = model.SaveWH65LPData(wh65lpData, rawData)
|
||||
if err != nil {
|
||||
log.Printf("保存数据到数据库失败: %v", err)
|
||||
} else {
|
||||
log.Printf("数据已成功保存到数据库")
|
||||
}
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
// 如果不是WH65LP数据,尝试解析为ECOWITT数据
|
||||
data := string(rawData)
|
||||
weatherData, err := model.ParseWeatherData(data)
|
||||
if err != nil {
|
||||
log.Printf("解析数据失败: %v", err)
|
||||
log.Printf("解析ECOWITT数据失败: %v", err)
|
||||
continue
|
||||
}
|
||||
|
||||
log.Println("成功解析气象站数据:")
|
||||
log.Println("成功解析ECOWITT气象站数据:")
|
||||
log.Println(weatherData)
|
||||
|
||||
if weatherData.StationID != "" {
|
||||
model.RegisterDevice(weatherData.StationID, addr)
|
||||
// 更新内存中的设备信息
|
||||
model.UpdateDeviceInMemory(weatherData.StationID, addr, model.DeviceTypeEcowitt)
|
||||
// 注册设备到数据库
|
||||
err = model.RegisterDeviceInDB(weatherData.StationID, addr)
|
||||
if err != nil {
|
||||
log.Printf("注册设备失败: %v", err)
|
||||
}
|
||||
log.Printf("设备 %s 已注册,IP: %s", weatherData.StationID, addr.String())
|
||||
} else {
|
||||
log.Printf("警告: 收到的数据没有站点ID")
|
||||
@ -156,61 +190,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/*")
|
||||
r.Static("/static", "static")
|
||||
r.GET("/", func(c *gin.Context) {
|
||||
c.HTML(200, "index.html", gin.H{})
|
||||
})
|
||||
r.Run(":10007")
|
||||
startUDP() // 直接运行UDP服务器,不再使用goroutine
|
||||
}
|
||||
|
||||
func hexDump(data []byte) string {
|
||||
|
||||
121
model/db.go
121
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
|
||||
}
|
||||
|
||||
@ -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
|
||||
}
|
||||
|
||||
@ -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,281 @@ 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 // 风速标志位
|
||||
}
|
||||
|
||||
// 辅助函数:将byte转换为二进制字符串
|
||||
func byteToBinary(b byte) string {
|
||||
return fmt.Sprintf("%08b", b)
|
||||
}
|
||||
|
||||
// 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字节)
|
||||
wd.FamilyCode = data[0]
|
||||
fmt.Printf("\n=== 字段解析详情 ===\n")
|
||||
fmt.Printf("1. 家族码: 0x%02X\n", wd.FamilyCode)
|
||||
|
||||
// 2. 设备ID (字节22,23,2)
|
||||
// ID = ID_HSB(22) + ID_MSB(23) + ID_LSB(2)
|
||||
// 按照文档顺序:00 2A 36
|
||||
id := uint32(0)<<16 | uint32(data[22])<<8 | uint32(data[1])
|
||||
wd.StationID = fmt.Sprintf("%06X", id)
|
||||
fmt.Printf("2. 设备ID: 0x%s (字节22=0x%02X, 字节21=0x%02X, 字节1=0x%02X)\n",
|
||||
wd.StationID, data[22], data[21], data[1])
|
||||
|
||||
// 3. 风向 (字节3,4)
|
||||
// 9位数据,直接转换为十进制即为角度
|
||||
windDir := uint16(data[2])
|
||||
wd.WindDirection = int(windDir)
|
||||
fmt.Printf("3. 风向: 0x%02X = %d°\n", windDir, wd.WindDirection)
|
||||
|
||||
// 4. 温度 (字节4,5)
|
||||
// 温度原始值 = 0x296 = 662
|
||||
// 计算公式:(662-400)/10 = 26.2
|
||||
tempRaw := uint16(data[3])<<8 | uint16(data[4])
|
||||
wd.Temperature = float64(tempRaw-400) / 10.0
|
||||
fmt.Printf("4. 温度: 0x%03X = %.1f°C\n", tempRaw, wd.Temperature)
|
||||
|
||||
// 5. 湿度 (字节6)
|
||||
// 直接转换为十进制
|
||||
wd.Humidity = int(data[5])
|
||||
fmt.Printf("5. 湿度: 0x%02X = %d%%\n", data[5], wd.Humidity)
|
||||
|
||||
// 6. 风速 (字节7)
|
||||
// 计算公式:原始值/8*0.51
|
||||
windSpeedRaw := uint16(data[6])
|
||||
wd.WindSpeed = float64(windSpeedRaw) / 8.0 * 0.51
|
||||
fmt.Printf("6. 风速: 0x%02X = %.2f m/s\n", windSpeedRaw, wd.WindSpeed)
|
||||
|
||||
// 7. 阵风 (字节8)
|
||||
// 计算公式:原始值*0.51
|
||||
wd.WindGust = float64(data[7]) * 0.51
|
||||
fmt.Printf("7. 阵风: 0x%02X = %.2f m/s\n", data[7], wd.WindGust)
|
||||
|
||||
// 8. 降雨量 (字节9,10)
|
||||
// 计算公式:原始值*0.254
|
||||
rainRaw := uint16(data[8])<<8 | uint16(data[9])
|
||||
wd.Rain = float64(rainRaw) * 0.254
|
||||
fmt.Printf("8. 降雨量: 0x%04X = %.3f mm\n", rainRaw, wd.Rain)
|
||||
|
||||
// 9. 紫外线 (字节11,12)
|
||||
uvRaw := uint16(data[10])<<8 | uint16(data[11])
|
||||
wd.UV = getUVIndex(uvRaw)
|
||||
fmt.Printf("9. 紫外线: 0x%04X = %d\n", uvRaw, wd.UV)
|
||||
|
||||
// 10. 光照 (字节13-15)
|
||||
// 计算公式:原始值/10
|
||||
lightRaw := uint32(data[12])<<16 | uint32(data[13])<<8 | uint32(data[14])
|
||||
wd.Light = float64(lightRaw) / 10.0
|
||||
fmt.Printf("10. 光照: 0x%06X = %.1f lux\n", lightRaw, wd.Light)
|
||||
|
||||
// 11. 气压 (字节18-20)
|
||||
// 计算公式:原始值/100
|
||||
pressureRaw := uint32(data[17])<<16 | uint32(data[18])<<8 | uint32(data[19])
|
||||
wd.Pressure = float64(pressureRaw) / 100.0
|
||||
fmt.Printf("11. 气压: 0x%06X = %.2f hPa\n", pressureRaw, wd.Pressure)
|
||||
|
||||
return wd, nil
|
||||
}
|
||||
|
||||
// 调试辅助函数
|
||||
func getBits(data []byte, startBit, length int) uint32 {
|
||||
var result uint32
|
||||
startByte := startBit / 8
|
||||
startBitInByte := startBit % 8
|
||||
|
||||
// 从起始字节开始读取
|
||||
result = uint32(data[startByte]) >> startBitInByte
|
||||
bitsGot := 8 - startBitInByte
|
||||
|
||||
// 如果需要更多位,继续读取后续字节
|
||||
for bitsGot < length {
|
||||
startByte++
|
||||
result |= uint32(data[startByte]) << bitsGot
|
||||
bitsGot += 8
|
||||
}
|
||||
|
||||
// 只保留需要的位数
|
||||
result &= (1 << length) - 1
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
// 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,
|
||||
}
|
||||
}
|
||||
|
||||
61
server/udp_server.go
Normal file
61
server/udp_server.go
Normal file
@ -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())
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user