Add RPi fan controller, laptop temp sender, and Quickshell widget
Initial implementation of a USB-gadget-linked cooling controller: - fan_controller.c: PWM PID control, tach reading, TCP temp receiver, Unix socket IPC, safe-mode fallback, CSV logging via pigpio. - temp_sender.py: laptop-side lm-sensors/thermal_zone temp sender with auto-reconnect. - FanController.qml: Quickshell floating widget for monitoring and setpoint/manual duty control over the Unix socket.
This commit is contained in:
@@ -0,0 +1,613 @@
|
||||
/*
|
||||
* fan_controller.c — Laptop cooling pad controller for Raspberry Pi Zero 2W
|
||||
*
|
||||
* Uses pigpio for hardware PWM and tach GPIO
|
||||
* Receives CPU temperature from laptop via TCP over USB gadget
|
||||
* Controls fan speed with a PID loop
|
||||
*
|
||||
* Build: gcc fan_controller.c -o fan_controller -lpigpio -lrt -lm
|
||||
* Run: sudo ./fan_controller
|
||||
*
|
||||
|
||||
*/
|
||||
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <stdint.h>
|
||||
#include <math.h>
|
||||
#include <unistd.h>
|
||||
#include <fcntl.h>
|
||||
#include <signal.h>
|
||||
#include <time.h>
|
||||
#include <sys/time.h>
|
||||
#include <errno.h>
|
||||
#include <sys/select.h>
|
||||
#include <sys/socket.h>
|
||||
#include <netinet/in.h>
|
||||
#include <arpa/inet.h>
|
||||
#include <netinet/tcp.h>
|
||||
#include <sys/un.h>
|
||||
#include <sys/stat.h>
|
||||
#include <pthread.h>
|
||||
#include <pigpio.h>
|
||||
|
||||
/* -------------------------------------------------------------------------
|
||||
* Configuration — adjust to your wiring
|
||||
* ------------------------------------------------------------------------- */
|
||||
|
||||
/* PWM — hardware PWM pins on RPi: GPIO 12, 13, 18, 19
|
||||
* GPIO 18 (physical pin 12) is the most commonly used */
|
||||
#define PWM_PIN 18 /* BCM numbering */
|
||||
#define PWM_FREQ 25000 /* 25 kHz — Intel 4-pin fan spec */
|
||||
|
||||
/* Tach — any spare GPIO, e.g. GPIO 24 (physical pin 18) */
|
||||
#define TACH_PIN 24 /* BCM numbering */
|
||||
#define TACH_POLL_MS 1000 /* measure RPM over this window */
|
||||
|
||||
/* TCP server — laptop connects to this port over USB gadget interface */
|
||||
#define TCP_PORT 9000
|
||||
#define TCP_BIND_ADDR "10.55.0.1" /* Pi's static IP on usb0 */
|
||||
#define RECV_TIMEOUT_S 5 /* seconds before assuming disconnect */
|
||||
|
||||
/* PID — tuned for Framework 13 thermal mass + Noctua A12x25 5V response */
|
||||
#define PID_SETPOINT 52.0f /* target temp °C — sits below FW13 fan
|
||||
spinup threshold (~65°C) */
|
||||
#define PID_KP 0.04f /* gentle — laptop thermals are slow */
|
||||
#define PID_KI 0.008f /* small — avoids windup on boost spikes */
|
||||
#define PID_KD 0.8f /* higher damping — A12x25 responds fast */
|
||||
#define PID_DT 2.0f /* loop interval seconds */
|
||||
|
||||
/* Fan safety — Noctua A12x25 5V specifics:
|
||||
* min reliable start: ~25% duty (~400 RPM)
|
||||
* max: 2000 RPM @ 100% */
|
||||
#define TEMP_MIN_SPIN 42.0f /* below this: fans off (FW13 idle range) */
|
||||
#define TEMP_FULL_BLAST 80.0f /* above this: always 100% */
|
||||
#define DUTY_MIN 0.25f /* A12x25 5V minimum reliable spin */
|
||||
#define DUTY_MAX 1.0f
|
||||
#define SAFE_DUTY 0.70f /* fallback if laptop disconnects */
|
||||
|
||||
/* Unix socket for widget IPC */
|
||||
#define SOCKET_PATH "/tmp/fan_controller.sock"
|
||||
|
||||
/* Logging */
|
||||
#define LOG_FILE "/var/log/fan_controller.log"
|
||||
|
||||
/* -------------------------------------------------------------------------
|
||||
* PWM + Tach via pigpio
|
||||
* ------------------------------------------------------------------------- */
|
||||
|
||||
/* pigpio duty cycle range is 0–1,000,000 */
|
||||
#define PIGPIO_DUTY_MAX 1000000
|
||||
|
||||
static int pwm_init(void) {
|
||||
if (gpioInitialise() < 0) {
|
||||
fprintf(stderr, "pigpio initialisation failed\n"
|
||||
"Make sure pigpiod is not already running and you are root.\n");
|
||||
return -1;
|
||||
}
|
||||
/* Start at 0% duty — fan off until we have temp data */
|
||||
if (gpioHardwarePWM(PWM_PIN, PWM_FREQ, 0) != 0) {
|
||||
fprintf(stderr, "gpioHardwarePWM failed — "
|
||||
"GPIO %d may not be a hardware PWM pin.\n"
|
||||
"Valid pins: 12, 13, 18, 19 (BCM)\n", PWM_PIN);
|
||||
gpioTerminate();
|
||||
return -1;
|
||||
}
|
||||
/* Tach pin: input with internal pull-up */
|
||||
gpioSetMode(TACH_PIN, PI_INPUT);
|
||||
gpioSetPullUpDown(TACH_PIN, PI_PUD_UP);
|
||||
|
||||
printf("pigpio initialised PWM=GPIO%d @ %dHz Tach=GPIO%d\n",
|
||||
PWM_PIN, PWM_FREQ, TACH_PIN);
|
||||
return 0;
|
||||
}
|
||||
|
||||
static void pwm_set_duty(float duty) {
|
||||
if (duty < 0.0f) duty = 0.0f;
|
||||
if (duty > 1.0f) duty = 1.0f;
|
||||
gpioHardwarePWM(PWM_PIN, PWM_FREQ, (unsigned)(duty * PIGPIO_DUTY_MAX));
|
||||
}
|
||||
|
||||
static void pwm_cleanup(void) {
|
||||
gpioHardwarePWM(PWM_PIN, PWM_FREQ, 0);
|
||||
gpioTerminate();
|
||||
}
|
||||
|
||||
/* Tach: count rising edges via pigpio alert callback */
|
||||
static volatile int tach_pulses = 0;
|
||||
|
||||
static void tach_callback(int gpio, int level, uint32_t tick) {
|
||||
(void)gpio; (void)tick;
|
||||
if (level == 1) tach_pulses++;
|
||||
}
|
||||
|
||||
static int tach_init(void) {
|
||||
/* pigpio must already be initialised */
|
||||
if (gpioSetAlertFunc(TACH_PIN, tach_callback) != 0) {
|
||||
fprintf(stderr, "Warning: could not set tach alert on GPIO%d\n",
|
||||
TACH_PIN);
|
||||
return -1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int tach_read_rpm(void) {
|
||||
tach_pulses = 0;
|
||||
time_sleep(TACH_POLL_MS / 1000.0);
|
||||
int pulses = tach_pulses;
|
||||
/* 2 pulses per revolution, window = TACH_POLL_MS ms */
|
||||
float window_s = TACH_POLL_MS / 1000.0f;
|
||||
return (int)((pulses / 2.0f) * (60.0f / window_s));
|
||||
}
|
||||
|
||||
/* -------------------------------------------------------------------------
|
||||
* PID controller
|
||||
* ------------------------------------------------------------------------- */
|
||||
|
||||
typedef struct {
|
||||
float kp, ki, kd;
|
||||
float setpoint;
|
||||
float integral;
|
||||
float prev_measured;
|
||||
float out_min, out_max;
|
||||
float dt;
|
||||
int initialized;
|
||||
} PID;
|
||||
|
||||
static PID fan_pid;
|
||||
|
||||
static void pid_init(PID *p, float kp, float ki, float kd,
|
||||
float setpoint, float dt) {
|
||||
p->kp = kp;
|
||||
p->ki = ki;
|
||||
p->kd = kd;
|
||||
p->setpoint = setpoint;
|
||||
p->integral = 0.0f;
|
||||
p->prev_measured = setpoint;
|
||||
p->out_min = 0.0f;
|
||||
p->out_max = 1.0f;
|
||||
p->dt = dt;
|
||||
p->initialized = 0;
|
||||
}
|
||||
|
||||
static float pid_update(PID *p, float measured) {
|
||||
/* Seed derivative term on first call to avoid kick */
|
||||
if (!p->initialized) {
|
||||
p->prev_measured = measured;
|
||||
p->initialized = 1;
|
||||
}
|
||||
|
||||
float error = measured - p->setpoint;
|
||||
|
||||
/* Integral with anti-windup */
|
||||
p->integral += error * p->dt;
|
||||
float windup_limit = p->out_max / (p->ki > 0 ? p->ki : 1e-6f);
|
||||
if (p->integral > windup_limit) p->integral = windup_limit;
|
||||
if (p->integral < -windup_limit) p->integral = -windup_limit;
|
||||
|
||||
/* Derivative on measurement to avoid setpoint-change kick */
|
||||
float derivative = -(measured - p->prev_measured) / p->dt;
|
||||
p->prev_measured = measured;
|
||||
|
||||
float out = (p->kp * error) + (p->ki * p->integral) + (p->kd * derivative);
|
||||
|
||||
if (out > p->out_max) out = p->out_max;
|
||||
if (out < p->out_min) out = p->out_min;
|
||||
return out;
|
||||
}
|
||||
|
||||
/* -------------------------------------------------------------------------
|
||||
* Low-pass filter
|
||||
* ------------------------------------------------------------------------- */
|
||||
|
||||
#define LPF_ALPHA 0.2f /* 0=frozen, 1=no filter */
|
||||
|
||||
static float lpf(float prev, float raw) {
|
||||
return prev * (1.0f - LPF_ALPHA) + raw * LPF_ALPHA;
|
||||
}
|
||||
|
||||
/* -------------------------------------------------------------------------
|
||||
* TCP server
|
||||
*
|
||||
* The Pi listens on TCP_PORT. The laptop (temp_sender.py) connects and
|
||||
* streams newline-terminated temperature readings. If the connection drops,
|
||||
* the main loop falls back to safe mode and waits for a reconnect.
|
||||
* ------------------------------------------------------------------------- */
|
||||
|
||||
static int tcp_listen_fd = -1; /* listening socket, kept open always */
|
||||
static int tcp_client_fd = -1; /* connected client, -1 when nobody in */
|
||||
static char tcp_buf[64];
|
||||
static int tcp_buf_len = 0;
|
||||
|
||||
static int tcp_server_init(void) {
|
||||
tcp_listen_fd = socket(AF_INET, SOCK_STREAM, 0);
|
||||
if (tcp_listen_fd < 0) { perror("socket"); return -1; }
|
||||
|
||||
/* Reuse address so restart after crash doesn't wait for TIME_WAIT */
|
||||
int yes = 1;
|
||||
setsockopt(tcp_listen_fd, SOL_SOCKET, SO_REUSEADDR, &yes, sizeof(yes));
|
||||
|
||||
struct sockaddr_in addr = {
|
||||
.sin_family = AF_INET,
|
||||
.sin_port = htons(TCP_PORT),
|
||||
};
|
||||
inet_pton(AF_INET, TCP_BIND_ADDR, &addr.sin_addr);
|
||||
|
||||
if (bind(tcp_listen_fd, (struct sockaddr *)&addr, sizeof(addr)) < 0) {
|
||||
perror("bind"); return -1;
|
||||
}
|
||||
if (listen(tcp_listen_fd, 1) < 0) {
|
||||
perror("listen"); return -1;
|
||||
}
|
||||
|
||||
/* Non-blocking so accept() in the main loop doesn't stall */
|
||||
fcntl(tcp_listen_fd, F_SETFL, O_NONBLOCK);
|
||||
|
||||
printf("TCP server listening on %s:%d\n", TCP_BIND_ADDR, TCP_PORT);
|
||||
return 0;
|
||||
}
|
||||
|
||||
/*
|
||||
* Accept a pending connection if one is waiting.
|
||||
* Called each loop iteration when tcp_client_fd == -1.
|
||||
*/
|
||||
static void tcp_try_accept(void) {
|
||||
struct sockaddr_in client;
|
||||
socklen_t len = sizeof(client);
|
||||
int fd = accept(tcp_listen_fd, (struct sockaddr *)&client, &len);
|
||||
if (fd < 0) return; /* EAGAIN — nobody waiting, that's fine */
|
||||
|
||||
/* Enable TCP keepalive so stale connections are detected quickly */
|
||||
int yes = 1;
|
||||
setsockopt(fd, SOL_SOCKET, SO_KEEPALIVE, &yes, sizeof(yes));
|
||||
setsockopt(fd, IPPROTO_TCP, TCP_KEEPIDLE,
|
||||
&(int){5}, sizeof(int)); /* start probing after 5s idle */
|
||||
setsockopt(fd, IPPROTO_TCP, TCP_KEEPINTVL,
|
||||
&(int){2}, sizeof(int)); /* probe every 2s */
|
||||
setsockopt(fd, IPPROTO_TCP, TCP_KEEPCNT,
|
||||
&(int){3}, sizeof(int)); /* drop after 3 failed probes */
|
||||
|
||||
tcp_client_fd = fd;
|
||||
tcp_buf_len = 0;
|
||||
|
||||
char ip[INET_ADDRSTRLEN];
|
||||
inet_ntop(AF_INET, &client.sin_addr, ip, sizeof(ip));
|
||||
printf("Laptop connected from %s\n", ip);
|
||||
}
|
||||
|
||||
static void tcp_client_close(void) {
|
||||
if (tcp_client_fd >= 0) { close(tcp_client_fd); tcp_client_fd = -1; }
|
||||
tcp_buf_len = 0;
|
||||
printf("Laptop disconnected — entering safe mode\n");
|
||||
}
|
||||
|
||||
/*
|
||||
* Read a newline-terminated float with timeout.
|
||||
* Returns temperature, or -1.0 on timeout/disconnect.
|
||||
*/
|
||||
static float tcp_recv_temp(float timeout_s) {
|
||||
struct timeval deadline, now, tv;
|
||||
gettimeofday(&deadline, NULL);
|
||||
deadline.tv_sec += (int)timeout_s;
|
||||
deadline.tv_usec += (int)((timeout_s - (int)timeout_s) * 1e6);
|
||||
if (deadline.tv_usec >= 1000000) {
|
||||
deadline.tv_sec++;
|
||||
deadline.tv_usec -= 1000000;
|
||||
}
|
||||
|
||||
while (1) {
|
||||
gettimeofday(&now, NULL);
|
||||
long us_left = (deadline.tv_sec - now.tv_sec) * 1000000
|
||||
+ (deadline.tv_usec - now.tv_usec);
|
||||
if (us_left <= 0) return -1.0f;
|
||||
|
||||
tv.tv_sec = us_left / 1000000;
|
||||
tv.tv_usec = us_left % 1000000;
|
||||
|
||||
fd_set rfds;
|
||||
FD_ZERO(&rfds);
|
||||
FD_SET(tcp_client_fd, &rfds);
|
||||
if (select(tcp_client_fd + 1, &rfds, NULL, NULL, &tv) <= 0)
|
||||
return -1.0f;
|
||||
|
||||
char c;
|
||||
ssize_t n = read(tcp_client_fd, &c, 1);
|
||||
if (n <= 0) return -1.0f; /* connection closed or error */
|
||||
|
||||
if (c == '\n') {
|
||||
tcp_buf[tcp_buf_len] = '\0';
|
||||
float val = (float)atof(tcp_buf);
|
||||
tcp_buf_len = 0;
|
||||
return val;
|
||||
}
|
||||
if (tcp_buf_len < (int)sizeof(tcp_buf) - 1)
|
||||
tcp_buf[tcp_buf_len++] = c;
|
||||
}
|
||||
}
|
||||
|
||||
/* -------------------------------------------------------------------------
|
||||
* Logging
|
||||
* ------------------------------------------------------------------------- */
|
||||
|
||||
static FILE *log_fp = NULL;
|
||||
|
||||
static void log_open(void) {
|
||||
log_fp = fopen(LOG_FILE, "a");
|
||||
if (!log_fp) fprintf(stderr, "Warning: cannot open log file %s\n", LOG_FILE);
|
||||
}
|
||||
|
||||
static void log_entry(float temp, float filtered, float duty, int rpm,
|
||||
int safe_mode) {
|
||||
time_t now = time(NULL);
|
||||
char ts[32];
|
||||
strftime(ts, sizeof(ts), "%Y-%m-%dT%H:%M:%S", localtime(&now));
|
||||
|
||||
const char *line = safe_mode ? " [SAFE MODE]" : "";
|
||||
printf("%s temp=%5.1f°C filtered=%5.1f°C duty=%3.0f%% rpm=%4d%s\n",
|
||||
ts, temp, filtered, duty * 100, rpm, line);
|
||||
fflush(stdout);
|
||||
|
||||
if (log_fp) {
|
||||
fprintf(log_fp,
|
||||
"%s,%.1f,%.1f,%.0f,%d,%d\n",
|
||||
ts, temp, filtered, duty * 100.0f, rpm, safe_mode);
|
||||
fflush(log_fp);
|
||||
}
|
||||
}
|
||||
|
||||
/* Forward declaration needed by IPC thread */
|
||||
extern volatile int running;
|
||||
|
||||
/* -------------------------------------------------------------------------
|
||||
* Unix socket IPC (for Quickshell widget)
|
||||
*
|
||||
* Protocol (newline terminated):
|
||||
* Query: "STATUS\n"
|
||||
* Reply: JSON: {"temp":52.1,"rpm":1200,"duty":45,"setpoint":52.0,"mode":"auto"}
|
||||
*
|
||||
* Command: "SET_SETPOINT 58.0\n" -> "OK\n"
|
||||
* Command: "SET_MODE auto\n" -> "OK\n"
|
||||
* Command: "SET_MODE manual\n" -> "OK\n"
|
||||
* Command: "SET_DUTY 75\n" -> "OK\n" (manual mode only, 0-100)
|
||||
* ------------------------------------------------------------------------- */
|
||||
|
||||
/* Shared state accessed by IPC thread */
|
||||
typedef struct {
|
||||
float temp;
|
||||
float filtered_temp;
|
||||
float duty;
|
||||
int rpm;
|
||||
int safe_mode;
|
||||
/* Writable by widget */
|
||||
float setpoint;
|
||||
int manual_mode;
|
||||
float manual_duty;
|
||||
} FanState;
|
||||
|
||||
static FanState state = {0};
|
||||
static pthread_mutex_t state_mutex = PTHREAD_MUTEX_INITIALIZER;
|
||||
|
||||
static int ipc_fd = -1;
|
||||
|
||||
static void ipc_handle_client(int cfd) {
|
||||
char buf[128] = {0};
|
||||
ssize_t n = recv(cfd, buf, sizeof(buf) - 1, 0);
|
||||
if (n <= 0) { close(cfd); return; }
|
||||
|
||||
/* Strip trailing newline/whitespace */
|
||||
for (int i = n - 1; i >= 0 && (buf[i] == '\n' || buf[i] == '\r'
|
||||
|| buf[i] == ' '); i--)
|
||||
buf[i] = '\0';
|
||||
|
||||
char reply[256];
|
||||
|
||||
pthread_mutex_lock(&state_mutex);
|
||||
|
||||
if (strcmp(buf, "STATUS") == 0) {
|
||||
snprintf(reply, sizeof(reply),
|
||||
"{\"temp\":%.1f,\"rpm\":%d,\"duty\":%.0f,"
|
||||
"\"setpoint\":%.1f,\"mode\":\"%s\",\"safe\":%d}\n",
|
||||
state.filtered_temp, state.rpm, state.duty * 100.0f,
|
||||
state.setpoint,
|
||||
state.manual_mode ? "manual" : "auto",
|
||||
state.safe_mode);
|
||||
|
||||
} else if (strncmp(buf, "SET_SETPOINT ", 13) == 0) {
|
||||
float sp = (float)atof(buf + 13);
|
||||
if (sp >= 35.0f && sp <= 85.0f) {
|
||||
state.setpoint = sp;
|
||||
fan_pid.setpoint = sp;
|
||||
snprintf(reply, sizeof(reply), "OK\n");
|
||||
} else {
|
||||
snprintf(reply, sizeof(reply), "ERR setpoint out of range\n");
|
||||
}
|
||||
|
||||
} else if (strncmp(buf, "SET_MODE ", 9) == 0) {
|
||||
if (strcmp(buf + 9, "manual") == 0) state.manual_mode = 1;
|
||||
else if (strcmp(buf + 9, "auto") == 0) state.manual_mode = 0;
|
||||
snprintf(reply, sizeof(reply), "OK\n");
|
||||
|
||||
} else if (strncmp(buf, "SET_DUTY ", 9) == 0) {
|
||||
float d = (float)atof(buf + 9) / 100.0f;
|
||||
if (d >= 0.0f && d <= 1.0f) {
|
||||
state.manual_duty = d;
|
||||
snprintf(reply, sizeof(reply), "OK\n");
|
||||
} else {
|
||||
snprintf(reply, sizeof(reply), "ERR duty out of range\n");
|
||||
}
|
||||
|
||||
} else {
|
||||
snprintf(reply, sizeof(reply), "ERR unknown command\n");
|
||||
}
|
||||
|
||||
pthread_mutex_unlock(&state_mutex);
|
||||
|
||||
send(cfd, reply, strlen(reply), 0);
|
||||
close(cfd);
|
||||
}
|
||||
|
||||
static void *ipc_thread(void *arg) {
|
||||
(void)arg;
|
||||
while (running) {
|
||||
struct timeval tv = { .tv_sec = 1, .tv_usec = 0 };
|
||||
fd_set rfds;
|
||||
FD_ZERO(&rfds);
|
||||
FD_SET(ipc_fd, &rfds);
|
||||
if (select(ipc_fd + 1, &rfds, NULL, NULL, &tv) > 0) {
|
||||
int cfd = accept(ipc_fd, NULL, NULL);
|
||||
if (cfd >= 0) ipc_handle_client(cfd);
|
||||
}
|
||||
}
|
||||
return NULL;
|
||||
}
|
||||
|
||||
static int ipc_init(void) {
|
||||
unlink(SOCKET_PATH); /* remove stale socket */
|
||||
|
||||
ipc_fd = socket(AF_UNIX, SOCK_STREAM, 0);
|
||||
if (ipc_fd < 0) { perror("ipc socket"); return -1; }
|
||||
|
||||
struct sockaddr_un addr = { .sun_family = AF_UNIX };
|
||||
strncpy(addr.sun_path, SOCKET_PATH, sizeof(addr.sun_path) - 1);
|
||||
|
||||
if (bind(ipc_fd, (struct sockaddr *)&addr, sizeof(addr)) < 0) {
|
||||
perror("ipc bind"); return -1;
|
||||
}
|
||||
if (listen(ipc_fd, 4) < 0) {
|
||||
perror("ipc listen"); return -1;
|
||||
}
|
||||
|
||||
/* Make socket readable by non-root (widget runs as user) */
|
||||
chmod(SOCKET_PATH, 0666);
|
||||
|
||||
pthread_t tid;
|
||||
pthread_create(&tid, NULL, ipc_thread, NULL);
|
||||
pthread_detach(tid);
|
||||
|
||||
printf("IPC socket: %s\n", SOCKET_PATH);
|
||||
return 0;
|
||||
}
|
||||
|
||||
static void ipc_cleanup(void) {
|
||||
if (ipc_fd >= 0) { close(ipc_fd); ipc_fd = -1; }
|
||||
unlink(SOCKET_PATH);
|
||||
}
|
||||
|
||||
/* -------------------------------------------------------------------------
|
||||
* Signal handling
|
||||
* ------------------------------------------------------------------------- */
|
||||
|
||||
volatile int running = 1;
|
||||
|
||||
static void handle_signal(int sig) {
|
||||
(void)sig;
|
||||
running = 0;
|
||||
}
|
||||
|
||||
/* -------------------------------------------------------------------------
|
||||
* Main
|
||||
* ------------------------------------------------------------------------- */
|
||||
|
||||
int main(void) {
|
||||
/* Signals */
|
||||
signal(SIGINT, handle_signal);
|
||||
signal(SIGTERM, handle_signal);
|
||||
|
||||
printf("=== RPi Zero 2W Fan Controller (USB gadget TCP) ===\n");
|
||||
|
||||
if (pwm_init() < 0) return EXIT_FAILURE; /* also calls gpioInitialise */
|
||||
tach_init(); /* non-fatal if it fails */
|
||||
if (tcp_server_init() < 0) return EXIT_FAILURE;
|
||||
if (ipc_init() < 0) return EXIT_FAILURE;
|
||||
|
||||
pid_init(&fan_pid, PID_KP, PID_KI, PID_KD, PID_SETPOINT, PID_DT);
|
||||
state.setpoint = PID_SETPOINT;
|
||||
state.manual_mode = 0;
|
||||
state.manual_duty = DUTY_MIN;
|
||||
log_open();
|
||||
|
||||
float filtered_temp = PID_SETPOINT;
|
||||
int safe_mode = 0;
|
||||
|
||||
printf("PID setpoint=%.1f°C kp=%.3f ki=%.3f kd=%.3f dt=%.1fs\n",
|
||||
PID_SETPOINT, PID_KP, PID_KI, PID_KD, PID_DT);
|
||||
printf("%-24s %-10s %-12s %-8s %-6s %s\n",
|
||||
"Timestamp", "Temp(°C)", "Filtered(°C)", "Duty(%)", "RPM", "Mode");
|
||||
|
||||
while (running) {
|
||||
/* --- Accept new connection if we don't have one --- */
|
||||
if (tcp_client_fd < 0) tcp_try_accept();
|
||||
|
||||
/* --- Receive temperature --- */
|
||||
float raw_temp = (tcp_client_fd >= 0)
|
||||
? tcp_recv_temp(RECV_TIMEOUT_S) : -1.0f;
|
||||
|
||||
if (raw_temp < 0.0f) {
|
||||
if (tcp_client_fd >= 0) tcp_client_close();
|
||||
safe_mode = 1;
|
||||
raw_temp = filtered_temp; /* log last known */
|
||||
} else {
|
||||
safe_mode = 0;
|
||||
filtered_temp = lpf(filtered_temp, raw_temp);
|
||||
}
|
||||
|
||||
/* --- Compute duty cycle --- */
|
||||
float duty;
|
||||
|
||||
pthread_mutex_lock(&state_mutex);
|
||||
int manual_mode = state.manual_mode;
|
||||
float manual_duty = state.manual_duty;
|
||||
/* Sync PID setpoint in case widget changed it */
|
||||
fan_pid.setpoint = state.setpoint;
|
||||
pthread_mutex_unlock(&state_mutex);
|
||||
|
||||
if (safe_mode) {
|
||||
duty = SAFE_DUTY;
|
||||
} else if (manual_mode) {
|
||||
duty = manual_duty;
|
||||
} else if (filtered_temp >= TEMP_FULL_BLAST) {
|
||||
duty = DUTY_MAX;
|
||||
} else if (filtered_temp < TEMP_MIN_SPIN) {
|
||||
duty = 0.0f;
|
||||
} else {
|
||||
duty = pid_update(&fan_pid, filtered_temp);
|
||||
if (duty > 0.0f && duty < DUTY_MIN)
|
||||
duty = DUTY_MIN;
|
||||
}
|
||||
|
||||
/* --- Apply PWM --- */
|
||||
pwm_set_duty(duty);
|
||||
|
||||
/* --- Read RPM --- */
|
||||
int rpm = tach_read_rpm();
|
||||
|
||||
/* --- Update shared state for IPC --- */
|
||||
pthread_mutex_lock(&state_mutex);
|
||||
state.temp = raw_temp;
|
||||
state.filtered_temp = filtered_temp;
|
||||
state.duty = duty;
|
||||
state.rpm = rpm;
|
||||
state.safe_mode = safe_mode;
|
||||
pthread_mutex_unlock(&state_mutex);
|
||||
|
||||
/* --- Log --- */
|
||||
log_entry(raw_temp, filtered_temp, duty, rpm, safe_mode);
|
||||
|
||||
/* --- Sleep remainder of dt (tach read already consumed ~1s) --- */
|
||||
float sleep_s = PID_DT - (TACH_POLL_MS / 1000.0f);
|
||||
if (sleep_s > 0.0f) usleep((useconds_t)(sleep_s * 1e6));
|
||||
}
|
||||
|
||||
/* --- Cleanup --- */
|
||||
printf("\nShutting down — spinning fans up briefly then off...\n");
|
||||
pwm_set_duty(1.0f);
|
||||
sleep(2);
|
||||
pwm_cleanup(); /* also calls gpioTerminate */
|
||||
tcp_client_close();
|
||||
if (tcp_listen_fd >= 0) close(tcp_listen_fd);
|
||||
ipc_cleanup();
|
||||
if (log_fp) fclose(log_fp);
|
||||
|
||||
return EXIT_SUCCESS;
|
||||
}
|
||||
Reference in New Issue
Block a user