Compare commits
13 Commits
feature/ne
...
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"
|
password: "yourpassword"
|
||||||
dbname: "weatherdb"
|
dbname: "weatherdb"
|
||||||
sslmode: "disable"
|
sslmode: "disable"
|
||||||
|
|
||||||
heartbeat:
|
|
||||||
interval: 5
|
|
||||||
message: "Hello"
|
|
||||||
|
|
||||||
device_check:
|
|
||||||
interval: 5
|
|
||||||
message: "Hello"
|
|
||||||
@ -21,21 +21,9 @@ type DatabaseConfig struct {
|
|||||||
SSLMode string `yaml:"sslmode"`
|
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 {
|
type Config struct {
|
||||||
Server ServerConfig `yaml:"server"`
|
Server ServerConfig `yaml:"server"`
|
||||||
Database DatabaseConfig `yaml:"database"`
|
Database DatabaseConfig `yaml:"database"`
|
||||||
Heartbeat HeartbeatConfig `yaml:"heartbeat"`
|
|
||||||
DeviceCheck DeviceCheckConfig `yaml:"device_check"`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var (
|
var (
|
||||||
|
|||||||
100
main.go
100
main.go
@ -16,8 +16,6 @@ import (
|
|||||||
|
|
||||||
"weatherstation/config"
|
"weatherstation/config"
|
||||||
"weatherstation/model"
|
"weatherstation/model"
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type UTF8Writer struct {
|
type UTF8Writer struct {
|
||||||
@ -123,7 +121,6 @@ func startUDP() {
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
rawData := buffer[:n]
|
rawData := buffer[:n]
|
||||||
data := string(rawData)
|
|
||||||
log.Printf("从 %s 接收到 %d 字节数据", addr.String(), n)
|
log.Printf("从 %s 接收到 %d 字节数据", addr.String(), n)
|
||||||
|
|
||||||
hexDump := hexDump(rawData)
|
hexDump := hexDump(rawData)
|
||||||
@ -131,17 +128,54 @@ func startUDP() {
|
|||||||
asciiDump := asciiDump(rawData)
|
asciiDump := asciiDump(rawData)
|
||||||
log.Printf("ASCII码:\n%s", asciiDump)
|
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)
|
weatherData, err := model.ParseWeatherData(data)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Printf("解析数据失败: %v", err)
|
log.Printf("解析ECOWITT数据失败: %v", err)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
log.Println("成功解析气象站数据:")
|
log.Println("成功解析ECOWITT气象站数据:")
|
||||||
log.Println(weatherData)
|
log.Println(weatherData)
|
||||||
|
|
||||||
if weatherData.StationID != "" {
|
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())
|
log.Printf("设备 %s 已注册,IP: %s", weatherData.StationID, addr.String())
|
||||||
} else {
|
} else {
|
||||||
log.Printf("警告: 收到的数据没有站点ID")
|
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() {
|
func main() {
|
||||||
setupLogger()
|
setupLogger()
|
||||||
go startUDP()
|
startUDP() // 直接运行UDP服务器,不再使用goroutine
|
||||||
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")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func hexDump(data []byte) string {
|
func hexDump(data []byte) string {
|
||||||
|
|||||||
121
model/db.go
121
model/db.go
@ -3,6 +3,7 @@ package model
|
|||||||
import (
|
import (
|
||||||
"database/sql"
|
"database/sql"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"net"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"weatherstation/config"
|
"weatherstation/config"
|
||||||
@ -12,6 +13,14 @@ import (
|
|||||||
|
|
||||||
var db *sql.DB
|
var db *sql.DB
|
||||||
|
|
||||||
|
// DBDevice 数据库中的设备信息
|
||||||
|
type DBDevice struct {
|
||||||
|
StationID string
|
||||||
|
IP string
|
||||||
|
LastUpdate time.Time
|
||||||
|
DeviceType DeviceType
|
||||||
|
}
|
||||||
|
|
||||||
func InitDB() error {
|
func InitDB() error {
|
||||||
cfg := config.GetConfig()
|
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 {
|
if db == nil {
|
||||||
return fmt.Errorf("数据库未初始化")
|
return fmt.Errorf("数据库未初始化")
|
||||||
}
|
}
|
||||||
@ -51,13 +60,14 @@ func ensureStationExists(stationID, password string) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if count == 0 {
|
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 {
|
if err != nil {
|
||||||
return fmt.Errorf("添加站点失败: %v", err)
|
return fmt.Errorf("添加站点失败: %v", err)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
_, err = db.Exec("UPDATE stations SET password = $1, last_update = $2 WHERE station_id = $3",
|
_, err = db.Exec("UPDATE stations SET password = $1, last_update = $2, device_type = $3 WHERE station_id = $4",
|
||||||
password, time.Now(), stationID)
|
password, time.Now(), deviceType, stationID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("更新站点失败: %v", err)
|
return fmt.Errorf("更新站点失败: %v", err)
|
||||||
}
|
}
|
||||||
@ -66,12 +76,74 @@ func ensureStationExists(stationID, password string) error {
|
|||||||
return nil
|
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 {
|
func SaveWeatherData(data *WeatherData, rawData string) error {
|
||||||
if db == nil {
|
if db == nil {
|
||||||
return fmt.Errorf("数据库未初始化")
|
return fmt.Errorf("数据库未初始化")
|
||||||
}
|
}
|
||||||
|
|
||||||
err := ensureStationExists(data.StationID, data.Password)
|
err := ensureStationExists(data.StationID, data.Password, DeviceTypeEcowitt)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@ -101,3 +173,42 @@ func SaveWeatherData(data *WeatherData, rawData string) error {
|
|||||||
|
|
||||||
return nil
|
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"
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Device struct {
|
// DeviceType 设备类型枚举
|
||||||
IP string
|
type DeviceType string
|
||||||
LastSeen time.Time
|
|
||||||
StationID 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 (
|
var (
|
||||||
devices = make(map[string]*Device)
|
devices = make(map[string]*MemoryDevice)
|
||||||
deviceMutex sync.RWMutex
|
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())
|
ip, _, _ := net.SplitHostPort(addr.String())
|
||||||
|
|
||||||
deviceMutex.Lock()
|
deviceMutex.Lock()
|
||||||
defer deviceMutex.Unlock()
|
defer deviceMutex.Unlock()
|
||||||
|
|
||||||
devices[stationID] = &Device{
|
devices[stationID] = &MemoryDevice{
|
||||||
IP: ip,
|
IP: ip,
|
||||||
LastSeen: time.Now(),
|
LastSeen: time.Now(),
|
||||||
StationID: stationID,
|
StationID: stationID,
|
||||||
|
DeviceType: deviceType,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func GetOnlineDevices() []*Device {
|
// GetOnlineDevicesFromMemory 获取内存中的在线设备
|
||||||
|
func GetOnlineDevicesFromMemory() []*MemoryDevice {
|
||||||
deviceMutex.RLock()
|
deviceMutex.RLock()
|
||||||
defer deviceMutex.RUnlock()
|
defer deviceMutex.RUnlock()
|
||||||
|
|
||||||
result := make([]*Device, 0, len(devices))
|
result := make([]*MemoryDevice, 0, len(devices))
|
||||||
for _, device := range devices {
|
for _, device := range devices {
|
||||||
if time.Since(device.LastSeen) < 10*time.Minute {
|
if time.Since(device.LastSeen) < 10*time.Minute {
|
||||||
result = append(result, device)
|
result = append(result, device)
|
||||||
@ -42,3 +56,14 @@ func GetOnlineDevices() []*Device {
|
|||||||
}
|
}
|
||||||
return result
|
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 (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/url"
|
"net/url"
|
||||||
"regexp"
|
|
||||||
"strconv"
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
type WeatherData struct {
|
type WeatherData struct {
|
||||||
@ -37,16 +38,28 @@ type WeatherData struct {
|
|||||||
RTFreq int
|
RTFreq int
|
||||||
}
|
}
|
||||||
|
|
||||||
var urlRegex = regexp.MustCompile(`/weatherstation/updateweatherstation\.php\?([^&\s]+(&[^&\s]+)*)`)
|
|
||||||
|
|
||||||
func ParseWeatherData(data string) (*WeatherData, error) {
|
func ParseWeatherData(data string) (*WeatherData, error) {
|
||||||
matches := urlRegex.FindStringSubmatch(data)
|
if !strings.Contains(data, "GET /weatherstation/updateweatherstation.php") {
|
||||||
if len(matches) < 2 {
|
return nil, fmt.Errorf("不是气象站数据")
|
||||||
return nil, fmt.Errorf("无法找到有效的气象站数据URL")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
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)
|
values, err := url.ParseQuery(queryString)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("解析查询参数失败: %v", err)
|
return nil, fmt.Errorf("解析查询参数失败: %v", err)
|
||||||
@ -161,12 +174,12 @@ func (w *WeatherData) String() string {
|
|||||||
风向: %d°
|
风向: %d°
|
||||||
风速: %.2f mph (%.2f km/h)
|
风速: %.2f mph (%.2f km/h)
|
||||||
阵风: %.2f mph (%.2f km/h)
|
阵风: %.2f mph (%.2f km/h)
|
||||||
降雨量: %.3f 英寸 (%.2f mm)
|
降雨量: %.3f 英寸/小时 (%.2f mm/h) - 当前降雨率
|
||||||
日降雨量: %.3f 英寸 (%.2f mm)
|
日降雨量: %.3f 英寸 (%.2f mm) - 过去24小时累计
|
||||||
周降雨量: %.3f 英寸 (%.2f mm)
|
周降雨量: %.3f 英寸 (%.2f mm) - 本周累计
|
||||||
月降雨量: %.3f 英寸 (%.2f mm)
|
月降雨量: %.3f 英寸 (%.2f mm) - 本月累计
|
||||||
年降雨量: %.3f 英寸 (%.2f mm)
|
年降雨量: %.3f 英寸 (%.2f mm) - 本年累计
|
||||||
总降雨量: %.3f 英寸 (%.2f mm)
|
总降雨量: %.3f 英寸 (%.2f mm) - 自设备安装以来累计
|
||||||
太阳辐射: %.2f W/m²
|
太阳辐射: %.2f W/m²
|
||||||
紫外线指数: %d
|
紫外线指数: %d
|
||||||
室内温度: %.1f°F (%.1f°C)
|
室内温度: %.1f°F (%.1f°C)
|
||||||
@ -175,7 +188,8 @@ func (w *WeatherData) String() string {
|
|||||||
相对气压: %.3f 英寸汞柱 (%.2f hPa)
|
相对气压: %.3f 英寸汞柱 (%.2f hPa)
|
||||||
低电量: %v
|
低电量: %v
|
||||||
软件类型: %s
|
软件类型: %s
|
||||||
日期UTC: %s`,
|
日期UTC: %s
|
||||||
|
`,
|
||||||
w.StationID,
|
w.StationID,
|
||||||
w.TempF, (w.TempF-32)*5/9,
|
w.TempF, (w.TempF-32)*5/9,
|
||||||
w.Humidity,
|
w.Humidity,
|
||||||
@ -201,3 +215,281 @@ func (w *WeatherData) String() string {
|
|||||||
w.DateUTC,
|
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