diff --git a/aht10/kernel-space/Makefile b/aht10/kernel-space/Makefile new file mode 100644 index 0000000..b12a0e8 --- /dev/null +++ b/aht10/kernel-space/Makefile @@ -0,0 +1,17 @@ +NAME := aht10-lab + +obj-m += $(NAME).o + +KDIR ?= /lib/modules/$(shell uname -r)/build +PWD := $(shell pwd) +OUT ?= $(PWD)/build + +all: + $(MAKE) -C $(KDIR) M=$(PWD) MO=$(OUT) modules + +clean: + $(MAKE) -C $(KDIR) M=$(PWD) MO=$(OUT) clean + rm -rf $(OUT) + +dts: + dtc -@ -I dts -O dtb -o $(NAME).dtbo $(NAME)-overlay.dts diff --git a/aht10/kernel-space/aht10-lab-overlay.dts b/aht10/kernel-space/aht10-lab-overlay.dts new file mode 100644 index 0000000..83d4a7d --- /dev/null +++ b/aht10/kernel-space/aht10-lab-overlay.dts @@ -0,0 +1,22 @@ +/dts-v1/; +/plugin/; + +/ { + compatible = "brcm,bcm2835"; + + fragment@0 { + target = <&i2c_arm>; + + __overlay__ { + status = "okay"; + #address-cells = <1>; + #size-cells = <0>; + + aht10@38 { + compatible = "learning,aht10-lab"; + reg = <0x38>; + status = "okay"; + }; + }; + }; +}; diff --git a/aht10/kernel-space/aht10-lab.c b/aht10/kernel-space/aht10-lab.c new file mode 100644 index 0000000..f445633 --- /dev/null +++ b/aht10/kernel-space/aht10-lab.c @@ -0,0 +1,357 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#define AHT10_DRIVER_NAME "aht10-lab" +#define AHT10_I2C_ADDR 0x38 + +/* AHT10 commands */ +#define AHT10_CMD_INIT 0xBE +#define AHT10_CMD_TRIGGER 0xAC +#define AHT10_CMD_SOFT_RESET 0xBA + +/* AHT10 status bits */ +#define AHT10_STATUS_BUSY BIT(7) +#define AHT10_STATUS_CALIBRATED BIT(3) + +#define AHT10_PAYLOAD_SIZE 6 +#define AHT10_UPDATE_INTERVAL_MS 1000 + +struct aht10_data { + struct i2c_client *client; + struct mutex lock; + + long temp_milli_c; + long hum_milli_percent; + + unsigned long last_update; + bool valid; +}; + +/* -------------------------------------------------------------------------- + * Sensor I/O helpers + * -------------------------------------------------------------------------- */ +static int aht10_init(struct i2c_client *client) +{ + u8 cmd[] = { AHT10_CMD_INIT, 0x08, 0x00 }; + int ret; + + ret = i2c_master_send(client, cmd, sizeof(cmd)); + if (ret != sizeof(cmd)) { + dev_err(&client->dev, "failed to send init command: ret = %d\n", ret); + return ret < 0 ? ret : -EIO; + } + + msleep(20); + dev_info(&client->dev, "sensor initialization command sent\n"); + + return 0; +} + +static int aht10_soft_reset(struct i2c_client *client) +{ + u8 cmd = AHT10_CMD_SOFT_RESET; + int ret; + + ret = i2c_master_send(client, &cmd, 1); + if (ret != 1) { + dev_err(&client->dev, "failed to send soft reset: ret=%d\n", ret); + return ret < 0 ? ret : -EIO; + } + + msleep(20); + return 0; +} + +static int aht10_read_measurement(struct i2c_client *client, + int *temp_milli_c, + int *hum_milli_percent) +{ + u8 cmd[] = { AHT10_CMD_TRIGGER, 0x33, 0x00 }; + u8 buf[AHT10_PAYLOAD_SIZE]; + u32 humidity_raw; + u32 temperature_raw; + u64 value; + int ret; + + ret = i2c_master_send(client, cmd, sizeof(cmd)); + if (ret != sizeof(cmd)) { + dev_err(&client->dev, "failed to send trigger command: ret = %d\n", ret); + return ret < 0 ? ret : -EIO; + } + + /* Datasheet requires more than 75 ms after triggering measurement. */ + msleep(80); + + ret = i2c_master_recv(client, buf, sizeof(buf)); + if (ret != sizeof(buf)) { + dev_err(&client->dev, "failed to read measurement payload: ret = %d\n", ret); + return ret < 0 ? ret : -EIO; + } + + dev_dbg(&client->dev, + "raw payload: %02x %02x %02x %02x %02x %02x\n", + buf[0], buf[1], buf[2], buf[3], buf[4], buf[5]); + + if (buf[0] & AHT10_STATUS_BUSY) { + dev_err(&client->dev, "sensor still busy!\n"); + return -EBUSY; + } + + if (!(buf[0] & AHT10_STATUS_CALIBRATED)) { + dev_warn(&client->dev, "sensor calibration bit is not set!\n"); + } + + humidity_raw = ((u32)buf[1] << 12) | + ((u32)buf[2] << 4) | + ((u32)buf[3] >> 4); + + temperature_raw = (((u32)buf[3] & 0x0F) << 16) | + ((u32)buf[4] << 8) | + ((u32)buf[5]); + + /* + * Humidity formula: + * RH = humidity_raw * 100 / 2^20 + * + * We store it as milli-percent: + * 45.678 %RH -> 45678 + */ + value = (u64)humidity_raw * 100000; + *hum_milli_percent = div_u64(value, 1 << 20); + + /* + * Temperature formula: + * T = temperature_raw * 200 / 2^20 - 50 + * + * We store it as milli-percent: + * 23.456 C -> 23456 + */ + value = (u64)temperature_raw * 200000; + *temp_milli_c = div_u64(value, 1 << 20) - 50000; + + if (*hum_milli_percent < 0 || *hum_milli_percent > 100000) { + dev_err(&client->dev, "invalid humidity: %d milli-percent\n", + *hum_milli_percent); + return -ERANGE; + } + + if (*temp_milli_c < -40000 || *temp_milli_c > 85000) { + dev_err(&client->dev, "invalid temperature: %d milli-percent\n", + *temp_milli_c); + return -ERANGE; + } + + return 0; +} + +static int aht10_update_measurement(struct aht10_data* data) +{ + int temp; + int hum; + int ret; + + if (data->valid && time_before(jiffies, data->last_update + msecs_to_jiffies(AHT10_UPDATE_INTERVAL_MS))) { + return 0; + } + + ret = aht10_read_measurement(data->client, &temp, &hum); + if (ret) { + return ret; + } + + data->temp_milli_c = temp; + data->hum_milli_percent = hum; + data->last_update = jiffies; + data->valid = true; + + return 0; +} + +/* -------------------------------------------------------------------------- + * HWMON integration + * -------------------------------------------------------------------------- */ +static const struct hwmon_channel_info *const aht10_hwmon_info[] = { + HWMON_CHANNEL_INFO(temp, HWMON_T_INPUT), + HWMON_CHANNEL_INFO(humidity, HWMON_T_INPUT), + NULL +}; + +static umode_t aht10_hwmon_is_visible(const void *drvdata, + enum hwmon_sensor_types type, + u32 attr, + int channel) +{ + switch (type) { + case hwmon_temp_input: + if (attr == hwmon_temp_input) + return 0444; + break; + case hwmon_humidity: + if (attr == hwmon_humidity_input) + return 0444; + break; + default: + break; + } + + return 0; +} + +static int aht10_hwmon_read(struct device *dev, + enum hwmon_sensor_types type, + u32 attr, + int channel, + long *val) +{ + struct aht10_data *data = dev_get_drvdata(dev); + int ret; + + mutex_lock(&data->lock); + + ret = aht10_update_measurement(data); + if (ret) + goto out_unlock; + + switch (type) { + case hwmon_temp_input: + if (attr != hwmon_temp_input) { + ret = -EOPNOTSUPP; + goto out_unlock; + } + *val = data->temp_milli_c; + ret = 0; + break; + + case hwmon_humidity: + if (attr != hwmon_humidity_input) { + ret = -EOPNOTSUPP; + goto out_unlock; + } + *val = data->hum_milli_percent; + ret = 0; + break; + + default: + ret = -EOPNOTSUPP; + break; + } + +out_unlock: + mutex_unlock(&data->lock); + return ret; +} + +static const struct hwmon_ops aht10_hwmon_ops = { + .is_visible = aht10_hwmon_is_visible, + .read = aht10_hwmon_read, +}; + +static const struct hwmon_chip_info aht10_chip_info = { + .ops = &aht10_hwmon_ops, + .info = aht10_hwmon_info, +}; + +/* -------------------------------------------------------------------------- + * I2C driver lifecycle + * -------------------------------------------------------------------------- */ +static int aht10_probe(struct i2c_client *client) +{ + struct i2c_adapter *adapter = client->adapter; + struct aht10_data *data; + struct device *hwmon_dev; + int ret; + + dev_info(&client->dev, "aht10_probe called: addr=0x%02x\n", client->addr); + + if (!i2c_check_functionality(adapter, I2C_FUNC_I2C)) { + dev_err(&client->dev, + "I2C adapter does not support plain I2C transfers!\n"); + return -EOPNOTSUPP; + } + + if (client->addr != AHT10_I2C_ADDR) { + dev_warn(&client->dev, + "unexpected I2C address 0x%02x, expected 0x%02x\n", + client->addr, AHT10_I2C_ADDR); + } + + data = devm_kzalloc(&client->dev, sizeof(*data), GFP_KERNEL); + if (!data) + return -ENOMEM; + + data->client = client; + mutex_init(&data->lock); + + i2c_set_clientdata(client, data); + + msleep(20); + + ret = aht10_init(client); + if (ret) + return ret; + + hwmon_dev = devm_hwmon_device_register_with_info(&client->dev, + "aht10_lab", + data, + &aht10_chip_info, + NULL); + if (IS_ERR(hwmon_dev)) { + dev_err(&client->dev, + "failed to register hwmon service: %ld\n", + PTR_ERR(hwmon_dev)); + return PTR_ERR(hwmon_dev); + } + + dev_info(&client->dev, "hwmon device registered succesfully\n"); + dev_info(&client->dev, "AHT10 driver bound successfully!\n"); + + return 0; +} + +static void aht10_remove(struct i2c_client *client) +{ + dev_info(&client->dev, "AHT10 driver removed!"); +} + +/* -------------------------------------------------------------------------- + * I2C driver registration + * -------------------------------------------------------------------------- */ +static const struct i2c_device_id aht10_id[] = { + { "aht10-lab", 0 }, + { "aht10", 0 }, + { } +}; +MODULE_DEVICE_TABLE(i2c, aht10_id); + +static const struct of_device_id aht10_of_match[] = { + { .compatible = "learning,aht10-lab" }, + { .compatible = "aosong,aht10" }, + { } +}; +MODULE_DEVICE_TABLE(of, aht10_of_match); + +static struct i2c_driver aht10_driver = { + .driver = { + .name = AHT10_DRIVER_NAME, + .of_match_table = aht10_of_match, + }, + .id_table = aht10_id, + .probe = aht10_probe, + .remove = aht10_remove, +}; + +module_i2c_driver(aht10_driver); + +MODULE_LICENSE("GPL"); +MODULE_AUTHOR("Dakata99"); +MODULE_DESCRIPTION("Kernel driver for AHT10 - temperature and humidity sensor."); +MODULE_VERSION("1.0"); diff --git a/aht10/kernel-space/install.sh b/aht10/kernel-space/install.sh new file mode 100755 index 0000000..2cc0cdf --- /dev/null +++ b/aht10/kernel-space/install.sh @@ -0,0 +1,57 @@ +#!/bin/bash + +if [[ $(which pyinstaller) == "" ]]; then + echo "pyinstaller is not installed. Please install it first." + exit 1 +fi + +NAME=aht10-web +APP=web.py +DIST=/tmp/dist-$NAME +DEST=/opt/$NAME + +# Make sure the dist directory does not exist +rm -rf $DIST + +# Install the bundle +pyinstaller \ + --clean \ + --onedir \ + --name $NAME \ + $APP \ + --add-data "templates:templates" \ + --add-data "static:static" \ + --distpath $DIST + +# Install the bundle to /opt/ +sudo rm -rf $DEST +sudo cp -r $DIST $DEST + +sudo tee /etc/systemd/system/$NAME.service > /dev/null << EOF +[Unit] +Description=AHT10 Flask Web Dashboard +Wants=network-online.target +After=network-online.target + +[Service] +Type=simple +User=rpidev + +RuntimeDirectory=$NAME +RuntimeDirectoryMode=0755 +WorkingDirectory=/run/$NAME + +ExecStart=/bin/sh -c 'export HOSTIP="$(hostname -I)" && exec $DEST/$NAME/$NAME' + +Restart=on-failure +RestartSec=3 + +[Install] +WantedBy=multi-user.target +EOF + +# Install it as a service +sudo systemctl daemon-reload +sudo systemctl enable --now $NAME.service +sudo systemctl restart $NAME.service +sudo systemctl status $NAME.service diff --git a/aht10/kernel-space/static/css/styles.css b/aht10/kernel-space/static/css/styles.css new file mode 100644 index 0000000..b88423d --- /dev/null +++ b/aht10/kernel-space/static/css/styles.css @@ -0,0 +1,236 @@ +* { + box-sizing: border-box; + margin: 0; + padding: 0; + font-family: "Segoe UI", Arial, sans-serif; +} + +body { + min-height: 100vh; + background: + radial-gradient(circle at top left, rgba(56, 189, 248, 0.16), transparent 32%), + radial-gradient(circle at bottom right, rgba(251, 146, 60, 0.12), transparent 30%), + linear-gradient(135deg, #111827, #1e293b); + display: flex; + justify-content: center; + align-items: center; + color: #e5e7eb; +} + +.dashboard { + width: min(1000px, 92%); + padding: 36px; + border-radius: 30px; + background: rgba(30, 41, 59, 0.86); + box-shadow: 0 24px 70px rgba(0, 0, 0, 0.34); + backdrop-filter: blur(18px); + border: 1px solid rgba(255, 255, 255, 0.08); +} + +.title { + text-align: center; + margin-bottom: 28px; +} + +.title h1 { + font-size: 2.2rem; + color: #f8fafc; +} + +.title p { + margin-top: 8px; + color: #cbd5e1; + font-size: 1rem; +} + +.time-box { + width: fit-content; + margin: 0 auto 35px auto; + padding: 18px 36px; + border-radius: 22px; + background: linear-gradient(135deg, #e2e8f0, #f8fafc); + color: #0f172a; + text-align: center; + box-shadow: 0 16px 35px rgba(0, 0, 0, 0.22); + border: 1px solid rgba(255, 255, 255, 0.4); +} + +.time-box .label { + font-size: 0.85rem; + color: #64748b; + letter-spacing: 1px; + text-transform: uppercase; + font-weight: 700; +} + +.time-box .time { + margin-top: 6px; + font-size: 2rem; + font-weight: 800; + color: #1e293b; +} + +.status-box { + display: flex; + align-items: center; + gap: 10px; + padding: 10px; + border: 1px solid #ccc; + width: fit-content; + border-radius: 6px; +} + +.light { + width: 16px; + height: 16px; + border-radius: 50%; + background-color: gray; + /* default */ +} + +.cards { + display: grid; + grid-template-columns: repeat(2, 1fr); + gap: 26px; +} + +.card { + position: relative; + overflow: hidden; + min-height: 300px; + padding: 32px; + border-radius: 28px; + background: #f8fafc; + color: #1e293b; + box-shadow: 0 18px 42px rgba(0, 0, 0, 0.22); + transition: transform 0.2s ease, box-shadow 0.2s ease; + border: 1px solid rgba(255, 255, 255, 0.6); +} + +.card:hover { + transform: translateY(-6px); + box-shadow: 0 24px 55px rgba(0, 0, 0, 0.3); +} + +.card.temperature { + background: linear-gradient(135deg, #fff7ed, #f8fafc); +} + +.card.humidity { + background: linear-gradient(135deg, #eff6ff, #f8fafc); +} + +.icon { + width: 84px; + height: 84px; + border-radius: 26px; + display: flex; + justify-content: center; + align-items: center; + font-size: 3rem; + flex-shrink: 0; + box-shadow: inset 0 0 0 1px rgba(255, 255, 255, 0.55); +} + +.temperature .icon { + background: linear-gradient(135deg, #fed7aa, #ffedd5); +} + +.humidity .icon { + background: linear-gradient(135deg, #bfdbfe, #dbeafe); +} + +.card-header { + display: flex; + align-items: center; + gap: 18px; + margin-bottom: 28px; +} + +.card h2 { + font-size: 1.55rem; + margin-bottom: 10px; + color: #0f172a; +} + +.value { + font-size: 4rem; + font-weight: 850; + margin-bottom: 10px; + letter-spacing: -2px; +} + +.temperature .value { + color: #ea580c; +} + +.humidity .value { + color: #2563eb; +} + +.unit { + font-size: 1.8rem; + color: #64748b; + margin-left: 4px; +} + +.description { + margin-top: 16px; + color: #64748b; + line-height: 1.6; +} + +.status { + display: inline-block; + padding: 9px 16px; + border-radius: 999px; + font-size: 0.88rem; + font-weight: 700; +} + +.temperature .status { + background: #ffedd5; + color: #c2410c; +} + +.humidity .status { + background: #dbeafe; + color: #1d4ed8; +} + +.circle { + position: absolute; + right: -55px; + top: -55px; + width: 180px; + height: 180px; + border-radius: 50%; + opacity: 0.2; +} + +.temperature .circle { + background: #fb923c; +} + +.humidity .circle { + background: #60a5fa; +} + +@media (max-width: 750px) { + body { + align-items: flex-start; + padding: 24px 0; + } + + .dashboard { + padding: 24px; + } + + .cards { + grid-template-columns: 1fr; + } + + .value { + font-size: 3.2rem; + } +} \ No newline at end of file diff --git a/aht10/kernel-space/static/js/script.js b/aht10/kernel-space/static/js/script.js new file mode 100644 index 0000000..7c142fe --- /dev/null +++ b/aht10/kernel-space/static/js/script.js @@ -0,0 +1,50 @@ +function updateClock() { + const now = new Date(); + + const time = now.toLocaleTimeString("en-GB", { + hour: "2-digit", + minute: "2-digit", + second: "2-digit" + }); + + document.getElementById("current-time").textContent = time; +} + +updateClock(); +setInterval(updateClock, 1000); + +function update() { + fetch("/sensor-data") + .then(response => response.json()) + .then(data => { + const light = document.getElementById("status-light"); + const text = document.getElementById("status-text"); + + if (data.ok) { + document.getElementById("temperature-value").textContent = + data.temperature.toFixed(2) + " °C"; + + document.getElementById("humidity-value").textContent = + data.humidity.toFixed(2) + " %"; + + document.getElementById("temperature-status").textContent = + data["temperature-status"]; + + document.getElementById("humidity-status").textContent = + data["humidity-status"]; + + light.style.backgroundColor = "green"; + text.textContent = 'Sensor is availble.' + } else { + light.style.backgroundColor = "red"; + text.textContent = 'Sensor is not available: ' + data.error; + } + }) + .catch(err => console.error("Error fetching env:", err)); +} + +// Update every 1 second +setInterval(update, 1000); + +// Run immediately on page load +update(); diff --git a/aht10/kernel-space/templates/index.html b/aht10/kernel-space/templates/index.html new file mode 100644 index 0000000..359d49b --- /dev/null +++ b/aht10/kernel-space/templates/index.html @@ -0,0 +1,77 @@ + + + + + + + Environment Monitor + + + + + +
+
+

Environment Monitor

+

Live temperature and humidity dashboard

+
+ +
+
Current Time
+
--:--:--
+ +
+
+ +
+
+ +
+
+
+
🌡️
+ +
+

Temperature

+
{{ temp_status }}
+
+
+ +
+ {{ temperature }}°C +
+ +

+ Measures the surrounding air temperature from your sensor. +

+ +
+
+ +
+
+
💧
+ +
+

Humidity

+
{{ humid_status }}
+
+
+ +
+ {{ humidity }}% +
+ +

+ Shows the amount of moisture currently present in the air. +

+ +
+
+
+
+ + + + + \ No newline at end of file diff --git a/aht10/kernel-space/web.py b/aht10/kernel-space/web.py new file mode 100644 index 0000000..44a8fe8 --- /dev/null +++ b/aht10/kernel-space/web.py @@ -0,0 +1,135 @@ +from flask import Flask, render_template, jsonify +from loguru import logger +from pathlib import Path +from typing import Optional +import os + +HOSTIP: str = os.environ.get("HOSTIP", "0.0.0.0").strip() +PORT: int = int(os.environ.get("PORT", 5000)) + +# --- hwmon -- +class Hwmon(): + I2C_DEVICE: Path = Path('/sys/bus/i2c/devices/1-0038') + + def __init__(self): + self._hwmon_root: Path = self.I2C_DEVICE / 'hwmon' + + if not self._hwmon_root.exists(): + raise RuntimeError(f"hwmon directory not found: {self._hwmon_root}") + + hwmon_dirs = list(self._hwmon_root.glob('hwmon*')) + + if not hwmon_dirs: + raise RuntimeError(f"no hwmon dirs found under: {self._hwmon_root}") + + hwmon_dir = hwmon_dirs[0] + self._temperature: Path = hwmon_dir / 'temp1_input' + self._humidity: Path = hwmon_dir / 'humidity1_input' + + if not self._temperature.exists(): + raise RuntimeError(f'missing temperature file: {self._temperature}') + + if not self._humidity.exists(): + raise RuntimeError(f'missing humidity file: {self._humidity}') + + def read_temperature(self): + return int(self._temperature.read_text().strip()) / 1000 + + def read_humidity(self): + return int(self._humidity.read_text().strip()) / 1000 + + def read(self): + temperature: int = self.read_temperature() + humidity: int = self.read_humidity() + + return { + "temperature": temperature, + "humidity": humidity, + "temperature-status": get_status(temperature, TEMP_MAPPING), + "humidity-status": get_status(humidity, HUM_MAPPING) + } + +HWMON: Optional[Hwmon] = None + +def get_hwmon() -> Hwmon: + global HWMON + + if HWMON is None: + return Hwmon() + + return HWMON + +# --- helpers --- + +TEMP_MAPPING: dict = { + "Cold": lambda temp: temp < 16, + "Comfortable": lambda temp: temp >= 16 and temp < 20, + "Hot": lambda temp: temp >= 20, +} + +HUM_MAPPING: dict = { + "Dry": lambda humidity: humidity < 30, + "Normal": lambda humidity: humidity >= 30 and humidity < 60, + "Humid": lambda humidity: humidity > 60, +} + +def get_status(value: float, mapping: dict) -> str: + for status, expression in mapping.items(): + if expression(value): + return status + return "Unknown" + +# --- web client --- +web = Flask(__name__) + +@web.route("/") +def index(): + try: + data: dict[str, int | str] = get_hwmon().read() + + return render_template( + "index.html", + sensor_ok=True, + temperature=data['temperature'], + humidity=data['humidity'], + temp_status=data['temperature-status'], + humid_status=data['humidity-status'], + error=None + ) + except Exception as e: + logger.error(f"Sensor unavailable: {e}") + + return render_template( + "index.html", + sensor_ok=False, + temperature=None, + humidity=None, + temp_status="Unknown", + humid_status="Unknown", + error=str(e) + ) + +@web.get("/sensor-data") +def env(): + try: + data: dict[str, int | str] = get_hwmon().read() + + return jsonify({ + "ok": True, + **data + }) + except Exception as e: + logger.error(f"Sensor unavailable: {e}") + + return jsonify({ + "ok": False, + "error": str(e) + }), 503 + +if __name__ == "__main__": + web.run( + host=HOSTIP, + port=PORT, + debug=False, + use_reloader=False, + ) diff --git a/aht10/static/css/styles.css b/aht10/user-space/static/css/styles.css similarity index 100% rename from aht10/static/css/styles.css rename to aht10/user-space/static/css/styles.css diff --git a/aht10/static/js/script.js b/aht10/user-space/static/js/script.js similarity index 67% rename from aht10/static/js/script.js rename to aht10/user-space/static/js/script.js index a85fa10..ef16245 100644 --- a/aht10/static/js/script.js +++ b/aht10/user-space/static/js/script.js @@ -13,27 +13,22 @@ function updateClock() { updateClock(); setInterval(updateClock, 1000); -/* - Later, when you connect this to Flask, you can replace the static - values with data from your backend, for example: - - fetch("/sensor-data") - .then(response => response.json()) - .then(data => { - document.getElementById("temperature-value").textContent = data.temperature; - document.getElementById("humidity-value").textContent = data.humidity; - }); -*/ - function updateEnv() { fetch("/api/env") .then(response => response.json()) .then(data => { + console.log(data); document.getElementById("temperature-value").textContent = data.temperature.toFixed(2) + " °C"; + document.getElementById("temperature-status").textContent = + data['temperature-status']; + document.getElementById("humidity-value").textContent = data.humidity.toFixed(2) + " %"; + + document.getElementById("humidity-status").textContent = + data['humidity-status']; }) .catch(err => console.error("Error fetching env:", err)); } diff --git a/aht10/templates/index.html b/aht10/user-space/templates/index.html similarity index 85% rename from aht10/templates/index.html rename to aht10/user-space/templates/index.html index f4f7a80..34b496e 100644 --- a/aht10/templates/index.html +++ b/aht10/user-space/templates/index.html @@ -28,12 +28,12 @@

Environment Monitor

Temperature

-
{{ temp_status }}
+
{{ temperature_status }}
- {{ temperature | round(2) }}°C + {{ temperature }}°C

@@ -49,12 +49,12 @@

Temperature

Humidity

-
{{ humid_status }}
+
{{ humidity_status }}
- {{ humidity | round(2) }}% + {{ humidity }}%

diff --git a/aht10/web.py b/aht10/user-space/web.py similarity index 51% rename from aht10/web.py rename to aht10/user-space/web.py index 39392e8..39c7081 100644 --- a/aht10/web.py +++ b/aht10/user-space/web.py @@ -1,4 +1,4 @@ -from flask import Flask, render_template +from flask import Flask, render_template, jsonify import board import adafruit_ahtx0 import time @@ -9,16 +9,16 @@ AHT10 = adafruit_ahtx0.AHTx0(board.I2C()) -TEMP_MAPPING: dict = { - "cold": lambda temp: temp < 16, - "comfortable": lambda temp: temp >= 16 and temp < 20, - "hot": lambda temp: temp >= 20, +TEMPERATURE_MAPPING: dict = { + "Cold": lambda temp: temp < 16, + "Comfortable": lambda temp: temp >= 16 and temp < 20, + "Hot": lambda temp: temp >= 20, } -HUM_MAPPING: dict = { - "dry": lambda humidity: humidity < 30, - "normal": lambda humidity: humidity >= 30 and humidity < 60, - "humid": lambda humidity: humidity > 60, +HUMIDITY_MAPPING: dict = { + "Dry": lambda humidity: humidity < 30, + "Normal": lambda humidity: humidity >= 30 and humidity < 60, + "Humid": lambda humidity: humidity > 60, } @@ -34,6 +34,7 @@ def get_status(value: float, mapping: dict) -> str: for status, expression in mapping.items(): if expression(value): return status + return "Unknown" web = Flask(__name__) @@ -45,14 +46,19 @@ def index(): "index.html", temperature=MEASUREMENTS["temperature"], humidity=MEASUREMENTS["humidity"], - temp_status=get_status(MEASUREMENTS["temperature"], TEMP_MAPPING).capitalize(), - humid_status=get_status(MEASUREMENTS["humidity"], HUM_MAPPING).capitalize(), + temperature_status=get_status(MEASUREMENTS["temperature"], TEMPERATURE_MAPPING), + humidity_status=get_status(MEASUREMENTS["humidity"], HUMIDITY_MAPPING), ) @web.get("/api/env") def env(): - return MEASUREMENTS + return jsonify({ + 'temperature': MEASUREMENTS["temperature"], + 'humidity': MEASUREMENTS["humidity"], + 'temperature-status': get_status(MEASUREMENTS["temperature"], TEMPERATURE_MAPPING), + 'humidity-status': get_status(MEASUREMENTS["humidity"], HUMIDITY_MAPPING), + }) threading.Thread(target=poll, args=(AHT10,), daemon=True).start() diff --git a/docs/aht10/driver.md b/docs/aht10/driver.md new file mode 100644 index 0000000..b368c23 --- /dev/null +++ b/docs/aht10/driver.md @@ -0,0 +1,67 @@ +# Linux kernel driver + +## Datasheet + +The notes below, describe the full measurement cycle: +``` +1. Power-on delay + - After power is applied, wait up to 20 ms before sending commands. + +2. Initialization + - Send initialization command: + 0xE1 0x08 0x00 + +3. Trigger measurement + - Send: + 0xAC 0x33 0x00 + +4. Wait for conversion + - Wait at least 75 ms. + +5. Read payload + - Read 6 bytes from the sensor. + +6. Check status + - byte[0] is the status byte. + - bit 7 = busy flag. + - bit 3 = calibration flag. + +7. Extract raw humidity and temperature + - Humidity is a 20-bit value. + - Temperature is a 20-bit value. + +8. Convert raw values + - RH = humidity_raw / 2^20 * 100 + - T = temperature_raw / 2^20 * 200 - 50 +``` + +## Hardware wiring + +## Device Tree overlay + +## Kernel module build/install + +To build the kernel driver do: +```bash +cd aht10/kernel-space +make +``` +and to insall it: +```bash +sudo insmod build/aht10-lab.ko +``` + +## hwmon sysfs paths + +## Flask setup + +## systemd service + + + +To create the `systemd` service, do: +```bash +sudo vim /etc/systemd/system/aht10-web.service +``` + +## Troubleshooting diff --git a/docs/aht10/web-client.md b/docs/aht10/web-client.md index 463dac5..9d8ae64 100644 --- a/docs/aht10/web-client.md +++ b/docs/aht10/web-client.md @@ -9,7 +9,7 @@ Firstly, setup the environment: then run: ```bash -flask --app aht10/web.py run -h $(hostname -I) +flask --app aht10/user-space/web.py run -h $(hostname -I) ``` Use `--debug` option to enable reloading (while developing). diff --git a/docs/tips.md b/docs/tips.md index b31c9d4..5159201 100644 --- a/docs/tips.md +++ b/docs/tips.md @@ -23,4 +23,26 @@ or in one line: uv run mkdocs serve -a $(hostname -I | cut -d' ' -f1):8000 ``` -Then open a browser on your PC and enter: `http://:8000`. +Then open a browser on your PC and enter: `http://:8000` or get it via: +```bash +echo "https://$(hostname -I | cut -d ' ' -f1):8000" +``` + +## How to run Flask? + +Firstly, setup the environment: +```bash +. setupenv +``` + +and then: +```bash +flask --app /path/to/app.py run -h $(hostname -I) +``` + +Use `--debug` option to enable reloading (while developing). + +Then open a browser on your PC and enter: `http://:5000`, where `=$(hostname -I)` or get it via: +```bash +echo "https://$(hostname -I | cut -d ' ' -f1):5000" +``` diff --git a/mkdocs.yml b/mkdocs.yml index 47e7290..cccfe5c 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -20,5 +20,6 @@ nav: - AHT10: - Overview: aht10/index.md - Web client: aht10/web-client.md + - Driver: aht10/driver.md - Tips: tips.md - References: references.md