找回密码
 立即注册
搜索
查看: 7932|回复: 112

[交流学习] 志愿气象观测站搭建过程记录

[复制链接]

3

主题

190

回帖

433

积分

热带低压

积分
433
发表于 2024-8-5 15:14 | 显示全部楼层 |阅读模式
本帖最后由 浅冰Column 于 2024-8-30 06:43 编辑

本人很早就想有一个自己的气象观测站,最近又了解到志愿气象观测站这一站种,更是蠢蠢欲动(划掉)
项目名为”冰点点“,取冰点0℃之意(其实灵感来自我同学外号)
本项目8月1号开始构思,8月3号立项,原定明年一月份开始搭建,但8月4日好友得知后大力投资,令本人感动无比,随即于今天,也就是8月5号正式开始搭建

评分

参与人数 3金钱 +2000 威望 +20 收起 理由
marktube_1 + 20 原创帖
Liv. + 1000 赞一个!
理可的呆萌呆毛 + 1000 TY_Board論壇第1000個主題貼!

查看全部评分

3

主题

190

回帖

433

积分

热带低压

积分
433
 楼主| 发表于 2024-8-5 15:52 | 显示全部楼层
本帖最后由 浅冰Column 于 2024-8-26 11:28 编辑

自动站配置单(要素中,背景色置绿表示达到国标,置黄表示接近国标)

(24/08/25:注:立柱高度改为3m,雨量计分辨率改为0.1mm)
(24/08/10:略改方案,重新传图)

附参考国标:GB/T 33703-2017《自动气象站观测规范》,来自中国气象局官网

本帖子中包含更多资源

您需要 登录 才可以下载或查看,没有账号?立即注册

×

点评

有没有发现这个帖子是论坛第1000帖。  发表于 2024-8-5 16:19

3

主题

190

回帖

433

积分

热带低压

积分
433
 楼主| 发表于 2024-8-6 12:04 | 显示全部楼层
福州市气象局回复了我关于志愿站申请的咨询

本帖子中包含更多资源

您需要 登录 才可以下载或查看,没有账号?立即注册

×

3

主题

190

回帖

433

积分

热带低压

积分
433
 楼主| 发表于 2024-8-6 18:08 | 显示全部楼层
本帖最后由 浅冰Column 于 2024-8-12 11:07 编辑

基于 Python 的气象要素数据获取程序,通过 Modbus-RTU 协议在 RS-485 上与传感器通信。
异步采集和保存传感器的风速、风向、温度、湿度、气压和降水量数据,并于每分钟、每小时产生时段数据。
记录日志,各传感器配置可独立自定义,程序模块化。
(24/08/12:更新程序)


程序代码

代码内关于单位矢量平均法计算平均风速的部分,我在教程区新开了一帖《使用单位矢量平均法计算平均风向(公式、代码)》,供各位参考。
其他详见代码内注释。
  1. import os
  2. import json
  3. import signal
  4. import logging
  5. import asyncio
  6. from logging.handlers import TimedRotatingFileHandler
  7. from datetime import datetime, timedelta
  8. from pymodbus.client import AsyncModbusSerialClient as ModbusClient
  9. from pymodbus.exceptions import ModbusException, ConnectionException
  10. from dataclasses import dataclass, asdict
  11. import math

  12. # Modbus 连接配置
  13. PORTS = {
  14.     "wind": {
  15.         "port": "/dev/ttyUSB0",
  16.         "baudrate": 4800,
  17.         "stopbits": 1,
  18.         "bytesize": 8,
  19.         "timeout": 3,
  20.     },
  21.     "wind_dir": {
  22.         "port": "/dev/ttyUSB1",
  23.         "baudrate": 4800,
  24.         "stopbits": 2,
  25.         "bytesize": 7,
  26.         "timeout": 3,
  27.     },
  28.     "thp": {
  29.         "port": "/dev/ttyUSB2",
  30.         "baudrate": 9600,
  31.         "stopbits": 1,
  32.         "bytesize": 8,
  33.         "timeout": 3,
  34.     },
  35.     "rain": {
  36.         "port": "/dev/ttyUSB3",
  37.         "baudrate": 4800,
  38.         "stopbits": 1,
  39.         "bytesize": 8,
  40.         "timeout": 3,
  41.     },
  42. }
  43. LOG_FILE = "sensors.log"
  44. MINUTE_DATA_DIR = "/data/minute"
  45. HOUR_DATA_DIR = "/data/hour"

  46. # 日志配置,使用按日切割的文件处理器记录日志
  47. logging.basicConfig(
  48.     handlers=[TimedRotatingFileHandler(LOG_FILE, when="D")],
  49.     level=logging.DEBUG,
  50.     format="[%(asctime)s][%(name)s][%(levelname)s] %(message)s",
  51. )


  52. # SIGINT 退出信号捕获配置
  53. def handle_sigint(signal_number, frame):
  54.     for task in asyncio.all_tasks():
  55.         task.cancel()
  56.     loop = asyncio.get_event_loop()
  57.     loop.stop()


  58. # 数据类,用于存储传感器数据
  59. @dataclass
  60. class SensorData:
  61.     wind_speed_avg: float
  62.     wind_direction_avg: float
  63.     max_wind_speed: float
  64.     max_wind_speed_direction: float
  65.     temperature: float
  66.     humidity: float
  67.     pressure: float
  68.     rainfall: float


  69. # Modbus 客户端交互类
  70. class ModbusClientHandler:
  71.     def __init__(self, port, baudrate, stopbits, bytesize, timeout):
  72.         self.port = port
  73.         self.baudrate = baudrate
  74.         self.stopbits = stopbits
  75.         self.bytesize = bytesize
  76.         self.timeout = timeout
  77.         self.client = None

  78.     # 异步上下文管理器,用于连接和断开 Modbus 客户端
  79.     async def __aenter__(self):
  80.         self.client = await ModbusClient(
  81.             port=self.port,
  82.             baudrate=self.baudrate,
  83.             stopbits=self.stopbits,
  84.             bytesize=self.bytesize,
  85.             timeout=self.timeout,
  86.             method="rtu",
  87.         )
  88.         if not self.client.protocol:
  89.             raise ConnectionException(f"无法连接到 {self.port}")
  90.         return self

  91.     async def __aexit__(self, exc_type, exc, tb):
  92.         if self.client:
  93.             await self.client.close()

  94.     # 通用读寄存器方法
  95.     async def read_registers(self, address, count, slave=1):
  96.         response = await self.client.read_holding_registers(address, count, slave)
  97.         if response.isError():
  98.             logging.error(f"Modbus 错误: {response}")
  99.             return ["/"] * count
  100.         return response.registers

  101.     # 获取风速和风级数据
  102.     async def get_wind_speed_scale(self):
  103.         data = await self.read_registers(address=0x00, count=0x02)
  104.         return {"wind_speed": data[0] * 0.1, "wind_scale": data[1]}

  105.     # 获取风向数据
  106.     async def get_wind_direction(self):
  107.         data = await self.read_registers(address=0x00, count=0x02)
  108.         return {"wind_direction": data[0]}

  109.     # 获取温度、湿度、气压数据
  110.     async def get_temperature_humidity_pressure(self):
  111.         data = await self.read_registers(address=0x00, count=0x03)
  112.         return {
  113.             "temperature": data[0] * 0.1,
  114.             "humidity": data[1] * 0.1,
  115.             "pressure": data[2] * 0.1,
  116.         }

  117.     # 获取降雨量数据
  118.     async def get_rainfall(self):
  119.         data = await self.read_registers(address=0x00, count=0x01)
  120.         return {"rainfall": data[0] * 0.1}


  121. # 单位矢量平均法计算平均风向
  122. def calculate_average_wind_direction(wind_directions):
  123.     # 转为弧度制
  124.     wind_directions_rad = [math.radians(d) for d in wind_directions]

  125.     # 计算平均值
  126.     X_avg = sum(math.sin(d) for d in wind_directions_rad) / len(wind_directions)
  127.     Y_avg = sum(math.cos(d) for d in wind_directions_rad) / len(wind_directions)

  128.     # 计算风向平均值的弧度
  129.     average_wind_direction_rad = math.atan2(X_avg, Y_avg)

  130.     # 转回角度制
  131.     average_wind_direction_deg = math.degrees(average_wind_direction_rad)

  132.     # 出界修正
  133.     if average_wind_direction_deg < 0:
  134.         average_wind_direction_deg += 360

  135.     return average_wind_direction_deg


  136. # 获取并处理分钟实况数据
  137. async def collect_minute_data(clients):
  138.     wind_speed_data, wind_direction_data = [], []
  139.     temperature_data, humidity_data, pressure_data, rainfall_data = [], [], [], []

  140.     for c in range(60):
  141.         wind = await clients["wind"].get_wind_speed_scale()  # 每秒采样一次风速和风级数据
  142.         wind_speed_data.append(wind["wind_speed"])

  143.         wind_dir = await clients["wind_dir"].get_wind_direction()  # 每秒采样一次风向数据
  144.         wind_direction_data.append(wind_dir["wind_direction"])

  145.         if c % 10 == 0:  # 每6秒采样一次温度、湿度、气压数据
  146.             thp = await clients["thp"].get_temperature_humidity_pressure()
  147.             temperature_data.append(thp["temperature"])
  148.             humidity_data.append(thp["humidity"])
  149.             pressure_data.append(thp["pressure"])

  150.         if c == 0:  # 每分钟采样一次降雨量数据
  151.             rainfall = await clients["rain"].get_rainfall()
  152.             rainfall_data.append(rainfall["rainfall"])

  153.         await asyncio.sleep(1)

  154.     # 计算各种平均值和最大值
  155.     wind_speed_avg = sum(wind_speed_data) / len(wind_speed_data)
  156.     wind_direction_avg = calculate_average_wind_direction(wind_direction_data)
  157.     max_wind_speed = max(wind_speed_data)
  158.     max_wind_speed_direction = wind_direction_data[wind_speed_data.index(max_wind_speed)]

  159.     # 返回分钟数据
  160.     return SensorData(
  161.         wind_speed_avg=wind_speed_avg,
  162.         wind_direction_avg=wind_direction_avg,
  163.         max_wind_speed=max_wind_speed,
  164.         max_wind_speed_direction=max_wind_speed_direction,
  165.         temperature=sum(temperature_data) / len(temperature_data),
  166.         humidity=sum(humidity_data) / len(humidity_data),
  167.         pressure=sum(pressure_data) / len(pressure_data),
  168.         rainfall=sum(rainfall_data) / len(rainfall_data),
  169.     )


  170. # 获取并保存分钟实况数据到本地
  171. async def save_minute_data(clients):
  172.     while True:
  173.         data = await collect_minute_data(clients)  # 获取分钟数据
  174.         now = datetime.now()
  175.         file_name = now.strftime("%Y-%m-%d %H:%M")  # 生成文件名
  176.         directory = MINUTE_DATA_DIR

  177.         if not os.path.exists(directory):
  178.             os.makedirs(directory)  # 如果目录不存在,则创建目录

  179.         try:
  180.             # 将数据保存为JSON格式文件
  181.             with open(os.path.join(directory, file_name), "w") as file:
  182.                 json.dump(asdict(data), file, indent=4)
  183.         except Exception as e:
  184.             logging.error(f"文件错误: {e}")

  185.         await asyncio.sleep(60)  # 每分钟获取并保存一次


  186. # 根据时段内分钟实况数据产生小时数据
  187. async def collect_hourly_data():
  188.     now = datetime.now()
  189.     hour_start = now.replace(minute=0, second=0, microsecond=0)  # 获取当前小时的起始时间
  190.     file_names = [
  191.         (hour_start + timedelta(minutes=i)).strftime("%Y-%m-%d %H:%M") for i in range(60)
  192.     ]  # 生成当前小时内所有分钟文件的名称
  193.     directory = MINUTE_DATA_DIR
  194.     data_points = []

  195.     for file_name in file_names:
  196.         file_path = os.path.join(directory, file_name)
  197.         if os.path.exists(file_path):
  198.             try:
  199.                 with open(file_path, "r") as file:
  200.                     data_points.append(json.load(file))  # 读取并加载分钟数据
  201.             except Exception as e:
  202.                 logging.error(f"文件错误: {e}")

  203.     if len(data_points) < 60:
  204.         return None  # 如果数据点不足60个,则跳过本次(不产生小时数据)

  205.     # 计算各种数据的平均值和最大值
  206.     wind_speed = [d["wind_speed_avg"] for d in data_points]
  207.     wind_direction = [d["wind_direction_avg"] for d in data_points]
  208.     max_wind_speed = max([d["max_wind_speed"] for d in data_points])
  209.     max_wind_speed_direction = data_points[[d["max_wind_speed"] for d in data_points].index(max_wind_speed)][
  210.         "max_wind_speed_direction"
  211.     ]

  212.     # 返回小时数据
  213.     return {
  214.         "wind_speed_avg_2min": (sum(wind_speed[-2:]) / 2 if len(wind_speed) >= 2 else "/"),
  215.         "wind_speed_avg_10min": (sum(wind_speed[-10:]) / 10 if len(wind_speed) >= 10 else "/"),
  216.         "wind_direction_avg_2min": (
  217.             calculate_average_wind_direction(wind_direction[-2:]) if len(wind_direction) >= 2 else "/"
  218.         ),
  219.         "wind_direction_avg_10min": (
  220.             calculate_average_wind_direction(wind_direction[-10:]) if len(wind_direction) >= 10 else "/"
  221.         ),
  222.         "max_wind_speed": max_wind_speed,
  223.         "max_wind_speed_direction": max_wind_speed_direction,
  224.         "temperature_avg": sum([d["temperature"] for d in data_points]) / len(data_points),
  225.         "humidity_avg": sum([d["humidity"] for d in data_points]) / len(data_points),
  226.         "pressure_avg": sum([d["pressure"] for d in data_points]) / len(data_points),
  227.         "rainfall_avg": sum([d["rainfall"] for d in data_points]) / len(data_points),
  228.     }


  229. # 获取并保存小时数据到本地
  230. async def save_hourly_data():
  231.     while True:
  232.         await asyncio.sleep(3600)  # 每小时获取并保存一次
  233.         data = await collect_hourly_data()
  234.         if data is None:
  235.             logging.warning("数据点不足,跳过本次小时数据生成。")
  236.             continue

  237.         now = datetime.now()
  238.         file_name = now.strftime("%Y-%m-%d %H")  # 生成小时文件名
  239.         directory = HOUR_DATA_DIR

  240.         if not os.path.exists(directory):
  241.             os.makedirs(directory)  # 如果目录不存在,则创建目录

  242.         try:
  243.             # 将小时数据保存为JSON格式文件
  244.             with open(os.path.join(directory, file_name), "w") as file:
  245.                 json.dump(data, file, indent=4)
  246.         except Exception as e:
  247.             logging.error(f"文件错误: {e}")


  248. # 主函数
  249. async def main():
  250.     # 注册 SIGINT 退出信号处理器
  251.     signal.signal(signal.SIGINT, handle_sigint)

  252.     # 采用异步上下文管理器
  253.     async with asyncio.TaskGroup() as tg:
  254.         clients = {key: await tg.start(ModbusClientHandler(**port)) for key, port in PORTS.items()}

  255.     # 创建并启动任务
  256.     minute_task = asyncio.create_task(save_minute_data(clients))
  257.     hourly_task = asyncio.create_task(save_hourly_data())

  258.     # 等待所有任务完成
  259.     try:
  260.         await asyncio.gather(minute_task, hourly_task)
  261.     except asyncio.CancelledError:
  262.         logging.info("正在退出")


  263. if __name__ == "__main__":
  264.     try:
  265.         asyncio.run(main())  # 运行主程序
  266.     except (ConnectionException, ModbusException) as e:
  267.         logging.error(f"Modbus 错误: {e}")
复制代码

点评

哦国标就这么写的 那没事了  发表于 2024-8-6 20:03
这数据直接写到本地文件里是不是不大好 😰  发表于 2024-8-6 19:56

0

主题

3

回帖

80

积分

热带扰动-TCFA

积分
80
发表于 2024-8-6 19:15 | 显示全部楼层
浅冰Column 发表于 2024-8-6 18:08
基于 Python 的气象要素数据获取程序,通过 Modbus-RTU 协议在 RS-485 上与传感器通信。
采用异步来采集和 ...

急急急急急, 速速补代码

点评

补了  发表于 2024-8-6 19:48
好好好  发表于 2024-8-6 19:16

评分

参与人数 1威望 +25 收起 理由
红豆棒冰冰 + 25 欢迎新人

查看全部评分

3

主题

190

回帖

433

积分

热带低压

积分
433
 楼主| 发表于 2024-8-10 20:52 | 显示全部楼层
陆陆续续到货了,再过两天到齐了就开始组装

0

主题

12

回帖

300

积分

热带低压

积分
300
发表于 2024-8-10 21:02 | 显示全部楼层
点个赞!

0

主题

12

回帖

297

积分

热带低压

积分
297
发表于 2024-8-11 02:42 | 显示全部楼层
最贵的居然是钢管么哈哈哈,第一次听说,期待更新

3

主题

190

回帖

433

积分

热带低压

积分
433
 楼主| 发表于 2024-8-11 16:49 | 显示全部楼层
本帖最后由 浅冰Column 于 2024-8-12 11:08 编辑

测试供电

先爆改一个 12v 电源作为测试时的供电

本帖子中包含更多资源

您需要 登录 才可以下载或查看,没有账号?立即注册

×

3

主题

190

回帖

433

积分

热带低压

积分
433
 楼主| 发表于 2024-8-11 17:06 | 显示全部楼层
本帖最后由 浅冰Column 于 2024-8-11 17:40 编辑

测试风向传感器

将风向传感器的电源正负接入刚刚爆改的测试供电
传感器的 RS485 正负接入 RS485 转 USB 转换器,转换器接入电脑
(因为懒而采用了质量极低的接线手法)


接线示意图



风向传感器测试代码
  1. from pymodbus.client import ModbusSerialClient as ModbusClient
  2. import time

  3. # 初始化客户端
  4. client = ModbusClient(port="com4", baudrate=4800, timeout=3, stopbits=1, bytesize=8)


  5. # 读取风向
  6. def read_wind_direction(client):
  7.     # 查询传感器
  8.     result = client.read_holding_registers(address=0x00, count=0x02, slave=1)
  9.     # 原数缩小 1/10 得到数据
  10.     wind_direction = result.registers[0] * 0.1
  11.     # 四舍五入至小数点后一位
  12.     return round(wind_direction, 1)


  13. def main():
  14.     # 连接传感器
  15.     client.connect()
  16.     try:
  17.         while True:
  18.             # 每 0.1 秒获取一次数据
  19.             wind_direction = read_wind_direction(client)
  20.             print(f"风向: {wind_direction}°")
  21.             time.sleep(0.1)
  22.     finally:
  23.         # 无论如何,最后关闭客户端
  24.         client.close()


  25. if __name__ == "__main__":
  26.     main()
复制代码



完成之后,传感器上电,运行测试代码

本帖子中包含更多资源

您需要 登录 才可以下载或查看,没有账号?立即注册

×
您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

QQ|Archiver|手机版|小黑屋|TY_Board论坛

GMT+8, 2024-9-17 04:17 , Processed in 0.050562 second(s), 20 queries .

Powered by Discuz! X3.5

© 2001-2024 Discuz! Team.

快速回复 返回顶部 返回列表