From c7accf6ec6a2c5699bee452b84696eee55f36a05 Mon Sep 17 00:00:00 2001 From: KenwoodFox Date: Sun, 14 Jun 2026 18:26:37 -0400 Subject: [PATCH] Inital ver --- .gitignore | 9 ++++ README.md | 18 ++++++++ requirements.txt | 1 + systemd/towerd.service | 14 ++++++ towerd/__init__.py | 3 ++ towerd/__main__.py | 3 ++ towerd/gpio_ctrl.py | 59 ++++++++++++++++++++++++++ towerd/main.py | 96 ++++++++++++++++++++++++++++++++++++++++++ towerd/thermal.py | 14 ++++++ 9 files changed, 217 insertions(+) create mode 100644 .gitignore create mode 100644 README.md create mode 100644 requirements.txt create mode 100644 systemd/towerd.service create mode 100644 towerd/__init__.py create mode 100644 towerd/__main__.py create mode 100644 towerd/gpio_ctrl.py create mode 100644 towerd/main.py create mode 100644 towerd/thermal.py diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..644aaf7 --- /dev/null +++ b/.gitignore @@ -0,0 +1,9 @@ +venv/ +__pycache__/ +*.py[cod] +*.egg-info/ +dist/ +build/ +.pytest_cache/ +.mypy_cache/ +.ruff_cache/ diff --git a/README.md b/README.md new file mode 100644 index 0000000..cd433bf --- /dev/null +++ b/README.md @@ -0,0 +1,18 @@ +# TowerD + +I wrote this to control the "hardware" on KW1FOX-1 tower. + +its.. not really intended for use anywhere else but, if anything inspires you go ahead and grab it! +Just some simple python. + +# Install + +```shell +sudo git clone https://git.kitsunehosting.net/Kenwood/towerd.git /opt/towerd +cd /opt/towerd +sudo python3 -m venv venv +sudo venv/bin/pip install -r requirements.txt +sudo cp systemd/towerd.service /etc/systemd/system/ +sudo systemctl daemon-reload +sudo systemctl enable --now towerd +``` \ No newline at end of file diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..326d9ff --- /dev/null +++ b/requirements.txt @@ -0,0 +1 @@ +gpiozero>=2.0 diff --git a/systemd/towerd.service b/systemd/towerd.service new file mode 100644 index 0000000..d08c113 --- /dev/null +++ b/systemd/towerd.service @@ -0,0 +1,14 @@ +[Unit] +Description=Tower Daemon +After=multi-user.target + +[Service] +Type=simple +User=root +WorkingDirectory=/opt/towerd +ExecStart=/opt/towerd/venv/bin/python -m towerd +Restart=on-failure +RestartSec=5 + +[Install] +WantedBy=multi-user.target diff --git a/towerd/__init__.py b/towerd/__init__.py new file mode 100644 index 0000000..aab60e0 --- /dev/null +++ b/towerd/__init__.py @@ -0,0 +1,3 @@ +"""Tower Daemon""" + +__version__ = "0.1.0" diff --git a/towerd/__main__.py b/towerd/__main__.py new file mode 100644 index 0000000..3d2e3c3 --- /dev/null +++ b/towerd/__main__.py @@ -0,0 +1,3 @@ +from towerd.main import main + +main() diff --git a/towerd/gpio_ctrl.py b/towerd/gpio_ctrl.py new file mode 100644 index 0000000..406ffae --- /dev/null +++ b/towerd/gpio_ctrl.py @@ -0,0 +1,59 @@ +from enum import Enum + +from gpiozero import LED, OutputDevice + + +class Status(Enum): + OK = "ok" + ERROR = "error" + + +# Blink timings (seconds) +SLOW_ON = 1.0 +SLOW_OFF = 1.0 +FAST_ON = 0.15 +FAST_OFF = 0.15 + + +class TowerGPIO: + """GPIO controls""" + + def __init__(self, status_pin: int = 4, fan_pin: int = 17) -> None: + self._status_led = LED(status_pin) + self._fan = OutputDevice(fan_pin, active_high=True, initial_value=False) + self._status = Status.OK + + @property + def status(self) -> Status: + return self._status + + @property + def fan_on(self) -> bool: + return self._fan.value + + def set_status(self, status: Status) -> None: + if status == self._status: + return + self._status = status + self._apply_blink() + + def set_fan(self, on: bool) -> None: + if on: + self._fan.on() + else: + self._fan.off() + + def start(self) -> None: + self._apply_blink() + + def stop(self) -> None: + self._status_led.off() + self._fan.off() + self._status_led.close() + self._fan.close() + + def _apply_blink(self) -> None: + if self._status is Status.OK: + self._status_led.blink(on_time=SLOW_ON, off_time=SLOW_OFF, background=True) + else: + self._status_led.blink(on_time=FAST_ON, off_time=FAST_OFF, background=True) diff --git a/towerd/main.py b/towerd/main.py new file mode 100644 index 0000000..0abc07c --- /dev/null +++ b/towerd/main.py @@ -0,0 +1,96 @@ +import argparse +import logging +import signal +import time +from dataclasses import dataclass + +from towerd.gpio_ctrl import Status, TowerGPIO +from towerd.thermal import read_cpu_temp_c + +log = logging.getLogger(__name__) + + +@dataclass(frozen=True) +class Config: + status_pin: int = 4 + fan_pin: int = 17 + fan_on_temp_c: float = 50.0 + fan_off_temp_c: float = 45.0 + poll_interval_s: float = 2.0 + error_temp_c: float = 80.0 + + +class TowerDaemon: + def __init__(self, config: Config) -> None: + self.config = config + self._running = True + self._gpio = TowerGPIO( + status_pin=config.status_pin, + fan_pin=config.fan_pin, + ) + + def run(self) -> None: + signal.signal(signal.SIGTERM, self._handle_signal) + signal.signal(signal.SIGINT, self._handle_signal) + + self._gpio.start() + log.info( + "Tower daemon started (status GPIO %d, fan GPIO %d)", + self.config.status_pin, + self.config.fan_pin, + ) + + try: + while self._running: + self._tick() + time.sleep(self.config.poll_interval_s) + finally: + self._gpio.stop() + log.info("Tower daemon stopped") + + def _tick(self) -> None: + temp_c = read_cpu_temp_c() + self._update_fan(temp_c) + self._update_status(temp_c) + + log.debug( + "temp=%.1fC fan=%s status=%s", + temp_c, + "on" if self._gpio.fan_on else "off", + self._gpio.status.value, + ) + + def _update_fan(self, temp_c: float) -> None: + if not self._gpio.fan_on and temp_c >= self.config.fan_on_temp_c: + self._gpio.set_fan(True) + log.info("Fan on (%.1fC >= %.1fC)", temp_c, self.config.fan_on_temp_c) + elif self._gpio.fan_on and temp_c <= self.config.fan_off_temp_c: + self._gpio.set_fan(False) + log.info("Fan off (%.1fC <= %.1fC)", temp_c, self.config.fan_off_temp_c) + + def _update_status(self, temp_c: float) -> None: + if temp_c >= self.config.error_temp_c: + self._gpio.set_status(Status.ERROR) + else: + self._gpio.set_status(Status.OK) + + def _handle_signal(self, signum: int, _frame) -> None: + log.info("Received signal %s, shutting down", signum) + self._running = False + + +def main() -> None: + parser = argparse.ArgumentParser(description="Tower Daemon") + parser.add_argument("-v", "--verbose", action="store_true", help="Enable debug logging") + args = parser.parse_args() + + logging.basicConfig( + level=logging.DEBUG if args.verbose else logging.INFO, + format="%(asctime)s %(levelname)s %(name)s: %(message)s", + ) + + TowerDaemon(Config()).run() + + +if __name__ == "__main__": + main() diff --git a/towerd/thermal.py b/towerd/thermal.py new file mode 100644 index 0000000..33b2b7c --- /dev/null +++ b/towerd/thermal.py @@ -0,0 +1,14 @@ +from pathlib import Path + +THERMAL_ZONE = Path("/sys/class/thermal/thermal_zone0/temp") + + +def read_cpu_temp_c() -> float: + """Get the CPU temp""" + if not THERMAL_ZONE.exists(): + raise FileNotFoundError( + f"Thermal sensor not found at {THERMAL_ZONE}. " + "Maybe running on bad hardware?" + ) + millidegrees = int(THERMAL_ZONE.read_text().strip()) + return millidegrees / 1000.0