i2c fixes + server improvements
This commit is contained in:
+48
-18
@@ -1,5 +1,6 @@
|
|||||||
import os
|
import os
|
||||||
from datetime import datetime, timedelta, timezone
|
from datetime import datetime, timedelta, timezone
|
||||||
|
from zoneinfo import ZoneInfo, ZoneInfoNotFoundError
|
||||||
|
|
||||||
import click
|
import click
|
||||||
from flask import Flask, jsonify, redirect, render_template, request, url_for
|
from flask import Flask, jsonify, redirect, render_template, request, url_for
|
||||||
@@ -10,7 +11,6 @@ from flask_login import (
|
|||||||
login_user,
|
login_user,
|
||||||
logout_user,
|
logout_user,
|
||||||
)
|
)
|
||||||
from sqlalchemy import func
|
|
||||||
|
|
||||||
from models import Sensor, SensorReadout, User, db
|
from models import Sensor, SensorReadout, User, db
|
||||||
|
|
||||||
@@ -137,37 +137,67 @@ def create_app():
|
|||||||
def api_sensor_readouts_hourly(sensor_id):
|
def api_sensor_readouts_hourly(sensor_id):
|
||||||
type_ = request.args.get("type")
|
type_ = request.args.get("type")
|
||||||
range_ = request.args.get("range", "all")
|
range_ = request.args.get("range", "all")
|
||||||
|
# Frontend wysyła strefę czasową przeglądarki (np. Europe/Warsaw),
|
||||||
|
# żeby "today" znaczyło "od północy czasu lokalnego", a nie UTC.
|
||||||
|
tz_name = request.args.get("tz", "Europe/Warsaw") or "Europe/Warsaw"
|
||||||
|
try:
|
||||||
|
local_tz = ZoneInfo(tz_name)
|
||||||
|
except (ZoneInfoNotFoundError, ValueError):
|
||||||
|
return jsonify({"error": f"unknown timezone: {tz_name}"}), 400
|
||||||
# Timestamps are stored as naive UTC, so the cutoff must be naive UTC too.
|
# Timestamps are stored as naive UTC, so the cutoff must be naive UTC too.
|
||||||
now = datetime.now(timezone.utc).replace(tzinfo=None)
|
now_utc = datetime.now(timezone.utc)
|
||||||
|
now_local = now_utc.astimezone(local_tz)
|
||||||
if range_ == "today":
|
if range_ == "today":
|
||||||
cutoff = now.replace(hour=0, minute=0, second=0, microsecond=0)
|
midnight_local = now_local.replace(hour=0, minute=0, second=0, microsecond=0)
|
||||||
|
cutoff = midnight_local.astimezone(timezone.utc).replace(tzinfo=None)
|
||||||
|
elif range_ == "24h":
|
||||||
|
cutoff = (now_utc - timedelta(hours=24)).replace(tzinfo=None)
|
||||||
elif range_ == "week":
|
elif range_ == "week":
|
||||||
cutoff = now - timedelta(days=7)
|
cutoff = (now_utc - timedelta(days=7)).replace(tzinfo=None)
|
||||||
elif range_ == "month":
|
elif range_ == "month":
|
||||||
cutoff = now - timedelta(days=30)
|
cutoff = (now_utc - timedelta(days=30)).replace(tzinfo=None)
|
||||||
elif range_ == "all":
|
elif range_ == "all":
|
||||||
cutoff = None
|
cutoff = None
|
||||||
else:
|
else:
|
||||||
return jsonify({"error": "range must be one of: today, week, month, all"}), 400
|
return jsonify({"error": "range must be one of: today, 24h, week, month, all"}), 400
|
||||||
query = (
|
query = db.session.query(SensorReadout.timestamp, SensorReadout.value).filter(
|
||||||
db.session.query(
|
SensorReadout.sensor_id == sensor_id
|
||||||
func.strftime("%Y-%m-%dT%H:00:00", SensorReadout.timestamp).label("hour"),
|
|
||||||
func.avg(SensorReadout.value).label("avg_value"),
|
|
||||||
)
|
|
||||||
.filter(SensorReadout.sensor_id == sensor_id)
|
|
||||||
)
|
)
|
||||||
if cutoff is not None:
|
if cutoff is not None:
|
||||||
query = query.filter(SensorReadout.timestamp >= cutoff)
|
query = query.filter(SensorReadout.timestamp >= cutoff)
|
||||||
if type_:
|
if type_:
|
||||||
query = query.filter(SensorReadout.type == type_)
|
query = query.filter(SensorReadout.type == type_)
|
||||||
rows = (
|
rows = query.order_by(SensorReadout.timestamp).all()
|
||||||
query.group_by("hour")
|
# Zakresy today i 24h: wszystkie pomiary jako punkty, bez uśredniania.
|
||||||
.order_by("hour")
|
if range_ in ("today", "24h"):
|
||||||
.all()
|
|
||||||
)
|
|
||||||
return jsonify(
|
return jsonify(
|
||||||
[{"timestamp": r.hour, "value": round(r.avg_value, 2)} for r in rows]
|
[
|
||||||
|
{
|
||||||
|
"timestamp": ts.strftime("%Y-%m-%dT%H:%M:%S"),
|
||||||
|
"value": value,
|
||||||
|
}
|
||||||
|
for ts, value in rows
|
||||||
|
]
|
||||||
)
|
)
|
||||||
|
# Pozostałe zakresy: średnia godzinowa liczona po pełnej godzinie
|
||||||
|
# lokalnej (poprawne też przy zmianie DST), znacznik to początek
|
||||||
|
# godziny w UTC (frontend dokleja "Z").
|
||||||
|
buckets: dict[datetime, list[float]] = {}
|
||||||
|
for ts, value in rows:
|
||||||
|
ts_utc = ts.replace(tzinfo=timezone.utc)
|
||||||
|
hour_local = ts_utc.astimezone(local_tz).replace(
|
||||||
|
minute=0, second=0, microsecond=0
|
||||||
|
)
|
||||||
|
hour_utc = hour_local.astimezone(timezone.utc).replace(tzinfo=None)
|
||||||
|
buckets.setdefault(hour_utc, []).append(value)
|
||||||
|
result = [
|
||||||
|
{
|
||||||
|
"timestamp": hour.strftime("%Y-%m-%dT%H:00:00"),
|
||||||
|
"value": round(sum(values) / len(values), 2),
|
||||||
|
}
|
||||||
|
for hour, values in sorted(buckets.items())
|
||||||
|
]
|
||||||
|
return jsonify(result)
|
||||||
|
|
||||||
# --- API: device writes (intentionally public — called by devices/sensors) ---
|
# --- API: device writes (intentionally public — called by devices/sensors) ---
|
||||||
|
|
||||||
|
|||||||
@@ -35,6 +35,7 @@
|
|||||||
<label for="range-select">Range:</label>
|
<label for="range-select">Range:</label>
|
||||||
<select id="range-select">
|
<select id="range-select">
|
||||||
<option value="today">Today</option>
|
<option value="today">Today</option>
|
||||||
|
<option value="24h">Last 24h</option>
|
||||||
<option value="week">Week</option>
|
<option value="week">Week</option>
|
||||||
<option value="month">Month</option>
|
<option value="month">Month</option>
|
||||||
<option value="all" selected>All</option>
|
<option value="all" selected>All</option>
|
||||||
@@ -44,6 +45,7 @@
|
|||||||
|
|
||||||
<div id="chart-section" class="hidden chart-box">
|
<div id="chart-section" class="hidden chart-box">
|
||||||
<canvas id="chart"></canvas>
|
<canvas id="chart"></canvas>
|
||||||
|
<p id="chart-info" class="desc"></p>
|
||||||
<p id="chart-error" class="error hidden"></p>
|
<p id="chart-error" class="error hidden"></p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -74,8 +76,13 @@
|
|||||||
const refreshButton = document.getElementById('refresh-button');
|
const refreshButton = document.getElementById('refresh-button');
|
||||||
const readoutsSection = document.getElementById('readouts-section');
|
const readoutsSection = document.getElementById('readouts-section');
|
||||||
const readoutsBody = document.getElementById('readouts-body');
|
const readoutsBody = document.getElementById('readouts-body');
|
||||||
|
const chartInfo = document.getElementById('chart-info');
|
||||||
let chart = null;
|
let chart = null;
|
||||||
|
|
||||||
|
// Strefa czasowa przeglądarki — backend liczy "today" od północy
|
||||||
|
// w tej strefie (np. Europe/Warsaw = GMT+2 latem).
|
||||||
|
const userTz = Intl.DateTimeFormat().resolvedOptions().timeZone || 'Europe/Warsaw';
|
||||||
|
|
||||||
// The page must keep working (dropdown + table) even if the
|
// The page must keep working (dropdown + table) even if the
|
||||||
// Chart.js CDN is unreachable — the chart is then simply skipped.
|
// Chart.js CDN is unreachable — the chart is then simply skipped.
|
||||||
if (window.Chart) {
|
if (window.Chart) {
|
||||||
@@ -87,12 +94,38 @@
|
|||||||
|
|
||||||
function formatTimestamp(iso) {
|
function formatTimestamp(iso) {
|
||||||
const d = new Date(iso);
|
const d = new Date(iso);
|
||||||
return d.toLocaleString('pl-PL', { timeZone: 'Europe/Warsaw' });
|
return d.toLocaleString('pl-PL', { timeZone: userTz });
|
||||||
}
|
}
|
||||||
|
|
||||||
function formatChartLabel(iso) {
|
function formatChartLabel(iso) {
|
||||||
const d = new Date(iso + 'Z');
|
const d = new Date(iso + 'Z');
|
||||||
return d.toLocaleString('pl-PL', { timeZone: 'Europe/Warsaw', hour: '2-digit', minute: '2-digit', day: '2-digit', month: '2-digit', year: 'numeric' });
|
return d.toLocaleString('pl-PL', { timeZone: userTz, hour: '2-digit', minute: '2-digit', day: '2-digit', month: '2-digit', year: 'numeric' });
|
||||||
|
}
|
||||||
|
|
||||||
|
function chartInfoText(range) {
|
||||||
|
if (range === 'today') return `Raw measurements since local midnight (${userTz}) — no averaging.`;
|
||||||
|
if (range === '24h') return 'Raw measurements from the last 24 hours — no averaging.';
|
||||||
|
const period = range === 'week' ? 'the last 7 days'
|
||||||
|
: range === 'month' ? 'the last 30 days'
|
||||||
|
: 'all stored data';
|
||||||
|
return `Hourly averages over ${period} — each point is the mean of all measurements in that local hour (${userTz}).`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ciśnienie jest składowane w bazie w paskalach (tak wysyła firmware),
|
||||||
|
// a wyświetlane w hektopaskalach z jednym miejscem po przecinku.
|
||||||
|
function displayValue(type, value) {
|
||||||
|
if (type === 'pressure') return Math.round(value / 10) / 10;
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatTableValue(type, value) {
|
||||||
|
if (type === 'pressure') return (value / 100).toFixed(1) + ' hPa';
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
function yAxisTitle(type) {
|
||||||
|
if (type === 'pressure') return 'pressure (hPa)';
|
||||||
|
return type;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function fetchJSON(url) {
|
async function fetchJSON(url) {
|
||||||
@@ -143,11 +176,14 @@
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
// fetch hourly data for chart
|
// fetch hourly data for chart
|
||||||
const hourly = await fetchJSON(`/api/sensors/${sensorId}/readouts/hourly?type=${type}&range=${range}`);
|
const hourly = await fetchJSON(`/api/sensors/${sensorId}/readouts/hourly?type=${encodeURIComponent(type)}&range=${encodeURIComponent(range)}&tz=${encodeURIComponent(userTz)}`);
|
||||||
if (!hourly.length) throw new Error('no data for this range');
|
if (!hourly.length) throw new Error('no data for this range');
|
||||||
|
|
||||||
const labels = hourly.map(r => formatChartLabel(r.timestamp));
|
const labels = hourly.map(r => formatChartLabel(r.timestamp));
|
||||||
const values = hourly.map(r => r.value);
|
const values = hourly.map(r => displayValue(type, r.value));
|
||||||
|
// Przy zakresach today/24h punktów jest dużo (~1/min),
|
||||||
|
// więc mniejsze markery żeby wykres był czytelny.
|
||||||
|
const dense = range === 'today' || range === '24h';
|
||||||
|
|
||||||
if (chart) chart.destroy();
|
if (chart) chart.destroy();
|
||||||
const ctx = document.getElementById('chart').getContext('2d');
|
const ctx = document.getElementById('chart').getContext('2d');
|
||||||
@@ -164,15 +200,15 @@
|
|||||||
pointBorderColor: '#55ffff',
|
pointBorderColor: '#55ffff',
|
||||||
fill: true,
|
fill: true,
|
||||||
tension: 0.3,
|
tension: 0.3,
|
||||||
pointRadius: 3,
|
pointRadius: dense ? 1.5 : 3,
|
||||||
}]
|
}]
|
||||||
},
|
},
|
||||||
options: {
|
options: {
|
||||||
responsive: true,
|
responsive: true,
|
||||||
plugins: { legend: { display: false } },
|
plugins: { legend: { display: false } },
|
||||||
scales: {
|
scales: {
|
||||||
x: { title: { display: true, text: 'Time (hourly avg)' } },
|
x: { title: { display: true, text: dense ? 'Time' : 'Time (hourly avg)' } },
|
||||||
y: { title: { display: true, text: type } }
|
y: { title: { display: true, text: yAxisTitle(type) } }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -180,9 +216,11 @@
|
|||||||
// The chart was created while the container was hidden,
|
// The chart was created while the container was hidden,
|
||||||
// so force a re-measure now that it is visible.
|
// so force a re-measure now that it is visible.
|
||||||
chart.resize();
|
chart.resize();
|
||||||
|
chartInfo.textContent = '> ' + chartInfoText(range);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
chartError.textContent = '! Chart error: ' + err.message;
|
chartError.textContent = '! Chart error: ' + err.message;
|
||||||
chartError.classList.remove('hidden');
|
chartError.classList.remove('hidden');
|
||||||
|
chartInfo.textContent = '';
|
||||||
chartSection.classList.remove('hidden');
|
chartSection.classList.remove('hidden');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -199,7 +237,7 @@
|
|||||||
const readouts = await fetchJSON(`/api/sensors/${sensorId}/readouts?type=${type}`);
|
const readouts = await fetchJSON(`/api/sensors/${sensorId}/readouts?type=${type}`);
|
||||||
readouts.forEach(r => {
|
readouts.forEach(r => {
|
||||||
const tr = document.createElement('tr');
|
const tr = document.createElement('tr');
|
||||||
tr.innerHTML = `<td>${formatTimestamp(r.timestamp)}</td><td>${r.value}</td>`;
|
tr.innerHTML = `<td>${formatTimestamp(r.timestamp)}</td><td>${formatTableValue(type, r.value)}</td>`;
|
||||||
readoutsBody.appendChild(tr);
|
readoutsBody.appendChild(tr);
|
||||||
});
|
});
|
||||||
readoutsSection.classList.remove('hidden');
|
readoutsSection.classList.remove('hidden');
|
||||||
|
|||||||
@@ -1,2 +1,2 @@
|
|||||||
idf_component_register(SRCS "thermo_sensor.c" "ygg_http_routes.c" "ygg_bmp280.c" "ygg_http_req.c" "ygg_state.c" "ygg_sh1106.c" "../common_esp32/ygg_wifi.c" "../common_esp32/ygg_http_server.c"
|
idf_component_register(SRCS "thermo_sensor.c" "ygg_http_routes.c" "ygg_bmp280.c" "ygg_http_req.c" "ygg_state.c" "ygg_sh1106.c" "ygg_lock.c" "../common_esp32/ygg_wifi.c" "../common_esp32/ygg_http_server.c"
|
||||||
INCLUDE_DIRS "." "../common_esp32")
|
INCLUDE_DIRS "." "../common_esp32")
|
||||||
|
|||||||
@@ -3,9 +3,11 @@
|
|||||||
#include <freertos/task.h>
|
#include <freertos/task.h>
|
||||||
#include <esp_system.h>
|
#include <esp_system.h>
|
||||||
#include <esp_timer.h>
|
#include <esp_timer.h>
|
||||||
|
#include <esp_log.h>
|
||||||
|
|
||||||
#include "nvs_flash.h"
|
#include "nvs_flash.h"
|
||||||
#include "esp_http_server.h"
|
#include "esp_http_server.h"
|
||||||
|
#include "i2cdev.h"
|
||||||
|
|
||||||
#include "ygg_http_server.h"
|
#include "ygg_http_server.h"
|
||||||
#include "ygg_http_routes.h"
|
#include "ygg_http_routes.h"
|
||||||
@@ -15,6 +17,7 @@
|
|||||||
#include "ygg_http_req.h"
|
#include "ygg_http_req.h"
|
||||||
#include "ygg_state.h"
|
#include "ygg_state.h"
|
||||||
#include "ygg_sh1106.h"
|
#include "ygg_sh1106.h"
|
||||||
|
#include "ygg_lock.h"
|
||||||
#include "lvgl.h"
|
#include "lvgl.h"
|
||||||
|
|
||||||
|
|
||||||
@@ -25,7 +28,10 @@ bmp280_t dev_bmp_280;
|
|||||||
|
|
||||||
void lvgl_task(void *pvParameters) {
|
void lvgl_task(void *pvParameters) {
|
||||||
while (1) {
|
while (1) {
|
||||||
|
if (ygg_lvgl_lock_take(pdMS_TO_TICKS(100))) {
|
||||||
lv_timer_handler();
|
lv_timer_handler();
|
||||||
|
ygg_lvgl_lock_give();
|
||||||
|
}
|
||||||
vTaskDelay(pdMS_TO_TICKS(10));
|
vTaskDelay(pdMS_TO_TICKS(10));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -34,10 +40,14 @@ void sensor_task(void *pvParameters) {
|
|||||||
// Wait 1s, to avoid corrupted first sensor readout
|
// Wait 1s, to avoid corrupted first sensor readout
|
||||||
vTaskDelay(pdMS_TO_TICKS(1000));
|
vTaskDelay(pdMS_TO_TICKS(1000));
|
||||||
while (1) {
|
while (1) {
|
||||||
ygg_bmp280_read_sensor(&dev_bmp_280);
|
esp_err_t err = ygg_bmp280_read_sensor(&dev_bmp_280);
|
||||||
// Najpierw OLED, potem (blokujący) HTTP — żeby ekran
|
// Najpierw OLED, potem (blokujący) HTTP — żeby ekran
|
||||||
// odświeżył się nawet gdy serwer nie odpowiada.
|
// odświeżył się nawet gdy serwer nie odpowiada.
|
||||||
|
// ygg_bmp280_read_sensor() aktualizuje CURR_READOUT TYLKO przy
|
||||||
|
// sukcesie, więc przy błędzie pokazujemy ostatnie dobre + ERR
|
||||||
|
// i NIE wysyłamy śmieci do bazy.
|
||||||
char buf[256];
|
char buf[256];
|
||||||
|
if (err == ESP_OK) {
|
||||||
snprintf(buf, sizeof(buf), "Temp: %.1f C\nHumid: %.1f %%\nPress: %.1f hPa",
|
snprintf(buf, sizeof(buf), "Temp: %.1f C\nHumid: %.1f %%\nPress: %.1f hPa",
|
||||||
CURR_READOUT.temperature,
|
CURR_READOUT.temperature,
|
||||||
CURR_READOUT.humidity,
|
CURR_READOUT.humidity,
|
||||||
@@ -46,7 +56,15 @@ void sensor_task(void *pvParameters) {
|
|||||||
ygg_send_sensor_readouts(BASE_URL,
|
ygg_send_sensor_readouts(BASE_URL,
|
||||||
SENSOR_NAME,
|
SENSOR_NAME,
|
||||||
CURR_READOUT);
|
CURR_READOUT);
|
||||||
vTaskDelay(pdMS_TO_TICKS(10000));
|
} else {
|
||||||
|
snprintf(buf, sizeof(buf), "SENSOR ERR\nTemp: %.1f C\nHumid: %.1f %%\nPress: %.1f hPa",
|
||||||
|
CURR_READOUT.temperature,
|
||||||
|
CURR_READOUT.humidity,
|
||||||
|
CURR_READOUT.pressure / 100.);
|
||||||
|
ygg_set_display_label(sh_display, label, buf);
|
||||||
|
ESP_LOGW(TAG, "skipping HTTP POST, keeping last good readout");
|
||||||
|
}
|
||||||
|
vTaskDelay(pdMS_TO_TICKS(60 * 1000));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -77,6 +95,12 @@ void app_main()
|
|||||||
httpd_register_uri_handler(server, &ygg_press_uri);
|
httpd_register_uri_handler(server, &ygg_press_uri);
|
||||||
httpd_register_uri_handler(server, &ygg_hum_uri);
|
httpd_register_uri_handler(server, &ygg_hum_uri);
|
||||||
|
|
||||||
|
// Jedna centralna inicjalizacja: mutexy + magistrala I2C.
|
||||||
|
// Wcześniej i2cdev_init() był wołany w obu ygg_*_init() + bus był
|
||||||
|
// tworzony przez dummy-device OLED, co mieszało.
|
||||||
|
ygg_lock_init();
|
||||||
|
ESP_ERROR_CHECK(i2cdev_init());
|
||||||
|
|
||||||
sh_display = ygg_sh1106_init();
|
sh_display = ygg_sh1106_init();
|
||||||
|
|
||||||
lv_obj_t *scr = lv_display_get_screen_active(sh_display);
|
lv_obj_t *scr = lv_display_get_screen_active(sh_display);
|
||||||
|
|||||||
@@ -1,29 +1,52 @@
|
|||||||
#include <stdio.h>
|
#include <stdio.h>
|
||||||
#include <string.h>
|
#include <string.h>
|
||||||
#include <bmp280.h>
|
#include <bmp280.h>
|
||||||
|
#include <esp_log.h>
|
||||||
#include "ygg_bmp280.h"
|
#include "ygg_bmp280.h"
|
||||||
#include "ygg_state.h"
|
#include "ygg_state.h"
|
||||||
|
#include "ygg_lock.h"
|
||||||
|
|
||||||
|
static const char *TAG = "ygg_bmp280";
|
||||||
|
|
||||||
void ygg_bmp280_init(bmp280_t *dev) {
|
void ygg_bmp280_init(bmp280_t *dev) {
|
||||||
ESP_ERROR_CHECK(i2cdev_init());
|
// UWAGA: i2cdev_init() + ygg_lock_init() robi app_main() raz przed
|
||||||
|
// wszystkimi initami. Tutaj już nie tworzymy magistrali.
|
||||||
bmp280_params_t params;
|
bmp280_params_t params;
|
||||||
bmp280_init_default_params(¶ms);
|
bmp280_init_default_params(¶ms);
|
||||||
memset(dev, 0, sizeof(bmp280_t));
|
memset(dev, 0, sizeof(bmp280_t));
|
||||||
|
|
||||||
ESP_ERROR_CHECK(bmp280_init_desc(dev, BMP280_I2C_ADDRESS_0, 0, I2C_MASTER_SDA, I2C_MASTER_SCL));
|
ESP_ERROR_CHECK(bmp280_init_desc(dev, BMP280_I2C_ADDRESS_0, 0, I2C_MASTER_SDA, I2C_MASTER_SCL));
|
||||||
|
// bmp280_init_desc() ustawia 1MHz - za szybko na pająka + dzieloną
|
||||||
|
// magistralę z OLED. Zjeżdżamy do 100kHz przed pierwszym użyciem
|
||||||
|
// (device jest dopinane do busa leniwie przy pierwszej transakcji).
|
||||||
|
dev->i2c_dev.cfg.master.clk_speed = YGG_I2C_FREQ_HZ;
|
||||||
ESP_ERROR_CHECK(bmp280_init(dev, ¶ms));
|
ESP_ERROR_CHECK(bmp280_init(dev, ¶ms));
|
||||||
|
|
||||||
bool bme280p = dev->id == BME280_CHIP_ID;
|
bool bme280p = dev->id == BME280_CHIP_ID;
|
||||||
printf("BMP280: found %s\n", bme280p ? "BME280" : "BMP280");
|
printf("BMP280: found %s\n", bme280p ? "BME280" : "BMP280");
|
||||||
}
|
}
|
||||||
|
|
||||||
void ygg_bmp280_read_sensor(bmp280_t *dev) {
|
esp_err_t ygg_bmp280_read_sensor(bmp280_t *dev) {
|
||||||
float pressure, temperature, humidity;
|
float pressure = 0, temperature = 0, humidity = 0;
|
||||||
bool bme280p = dev->id == BME280_CHIP_ID;
|
bool bme280p = dev->id == BME280_CHIP_ID;
|
||||||
|
|
||||||
if (bmp280_read_float(dev, &temperature, &pressure, &humidity) != ESP_OK)
|
// Cały odczyt pod wspólnym mutexem I2C - OLED (esp_lcd) używa
|
||||||
printf("Temperature/pressure reading failed\n");
|
// tego samego busa bez wiedzy o mutexach i2cdev.
|
||||||
|
// Bez tego flush LVGL potrafi wejść w środek odczytu -> timeout.
|
||||||
|
if (!ygg_i2c_lock_take(pdMS_TO_TICKS(1000))) {
|
||||||
|
ESP_LOGW(TAG, "I2C busy, skipping readout");
|
||||||
|
return ESP_ERR_TIMEOUT;
|
||||||
|
}
|
||||||
|
esp_err_t err = bmp280_read_float(dev, &temperature, &pressure, &humidity);
|
||||||
|
ygg_i2c_lock_give();
|
||||||
|
|
||||||
|
if (err != ESP_OK) {
|
||||||
|
ESP_LOGW(TAG, "Temperature/pressure reading failed: %s - keeping last good readout",
|
||||||
|
esp_err_to_name(err));
|
||||||
|
return err;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Aktualizuj globalny stan TYLKO przy sukcesie.
|
||||||
CURR_READOUT.temperature = temperature;
|
CURR_READOUT.temperature = temperature;
|
||||||
CURR_READOUT.pressure = pressure;
|
CURR_READOUT.pressure = pressure;
|
||||||
CURR_READOUT.humidity = humidity;
|
CURR_READOUT.humidity = humidity;
|
||||||
@@ -32,4 +55,5 @@ void ygg_bmp280_read_sensor(bmp280_t *dev) {
|
|||||||
printf(", Humidity: %.2f\n", humidity);
|
printf(", Humidity: %.2f\n", humidity);
|
||||||
else
|
else
|
||||||
printf("\n");
|
printf("\n");
|
||||||
|
return ESP_OK;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,11 +2,13 @@
|
|||||||
#define YGG_BMP280_H
|
#define YGG_BMP280_H
|
||||||
|
|
||||||
#include <bmp280.h>
|
#include <bmp280.h>
|
||||||
|
#include <esp_err.h>
|
||||||
|
|
||||||
#define I2C_MASTER_SDA 19
|
#define I2C_MASTER_SDA 19
|
||||||
#define I2C_MASTER_SCL 23
|
#define I2C_MASTER_SCL 23
|
||||||
|
#define YGG_I2C_FREQ_HZ 100000
|
||||||
|
|
||||||
void ygg_bmp280_init(bmp280_t *dev);
|
void ygg_bmp280_init(bmp280_t *dev);
|
||||||
void ygg_bmp280_read_sensor(bmp280_t *dev);
|
esp_err_t ygg_bmp280_read_sensor(bmp280_t *dev);
|
||||||
|
|
||||||
#endif
|
#endif
|
||||||
|
|||||||
@@ -0,0 +1,40 @@
|
|||||||
|
#include "ygg_lock.h"
|
||||||
|
#include <freertos/semphr.h>
|
||||||
|
|
||||||
|
static SemaphoreHandle_t s_i2c_mutex = NULL;
|
||||||
|
static SemaphoreHandle_t s_lvgl_mutex = NULL;
|
||||||
|
|
||||||
|
void ygg_lock_init(void) {
|
||||||
|
if (s_i2c_mutex == NULL) {
|
||||||
|
s_i2c_mutex = xSemaphoreCreateMutex();
|
||||||
|
}
|
||||||
|
if (s_lvgl_mutex == NULL) {
|
||||||
|
s_lvgl_mutex = xSemaphoreCreateMutex();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bool ygg_i2c_lock_take(TickType_t timeout) {
|
||||||
|
if (s_i2c_mutex == NULL) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return xSemaphoreTake(s_i2c_mutex, timeout) == pdTRUE;
|
||||||
|
}
|
||||||
|
|
||||||
|
void ygg_i2c_lock_give(void) {
|
||||||
|
if (s_i2c_mutex != NULL) {
|
||||||
|
xSemaphoreGive(s_i2c_mutex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bool ygg_lvgl_lock_take(TickType_t timeout) {
|
||||||
|
if (s_lvgl_mutex == NULL) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return xSemaphoreTake(s_lvgl_mutex, timeout) == pdTRUE;
|
||||||
|
}
|
||||||
|
|
||||||
|
void ygg_lvgl_lock_give(void) {
|
||||||
|
if (s_lvgl_mutex != NULL) {
|
||||||
|
xSemaphoreGive(s_lvgl_mutex);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
#ifndef YGG_LOCK_H
|
||||||
|
#define YGG_LOCK_H
|
||||||
|
|
||||||
|
#include <freertos/FreeRTOS.h>
|
||||||
|
#include <stdbool.h>
|
||||||
|
|
||||||
|
void ygg_lock_init(void);
|
||||||
|
|
||||||
|
bool ygg_i2c_lock_take(TickType_t timeout);
|
||||||
|
void ygg_i2c_lock_give(void);
|
||||||
|
|
||||||
|
bool ygg_lvgl_lock_take(TickType_t timeout);
|
||||||
|
void ygg_lvgl_lock_give(void);
|
||||||
|
|
||||||
|
#endif
|
||||||
@@ -6,14 +6,19 @@
|
|||||||
#include "esp_lcd_panel_ops.h"
|
#include "esp_lcd_panel_ops.h"
|
||||||
#include "esp_timer.h"
|
#include "esp_timer.h"
|
||||||
#include "esp_lcd_panel_sh1106.h"
|
#include "esp_lcd_panel_sh1106.h"
|
||||||
|
#include "esp_log.h"
|
||||||
|
#include "ygg_bmp280.h"
|
||||||
|
#include "ygg_lock.h"
|
||||||
|
|
||||||
#define I2C_HOST I2C_NUM_0
|
#define I2C_HOST I2C_NUM_0
|
||||||
#define EXAMPLE_I2C_MASTER_FREQ_HZ 400000
|
#define EXAMPLE_I2C_MASTER_FREQ_HZ YGG_I2C_FREQ_HZ
|
||||||
#define EXAMPLE_LCD_V_RES 64
|
#define EXAMPLE_LCD_V_RES 64
|
||||||
#define EXAMPLE_LCD_H_RES 128
|
#define EXAMPLE_LCD_H_RES 128
|
||||||
#define EXAMPLE_LVGL_PALETTE_SIZE 8
|
#define EXAMPLE_LVGL_PALETTE_SIZE 8
|
||||||
#define SH1106_I2C_SDA_GPIO 19
|
#define SH1106_I2C_SDA_GPIO I2C_MASTER_SDA
|
||||||
#define SH1106_I2C_SCL_GPIO 23
|
#define SH1106_I2C_SCL_GPIO I2C_MASTER_SCL
|
||||||
|
|
||||||
|
static const char *TAG = "ygg_sh1106";
|
||||||
|
|
||||||
static uint8_t oled_buffer[EXAMPLE_LCD_H_RES * EXAMPLE_LCD_V_RES / 8];
|
static uint8_t oled_buffer[EXAMPLE_LCD_H_RES * EXAMPLE_LCD_V_RES / 8];
|
||||||
|
|
||||||
@@ -51,8 +56,21 @@ static void example_lvgl_flush_cb(lv_display_t *disp, const lv_area_t *area, uin
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// pass the draw buffer to the driver
|
// pass the draw buffer to the driver.
|
||||||
esp_lcd_panel_draw_bitmap(panel_handle, x1, y1, x2 + 1, y2 + 1, oled_buffer);
|
// Cały transfer pod wspólnym mutexem I2C - BMP280 (i2cdev) używa
|
||||||
|
// tego samego busa. Bez tego odczyt sensora potrafi wejść w środek
|
||||||
|
// rysowania -> I2C hardware timeout po obu stronach.
|
||||||
|
// UWAGA: nie bierzemy tu mutexa LVGL - flush jest wołany z lvgl_task
|
||||||
|
// już pod mutexem LVGL (z lv_timer_handler). Branie go tu = deadlock.
|
||||||
|
if (ygg_i2c_lock_take(pdMS_TO_TICKS(1000))) {
|
||||||
|
esp_err_t err = esp_lcd_panel_draw_bitmap(panel_handle, x1, y1, x2 + 1, y2 + 1, oled_buffer);
|
||||||
|
ygg_i2c_lock_give();
|
||||||
|
if (err != ESP_OK) {
|
||||||
|
ESP_LOGW(TAG, "draw_bitmap failed: %s", esp_err_to_name(err));
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
ESP_LOGW(TAG, "I2C busy, skipping flush");
|
||||||
|
}
|
||||||
lv_display_flush_ready(disp);
|
lv_display_flush_ready(disp);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -71,7 +89,8 @@ void ygg_init_tick_cb() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
lv_display_t* ygg_sh1106_init() {
|
lv_display_t* ygg_sh1106_init() {
|
||||||
ESP_ERROR_CHECK(i2cdev_init());
|
// UWAGA: i2cdev_init() + ygg_lock_init() robi app_main() raz przed
|
||||||
|
// wszystkimi initami. Tutaj już nie tworzymy magistrali.
|
||||||
|
|
||||||
static i2c_dev_t oled_i2c_dev;
|
static i2c_dev_t oled_i2c_dev;
|
||||||
memset(&oled_i2c_dev, 0, sizeof(oled_i2c_dev));
|
memset(&oled_i2c_dev, 0, sizeof(oled_i2c_dev));
|
||||||
@@ -102,6 +121,9 @@ lv_display_t* ygg_sh1106_init() {
|
|||||||
// Create the i2c io handle
|
// Create the i2c io handle
|
||||||
esp_lcd_panel_io_handle_t io_handle = NULL;
|
esp_lcd_panel_io_handle_t io_handle = NULL;
|
||||||
esp_lcd_panel_io_i2c_config_t io_config = ESP_SH1106_DEFAULT_IO_CONFIG;
|
esp_lcd_panel_io_i2c_config_t io_config = ESP_SH1106_DEFAULT_IO_CONFIG;
|
||||||
|
// Zejście z domyślnych 400kHz na 100kHz - wspólna magistrala z BMP280,
|
||||||
|
// pająk + słabe pull-upy. 400kHz/1MHz = timeouty.
|
||||||
|
io_config.scl_speed_hz = EXAMPLE_I2C_MASTER_FREQ_HZ;
|
||||||
ESP_ERROR_CHECK(esp_lcd_new_panel_io_i2c(i2c_bus_handle, &io_config, &io_handle));
|
ESP_ERROR_CHECK(esp_lcd_new_panel_io_i2c(i2c_bus_handle, &io_config, &io_handle));
|
||||||
|
|
||||||
|
|
||||||
@@ -149,9 +171,16 @@ lv_display_t* ygg_sh1106_init() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void ygg_set_display_label(lv_display_t *display, lv_obj_t *label, const char *str) {
|
void ygg_set_display_label(lv_display_t *display, lv_obj_t *label, const char *str) {
|
||||||
|
// LVGL nie jest thread-safe. Ta funkcja jest wołana z sensor_task,
|
||||||
|
// a lv_timer_handler chodzi w lvgl_task - wszystko pod jednym mutexem.
|
||||||
|
if (!ygg_lvgl_lock_take(pdMS_TO_TICKS(500))) {
|
||||||
|
ESP_LOGW(TAG, "LVGL busy, skipping label update");
|
||||||
|
return;
|
||||||
|
}
|
||||||
lv_label_set_text(label, str);
|
lv_label_set_text(label, str);
|
||||||
/* Size of the screen (if you use rotation 90 or 270, please use lv_display_get_vertical_resolution) */
|
/* Size of the screen (if you use rotation 90 or 270, please use lv_display_get_vertical_resolution) */
|
||||||
lv_obj_set_width(label, lv_display_get_horizontal_resolution(display));
|
lv_obj_set_width(label, lv_display_get_horizontal_resolution(display));
|
||||||
lv_obj_set_style_text_align(label, LV_TEXT_ALIGN_CENTER, 0);
|
lv_obj_set_style_text_align(label, LV_TEXT_ALIGN_CENTER, 0);
|
||||||
lv_obj_align(label, LV_ALIGN_TOP_MID, 0, 0);
|
lv_obj_align(label, LV_ALIGN_TOP_MID, 0, 0);
|
||||||
|
ygg_lvgl_lock_give();
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user