Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions main/apps/app_setup/app_setup.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,11 @@ void AppSetup::onOpen()
_destroy_menu = true;
_worker = std::make_unique<ButtonWorker>();
}},
{"Wi-Fi",
[&]() {
_destroy_menu = true;
_worker = std::make_unique<WifiWorker>();
}},
},
},
{
Expand Down Expand Up @@ -114,7 +119,12 @@ void AppSetup::onRunning()
if (_worker) {
_worker->update();
if (_worker->isDone()) {
const bool exit_app = _worker->shouldExitApp();
_worker.reset();
if (exit_app) {
close();
return;
}
_menu_page = std::make_unique<view::SelectMenuPage>(_menu_sections);
}
}
Expand Down
118 changes: 118 additions & 0 deletions main/apps/app_setup/workers/wifi.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
/*
* SPDX-FileCopyrightText: 2026 M5Stack Technology CO LTD
*
* SPDX-License-Identifier: MIT
*/
#include "workers.h"
#include <assets/assets.h>
#include <mooncake_log.h>
#include <ssid_manager.h>
#include <wifi_manager.h>

using namespace uitk;
using namespace uitk::lvgl_cpp;
using namespace setup_workers;

class WifiWorker::WifiView {
public:
WifiView()
{
_panel = std::make_unique<Container>(lv_screen_active());
_panel->setSize(466, 466);
_panel->setAlign(LV_ALIGN_CENTER);
_panel->setRadius(0);
_panel->setBorderWidth(0);
_panel->setPadding(42, 92, 42, 42);
_panel->setBgColor(lv_color_black());
_panel->removeFlag(LV_OBJ_FLAG_SCROLLABLE);

_title = std::make_unique<Label>(_panel->get());
_title->setTextFont(&MontserratSemiBold26);
_title->setTextColor(lv_color_hex(0xFFFFFF));
_title->align(LV_ALIGN_TOP_MID, 0, 0);
_title->setText("Wi-Fi");

_status = std::make_unique<Label>(_panel->get());
_status->setTextFont(&lv_font_montserrat_18);
_status->setTextColor(lv_color_hex(0xFFFFFF));
_status->align(LV_ALIGN_TOP_MID, 0, 72);
_status->setWidth(380);
_status->setTextAlign(LV_TEXT_ALIGN_CENTER);

_detail = std::make_unique<Label>(_panel->get());
_detail->setTextFont(&lv_font_montserrat_14);
_detail->setTextColor(lv_color_hex(0xBFBFBF));
_detail->align(LV_ALIGN_TOP_MID, 0, 150);
_detail->setWidth(380);
_detail->setTextAlign(LV_TEXT_ALIGN_CENTER);
}

void setText(const std::string& status, const std::string& detail)
{
_status->setText(status);
_detail->setText(detail);
}

private:
std::unique_ptr<Container> _panel;
std::unique_ptr<Label> _title;
std::unique_ptr<Label> _status;
std::unique_ptr<Label> _detail;
};

WifiWorker::WifiWorker()
{
auto& wifi = WifiManager::GetInstance();
_restore_station = !SsidManager::GetInstance().GetSsidList().empty();

if (_restore_station || !wifi.IsConfigMode()) {
wifi.StartConfigAp();
}

_view = std::make_unique<WifiView>();
_next_update_tick = 0;
}

WifiWorker::~WifiWorker()
{
auto& wifi = WifiManager::GetInstance();
if (wifi.IsConfigMode() && _restore_station) {
wifi.StopConfigAp();
wifi.StartStation();
}
}

void WifiWorker::update()
{
const auto now = GetHAL().millis();
if (now < _next_update_tick || !_view) {
return;
}
_next_update_tick = now + 500;

auto& wifi = WifiManager::GetInstance();
const bool has_saved_wifi = !SsidManager::GetInstance().GetSsidList().empty();

// The portal exits only after the H5 form has been submitted successfully.
// Once the saved network is connected, return all the way to Launcher.
if (!_config_exit_seen && !wifi.IsConfigMode() && has_saved_wifi) {
_config_exit_seen = true;
}
if (_config_exit_seen && wifi.IsConnected()) {
_exit_to_launcher = true;
_is_done = true;
return;
}

if (wifi.IsConfigMode()) {
_view->setText(
"Connect phone to device Wi-Fi",
fmt::format("{}\nOpen http://192.168.4.1\nThe phone may open this page automatically.", wifi.GetApSsid()));
} else if (wifi.IsConnected()) {
_view->setText("Wi-Fi connected", fmt::format("{}\nIP: {}", wifi.GetSsid(), wifi.GetIpAddress()));
} else if (has_saved_wifi) {
_view->setText("Connecting...", "Saved Wi-Fi is being retried.");
} else {
_view->setText("Wi-Fi setup", "Connect to the device Wi-Fi to configure it.");
}
}
25 changes: 25 additions & 0 deletions main/apps/app_setup/workers/workers.h
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,11 @@ class WorkerBase {
return _is_done;
}

virtual bool shouldExitApp() const
{
return false;
}

protected:
bool _is_done = false;
};
Expand Down Expand Up @@ -138,4 +143,24 @@ class AboutWorker : public WorkerBase {
int _pending_burst_steps = 0;
};

class WifiWorker : public WorkerBase {
public:
WifiWorker();
~WifiWorker();
void update() override;
bool shouldExitApp() const override
{
return _exit_to_launcher;
}

private:
class WifiView;

std::unique_ptr<WifiView> _view;
uint32_t _next_update_tick = 0;
bool _restore_station = false;
bool _config_exit_seen = false;
bool _exit_to_launcher = false;
};

} // namespace setup_workers
163 changes: 150 additions & 13 deletions main/apps/common/arc_top_clock/arc_top_clock.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
#include <assets/assets.h>
#include <mooncake_log.h>
#include <hal/hal.h>
#include <wifi_manager.h>
#include <string>
#include <ctime>
#include <cmath>
Expand All @@ -15,28 +16,68 @@ using namespace uitk;
using namespace uitk::lvgl_cpp;
using namespace view;

static const std::vector<int> _label_rotation_map = {-100, -46, 0, 32, 84};
static const std::vector<int> _label_x_map = {3, 3, 1, 4, 4};
static const std::vector<int> _label_y_map = {0, 0, -2, -1, -1};
// All top indicators share the same circular geometry, based on the centre
// of the 466 x 466 AMOLED. Angles start at 12 o'clock and increase clockwise.
static constexpr float _screen_center = 233.0f;
static constexpr float _clock_radius = 200.0f;
// Balance the actual widths of MM/DD, HH:MM and the Wi-Fi glyph as one
// header group. The time is intentionally offset a little right so the
// complete three-part composition is centred at 12 o'clock.
static constexpr float _date_start_angle = -34.0f;
static constexpr float _clock_start_angle = -3.0f;
static constexpr float _clock_angle_step = 5.0f;
static constexpr float _wifi_angle = 31.5f;
// The connection point is 10 px inward from the canvas pivot. Keeping the
// pivot at 210 therefore puts that bottom point on the time's 200 px arc.
static constexpr float _wifi_radius = 210.0f;
static constexpr int _wifi_icon_width = 42;
static constexpr int _wifi_icon_height = 38;

static float degrees_to_radians(float degrees)
{
return degrees * static_cast<float>(M_PI) / 180.0f;
}

void ArcTopClock::init()
{
setSize(114, 36);
// This is only a lifecycle owner. The clock glyphs themselves live on
// the top layer so they can be positioned directly on the circular edge.
setSize(1, 1);
setBorderWidth(0);
setOutlineWidth(0);
setPaddingAll(0);
setBgOpa(LV_OPA_TRANSP);
removeFlag(LV_OBJ_FLAG_SCROLLABLE);

for (int i = 0; i < labels.size(); i++) {
labels[i] = std::make_unique<Label>(get());
labels[i]->setText(".");
labels[i]->setTextFont(&lv_font_maple_mono_medium_28);
labels[i]->setTextColor(lv_color_hex(color));
labels[i]->setTransformPivot(0, displayRadius);
labels[i]->align(LV_ALIGN_CENTER, _label_x_map[i], _label_y_map[i]);
labels[i]->setRotation(_label_rotation_map[i]);
}
auto init_labels = [this](auto& group) {
for (auto& label : group) {
label = std::make_unique<Label>(lv_layer_top());
label->setText(".");
label->setTextFont(&lv_font_maple_mono_medium_28);
label->setTextColor(lv_color_hex(color));
label->setBgOpa(LV_OPA_TRANSP);
label->setBorderWidth(0);
}
};
init_labels(date_labels);
init_labels(labels);

// Draw the complete indicator in one canvas. This avoids the unreliable
// layout of tiny nested LVGL objects on this display.
wifi_icon = std::make_unique<Canvas>(lv_layer_top());
wifi_icon->setSize(_wifi_icon_width, _wifi_icon_height);
wifi_icon->setBgOpa(LV_OPA_TRANSP);
wifi_icon->setBorderWidth(0);
wifi_icon->removeFlag(LV_OBJ_FLAG_SCROLLABLE);
wifi_icon->addFlag(LV_OBJ_FLAG_FLOATING);
wifi_icon->createBuffer(_wifi_icon_width, _wifi_icon_height, LV_COLOR_FORMAT_ARGB8888);

const float wifi_radians = degrees_to_radians(_wifi_angle);
const int wifi_center_x = std::lround(_screen_center + _wifi_radius * std::sin(wifi_radians));
const int wifi_center_y = std::lround(_screen_center - _wifi_radius * std::cos(wifi_radians));
wifi_icon->setPos(wifi_center_x - _wifi_icon_width / 2, wifi_center_y - _wifi_icon_height / 2);
wifi_icon->setTransformPivot(_wifi_icon_width / 2, _wifi_icon_height / 2);
wifi_icon->setRotation(std::lround(_wifi_angle * 10.0f));

update(true);
}
Expand All @@ -46,12 +87,63 @@ void ArcTopClock::update(bool force)
if (force || GetHAL().millis() - update_time_count > updateInterval) {
std::time_t now = std::time(nullptr);
std::tm* localTime = std::localtime(&now);
set_date_to(fmt::format("{:02d}/{:02d}", localTime->tm_mon + 1, localTime->tm_mday));
set_clock_to(fmt::format("{:02d}:{:02d}", localTime->tm_hour, localTime->tm_min));
// Text and font changes invalidate LVGL's content-sized labels. Flush
// that deferred layout before reading their dimensions, otherwise the
// first frame after returning to Launcher uses the old placeholder
// size and visibly jumps on the next one-second update.
lv_obj_update_layout(lv_layer_top());
layout_date();
layout_clock();
update_wifi_indicator();
// set_clock_to("00:00");
update_time_count = GetHAL().millis();
}
}

void ArcTopClock::update_wifi_indicator()
{
auto& wifi = WifiManager::GetInstance();

int level = 0;
if (wifi.IsConnected()) {
const int rssi = wifi.GetRssi();
level = rssi >= -55 ? 4 : rssi >= -67 ? 3 : rssi >= -75 ? 2 : rssi >= -85 ? 1 : 0;
}

draw_wifi_indicator(level);
}

void ArcTopClock::draw_wifi_indicator(int level)
{
// Standard Wi-Fi glyph: a connection point and three nested arches.
// RSSI controls the number of white arches; inactive arches remain dim.
wifi_icon->fillBg(lv_color_black(), LV_OPA_TRANSP);
wifi_icon->startDrawing();

constexpr int center_x = _wifi_icon_width / 2;
constexpr int center_y = 29;
// Keep a clear gap between the connection dot and the inner arch while
// matching the thin stroke weight of the clock text.
constexpr int radii[] = {10, 15, 20};
constexpr int widths[] = {3, 3, 3};

for (int i = 0; i < 3; ++i) {
const bool active = level >= (i + 2);
const auto arc_color = lv_color_hex(active ? 0xFFFFFF : 0x5D6875);
const auto arc_opacity = active ? LV_OPA_COVER : LV_OPA_50;
// 225° to 315° traces a top-facing arch in LVGL's clockwise system.
wifi_icon->drawArc(center_x, center_y, radii[i], 225, 315, arc_color, arc_opacity, widths[i], true);
}

const auto dot_color = lv_color_hex(level > 0 ? 0xFFFFFF : 0x5D6875);
wifi_icon->drawCircle(center_x, center_y, 3, dot_color, level > 0 ? LV_OPA_COVER : LV_OPA_50);

wifi_icon->finishDrawing();
lv_obj_invalidate(wifi_icon->get());
}

void ArcTopClock::set_clock_to(const std::string_view text)
{
const int count = std::min((int)text.size(), (int)labels.size());
Expand All @@ -64,3 +156,48 @@ void ArcTopClock::set_clock_to(const std::string_view text)
}
}
}

void ArcTopClock::set_date_to(const std::string_view text)
{
const int count = std::min((int)text.size(), (int)date_labels.size());
for (int i = 0; i < count; i++) {
if (text[i] == '0') {
date_labels[i]->setText("O");
} else {
char buf[2] = {text[i], '\0'};
date_labels[i]->setText(buf);
}
}
}

void ArcTopClock::layout_date()
{
for (int i = 0; i < date_labels.size(); ++i) {
const float angle = _date_start_angle + _clock_angle_step * i;
const float radians = degrees_to_radians(angle);
const int center_x = std::lround(_screen_center + _clock_radius * std::sin(radians));
const int center_y = std::lround(_screen_center - _clock_radius * std::cos(radians));
const int label_w = lv_obj_get_width(date_labels[i]->get());
const int label_h = lv_obj_get_height(date_labels[i]->get());

date_labels[i]->setTransformPivot(label_w / 2, label_h / 2);
date_labels[i]->setRotation(std::lround(angle * 10.0f));
date_labels[i]->setPos(center_x - label_w / 2, center_y - label_h / 2);
}
}

void ArcTopClock::layout_clock()
{
for (int i = 0; i < labels.size(); ++i) {
const float angle = _clock_start_angle + _clock_angle_step * i;
const float radians = degrees_to_radians(angle);
const int center_x = std::lround(_screen_center + _clock_radius * std::sin(radians));
const int center_y = std::lround(_screen_center - _clock_radius * std::cos(radians));
const int label_w = lv_obj_get_width(labels[i]->get());
const int label_h = lv_obj_get_height(labels[i]->get());

labels[i]->setTransformPivot(label_w / 2, label_h / 2);
labels[i]->setRotation(std::lround(angle * 10.0f));
labels[i]->setPos(center_x - label_w / 2, center_y - label_h / 2);
}
}
Loading