Inital ver

This commit is contained in:
KenwoodFox
2026-06-14 18:26:37 -04:00
commit c7accf6ec6
9 changed files with 217 additions and 0 deletions

9
.gitignore vendored Normal file
View File

@@ -0,0 +1,9 @@
venv/
__pycache__/
*.py[cod]
*.egg-info/
dist/
build/
.pytest_cache/
.mypy_cache/
.ruff_cache/

18
README.md Normal file
View File

@@ -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
```

1
requirements.txt Normal file
View File

@@ -0,0 +1 @@
gpiozero>=2.0

14
systemd/towerd.service Normal file
View File

@@ -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

3
towerd/__init__.py Normal file
View File

@@ -0,0 +1,3 @@
"""Tower Daemon"""
__version__ = "0.1.0"

3
towerd/__main__.py Normal file
View File

@@ -0,0 +1,3 @@
from towerd.main import main
main()

59
towerd/gpio_ctrl.py Normal file
View File

@@ -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)

96
towerd/main.py Normal file
View File

@@ -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()

14
towerd/thermal.py Normal file
View File

@@ -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