* sensor-monitord.c - Systemd-managed BME280 sensor daemon
* Reads BME280 via /dev/i2c-1, stores readings in shared memory,
* serves clients over a Unix domain socket, and feeds the hardware
* watchdog. Designed for the Raspberry Pi Zero 2 W.
* Build: aarch64-linux-gnu-gcc -o sensor-monitord sensor-monitord.c \
* -lrt -lpthread -lsystemd -lm
#include <linux/i2c-dev.h>
#include <systemd/sd-daemon.h>
#include <systemd/sd-journal.h>
#include "sensor-protocol.h"
/* BME280 I2C address and key registers */
#define BME280_REG_ID 0xD0
#define BME280_REG_CTRL_HUM 0xF2
#define BME280_REG_CTRL_MEAS 0xF4
#define BME280_REG_CONFIG 0xF5
#define BME280_REG_DATA 0xF7
#define BME280_CHIP_ID 0x60
/* Calibration data storage */
int16_t dig_P2, dig_P3, dig_P4, dig_P5;
int16_t dig_P6, dig_P7, dig_P8, dig_P9;
int16_t dig_H2, dig_H4, dig_H5;
static volatile sig_atomic_t running = 1;
static int watchdog_fd = -1;
static int listen_fd = -1;
static struct sensor_ring *ring = NULL;
static struct bme280_calib calib;
static int sample_interval_ms = 1000;
/* ----------------------------------------------------------------
* ---------------------------------------------------------------- */
static void signal_handler(int sig)
static void install_signal_handlers(void)
memset(&sa, 0, sizeof(sa));
sa.sa_handler = signal_handler;
sigemptyset(&sa.sa_mask);
sigaction(SIGTERM, &sa, NULL);
sigaction(SIGINT, &sa, NULL);
sigaction(SIGHUP, &sa, NULL);
/* Ignore SIGPIPE so writing to a disconnected socket returns EPIPE */
signal(SIGPIPE, SIG_IGN);
/* ----------------------------------------------------------------
* ---------------------------------------------------------------- */
static int i2c_write_byte(int fd, uint8_t reg, uint8_t val)
uint8_t buf[2] = { reg, val };
if (write(fd, buf, 2) != 2)
static int i2c_read_bytes(int fd, uint8_t reg, uint8_t *buf, int len)
if (write(fd, ®, 1) != 1)
if (read(fd, buf, len) != len)
/* ----------------------------------------------------------------
* BME280 initialization and reading
* ---------------------------------------------------------------- */
static int bme280_init(const char *i2c_dev)
uint8_t cal1[26], cal2[7];
i2c_fd = open(i2c_dev, O_RDWR);
sd_journal_print(LOG_ERR, "Failed to open %s: %s",
i2c_dev, strerror(errno));
if (ioctl(i2c_fd, I2C_SLAVE, BME280_ADDR) < 0) {
sd_journal_print(LOG_ERR, "I2C_SLAVE ioctl failed: %s",
if (i2c_read_bytes(i2c_fd, BME280_REG_ID, &id, 1) < 0 ||
sd_journal_print(LOG_ERR, "BME280 not found (id=0x%02x)", id);
/* Read temperature and pressure calibration (0x88..0xA1) */
if (i2c_read_bytes(i2c_fd, 0x88, cal1, 26) < 0)
calib.dig_T1 = (uint16_t)(cal1[1] << 8 | cal1[0]);
calib.dig_T2 = (int16_t)(cal1[3] << 8 | cal1[2]);
calib.dig_T3 = (int16_t)(cal1[5] << 8 | cal1[4]);
calib.dig_P1 = (uint16_t)(cal1[7] << 8 | cal1[6]);
calib.dig_P2 = (int16_t)(cal1[9] << 8 | cal1[8]);
calib.dig_P3 = (int16_t)(cal1[11] << 8 | cal1[10]);
calib.dig_P4 = (int16_t)(cal1[13] << 8 | cal1[12]);
calib.dig_P5 = (int16_t)(cal1[15] << 8 | cal1[14]);
calib.dig_P6 = (int16_t)(cal1[17] << 8 | cal1[16]);
calib.dig_P7 = (int16_t)(cal1[19] << 8 | cal1[18]);
calib.dig_P8 = (int16_t)(cal1[21] << 8 | cal1[20]);
calib.dig_P9 = (int16_t)(cal1[23] << 8 | cal1[22]);
/* Humidity calibration byte at 0xA1 */
/* Read humidity calibration (0xE1..0xE7) */
if (i2c_read_bytes(i2c_fd, 0xE1, cal2, 7) < 0)
calib.dig_H2 = (int16_t)(cal2[1] << 8 | cal2[0]);
calib.dig_H4 = (int16_t)((cal2[3] << 4) | (cal2[4] & 0x0F));
calib.dig_H5 = (int16_t)((cal2[5] << 4) | (cal2[4] >> 4));
calib.dig_H6 = (int8_t)cal2[6];
/* Configure: humidity oversampling x1 */
i2c_write_byte(i2c_fd, BME280_REG_CTRL_HUM, 0x01);
/* Configure: standby 1000ms, filter off */
i2c_write_byte(i2c_fd, BME280_REG_CONFIG, 0xA0);
/* Configure: temp oversampling x1, pressure oversampling x1, normal mode */
i2c_write_byte(i2c_fd, BME280_REG_CTRL_MEAS, 0x27);
sd_journal_print(LOG_INFO, "BME280 initialized on %s", i2c_dev);
static int bme280_read(struct sensor_reading *out)
int32_t adc_T, adc_P, adc_H;
double var1, var2, T, P, H;
if (i2c_read_bytes(i2c_fd, BME280_REG_DATA, data, 8) < 0)
adc_P = (int32_t)((data[0] << 12) | (data[1] << 4) | (data[2] >> 4));
adc_T = (int32_t)((data[3] << 12) | (data[4] << 4) | (data[5] >> 4));
adc_H = (int32_t)((data[6] << 8) | data[7]);
/* Temperature compensation (from BME280 datasheet) */
var1 = ((double)adc_T / 16384.0 - (double)calib.dig_T1 / 1024.0)
var2 = (((double)adc_T / 131072.0 - (double)calib.dig_T1 / 8192.0)
* ((double)adc_T / 131072.0 - (double)calib.dig_T1 / 8192.0))
t_fine = (int32_t)(var1 + var2);
T = (var1 + var2) / 5120.0;
/* Pressure compensation */
var1 = ((double)t_fine / 2.0) - 64000.0;
var2 = var1 * var1 * (double)calib.dig_P6 / 32768.0;
var2 = var2 + var1 * (double)calib.dig_P5 * 2.0;
var2 = (var2 / 4.0) + ((double)calib.dig_P4 * 65536.0);
var1 = ((double)calib.dig_P3 * var1 * var1 / 524288.0
+ (double)calib.dig_P2 * var1) / 524288.0;
var1 = (1.0 + var1 / 32768.0) * (double)calib.dig_P1;
P = 1048576.0 - (double)adc_P;
P = (P - (var2 / 4096.0)) * 6250.0 / var1;
var1 = (double)calib.dig_P9 * P * P / 2147483648.0;
var2 = P * (double)calib.dig_P8 / 32768.0;
P = P + (var1 + var2 + (double)calib.dig_P7) / 16.0;
P = P / 100.0; /* Convert Pa to hPa */
/* Humidity compensation */
H = (double)t_fine - 76800.0;
H = (adc_H - (calib.dig_H4 * 64.0 + calib.dig_H5 / 16384.0 * H))
* (calib.dig_H2 / 65536.0
* (1.0 + calib.dig_H6 / 67108864.0 * H
* (1.0 + calib.dig_H3 / 67108864.0 * H)));
H = H * (1.0 - calib.dig_H1 * H / 524288.0);
if (H > 100.0) H = 100.0;
clock_gettime(CLOCK_REALTIME, &ts);
out->timestamp_ms = (uint64_t)ts.tv_sec * 1000
+ (uint64_t)ts.tv_nsec / 1000000;
/* ----------------------------------------------------------------
* Shared memory ring buffer
* ---------------------------------------------------------------- */
static int ring_init(void)
shm_fd = shm_open(SHM_NAME, O_CREAT | O_RDWR, 0644);
sd_journal_print(LOG_ERR, "shm_open failed: %s", strerror(errno));
if (ftruncate(shm_fd, sizeof(struct sensor_ring)) < 0) {
sd_journal_print(LOG_ERR, "ftruncate failed: %s", strerror(errno));
ring = mmap(NULL, sizeof(struct sensor_ring),
PROT_READ | PROT_WRITE, MAP_SHARED, shm_fd, 0);
if (ring == MAP_FAILED) {
sd_journal_print(LOG_ERR, "mmap failed: %s", strerror(errno));
/* Zero the ring on startup */
memset(ring, 0, sizeof(struct sensor_ring));
sd_journal_print(LOG_INFO, "Shared memory ring buffer initialized "
"(%zu bytes)", sizeof(struct sensor_ring));
static void ring_push(const struct sensor_reading *r)
uint32_t idx = atomic_load(&ring->write_index);
ring->readings[idx % RING_CAPACITY] = *r;
atomic_store(&ring->write_index, idx + 1);
uint32_t cnt = atomic_load(&ring->count);
atomic_store(&ring->count, cnt + 1);
static void ring_cleanup(void)
munmap(ring, sizeof(struct sensor_ring));
/* ----------------------------------------------------------------
* ---------------------------------------------------------------- */
static int watchdog_init(void)
watchdog_fd = open("/dev/watchdog", O_WRONLY);
sd_journal_print(LOG_WARNING,
"Cannot open /dev/watchdog: %s (continuing without "
"hardware watchdog)", strerror(errno));
return 0; /* Non-fatal: continue without hardware watchdog */
sd_journal_print(LOG_INFO, "Hardware watchdog opened");
static void watchdog_kick(void)
if (write(watchdog_fd, "k", 1) != 1)
sd_journal_print(LOG_ERR, "Watchdog kick failed");
static void watchdog_close(void)
/* Write 'V' (magic close) to disable watchdog on clean shutdown */
write(watchdog_fd, "V", 1);
/* ----------------------------------------------------------------
* Unix domain socket server
* ---------------------------------------------------------------- */
static int socket_init(void)
listen_fd = socket(AF_UNIX, SOCK_STREAM | SOCK_NONBLOCK, 0);
sd_journal_print(LOG_ERR, "socket() failed: %s", strerror(errno));
memset(&addr, 0, sizeof(addr));
addr.sun_family = AF_UNIX;
strncpy(addr.sun_path, SENSOR_SOCK_PATH, sizeof(addr.sun_path) - 1);
unlink(SENSOR_SOCK_PATH);
if (bind(listen_fd, (struct sockaddr *)&addr, sizeof(addr)) < 0) {
sd_journal_print(LOG_ERR, "bind(%s) failed: %s",
SENSOR_SOCK_PATH, strerror(errno));
chmod(SENSOR_SOCK_PATH, 0666);
if (listen(listen_fd, 5) < 0) {
sd_journal_print(LOG_ERR, "listen() failed: %s", strerror(errno));
sd_journal_print(LOG_INFO, "Listening on %s", SENSOR_SOCK_PATH);
static void handle_client(int client_fd)
char response[MAX_RESPONSE_LEN];
n = recv(client_fd, cmd, sizeof(cmd) - 1, 0);
/* Strip trailing newline */
if (n > 0 && cmd[n - 1] == '\n')
if (strcmp(cmd, CMD_LATEST) == 0) {
uint32_t widx = atomic_load(&ring->write_index);
len = snprintf(response, sizeof(response),
"ERROR: no readings yet\n");
struct sensor_reading *r =
&ring->readings[(widx - 1) % RING_CAPACITY];
len = snprintf(response, sizeof(response),
r->temperature, r->pressure,
r->humidity, (unsigned long)r->timestamp_ms);
} else if (strcmp(cmd, CMD_HISTORY) == 0) {
uint32_t widx = atomic_load(&ring->write_index);
uint32_t cnt = atomic_load(&ring->count);
/* Limit history output to last 10 readings */
uint32_t show = (cnt > 10) ? 10 : cnt;
uint32_t begin = (widx >= show) ? widx - show : 0;
offset += snprintf(response + offset, sizeof(response) - offset,
for (uint32_t i = begin; i < widx && offset < (int)sizeof(response) - 128; i++) {
struct sensor_reading *r = &ring->readings[i % RING_CAPACITY];
offset += snprintf(response + offset, sizeof(response) - offset,
r->temperature, r->pressure,
r->humidity, (unsigned long)r->timestamp_ms);
} else if (strncmp(cmd, CMD_INTERVAL, strlen(CMD_INTERVAL)) == 0) {
/* "interval 2000" sets sample interval to 2000ms */
const char *val = cmd + strlen(CMD_INTERVAL);
while (*val == ' ') val++;
int new_interval = atoi(val);
if (new_interval >= 100 && new_interval <= 60000) {
sample_interval_ms = new_interval;
len = snprintf(response, sizeof(response),
"OK: interval=%d ms\n", sample_interval_ms);
sd_journal_send("MESSAGE=Sample interval changed to %d ms",
"PRIORITY=%d", LOG_NOTICE,
"SENSOR_EVENT=interval_change",
len = snprintf(response, sizeof(response),
"ERROR: interval must be 100..60000 ms\n");
len = snprintf(response, sizeof(response),
"ERROR: unknown command '%s'\n"
"Commands: latest, history, interval <ms>\n", cmd);
send(client_fd, response, len, 0);
/* ----------------------------------------------------------------
* ---------------------------------------------------------------- */
int main(int argc, char *argv[])
uint64_t watchdog_usec = 0;
struct timespec last_sample, now;
install_signal_handlers();
/* Parse optional command-line interval */
sample_interval_ms = atoi(argv[1]);
if (sample_interval_ms < 100)
sample_interval_ms = 1000;
/* Initialize subsystems */
if (bme280_init("/dev/i2c-1") < 0)
/* Check if systemd watchdog is enabled */
wd_enabled = sd_watchdog_enabled(0, &watchdog_usec);
sd_journal_print(LOG_INFO,
"systemd watchdog enabled, interval=%lu us",
(unsigned long)watchdog_usec);
/* Notify systemd that startup is complete */
sd_notify(0, "READY=1\nSTATUS=Monitoring BME280 sensor");
clock_gettime(CLOCK_MONOTONIC, &last_sample);
sd_journal_send("MESSAGE=sensor-monitord started, interval=%d ms",
"SAMPLE_INTERVAL_MS=%d", sample_interval_ms,
/* Accept client connections with a short timeout */
FD_SET(listen_fd, &readfds);
tv.tv_usec = 50000; /* 50ms poll */
int sel = select(listen_fd + 1, &readfds, NULL, NULL, &tv);
if (sel > 0 && FD_ISSET(listen_fd, &readfds)) {
int client_fd = accept(listen_fd, NULL, NULL);
handle_client(client_fd);
/* Check if it is time to sample */
clock_gettime(CLOCK_MONOTONIC, &now);
long elapsed_ms = (now.tv_sec - last_sample.tv_sec) * 1000
+ (now.tv_nsec - last_sample.tv_nsec) / 1000000;
if (elapsed_ms >= sample_interval_ms) {
struct sensor_reading reading;
if (bme280_read(&reading) == 0) {
/* Log every 60th reading with structured fields */
static int log_counter = 0;
if (++log_counter >= 60) {
"MESSAGE=T=%.2f C, P=%.2f hPa, H=%.2f %%RH",
reading.temperature, reading.pressure,
"SENSOR_TYPE=temperature",
"TEMPERATURE=%.2f", reading.temperature,
"PRESSURE=%.2f", reading.pressure,
"HUMIDITY=%.2f", reading.humidity,
sd_journal_print(LOG_WARNING, "BME280 read failed");
/* Kick hardware watchdog */
/* Notify systemd watchdog */
sd_notify(0, "WATCHDOG=1");
sd_notify(0, "STOPPING=1\nSTATUS=Shutting down");
sd_journal_print(LOG_INFO, "sensor-monitord shutting down");
unlink(SENSOR_SOCK_PATH);
Comments