OPi: авто-реконнект MQTT + проверка rc у publish (heartbeat и вес молча терялись с 10.07)
This commit is contained in:
@@ -93,3 +93,10 @@
|
|||||||
- grab_frame: убран scale=1280, отдаём полный кадр; сервер сам кропает зону номера, делает автоконтраст и распознаёт жёлтые/белые номера
|
- grab_frame: убран scale=1280, отдаём полный кадр; сервер сам кропает зону номера, делает автоконтраст и распознаёт жёлтые/белые номера
|
||||||
|
|
||||||
## 2026-06-26 — порог срабатывания MIN_WEIGHT 1600 -> 2000 кг (отсечь ложные заезды)
|
## 2026-06-26 — порог срабатывания MIN_WEIGHT 1600 -> 2000 кг (отсечь ложные заезды)
|
||||||
|
|
||||||
|
## 2026-07-15 — MQTT: авто-реконнект и честная публикация
|
||||||
|
- publish() теперь проверяет rc (paho НЕ бросает исключение при отсутствии связи, qos=0 молча терялся)
|
||||||
|
- on_connect/on_disconnect + reconnect_delay_set(1..60с); подписки восстанавливаются после каждого реконнекта
|
||||||
|
- ensure_mqtt_connected() в heartbeat_loop и перед publish_weight — самолечение канала
|
||||||
|
- heartbeat пишет в лог, дошёл ли статус ("НЕ ОТПРАВЛЕН (нет связи с брокерами)")
|
||||||
|
- Причина: 10.07 оба MQTT-канала (к нам :1884 и к ребятам 192.168.20.9:1883) отвалились и не переподключились; фото шли по HTTP, поэтому проблема была не видна
|
||||||
|
|||||||
+96
-27
@@ -176,34 +176,98 @@ def on_message(client, userdata, msg):
|
|||||||
mqtt1 = mqtt.Client(client_id="scales_opi_1")
|
mqtt1 = mqtt.Client(client_id="scales_opi_1")
|
||||||
mqtt2 = mqtt.Client(client_id="scales_opi_2")
|
mqtt2 = mqtt.Client(client_id="scales_opi_2")
|
||||||
|
|
||||||
|
# (client, имя, host, port, топик подписки)
|
||||||
|
_MQTT_DEFS = (
|
||||||
|
(mqtt1, 'MQTT1', MQTT1_HOST, MQTT1_PORT, MQTT_GO_TOPIC),
|
||||||
|
(mqtt2, 'MQTT2', MQTT2_HOST, MQTT2_PORT, MQTT_CMD_TOPIC),
|
||||||
|
)
|
||||||
|
_loop_started = {'MQTT1': False, 'MQTT2': False}
|
||||||
|
|
||||||
|
def _setup_client(client, name, sub_topic):
|
||||||
|
"""Колбэки + backoff. Подписку восстанавливаем в on_connect (после КАЖДОГО реконнекта)."""
|
||||||
|
client.on_message = on_message
|
||||||
|
try:
|
||||||
|
client.reconnect_delay_set(min_delay=1, max_delay=60)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
def _on_connect(c, userdata, flags, rc, *a):
|
||||||
|
if rc == 0:
|
||||||
|
print(f"[{name}] Подключён к брокеру, подписка на {sub_topic}")
|
||||||
|
try:
|
||||||
|
c.subscribe(sub_topic)
|
||||||
|
except Exception as e:
|
||||||
|
print(f"[{name}] Ошибка подписки: {e}")
|
||||||
|
else:
|
||||||
|
print(f"[{name}] Подключение отклонено, rc={rc}")
|
||||||
|
|
||||||
|
def _on_disconnect(c, userdata, *a):
|
||||||
|
rc = a[-1] if a else '?'
|
||||||
|
print(f"[{name}] ОТКЛЮЧЁН от брокера (rc={rc}) — будет переподключение")
|
||||||
|
|
||||||
|
client.on_connect = _on_connect
|
||||||
|
client.on_disconnect = _on_disconnect
|
||||||
|
|
||||||
def mqtt_connect():
|
def mqtt_connect():
|
||||||
|
if MQTT2_USER:
|
||||||
|
try:
|
||||||
|
mqtt2.username_pw_set(MQTT2_USER, MQTT2_PASS)
|
||||||
|
except Exception as e:
|
||||||
|
print(f"[MQTT2] Ошибка авторизации: {e}")
|
||||||
|
for client, name, host, port, sub in _MQTT_DEFS:
|
||||||
|
_setup_client(client, name, sub)
|
||||||
|
try:
|
||||||
|
client.connect(host, port, keepalive=60)
|
||||||
|
client.loop_start()
|
||||||
|
_loop_started[name] = True
|
||||||
|
print(f"[{name}] Подключение к {host}:{port} инициировано")
|
||||||
|
except Exception as e:
|
||||||
|
# сеть может быть не готова при старте — поднимем в ensure_mqtt_connected()
|
||||||
|
print(f"[{name}] Ошибка подключения к {host}:{port}: {e}")
|
||||||
|
if not _loop_started[name]:
|
||||||
|
try:
|
||||||
|
client.loop_start()
|
||||||
|
_loop_started[name] = True
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
def ensure_mqtt_connected():
|
||||||
|
"""Самолечение: если клиент отвалился — поднимаем связь. Вызывается из heartbeat_loop."""
|
||||||
|
for client, name, host, port, sub in _MQTT_DEFS:
|
||||||
|
try:
|
||||||
|
if client.is_connected():
|
||||||
|
continue
|
||||||
|
print(f"[{name}] Нет соединения — переподключаюсь к {host}:{port}")
|
||||||
|
try:
|
||||||
|
client.reconnect()
|
||||||
|
except Exception:
|
||||||
|
client.connect(host, port, keepalive=60)
|
||||||
|
if not _loop_started[name]:
|
||||||
|
client.loop_start()
|
||||||
|
_loop_started[name] = True
|
||||||
|
except Exception as e:
|
||||||
|
print(f"[{name}] Переподключение не удалось: {e}")
|
||||||
|
|
||||||
|
def _pub(client, name, topic, payload, **kw):
|
||||||
|
"""publish с проверкой результата: paho НЕ бросает исключение, если клиента отключили."""
|
||||||
try:
|
try:
|
||||||
mqtt1.on_message = on_message
|
info = client.publish(topic, payload, **kw)
|
||||||
mqtt1.connect(MQTT1_HOST, MQTT1_PORT, keepalive=60)
|
rc = getattr(info, 'rc', 0)
|
||||||
mqtt1.subscribe(MQTT_GO_TOPIC)
|
if rc != 0:
|
||||||
mqtt1.loop_start()
|
print(f"[{name}] НЕ доставлено в {topic} (rc={rc}, нет связи с брокером)")
|
||||||
print(f"[MQTT1] Подключён к {MQTT1_HOST}:{MQTT1_PORT}, подписан на {MQTT_GO_TOPIC}")
|
return False
|
||||||
|
return True
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"[MQTT1] Ошибка подключения: {e}")
|
print(f"[{name}] Ошибка публикации в {topic}: {e}")
|
||||||
try:
|
return False
|
||||||
mqtt2.username_pw_set(MQTT2_USER, MQTT2_PASS)
|
|
||||||
mqtt2.connect(MQTT2_HOST, MQTT2_PORT, keepalive=60)
|
|
||||||
mqtt2.on_message = on_message
|
|
||||||
mqtt2.subscribe(MQTT_CMD_TOPIC)
|
|
||||||
mqtt2.loop_start()
|
|
||||||
print(f"[MQTT2] Подключён к {MQTT2_HOST}:{MQTT2_PORT}, подписан на {MQTT_CMD_TOPIC}")
|
|
||||||
except Exception as e:
|
|
||||||
print(f"[MQTT2] Ошибка подключения: {e}")
|
|
||||||
|
|
||||||
def publish_weight(w):
|
def publish_weight(w):
|
||||||
ts = datetime.datetime.now().strftime('%Y-%m-%dT%H:%M:%S')
|
ts = datetime.datetime.now().strftime('%Y-%m-%dT%H:%M:%S')
|
||||||
payload = f"{w:.1f}"
|
payload = f"{w:.1f}"
|
||||||
for client, name in ((mqtt1, 'MQTT1'), (mqtt2, 'MQTT2')):
|
ensure_mqtt_connected()
|
||||||
try:
|
for client, name, _h, _p, _s in _MQTT_DEFS:
|
||||||
client.publish(MQTT_WEIGHT_TOPIC, payload)
|
if _pub(client, name, MQTT_WEIGHT_TOPIC, payload):
|
||||||
print(f"[{name}] → {MQTT_WEIGHT_TOPIC}: {payload} кг [{ts}]")
|
print(f"[{name}] → {MQTT_WEIGHT_TOPIC}: {payload} кг [{ts}]")
|
||||||
except Exception as e:
|
|
||||||
print(f"[{name}] Ошибка публикации: {e}")
|
|
||||||
|
|
||||||
|
|
||||||
# ════════════════════════════════════════════════════════════════════════════════
|
# ════════════════════════════════════════════════════════════════════════════════
|
||||||
@@ -390,11 +454,11 @@ def build_status(current_state):
|
|||||||
|
|
||||||
def publish_status(current_state):
|
def publish_status(current_state):
|
||||||
payload = json.dumps(build_status(current_state))
|
payload = json.dumps(build_status(current_state))
|
||||||
for client, name in ((mqtt1, 'MQTT1'), (mqtt2, 'MQTT2')):
|
ok = []
|
||||||
try:
|
for client, name, _h, _p, _s in _MQTT_DEFS:
|
||||||
client.publish(MQTT_STATUS_TOPIC, payload, qos=0, retain=True)
|
if _pub(client, name, MQTT_STATUS_TOPIC, payload, qos=0, retain=True):
|
||||||
except Exception as e:
|
ok.append(name)
|
||||||
print(f"[{name}] Ошибка публикации статуса: {e}")
|
return ok
|
||||||
|
|
||||||
_state_ref = {'value': 'EMPTY'} # шарится с heartbeat-потоком без блокировок (GIL спасает на простой записи)
|
_state_ref = {'value': 'EMPTY'} # шарится с heartbeat-потоком без блокировок (GIL спасает на простой записи)
|
||||||
|
|
||||||
@@ -406,9 +470,14 @@ def heartbeat_loop():
|
|||||||
sent, pending = outbox.flush()
|
sent, pending = outbox.flush()
|
||||||
if sent > 0:
|
if sent > 0:
|
||||||
print(f"[OUTBOX] Досланных: {sent}, осталось: {pending}")
|
print(f"[OUTBOX] Досланных: {sent}, осталось: {pending}")
|
||||||
|
# Держим MQTT живым (самолечение после разрывов)
|
||||||
|
ensure_mqtt_connected()
|
||||||
# Публикуем статус (включая outbox_pending)
|
# Публикуем статус (включая outbox_pending)
|
||||||
publish_status(_state_ref['value'])
|
ok = publish_status(_state_ref['value'])
|
||||||
print(f"[HEARTBEAT] state={_state_ref['value']}")
|
if ok:
|
||||||
|
print(f"[HEARTBEAT] state={_state_ref['value']} → {', '.join(ok)}")
|
||||||
|
else:
|
||||||
|
print(f"[HEARTBEAT] state={_state_ref['value']} — НЕ ОТПРАВЛЕН (нет связи с брокерами)")
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"[HEARTBEAT] Ошибка: {e}")
|
print(f"[HEARTBEAT] Ошибка: {e}")
|
||||||
time.sleep(HEARTBEAT_INTERVAL)
|
time.sleep(HEARTBEAT_INTERVAL)
|
||||||
|
|||||||
Reference in New Issue
Block a user