Загрузка данных


#!/usr/bin/env python3

import time
import math
from smbus import SMBus

import board
import adafruit_bmp280


# ============================================================
# НАСТРОЙКИ
# ============================================================

BMP280_ADDRESS = 0x76
I2C_BUS = 1

# Сколько измерений АЦП усреднять
SAMPLES = 20

# Пауза между измерениями АЦП
SAMPLE_DELAY = 0.01

# Пауза после вывода результата
DISPLAY_DELAY = 2.0

# Как часто пытаться заново найти BMP280 после ошибки
BMP_RETRY_DELAY = 3.0

# Делитель напряжения
R1 = 51000.0
R2 = 10000.0

# Коэффициент расхода
K = 0.0004022  # 0.0004022 для 91 мм; 0.0003021 для 80 мм


# ============================================================
# ИНИЦИАЛИЗАЦИЯ I2C
# ============================================================

bus = None
bmp280 = None
bmp_ok = False
last_bmp_retry = 0.0


def init_bmp280(force=False):
    """
    Инициализация BMP280.
    Если датчик временно недоступен — программа НЕ завершается.
    """
    global bmp280, bmp_ok, last_bmp_retry

    now = time.monotonic()

    if not force and (now - last_bmp_retry) < BMP_RETRY_DELAY:
        return False

    last_bmp_retry = now

    try:
        i2c = board.I2C()
        sensor = adafruit_bmp280.Adafruit_BMP280_I2C(
            i2c,
            address=BMP280_ADDRESS
        )

        # Проверяем, что датчик реально отвечает
        _ = sensor.pressure
        _ = sensor.temperature

        bmp280 = sensor
        bmp_ok = True

        print("\n[BMP280] Датчик найден, данные снова доступны.\n")
        return True

    except Exception as e:
        bmp280 = None
        bmp_ok = False
        print(
            "\n[BMP280] Ожидание данных, подождите..."
            f"\n[BMP280] Причина: {e}\n"
        )
        return False


def read_bmp280():
    """
    Чтение давления и температуры BMP280.
    Возвращает (Patm, temperature) либо None при ошибке.
    """
    global bmp_ok, bmp280

    if not bmp_ok or bmp280 is None:
        if not init_bmp280():
            return None

    try:
        pressure = float(bmp280.pressure) * 100.0
        temperature = float(bmp280.temperature)

        # Проверяем явно невалидные значения
        if not math.isfinite(pressure) or not math.isfinite(temperature):
            raise ValueError(
                f"Некорректные данные BMP280: P={pressure}, T={temperature}"
            )

        return pressure, temperature

    except Exception as e:
        bmp_ok = False
        bmp280 = None

        print(
            "\n[BMP280] Данные временно недоступны."
            "\n[BMP280] Ожидание данных, подождите..."
            f"\n[BMP280] Ошибка: {e}\n"
        )

        # Сразу пытаемся восстановить датчик.
        init_bmp280(force=True)
        return None


def init_bus():
    """
    Инициализация SMBus.
    При проблеме возвращает False вместо падения программы.
    """
    global bus

    try:
        bus = SMBus(I2C_BUS)
        return True
    except Exception as e:
        bus = None
        print(
            "\n[I2C] Шина недоступна."
            "\n[I2C] Ожидание данных, подождите..."
            f"\n[I2C] Ошибка: {e}\n"
        )
        return False


# ============================================================
# ЧТЕНИЕ АЦП
# ============================================================

CHANNELS = {
    0x48: {
        "select": [0x41, 0x42, 0x43, 0x40],
        "names": ["B1", "B2", "B3", "B4"],
    },
    0x49: {
        "select": [0x41, 0x42, 0x43, 0x40],
        "names": ["B5", "B6", "B7", "B8"],
    },
    0x4A: {
        "select": [0x41, 0x42, 0x43, 0x40],
        "names": ["B9", "B10", "B11", "B12"],
    },
    0x4B: {
        "select": [0x41, 0x42, 0x43, 0x40],
        "names": ["B13", "B14", "B15", "B0"],
    },
    0x4C: {
        "select": [0x41, 0x42, 0x43, 0x40],
        "names": ["B21", "B22", "B23", "B20"],
    },
}


def read_adc_channels():
    """
    Читает все используемые каналы.
    Возвращает словарь B1...B15/B0/B21 либо None.
    """
    global bus

    if bus is None:
        if not init_bus():
            return None

    result = {}

    try:
        for address, cfg in CHANNELS.items():
            for select, name in zip(cfg["select"], cfg["names"]):
                bus.write_byte(address, select)
                result[name] = bus.read_byte(address)

        return result

    except Exception as e:
        print(
            "\n[I2C ADC] Ошибка чтения АЦП."
            "\n[I2C ADC] Ожидание данных, подождите..."
            f"\n[I2C ADC] Ошибка: {e}\n"
        )

        # SMBus иногда восстанавливается только после повторного открытия.
        try:
            bus.close()
        except Exception:
            pass

        bus = None
        return None


# ============================================================
# ВСПОМОГАТЕЛЬНЫЕ ФУНКЦИИ
# ============================================================

def clear_screen():
    print("\033[2J\033[H", end="")


def safe_sqrt(value):
    """
    Защита от sqrt() отрицательного значения из-за
    временно некорректного сигнала датчика.
    """
    if not math.isfinite(value) or value < 0:
        return 0.0
    return math.sqrt(value)


def average_to_flow(B, Patm, P, TK):
    """
    Расчёт расходов q1...q12.
    """
    u = R2 / (R2 + R1)

    vout = {}
    vin = {}
    dp = {}
    D = {}
    G = {}
    q = {}

    for n in range(1, 13):
        vout[n] = 3.3 * B[f"B{n}"] / 255.0
        vin[n] = vout[n] / u
        dp[n] = 500.0 * vin[n]

        D[n] = dp[n] * P / TK
        G[n] = K * safe_sqrt(D[n])
        q[n] = int(G[n] * 3600.0 / 1.292)

    return q, G


# ============================================================
# ОСНОВНАЯ ПРОГРАММА
# ============================================================

def main():
    global bus

    # Начальная инициализация.
    init_bus()
    init_bmp280(force=True)

    sums = {
        "B1": 0, "B2": 0, "B3": 0, "B4": 0,
        "B5": 0, "B6": 0, "B7": 0, "B8": 0,
        "B9": 0, "B10": 0, "B11": 0, "B12": 0,
        "B13": 0, "B14": 0, "B15": 0,
        "B0": 0, "B21": 0,
    }

    x = 0

    while True:
        try:
            # ------------------------------------------------
            # Считываем АЦП
            # ------------------------------------------------
            adc = read_adc_channels()

            if adc is None:
                # Не падаем и не продолжаем на старых данных.
                x = 0
                for key in sums:
                    sums[key] = 0

                time.sleep(1.0)
                continue

            # ------------------------------------------------
            # Накопление среднего
            # ------------------------------------------------
            for key in sums:
                sums[key] += adc[key]

            x += 1

            if x < SAMPLES:
                time.sleep(SAMPLE_DELAY)
                continue

            # ------------------------------------------------
            # Получаем средние значения
            # ------------------------------------------------
            B = {
                key: sums[key] / SAMPLES
                for key in sums
            }

            # Сбрасываем накопитель сразу после формирования
            # среднего, чтобы ошибка дальше не испортила следующий цикл.
            x = 0
            for key in sums:
                sums[key] = 0

            # ------------------------------------------------
            # BMP280
            # ------------------------------------------------
            bmp_data = read_bmp280()

            if bmp_data is None:
                clear_screen()
                print("\n")
                print("================================================")
                print("       ОЖИДАНИЕ ДАННЫХ BMP280")
                print("       Подождите, идёт восстановление...")
                print("================================================")
                print("\nАЦП продолжает работать.")
                print("Расчёт давления/расхода временно приостановлен.")
                print("\n")

                time.sleep(1.0)
                continue

            Patm, t = bmp_data
            PatmHg = int(0.00750063 * Patm)

            # ------------------------------------------------
            # Расчёт параметров
            # ------------------------------------------------
            u = R2 / (R2 + R1)

            # Давление перед холодной шайбой
            vout0 = 3.3 * B["B0"] / 255.0
            vin0 = vout0 / u
            dp0 = 700.0 * vin0

            # Давление формирующего коллектора
            vout13 = 3.3 * B["B13"] / 255.0
            vin13 = vout13 / u
            Pst = 2500.0 * vin13
            Pdv = Pst + Patm

            # Температура перед холодной шайбой
            vout21 = 3.3 * B["B21"] / 255.0
            vin21 = vout21 / u
            Tdv = int(15.0 * vin21 - 50.0)
            TdvK = Tdv + 273

            if TdvK <= 0:
                print("\n[WARN] Некорректная Tdv. Ожидание следующего измерения...")
                time.sleep(1.0)
                continue

            D0 = dp0 * Pdv / TdvK
            G0 = 0.002038 * safe_sqrt(D0)

            # Давление в форкамере
            vout14 = 3.3 * B["B14"] / 255.0
            vin14 = vout14 / u
            Pfp = 2500.0 * vin14

            P = int(Pfp + Patm)
            PH2O = int(0.1 * Pfp)
            Pfp = int(Pfp)

            # Температура
            vout15 = 3.3 * B["B15"] / 255.0
            vin15 = vout15 / u
            T = int(94.0 * vin15 - 40.0)
            TK = T + 273

            if TK <= 0 or P <= 0:
                print("\n[WARN] Некорректные P/T. Ожидание следующего измерения...")
                time.sleep(1.0)
                continue

            # ------------------------------------------------
            # Расходы q1...q12
            # ------------------------------------------------
            q, G = average_to_flow(B, Patm, P, TK)

            q1 = q[1]
            q2 = q[2]
            q3 = q[3]
            q4 = q[4]
            q5 = q[5]
            q6 = q[6]
            q7 = q[7]
            q8 = q[8]
            q9 = q[9]
            q10 = q[10]
            q11 = q[11]
            q12 = q[12]

            Sf = (
                q1 + q2 + q3 + q4 + q5 + q6 +
                q7 + q8 + q9 + q10 + q11 + q12
            )

            Q = int(G0 * 3600.0 / 1.292)
            Q = Q + 1

            AQ = Q - Sf

            # Защита от деления на ноль
            if Q != 0:
                DQ = int(100 * AQ / Q)
            else:
                DQ = 0

            # ------------------------------------------------
            # Вывод
            # ------------------------------------------------
            clear_screen()

            print(
                "                       "
                f"Pfp:{Pfp:0.0f} Pa"
                "   = "
                f"{PH2O} mmH2O"
                f"         T={T}*C"
            )

            print(
                "                              "
                f"Sf = {Sf} nm3/h"
                f"     Tdv={Tdv}*C"
            )

            print()
            print(
                "        q1     q2     q3     q4     q5     q6     "
                "q7     q8     q9    q10    q11    q12"
            )
            print()

            print(
                f"        {q1:4d}   {q2:4d}   {q3:4d}   {q4:4d}   "
                f"{q5:4d}   {q6:4d}   {q7:4d}   {q8:4d}   "
                f"{q9:4d}   {q10:4d}   {q11:4d}   {q12:4d}"
            )

            print()
            print(
                f"       Q = {Q} nm3/h"
                f"    AQ = {AQ} nm3/h"
                f"    DQ = {DQ}%"
                f"    Patm:{PatmHg}mmHg"
                f"    t:{t:0.0f}*C"
            )

            print()
            print("       BMP280: OK")
            print()

            time.sleep(DISPLAY_DELAY)

        except KeyboardInterrupt:
            print("\nПрограмма остановлена пользователем.")
            break

        except Exception as e:
            # Самая важная защита:
            # любая неожиданная ошибка больше НЕ убивает программу.
            print("\n================================================")
            print(" НЕОЖИДАННАЯ ОШИБКА — ПРОГРАММА ПРОДОЛЖИТ РАБОТУ")
            print("================================================")
            print(f"Ошибка: {type(e).__name__}: {e}")
            print("Ожидание данных, подождите...")
            print("")

            # Начинаем новое окно усреднения,
            # чтобы старые данные не смешивались с новыми.
            x = 0
            for key in sums:
                sums[key] = 0

            time.sleep(2.0)

    # Корректное закрытие SMBus
    try:
        if bus is not None:
            bus.close()
    except Exception:
        pass


if __name__ == "__main__":
    main()