Inital ver
This commit is contained in:
9
.gitignore
vendored
Normal file
9
.gitignore
vendored
Normal file
@@ -0,0 +1,9 @@
|
||||
venv/
|
||||
__pycache__/
|
||||
*.py[cod]
|
||||
*.egg-info/
|
||||
dist/
|
||||
build/
|
||||
.pytest_cache/
|
||||
.mypy_cache/
|
||||
.ruff_cache/
|
||||
18
README.md
Normal file
18
README.md
Normal 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
1
requirements.txt
Normal file
@@ -0,0 +1 @@
|
||||
gpiozero>=2.0
|
||||
14
systemd/towerd.service
Normal file
14
systemd/towerd.service
Normal 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
3
towerd/__init__.py
Normal file
@@ -0,0 +1,3 @@
|
||||
"""Tower Daemon"""
|
||||
|
||||
__version__ = "0.1.0"
|
||||
3
towerd/__main__.py
Normal file
3
towerd/__main__.py
Normal file
@@ -0,0 +1,3 @@
|
||||
from towerd.main import main
|
||||
|
||||
main()
|
||||
59
towerd/gpio_ctrl.py
Normal file
59
towerd/gpio_ctrl.py
Normal 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
96
towerd/main.py
Normal 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
14
towerd/thermal.py
Normal 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
|
||||
Reference in New Issue
Block a user