448 lines
14 KiB
Python
448 lines
14 KiB
Python
|
|
#!/usr/bin/env python
|
|||
|
|
# -*- coding: utf-8 -*-
|
|||
|
|
'''
|
|||
|
|
# @Time : 2025/2/14 14:43
|
|||
|
|
# @Author : hjw
|
|||
|
|
# @File : test.py
|
|||
|
|
'''
|
|||
|
|
import os
|
|||
|
|
import time
|
|||
|
|
import threading
|
|||
|
|
import serial
|
|||
|
|
import socket
|
|||
|
|
import logging
|
|||
|
|
import json
|
|||
|
|
from queue import Queue
|
|||
|
|
from abc import ABC, abstractmethod
|
|||
|
|
from collections import deque
|
|||
|
|
|
|||
|
|
# 配置日志系统
|
|||
|
|
logging.basicConfig(
|
|||
|
|
level=logging.INFO,
|
|||
|
|
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
|
|||
|
|
handlers=[
|
|||
|
|
logging.FileHandler('controller.log'),
|
|||
|
|
logging.StreamHandler()
|
|||
|
|
]
|
|||
|
|
)
|
|||
|
|
logger = logging.getLogger(__name__)
|
|||
|
|
|
|||
|
|
|
|||
|
|
def parse_config(file_path):
|
|||
|
|
config = {}
|
|||
|
|
current_level = [config] # 使用栈来跟踪当前层级的字典
|
|||
|
|
indent_stack = [] # 记录缩进层级
|
|||
|
|
|
|||
|
|
with open(file_path, 'r', encoding='utf-8') as file:
|
|||
|
|
for line in file:
|
|||
|
|
line = line.rstrip() # 去掉行尾的换行符和空格
|
|||
|
|
if not line or line.startswith('#'): # 跳过空行和注释
|
|||
|
|
continue
|
|||
|
|
|
|||
|
|
# 检测缩进层级
|
|||
|
|
indent = len(line) - len(line.lstrip())
|
|||
|
|
while indent_stack and indent <= indent_stack[-1]:
|
|||
|
|
current_level.pop()
|
|||
|
|
indent_stack.pop()
|
|||
|
|
|
|||
|
|
key, value = line.split(':', 1) # 分割键和值
|
|||
|
|
key = key.strip()
|
|||
|
|
value = value.strip()
|
|||
|
|
|
|||
|
|
# 处理值
|
|||
|
|
if value.startswith('[') and value.endswith(']'): # 列表
|
|||
|
|
value = [int(x.strip()) for x in value[1:-1].split(',')]
|
|||
|
|
elif value.isdigit(): # 数字
|
|||
|
|
value = int(value)
|
|||
|
|
elif value.replace('.', '', 1).isdigit(): # 浮点数
|
|||
|
|
value = float(value)
|
|||
|
|
elif value.lower() in ('true', 'false'): # 布尔值
|
|||
|
|
value = value.lower() == 'true'
|
|||
|
|
|
|||
|
|
# 插入到当前层级的字典中
|
|||
|
|
if value: # 如果有值,则直接赋值
|
|||
|
|
current_level[-1][key] = value
|
|||
|
|
else: # 如果没有值,则创建一个新字典
|
|||
|
|
new_dict = {}
|
|||
|
|
current_level[-1][key] = new_dict
|
|||
|
|
current_level.append(new_dict)
|
|||
|
|
indent_stack.append(indent)
|
|||
|
|
|
|||
|
|
return config
|
|||
|
|
|
|||
|
|
|
|||
|
|
class GPIOManager:
|
|||
|
|
def __init__(self, config):
|
|||
|
|
self.pins = config['gpio']['pins']
|
|||
|
|
self.pulse_per_rev = config['gpio']['pulse_per_rev']
|
|||
|
|
self.threads = {}
|
|||
|
|
self.stop_flags = {}
|
|||
|
|
self.gpio_files = {}
|
|||
|
|
self.lock = threading.Lock()
|
|||
|
|
|
|||
|
|
for pin in self.pins:
|
|||
|
|
self._init_gpio(pin)
|
|||
|
|
|
|||
|
|
def _init_gpio(self, pin):
|
|||
|
|
try:
|
|||
|
|
export_path = '/sys/class/gpio/export'
|
|||
|
|
gpio_path = f'/sys/class/gpio/gpio{pin}'
|
|||
|
|
|
|||
|
|
if not os.path.exists(gpio_path):
|
|||
|
|
with open(export_path, 'w') as f:
|
|||
|
|
f.write(str(pin))
|
|||
|
|
time.sleep(0.1)
|
|||
|
|
|
|||
|
|
direction_path = f'{gpio_path}/direction'
|
|||
|
|
with open(direction_path, 'w') as f:
|
|||
|
|
f.write('out')
|
|||
|
|
|
|||
|
|
self.gpio_files[pin] = open(f'{gpio_path}/value', 'r+')
|
|||
|
|
logger.info(f"GPIO {pin} 初始化完成")
|
|||
|
|
except Exception as e:
|
|||
|
|
logger.error(f"GPIO {pin} 初始化失败: {str(e)}")
|
|||
|
|
raise
|
|||
|
|
|
|||
|
|
def set_speed(self, pin_id, speed):
|
|||
|
|
with self.lock:
|
|||
|
|
if pin_id not in self.pins:
|
|||
|
|
raise ValueError(f"无效的GPIO引脚: {pin_id}")
|
|||
|
|
|
|||
|
|
# 停止现有线程
|
|||
|
|
if pin_id in self.threads:
|
|||
|
|
self.stop_flags[pin_id] = True
|
|||
|
|
self.threads[pin_id].join()
|
|||
|
|
|
|||
|
|
if speed <= 0:
|
|||
|
|
self._write_value(pin_id, 0)
|
|||
|
|
return
|
|||
|
|
|
|||
|
|
# 计算间隔时间(400脉冲/转)
|
|||
|
|
interval = 1.0 / (2 * speed) # 每个周期包含高低电平
|
|||
|
|
self.stop_flags[pin_id] = False
|
|||
|
|
self.threads[pin_id] = threading.Thread(
|
|||
|
|
target=self._pulse_loop,
|
|||
|
|
args=(pin_id, interval),
|
|||
|
|
daemon=True
|
|||
|
|
)
|
|||
|
|
self.threads[pin_id].start()
|
|||
|
|
|
|||
|
|
def _pulse_loop(self, pin_id, interval):
|
|||
|
|
while not self.stop_flags.get(pin_id, False):
|
|||
|
|
self._write_value(pin_id, 1)
|
|||
|
|
time.sleep(interval)
|
|||
|
|
self._write_value(pin_id, 0)
|
|||
|
|
time.sleep(interval)
|
|||
|
|
|
|||
|
|
def _write_value(self, pin_id, value):
|
|||
|
|
try:
|
|||
|
|
self.gpio_files[pin_id].seek(0)
|
|||
|
|
self.gpio_files[pin_id].write(str(value))
|
|||
|
|
self.gpio_files[pin_id].truncate()
|
|||
|
|
self.gpio_files[pin_id].flush()
|
|||
|
|
os.fsync(self.gpio_files[pin_id].fileno())
|
|||
|
|
except IOError as e:
|
|||
|
|
logger.error(f"GPIO写入错误: {str(e)}")
|
|||
|
|
|
|||
|
|
def cleanup(self):
|
|||
|
|
with self.lock:
|
|||
|
|
for pin in self.pins:
|
|||
|
|
if pin in self.threads:
|
|||
|
|
self.stop_flags[pin] = True
|
|||
|
|
self.threads[pin].join()
|
|||
|
|
self._write_value(pin, 0)
|
|||
|
|
for f in self.gpio_files.values():
|
|||
|
|
f.close()
|
|||
|
|
logger.info("GPIO资源已释放")
|
|||
|
|
|
|||
|
|
|
|||
|
|
class RS485Reader:
|
|||
|
|
def __init__(self, config):
|
|||
|
|
self.port = config['serial']['port']
|
|||
|
|
self.baudrate = config['serial']['baudrate']
|
|||
|
|
self.ser = None
|
|||
|
|
self.current_weight = 0
|
|||
|
|
self.running = True
|
|||
|
|
self.lock = threading.Lock()
|
|||
|
|
self.buffer = deque(maxlen=10) # 数据缓冲
|
|||
|
|
self._connect()
|
|||
|
|
|
|||
|
|
def _connect(self):
|
|||
|
|
try:
|
|||
|
|
self.ser = serial.Serial(
|
|||
|
|
port=self.port,
|
|||
|
|
baudrate=self.baudrate,
|
|||
|
|
bytesize=8,
|
|||
|
|
parity='N',
|
|||
|
|
stopbits=1,
|
|||
|
|
timeout=0.1
|
|||
|
|
)
|
|||
|
|
logger.info(f"成功连接串口设备 {self.port}")
|
|||
|
|
except Exception as e:
|
|||
|
|
logger.error(f"串口连接失败: {str(e)}")
|
|||
|
|
raise
|
|||
|
|
|
|||
|
|
def read_weight(self):
|
|||
|
|
cmd = bytes.fromhex('010300000002c40b')
|
|||
|
|
try:
|
|||
|
|
self.ser.write(cmd)
|
|||
|
|
response = self.ser.read(9)
|
|||
|
|
if len(response) == 9 and response[:3] == bytes.fromhex("010304"):
|
|||
|
|
weight = int.from_bytes(response[3:5], 'big')
|
|||
|
|
with self.lock:
|
|||
|
|
self.buffer.append(weight)
|
|||
|
|
self.current_weight = sum(self.buffer) // len(self.buffer)
|
|||
|
|
return True
|
|||
|
|
return False
|
|||
|
|
except Exception as e:
|
|||
|
|
logger.error(f"读取重量失败: {str(e)}")
|
|||
|
|
self._reconnect()
|
|||
|
|
return False
|
|||
|
|
|
|||
|
|
def _reconnect(self):
|
|||
|
|
max_retries = 3
|
|||
|
|
for i in range(max_retries):
|
|||
|
|
try:
|
|||
|
|
if self.ser:
|
|||
|
|
self.ser.close()
|
|||
|
|
self._connect()
|
|||
|
|
return True
|
|||
|
|
except Exception as e:
|
|||
|
|
logger.warning(f"重连尝试 {i + 1}/{max_retries} 失败")
|
|||
|
|
time.sleep(1)
|
|||
|
|
logger.error("串口重连失败")
|
|||
|
|
return False
|
|||
|
|
|
|||
|
|
def stop(self):
|
|||
|
|
self.running = False
|
|||
|
|
if self.ser and self.ser.is_open:
|
|||
|
|
self.ser.close()
|
|||
|
|
|
|||
|
|
|
|||
|
|
class ControlAlgorithm(ABC):
|
|||
|
|
@abstractmethod
|
|||
|
|
def calculate_speed(self, current, target):
|
|||
|
|
pass
|
|||
|
|
|
|||
|
|
|
|||
|
|
class PIDAlgorithm(ControlAlgorithm):
|
|||
|
|
def __init__(self, kp=0.5, ki=0.01, kd=0.1):
|
|||
|
|
self.kp = kp
|
|||
|
|
self.ki = ki
|
|||
|
|
self.kd = kd
|
|||
|
|
self.integral = 0
|
|||
|
|
self.last_error = 0
|
|||
|
|
self.min_speed = 10 # 最小运行速度
|
|||
|
|
self.max_speed = 100 # 最大运行速度
|
|||
|
|
|
|||
|
|
def calculate_speed(self, current, target):
|
|||
|
|
error = target - current
|
|||
|
|
self.integral += error
|
|||
|
|
derivative = error - self.last_error
|
|||
|
|
self.last_error = error
|
|||
|
|
|
|||
|
|
# 抗积分饱和
|
|||
|
|
if self.integral > 1000:
|
|||
|
|
self.integral = 1000
|
|||
|
|
elif self.integral < -1000:
|
|||
|
|
self.integral = -1000
|
|||
|
|
|
|||
|
|
output = self.kp * error + self.ki * self.integral + self.kd * derivative
|
|||
|
|
#误差5g 5*0.5 + 1000* 0.01 + [0.5*5] = 2.5 + 10 + ... = 12.5
|
|||
|
|
#误差500g 500*0.5 + 1000* 0.01 + [0.5*10] = 250 + 10 + ... = 260
|
|||
|
|
return max(self.min_speed, min(self.max_speed, output)) #
|
|||
|
|
|
|||
|
|
|
|||
|
|
class FuzzyLogicAlgorithm(ControlAlgorithm):
|
|||
|
|
def calculate_speed(self, current, target):
|
|||
|
|
error = target - current
|
|||
|
|
abs_error = abs(error)
|
|||
|
|
|
|||
|
|
if abs_error > 100:
|
|||
|
|
return 100
|
|||
|
|
elif abs_error > 50:
|
|||
|
|
return 70
|
|||
|
|
elif abs_error > 20:
|
|||
|
|
return 50
|
|||
|
|
else:
|
|||
|
|
return 30
|
|||
|
|
|
|||
|
|
|
|||
|
|
class NetworkHandler:
|
|||
|
|
def __init__(self, config):
|
|||
|
|
self.host = config['network']['host']
|
|||
|
|
self.port = config['network']['port']
|
|||
|
|
self.sock = None
|
|||
|
|
self.command_queue = Queue()
|
|||
|
|
self.status = {
|
|||
|
|
'measuring': False,
|
|||
|
|
'error': None,
|
|||
|
|
'current_weight': 0,
|
|||
|
|
'target_weight': 0,
|
|||
|
|
'algorithm': 'pid'
|
|||
|
|
}
|
|||
|
|
self.lock = threading.Lock()
|
|||
|
|
self.running = True
|
|||
|
|
|
|||
|
|
def start_server(self):
|
|||
|
|
self.sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
|||
|
|
self.sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
|
|||
|
|
try:
|
|||
|
|
self.sock.bind((self.host, self.port))
|
|||
|
|
self.sock.listen(5)
|
|||
|
|
logger.info(f"网络服务启动于 {self.host}:{self.port}")
|
|||
|
|
|
|||
|
|
while self.running:
|
|||
|
|
conn, addr = self.sock.accept()
|
|||
|
|
threading.Thread(target=self._handle_client, args=(conn, addr), daemon=True).start()
|
|||
|
|
except Exception as e:
|
|||
|
|
logger.error(f"网络服务异常: {str(e)}")
|
|||
|
|
finally:
|
|||
|
|
self.sock.close()
|
|||
|
|
|
|||
|
|
def _handle_client(self, conn, addr):
|
|||
|
|
try:
|
|||
|
|
data = conn.recv(1024)
|
|||
|
|
if data:
|
|||
|
|
cmd = json.loads(data.decode())
|
|||
|
|
self._process_command(cmd)
|
|||
|
|
|
|||
|
|
# 返回当前状态
|
|||
|
|
with self.lock:
|
|||
|
|
response = json.dumps(self.status)
|
|||
|
|
conn.send(response.encode())
|
|||
|
|
except json.JSONDecodeError:
|
|||
|
|
logger.warning("收到无效的JSON指令")
|
|||
|
|
except Exception as e:
|
|||
|
|
logger.error(f"客户端处理异常: {str(e)}")
|
|||
|
|
finally:
|
|||
|
|
conn.close()
|
|||
|
|
|
|||
|
|
def _process_command(self, cmd):
|
|||
|
|
with self.lock:
|
|||
|
|
if cmd.get('command') == 'set_target':
|
|||
|
|
|
|||
|
|
self.status['target_weight'] = cmd['payload']['target_weight']
|
|||
|
|
self.status['algorithm'] = cmd['payload'].get('algorithm', 'pid')
|
|||
|
|
self.status['measuring'] = True
|
|||
|
|
print("收到指令set_target:", self.status['target_weight'])
|
|||
|
|
elif cmd.get('command') == 'stop':
|
|||
|
|
self.status['measuring'] = False
|
|||
|
|
|
|||
|
|
|
|||
|
|
class Controller:
|
|||
|
|
def __init__(self, config_path='config.yaml'):
|
|||
|
|
|
|||
|
|
self.config = parse_config(config_path)
|
|||
|
|
|
|||
|
|
# 初始化组件
|
|||
|
|
self.gpio = GPIOManager(self.config)
|
|||
|
|
self.sensor = RS485Reader(self.config)
|
|||
|
|
self.network = NetworkHandler(self.config)
|
|||
|
|
self.algorithm = None
|
|||
|
|
self.running = False
|
|||
|
|
|
|||
|
|
# 共享状态
|
|||
|
|
self.current_weight = 0
|
|||
|
|
self.target_weight = 0
|
|||
|
|
self.system_status = 'idle'
|
|||
|
|
self.status_lock = threading.Lock()
|
|||
|
|
|
|||
|
|
# 看门狗线程
|
|||
|
|
self.watchdog = threading.Thread(target=self._watchdog, daemon=True)
|
|||
|
|
|
|||
|
|
def start(self):
|
|||
|
|
self.running = True
|
|||
|
|
threads = [
|
|||
|
|
threading.Thread(target=self._weight_reading_loop),
|
|||
|
|
threading.Thread(target=self._control_loop),
|
|||
|
|
threading.Thread(target=self.network.start_server),
|
|||
|
|
self.watchdog
|
|||
|
|
]
|
|||
|
|
for t in threads:
|
|||
|
|
t.start()
|
|||
|
|
logger.info("系统启动完成")
|
|||
|
|
|
|||
|
|
def _weight_reading_loop(self):
|
|||
|
|
while self.running:
|
|||
|
|
if self.sensor.read_weight():
|
|||
|
|
with self.sensor.lock:
|
|||
|
|
self.current_weight = self.sensor.current_weight
|
|||
|
|
with self.status_lock:
|
|||
|
|
self.network.status['current_weight'] = self.current_weight
|
|||
|
|
if self.current_weight >= self.network.status['target_weight']:
|
|||
|
|
self.network.status['measuring'] = False
|
|||
|
|
time.sleep(0.05) # 20Hz
|
|||
|
|
#time.sleep(0.1) # 20Hz
|
|||
|
|
|
|||
|
|
def _control_loop(self):
|
|||
|
|
while self.running:
|
|||
|
|
with self.status_lock:
|
|||
|
|
measuring = self.network.status['measuring']
|
|||
|
|
target = self.network.status['target_weight']
|
|||
|
|
algorithm = self.network.status['algorithm']
|
|||
|
|
|
|||
|
|
if measuring:
|
|||
|
|
self._select_algorithm(algorithm)
|
|||
|
|
speed = self.algorithm.calculate_speed(self.current_weight, target)
|
|||
|
|
rpm = speed # 假设速度单位直接对应RPM
|
|||
|
|
pulse_rate = (int(self.config['gpio']['pulse_per_rev']) * rpm) / 60
|
|||
|
|
self.gpio.set_speed(8, pulse_rate)
|
|||
|
|
else:
|
|||
|
|
self.gpio.set_speed(8, 0)
|
|||
|
|
while not self.running and not self.network.status['measuring']:
|
|||
|
|
time.sleep(0.1) # 短暂休眠,避免CPU占用过高
|
|||
|
|
time.sleep(0.1)
|
|||
|
|
|
|||
|
|
def _select_algorithm(self, name):
|
|||
|
|
if name == 'pid':
|
|||
|
|
params = self.config['algorithm']['params']['pid']
|
|||
|
|
self.algorithm = PIDAlgorithm(**params)
|
|||
|
|
elif name == 'fuzzy':
|
|||
|
|
self.algorithm = FuzzyLogicAlgorithm()
|
|||
|
|
else:
|
|||
|
|
self.algorithm = PIDAlgorithm()
|
|||
|
|
|
|||
|
|
def _watchdog(self):
|
|||
|
|
while self.running:
|
|||
|
|
# 检查线程状态
|
|||
|
|
threads_alive = all([
|
|||
|
|
threading.current_thread().is_alive(),
|
|||
|
|
self.sensor.running,
|
|||
|
|
self.network.running
|
|||
|
|
])
|
|||
|
|
|
|||
|
|
if not threads_alive:
|
|||
|
|
logger.critical("检测到线程异常,触发紧急停止!")
|
|||
|
|
self.emergency_stop()
|
|||
|
|
break
|
|||
|
|
|
|||
|
|
time.sleep(5)
|
|||
|
|
|
|||
|
|
def emergency_stop(self):
|
|||
|
|
with self.status_lock:
|
|||
|
|
self.network.status['measuring'] = False
|
|||
|
|
self.network.status['error'] = 'emergency_stop'
|
|||
|
|
self.gpio.cleanup()
|
|||
|
|
self.sensor.stop()
|
|||
|
|
self.running = False
|
|||
|
|
logger.info("系统已紧急停止")
|
|||
|
|
|
|||
|
|
def shutdown(self):
|
|||
|
|
self.running = False
|
|||
|
|
self.gpio.cleanup()
|
|||
|
|
self.sensor.stop()
|
|||
|
|
self.network.running = False
|
|||
|
|
logger.info("系统正常关闭")
|
|||
|
|
|
|||
|
|
|
|||
|
|
if __name__ == "__main__":
|
|||
|
|
ctrl = Controller()
|
|||
|
|
try:
|
|||
|
|
ctrl.start()
|
|||
|
|
while True:
|
|||
|
|
time.sleep(1)
|
|||
|
|
except KeyboardInterrupt:
|
|||
|
|
logger.info("收到停止信号...")
|
|||
|
|
finally:
|
|||
|
|
ctrl.shutdown()
|