import time import json import threading from flask import Blueprint, Response, current_app, jsonify, request from app.routes.auth_decorators import require_paired_terminal from app.services.hardware import ScalesReader bp = Blueprint("scales", __name__) _scales_reader = None _calibration_state = {"target_weight": None, "initial_raw": None} _calibration_lock = threading.Lock() def _error(message: str, status_code: int = 400): return jsonify({"error": True, "status": "error", "message": message}), status_code def _get_reader() -> ScalesReader: global _scales_reader if _scales_reader is None: _scales_reader = ScalesReader.from_config(current_app.config) _scales_reader.bind_flask_app(current_app._get_current_object()) _scales_reader.start() return _scales_reader @bp.get("/current_weight") @require_paired_terminal def current_weight(): reader = _get_reader() payload = {"weight": reader.get_current_weight(), **reader.get_scale_health()} return jsonify(payload), 200 @bp.post("/tare") @require_paired_terminal def tare_weight(): reader = _get_reader() try: tare_value = reader.tare() except ValueError as e: return jsonify({"status": "error", "message": str(e)}), 400 return jsonify( { "status": "success", "message": "Новый ноль сохранён", "new_zero": round(float(tare_value), 4), "success": True, } ) @bp.post("/calibrate") @require_paired_terminal def calibrate_weight(): data = request.get_json(silent=True) or {} reader = _get_reader() if data.get("target_weight") is not None: try: target_weight = float(data.get("target_weight")) except (TypeError, ValueError): return _error("target_weight должен быть числом", 400) if target_weight <= 0: return _error("target_weight должен быть больше 0", 400) try: initial_raw = reader.average_recent_raw( int(current_app.config.get("TARE_RAW_WINDOW", 2)) ) except ValueError as e: return _error(str(e), 400) with _calibration_lock: _calibration_state["target_weight"] = target_weight _calibration_state["initial_raw"] = initial_raw return jsonify( { "status": "confirm", "message": f"Поместите груз {target_weight}кг и подтвердите", } ) raw_factor = data.get("calibration_factor") if raw_factor is None: return _error("Нужно указать calibration_factor или target_weight", 400) try: factor = float(raw_factor) except (TypeError, ValueError): return _error("calibration_factor должен быть числом", 400) try: applied = reader.calibrate(factor) except ValueError as e: return _error(str(e), 400) return jsonify({"status": "success", "success": True, "calibration_factor": applied}) @bp.post("/calibrate/continue") @require_paired_terminal def calibrate_continue(): reader = _get_reader() with _calibration_lock: target_weight = _calibration_state.get("target_weight") initial_raw = _calibration_state.get("initial_raw") if not target_weight or initial_raw is None: return _error("Калибровка не начата", 400) try: final_raw = reader.average_recent_raw(int(current_app.config.get("TARE_RAW_WINDOW", 2))) counts_per_kg = (final_raw - float(initial_raw)) / float(target_weight) applied = reader.set_counts_per_kg(float(counts_per_kg)) except ValueError as e: return _error(str(e), 400) with _calibration_lock: _calibration_state["target_weight"] = None _calibration_state["initial_raw"] = None return jsonify( { "status": "success", "message": "Калибровка завершена!", "calibration_factor": round(float(applied), 6), } ) @bp.get("/current_raw_data") @require_paired_terminal def current_raw_data(): reader = _get_reader() return jsonify(reader.calibration_debug_payload()) @bp.get("/stream_weight") @require_paired_terminal def stream_weight(): reader = _get_reader() # Интервал нужно взять здесь: внутри генератора контекст приложения уже снят. read_interval = float(current_app.config.get("READ_INTERVAL", 0.05)) def _events(): while True: payload = {"weight": reader.get_current_weight(), **reader.get_scale_health()} yield f"data: {json.dumps(payload, ensure_ascii=False)}\n\n" time.sleep(read_interval) return Response(_events(), mimetype="text/event-stream")