AnasSarkiz/magnetic-shutter-remote-r8

A USB‑C rechargeable battery-powered ESP32‑C3 wireless controller with Li‑ion charging, protected 3.3‑V buck‑boost regulation, thermal/safety monitoring, status LEDs, buttons, programming UART, crystal clock, and 2.4‑GHz antenna matching network.

Version
0.4.0-prototype
License
unset
Stars
0

scripts/r7_power_budget.py

"""R7 feasibility calculations; assumptions are not measured efficiencies/runtime."""

from dataclasses import dataclass, asdict
import json


@dataclass(frozen=True)
class Budget:
    led_power_w: float
    minimum_loaded_supply_v: float = 3.3
    assumed_minimum_conversion_efficiency: float = 0.80
    radio_and_control_reserve_a: float = 0.020
    capacity_ah: float = 0.850
    nominal_battery_v: float = 3.7
    assumed_usable_energy_fraction: float = 0.65
    maximum_continuous_battery_a: float = 0.850

    def calculate(self):
        if not (0 < self.assumed_minimum_conversion_efficiency <= 1):
            raise ValueError("Efficiency must be in (0, 1]")
        if not (0 < self.assumed_usable_energy_fraction <= 1):
            raise ValueError("Usable energy fraction must be in (0, 1]")
        if min(self.led_power_w, self.minimum_loaded_supply_v,
               self.capacity_ah, self.nominal_battery_v) <= 0:
            raise ValueError("Power, voltage, and capacity must be positive")
        torch_a = self.led_power_w / self.assumed_minimum_conversion_efficiency / self.minimum_loaded_supply_v
        total_a = torch_a + self.radio_and_control_reserve_a
        usable_wh = self.capacity_ah * self.nominal_battery_v * self.assumed_usable_energy_fraction
        load_w = self.led_power_w / self.assumed_minimum_conversion_efficiency + self.nominal_battery_v * self.radio_and_control_reserve_a
        return {
            "inputs": asdict(self),
            "torch_input_a": torch_a,
            "total_battery_a": total_a,
            "continuous_current_margin_a": self.maximum_continuous_battery_a - total_a,
            "estimated_runtime_minutes": 60 * usable_wh / load_w,
            "runtime_status": "MODEL ONLY — POST-PROTOTYPE PHYSICAL VALIDATION",
        }


def tps2553_limits(resistor_kohm: float, tolerance: float = 0.01):
    """TI SLVS841F section 9.5.1, equation 1. R is in kOhm; results in mA."""
    if not 15 <= resistor_kohm * (1 - tolerance) <= resistor_kohm * (1 + tolerance) <= 232:
        raise ValueError("RILIM outside TI's recommended range")
    if not 0 <= tolerance < 1:
        raise ValueError("Invalid resistor tolerance")
    return {
        "minimum_ma": 25230 / (resistor_kohm * (1 + tolerance)) ** 1.016,
        "nominal_ma": 23950 / resistor_kohm ** 0.977,
        "maximum_ma": 22980 / (resistor_kohm * (1 - tolerance)) ** 0.94,
    }


if __name__ == "__main__":
    print(json.dumps({
        "revision": "R7 feasibility — components/layout not yet qualified",
        "date": "2026-10-04",
        "battery_candidate": "Jauch LP603443JU+PCM+2 WIRES 70MM / 246512 (850 mAh minimum, external, no C-number)",
        "scenarios": [Budget(p).calculate() for p in (1.5, 1.7, 2.0)],
        "current_limiter_candidate_35_7k_1pct": tps2553_limits(35.7),
        "gates": {
            "led_voltage_current_envelope": "OSRAM 2700/6500 K CRI90 pair selected; final driver/load setting pending",
            "loaded_supply_floor": "3.3 V DESIGN ASSUMPTION; NOT IMPLEMENTED OR VALIDATED",
            "efficiency": "0.80 ASSUMPTION; PROTOTYPE MEASUREMENT REQUIRED",
            "battery_harness_and_pack_envelope": "Jauch Rev1.1 maximum45x34.5x6.6 mm; red+/black-; final enclosure/harness fit pending",
            "thermal": "PENDING LED SELECTION AND ENCLOSURE ANALYSIS",
            "no_firmware_only_fault_protection": "HARDWARE CURRENT LIMIT REQUIRED",
        },
    }, indent=2))