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