@@ -0,0 +1,4 @@
|
||||
from .scales_reader import ScalesReader
|
||||
from .gpio_controller import GPIOController
|
||||
from .hx711_wrapper import HX711UnavailableError, HX711Wrapper
|
||||
|
||||
@@ -0,0 +1,107 @@
|
||||
import threading
|
||||
import time
|
||||
from typing import Optional
|
||||
|
||||
|
||||
class GPIOController:
|
||||
"""Thread-safe singleton controller for GPIO output pin."""
|
||||
|
||||
_instance = None
|
||||
_instance_lock = threading.Lock()
|
||||
|
||||
def __init__(self, pin: int, simulation_mode: bool = False):
|
||||
self.pin = pin
|
||||
self.simulation_mode = simulation_mode
|
||||
self._lock = threading.Lock()
|
||||
self._blink_stop_event = threading.Event()
|
||||
self._blink_thread: Optional[threading.Thread] = None
|
||||
self._state = False
|
||||
|
||||
self._gpio = None
|
||||
self._gpio_available = False
|
||||
if not self.simulation_mode:
|
||||
try:
|
||||
import RPi.GPIO as GPIO # type: ignore[import-not-found]
|
||||
|
||||
self._gpio = GPIO
|
||||
self._gpio.setmode(GPIO.BCM)
|
||||
self._gpio.setup(self.pin, GPIO.OUT)
|
||||
self._gpio.output(self.pin, GPIO.LOW)
|
||||
self._gpio_available = True
|
||||
except Exception:
|
||||
self._gpio = None
|
||||
self._gpio_available = False
|
||||
|
||||
@classmethod
|
||||
def get_instance(cls, pin: int = 18, simulation_mode: bool = False):
|
||||
with cls._instance_lock:
|
||||
if cls._instance is None:
|
||||
cls._instance = cls(pin=pin, simulation_mode=simulation_mode)
|
||||
return cls._instance
|
||||
|
||||
@classmethod
|
||||
def _reset_for_tests(cls):
|
||||
with cls._instance_lock:
|
||||
if cls._instance is not None:
|
||||
cls._instance.cleanup()
|
||||
cls._instance = None
|
||||
|
||||
def _write_locked(self, state: bool) -> None:
|
||||
self._state = state
|
||||
if self._gpio_available and self._gpio is not None:
|
||||
self._gpio.output(self.pin, self._gpio.HIGH if state else self._gpio.LOW)
|
||||
|
||||
def on(self) -> None:
|
||||
with self._lock:
|
||||
self._write_locked(True)
|
||||
|
||||
def off(self) -> None:
|
||||
with self._lock:
|
||||
self._write_locked(False)
|
||||
|
||||
def is_on(self) -> bool:
|
||||
with self._lock:
|
||||
return bool(self._state)
|
||||
|
||||
@property
|
||||
def rpi_gpio_available(self) -> bool:
|
||||
"""True, если поднят настоящий RPi.GPIO (Raspberry Pi). В симуляции всегда False."""
|
||||
return bool(self._gpio_available)
|
||||
|
||||
def stop_blink(self) -> None:
|
||||
self._blink_stop_event.set()
|
||||
if self._blink_thread and self._blink_thread.is_alive():
|
||||
self._blink_thread.join(timeout=2)
|
||||
self._blink_thread = None
|
||||
self._blink_stop_event.clear()
|
||||
|
||||
def blink(self, times: int = 3, delay: float = 0.2) -> None:
|
||||
if times <= 0:
|
||||
return
|
||||
|
||||
self.stop_blink()
|
||||
|
||||
def _worker():
|
||||
for _ in range(times):
|
||||
if self._blink_stop_event.is_set():
|
||||
break
|
||||
self.on()
|
||||
time.sleep(delay)
|
||||
if self._blink_stop_event.is_set():
|
||||
break
|
||||
self.off()
|
||||
time.sleep(delay)
|
||||
self.off()
|
||||
|
||||
self._blink_thread = threading.Thread(target=_worker, daemon=True, name="gpio-blink")
|
||||
self._blink_thread.start()
|
||||
|
||||
def cleanup(self) -> None:
|
||||
self.stop_blink()
|
||||
self.off()
|
||||
if self._gpio_available and self._gpio is not None:
|
||||
try:
|
||||
self._gpio.cleanup(self.pin)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
@@ -0,0 +1,74 @@
|
||||
import random
|
||||
|
||||
|
||||
class HX711UnavailableError(RuntimeError):
|
||||
"""Нет HX711 при выключенной симуляции или срыв чтения с датчика."""
|
||||
|
||||
|
||||
class HX711Wrapper:
|
||||
"""Minimal wrapper for HX711 access with simulation fallback."""
|
||||
|
||||
def __init__(self, simulation_mode: bool = False, dout_pin: int = 2, pd_sck_pin: int = 3):
|
||||
self._sim_weight = 0.0
|
||||
self._device = None
|
||||
self._dout_pin = int(dout_pin)
|
||||
self._pd_sck_pin = int(pd_sck_pin)
|
||||
self.simulation_mode = False
|
||||
self.apply_simulation_mode(simulation_mode)
|
||||
|
||||
def apply_simulation_mode(self, simulation_mode: bool) -> None:
|
||||
"""Переключить симуляцию и при необходимости поднять/сбросить драйвер HX711."""
|
||||
simulation_mode = bool(simulation_mode)
|
||||
if simulation_mode == self.simulation_mode:
|
||||
if simulation_mode or self._device is not None:
|
||||
return
|
||||
self.simulation_mode = simulation_mode
|
||||
if simulation_mode:
|
||||
self._device = None
|
||||
return
|
||||
self._ensure_device()
|
||||
|
||||
def _ensure_device(self) -> None:
|
||||
if self._device is not None:
|
||||
return
|
||||
try:
|
||||
from hx711 import HX711 # type: ignore[import-not-found]
|
||||
|
||||
self._device = HX711(dout_pin=self._dout_pin, pd_sck_pin=self._pd_sck_pin)
|
||||
except Exception:
|
||||
self._device = None
|
||||
|
||||
def read_weight_sample(self) -> float:
|
||||
if self.simulation_mode:
|
||||
self._sim_weight = max(0.0, self._sim_weight + random.uniform(-1.0, 2.5))
|
||||
return self._sim_weight
|
||||
if self._device is None:
|
||||
raise HX711UnavailableError(
|
||||
"HX711 недоступен: модуль не загрузился или датчик не инициализирован"
|
||||
)
|
||||
try:
|
||||
return self._read_raw_mean(times=3)
|
||||
except HX711UnavailableError:
|
||||
raise
|
||||
except Exception as exc:
|
||||
raise HX711UnavailableError(f"Ошибка чтения HX711: {exc}") from exc
|
||||
|
||||
def _read_raw_mean(self, times: int = 3) -> float:
|
||||
"""Среднее сырых отсчётов: tatobari (get_raw_data) или gandalf15 (get_raw_data_mean)."""
|
||||
device = self._device
|
||||
if device is None:
|
||||
raise HX711UnavailableError(
|
||||
"HX711 недоступен: модуль не загрузился или датчик не инициализирован"
|
||||
)
|
||||
if hasattr(device, "get_raw_data_mean"):
|
||||
return float(device.get_raw_data_mean(times))
|
||||
if hasattr(device, "get_raw_data"):
|
||||
raw = device.get_raw_data(times=times)
|
||||
if isinstance(raw, list):
|
||||
if not raw:
|
||||
return 0.0
|
||||
return float(sum(raw) / len(raw))
|
||||
return float(raw)
|
||||
raise HX711UnavailableError(
|
||||
"HX711: нет метода чтения (ожидается get_raw_data или get_raw_data_mean)"
|
||||
)
|
||||
@@ -0,0 +1,440 @@
|
||||
import threading
|
||||
import time
|
||||
from collections import deque
|
||||
from queue import Empty, Queue
|
||||
from statistics import median
|
||||
from typing import Deque, Dict, Optional
|
||||
|
||||
from .hx711_wrapper import HX711UnavailableError, HX711Wrapper
|
||||
|
||||
|
||||
def _scale_hardware_platform() -> bool:
|
||||
try:
|
||||
from app.services.hardware_settings_service import is_scale_hardware_platform
|
||||
|
||||
return is_scale_hardware_platform()
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
|
||||
class ScalesReader:
|
||||
"""Thread-safe scales reader with configurable filter pipeline."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
simulation_mode: bool,
|
||||
read_interval: float,
|
||||
samples_per_read: int,
|
||||
sample_history_size: int,
|
||||
median_window: int,
|
||||
avg_window: int,
|
||||
weights,
|
||||
smoothing_factor: float,
|
||||
round_to_step: float,
|
||||
tare_raw_window: int,
|
||||
hx711_dout_pin: int,
|
||||
hx711_pd_sck_pin: int,
|
||||
):
|
||||
self.simulation_mode = simulation_mode
|
||||
self.read_interval = read_interval
|
||||
self.samples_per_read = samples_per_read
|
||||
self.sample_history_size = sample_history_size
|
||||
self.median_window = max(1, median_window)
|
||||
self.avg_window = max(1, avg_window)
|
||||
self.weights = list(weights) if weights else [0.2, 0.3, 0.5]
|
||||
self.smoothing_factor = smoothing_factor
|
||||
self.round_to_step = round_to_step
|
||||
self.tare_raw_window = max(1, tare_raw_window)
|
||||
|
||||
self._stop_event = threading.Event()
|
||||
self._queue: Queue[float] = Queue(maxsize=max(200, sample_history_size))
|
||||
self._lock = threading.Lock()
|
||||
self._reader_thread: Optional[threading.Thread] = None
|
||||
self._processor_thread: Optional[threading.Thread] = None
|
||||
|
||||
self._raw_history: Deque[float] = deque(maxlen=sample_history_size)
|
||||
self._filtered_history: Deque[float] = deque(maxlen=sample_history_size)
|
||||
self._weight_history: Deque[float] = deque(maxlen=sample_history_size)
|
||||
self._current_weight = 0.0
|
||||
self._tare_raw = 0.0
|
||||
self._counts_per_kg = 1.0
|
||||
self._sim_weight = 0.0
|
||||
self._sim_weight_kg = 0.0
|
||||
self._last_hx711_error: Optional[str] = None
|
||||
self._hx711_error_active = False
|
||||
self._error_streak = 0
|
||||
self._last_success_at: Optional[float] = None
|
||||
self._flask_app = None
|
||||
self._scale_hardware_platform = _scale_hardware_platform()
|
||||
self._hx711 = HX711Wrapper(
|
||||
simulation_mode=not self._hx711_real_hardware(),
|
||||
dout_pin=hx711_dout_pin,
|
||||
pd_sck_pin=hx711_pd_sck_pin,
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def from_config(cls, config: Dict):
|
||||
return cls(
|
||||
simulation_mode=bool(config.get("SIMULATION_MODE", False)),
|
||||
read_interval=float(config.get("READ_INTERVAL", 0.05)),
|
||||
samples_per_read=int(config.get("SAMPLES_PER_READ", 3)),
|
||||
sample_history_size=int(config.get("SAMPLE_HISTORY_SIZE", 200)),
|
||||
median_window=int(config.get("MEDIAN_WINDOW", 6)),
|
||||
avg_window=int(config.get("AVG_WINDOW", 4)),
|
||||
weights=config.get("WEIGHTS", (0.2, 0.3, 0.5)),
|
||||
smoothing_factor=float(config.get("SMOOTHING_FACTOR", 0.3)),
|
||||
round_to_step=float(config.get("ROUND_TO_STEP", 1)),
|
||||
tare_raw_window=int(config.get("TARE_RAW_WINDOW", 2)),
|
||||
hx711_dout_pin=int(config.get("HX711_DOUT_PIN", 2)),
|
||||
hx711_pd_sck_pin=int(config.get("HX711_PD_SCK_PIN", 3)),
|
||||
)
|
||||
|
||||
def start(self) -> None:
|
||||
with self._lock:
|
||||
if self._reader_thread and self._reader_thread.is_alive():
|
||||
return
|
||||
self._load_runtime_settings_locked()
|
||||
self._stop_event.clear()
|
||||
self._reader_thread = threading.Thread(
|
||||
target=self._reader_loop, daemon=True, name="scales-reader"
|
||||
)
|
||||
self._processor_thread = threading.Thread(
|
||||
target=self._processor_loop, daemon=True, name="scales-processor"
|
||||
)
|
||||
self._reader_thread.start()
|
||||
self._processor_thread.start()
|
||||
|
||||
def stop(self) -> None:
|
||||
self._stop_event.set()
|
||||
if self._reader_thread and self._reader_thread.is_alive():
|
||||
self._reader_thread.join(timeout=2)
|
||||
if self._processor_thread and self._processor_thread.is_alive():
|
||||
self._processor_thread.join(timeout=2)
|
||||
|
||||
def tare(self) -> float:
|
||||
with self._lock:
|
||||
if not self._raw_history:
|
||||
raise ValueError("Недостаточно данных для обнуления")
|
||||
window = list(self._raw_history)[-self.tare_raw_window :]
|
||||
self._tare_raw = float(sum(window) / max(1, len(window)))
|
||||
self._persist_hardware_settings_locked()
|
||||
return self._tare_raw
|
||||
|
||||
def calibrate(self, calibration_factor: float) -> float:
|
||||
if calibration_factor <= 0:
|
||||
raise ValueError("calibration_factor must be positive")
|
||||
with self._lock:
|
||||
self._counts_per_kg = calibration_factor
|
||||
self._persist_hardware_settings_locked()
|
||||
return self._counts_per_kg
|
||||
|
||||
def set_counts_per_kg(self, counts_per_kg: float) -> float:
|
||||
return self.calibrate(counts_per_kg)
|
||||
|
||||
def get_current_weight(self) -> int:
|
||||
with self._lock:
|
||||
return int(self._current_weight)
|
||||
|
||||
def get_calibration_factor(self) -> float:
|
||||
with self._lock:
|
||||
return float(self._counts_per_kg)
|
||||
|
||||
def get_counts_per_kg(self) -> float:
|
||||
return self.get_calibration_factor()
|
||||
|
||||
def bind_flask_app(self, app) -> None:
|
||||
"""Для журнала периферии из фонового потока чтения."""
|
||||
self._flask_app = app
|
||||
|
||||
def get_last_hx711_error(self) -> Optional[str]:
|
||||
with self._lock:
|
||||
return self._last_hx711_error
|
||||
|
||||
def get_simulation_state(self) -> dict:
|
||||
with self._lock:
|
||||
return {
|
||||
"simulation_mode": bool(self.simulation_mode),
|
||||
"simulation_weight_kg": float(self._sim_weight_kg),
|
||||
"scale_hardware_platform": bool(self._scale_hardware_platform),
|
||||
}
|
||||
|
||||
def _hx711_real_hardware(self) -> bool:
|
||||
return bool(not self.simulation_mode and self._scale_hardware_platform)
|
||||
|
||||
def _should_poll_scales(self) -> bool:
|
||||
return bool(self.simulation_mode or self._scale_hardware_platform)
|
||||
|
||||
def _sync_hx711_wrapper_mode(self) -> None:
|
||||
# На не-ARM без симуляции драйвер не поднимаем — нет GPIO/HX711.
|
||||
self._hx711.apply_simulation_mode(not self._hx711_real_hardware())
|
||||
|
||||
def set_simulation_mode(self, enabled: bool) -> dict:
|
||||
enabled = bool(enabled)
|
||||
with self._lock:
|
||||
self.simulation_mode = enabled
|
||||
self._sync_hx711_wrapper_mode()
|
||||
if enabled:
|
||||
self._last_hx711_error = None
|
||||
self._hx711_error_active = False
|
||||
self._error_streak = 0
|
||||
self._apply_simulated_weight_locked()
|
||||
else:
|
||||
self._last_hx711_error = None
|
||||
self._hx711_error_active = False
|
||||
self._error_streak = 0
|
||||
self._persist_hardware_settings_locked()
|
||||
self._sync_gpio_simulation_mode(enabled)
|
||||
return self.get_simulation_state()
|
||||
|
||||
def set_simulation_weight(self, weight_kg: float) -> int:
|
||||
with self._lock:
|
||||
if not self.simulation_mode:
|
||||
raise ValueError("Симуляция выключена — включите её перед изменением веса")
|
||||
self._sim_weight_kg = max(0.0, float(weight_kg))
|
||||
self._apply_simulated_weight_locked()
|
||||
self._persist_hardware_settings_locked()
|
||||
return int(self._sim_weight_kg)
|
||||
|
||||
def adjust_simulation_weight(self, delta_kg: int) -> int:
|
||||
with self._lock:
|
||||
if not self.simulation_mode:
|
||||
raise ValueError("Симуляция выключена — включите её перед изменением веса")
|
||||
self._sim_weight_kg = max(0.0, self._sim_weight_kg + int(delta_kg))
|
||||
self._apply_simulated_weight_locked()
|
||||
self._persist_hardware_settings_locked()
|
||||
return int(self._sim_weight_kg)
|
||||
|
||||
@staticmethod
|
||||
def _sync_gpio_simulation_mode(enabled: bool) -> None:
|
||||
try:
|
||||
from .gpio_controller import GPIOController
|
||||
|
||||
inst = GPIOController._instance
|
||||
if inst is not None:
|
||||
inst.simulation_mode = bool(enabled)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
def _apply_simulated_weight_locked(self) -> None:
|
||||
cpk = self._counts_per_kg if self._counts_per_kg > 0 else 1.0
|
||||
raw = self._tare_raw + self._sim_weight_kg * cpk
|
||||
self._sim_weight = raw
|
||||
self._current_weight = float(int(self._sim_weight_kg))
|
||||
self._raw_history.append(raw)
|
||||
self._weight_history.append(float(self._current_weight))
|
||||
|
||||
def get_scale_debug_snapshot(self) -> dict:
|
||||
with self._lock:
|
||||
raw = list(self._raw_history)
|
||||
tare = float(self._tare_raw)
|
||||
cpk = float(self._counts_per_kg)
|
||||
streak = int(self._error_streak)
|
||||
last_ok = self._last_success_at
|
||||
last_raw = float(raw[-1]) if raw else None
|
||||
raw_min = float(min(raw)) if raw else None
|
||||
raw_max = float(max(raw)) if raw else None
|
||||
return {
|
||||
"counts_per_kg": cpk,
|
||||
"tare_raw": tare,
|
||||
"raw_history_len": len(raw),
|
||||
"last_raw": last_raw,
|
||||
"raw_min_recent": raw_min,
|
||||
"raw_max_recent": raw_max,
|
||||
"error_streak": streak,
|
||||
"last_success_at": last_ok,
|
||||
}
|
||||
|
||||
def get_scale_health(self) -> dict:
|
||||
"""Для API: есть ли достоверные отсчёты с датчика (при SIMULATION_MODE=0)."""
|
||||
with self._lock:
|
||||
sim = self.simulation_mode
|
||||
driver_up = getattr(self._hx711, "_device", None) is not None
|
||||
err = self._last_hx711_error
|
||||
has_samples = len(self._raw_history) > 0
|
||||
if sim:
|
||||
return {"simulation_mode": True, "hx711_ok": True, "hx711_error": None}
|
||||
if not self._scale_hardware_platform:
|
||||
return {
|
||||
"simulation_mode": False,
|
||||
"hx711_ok": False,
|
||||
"hx711_skipped": True,
|
||||
"hx711_error": None,
|
||||
}
|
||||
if not driver_up:
|
||||
return {
|
||||
"simulation_mode": False,
|
||||
"hx711_ok": False,
|
||||
"hx711_error": err or "HX711 не инициализирован",
|
||||
}
|
||||
if err:
|
||||
return {"simulation_mode": False, "hx711_ok": False, "hx711_error": err}
|
||||
if not has_samples:
|
||||
return {
|
||||
"simulation_mode": False,
|
||||
"hx711_ok": False,
|
||||
"hx711_error": "Ожидание первого отсчёта с датчика",
|
||||
}
|
||||
return {"simulation_mode": False, "hx711_ok": True, "hx711_error": None}
|
||||
|
||||
def average_recent_raw(self, window: Optional[int] = None) -> float:
|
||||
with self._lock:
|
||||
if not self._raw_history:
|
||||
raise ValueError("Нет сырых данных для расчёта")
|
||||
size = max(1, int(window or self.tare_raw_window))
|
||||
values = list(self._raw_history)[-size:]
|
||||
return float(sum(values) / max(1, len(values)))
|
||||
|
||||
def calibration_debug_payload(self) -> dict:
|
||||
"""Снимок сырых отсчётов для шаблона калибровки (как в легаси weight_data)."""
|
||||
with self._lock:
|
||||
raw = list(self._raw_history)
|
||||
weights = list(self._weight_history)
|
||||
return {
|
||||
"last_50_raw": raw[-50:],
|
||||
"readings_history": raw[-20:],
|
||||
"weight_history": weights[-20:],
|
||||
}
|
||||
|
||||
def _load_runtime_settings_locked(self) -> None:
|
||||
try:
|
||||
from app.models import HardwareSetting
|
||||
from app import db
|
||||
|
||||
row = db.session.get(HardwareSetting, 1)
|
||||
if row is None:
|
||||
row = HardwareSetting(
|
||||
id=1,
|
||||
counts_per_kg=float(self._counts_per_kg or 1.0),
|
||||
tare_raw=float(self._tare_raw or 0.0),
|
||||
simulation_mode=bool(self.simulation_mode),
|
||||
simulation_weight_kg=float(self._sim_weight_kg or 0.0),
|
||||
)
|
||||
db.session.add(row)
|
||||
db.session.commit()
|
||||
else:
|
||||
self._counts_per_kg = float(row.counts_per_kg or 1.0)
|
||||
self._tare_raw = float(row.tare_raw or 0.0)
|
||||
self._sim_weight_kg = max(0.0, float(row.simulation_weight_kg or 0.0))
|
||||
sim = bool(row.simulation_mode)
|
||||
self.simulation_mode = sim
|
||||
self._sync_hx711_wrapper_mode()
|
||||
if sim:
|
||||
self._apply_simulated_weight_locked()
|
||||
except Exception:
|
||||
self._counts_per_kg = 1.0
|
||||
self._tare_raw = 0.0
|
||||
self._sim_weight_kg = 0.0
|
||||
|
||||
def _persist_hardware_settings_locked(self) -> None:
|
||||
from app.models import HardwareSetting
|
||||
from app import db
|
||||
|
||||
row = db.session.get(HardwareSetting, 1)
|
||||
if row is None:
|
||||
row = HardwareSetting(id=1)
|
||||
db.session.add(row)
|
||||
row.counts_per_kg = float(self._counts_per_kg)
|
||||
row.tare_raw = float(self._tare_raw)
|
||||
row.simulation_mode = bool(self.simulation_mode)
|
||||
row.simulation_weight_kg = float(self._sim_weight_kg)
|
||||
db.session.commit()
|
||||
|
||||
def _reader_loop(self) -> None:
|
||||
while not self._stop_event.is_set():
|
||||
if not self._should_poll_scales():
|
||||
time.sleep(self.read_interval)
|
||||
continue
|
||||
for _ in range(self.samples_per_read):
|
||||
try:
|
||||
sample = self._read_raw_sample()
|
||||
except HX711UnavailableError as exc:
|
||||
msg = str(exc)
|
||||
with self._lock:
|
||||
self._last_hx711_error = msg
|
||||
self._error_streak += 1
|
||||
was_active = self._hx711_error_active
|
||||
self._hx711_error_active = True
|
||||
self._emit_hx711_state(was_active=was_active, is_error=True, message=msg)
|
||||
continue
|
||||
with self._lock:
|
||||
was_active = self._hx711_error_active
|
||||
self._last_hx711_error = None
|
||||
self._hx711_error_active = False
|
||||
self._error_streak = 0
|
||||
self._last_success_at = time.time()
|
||||
if was_active:
|
||||
self._emit_hx711_state(was_active=True, is_error=False, message="")
|
||||
try:
|
||||
self._queue.put(sample, timeout=0.05)
|
||||
except Exception:
|
||||
pass
|
||||
time.sleep(self.read_interval)
|
||||
|
||||
def _processor_loop(self) -> None:
|
||||
while not self._stop_event.is_set():
|
||||
try:
|
||||
raw = self._queue.get(timeout=0.2)
|
||||
except Empty:
|
||||
continue
|
||||
|
||||
with self._lock:
|
||||
self._raw_history.append(raw)
|
||||
filtered = self._apply_filters_locked()
|
||||
counts_per_kg = self._counts_per_kg if self._counts_per_kg > 0 else 1.0
|
||||
weight_kg = (filtered - self._tare_raw) / counts_per_kg
|
||||
if self.round_to_step > 0:
|
||||
weight_kg = round(weight_kg / self.round_to_step) * self.round_to_step
|
||||
else:
|
||||
weight_kg = round(weight_kg)
|
||||
# Целые килограммы; отрицательные значения допустимы (ниже нуля после tare).
|
||||
self._current_weight = float(int(round(weight_kg)))
|
||||
self._weight_history.append(float(self._current_weight))
|
||||
|
||||
def _emit_hx711_state(self, *, was_active: bool, is_error: bool, message: str) -> None:
|
||||
app = self._flask_app
|
||||
if app is None or self.simulation_mode or not self._scale_hardware_platform:
|
||||
return
|
||||
try:
|
||||
with app.app_context():
|
||||
from app.services.admin_peripheral_monitor import (
|
||||
notify_hx711_error,
|
||||
notify_hx711_recovered,
|
||||
)
|
||||
|
||||
if is_error:
|
||||
notify_hx711_error(app, message)
|
||||
elif was_active:
|
||||
notify_hx711_recovered(app)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
def _read_raw_sample(self) -> float:
|
||||
if self.simulation_mode:
|
||||
cpk = self._counts_per_kg if self._counts_per_kg > 0 else 1.0
|
||||
return self._tare_raw + self._sim_weight_kg * cpk
|
||||
return self._hx711.read_weight_sample()
|
||||
|
||||
def _apply_filters_locked(self) -> float:
|
||||
if not self._raw_history:
|
||||
return 0.0
|
||||
|
||||
values = list(self._raw_history)
|
||||
median_slice = values[-self.median_window :]
|
||||
median_val = float(median(median_slice))
|
||||
|
||||
avg_slice = values[-self.avg_window :]
|
||||
avg_val = float(sum(avg_slice) / max(1, len(avg_slice)))
|
||||
|
||||
weighted_slice = values[-len(self.weights) :]
|
||||
if len(weighted_slice) < len(self.weights):
|
||||
missing = len(self.weights) - len(weighted_slice)
|
||||
weighted_slice = [weighted_slice[0]] * missing + weighted_slice if weighted_slice else [0.0] * len(self.weights)
|
||||
weighted_val = sum(w * x for w, x in zip(self.weights, weighted_slice))
|
||||
|
||||
combined = (median_val + avg_val + weighted_val) / 3.0
|
||||
|
||||
prev = self._filtered_history[-1] if self._filtered_history else combined
|
||||
smoothed = (self.smoothing_factor * combined) + ((1.0 - self.smoothing_factor) * prev)
|
||||
self._filtered_history.append(smoothed)
|
||||
return smoothed
|
||||
|
||||
Reference in New Issue
Block a user