ebuild_rasp2/ebuild/io/adc_mcp3008.py

424 lines
14 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# -*- coding: utf-8 -*-
from __future__ import annotations
__title__ = "analog_sensors"
__author__ = 'Mehmet Karatay & "Saraswati" (ChatGPT)'
__purpose__ = "MCP3008 tabanlı basınç, gaz, yağmur ve LDR sensörleri için sınıf bazlı arayüz"
__version__ = "0.1.0"
__date__ = "2025-11-21"
"""
ebuild/core/analog_sensors.py
Revision : 2025-11-21
Authors : Mehmet Karatay & "Saraswati" (ChatGPT)
Amaç
-----
- MCP3008 ADC üzerinden bağlı analog sensörler için tekil sınıf yapıları
sağlamak:
* PressureAnalogSensor : su hattı basınç sensörü
* GasAnalogSensor : MQ-4 veya benzeri gaz sensörü
* RainAnalogSensor : yağmur sensörü (wet/dry)
* LDRAnalogSensor : ışık seviyesi sensörü
- Her bir sensör:
* MCP3008ADC üzerinden ilgili kanalı okur (config_statics.ADC_CHANNELS).
* Ham raw (0..1023) ve volt cinsinden değer döndürebilir.
* Eşik ve basit state (SAFE/WARN/ALARM vb.) hesabını kendi içinde tutar.
- Güvenlik ve uyarı mantığı üst kattaki HeatEngine/Burner/Buzzer/Legacy
ile kolay entegre edilebilir.
Notlar
------
- Bu modül "karar mantığı" ve analog okuma katmanını birleştirir.
- Röle kapama, sistem shutdown, buzzer vb. aksiyonlar yine üst katmanda
yapılmalıdır.
"""
from dataclasses import dataclass, field
from typing import Optional, Dict
try:
from ..io.adc_mcp3008 import MCP3008ADC
from .. import config_statics as cfg
except ImportError:
MCP3008ADC = None # type: ignore
cfg = None # type: ignore
# -------------------------------------------------------------
# Ortak durum enum'u
# -------------------------------------------------------------
class SafetyState:
SAFE = "SAFE"
WARN = "WARN"
ALARM = "ALARM"
# -------------------------------------------------------------
# Ortak base sınıf
# -------------------------------------------------------------
@dataclass
class BaseAnalogSensor:
"""
MCP3008 üzerinden tek bir analog kanalı temsil eden temel sınıf.
Özellikler:
-----------
- adc : MCP3008ADC örneği
- channel : int kanal no (0..7)
- name : mantıksal isim (örn: "gas", "pressure")
- last_raw : son okunan ham değer (0..1023)
- last_volt : son okunan volt cinsinden değer
"""
adc: MCP3008ADC
name: str
channel: Optional[int] = None
last_raw: Optional[int] = None
last_volt: Optional[float] = None
def __post_init__(self) -> None:
# Eğer kanal configten alınacaksa burada çöz
if self.channel is None and cfg is not None:
ch_map = getattr(cfg, "ADC_CHANNELS", {})
if self.name in ch_map:
self.channel = int(ch_map[self.name])
if self.channel is None:
raise ValueError(f"{self.__class__.__name__}: '{self.name}' için kanal bulunamadı.")
# ------------------------------------------------------------------
def read_raw(self) -> Optional[int]:
"""
ADC'den ham değeri okur (0..1023).
"""
if self.channel is None:
return None
raw = self.adc.read_raw(self.channel)
self.last_raw = raw
return raw
def read_voltage(self) -> Optional[float]:
"""
ADC'den raw okur ve volt cinsine çevirir.
"""
if self.channel is None:
return None
volt = self.adc.read_voltage(self.channel)
self.last_volt = volt
return volt
def update(self) -> Optional[int]:
"""
Varsayılan olarak sadece raw okur; alt sınıflar state hesaplarını
kendi override'larında yapar.
"""
return self.read_raw()
def summary(self) -> str:
return f"{self.__class__.__name__}(name={self.name}, ch={self.channel}, raw={self.last_raw}, V={self.last_volt})"
# -------------------------------------------------------------
# Gaz sensörü (MQ-4) kill switch mantığı ile
# -------------------------------------------------------------
@dataclass
class GasAnalogSensor(BaseAnalogSensor):
"""
Gaz sensörü (MQ-4) için analog ve güvenlik mantığı.
State mantığı:
- raw >= alarm_threshold → ALARM, latched
- raw >= warn_threshold → WARN (latched varsa ALARM)
- trend (slope) ile hızlı artış + warn üstü → ALARM
- Diğer durumlarda SAFE (latched yoksa).
latched_alarm True olduğu sürece:
- state ALARM olarak kalır
- should_shutdown_system() True döner
"""
warn_threshold: int = field(default=150)
alarm_threshold: int = field(default=250)
slope_min_delta: int = field(default=30)
slope_window: int = field(default=5)
history_len: int = field(default=20)
state: str = SafetyState.SAFE
latched_alarm: bool = False
_history: list[int] = field(default_factory=list)
def __post_init__(self) -> None:
super().__post_init__()
# Konfig override
if cfg is not None:
self.warn_threshold = int(getattr(cfg, "GAS_WARN_THRESHOLD_RAW", self.warn_threshold))
self.alarm_threshold = int(getattr(cfg, "GAS_ALARM_THRESHOLD_RAW", self.alarm_threshold))
self.slope_min_delta = int(getattr(cfg, "GAS_SLOPE_MIN_DELTA", self.slope_min_delta))
self.slope_window = int(getattr(cfg, "GAS_SLOPE_WINDOW", self.slope_window))
# ------------------------------------------------------------------
def reset_latch(self) -> None:
"""
Gaz alarm latch'ini manuel olarak sıfırlar.
"""
self.latched_alarm = False
# state sonraki update ile yeniden değerlendirilecek.
def update(self) -> Optional[int]:
"""
Ham değeri okur ve state hesaplar.
"""
raw = self.read_raw()
if raw is None:
return None
# history güncelle
self._history.append(raw)
if len(self._history) > self.history_len:
self._history = self._history[-self.history_len:]
self._evaluate_state(raw)
return raw
def _evaluate_state(self, raw: int) -> None:
# Trend kontrolü
slope_alarm = False
if len(self._history) >= self.slope_window:
first = self._history[-self.slope_window]
delta = raw - first
if delta >= self.slope_min_delta:
slope_alarm = True
# Eşik mantığı
if raw >= self.alarm_threshold or (slope_alarm and raw >= self.warn_threshold):
self.state = SafetyState.ALARM
self.latched_alarm = True
elif raw >= self.warn_threshold:
if self.latched_alarm:
self.state = SafetyState.ALARM
else:
self.state = SafetyState.WARN
else:
if self.latched_alarm:
self.state = SafetyState.ALARM
else:
self.state = SafetyState.SAFE
def should_shutdown_system(self) -> bool:
"""
Gaz açısından sistemin tamamen kapatılması gerekip gerekmediğini
söyler.
"""
return self.latched_alarm
def summary(self) -> str:
return (
f"GasAnalogSensor(ch={self.channel}, raw={self.last_raw}, "
f"state={self.state}, latched={self.latched_alarm})"
)
# -------------------------------------------------------------
# Basınç sensörü limit kontrolü
# -------------------------------------------------------------
@dataclass
class PressureAnalogSensor(BaseAnalogSensor):
"""
Su hattı basınç sensörü.
State mantığı:
- raw < (min_raw - warn_hyst) veya raw > (max_raw + warn_hyst) → WARN
- min_raw <= raw <= max_raw → SAFE
- Aradaki buffer bölgede state korunur.
"""
min_raw: int = field(default=200)
max_raw: int = field(default=900)
warn_hyst: int = field(default=20)
state: str = SafetyState.SAFE
def __post_init__(self) -> None:
super().__post_init__()
if cfg is not None:
self.min_raw = int(getattr(cfg, "PRESSURE_MIN_RAW", self.min_raw))
self.max_raw = int(getattr(cfg, "PRESSURE_MAX_RAW", self.max_raw))
self.warn_hyst = int(getattr(cfg, "PRESSURE_WARN_HYST", self.warn_hyst))
def update(self) -> Optional[int]:
raw = self.read_raw()
if raw is None:
return None
self._evaluate_state(raw)
return raw
def _evaluate_state(self, raw: int) -> None:
if raw < (self.min_raw - self.warn_hyst) or raw > (self.max_raw + self.warn_hyst):
self.state = SafetyState.WARN
elif self.min_raw <= raw <= self.max_raw:
self.state = SafetyState.SAFE
# Buffer bölgede state korunur.
def is_pressure_ok(self) -> bool:
return self.state == SafetyState.SAFE
def summary(self) -> str:
return (
f"PressureAnalogSensor(ch={self.channel}, raw={self.last_raw}, "
f"state={self.state}, min={self.min_raw}, max={self.max_raw})"
)
# -------------------------------------------------------------
# Yağmur sensörü basit dry/wet mantığı
# -------------------------------------------------------------
@dataclass
class RainAnalogSensor(BaseAnalogSensor):
"""
Yağmur sensörü (analog).
Basit model:
- raw <= dry_threshold → DRY
- raw >= wet_threshold → WET
- arası → MID
Gerektiğinde bu sınıf geliştirilebilir (ör. şiddetli yağmur vs.).
"""
dry_threshold: int = field(default=100)
wet_threshold: int = field(default=400)
state: str = "UNKNOWN" # "DRY", "MID", "WET"
def __post_init__(self) -> None:
super().__post_init__()
# İleride configten override eklenebilir:
if cfg is not None:
self.dry_threshold = int(getattr(cfg, "RAIN_DRY_THRESHOLD_RAW", self.dry_threshold))
self.wet_threshold = int(getattr(cfg, "RAIN_WET_THRESHOLD_RAW", self.wet_threshold))
def update(self) -> Optional[int]:
raw = self.read_raw()
if raw is None:
return None
self._evaluate_state(raw)
return raw
def _evaluate_state(self, raw: int) -> None:
if raw <= self.dry_threshold:
self.state = "DRY"
elif raw >= self.wet_threshold:
self.state = "WET"
else:
self.state = "MID"
def is_raining(self) -> bool:
return self.state == "WET"
def summary(self) -> str:
return (
f"RainAnalogSensor(ch={self.channel}, raw={self.last_raw}, "
f"state={self.state}, dry_th={self.dry_threshold}, wet_th={self.wet_threshold})"
)
# -------------------------------------------------------------
# LDR sensörü ışık seviyesi
# -------------------------------------------------------------
@dataclass
class LDRAnalogSensor(BaseAnalogSensor):
"""
LDR (ışık) sensörü.
Basit model:
- raw <= dark_threshold → DARK
- raw >= bright_threshold → BRIGHT
- arası → MID
Bu bilgi:
- dış ortam karanlık/aydınlık
- gece/gündüz teyidi
- aydınlatma / gösterge kararları
için kullanılabilir.
"""
dark_threshold: int = field(default=200)
bright_threshold: int = field(default=800)
state: str = "UNKNOWN" # "DARK", "MID", "BRIGHT"
def __post_init__(self) -> None:
super().__post_init__()
if cfg is not None:
self.dark_threshold = int(getattr(cfg, "LDR_DARK_THRESHOLD_RAW", self.dark_threshold))
self.bright_threshold = int(getattr(cfg, "LDR_BRIGHT_THRESHOLD_RAW", self.bright_threshold))
def update(self) -> Optional[int]:
raw = self.read_raw()
if raw is None:
return None
self._evaluate_state(raw)
return raw
def _evaluate_state(self, raw: int) -> None:
if raw <= self.dark_threshold:
self.state = "DARK"
elif raw >= self.bright_threshold:
self.state = "BRIGHT"
else:
self.state = "MID"
def is_dark(self) -> bool:
return self.state == "DARK"
def is_bright(self) -> bool:
return self.state == "BRIGHT"
def summary(self) -> str:
return (
f"LDRAnalogSensor(ch={self.channel}, raw={self.last_raw}, "
f"state={self.state}, dark_th={self.dark_threshold}, bright_th={self.bright_threshold})"
)
# -------------------------------------------------------------
# Tümünü toplayan minik hub (opsiyonel)
# -------------------------------------------------------------
class AnalogSensorsHub:
"""
MCP3008 üstündeki tüm analog sensörleri yöneten yardımcı sınıf.
- pressure : PressureAnalogSensor
- gas : GasAnalogSensor
- rain : RainAnalogSensor
- ldr : LDRAnalogSensor
"""
def __init__(self, adc: MCP3008ADC) -> None:
self.adc = adc
self.pressure = PressureAnalogSensor(adc=self.adc, name="pressure")
self.gas = GasAnalogSensor(adc=self.adc, name="gas")
self.rain = RainAnalogSensor(adc=self.adc, name="rain")
self.ldr = LDRAnalogSensor(adc=self.adc, name="ldr")
def update_all(self) -> Dict[str, Optional[int]]:
"""
Tüm sensörleri günceller ve ham değerleri döndürür.
"""
return {
"pressure": self.pressure.update(),
"gas": self.gas.update(),
"rain": self.rain.update(),
"ldr": self.ldr.update(),
}
def should_shutdown_system(self) -> bool:
"""
Gaz sensörü açısından kill-switch gerekip gerekmediğini söyler.
"""
return self.gas.should_shutdown_system()