i2c fixes + server improvements

This commit is contained in:
2026-10-05 19:28:37 +02:00
parent 7094d05030
commit 02f3df8c41
9 changed files with 254 additions and 52 deletions
+48 -18
View File
@@ -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) ---
+46 -8
View File
@@ -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 -1
View File
@@ -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")
+26 -2
View File
@@ -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);
+29 -5
View File
@@ -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(&params); bmp280_init_default_params(&params);
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, &params)); ESP_ERROR_CHECK(bmp280_init(dev, &params));
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;
} }
+3 -1
View File
@@ -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
+40
View File
@@ -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);
}
}
+15
View File
@@ -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
+35 -6
View File
@@ -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();
} }