first commit

This commit is contained in:
stuce-bot 2025-06-30 20:47:33 +02:00
commit 5893b00dd2
1669 changed files with 1982740 additions and 0 deletions

View file

@ -0,0 +1,9 @@
/*
* SPDX-FileCopyrightText: 2024 M5Stack Technology CO LTD
*
* SPDX-License-Identifier: MIT
*/
/*
Example of using only unit component without UnitUnified manager
*/
#include "main/ComponentOnly.cpp"

View file

@ -0,0 +1,25 @@
# M5UnitUnified example
## ComponentOnly
### Overview
This is an example of using only the component of unit without the management mechanism of the unit.
The source uses UnitCO2, so if you want to use other units, you will need to modify it accordingly.
### ArduinoIDE
Install each unit's library with the library manager.
### PlatformIO
For convenience of unit testing, the libraries for each unit are registered in lib\_deps.
Use the env of the applicable Core device.
---
### 概要
ユニットの管理機構を使わず、各ユニットのコンポーネントのみで使用する例です。
ソースでは UnitCO2 を使用していますので、他のユニットを使用する場合は適宜変更が必要です。
### ArduinoIDE
各ユニットのライブラリをライブラリマネージャでインストールしてください。
### PlatformIO
ユニットテストの都合上、各ユニット毎のライブラリは lib\_deps に登録されています
該当する Core デバイスの env を使用しください。

View file

@ -0,0 +1,41 @@
/*
* SPDX-FileCopyrightText: 2024 M5Stack Technology CO LTD
*
* SPDX-License-Identifier: MIT
*/
/*
Example of using only unit component without UnitUnified manager
If you use other units, change include files(*1), instances(*2), and get values(*3)
*/
#include <M5Unified.h>
#include <M5UnitUnified.h>
#include <M5UnitUnifiedENV.h> // *1 Include the header of the unit to be used
m5::unit::UnitCO2 unit; // *2 Instance of the unit
void setup()
{
M5.begin();
auto pin_num_sda = M5.getPin(m5::pin_name_t::port_a_sda);
auto pin_num_scl = M5.getPin(m5::pin_name_t::port_a_scl);
M5_LOGI("getPin: SDA:%u SCL:%u", pin_num_sda, pin_num_scl);
Wire.begin(pin_num_sda, pin_num_scl, 400 * 1000U);
M5.Display.clear(TFT_DARKGREEN);
if (!unit.assign(Wire) // Assign Wire
|| unit.begin()) { // Begin unit
M5_LOGE("Failed to assign/begin");
M5.Display.clear(TFT_RED);
}
}
void loop()
{
M5.update();
unit.update(); // Explicitly call unit.update() yourself
if (unit.updated()) {
// *3 Obtaining unit-specific measurements
M5_LOGI("CO2:%u Temp:%f Hum:%f", unit.co2(), unit.temperature(), unit.humidity());
}
}

View file

@ -0,0 +1,25 @@
# M5UnitUnified example
## SelfUpdate
### Overview
Here is an example of how to update a unit independently in a separate task.
The source uses UnitCO2, so if you want to use other units, you will need to modify it accordingly.
### ArduinoIDE
Install each unit's library with the library manager.
### PlatformIO
For convenience of unit testing, the libraries for each unit are registered in lib\_deps.
Use the env of the applicable Core device.
---
### 概要
ユニットの更新を別タスクで独自に行う場合の例です。
ソースでは UnitCO2 を使用していますので、他のユニットを使用する場合は適宜変更が必要です。
### ArduinoIDE
各ユニットのライブラリをライブラリマネージャでインストールしてください。
### PlatformIO
ユニットテストの都合上、各ユニット毎のライブラリは lib\_deps に登録されています
該当する Core デバイスの env を使用しください。

View file

@ -0,0 +1,9 @@
/*
* SPDX-FileCopyrightText: 2024 M5Stack Technology CO LTD
*
* SPDX-License-Identifier: MIT
*/
/*
To update the unit yourself usage example
*/
#include "main/SelfUpdate.cpp"

View file

@ -0,0 +1,63 @@
/*
* SPDX-FileCopyrightText: 2024 M5Stack Technology CO LTD
*
* SPDX-License-Identifier: MIT
*/
/*
To update the unit yourself usage example (UnitCO2)
If you use other units, change include files(*1), instances(*2), and get values(*3)
*/
#include <M5Unified.h>
#include <M5UnitUnified.h>
#include <M5UnitUnifiedENV.h> // *1 Include the header of the unit to be used
m5::unit::UnitUnified Units;
m5::unit::UnitCO2 unit; // *2 Instance of the unit
void update_task(void*)
{
for (;;) {
// If exclusive control is required for access to Wire, insert appropriate controls
unit.update(); // Explicitly call unit.update() yourself
if (unit.updated()) {
// *3 Obtaining unit-specific measurements
M5_LOGI("CO2:%u Temp:%f Hum:%f", unit.co2(), unit.temperature(), unit.humidity());
}
m5::utility::delay(1);
}
}
void setup()
{
M5.begin();
auto pin_num_sda = M5.getPin(m5::pin_name_t::port_a_sda);
auto pin_num_scl = M5.getPin(m5::pin_name_t::port_a_scl);
M5_LOGI("getPin: SDA:%u SCL:%u", pin_num_sda, pin_num_scl);
Wire.begin(pin_num_sda, pin_num_scl, 400 * 1000U);
auto ccfg = unit.component_config();
ccfg.self_update = true;
unit.component_config(ccfg);
M5.Display.clear(TFT_DARKGREEN);
if (!Units.add(unit, Wire) // Add unit to UnitUnified manager
|| !Units.begin()) { // Begin each unit
M5_LOGE("Failed to add/begin");
M5.Display.clear(TFT_RED);
return;
}
xTaskCreateUniversal(update_task, "update_task", 8192, nullptr, 1, nullptr,
#if defined(CONFIG_IDF_TARGET_ESP32C6)
PRO_CPU_NUM);
#else
APP_CPU_NUM);
#endif
}
void loop()
{
M5.update();
Units.update(); // unit.update() is not called within this function
M5_LOGI("loop");
m5::utility::delay(1000);
}

View file

@ -0,0 +1,24 @@
# M5UnitUnified example
## Simple
### Overview
Here is an example of the most standard usage.
The source uses UnitCO2, so if you want to use other units, you will need to modify it accordingly.
### ArduinoIDE
Install each unit's library with the library manager.
### PlatformIO
For convenience of unit testing, the libraries for each unit are registered in lib\_deps.
Use the env of the applicable Core device.
---
### 概要
最もスタンダードな使い方の例です。
ソースでは UnitCO2 を使用していますので、他のユニットを使用する場合は適宜変更が必要です。
### ArduinoIDE
各ユニットのライブラリをライブラリマネージャでインストールしてください。
### PlatformIO
ユニットテストの都合上、各ユニット毎のライブラリは lib\_deps に登録されています
該当する Core デバイスの env を使用しください。

View file

@ -0,0 +1,9 @@
/*
* SPDX-FileCopyrightText: 2024 M5Stack Technology CO LTD
*
* SPDX-License-Identifier: MIT
*/
/*
Simple usage example
*/
#include "main/Simple.cpp"

View file

@ -0,0 +1,42 @@
/*
* SPDX-FileCopyrightText: 2024 M5Stack Technology CO LTD
*
* SPDX-License-Identifier: MIT
*/
/*
Simple usage example (UnitCO2)
If you use other units, change include files(*1), instances(*2), and get values(*3)
*/
#include <M5Unified.h>
#include <M5UnitUnified.h>
#include <M5UnitUnifiedENV.h> // *1 Include the header of the unit to be used
m5::unit::UnitUnified Units;
m5::unit::UnitCO2 unit; // *2 Instance of the unit
void setup()
{
M5.begin();
auto pin_num_sda = M5.getPin(m5::pin_name_t::port_a_sda);
auto pin_num_scl = M5.getPin(m5::pin_name_t::port_a_scl);
M5_LOGI("getPin: SDA:%u SCL:%u", pin_num_sda, pin_num_scl);
Wire.begin(pin_num_sda, pin_num_scl, 400 * 1000U);
M5.Display.clear(TFT_DARKGREEN);
if (!Units.add(unit, Wire) // Add unit to UnitUnified manager
|| !Units.begin()) { // Begin each unit
M5_LOGE("Failed to add/begin");
M5.Display.clear(TFT_RED);
}
}
void loop()
{
M5.update();
Units.update();
if (unit.updated()) {
// *3 Obtaining unit-specific measurements
M5_LOGI("CO2:%u Temp:%f Hum:%f", unit.co2(), unit.temperature(), unit.humidity());
}
}

View file

@ -0,0 +1,9 @@
/*
* SPDX-FileCopyrightText: 2024 M5Stack Technology CO LTD
*
* SPDX-License-Identifier: MIT
*/
/*
For ArduinoIDE
*/
#include "main/MultipleUnits.cpp"

View file

@ -0,0 +1,33 @@
# M5UnitUnified example
## MultipleUnits
### Overview
This is a demo program that was shown at MFT2024 and M5JPTour2024.
It displays information on 4 units (Vmeter, TVOC, ENVIII, HEART) via UnitPaHub2.
The address of UnitPaHub2 must be 0x71 (because it conflicts with ENVIII).
See also https://docs.m5stack.com/en/unit/pahub2
NOTICE: Use Core devices capable of displaying 320x240 pixels
### ArduinoIDE
Install each unit's library with the library manager.
### PlatformIO
For convenience of unit testing, the libraries for each unit are registered in lib\_deps.
Use the env of the applicable Core device.
---
### 概要
MFT2024, M5JPTour2024 にて公開されていたデモプログラムです。
UnitPaHub2 を介して 4つのユニット (Vmeter, TVOC, ENVIII, HEART) の情報を表示します。
UnitPaHub2 のアドレスを 0x71 にする必要があります(ENVIII と衝突するため)
こちらを参照 https://docs.m5stack.com/en/unit/pahub2
注意: 320x240 ピクセルの表示ができる Core デバイスを使用してください。
### ArduinoIDE
各ユニットのライブラリをライブラリマネージャでインストールしてください。
### PlatformIO
ユニットテストの都合上、各ユニット毎のライブラリは lib\_deps に登録されています
該当する Core デバイスの env を使用しください。

View file

@ -0,0 +1,432 @@
/*
* SPDX-FileCopyrightText: 2024 M5Stack Technology CO LTD
*
* SPDX-License-Identifier: MIT
*/
/*
Demonstration of using M5UnitUnified with multiple units
Required Devices:
- Any Core with LCD
- UnitPaHub2
- UnitVmeter : 0
- UnitTVOC : 1
- UnitENV3 : 2
- UnitHEART : 3
*/
#include <M5Unified.h>
#include <M5UnitUnified.h>
#include <M5UnitUnifiedHUB.h>
#include <M5UnitUnifiedENV.h> // ENVIII,TVOC
#include <M5UnitUnifiedMETER.h> // Vmeter
#include <M5UnitUnifiedHEART.h> // HEART
#include <Wire.h>
#if __has_include(<esp_idf_version.h>)
#include <esp_idf_version.h>
#else // esp_idf_version.h has been introduced in Arduino 1.0.5 (ESP-IDF3.3)
#define ESP_IDF_VERSION_VAL(major, minor, patch) ((major << 16) | (minor << 8) | (patch))
#define ESP_IDF_VERSION ESP_IDF_VERSION_VAL(3, 2, 0)
#endif
#include "../src/ui/ui_UnitVmeter.hpp"
#include "../src/ui/ui_UnitTVOC.hpp"
#include "../src/ui/ui_UnitENV3.hpp"
#include "../src/ui/ui_UnitHEART.hpp"
using namespace m5::unit;
namespace {
LGFX_Sprite strips[2];
constexpr uint32_t SPLIT_NUM{4};
int32_t strip_height{};
auto& lcd = M5.Display;
UnitUnified Units;
UnitPaHub2 unitPaHub{0x71}; // NEED changed register to 0x71. see also https://docs.m5stack.com/en/unit/pahub2
UnitVmeter unitVmeter; // channel 0
UnitTVOC unitTVOC; // channel 1
UnitENV3 unitENV3; // channel 2
UnitHEART unitHeart; // channel 3
auto& unitSHT30 = unitENV3.sht30; // alias
auto& unitQMP6988 = unitENV3.qmp6988; // alias
UnitVmeterSmallUI vmeterSmallUI(&lcd);
UnitTVOCSmallUI tvocSmallUI(&lcd);
UnitHEARTSmallUI heartSmallUI(&lcd);
UnitENV3SmallUI env3SmallUI(&lcd);
volatile SemaphoreHandle_t _updateLock{};
constexpr TickType_t ui_take_wait{0};
void prepare()
{
// Each unit settings
{
auto ccfg = unitVmeter.component_config();
ccfg.self_update = true; // Don't update in UnitUnified::update, update explicitly myself.
ccfg.stored_size = 64; // Number of elements in the circular buffer that the instance has.
unitVmeter.component_config(ccfg);
// Setup fro begin
auto cfg = unitVmeter.config();
// 12 ms is used by TVOC, so frequency is reduced
cfg.rate = m5::unit::ads111x::Sampling::Rate64; // 64mps
unitVmeter.config(cfg);
}
{
// Setup fro begin
auto cfg = unitTVOC.config();
cfg.interval = 1000 / 10; // 10 mps
unitTVOC.config(cfg);
auto ccfg = unitTVOC.component_config();
ccfg.self_update = true; // Don't update in UnitUnified::update, update explicitly myself.
ccfg.stored_size = 1000 / cfg.interval; // Number of elements in the circular buffer that the instance has
unitTVOC.component_config(ccfg);
}
{
auto ccfg = unitSHT30.component_config();
ccfg.self_update = true;
ccfg.stored_size = 10; // Number of elements in the circular buffer that the instance has
unitSHT30.component_config(ccfg);
// Setup fro begin
auto cfg = unitSHT30.config();
cfg.mps = m5::unit::sht30::MPS::Ten; // 10 mps
unitSHT30.config(cfg);
}
{
auto ccfg = unitQMP6988.component_config();
ccfg.self_update = true;
ccfg.stored_size = 16; // Number of elements in the circular buffer that the instance has
unitQMP6988.component_config(ccfg);
// Setup fro begin
auto cfg = unitQMP6988.config();
cfg.standby =
m5::unit::qmp6988::Standby::Time50ms; // about 16 mps (Calculated from other parameters and this value
unitQMP6988.config(cfg);
}
{
auto ccfg = unitHeart.component_config();
ccfg.self_update = true; // Don't update in UnitUnified::update, update explicitly myself.
ccfg.stored_size = 160; // Number of elements in the circular buffer that the instance has
unitHeart.component_config(ccfg);
}
// UI
heartSmallUI.construct();
tvocSmallUI.construct();
vmeterSmallUI.construct();
env3SmallUI.construct();
heartSmallUI.monitor().setSamplingRate(m5::unit::max30100::getSamplingRate(unitHeart.config().sampling_rate));
}
// task for Vmeter
void update_vmeter(void*)
{
static uint32_t fcnt{}, mps{}, mcnt{};
static unsigned long start_at{};
for (;;) {
// Exclusive control of TwoWire access and unit updates
xSemaphoreTake(_updateLock, portMAX_DELAY);
unitVmeter.update();
xSemaphoreGive(_updateLock);
// If measurement data is available, acquire it and pass it to the UI.
// If the UI is locked, skip and continue.
if (!unitVmeter.empty()) {
if (vmeterSmallUI.lock(ui_take_wait)) {
mcnt += unitVmeter.available();
while (unitVmeter.available()) {
vmeterSmallUI.push_back(unitVmeter.voltage()); // Gets the oldest data
unitVmeter.discard(); // Discard oldest one
}
vmeterSmallUI.unlock();
}
}
// std::this_thread::yield();
m5::utility::delay(1);
++fcnt;
auto now = m5::utility::millis();
if (now >= start_at + 1000) {
mps = fcnt;
M5_LOGD("Vmeter:%u (%u)", mps, mcnt);
fcnt = mcnt = 0;
start_at = now;
}
}
}
// Task for TVOC
void update_tvoc(void*)
{
static uint32_t fcnt{}, mps{}, mcnt{};
static unsigned long start_at{};
// Waiting for SGP30 to start periodic measurement (15sec)
for (;;) {
if (unitTVOC.canMeasurePeriodic()) {
break;
}
// Exclusive control of TwoWire access and unit updates
xSemaphoreTake(_updateLock, portMAX_DELAY);
unitTVOC.update();
xSemaphoreGive(_updateLock);
m5::utility::delay(1000);
}
for (;;) {
// Exclusive control of TwoWire access and unit updates
xSemaphoreTake(_updateLock, portMAX_DELAY);
// TVOC measuement needs 12ms for read...
unitTVOC.update();
xSemaphoreGive(_updateLock);
// If measurement data is available, acquire it and pass it to the UI.
// If the UI is locked, skip and continue.
if (!unitTVOC.empty()) {
if (tvocSmallUI.lock(ui_take_wait)) {
mcnt += unitTVOC.available();
while (unitTVOC.available()) {
tvocSmallUI.push_back(unitTVOC.co2eq(), unitTVOC.tvoc()); // Gets the oldest data
unitTVOC.discard(); // Discard oldest one
}
tvocSmallUI.unlock();
}
}
// std::this_thread::yield();
m5::utility::delay(1);
++fcnt;
auto now = m5::utility::millis();
if (now >= start_at + 1000) {
mps = fcnt;
M5_LOGD("TVOC:%u (%u)", mps, mcnt);
fcnt = mcnt = 0;
start_at = now;
}
}
}
// Task for SHT30(ENV3)
void update_sht30(void*)
{
static uint32_t fcnt{}, mps{}, mcnt{};
static unsigned long start_at{};
for (;;) {
// Exclusive control of TwoWire access and unit updates
xSemaphoreTake(_updateLock, portMAX_DELAY);
unitSHT30.update();
xSemaphoreGive(_updateLock);
// If measurement data is available, acquire it and pass it to the UI.
// If the UI is locked, skip and continue.
if (!unitSHT30.empty()) {
if (env3SmallUI.lock(ui_take_wait)) {
mcnt += unitSHT30.available();
auto latest = unitSHT30.latest();
env3SmallUI.sht30_push_back(latest.temperature(), latest.humidity()); // Gets the latest data
unitSHT30.flush(); // Discard all data
env3SmallUI.unlock();
}
}
// std::this_thread::yield();
m5::utility::delay(1);
++fcnt;
auto now = m5::utility::millis();
if (now >= start_at + 1000) {
mps = fcnt;
M5_LOGD("SHT30:%u (%u)", mps, mcnt);
fcnt = mcnt = 0;
start_at = now;
}
}
}
// Task for QMP6988(ENV3)
void update_qmp6988(void*)
{
static uint32_t fcnt{}, mps{}, mcnt{};
static unsigned long start_at{};
for (;;) {
// Exclusive control of TwoWire access and unit updates
xSemaphoreTake(_updateLock, portMAX_DELAY);
unitQMP6988.update();
xSemaphoreGive(_updateLock);
// If measurement data is available, acquire it and pass it to the UI.
// If the UI is locked, skip and continue.
if (!unitQMP6988.empty()) {
if (env3SmallUI.lock(ui_take_wait)) {
mcnt += unitQMP6988.available();
auto latest = unitQMP6988.latest();
env3SmallUI.qmp6988_push_back(latest.temperature(), latest.pressure()); // Gets the latest data
unitQMP6988.flush(); // Discard all data
env3SmallUI.unlock();
}
}
// std::this_thread::yield();
m5::utility::delay(1);
++fcnt;
auto now = m5::utility::millis();
if (now >= start_at + 1000) {
mps = fcnt;
M5_LOGD("QMP6988:%u (%u)", mps, mcnt);
fcnt = mcnt = 0;
start_at = now;
}
}
}
// Task for HEART
void update_heart(void*)
{
static uint32_t fcnt{}, mps{}, mcnt{};
static unsigned long start_at{};
for (;;) {
// Exclusive control of TwoWire access and unit updates
xSemaphoreTake(_updateLock, portMAX_DELAY);
unitHeart.update();
xSemaphoreGive(_updateLock);
// If measurement data is available, acquire it and pass it to the UI.
// If the UI is locked, skip and continue.
if (!unitHeart.empty()) {
if (heartSmallUI.lock(ui_take_wait)) {
mcnt += unitHeart.available();
while (unitHeart.available()) {
heartSmallUI.push_back(unitHeart.ir(), unitHeart.red());
unitHeart.discard();
}
heartSmallUI.unlock();
}
}
m5::utility::delay(1);
// std::this_thread::yield();
++fcnt;
auto now = m5::utility::millis();
if (now >= start_at + 1000) {
mps = fcnt;
M5_LOGD("Heart:%u (%u)", mps, mcnt);
fcnt = mcnt = 0;
start_at = now;
}
}
}
void drawUI(LovyanGFX& dst, const uint32_t x, const uint32_t yoffset)
{
vmeterSmallUI.push(&dst, 0, 0 + yoffset);
tvocSmallUI.push(&dst, lcd.width() >> 1, 0 + yoffset);
env3SmallUI.push(&dst, 0, (lcd.height() >> 1) + yoffset);
heartSmallUI.push(&dst, lcd.width() >> 1, (lcd.height() >> 1) + yoffset);
}
} // namespace
void setup()
{
M5.begin();
lcd.startWrite();
lcd.clear(TFT_DARKGRAY);
//
strip_height = lcd.height() / SPLIT_NUM;
uint32_t cnt{};
for (auto&& spr : strips) {
spr.setPsram(false);
spr.setColorDepth(lcd.getColorDepth());
cnt += spr.createSprite(lcd.width(), strip_height) ? 1 : 0;
}
assert(cnt == 2 && "Failed to create sprite");
prepare();
auto pin_num_sda = M5.getPin(m5::pin_name_t::port_a_sda);
auto pin_num_scl = M5.getPin(m5::pin_name_t::port_a_scl);
M5_LOGI("getPin: SDA:%u SCL:%u", pin_num_sda, pin_num_scl);
Wire.begin(pin_num_sda, pin_num_scl, 400000U);
if (!unitPaHub.add(unitVmeter, 0) /* Connect Vmeter to PaHub2 ch:0 */
|| !unitPaHub.add(unitTVOC, 1) /* Connect TVOC to PaHub2 ch:1 */
|| !unitPaHub.add(unitENV3, 2) /* Connect ENV3 to PaHub2 ch:2 */
|| !unitPaHub.add(unitHeart, 3) /* Connect HEART to PaHub2 ch:3 */
|| !Units.add(unitPaHub, Wire) /* Connect PaHub2 to Core */
|| !Units.begin() /* Begin UnitUnified */
) {
M5_LOGE("Failed to begin");
lcd.clear(TFT_RED);
while (true) {
m5::utility::delay(10000);
}
}
lcd.clear(TFT_DARKGREEN);
M5_LOGI("M5UnitUnified has been begun");
M5_LOGI("%s", Units.debugInfo().c_str());
M5_LOGI("CPP %ld", __cplusplus);
M5_LOGI("ESP-IDF Version %d.%d.%d", (ESP_IDF_VERSION >> 16) & 0xFF, (ESP_IDF_VERSION >> 8) & 0xFF,
ESP_IDF_VERSION & 0xFF);
M5_LOGI("BOARD:%X", M5.getBoard());
M5_LOGI("Heap: %u", esp_get_free_heap_size());
//
_updateLock = xSemaphoreCreateBinary();
xSemaphoreGive(_updateLock);
xTaskCreateUniversal(update_vmeter, "vmeter", 8192, nullptr, 2, nullptr, PRO_CPU_NUM);
xTaskCreateUniversal(update_tvoc, "tvoc", 8192, nullptr, 1, nullptr, PRO_CPU_NUM);
xTaskCreateUniversal(update_sht30, "sht30", 8192, nullptr, 1, nullptr, PRO_CPU_NUM);
xTaskCreateUniversal(update_qmp6988, "qmp6988", 8192, nullptr, 1, nullptr, PRO_CPU_NUM);
xTaskCreateUniversal(update_heart, "heart", 8192, nullptr, 1, nullptr, PRO_CPU_NUM);
}
void loop()
{
static uint32_t fpsCnt{}, fps{};
static unsigned long start_at{};
++fpsCnt;
auto now = m5::utility::millis();
if (now >= start_at + 1000) {
fps = fpsCnt;
M5_LOGD("FPS:%u", fps);
fpsCnt = 0;
start_at = now;
}
M5.update();
// All units do their own updates, so there is no need to call for a unit-wide update here.
// xSemaphoreTake(_updateLock, portMAX_DELAY);
// unitTVOC.update();
// xSemaphoreGive(_updateLock);
tvocSmallUI.update();
vmeterSmallUI.update();
heartSmallUI.update();
env3SmallUI.update();
static uint32_t current{};
int32_t offset{};
uint32_t cnt{SPLIT_NUM};
while (cnt--) {
auto& spr = strips[current];
spr.clear();
drawUI(spr, 0, offset);
spr.pushSprite(&lcd, 0, -offset);
current ^= 1;
offset -= strip_height;
}
}

View file

@ -0,0 +1,100 @@
/*
* SPDX-FileCopyrightText: 2024 M5Stack Technology CO LTD
*
* SPDX-License-Identifier: MIT
*/
/*!
@brief Configrate time
@file config_time.cpp
*/
#include "config_time.hpp"
#include <M5Unified.h>
#include <M5Utility.h>
#include <WiFi.h>
#include <esp_sntp.h>
#include <random>
#include <algorithm>
namespace {
// NTP server URI
constexpr char ntp0[] = "ntp.nict.jp";
constexpr char ntp1[] = "ntp.jst.mfeed.ad.jp";
constexpr char ntp2[] = "time.cloudflare.com";
const char* ntpURLTable[] = {ntp0, ntp1, ntp2};
constexpr char defaultPosixTZ[] = "JST-9"; // Asia/Tokyo
auto rng = std::default_random_engine{};
} // namespace
bool isEnabledRTC()
{
// Check RTC if exists
if (M5.Rtc.isEnabled()) {
auto dt = M5.Rtc.getDateTime(); // GMT
if (dt.date.year > 2016) {
M5_LOGV("RTC time already set. (GMT) %04d/%02d/%2d %02d:%02d:%02d", dt.date.year, dt.date.month,
dt.date.date, dt.time.hours, dt.time.minutes, dt.time.seconds);
return true;
}
M5_LOGW("RTC is not set to the correct time");
}
return false;
}
void setTimezone(const char* posix_tz)
{
setenv("TZ", posix_tz ? posix_tz : defaultPosixTZ, 1);
tzset();
}
bool configTime(const char* posix_tz, const char* ssid, const char* password)
{
M5_LOGI("Configrate time");
// WiFi connect
if (ssid && password) {
WiFi.begin(ssid, password);
} else {
// Connect to credential in Hardware. (ESP32 saves the last WiFi connection)
WiFi.begin();
}
int32_t retry{10};
while (WiFi.status() != WL_CONNECTED && --retry >= 0) {
M5_LOGI(".");
m5::utility::delay(1000);
}
if (WiFi.status() != WL_CONNECTED) {
M5_LOGE("Failed to connect WiFi");
return false;
}
std::shuffle(std::begin(ntpURLTable), std::end(ntpURLTable), rng);
configTzTime(posix_tz ? posix_tz : defaultPosixTZ, ntpURLTable[0], ntpURLTable[1], ntpURLTable[2]);
// Waiting for time synchronization
retry = 10;
sntp_sync_status_t st{};
while (((st = sntp_get_sync_status()) == SNTP_SYNC_STATUS_RESET) && --retry >= 0) {
M5_LOGI("Time synchronization in progress");
m5::utility::delay(1000);
}
WiFi.disconnect(true);
WiFi.mode(WIFI_OFF);
std::tm discard{};
if ((st != SNTP_SYNC_STATUS_COMPLETED) || !getLocalTime(&discard, 10 * 1000 /* timeout */)) {
M5_LOGE("Failed to sync time");
return false;
}
// Set RTC if exists
if (M5.Rtc.isEnabled()) {
time_t t = time(nullptr) + 1;
while (t > time(nullptr)) {
/* Nop */
}
M5.Rtc.setDateTime(std::gmtime(&t));
}
return true;
}

View file

@ -0,0 +1,17 @@
/*
* SPDX-FileCopyrightText: 2024 M5Stack Technology CO LTD
*
* SPDX-License-Identifier: MIT
*/
/*!
@brief Configrate time by NTP
@file config_time.cpp
*/
#ifndef CONFIG_TIME_HPP
#define CONFIG_TIME_HPP
bool isEnabledRTC();
void setTimezone(const char* posix_tz = nullptr);
bool configTime(const char* ssid = nullptr, const char* password = nullptr);
#endif

View file

@ -0,0 +1,128 @@
/*
* SPDX-FileCopyrightText: 2024 M5Stack Technology CO LTD
*
* SPDX-License-Identifier: MIT
*/
/*!
@file ui_bar_meter.cpp
@brief Bar meter
*/
#include "ui_bar_meter.hpp"
#include <M5Utility.h>
namespace {
constexpr float table0[] = {0.0f, 0.25f, 0.5f, 0.75f, 1.0f};
constexpr float table1[] = {0.125f, 0.125f * 3, 0.125f * 5, 0.125f * 7};
} // namespace
namespace m5 {
namespace ui {
void BarMeterH::render(LovyanGFX* dst, const int32_t x, const int32_t y, const int32_t val)
{
dst->setClipRect(x, y, width(), height());
const auto t = height() >> 3;
const auto gy = y + t * 6;
const auto gw = width() - 1;
const auto gh0 = t;
const auto gh1 = gh0 >> 1;
dst->fillRect(x, y, width(), height(), TFT_BLUE);
// gauge
dst->drawFastHLine(x, gy, width(), gaugeColor());
for (auto&& e : table0) {
dst->drawFastVLine(x + gw * e, gy - gh0, gh0, gaugeColor());
}
for (auto&& e : table1) {
dst->drawFastVLine(x + gw * e, gy - gh1, gh1, gaugeColor());
}
// needle
dst->drawFastVLine(x + gw * ratio(val), y, height(), needleColor());
dst->clearClipRect();
}
void BarMeterV::render(LovyanGFX* dst, const int32_t x, const int32_t y, const int32_t val)
{
dst->setClipRect(x, y, width(), height());
const auto t = width() >> 3;
const auto gx = x + t * 6;
const auto gh = height() - 1;
const auto gw0 = t;
const auto gw1 = gw0 >> 1;
// gauge
dst->drawFastVLine(gx, y, height(), gaugeColor());
for (auto&& e : table0) {
dst->drawFastHLine(gx - gw0, y + gh * e, gw0, gaugeColor());
}
for (auto&& e : table1) {
dst->drawFastHLine(gx - gw1, y + gh * e, gw1, gaugeColor());
}
// needle
dst->drawFastHLine(x, y + gh * (1.0f - ratio(val)), width(), needleColor());
dst->clearClipRect();
}
void ColorBarMeterH::render(LovyanGFX* dst, const int32_t x, const int32_t y, const int32_t val)
{
const auto w = width();
const auto t = height() >> 3;
const auto h = t << 2;
auto left = x;
auto top = y + height() / 2 - height() / 4;
auto gw = width() - 1;
dst->setClipRect(x, y, width(), height());
// gauge
if (!_crange.empty()) {
dst->fillRect(left, top, w, h, _crange.back().clr);
for (auto it = _crange.crbegin() + 1; it != _crange.crend(); ++it) {
int32_t ww = w * ratio(it->lesseq);
dst->fillRect(left, top, ww, h, it->clr);
}
} else {
dst->fillRect(left, top, w, h, backgroundColor());
}
dst->drawRect(left, top, w, h, gaugeColor());
// needle
dst->drawFastVLine(left + gw * ratio(val), y, height(), needleColor());
dst->clearClipRect();
}
void ColorBarMeterV::render(LovyanGFX* dst, const int32_t x, const int32_t y, const int32_t val)
{
const auto h = height();
const auto w = width() >> 1;
auto top = y;
auto left = x + width() / 2 - width() / 4;
auto gh = height() - 1;
dst->setClipRect(x, y, width(), height());
// gauge
if (!_crange.empty()) {
dst->fillRect(left, top, w, h, _crange.back().clr);
for (auto it = _crange.crbegin() + 1; it != _crange.crend(); ++it) {
int32_t hh = h * ratio(it->lesseq);
dst->fillRect(left, top + height() - hh, w, hh, it->clr);
}
} else {
dst->fillRect(left, top, w, h, backgroundColor());
}
dst->drawRect(left, top, w, h, gaugeColor());
// needle
dst->drawFastHLine(x, y + gh * (1.0f - ratio(val)), width(), needleColor());
dst->clearClipRect();
}
} // namespace ui
} // namespace m5

View file

@ -0,0 +1,109 @@
/*
* SPDX-FileCopyrightText: 2024 M5Stack Technology CO LTD
*
* SPDX-License-Identifier: MIT
*/
/*!
@file ui_scale_meter.hpp
@brief Bar meter
*/
#ifndef UI_PARTS_BAR_METER_HPP
#define UI_PARTS_BAR_METER_HPP
#include "ui_base.hpp"
#include <initializer_list>
namespace m5 {
namespace ui {
/*!
@class BarMeterH
@brief Horizontal bar meter
*/
class BarMeterH : public Base {
public:
BarMeterH(LovyanGFX* parent, const int32_t minimum, const int32_t maximum, const int32_t wid, const int32_t hgt)
: Base(parent, minimum, maximum, wid, hgt)
{
}
virtual ~BarMeterH()
{
}
protected:
virtual void render(LovyanGFX* dst, const int32_t x, const int32_t y, const int32_t val) override;
};
/*!
@class BarMeterH
@brief Vertical bar meter
*/
class BarMeterV : public Base {
public:
BarMeterV(LovyanGFX* parent, const int32_t minimum, const int32_t maximum, const int32_t wid, const int32_t hgt)
: Base(parent, minimum, maximum, wid, hgt)
{
}
virtual ~BarMeterV()
{
}
protected:
virtual void render(LovyanGFX* dst, const int32_t x, const int32_t y, const int32_t val) override;
};
struct ColorRange {
int32_t lesseq;
m5gfx::rgb565_t clr;
};
class ColorBarMeterH : public Base {
public:
ColorBarMeterH(LovyanGFX* parent, const int32_t minimum, const int32_t maximum, const int32_t wid,
const int32_t hgt, std::initializer_list<ColorRange> init = {})
: Base(parent, minimum, maximum, wid, hgt), _crange(init.begin(), init.end())
{
}
virtual ~ColorBarMeterH()
{
}
void setColorRange(std::initializer_list<ColorRange> init)
{
_crange = std::vector<ColorRange>(init);
}
protected:
virtual void render(LovyanGFX* dst, const int32_t x, const int32_t y, const int32_t val) override;
private:
std::vector<ColorRange> _crange{};
};
class ColorBarMeterV : public Base {
public:
ColorBarMeterV(LovyanGFX* parent, const int32_t minimum, const int32_t maximum, const int32_t wid,
const int32_t hgt, std::initializer_list<ColorRange> init = {})
: Base(parent, minimum, maximum, wid, hgt), _crange(init.begin(), init.end())
{
}
virtual ~ColorBarMeterV()
{
}
void setColorRange(std::initializer_list<ColorRange> init)
{
_crange = std::vector<ColorRange>(init);
}
protected:
virtual void render(LovyanGFX* dst, const int32_t x, const int32_t y, const int32_t val) override;
private:
std::vector<ColorRange> _crange{};
};
} // namespace ui
} // namespace m5
#endif

View file

@ -0,0 +1,43 @@
/*
* SPDX-FileCopyrightText: 2024 M5Stack Technology CO LTD
*
* SPDX-License-Identifier: MIT
*/
/*!
@file ui_base.cpp
@brief Base class for UI
*/
#include "ui_base.hpp"
#include <M5Utility.h>
namespace m5 {
namespace ui {
void Base::animate(const int32_t val, const elapsed_time_t dur)
{
if (_to != val && _min != _max) {
_from = _value;
_to = std::min(std::max(val, _min), _max);
_start_at = m5::utility::millis();
_duration = dur;
}
}
bool Base::update()
{
if (_start_at) {
auto now = m5::utility::millis();
if (now >= _start_at + _duration) {
_start_at = 0;
_value = _to;
} else {
float t = (now - _start_at) / (float)_duration;
_value = _from + (_to - _from) * t;
}
return true;
}
return false;
}
} // namespace ui
} // namespace m5

View file

@ -0,0 +1,129 @@
/*
* SPDX-FileCopyrightText: 2024 M5Stack Technology CO LTD
*
* SPDX-License-Identifier: MIT
*/
/*!
@file ui_base.hpp
@brief Base class for UI
*/
#ifndef UI_BASE_HPP
#define UI_BASE_HPP
#include <M5GFX.h>
#include <algorithm>
#include <cmath>
namespace m5 {
namespace ui {
class Base {
public:
using elapsed_time_t = unsigned long;
Base(LovyanGFX* parent, const int32_t minimum, const int32_t maximum, const int32_t wid, const int32_t hgt)
: _parent(parent),
_min{minimum},
_max{maximum},
_value{minimum},
_from{minimum},
_to{minimum},
_wid{wid},
_hgt{hgt}
{
}
virtual ~Base()
{
}
inline int32_t value() const
{
return _value;
}
inline int32_t valueTo() const
{
return _to;
}
inline int32_t width() const
{
return _wid;
}
inline int32_t height() const
{
return _hgt;
}
inline int32_t range() const
{
return _max - _min;
}
inline m5gfx::rgb565_t needleColor() const
{
return _needleClr;
}
inline m5gfx::rgb565_t gaugeColor() const
{
return _gaugeClr;
}
inline m5gfx::rgb565_t backgroundColor() const
{
return _bgClr;
}
template <typename T>
void setNeedleColor(const T& clr)
{
_needleClr = clr;
}
template <typename T>
void setGaugeColor(const T& clr)
{
_gaugeClr = clr;
}
template <typename T>
void setBackgroundColor(const T& clr)
{
_bgClr = clr;
}
virtual bool update();
///@name Control
///@{
virtual void animate(const int32_t val, const elapsed_time_t dur);
inline void set(const int32_t val)
{
animate(val, 0U);
}
///@}
///@name Push
///@{
inline void push(const int32_t x, const int32_t y)
{
push(_parent, x, y);
}
virtual void push(LovyanGFX* dst, const int32_t x, const int32_t y)
{
render(dst, x, y, _value);
}
///@}
protected:
inline float ratio(const int32_t val)
{
return range() > 0 ? (std::min(std::max(val, _min), _max) - _min) / (float)range() : 0.0f;
}
virtual void render(LovyanGFX* dst, const int32_t x, const int32_t y, const int32_t val)
{
}
private:
LovyanGFX* _parent{};
int32_t _min{}, _max{}, _value{}, _from{}, _to{}, _wid{}, _hgt{};
elapsed_time_t _start_at{}, _duration{};
m5gfx::rgb565_t _needleClr{TFT_WHITE}, _gaugeClr{TFT_DARKGRAY}, _bgClr{TFT_BLACK};
};
} // namespace ui
} // namespace m5
#endif

View file

@ -0,0 +1,57 @@
/*
* SPDX-FileCopyrightText: 2024 M5Stack Technology CO LTD
*
* SPDX-License-Identifier: MIT
*/
/*!
@file ui_gauge_meter.cpp
@brief Gauge meter
*/
#include "ui_gauge_meter.hpp"
#include <M5Utility.h>
#include <cassert>
namespace m5 {
namespace ui {
GaugeMeter::GaugeMeter(LovyanGFX* parent, const int32_t minimum, const int32_t maximum, const float minDeg,
const float maxDeg, const int32_t wid, const int32_t hgt, const int32_t thickness)
: GaugeMeter(parent, minimum, maximum, minDeg, maxDeg, wid, hgt, (wid >> 1) - 1, (hgt >> 1) - 1,
std::min(wid >> 1, hgt >> 1) - 1, thickness)
{
}
GaugeMeter::GaugeMeter(LovyanGFX* parent, const int32_t minimum, const int32_t maximum, const float minDeg,
const float maxDeg, const int32_t wid, const int32_t hgt, const int32_t cx, const int32_t cy,
const int32_t radius, const int32_t thickness)
: Base(parent, minimum, maximum, wid, hgt),
_cx(cx),
_cy(cy),
_radius(radius),
_thickness{thickness},
_minDeg{minDeg},
_maxDeg{maxDeg}
{
}
void GaugeMeter::render(LovyanGFX* dst, const int32_t x, const int32_t y, const int32_t val)
{
dst->setClipRect(x, y, width(), height());
int32_t r0{_radius}, r1{_radius - _thickness};
float sdeg{std::fmin(_minDeg, _maxDeg)};
float edeg{std::fmax(_minDeg, _maxDeg)};
// float sdeg{_minDeg};
// float edeg{_maxDeg};
dst->fillArc(x + _cx, y + _cy, r0, r1, sdeg, edeg, backgroundColor());
float deg = _minDeg + (_maxDeg - _minDeg) * ratio(val);
dst->fillArc(x + _cx, y + _cy, r0, r1, sdeg, deg, needleColor());
dst->drawArc(x + _cx, y + _cy, r0, r1, sdeg, edeg, gaugeColor());
dst->clearClipRect();
}
} // namespace ui
} // namespace m5

View file

@ -0,0 +1,41 @@
/*
* SPDX-FileCopyrightText: 2024 M5Stack Technology CO LTD
*
* SPDX-License-Identifier: MIT
*/
/*!
@file ui_gauge_meter.hpp
@brief Gauge meter
*/
#ifndef UI_GAUGE_METER_HPP
#define UI_GAUGE_METER_HPP
#include "ui_base.hpp"
namespace m5 {
namespace ui {
class GaugeMeter : public Base {
public:
GaugeMeter(LovyanGFX* parent, const int32_t minimum, const int32_t maximum, const float minDeg, const float maxDeg,
const int32_t wid, const int32_t hgt, const int32_t thickness);
GaugeMeter(LovyanGFX* parent, const int32_t minimum, const int32_t maximum, const float minDeg, const float maxDeg,
const int32_t wid, const int32_t hgt, const int32_t cx, const int32_t cy, const int32_t radius,
const int32_t thickness);
virtual ~GaugeMeter()
{
}
protected:
virtual void render(LovyanGFX* dst, const int32_t x, const int32_t y, const int32_t val) override;
private:
int32_t _cx{}, _cy{}, _radius{}, _thickness{};
float _minDeg{}, _maxDeg{};
};
} // namespace ui
} // namespace m5
#endif

View file

@ -0,0 +1,57 @@
/*
* SPDX-FileCopyrightText: 2024 M5Stack Technology CO LTD
*
* SPDX-License-Identifier: MIT
*/
/*!
@file ui_lgfx_extesion.hpp
@brief M5GFX lgfx extensions
*/
#ifndef UI_LGFX_EXTENSION_HPP
#define UI_LGFX_EXTENSION_HPP
#include <M5GFX.h>
namespace m5 {
namespace lgfx {
/*!
@brief Push sprite partialty
@param dst Push target
@param dx Destination X coordinate for push
@param dy Destination Y coordinate for push
@param width Width of partial rectangle
@param height Height of partial rectangle
@param src Source sprite
@param sx Source X coordinate for push
@param sy Source Y coordinate for push
@warning If you have already set clip rect to dst, save and set it again on your own.
@warning After this function call, the clip rectangle in dst is cleared.
*/
inline void pushPartial(LovyanGFX* dst, const int32_t dx, const int32_t dy, const int32_t width, const int32_t height,
LGFX_Sprite* src, const int32_t sx, const int32_t sy)
{
dst->setClipRect(dx, dy, width, height);
src->pushSprite(dst, dx - sx, dy - sy);
dst->clearClipRect();
}
/*!
@copybrief pushPartial(LovyanGFX* dst, const int32_t dx, const int32_t dy, const int32_t width, const int32_t height,
LGFX_Sprite* src, const int32_t sx, const int32_t sy)
@copydoc pushPartial(LovyanGFX* dst, const int32_t dx, const int32_t dy, const int32_t width, const int32_t height,
LGFX_Sprite* src, const int32_t sx, const int32_t sy)
@param transp Color/Palette for transparent
@tparam T Color/Palettetype
*/
template <typename T>
void pushPartial(LovyanGFX* dst, const int32_t dx, const int32_t dy, const int32_t width, const int32_t height,
LGFX_Sprite* src, const int32_t sx, const int32_t sy, const T& transp)
{
dst->setClipRect(dx, dy, width, height);
src->pushSprite(dst, dx - sx, dy - sy, transp);
dst->clearClipRect();
}
} // namespace lgfx
} // namespace m5
#endif

View file

@ -0,0 +1,166 @@
/*
* SPDX-FileCopyrightText: 2024 M5Stack Technology CO LTD
*
* SPDX-License-Identifier: MIT
*/
/*!
@file ui_plotter.cpp
@brief Plotter
*/
#include "ui_plotter.hpp"
#include <algorithm>
namespace m5 {
namespace ui {
Plotter::Plotter(LovyanGFX* parent, const size_t maxPlot, const int32_t wid, const int32_t hgt,
const int32_t coefficient)
: _parent(parent), _wid{wid}, _hgt{hgt}, _coefficient(coefficient), _data(maxPlot), _autoScale{true}
{
}
Plotter::Plotter(LovyanGFX* parent, const size_t maxPlot, const int32_t minimum, const int32_t maximum,
const int32_t wid, const int32_t hgt, const int32_t coefficient)
: _parent(parent),
_min{minimum},
_max{maximum},
_wid{wid},
_hgt{hgt},
_coefficient(coefficient),
_data(maxPlot),
_autoScale{false}
{
}
void Plotter::update()
{
if (_cb && _autoScale && _cb->size() >= 2) {
auto it = std::minmax_element(_cb->cbegin(), _cb->cend());
_min = *(it.first);
_max = *(it.second);
if (_min == _max) {
++_max;
}
}
}
void Plotter::push_back(const float val)
{
push_back((int32_t)(val * _coefficient));
}
void Plotter::push_back(const int32_t val)
{
auto v = _autoScale ? val : std::min(std::max(val, _min), _max);
_data.push_back(v);
if (_autoScale && _data.size() >= 2) {
#if 0
if (_min == _max) {
auto it = std::minmax_element(_data.cbegin(), _data.cend());
_min = *(it.first);
_max = *(it.second);
} else {
if (v < _min) {
_min = v;
}
if (v > _max) {
_max = v;
}
}
#else
auto it = std::minmax_element(_data.cbegin(), _data.cend());
_min = *(it.first);
_max = *(it.second);
if (_min == _max) {
++_max;
}
#endif
}
}
void Plotter::assign(m5::container::CircularBuffer<int32_t>& cb)
{
_cb = &cb;
if (_autoScale && _cb->size() >= 2) {
auto it = std::minmax_element(_cb->cbegin(), _cb->cend());
_min = *(it.first);
_max = *(it.second);
}
}
void Plotter::push(LovyanGFX* dst, const int32_t x, const int32_t y)
{
dst->setClipRect(x, y, width(), height());
// gauge
dst->drawFastHLine(x, y, _wid, _gaugeClr);
dst->drawFastHLine(x, y + (_hgt >> 1), _wid, _gaugeClr);
dst->drawFastHLine(x, y + (_hgt >> 2), _wid, _gaugeClr);
dst->drawFastHLine(x, y + (_hgt >> 2) * 3, _wid, _gaugeClr);
dst->drawFastHLine(x, y + _hgt - 1, _wid, _gaugeClr);
if (_data.size() >= 2) {
auto it = _cb ? _cb->cbegin() : _data.cbegin();
auto itend = _cb ? --_cb->cend() : --_data.cend();
auto sz = _cb ? _cb->size() : _data.size();
const float range{(float)_max - _min};
const int32_t hh{_hgt - 1};
int32_t left{x};
// plot latest
if (sz > _wid) {
auto cnt{sz - _wid};
while (cnt--) {
++it; // Bidirectional iterator, so only ++/-- is available.
}
}
if (sz < _wid) {
left += _wid - sz;
}
while (it != itend) {
int32_t s{*it}, e{*(++it)};
dst->drawLine(left, y + hh - hh * (s - _min) / range, left + 1, y + hh - hh * (e - _min) / range,
_needleClr);
++left;
}
}
//
auto f = dst->getFont();
auto td = dst->getTextDatum();
dst->setFont(&fonts::Font0);
dst->setTextColor(TFT_WHITE);
dst->setTextDatum(_tdatum);
int32_t tx{x}; // left
switch (_tdatum & 0x03) {
case 1: // center
tx = x + (_wid >> 1);
break;
case 2: // right:
tx = x + _wid;
break;
default:
break;
}
auto s = m5::utility::formatString("%d%s", _min / _coefficient, _ustr ? _ustr : "");
dst->drawString(s.c_str(), tx, y + _hgt - 8);
if (_min != _max) {
auto s = m5::utility::formatString("%d%s", _max / _coefficient, _ustr ? _ustr : "");
dst->drawString(s.c_str(), tx, y);
if (_max - _min > 1) {
s = m5::utility::formatString("%d%s", (_min + ((_max - _min) >> 1)) / _coefficient, _ustr ? _ustr : "");
dst->drawString(s.c_str(), tx, y + _hgt / 2 - 4);
}
}
dst->setTextDatum(td);
dst->setFont(f);
dst->clearClipRect();
}
} // namespace ui
} // namespace m5

View file

@ -0,0 +1,109 @@
/*
* SPDX-FileCopyrightText: 2024 M5Stack Technology CO LTD
*
* SPDX-License-Identifier: MIT
*/
/*!
@file ui_plotter.hpp
@brief Plotter
*/
#ifndef UI_PLOTTER_HPP
#define UI_PLOTTER_HPP
#include <M5GFX.h>
#include <M5Utility.h>
namespace m5 {
namespace ui {
class Plotter {
public:
Plotter(LovyanGFX* parent, const size_t maxPlot, const int32_t wid, const int32_t hgt,
const int32_t coefficient = 1);
Plotter(LovyanGFX* parent, const size_t maxPlot, const int32_t minimum, const int32_t maximum, const int32_t wid,
const int32_t hgt, const int32_t coefficient = 1);
void update();
inline int32_t width() const
{
return _wid;
}
inline int32_t height() const
{
return _hgt;
}
inline int32_t minimum() const
{
return _min;
}
inline int32_t maximum() const
{
return _max;
}
template <typename T>
void setNeedleColor(const T& clr)
{
_needleClr = clr;
}
template <typename T>
void setGaugeColor(const T& clr)
{
_gaugeClr = clr;
}
template <typename T>
void setBackgroundColor(const T& clr)
{
_bgClr = clr;
}
inline void setUnitString(const char* s)
{
_ustr = s;
}
inline void setGaugeTextDatum(const textdatum_t datum)
{
_tdatum = datum;
}
void push_back(const float val);
void push_back(const int32_t val);
void assign(m5::container::CircularBuffer<int32_t>& cb);
inline void push(const int32_t x, const int32_t y)
{
push(_parent, x, y);
}
virtual void push(LovyanGFX* dst, const int32_t x, const int32_t y);
protected:
m5gfx::rgb565_t needleColor() const
{
return _needleClr;
}
m5gfx::rgb565_t gaugeColor() const
{
return _gaugeClr;
}
m5gfx::rgb565_t backgroundColor() const
{
return _bgClr;
}
protected:
private:
LovyanGFX* _parent{};
int32_t _min{}, _max{}, _wid{}, _hgt{}, _coefficient{};
m5::container::CircularBuffer<int32_t> _data;
m5::container::CircularBuffer<int32_t>* _cb{};
m5gfx::rgb565_t _needleClr{TFT_WHITE}, _gaugeClr{TFT_DARKGRAY}, _bgClr{TFT_BLACK};
textdatum_t _tdatum{textdatum_t::top_left};
const char* _ustr{};
bool _autoScale{};
};
} // namespace ui
} // namespace m5
#endif

View file

@ -0,0 +1,109 @@
/*
* SPDX-FileCopyrightText: 2024 M5Stack Technology CO LTD
*
* SPDX-License-Identifier: MIT
*/
/*!
@file ui_rotary_couter.cpp
@brief Rotary counter
*/
#include "ui_rotary_counter.hpp"
#include <M5Utility.h>
#include <cassert>
namespace m5 {
namespace ui {
RotaryCounter::Number::Number(LGFX_Sprite* src, const uint8_t base) : _src{src}, _base{base}
{
assert(_src && "Source must be NOT nullptr");
_height = _src->height() / (_base + 1);
}
void RotaryCounter::Number::animate(const uint8_t num, const uint32_t dur)
{
_duration = dur;
auto n = num % _base;
const int32_t shgt{_height * _base};
if (_to != n) {
// _y = _fy = _ty % (_height * _base);
_fy = _y % shgt;
_ty = n * _height;
if (_ty < _fy) {
_ty += shgt;
}
_start_at = m5::utility::millis();
_to = n;
// printf("==> %d >Y ;%d -> %d\n", _to, _fy, _ty);
}
}
bool RotaryCounter::Number::update(const unsigned long now)
{
const int32_t shgt{_height * _base};
if (_start_at) {
if (now >= _start_at + _duration) {
_start_at = 0;
_ty %= shgt;
_y = _fy = _ty;
} else {
float t = (now - _start_at) / (float)_duration;
_y = (int16_t)(_fy + (_ty - _fy) * t) % shgt;
// printf(">>> [%d] %d: (%d - %d) %f\n", _to, _y, _fy, _ty, t);
}
return true;
}
return false;
}
RotaryCounter::RotaryCounter(LovyanGFX* parent, const size_t maxDigits, LGFX_Sprite* src, const uint8_t base)
: _parent(parent), _base(base)
{
_numbers.resize(maxDigits);
if (src) {
construct(src);
}
}
void RotaryCounter::construct(LGFX_Sprite* src)
{
assert(src != nullptr && "src must be NOT nullptr");
for (auto& n : _numbers) {
n = Number(src, _base);
}
}
bool RotaryCounter::update()
{
bool updated{};
if (!_pause) {
auto now = m5::utility::millis();
for (auto&& n : _numbers) {
updated |= n.update(now);
}
}
return updated;
}
void RotaryCounter::animate(const uint32_t val, const unsigned long dur)
{
uint32_t v{val};
for (auto it = _numbers.rbegin(); it != _numbers.rend(); ++it) {
it->animate(v % 10, dur);
v /= 10;
}
}
void RotaryCounter::animate(const size_t digit, const uint8_t val, const unsigned long dur)
{
if (digit >= _numbers.size()) {
M5_LIB_LOGE("Illegal digit %zu/%zu", digit, _numbers.size());
return;
}
_numbers[digit].animate(val, dur);
}
} // namespace ui
} // namespace m5

View file

@ -0,0 +1,194 @@
/*
* SPDX-FileCopyrightText: 2024 M5Stack Technology CO LTD
*
* SPDX-License-Identifier: MIT
*/
/*!
@file ui_rotary_couter.hpp
@brief Rotary counter
*/
#ifndef UI_ROTARY_COUNTER_HPP
#define UI_ROTARY_COUNTER_HPP
#include <M5GFX.h>
#include <vector>
#include "ui_lgfx_extension.hpp"
namespace m5 {
namespace ui {
/*!
@class RotaryCounter
@brief Rotary counter with any digits
*/
class RotaryCounter {
public:
using elapsed_time_t = unsigned long;
// For each number
class Number {
public:
Number()
{
}
Number(LGFX_Sprite* src, const uint8_t base = 10);
inline LGFX_Sprite* sprite()
{
return _src;
}
inline int32_t sourceY() const
{
return _y;
}
inline uint32_t width() const
{
return _src ? _src->width() : 0U;
}
inline uint32_t height() const
{
return _height;
}
inline void set(const uint8_t num)
{
animate(num, 0);
}
void animate(const uint8_t num, const uint32_t dur);
bool update(const elapsed_time_t now);
private:
LGFX_Sprite* _src{};
int32_t _height{}, _fy{}, _ty{}, _y{};
elapsed_time_t _start_at{}, _duration{};
uint8_t _base{};
uint8_t _to{};
};
using vector_type_t = std::vector<Number>;
/*!
@brief Constructor
@param parent Push target
@param src Source sprite
@param digits Number of digits
@param base How many decimal digits?
@note The source sprite should consist of the following
[0...9] [0...5] [0...2]
+---+ +---+ +---+
| 0 | | 0 | | 0 |
| 1 | | 1 | | 1 |
| 2 | | 2 | | 2 |
| 3 | | 3 | | 0 |
| 4 | | 4 | +---+
| 5 | | 5 |
| 6 | | 0 |
| 7 | +---*
| 8 |
| 9 |
| 0 |
+---+
*/
RotaryCounter(LovyanGFX* parent, const size_t digits, LGFX_Sprite* src = nullptr, const uint8_t base = 10);
virtual ~RotaryCounter()
{
}
//! @brief Construct with source sprite
void construct(LGFX_Sprite* src);
//!@brief Update all numbers
virtual bool update();
///@Properties
///@{
const vector_type_t& numbers() const
{
return _numbers;
}
vector_type_t& numbers()
{
return _numbers;
}
///@}
///@name Control
///@{
/*!@brief Pause/Resume */
inline void pause(const bool paused)
{
_pause = paused;
}
//! @brief Pause
inline void pause()
{
pause(true);
}
//! @brief Resume
inline void resume()
{
pause(false);
}
//! @brief Animate and change values (all)
void animate(const uint32_t val, const elapsed_time_t dur);
//! @brief Set value (all)
inline void set(const uint32_t val)
{
animate(val, 0U);
}
//! Animate and change values (partial)
void animate(const size_t digit, const uint8_t val, const elapsed_time_t dur);
//! @brief Set value (partial)
inline void set(const size_t digit, const uint8_t val)
{
animate(digit, val, 0U);
}
///@}
///@warning If you have already set clip rect to dst, save and set it again on your own.
///@warning After this function call, the clip rectangle in dst is cleared.
///@name Push
///@{
inline void push(const int32_t x, const int32_t y)
{
push(_parent, x, y);
}
void push(LovyanGFX* dst, const int32_t x, const int32_t y)
{
int32_t left{x};
for (auto&& n : _numbers) {
m5::lgfx::pushPartial(dst, left, y, n.width(), n.height(), n.sprite(), 0, n.sourceY());
left += n.width();
}
}
template <typename T>
inline void push(const int32_t x, const int32_t y, const T& transp)
{
push(_parent, x, y, transp);
}
template <typename T>
void push(LovyanGFX* dst, const int32_t x, const int32_t y, const T& transp)
{
int32_t left{x};
if (!_fit) {
fit();
_fit = true;
}
for (auto&& n : _numbers) {
m5::lgfx::pushPartial(dst, left, y, n.width(), n.height(), n.sprite(), 0, n.sourceY(), transp);
left += n.width();
}
}
///@}
protected:
void fit();
LovyanGFX* _parent{};
int32_t _height{};
vector_type_t _numbers{};
uint8_t _base{};
bool _fit{}, _pause{};
};
} // namespace ui
} // namespace m5
#endif

View file

@ -0,0 +1,67 @@
/*
* SPDX-FileCopyrightText: 2024 M5Stack Technology CO LTD
*
* SPDX-License-Identifier: MIT
*/
/*!
@file ui_scale_meter.cpp
@brief Scale meter
*/
#include "ui_scale_meter.hpp"
#include <M5Utility.h>
#include <cassert>
namespace {
constexpr float table0[] = {0.0f, 0.25f, 0.5f, 0.75f, 1.0f};
constexpr float table1[] = {0.125f, 0.125f * 3, 0.125f * 5, 0.125f * 7};
} // namespace
namespace m5 {
namespace ui {
void ScaleMeter::render(LovyanGFX* dst, const int32_t x, const int32_t y, const int32_t val)
{
dst->setClipRect(x, y, width(), height());
auto rad = _radius - 1;
// gauge
int32_t r0{rad}, r1{rad - 1};
float sdeg{std::fmin(_minDeg, _maxDeg)};
float edeg{std::fmax(_minDeg, _maxDeg)};
dst->fillArc(x + _cx, y + _cy, r0, r1, sdeg, edeg, gaugeColor());
const auto w = _maxDeg - _minDeg;
constexpr float deg_to_rad = 0.017453292519943295769236907684886f;
for (auto&& e : table0) {
const float f = _minDeg + w * e;
const float cf = std::cos(f * deg_to_rad);
const float sf = std::sin(f * deg_to_rad);
int32_t sx = rad * cf;
int32_t sy = rad * sf;
int32_t ex = (rad - 4) * cf;
int32_t ey = (rad - 4) * sf;
dst->drawLine(x + _cx + sx, y + _cy + sy, x + _cx + ex, y + _cy + ey, gaugeColor());
}
for (auto&& e : table1) {
const float f = _minDeg + w * e;
const float cf = std::cos(f * deg_to_rad);
const float sf = std::sin(f * deg_to_rad);
int32_t sx = rad * cf;
int32_t sy = rad * sf;
int32_t ex = (rad - 2) * cf;
int32_t ey = (rad - 2) * sf;
dst->drawLine(x + _cx + sx, y + _cy + sy, x + _cx + ex, y + _cy + ey, gaugeColor());
}
// needle
float deg = _minDeg + (_maxDeg - _minDeg) * ratio(val);
int32_t tx = rad * std::cos(deg * deg_to_rad);
int32_t ty = rad * std::sin(deg * deg_to_rad);
dst->drawLine(x + _cx, y + _cy, x + _cx + tx, y + _cy + ty, needleColor());
dst->clearClipRect();
}
} // namespace ui
} // namespace m5

View file

@ -0,0 +1,41 @@
/*
* SPDX-FileCopyrightText: 2024 M5Stack Technology CO LTD
*
* SPDX-License-Identifier: MIT
*/
/*!
@file ui_scale_meter.hpp
@brief Scale meter
*/
#ifndef UI_SCALE_METER_HPP
#define UI_SCALE_METER_HPP
#include "ui_base.hpp"
namespace m5 {
namespace ui {
class ScaleMeter : public Base {
public:
ScaleMeter(LovyanGFX* parent, const int32_t minimum, const int32_t maximum, const float minDeg, const float maxDeg,
const int32_t wid, const int32_t hgt, const int32_t cx, const int32_t cy, const uint32_t radius)
: Base(parent, minimum, maximum, wid, hgt), _cx(cx), _cy(cy), _radius(radius), _minDeg{minDeg}, _maxDeg{maxDeg}
{
}
virtual ~ScaleMeter()
{
}
protected:
virtual void render(LovyanGFX* dst, const int32_t x, const int32_t y, const int32_t val) override;
private:
int32_t _cx{}, _cy{}, _radius{};
float _minDeg{}, _maxDeg{};
};
} // namespace ui
} // namespace m5
#endif

View file

@ -0,0 +1,50 @@
/*
* SPDX-FileCopyrightText: 2024 M5Stack Technology CO LTD
*
* SPDX-License-Identifier: MIT
*/
/*!
@file sprite.cpp
@brief Shared sprite
*/
#include "sprite.hpp"
struct NumberSprite {
const lgfx::IFont* font;
LGFX_Sprite* sprite;
const int32_t width, height;
const uint32_t base;
};
// For rotary counter
LGFX_Sprite number10_6x8;
LGFX_Sprite number10_8x16;
LGFX_Sprite number6_6x8;
LGFX_Sprite number6_8x16;
void make_shared_sprites()
{
NumberSprite table[] = {
{&fonts::Font0, &number10_6x8, 6, 8, 10},
{&fonts::Font2, &number10_8x16, 8, 16, 10},
{&fonts::Font0, &number6_6x8, 6, 8, 6},
{&fonts::Font2, &number6_8x16, 8, 16, 6},
};
for (auto&& e : table) {
e.sprite->setPsram(false);
e.sprite->setColorDepth(1);
e.sprite->createSprite(e.width + 2, e.height * (e.base + 1));
e.sprite->setPaletteColor(0, TFT_BLACK);
e.sprite->setPaletteColor(1, TFT_WHITE);
e.sprite->setTextColor(1, 0);
e.sprite->setFont(e.font);
for (int i = 0; i <= e.base; ++i) {
e.sprite->setCursor(1, i * e.height + 1);
e.sprite->printf("%d", i % e.base);
}
// e.sprite->drawFastVLine(0, 0, e.sprite->height(), 1);
// e.sprite->drawFastVLine(e.sprite->width() - 1, 0, e.sprite->height(), 1);
}
}

View file

@ -0,0 +1,23 @@
/*
* SPDX-FileCopyrightText: 2024 M5Stack Technology CO LTD
*
* SPDX-License-Identifier: MIT
*/
/*!
@file sprite.hpp
@brief Shared sprite
*/
#ifndef SPRITE_HPP
#define SPRITE_HPP
#include <M5GFX.h>
void make_shared_sprites();
// For rotary counter
extern LGFX_Sprite number10_6x8;
extern LGFX_Sprite number10_8x16;
extern LGFX_Sprite number6_6x8;
extern LGFX_Sprite number6_8x16;
#endif

View file

@ -0,0 +1,33 @@
/*
* SPDX-FileCopyrightText: 2024 M5Stack Technology CO LTD
*
* SPDX-License-Identifier: MIT
*/
/*!
@file ui_UnitBase.cpp
@brief UI for UnitBase
*/
#include "ui_UnitBase.hpp"
UnitUIBase::UnitUIBase(LovyanGFX* parent) : _parent(parent)
{
_sem = xSemaphoreCreateBinary();
xSemaphoreGive(_sem);
}
UnitUIBase::~UnitUIBase()
{
xSemaphoreTake(_sem, portMAX_DELAY);
vSemaphoreDelete(_sem);
}
bool UnitUIBase::lock(portTickType bt)
{
return xSemaphoreTake(_sem, bt) == pdTRUE;
}
void UnitUIBase::unlock()
{
xSemaphoreGive(_sem);
}

View file

@ -0,0 +1,43 @@
/*
* SPDX-FileCopyrightText: 2024 M5Stack Technology CO LTD
*
* SPDX-License-Identifier: MIT
*/
/*!
@file ui_UnitBase.hpp
@brief UI for UnitBase
*/
#ifndef UI_UNIT_BASE_HPP
#define UI_UNIT_BASE_HPP
#include <freertos/FreeRTOS.h>
#include <M5GFX.h>
#include <vector>
class UnitUIBase {
public:
explicit UnitUIBase(LovyanGFX* parent);
virtual ~UnitUIBase();
bool lock(portTickType bt = portMAX_DELAY);
// TickType_t
void unlock();
virtual void construct() = 0;
virtual void update() = 0;
virtual void push(LovyanGFX* dst, const int32_t x, const int32_t y) = 0;
inline void push(const int32_t x, const int32_t y)
{
push(_parent, x, y);
}
protected:
LovyanGFX* _parent{};
int32_t _wid{}, _hgt{};
private:
volatile SemaphoreHandle_t _sem{};
};
#endif

View file

@ -0,0 +1,120 @@
/*
* SPDX-FileCopyrightText: 2024 M5Stack Technology CO LTD
*
* SPDX-License-Identifier: MIT
*/
/*!
@file ui_UnitCO2.cpp
@brief UI for UnitCO2
*/
#include "ui_UnitCO2.hpp"
#include <M5Unified.h>
#include <M5Utility.h>
#include <iterator>
namespace {
constexpr int32_t GAP{2};
constexpr float COEFF{100.0f};
constexpr float COEFF_RECIPROCAL{1.0f / COEFF};
constexpr int32_t min_co2{0};
constexpr int32_t max_co2{6000};
constexpr int32_t min_temp{-10};
constexpr int32_t max_temp{40};
m5gfx::rgb565_t temp_chooseColor(const int32_t val)
{
return val > 0 ? m5gfx::rgb565_t(0xfe, 0xcb, 0xf2) : m5gfx::rgb565_t(0xb8, 0xc2, 0xf2);
}
constexpr std::initializer_list<m5::ui::ColorRange> co2_color_table = {
{1000, m5gfx::rgb565_t(TFT_GREEN)},
{1500, m5gfx::rgb565_t(TFT_GOLD)},
{2500, m5gfx::rgb565_t(TFT_ORANGE)},
{6000, m5gfx::rgb565_t(TFT_RED)},
};
m5gfx::rgb565_t co2_chooseColor(const int32_t val)
{
for (auto&& e : co2_color_table) {
if (val <= e.lesseq) {
return e.clr;
}
}
return (std::end(co2_color_table) - 1)->clr;
}
} // namespace
void UnitCO2SmallUI::construct()
{
auto& lcd = M5.Display;
_wid = lcd.width() >> 1;
_hgt = lcd.height() >> 1;
int32_t wh = std::max(_wid / 2, _hgt / 2) - GAP * 2;
_tempMeter.reset(new m5::ui::GaugeMeter(_parent, min_temp * COEFF, max_temp * COEFF, 90.0f + 45.0f,
90.0f - 45.0f + 360.f, wh, wh, 10));
_tempMeter->set(0);
_co2Meter.reset(
new m5::ui::ColorBarMeterH(_parent, min_co2, max_co2, _wid - GAP * 2, _hgt - wh - GAP * 2, co2_color_table));
}
void UnitCO2SmallUI::push_back(const int32_t co2, const float temp)
{
_tempMeter->animate(temp * COEFF, 1000);
_co2Meter->animate(co2, 1000);
}
void UnitCO2SmallUI::update()
{
_tempMeter->update();
_tempMeter->setNeedleColor(temp_chooseColor(_tempMeter->value()));
_co2Meter->update();
}
void UnitCO2SmallUI::push(LovyanGFX* dst, const int32_t x, const int32_t y)
{
auto left = x;
auto right = x + _wid - 1;
auto top = y;
auto bottom = y + _hgt - 1;
auto w = right - left + 1;
auto h = bottom - top + 1;
// BG
dst->fillRoundRect(x, y, _wid, _hgt, GAP * 2, TFT_MAGENTA);
dst->fillRoundRect(x + GAP, y + GAP, _wid - GAP * 2, _hgt - GAP * 2, GAP, TFT_BLACK);
auto tcx = left + GAP;
auto tcy = top + GAP;
_tempMeter->push(dst, x + GAP, y + GAP);
_co2Meter->push(dst, x + GAP, tcy + _tempMeter->height() + GAP * 2);
//
auto f = dst->getFont();
dst->setFont(&fonts::Font0);
dst->setTextColor(TFT_WHITE);
auto td = dst->getTextDatum();
dst->setTextDatum(textdatum_t::middle_center);
auto s = m5::utility::formatString("%3.2fC", _tempMeter->value() * COEFF_RECIPROCAL);
dst->drawString(s.c_str(), tcx + _tempMeter->width() / 2, tcy + _tempMeter->height() / 2);
dst->drawString("TEMP", tcx + _tempMeter->width() / 2, tcy + _tempMeter->height() / 2 + 10);
dst->setTextDatum(textdatum_t::top_right);
dst->drawString("CO2", right - GAP, top + _tempMeter->height() + GAP - 10);
auto sw = dst->drawString("ppm", right - GAP, top + _tempMeter->height() + GAP);
dst->setTextColor((uint16_t)co2_chooseColor(_co2Meter->value()));
s = m5::utility::formatString("%d", _co2Meter->value());
dst->drawString(s.c_str(), right - GAP - sw, top + _tempMeter->height() + GAP);
dst->setFont(&fonts::Font2);
dst->setTextColor(TFT_WHITE);
dst->setTextDatum(textdatum_t::middle_center);
dst->drawString("UnitCO2", left + w / 4 * 3, top + h / 4);
dst->setTextDatum(td);
dst->setFont(f);
}

View file

@ -0,0 +1,38 @@
/*
* SPDX-FileCopyrightText: 2024 M5Stack Technology CO LTD
*
* SPDX-License-Identifier: MIT
*/
/*!
@file ui_UnitCO2.hpp
@brief UI for UnitCO2
*/
#ifndef UI_UNIT_CO2_HPP
#define UI_UNIT_CO2_HPP
#include "parts/ui_rotary_counter.hpp"
#include "parts/ui_scale_meter.hpp"
#include "parts/ui_gauge_meter.hpp"
#include "parts/ui_bar_meter.hpp"
#include "ui_UnitBase.hpp"
#include <memory>
class UnitCO2SmallUI : public UnitUIBase {
public:
explicit UnitCO2SmallUI(LovyanGFX* parent = nullptr) : UnitUIBase(parent)
{
}
void push_back(const int32_t co2, const float temp);
virtual void construct() override;
virtual void update() override;
virtual void push(LovyanGFX* dst, const int32_t x, const int32_t y) override;
private:
LovyanGFX* _parent{};
int32_t _wid{}, _hgt{};
std::unique_ptr<m5::ui::GaugeMeter> _tempMeter{};
std::unique_ptr<m5::ui::ColorBarMeterH> _co2Meter{};
};
#endif

View file

@ -0,0 +1,192 @@
/*
* SPDX-FileCopyrightText: 2024 M5Stack Technology CO LTD
*
* SPDX-License-Identifier: MIT
*/
/*!
@file ui_UnitENV3.cpp
@brief UI for UnitENV3
*/
#include "ui_UnitENV3.hpp"
#include <M5Unified.h>
#include <M5Utility.h>
#include <cassert>
namespace {
constexpr int32_t GAP{2};
constexpr float COEFF{100.0f};
constexpr float COEFF_RECIPROCAL{1.0f / COEFF};
constexpr m5gfx::rgb565_t hum_needle_color{32, 147, 223};
constexpr m5gfx::rgb565_t pres_needle_color{161, 54, 64};
constexpr int32_t min_temp{-10};
constexpr int32_t max_temp{40};
constexpr int32_t min_hum{0};
constexpr int32_t max_hum{100};
constexpr int32_t min_pres{0};
constexpr int32_t max_pres{1500};
m5gfx::rgb565_t temp_chooseColor(const int32_t val)
{
return val > 0 ? m5gfx::rgb565_t(0xfe, 0xcb, 0xf2) : m5gfx::rgb565_t(0xb8, 0xc2, 0xf2);
}
} // namespace
void UnitENV3SmallUI::construct()
{
auto& lcd = M5.Display;
_wid = lcd.width() >> 1;
_hgt = lcd.height() >> 1;
// auto left = 0 + GAP;
// auto right = _wid - GAP - 1;
// auto top = 0;
// auto bottom = _hgt - 1;
auto w = _wid / 5;
auto h = _hgt / 2 - GAP * 2;
auto rad = std::min(w, h / 2);
auto wh = (std::min(_wid, _hgt) >> 1) - GAP * 2;
_tempMeterSHT.reset(
new m5::ui::GaugeMeter(_parent, min_temp * COEFF, max_temp * COEFF, 25.0f, -25.0f + 360.f, wh, wh, 10));
_tempMeterQMP.reset(
new m5::ui::GaugeMeter(_parent, min_temp * COEFF, max_temp * COEFF, 25.0f, -25.0f + 360.f, wh, wh, 10));
_humMeter.reset(
new m5::ui::ScaleMeter(_parent, min_hum * COEFF, max_hum * COEFF, 360.0f + 90.0f, 270.0f, w, h, 0, h / 2, rad));
_presMeter.reset(new m5::ui::ScaleMeter(_parent, min_pres * COEFF, max_pres * COEFF, 360.0f + 90.0f, 270.0f, w, h,
0, h / 2, rad));
_humMeter->setNeedleColor(hum_needle_color);
_presMeter->setNeedleColor(pres_needle_color);
}
void UnitENV3SmallUI::sht30_push_back(const float tmp, const float hum)
{
_tempMeterSHT->animate(tmp * COEFF, 10);
_humMeter->animate(hum * COEFF, 10);
}
void UnitENV3SmallUI::qmp6988_push_back(const float tmp, const float pa)
{
_tempMeterQMP->animate(tmp * COEFF, 10);
_presMeter->animate(pa * COEFF * 0.01f, 10); // pa to hPa
}
void UnitENV3SmallUI::update()
{
lock();
_tempMeterSHT->update();
_tempMeterQMP->update();
_tempMeterSHT->setNeedleColor(temp_chooseColor(_tempMeterSHT->value()));
_tempMeterQMP->setNeedleColor(temp_chooseColor(_tempMeterQMP->value()));
_humMeter->update();
_presMeter->update();
unlock();
}
void UnitENV3SmallUI::push(LovyanGFX* dst, const int32_t x, const int32_t y)
{
auto left = x;
auto right = x + _wid - 1;
auto top = y;
auto bottom = y + _hgt - 1;
auto w = right - left + 1;
auto h = bottom - top + 1;
auto sx = left + GAP;
auto sy = top + GAP;
auto qx = left + GAP;
auto qy = top + h / 2 + GAP;
auto hx = right - (_humMeter->width() + GAP);
auto hy = top + GAP;
auto px = right - (_presMeter->width() + GAP);
auto py = top + h / 2 + GAP;
auto f = dst->getFont();
dst->setFont(&fonts::Font0);
dst->setTextColor(TFT_WHITE);
auto td = dst->getTextDatum();
// BG
dst->fillRoundRect(x, y, _wid, _hgt, GAP, TFT_GREEN);
dst->fillRoundRect(x + GAP, y + GAP, _wid - GAP * 2, _hgt - GAP * 2, GAP, TFT_BLACK);
// meters
_tempMeterSHT->push(dst, sx, sy);
_humMeter->push(dst, hx, hy);
_tempMeterQMP->push(dst, qx, qy);
_presMeter->push(dst, px, py);
// text
dst->setTextDatum(textdatum_t::middle_left);
auto s = m5::utility::formatString("T:%3.2fC", _tempMeterSHT->value() * COEFF_RECIPROCAL);
dst->drawString(s.c_str(), sx + 16, sy + _tempMeterSHT->height() / 2);
s = m5::utility::formatString("T:%3.2fC", _tempMeterQMP->value() * COEFF_RECIPROCAL);
dst->drawString(s.c_str(), qx + 16, qy + _tempMeterQMP->height() / 2);
dst->setTextDatum(textdatum_t::middle_right);
s = m5::utility::formatString("H:%3.2f%%", _humMeter->value() * COEFF_RECIPROCAL);
dst->drawString(s.c_str(), hx, hy + _humMeter->height() / 2);
s = m5::utility::formatString("P:%4.0f", _presMeter->value() * COEFF_RECIPROCAL);
dst->drawString(s.c_str(), px, py + _presMeter->height() / 2);
#if 0
auto s = m5::utility::formatString("%dC", max_temp);
dst->drawString(s.c_str(), sx, sy);
dst->drawString(s.c_str(), qx, qy);
s = m5::utility::formatString("%dC", (max_temp - min_temp) / 2);
dst->setTextDatum(textdatum_t::middle_left);
dst->drawString(s.c_str(), sx, sy + _tempMeterSHT->height() / 2);
dst->drawString(s.c_str(), qx, qy + _tempMeterSHT->height() / 2);
dst->setTextDatum(textdatum_t::bottom_left);
s = m5::utility::formatString("%dC", min_temp);
dst->drawString(s.c_str(), sx, sy + _tempMeterSHT->height());
dst->drawString(s.c_str(), qx, qy + _tempMeterSHT->height());
#endif
dst->setTextDatum(textdatum_t::top_right);
s = m5::utility::formatString("%d%%", max_hum);
dst->drawString(s.c_str(), right - GAP, hy);
dst->setTextDatum(textdatum_t::bottom_right);
s = m5::utility::formatString("%d%%", min_hum);
dst->drawString(s.c_str(), right - GAP, hy + _humMeter->height());
dst->setTextDatum(textdatum_t::top_right);
s = m5::utility::formatString("%dhPa", max_pres);
dst->drawString(s.c_str(), right - GAP, py);
dst->setTextDatum(textdatum_t::bottom_right);
s = m5::utility::formatString("%dhPa", min_pres);
dst->drawString(s.c_str(), right - GAP, py + _presMeter->height());
//
#if 0
dst->setTextDatum(textdatum_t::top_left);
dst->drawString("SHT30", sx + _tempMeterSHT->width() + GAP, sy + 10);
s = m5::utility::formatString(" T: %3.2f C", _tempMeterSHT->valueTo() * COEFF_RECIPROCAL);
dst->drawString(s.c_str(), sx + _tempMeterSHT->width() + GAP, sy + 10 * 2);
s = m5::utility::formatString(" H: %3.2f RH", _humMeter->valueTo() * COEFF_RECIPROCAL);
dst->drawString(s.c_str(), sx + _tempMeterSHT->width() + GAP, sy + 10 * 3);
dst->setTextDatum(textdatum_t::bottom_right);
dst->drawString("QMP6988", right - (_tempMeterSHT->width() + GAP), bottom - GAP - 10 + 1);
s = m5::utility::formatString(" T: %4.2f C", _tempMeterQMP->valueTo() * COEFF_RECIPROCAL);
dst->drawString(s.c_str(), right - (_tempMeterSHT->width() + GAP), bottom - GAP - 10 * 2);
s = m5::utility::formatString(" P: %4.2f hPa", _presMeter->valueTo() * COEFF_RECIPROCAL);
dst->drawString(s.c_str(), right - (_tempMeterSHT->width() + GAP), bottom - GAP - 10 * 3);
#endif
dst->setTextDatum(textdatum_t::top_center);
dst->drawString("SHT30", x + w / 2, top + GAP);
dst->setTextDatum(textdatum_t::bottom_center);
dst->drawString("QMP6988", x + w / 2, bottom - GAP);
dst->setFont(&fonts::Font2);
dst->setTextColor(TFT_GREEN);
dst->setTextDatum(textdatum_t::middle_center);
dst->drawString("UnitENVIII", left + w / 2, top + h / 2);
dst->setTextDatum(td);
dst->setFont(f);
}

View file

@ -0,0 +1,39 @@
/*
* SPDX-FileCopyrightText: 2024 M5Stack Technology CO LTD
*
* SPDX-License-Identifier: MIT
*/
/*!
@file ui_UnitENV3.hpp
@brief UI for UnitENV3
*/
#ifndef UI_UNIT_ENV3_HPP
#define UI_UNIT_ENV3_HPP
#include "parts/ui_scale_meter.hpp"
#include "parts/ui_gauge_meter.hpp"
#include "ui_UnitBase.hpp"
#include <memory>
class UnitENV3SmallUI : public UnitUIBase {
public:
explicit UnitENV3SmallUI(LovyanGFX* parent) : UnitUIBase(parent)
{
}
void construct() override;
void sht30_push_back(const float tmp, const float hum); // SHT30
void qmp6988_push_back(const float tmp, const float pres); // QMP6988
void update() override;
void push(LovyanGFX* dst, const int32_t x, const int32_t y) override;
private:
std::unique_ptr<m5::ui::GaugeMeter> _tempMeterSHT{};
std::unique_ptr<m5::ui::GaugeMeter> _tempMeterQMP{};
std::unique_ptr<m5::ui::ScaleMeter> _humMeter;
std::unique_ptr<m5::ui::ScaleMeter> _presMeter;
};
#endif

View file

@ -0,0 +1,115 @@
/*
* SPDX-FileCopyrightText: 2024 M5Stack Technology CO LTD
*
* SPDX-License-Identifier: MIT
*/
/*!
@file ui_UnitHEART.cpp
@brief UI for UnitHEART
*/
#include "ui_UnitHEART.hpp"
#include <M5Unified.h>
#include <M5Utility.h>
#include <cassert>
namespace {
constexpr int32_t GAP{2};
constexpr float COEFF{100.0f};
constexpr float COEFF_RECIPROCAL{1.0f / COEFF};
constexpr m5gfx::rgb565_t ir_gauge_color{161, 54, 54};
constexpr m5gfx::rgb565_t spo2_gauge_color{38, 41, 64};
constexpr int32_t min_spo2{90};
constexpr int32_t max_spo2{100};
constexpr char spO2ustr[] = "%";
} // namespace
void UnitHEARTSmallUI::construct()
{
auto& lcd = M5.Display;
_wid = lcd.width() >> 1;
_hgt = lcd.height() >> 1;
auto gw = _wid - GAP * 2;
auto gh = (_hgt >> 1) - (GAP * 2 + 16);
_irPlotter.reset(new m5::ui::Plotter(_parent, gw, gw, gh));
_irPlotter->setGaugeColor(ir_gauge_color);
_spO2Plotter.reset(new m5::ui::Plotter(_parent, gw, min_spo2 * COEFF, max_spo2 * COEFF, gw, gh, COEFF));
_spO2Plotter->setGaugeColor(spo2_gauge_color);
_spO2Plotter->setUnitString(spO2ustr);
_spO2Plotter->setGaugeTextDatum(textdatum_t::top_right);
}
void UnitHEARTSmallUI::push_back(const int32_t ir, const int32_t red)
{
_intermediateBuffer.emplace_back(Data{ir, red});
}
void UnitHEARTSmallUI::update()
{
if (_beatCounter > 0) {
--_beatCounter;
}
lock();
for (auto&& e : _intermediateBuffer) {
_monitor.push_back(e.ir, e.red);
_monitor.update();
beat(_monitor.isBeat());
_bpm = _monitor.bpm();
// _irPlotter->push_back(e.ir);
_irPlotter->push_back(_monitor.latestIR());
_spO2Plotter->push_back(_monitor.SpO2());
}
_intermediateBuffer.clear();
unlock();
_irPlotter->update();
_spO2Plotter->update();
}
void UnitHEARTSmallUI::push(LovyanGFX* dst, const int32_t x, const int32_t y)
{
auto f = dst->getFont();
dst->setFont(&fonts::Font0);
dst->setTextColor(TFT_WHITE);
auto td = dst->getTextDatum();
auto left = x;
auto right = x + _wid - 1;
auto top = y;
auto bottom = y + _hgt - 1;
auto w = right - left + 1;
auto h = bottom - top + 1;
// BG
dst->fillRoundRect(x, y, _wid, _hgt, GAP, TFT_YELLOW);
dst->fillRoundRect(x + GAP, y + GAP, _wid - GAP * 2, _hgt - GAP * 2, GAP, TFT_BLACK);
_irPlotter->push(dst, x + GAP, y + GAP);
_spO2Plotter->push(dst, x + GAP, y + _hgt - GAP - _spO2Plotter->height());
auto s = m5::utility::formatString("HR:%3dbpm", _bpm);
dst->drawString(s.c_str(), left + GAP * 2, top + GAP * 2 + _irPlotter->height());
dst->setTextDatum(textdatum_t::bottom_right);
s = m5::utility::formatString("SpO2:%3.2f%%", _monitor.SpO2());
dst->drawString(s.c_str(), right - GAP, bottom - _spO2Plotter->height() - GAP);
constexpr int32_t radius{4};
dst->fillCircle(right - radius * 2 - GAP, top + GAP * 2 + _irPlotter->height() + radius, radius,
_beatCounter > 0 ? TFT_RED : TFT_DARKGRAY);
dst->setFont(&fonts::Font2);
dst->setTextDatum(textdatum_t::middle_center);
dst->setTextColor(TFT_YELLOW);
dst->drawString("UnitHEART", left + w / 2, top + h / 2);
dst->setTextDatum(td);
dst->setFont(f);
}

View file

@ -0,0 +1,54 @@
/*
* SPDX-FileCopyrightText: 2024 M5Stack Technology CO LTD
*
* SPDX-License-Identifier: MIT
*/
/*!
@file ui_UnitHEART.hpp
@brief UI for UnitHEART
*/
#ifndef UI_UNIT_HEART_HPP
#define UI_UNIT_HEART_HPP
#include "parts/ui_plotter.hpp"
#include "ui_unitBase.hpp"
#include <M5UnitUnifiedHEART.h>
#include <memory>
class UnitHEARTSmallUI : public UnitUIBase {
public:
explicit UnitHEARTSmallUI(LovyanGFX* parent = nullptr) : UnitUIBase(parent)
{
}
inline m5::heart::PulseMonitor& monitor()
{
return _monitor;
}
inline void beat(bool beated)
{
if (beated) {
_beatCounter = 4;
}
}
void push_back(const int32_t ir, const int32_t red);
void construct() override;
void update() override;
void push(LovyanGFX* dst, const int32_t x, const int32_t y) override;
private:
std::unique_ptr<m5::ui::Plotter> _irPlotter{};
std::unique_ptr<m5::ui::Plotter> _spO2Plotter{};
m5::heart::PulseMonitor _monitor{100.0f};
int32_t _beatCounter{}, _bpm{};
struct Data {
int32_t ir, red;
};
std::vector<Data> _intermediateBuffer{};
};
#endif

View file

@ -0,0 +1,116 @@
/*
* SPDX-FileCopyrightText: 2024 M5Stack Technology CO LTD
*
* SPDX-License-Identifier: MIT
*/
/*!
@file ui_UnitTVOC.cpp
@brief UI for UnitTVOC
*/
#include "ui_UnitTVOC.hpp"
#include <M5Unified.h>
#include <cassert>
namespace {
const int32_t GAP{2};
constexpr m5gfx::rgb565_t co2_gauge_color{38, 41, 64};
constexpr m5gfx::rgb565_t tvoc_gauge_color{64, 48, 26};
constexpr char co2ustr[] = "ppm";
constexpr char tvocustr[] = "ppb";
constexpr std::initializer_list<m5::ui::ColorRange> tvocGauge = {
{220, m5gfx::rgb565_t(TFT_GREEN)}, {660, m5gfx::rgb565_t(TFT_GOLD)}, {1430, m5gfx::rgb565_t(TFT_ORANGE)},
{2000, m5gfx::rgb565_t(TFT_RED)}, {3300, m5gfx::rgb565_t(TFT_VIOLET)}, {5500, m5gfx::rgb565_t(TFT_PURPLE)},
};
} // namespace
void UnitTVOCSmallUI::construct()
{
auto& lcd = M5.Display;
_wid = lcd.width() >> 1;
_hgt = lcd.height() >> 1;
auto bw = _wid / 6 - GAP * 2;
auto bh = (_hgt >> 1) - GAP * 2 - 8;
auto pw = _wid / 6 * 5 - GAP * 2;
auto ph = (_hgt >> 1) - GAP * 2 - 8;
_co2Bar.reset(new m5::ui::BarMeterV(_parent, 400, 6000, bw, bh));
_tvocBar.reset(new m5::ui::ColorBarMeterV(_parent, 0, 5500, bw, bh, tvocGauge));
_co2Plotter.reset(new m5::ui::Plotter(_parent, pw, pw, ph));
_co2Plotter->setGaugeColor(co2_gauge_color);
_co2Plotter->setUnitString(co2ustr);
_co2Plotter->setGaugeTextDatum(textdatum_t::top_right);
_tvocPlotter.reset(new m5::ui::Plotter(_parent, pw, pw, ph));
_tvocPlotter->setGaugeColor(tvoc_gauge_color);
_tvocPlotter->setUnitString(tvocustr);
_intermediateBuffer.reserve(pw);
_intermediateBuffer.clear();
}
void UnitTVOCSmallUI::push_back(const int32_t co2, const int32_t tvoc)
{
_co2Bar->animate(co2, 10);
_tvocBar->animate(tvoc, 10);
_intermediateBuffer.emplace_back(Data{co2, tvoc});
}
void UnitTVOCSmallUI::update()
{
lock();
for (auto&& e : _intermediateBuffer) {
_co2Plotter->push_back(e.co2);
_tvocPlotter->push_back(e.tvoc);
}
_intermediateBuffer.clear();
unlock();
_co2Bar->update();
_tvocBar->update();
_co2Plotter->update();
_tvocPlotter->update();
}
void UnitTVOCSmallUI::push(LovyanGFX* dst, const int32_t x, const int32_t y)
{
auto left = x;
auto right = x + _wid - 1;
auto top = y;
auto bottom = y + _hgt - 1;
auto w = right - left + 1;
auto h = bottom - top + 1;
auto f = dst->getFont();
dst->setFont(&fonts::Font0);
dst->setTextColor(TFT_WHITE);
auto td = dst->getTextDatum();
// BG
dst->fillRoundRect(x, y, _wid, _hgt, GAP, TFT_BLUE);
dst->fillRoundRect(x + GAP, y + GAP, _wid - GAP * 2, _hgt - GAP * 2, GAP, TFT_BLACK);
_co2Bar->push(dst, left + GAP, y + GAP);
_co2Plotter->push(dst, right - _co2Plotter->width() - GAP, y + GAP);
_tvocPlotter->push(dst, left + GAP, bottom - _tvocPlotter->height() - GAP);
_tvocBar->push(dst, right - _tvocBar->width() - GAP, bottom - _tvocPlotter->height() - GAP);
dst->drawString("CO2eq", left + GAP * 3 + _co2Bar->width(), y + GAP);
dst->setTextDatum(textdatum_t::bottom_right);
dst->drawString("TVOC", right - (GAP * 2 + _tvocBar->width()), bottom);
dst->setFont(&fonts::Font2);
dst->setTextColor(TFT_BLUE);
dst->setTextDatum(textdatum_t::middle_center);
dst->drawString("UnitTVOC", x + w / 2, y + h / 2);
dst->setTextDatum(td);
dst->setFont(f);
}
// current valiue

View file

@ -0,0 +1,41 @@
/*
* SPDX-FileCopyrightText: 2024 M5Stack Technology CO LTD
*
* SPDX-License-Identifier: MIT
*/
/*!
@file ui_UnitTVOC.hpp
@brief UI for UnitTVOC
*/
#ifndef UI_UNIT_TVOC_HPP
#define UI_UNIT_TVOC_HPP
#include "parts/ui_bar_meter.hpp"
#include "parts/ui_plotter.hpp"
#include "ui_UnitBase.hpp"
#include <memory>
class UnitTVOCSmallUI : public UnitUIBase {
public:
explicit UnitTVOCSmallUI(LovyanGFX* parent) : UnitUIBase(parent)
{
}
void push_back(const int32_t co2, const int32_t tvoc);
void construct() override;
void update() override;
void push(LovyanGFX* dst, const int32_t x, const int32_t y) override;
private:
std::unique_ptr<m5::ui::Plotter> _co2Plotter{};
std::unique_ptr<m5::ui::BarMeterV> _co2Bar{};
std::unique_ptr<m5::ui::Plotter> _tvocPlotter{};
std::unique_ptr<m5::ui::ColorBarMeterV> _tvocBar{};
struct Data {
int32_t co2, tvoc;
};
std::vector<Data> _intermediateBuffer{};
};
#endif

View file

@ -0,0 +1,88 @@
/*
* SPDX-FileCopyrightText: 2024 M5Stack Technology CO LTD
*
* SPDX-License-Identifier: MIT
*/
/*!
@file ui_UnitVmeter.cpp
@brief UI for UnitVmeter
*/
#include "ui_UnitVmeter.hpp"
#include <M5Unified.h>
#include <M5Utility.h>
#include <cassert>
namespace {
constexpr int32_t GAP{2};
constexpr m5gfx::rgb565_t voltage_gauge_color{129, 134, 80};
constexpr char vustr[] = "mV";
} // namespace
void UnitVmeterSmallUI::construct()
{
auto& lcd = M5.Display;
_wid = lcd.width() >> 1;
_hgt = lcd.height() >> 1;
auto pw = _wid - GAP * 2;
// auto ph = (_hgt >> 2) * 3 - GAP * 2;
auto ph = (_hgt >> 1) - GAP * 2 - 4;
_voltagePlotter.reset(new m5::ui::Plotter(_parent, pw, pw, ph));
_voltagePlotter->setUnitString(vustr);
_voltagePlotter->setGaugeColor(voltage_gauge_color);
_intermediateBuffer.reserve(pw);
_intermediateBuffer.clear();
}
void UnitVmeterSmallUI::push_back(const float mv)
{
_intermediateBuffer.emplace_back(mv);
}
void UnitVmeterSmallUI::update()
{
lock();
for (auto&& e : _intermediateBuffer) {
_voltagePlotter->push_back(e);
}
_intermediateBuffer.clear();
unlock();
_voltagePlotter->update();
}
void UnitVmeterSmallUI::push(LovyanGFX* dst, const int32_t x, const int32_t y)
{
auto left = x;
auto right = x + _wid - 1;
auto top = y;
auto bottom = y + _hgt - 1;
auto w = right - left + 1;
auto h = bottom - top + 1;
auto f = dst->getFont();
dst->setFont(&fonts::Font0);
dst->setTextColor(TFT_WHITE);
auto td = dst->getTextDatum();
// BG
dst->fillRoundRect(x, y, _wid, _hgt, GAP, TFT_RED);
dst->fillRoundRect(x + GAP, y + GAP, _wid - GAP * 2, _hgt - GAP * 2, GAP, TFT_BLACK);
_voltagePlotter->push(dst, x + GAP, y + GAP);
auto s = m5::utility::formatString("MIN:%5dmV", _voltagePlotter->minimum());
dst->drawString(s.c_str(), x + GAP * 2, y + GAP * 2 + _voltagePlotter->height() + 16);
s = m5::utility::formatString("MAX:%5dmV", _voltagePlotter->maximum());
dst->drawString(s.c_str(), x + GAP * 2, y + GAP * 2 + _voltagePlotter->height() + 16 + 10 * 1);
dst->setFont(&fonts::Font2);
dst->setTextColor(TFT_RED);
dst->setTextDatum(textdatum_t::middle_center);
dst->drawString("UnitVmeter", left + w / 2, top + h / 2);
dst->setTextDatum(td);
dst->setFont(f);
}

View file

@ -0,0 +1,34 @@
/*
* SPDX-FileCopyrightText: 2024 M5Stack Technology CO LTD
*
* SPDX-License-Identifier: MIT
*/
/*!
@file ui_UnitVmeter.hpp
@brief UI for UnitVmeter
*/
#ifndef UI_UNIT_VMETER_HPP
#define UI_UNIT_VMETER_HPP
#include "parts/ui_plotter.hpp"
#include "ui_UnitBase.hpp"
#include <memory>
class UnitVmeterSmallUI : public UnitUIBase {
public:
explicit UnitVmeterSmallUI(LovyanGFX* parent) : UnitUIBase(parent)
{
}
void push_back(const float mv);
void construct() override;
void update() override;
void push(LovyanGFX* dst, const int32_t x, const int32_t y) override;
private:
std::unique_ptr<m5::ui::Plotter> _voltagePlotter{};
std::vector<float> _intermediateBuffer{};
};
#endif