Pārlūkot izejas kodu

分炉分坯功能初始提交

oldwine 5 mēneši atpakaļ
vecāks
revīzija
3e8cd7d6fd
9 mainītis faili ar 1062 papildinājumiem un 0 dzēšanām
  1. 29 0
      conf/5#nodes.csv
  2. 29 0
      conf/6#nodes.csv
  3. 78 0
      main.py
  4. 285 0
      models/billet_counter.py
  5. 200 0
      models/data_sender.py
  6. 39 0
      utils/logger.py
  7. 93 0
      utils/mqttdata.py
  8. 135 0
      utils/s7data.py
  9. 174 0
      utils/statepoint.py

+ 29 - 0
conf/5#nodes.csv

@@ -0,0 +1,29 @@
+name,type,db,start,offset,size,read_allow,write_allow,frequency
+浇铸信号,boollist,130,0,0,1,TRUE,FALSE,500
+大包重量1,real,130,20,0,4,TRUE,FALSE,500
+大包重量2,real,130,40,0,4,TRUE,FALSE,500
+中包重量,real,130,44,0,4,TRUE,FALSE,500
+L1切割信号,boollist,131,4,0,1,TRUE,FALSE,500
+L2切割信号,boollist,132,4,0,1,TRUE,FALSE,500
+L3切割信号,boollist,133,4,0,1,TRUE,FALSE,500
+L4切割信号,boollist,134,4,0,1,TRUE,FALSE,500
+L5切割信号,boollist,135,4,0,1,TRUE,FALSE,500
+L6切割信号,boollist,136,4,0,1,TRUE,FALSE,500
+L7切割信号,boollist,137,4,0,1,TRUE,FALSE,500
+L8切割信号,boollist,138,4,0,1,TRUE,FALSE,500
+L1定尺,int,100,20,0,2,TRUE,FALSE,500
+L2定尺,int,100,22,0,2,TRUE,FALSE,500
+L3定尺,int,100,24,0,2,TRUE,FALSE,500
+L4定尺,int,100,26,0,2,TRUE,FALSE,500
+L5定尺,int,101,20,0,2,TRUE,FALSE,500
+L6定尺,int,101,22,0,2,TRUE,FALSE,500
+L7定尺,int,101,24,0,2,TRUE,FALSE,500
+L8定尺,int,101,26,0,2,TRUE,FALSE,500
+L1拉速,real,131,20,0,4,TRUE,FALSE,500
+L2拉速,real,132,20,0,4,TRUE,FALSE,500
+L3拉速,real,133,20,0,4,TRUE,FALSE,500
+L4拉速,real,134,20,0,4,TRUE,FALSE,500
+L5拉速,real,135,20,0,4,TRUE,FALSE,500
+L6拉速,real,136,20,0,4,TRUE,FALSE,500
+L7拉速,real,137,20,0,4,TRUE,FALSE,500
+L8拉速,real,138,20,0,4,TRUE,FALSE,500

+ 29 - 0
conf/6#nodes.csv

@@ -0,0 +1,29 @@
+name,type,db,start,offset,size,read_allow,write_allow,frequency
+浇铸信号,boollist,230,0,0,1,TRUE,FALSE,500
+大包重量1,real,230,20,0,4,TRUE,FALSE,500
+大包重量2,real,230,40,0,4,TRUE,FALSE,500
+中包重量,real,230,28,0,4,TRUE,FALSE,500
+L1切割信号,boollist,231,4,0,1,TRUE,FALSE,500
+L2切割信号,boollist,232,4,0,1,TRUE,FALSE,500
+L3切割信号,boollist,233,4,0,1,TRUE,FALSE,500
+L4切割信号,boollist,234,4,0,1,TRUE,FALSE,500
+L5切割信号,boollist,235,4,0,1,TRUE,FALSE,500
+L6切割信号,boollist,236,4,0,1,TRUE,FALSE,500
+L7切割信号,boollist,237,4,0,1,TRUE,FALSE,500
+L8切割信号,boollist,238,4,0,1,TRUE,FALSE,500
+L1定尺,int,200,20,0,2,TRUE,FALSE,500
+L2定尺,int,200,22,0,2,TRUE,FALSE,500
+L3定尺,int,200,24,0,2,TRUE,FALSE,500
+L4定尺,int,200,26,0,2,TRUE,FALSE,500
+L5定尺,int,201,20,0,2,TRUE,FALSE,500
+L6定尺,int,201,22,0,2,TRUE,FALSE,500
+L7定尺,int,201,24,0,2,TRUE,FALSE,500
+L8定尺,int,201,26,0,2,TRUE,FALSE,500
+L1拉速,real,231,20,0,4,TRUE,FALSE,500
+L2拉速,real,232,20,0,4,TRUE,FALSE,500
+L3拉速,real,233,20,0,4,TRUE,FALSE,500
+L4拉速,real,234,20,0,4,TRUE,FALSE,500
+L5拉速,real,235,20,0,4,TRUE,FALSE,500
+L6拉速,real,236,20,0,4,TRUE,FALSE,500
+L7拉速,real,237,20,0,4,TRUE,FALSE,500
+L8拉速,real,238,20,0,4,TRUE,FALSE,500

+ 78 - 0
main.py

@@ -0,0 +1,78 @@
+from models.billet_counter import Counter
+from models.data_sender import Sender
+from utils.s7data import S7data, S7Client
+from utils.mqttdata import Mqttdata, MqttClient
+from utils.logger import Logger
+
+
+##############################################################
+# 日志配置
+
+logger_5 = Logger('5#')
+logger_5.file_on('logs/5#log.log')
+logger_5.screen_on()
+
+logger_6 = Logger('6#')
+logger_6.file_on('logs/6#log.log')
+logger_6.screen_on()
+
+logger_sender = Logger('sender')
+logger_sender.file_on('logs/sender_log.log')
+logger_sender.screen_on()
+
+
+##############################################################
+# S7数据源配置
+
+s7_5 = S7Client()
+s7_5.connect('192.168.1.215', 0, 0)
+data_5 = S7data(r"conf/5#nodes.csv")
+data_5.set_S7Client(s7_5)
+data_5.set_logger(logger_5)
+data_5.start_auto_update()
+logger_5.info('[PREPARE]5#机PLC连接成功')
+
+s7_6 = S7Client()
+s7_6.connect('192.168.1.216', 0, 0)
+data_6 = S7data(r"conf/6#nodes.csv")
+data_6.set_S7Client(s7_6)
+data_6.set_logger(logger_6)
+data_6.start_auto_update()
+logger_6.info('[PREPARE]6#机PLC连接成功')
+
+
+##############################################################
+# MQTT数据源配置
+
+mqtt_mes = MqttClient('python-mqtt-biller_digitalization', 'readonly', '1qazxsw@')
+mqtt_mes.connect('192.168.12.201', 1883)
+mqtt_mes.loop_start()
+data_mes = Mqttdata()
+data_mes.set_mqtt_client(mqtt_mes)
+logger_5.info('[PREPARE]与MES使用MQTT连接成功')
+logger_6.info('[PREPARE]与MES使用MQTT连接成功')
+
+mqtt_web = MqttClient('python-mqtt-992')
+mqtt_web.connect('192.168.0.119', 1883)
+mqtt_web.loop_start()
+data_web = Mqttdata()
+data_web.set_mqtt_client(mqtt_web)
+logger_sender.info('[PREPARE]与WEB业务平台使用MQTT连接成功')
+
+
+##############################################################
+# 数据发送服务
+
+sender = Sender(logger_sender)
+sender.set_mqtt_client(mqtt_web)
+
+# debug设置
+# sender.http_flag = False
+
+
+##############################################################
+# 分炉分坯服务
+
+flfp_5 = Counter(data_mes, data_5, 5, logger_5, sender)
+
+flfp_6 = Counter(data_mes, data_6, 6, logger_6, sender)

+ 285 - 0
models/billet_counter.py

@@ -0,0 +1,285 @@
+from utils.statepoint import *
+from utils.mqttdata import *
+from utils.s7data import *
+from models.data_sender import *
+import logging
+
+_Debug = 0
+
+class Counter:
+    @property
+    def data_mqtt(self):
+        if self._data_mqtt.cli == None:
+            raise ValueError('The MQTT connection to MES has not been initialized.')
+        return self._data_mqtt
+    
+    @property
+    def data_s7(self):
+        if self._data_s7 == None:
+            raise ValueError('The S7 connection to the casting machine PLC has not been initialized.')
+        return self._data_s7
+    
+    @property
+    def sender(self):
+        if self._sender == None:
+            raise ValueError('The sender has not been set.')
+        return self._sender
+        
+    # 数据点定义
+    def create_data_point(self, ccmNo):
+        self.ladle_weight_1 = self.data_s7.make_point('大包重量1')
+        self.ladle_weight_2 = self.data_s7.make_point('大包重量2')
+
+        self.begin_pour = self.data_mqtt.make_point(f'{ccmNo}#开浇信号')
+        self.end_pour = self.data_mqtt.make_point(f'{ccmNo}#停浇信号')
+
+        if _Debug:
+            self.begin_pour_ring = self.data_s7.make_point('浇铸信号[2]')
+
+        self.begin_cutting = []
+        for i in range(8):
+            self.begin_cutting.append(self.data_s7.make_point(f'L{i+1}切割信号[0]'))
+
+        self.end_cutting = []
+        for i in range(8):
+            self.end_cutting.append(self.data_s7.make_point(f'L{i+1}切割信号[1]'))
+
+        self.length_cutting = []
+        for i in range(8):
+            self.length_cutting.append(self.data_s7.make_point(f'L{i+1}定尺'))
+
+        self.drawing_speed = []
+        for i in range(8):
+            self.drawing_speed.append(self.data_s7.make_point(f'L{i+1}拉速'))
+
+
+    def init_data_point(self):
+        # 统一初始化数据点
+        self.begin_pour.allow_update(False)
+        self.begin_pour.set_state(False)
+        self.end_pour.allow_update(False)
+        self.end_pour.set_state(False)
+        for i in range(8):
+            self.begin_cutting[i].allow_update(False)
+            self.begin_cutting[i].set_state(False)
+            self.begin_cutting[i].data = 0
+            self.end_cutting[i].allow_update(False)
+            self.end_cutting[i].set_state(False)
+            self.end_cutting[i].data = 0
+
+        # 数据点逻辑设置
+        self.begin_pour.set_excite_action(self.begin_pour_action)
+        self.end_pour.set_excite_action(self.end_pour_action)
+
+        for i in range(8):
+            self.begin_cutting[i].set_excite_action(lambda i=i: self.begin_cutting_action(i))
+            self.begin_cutting[i].set_keep_time(3000)
+            self.end_cutting[i].set_excite_action(lambda i=i: self.end_cutting_action(i))
+            self.end_cutting[i].set_reset_action(lambda i=i: self.end_cutting[i].allow_update(False))
+
+        # 统一开启数据点
+        self.begin_pour.allow_update()
+        self.end_pour.allow_update()
+        for i in range(8):
+            self.begin_cutting[i].allow_update()
+
+
+    def __init__(self, data_mqtt: Mqttdata, data_s7: S7data, ccmNo, logger: logging.Logger, sender: Sender):
+        # 模块入口、出口、日志定义
+        self._data_mqtt = data_mqtt
+        self._data_s7 = data_s7
+        self._sender = sender
+        self.logger = logger
+
+        self.logger.info(f"[Counter]分炉分坯模块:{ccmNo}号机模块启动")
+
+        # 配置必须的数据点
+        self.create_data_point(ccmNo)
+        self.init_data_point()
+
+        #分炉分坯功能
+        self.last_cutting_timestamp = 0
+
+        self.strand = [0, 0, 0, 0, 0, 0, 0, 0]
+        self.cutting_state_heat = [{}, {}, {}, {}, {}, {}, {}, {}]
+        self.cutting_state_heat_index = [0, 0, 0, 0, 0, 0, 0, 0]
+        self.total = 0
+        self.limit_count = 0
+        self.limit_target = 24
+        self.limit_flag = False
+        self.lock = threading.Lock()
+        self.old_heat = {}
+        self.new_heat = {}
+        self.last_cutting_strand = 0
+
+
+    def begin_pour_action(self):
+        # 标志是否为铸机开机的第一次开浇
+        flag = (time.time() - self.last_cutting_timestamp) > 1800
+
+        # 大包重量选择算法
+        ladle_weight = max(self.ladle_weight_1.data, self.ladle_weight_2.data)
+
+        # 写入日志
+        if flag:
+            self.logger.info(f'[Counter]首次开浇:{self.begin_pour.data['heatNo']}')
+        elif self.new_heat == {}:
+            self.logger.info(f'[Counter]炉次开浇:{self.begin_pour.data['heatNo']},当前出坯炉次:{"未知" if self.old_heat == {} else self.old_heat["heatNo"]}')
+        else:
+            self.logger.warning(f'[Counter]炉次开浇:{self.begin_pour.data['heatNo']},炉次{self.new_heat['heatNo']}被覆盖')
+
+        # 维护换炉操作
+        if flag:
+            self.old_heat = self.begin_pour.data
+            self.new_heat = {}
+        else:
+            self.new_heat = self.begin_pour.data
+            self.start_limit()
+
+        # 使用sender向外发送信号
+        self.sender.begin_pour(self.begin_pour.data, ladle_weight)
+
+
+    def end_pour_action(self):
+        # 写入日志
+        self.logger.info(f'[Counter]炉次停浇:{self.end_pour.data["heatNo"]}')
+
+        # 使用sender向外发送信号
+        if self.old_heat:
+            self.sender.end_pour(self.end_pour.data)
+
+
+    def cutting_data_comple(self, i):
+        count = 3
+
+        # 补充计入模块
+        while count:
+            time.sleep(0.5)
+            sizing = self.length_cutting[i].data
+            speed = self.drawing_speed[i].data
+            if 0 < sizing < 30000 and 0 < speed < 10:
+                self.strand_add(i+1, sizing, speed)
+                return None
+            count -= 1
+        
+        time.sleep(0.5)
+        sizing = self.length_cutting[i].data
+        speed = self.drawing_speed[i].data
+        if not (0 < sizing < 30000 and 0 < speed < 10):
+            self.logger.warning("[Counter]请注意,定尺/拉速数据持续异常,已按照异常数据发送")
+
+        self.strand_add(i+1, sizing, speed)
+        self.end_cutting[i].allow_update()
+
+    def begin_cutting_action(self, i):
+        # 写入日志
+        self.logger.info(f'[Counter]{i+1}流:开始切割')
+
+        # 计入模块
+        sizing = self.length_cutting[i].data
+        speed = self.drawing_speed[i].data
+        if 0 < sizing < 30000 and 0 < speed < 10:
+            self.strand_add(i+1, sizing, speed)
+            self.end_cutting[i].allow_update()
+        else:
+            threading.Thread(target=self.cutting_data_comple, args=(i,)).start()
+
+
+
+    def end_cutting_action(self, i):
+        # 写入日志
+        self.logger.info(f'[Counter]{i+1}流:完成切割')
+
+        # 复位流状态
+        cutting_state_heat = self.cutting_state_heat[i]
+        cutting_state_heat_index = self.cutting_state_heat_index[i]
+        self.cutting_state_heat[i] = {}
+        self.cutting_state_heat_index[i] = 0
+
+        if cutting_state_heat:
+            # 使用sender向外发送信号
+            self.sender.end_cut(cutting_state_heat, cutting_state_heat_index)
+            # 检查自己是否是本炉最后一根钢坯
+            if self.last_cutting_strand == i+1:
+                self.last_cutting_strand = 0
+                self.sender.heat_last(cutting_state_heat)
+
+
+    def strand_add(self, sno, sizing, speed):
+        with self.lock:
+            # 维护辅助时间戳
+            self.last_cutting_timestamp = time.time()
+
+            #维护内部正确性的主要算法
+            if self.limit_flag:
+                self.limit_count += 1
+                if self.limit_count == self.limit_target:
+                    # 此处已经在切割本炉最后一根
+                    if self.old_heat:
+                        self.last_cutting_strand = sno
+                if self.limit_count > self.limit_target:
+                    # 此处已经在切割新炉第一根
+                    self.change_heat()
+                    self.total = 0
+                    self.strand = [0, 0, 0, 0, 0, 0, 0, 0]
+                    self.limit_count = 0
+                    self.limit_flag = False
+            self.strand[sno-1] += 1
+            self.total += 1
+
+            heatData = self.old_heat
+            heatIndex = self.total
+            strandIndex = self.strand[sno-1]
+        
+        if heatData:
+            # 记录当前流状态,帮助停切信号判断钢坯信息
+            self.cutting_state_heat[sno-1] = heatData
+            self.cutting_state_heat_index[sno-1] = heatIndex
+            # 生成坯号,使用sender向外发送信号
+            ccmNo = heatData['ccmNo']
+            billetNo = heatData['heatNo'] + ccmNo + str(sno) + '{:0>2}'.format(strandIndex)
+            self.logger.info(f"[Counter]{sno}流:{heatData['heatNo']}炉第{heatIndex}根计入系统,坯号:{billetNo}")
+            self.sender.begin_cut(heatData, billetNo, heatIndex, sizing, speed)
+            # 使用sender发送炉次首次开切信号
+            if heatIndex == 1:
+                self.sender.heat_first(heatData)
+        else:
+            self.logger.info(f"[Counter]{sno}流:未知炉第{heatIndex}根,本炉无法计入系统,下一炉开始正常")
+
+
+    def start_limit(self):
+        # 根据实际情况设置本炉总支数
+        with self.lock:
+            if self.old_heat:
+                if self.total % 4 == 1:
+                    self.limit_target = 23
+                elif self.total % 4 == 2:
+                    if self.total >= 28:
+                        self.limit_target = 22
+                    else:
+                        self.limit_target = 26
+                elif self.total % 4 == 3:
+                    self.limit_target = 25
+                else:
+                    self.limit_target = 24
+            else:
+                self.limit_target = 24
+                
+            self.limit_flag = True
+
+
+    def change_heat(self):
+        # 异常情况
+        if not self.new_heat:
+            self.logger.error('[Counter]换炉:失败,无新炉次信息')
+            return None
+        
+        # 写入日志
+        if self.old_heat:
+            self.logger.info(f'[Counter]换炉:{self.old_heat["heatNo"]}->{self.new_heat["heatNo"]}')
+        else:
+            self.logger.info(f'[Counter]换炉:未知炉次->{self.new_heat["heatNo"]}')
+
+        #真正换炉过程
+        self.old_heat = self.new_heat
+        self.new_heat = {}

+ 200 - 0
models/data_sender.py

@@ -0,0 +1,200 @@
+import paho.mqtt.client as mqtt
+import json, time, requests
+
+
+class Sender:
+    def __init__(self, logger):
+        self.logger = logger
+
+        self.topic = {
+            'billet_add': 'trace/performance/billet/add',
+            'heat_add': 'trace/performance/converter/add'
+        }
+
+        self.url = {
+            'billet_add': 'http://192.168.0.119:8181/jeecg-boot/actualControl/billetActual/reportRealTimeBilletBasicInfo',
+            'heat_add': 'http://192.168.0.119:8181/jeecg-boot/actualControl/heatsActuals/reportRealTimeHeatsActuals'
+        }
+
+        self.heat_tangible_temp = {
+            'optype'            : 0,
+            'casterCode'        : '',
+            'emptyLadleWeight'  : 0.0,
+            'fullLadleWeight'   : 0.0,
+            'grade'             : '',
+            'spec'              : '',
+            'heatsCode'         : '',
+            'firstCutTime'      : '',
+            'lastCutTime'       : '',
+            'ladleCode'         : '',
+            'moltenSteelWeight' : 0.0,
+            'startPourTime'     : '',
+            'stopPourTime'      : '',
+            'blankOutput'       : 0.0,
+            'billetSum'         : 0
+        }
+
+        self.billet_tangible_temp = {
+            'optype'            : 0,
+            'heatNo'            : '',
+            'billetNo'          : '',
+            'grade'             : '',
+            'ladleNo'           : '',
+            'ccmNo'             : '',
+            'strandNo'          : '',
+            'heatnoIndex'       : 0,
+            'length'            : 0,
+            'strandnoIndex'     : 0,
+            'castingSpeed'      : 0.0,
+            'actualLength'      : 0,
+            'spec'              : '',
+            'width'             : 170,
+            'thickness'         : 170,
+            'weight'            : 0.0,
+            'cutStartTime'      : '',
+            'cutStopTime'       : ''
+        }
+
+        self.mqtt_cli = None
+        self.http_flag = True
+        self._cache = {}
+        self._billet = {}
+
+    def set_mqtt_client(self, cli):
+        self.mqtt_cli = cli
+
+    def send(self, purpose, payload, qos=2):
+        if not isinstance(payload, dict):
+            self.logger.error('[SENDER]发送数据非dict类型')
+            raise TypeError(f"Need a dict type but {type(payload)} given.")
+            return None
+        
+        self.logger.info(f"[SENDER]使用 {'MQTT'if self.mqtt_cli else ''} {'HTTP'if self.http_flag else ''} 发送数据")
+        self.logger.debug(f"[SENDER]{purpose}:{payload}")
+        
+        if self.mqtt_cli:
+            if not self.mqtt_cli.is_connected:
+                self.logger.error('[SENDER]MQTT:发送失败,MQTT未连接')
+
+            if self.mqtt_cli.publish(self.topic[purpose], json.dumps(payload), qos)[0]:
+                self.logger.error('[SENDER]MQTT:数据包发送失败')
+            else:
+                self.logger.info('[SENDER]MQTT:数据包发送成功')
+
+        if self.http_flag:
+            try:
+                self.logger.debug('[SENDER]HTTP:'+requests.post(self.url[purpose], json=payload, timeout=2).text)
+            except:
+                self.logger.error('[SENDER]HTTP:请求超时')
+
+    def begin_pour(self, heat_data, bigbagweight):
+        tmp = self.heat_tangible_temp.copy()
+        tmp['optype'] = 1
+        tmp['casterCode'] = heat_data.get('ccmNo', '')
+        tmp['emptyLadleWeight'] = bigbagweight - heat_data.get('netWeight', 0)
+        tmp['fullLadleWeight'] = bigbagweight
+        tmp['grade'] = heat_data.get('grade', '')
+        tmp['spec'] = heat_data.get('spec', '')
+        tmp['heatsCode'] = heat_data.get('heatNo', '')
+        tmp['ladleCode'] = heat_data.get('ladleNo', '')
+        tmp['moltenSteelWeight'] = heat_data.get('netWeight', 0)
+        tmp['startPourTime'] = heat_data.get('sendTime', '')
+        
+        #此处应存储进数据库,暂时使用缓存处理
+        self._cache[heat_data.get('heatNo', 'error')] = tmp
+        self.send('heat_add', tmp)
+
+    def end_pour(self, heat_data):
+        tmp = self._cache[heat_data['heatNo']]
+        tmp['optype'] = 2
+        tmp['grade'] = heat_data.get('grade', '')
+        tmp['spec'] = heat_data.get('spec', '')
+        tmp['stopPourTime'] = heat_data.get('sendTime', '')
+
+        #此处应存储进数据库,暂时使用缓存处理
+        self._cache[heat_data.get('heatNo', 'error')] = tmp
+        self.send('heat_add', tmp)
+
+    def heat_first(self, heat_data):
+        cuttime = time.strftime('%Y-%m-%d %H:%M:%S', time.localtime())
+        tmp = self._cache[heat_data['heatNo']]
+        tmp['optype'] = 2
+        tmp['firstCutTime'] = cuttime
+
+        #此处应存储进数据库,暂时使用缓存处理
+        self._cache[heat_data.get('heatNo', 'error')] = tmp
+        self.send('heat_add', tmp)
+
+    def heat_last(self, heat_data):
+        cuttime = time.strftime('%Y-%m-%d %H:%M:%S', time.localtime())
+        tmp = self._cache[heat_data['heatNo']]
+        tmp['optype'] = 2
+        tmp['lastCutTime'] = cuttime
+
+        #此处应存储进数据库,暂时使用缓存处理
+        self._cache[heat_data.get('heatNo', 'error')] = tmp
+        self.send('heat_add', tmp)
+
+    def begin_cut(self, heat_data, billetNo, heatnoIndex, sizing, speed):
+        cuttime = time.strftime('%Y-%m-%d %H:%M:%S', time.localtime())
+        tmp = self.billet_tangible_temp.copy()
+        tmp['optype'] = 1
+        tmp['heatNo'] = heat_data.get('heatNo', '')
+        tmp['billetNo'] = billetNo
+        tmp['grade'] = heat_data.get('grade', '')
+        tmp['spec'] = heat_data.get('spec', '')
+        tmp['ladleNo'] = heat_data.get('ladleNo', '')
+        tmp['ccmNo'] = heat_data.get('ccmNo', '')
+        tmp['strandNo'] = billetNo[-3]
+        tmp['heatnoIndex'] = heatnoIndex
+        tmp['length'] = sizing
+        tmp['strandnoIndex'] = int(billetNo[-2:])
+        tmp['castingSpeed'] = speed
+        tmp['weight'] = sizing / 1000 * 0.2265
+        tmp['cutStartTime'] = cuttime
+
+        #此处应存储进数据库,暂时使用缓存处理
+        self._billet[heat_data.get('heatNo', '')+str(heatnoIndex)] = tmp
+        self.send('billet_add', tmp)
+
+    def end_cut(self, heat_data, heatnoIndex):
+        cuttime = time.strftime('%Y-%m-%d %H:%M:%S', time.localtime())
+        trycount = 4
+        tmp = self._billet.get(heat_data['heatNo']+str(heatnoIndex), {})
+        while not tmp:
+            trycount -= 1
+            time.sleep(0.5)
+            tmp = self._billet.get(heat_data['heatNo']+str(heatnoIndex), {})
+        if not tmp:
+            self.logger.error(f"[SENDER]停切:找不到对应的开切数据,炉号:{heat_data['heatNo']},序号:{heatnoIndex}")
+            return None
+
+        tmp['optype'] = 2
+        tmp['cutStopTime'] = cuttime
+
+        #此处应存储进数据库,暂时使用缓存处理
+        self._billet[heat_data.get('heatNo', '')+str(heatnoIndex)] = tmp
+        self.send('billet_add', tmp)
+
+if __name__ == '__main__':
+    mqttcli = mqtt.Client(mqtt.CallbackAPIVersion.VERSION2, 'python-mqtt-992-sender_test')
+    mqttcli.username_pw_set('admin', '123456')
+    mqttcli.connect('192.168.0.119', 1883)
+    mqttcli.loop_start()
+    test = Sender()
+    test.set_mqtt_client(mqttcli)
+
+    testheat = {"netWeight":140.06,"ladleNo":"13","heatNo":"24617099","grade":"微氮铌钢","castState":1,"spec":"上若泰基","ccmNo":"6","sendTime":"2024-11-28 12:57:09"}
+    test.begin_pour(testheat, 210.0)
+    time.sleep(1)
+    test.heat_first(testheat)
+
+    time.sleep(1)
+    test.begin_cut(testheat, "246170996301", 5, 11000, 2.85)
+    time.sleep(1)
+    test.end_cut(testheat, 5)
+
+    time.sleep(1)
+    test.end_pour({"netWeight":0.0,"ladleNo":"","heatNo":"24617099","grade":"微氮铌钢","castState":0,"spec":"上若泰基","ccmNo":"6","sendTime":"2024-11-28 13:21:20"})
+    time.sleep(1)
+    test.heat_last(testheat)

+ 39 - 0
utils/logger.py

@@ -0,0 +1,39 @@
+import logging
+
+class Logger(logging.Logger):
+    def __init__(self, name):
+        super().__init__(name)
+
+        self.setLevel(level=logging.DEBUG)
+        self.format_default = '%(asctime)s - %(name)s - %(levelname)s - %(message)s'
+
+        self.console = None
+        self.handler = None
+
+    def screen_on(self, level=logging.DEBUG, format=None):
+        if self.console:
+            return None
+        
+        if format == None:
+            formatter = logging.Formatter(self.format_default)
+        else:
+            formatter = logging.Formatter(format)
+
+        self.console = logging.StreamHandler()
+        self.console.setLevel(level)
+        self.console.setFormatter(formatter)
+        self.addHandler(self.console)
+
+    def file_on(self, path='log.txt', level=logging.DEBUG, format=None):
+        if self.handler:
+            return None
+        
+        if format == None:
+            formatter = logging.Formatter(self.format_default)
+        else:
+            formatter = logging.Formatter(format)
+
+        self.handler = logging.FileHandler(path)
+        self.handler.setLevel(level)
+        self.handler.setFormatter(formatter)
+        self.addHandler(self.handler)

+ 93 - 0
utils/mqttdata.py

@@ -0,0 +1,93 @@
+import paho.mqtt.client as mqtt
+import json, warnings
+from utils.statepoint import *
+
+class MqttClient(mqtt.Client):
+    def __init__(self, client_id, username=None, password=None, version=mqtt.CallbackAPIVersion.VERSION2):
+        super().__init__(version, client_id)
+
+        if username and password:
+            self.username_pw_set(username, password)
+
+class Mqttdata:
+    def __init__(self):
+        self.logger = None
+
+        self.thread = None
+        self.node_data = {'5#开浇信号': {}, '5#停浇信号': {}, '6#开浇信号': {}, '6#停浇信号': {}}
+        self.target_from_name = {}
+
+    def set_logger(self, logger):
+        self.logger = logger
+
+    def set_mqtt_client(self, cli):
+        self.cli = cli
+        self.cli.on_connect = self.on_connect
+        self.cli.on_message = self.on_message
+        self.cli.subscribe('data/service/cast/info', qos=2)
+
+    def start_auto_update(self):
+        if self.thread == None:
+            self.thread = threading.Thread(target=self.cli.loop_forever)
+        self.thread.start()
+
+    def send(self, name):
+        if name in self.target_from_name:
+            for i in self.target_from_name[name]:
+                i.inject(self.node_data[name])
+                timer = threading.Timer(5, lambda i=i: i.set_state(False))
+                timer.start()
+        
+    def on_subscribe(self, client, userdata, mid, reason_code_list, properties):
+        if reason_code_list[0].is_failure:
+            warnings.warn(f"Broker rejected you subscription: {reason_code_list[0]}")
+            if self.logger:
+                self.logger.error(f"Broker rejected you subscription: {reason_code_list[0]}")
+        else:
+            if self.logger:
+                self.logger.info(f"Broker granted the following QoS: {reason_code_list[0].value}")
+
+    def on_message(self, client, userdata, message):
+        # logger.debug(message.payload.decode())
+        data = json.loads(message.payload.decode())
+        if 'ccmNo' not in data or 'castState' not in data:
+            warnings.warn('[MES]MQTT报文格式错误')
+            if self.logger:
+                self.logger.error('[MES]MQTT报文格式错误')
+            return None
+        if int(data['ccmNo']) == 6:
+            if data['castState'] and data != self.node_data['6#开浇信号']:
+                self.node_data['6#开浇信号'] = data
+                self.send('6#开浇信号')
+            elif not data['castState'] and data != self.node_data['6#停浇信号']:
+                self.node_data['6#停浇信号'] = data
+                self.send('6#停浇信号')
+        elif int(data['ccmNo']) == 5:
+            if data['castState'] and data != self.node_data['5#开浇信号']:
+                self.node_data['5#开浇信号'] = data
+                self.send('5#开浇信号')
+            elif not data['castState'] and data != self.node_data['5#停浇信号']:
+                self.node_data['5#停浇信号'] = data
+                self.send('5#停浇信号')
+        else:
+            warnings.warn('[MES]MQTT收到未知铸机号')
+            if self.logger:
+                self.logger.error('[MES]MQTT收到未知铸机号')
+
+    def on_connect(self, client, userdata, flags, reason_code, properties):
+        if reason_code.is_failure:
+            warnings.warn(f"Failed to connect: {reason_code}. loop_forever() will retry connection")
+            if self.logger:
+                self.logger.error(f"Failed to connect: {reason_code}. loop_forever() will retry connection")
+        else:
+            if self.logger:
+                self.logger.info("MQTT connection succeeded")
+
+    def make_point(self, name):
+        if name not in self.node_data:
+            raise ValueError("创建了未配置的点")
+        if name not in self.target_from_name:
+            self.target_from_name[name] = []
+        res = Statepoint()
+        self.target_from_name[name].append(res)
+        return res

+ 135 - 0
utils/s7data.py

@@ -0,0 +1,135 @@
+import snap7, csv, threading, warnings, time
+from utils.statepoint import *
+
+S7Client = snap7.client.Client
+
+class S7data:
+    def __init__(self, csvfile):
+        self.logger = None
+
+        self.S7Client = None
+        self.lock = threading.Lock()
+        self.thread_run = False
+        self.threads = []
+        self.nodes = {}
+        self.node_data = {}
+        self.target_from_name = {}
+        with open(csvfile) as f:
+            for i in csv.DictReader(f):
+                if i['name'] in self.nodes:
+                    raise Exception(f"S7配置文件节点名称重复:{i['name']}")
+                else:
+                    self.nodes[i['name']] = i
+                    self.node_data[i['name']] = bytearray(int(i['size']))
+
+    def set_logger(self, logger):
+        self.logger = logger
+
+    def set_S7Client(self, s7c):
+        self.S7Client = s7c
+
+    def get_S7Client(self):
+        return self.S7Client
+
+    def send(self, name):
+        if self.nodes[name]['type'] == 'int':
+            data = snap7.util.get_int(self.node_data[name], 0)
+        elif self.nodes[name]['type'] == 'bool':
+            data = snap7.util.get_bool(self.node_data[name], 0, int(self.nodes[name]['offset']))
+        elif self.nodes[name]['type'] == 'boollist':
+            data = [(self.node_data[name][0] >> i) & 1 for i in range(8)]
+        elif self.nodes[name]['type'] == 'real':
+            data = snap7.util.get_real(self.node_data[name], 0)
+        elif self.nodes[name]['type'] == 'string':
+            data = name, snap7.util.get_string(self.node_data[name], 0)
+        elif self.nodes[name]['type'] == 'wstring':
+            data = self.node_data[name][4:].decode(encoding='utf-16be')
+        else:
+            warnings.warn('暂不支持的类型:' + self.nodes[name]['type'])
+            if self.logger:
+                self.logger.error('暂不支持的类型:' + self.nodes[name]['type'])
+            return None
+
+        if name in self.target_from_name:
+            for i in self.target_from_name[name]:
+                i.inject(data)
+        if self.nodes[name]['type'] == 'boollist' and name + '*' in self.target_from_name:
+            for i in range(8):
+                for j in self.target_from_name[name+'*'][i]:
+                    j.inject(data[i])
+
+    def update(self, name):
+        nodeinfo = self.nodes[name]
+        try:
+            while True:
+                if not self.thread_run:
+                    return None
+                self.lock.acquire()
+                if not self.S7Client.get_connected():
+                    warnings.warn('S7Client连接中断')
+                    if self.logger:
+                        self.logger.error('S7Client连接中断')
+                    self.thread_run = False
+                    self.lock.release()
+                    return None
+                tmp = self.S7Client.db_read(int(nodeinfo['db']), int(nodeinfo['start']), int(nodeinfo['size']))
+                self.lock.release()
+                if self.node_data[name] != tmp:
+                    self.node_data[name] = tmp
+                    self.send(name)
+                time.sleep(float(nodeinfo['frequency']) / 1000)
+        except RuntimeError as reason:
+            warnings.warn(reason)
+            if self.logger:
+                self.logger.error(reason)
+            self.thread_run = False
+            self.lock.release()
+
+    def start_auto_update(self):
+        if self.thread_run:
+            return None
+        self.threads = []
+        if self.S7Client == None:
+            warnings.warn('未初始化S7Client')
+            if self.logger:
+                self.logger.error('未初始化S7Client')
+            return None
+        if not self.S7Client.get_connected():
+            warnings.warn('S7Client未连接')
+            if self.logger:
+                self.logger.error('S7Client未连接')
+            return None
+        for key, value in self.nodes.items():
+            if value['read_allow'].upper() != 'FALSE':
+                self.threads.append(threading.Thread(target=self.update, args=(value['name'],)))
+        self.thread_run = True
+        for i in self.threads:
+            i.start()
+
+    def end_auto_update(self):
+        self.thread_run = False
+        for i in self.threads:
+            i.join()
+
+    def make_point(self, name):
+        index = -1
+        solvedname = name
+        if len(name) > 3 and name[-3] == '[' and name[-1] == ']' and name[-2].isdigit() and 0 <= int(name[-2]) < 8:
+            index = int(name[-2])
+            name = name[:-3]
+            solvedname = name + '*'
+        if name not in self.nodes:
+            raise ValueError("创建了未配置的点")
+
+        if solvedname not in self.target_from_name:
+            if index == -1:
+                self.target_from_name[solvedname] = []
+            else:
+                self.target_from_name[solvedname] = [[],[],[],[],[],[],[],[]]
+                
+        res = Statepoint()
+        if index == -1:
+            self.target_from_name[solvedname].append(res)
+        else:
+            self.target_from_name[solvedname][index].append(res)
+        return res

+ 174 - 0
utils/statepoint.py

@@ -0,0 +1,174 @@
+import threading
+
+class Statepoint:
+    def __init__(self, initvalue = False, initstate = False):
+        self.data = initvalue
+        self.state = initstate
+        self.lock = threading.Lock()
+        self.permitted_update = True
+        self.__private_permitted_update = True
+        self.converter = lambda data: bool(data)
+        self.do_excite = lambda: None
+        self.do_reset = lambda: None
+        self.keep_time = 1000
+        self.pre_reset = False
+
+    def inject(self, data):
+        if self.data == data:
+            return None
+        #数据更新
+        self.data = data
+        #状态更新
+        if self.permitted_update and self.__private_permitted_update:
+            self.__async_update_state()
+            #self.__update_state()
+
+    def excite(self):
+        #logger.info('excite to next')
+        self.do_excite()
+
+    def reset(self):
+        self.do_reset()
+
+    def __update_state(self):
+        self.lock.acquire()
+        last_state = self.state
+        self.state = self.converter(self.data)
+        if last_state == False and self.state == True:
+            self.pre_reset = False
+            self.excite()
+        elif last_state == True and self.state == False:
+            if self.keep_time <= 0:
+                self.reset()
+            elif self.pre_reset:
+                self.pre_reset = False
+                self.reset()
+            else:
+                self.state = True
+                self.__private_allow_update(False)
+                self.pre_reset = True
+                timer = threading.Timer(self.keep_time/1000, lambda: self.__private_allow_update())
+                timer.start()
+        elif last_state == True and self.state == True:
+            self.pre_reset = False
+        else:
+            self.pre_reset = False
+        self.lock.release()
+
+    def __async_update_state(self):
+        threading.Thread(target=self.__update_state).start()
+
+    def allow_update(self, enable: bool = True):
+        self.permitted_update = enable
+        if enable and self.__private_permitted_update:
+            self.__async_update_state()
+            #self.__update_state()
+
+    def __private_allow_update(self, enable: bool = True):
+        self.__private_permitted_update = enable
+        if enable:
+            self.__async_update_state()
+            #self.__update_state()
+
+    def set_convertor(self, func = lambda data: bool(data)):
+        if callable(func):
+            self.converter = func
+        else:
+            raise TypeError('The parameter func can only be a function')
+
+    def set_excite_action(self, func = lambda: None):
+        if callable(func):
+            self.do_excite = func
+        else:
+            raise TypeError('The parameter func can only be a function')
+
+    def set_reset_action(self, func = lambda: None):
+        if callable(func):
+            self.do_reset = func
+        else:
+            raise TypeError('The parameter func can only be a function')
+
+    def set_keep_time(self, keeptime):
+        self.keep_time = keeptime
+
+    def set_state(self, state):
+        self.state = state
+
+class Through_state_continues3(Statepoint):
+    def __init__(self, p1, p2, p3):
+        super().__init__()
+        self.point1 = p1
+        self.point2 = p2
+        self.point3 = p3
+        self.point1.allow_update(False)
+        self.point2.allow_update(False)
+        self.point3.allow_update(False)
+        self.point1.set_excite_action(lambda: self.point2.allow_update())
+        self.point2.set_excite_action(lambda: self.point3.allow_update())
+        self.point3.set_excite_action(lambda: self.inject(True))
+        
+        self.point1.set_reset_action(lambda: (None if self.point2.state else self.point2.allow_update(False),
+                                              None if self.point2.state or self.point3.state else self.inject(False)))
+        self.point2.set_reset_action(lambda: (None if self.point3.state else self.point3.allow_update(False),
+                                              None if self.point1.state else self.inject(False)))
+        self.point3.set_reset_action(lambda: None if self.point2.state else self.inject(False))
+        # self.set_excite_action(lambda: logger.info('经过点已触发'))
+        # self.set_reset_action(lambda: logger.info('已前往推钢区域'))
+
+    def reset(self):
+        self.point2.allow_update(False)
+        self.point3.allow_update(False)
+        self.point3.state = False
+        super().reset()
+        if self.point1.state:
+            self.point1.excite()
+
+    def allow_update(self, enable: bool = True):
+        if enable:
+            self.point1.allow_update(enable)
+        else:
+            self.permitted_update = False
+            self.point1.allow_update(False)
+            self.point2.allow_update(False)
+            self.point3.allow_update(False)
+            self.point1.state = False
+            self.point2.state = False
+            self.point3.state = False
+            self.permitted_update = True
+            
+
+class Through_state_separation2(Statepoint):
+    def __init__(self, p1, p2):
+        super().__init__()
+        self.point1 = p1
+        self.point2 = p2
+        self.point1.allow_update(False)
+        self.point2.allow_update(False)
+        #self.point1.set_keep_time(3000)
+        self.point1.set_excite_action(lambda: self.point2.allow_update())
+        self.point2.set_excite_action(lambda: self.inject(True))
+        
+        self.point1.set_reset_action(lambda: None if self.point2.state else self.point2.allow_update(False))
+        self.point2.set_reset_action(lambda: self.inject(False))
+        # self.set_excite_action(lambda: logger.debug('推钢机已经推动钢坯'))
+
+    def reset(self):
+        self.point1.allow_update(False)
+        self.point2.allow_update(False)
+        self.point1.state = False
+        self.point2.state = False
+        #logger.debug('推钢机刚刚经过')
+        super().reset()
+
+    def allow_update(self, enable: bool = True):
+        if enable:
+            # logger.debug('open')
+            self.point1.allow_update()
+        else:
+            # logger.debug('close')
+            self.permitted_update = False
+            self.point1.allow_update(False)
+            self.point2.allow_update(False)
+            self.point1.state = False
+            self.point2.state = False
+            self.permitted_update = True