This commit is contained in:
Felipe Martínez 2024-10-02 17:47:58 +00:00 committed by GitHub
commit 52061ad50f
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
8 changed files with 894 additions and 2 deletions

View file

@ -397,6 +397,7 @@ list(APPEND SOURCE_FILES
displayapp/screens/Alarm.cpp
displayapp/screens/Styles.cpp
displayapp/screens/WeatherSymbols.cpp
displayapp/screens/ASM.cpp
displayapp/Colors.cpp
displayapp/widgets/Counter.cpp
displayapp/widgets/PageIndicator.cpp

View file

@ -70,7 +70,11 @@ int FS::FileWrite(lfs_file_t* file_p, const uint8_t* buff, uint32_t size) {
}
int FS::FileSeek(lfs_file_t* file_p, uint32_t pos) {
return lfs_file_seek(&lfs, file_p, pos, LFS_SEEK_SET);
return FileSeek(file_p, pos, LFS_SEEK_SET);
}
int FS::FileSeek(lfs_file_t* file_p, uint32_t pos, int whence) {
return lfs_file_seek(&lfs, file_p, pos, whence);
}
int FS::FileDelete(const char* fileName) {

View file

@ -17,6 +17,7 @@ namespace Pinetime {
int FileRead(lfs_file_t* file_p, uint8_t* buff, uint32_t size);
int FileWrite(lfs_file_t* file_p, const uint8_t* buff, uint32_t size);
int FileSeek(lfs_file_t* file_p, uint32_t pos);
int FileSeek(lfs_file_t* file_p, uint32_t pos, int whence);
int FileDelete(const char* fileName);

View file

@ -2,6 +2,7 @@
#include "displayapp/apps/Apps.h"
#include "Controllers.h"
#include "displayapp/screens/ASM.h"
#include "displayapp/screens/Alarm.h"
#include "displayapp/screens/Dice.h"
#include "displayapp/screens/Timer.h"

View file

@ -42,7 +42,8 @@ namespace Pinetime {
SettingChimes,
SettingShakeThreshold,
SettingBluetooth,
Error
Error,
ASM
};
enum class WatchFace : uint8_t {

View file

@ -2,6 +2,7 @@ if(DEFINED ENABLE_USERAPPS)
set(USERAPP_TYPES ${ENABLE_USERAPPS} CACHE STRING "List of user apps to build into the firmware")
else ()
set(DEFAULT_USER_APP_TYPES "Apps::StopWatch")
set(DEFAULT_USER_APP_TYPES "${DEFAULT_USER_APP_TYPES}, Apps::ASM")
set(DEFAULT_USER_APP_TYPES "${DEFAULT_USER_APP_TYPES}, Apps::Alarm")
set(DEFAULT_USER_APP_TYPES "${DEFAULT_USER_APP_TYPES}, Apps::Timer")
set(DEFAULT_USER_APP_TYPES "${DEFAULT_USER_APP_TYPES}, Apps::Steps")

View file

@ -0,0 +1,564 @@
#include "ASM.h"
#include <libraries/log/nrf_log.h>
#include <assert.h>
#include <cstdio>
using namespace Pinetime::Applications::Screens;
constexpr lv_font_t* fonts[] = {
&fontawesome_weathericons,
&jetbrains_mono_42,
&jetbrains_mono_76,
&jetbrains_mono_bold_20,
&jetbrains_mono_extrabold_compressed,
&lv_font_sys_48,
&open_sans_light,
};
constexpr int num_fonts = sizeof(fonts) / sizeof(fonts[0]);
constexpr uint32_t handler_return_pc_mark = 1 << 31;
struct CallbackInfo {
ASM* instance;
uint32_t callback_pc;
};
static void event_handler(lv_obj_t* obj, lv_event_t event) {
CallbackInfo* cbInfo = static_cast<CallbackInfo*>(lv_obj_get_user_data(obj));
cbInfo->instance->OnObjectEvent(obj, event);
}
ASM::ASM(Controllers::DateTime& dateTimeController,
const Controllers::Battery& batteryController,
const Controllers::Ble& bleController,
Controllers::FS& fsController)
: dateTimeController(dateTimeController), batteryController(batteryController), bleController(bleController), fs(fsController) {
int result = fsController.FileOpen(&file, "program.bin", LFS_O_RDONLY);
asm_assert(result >= 0);
result = fsController.FileSeek(&file, 0, LFS_SEEK_END);
asm_assert(result >= 0);
program_size = result;
fsController.FileSeek(&file, 0, LFS_SEEK_SET);
populate_cache(0);
Refresh();
}
ASM::~ASM() {
if (taskRefresh != nullptr) {
lv_task_del(taskRefresh);
}
if (statusIcons) {
lv_obj_del(statusIcons->GetObject());
}
fs.FileClose(&file);
// We don't need to clean the screen since all objects are deleted when their shared_ptr is dropped
// lv_obj_clean(lv_scr_act());
}
void ASM::populate_cache(size_t pos) {
int result = fs.FileSeek(&file, pos);
asm_assert(result >= 0);
result = fs.FileRead(&file, cache, cache_size);
asm_assert(result >= 0);
cache_start = pos;
}
void ASM::run() {
bool stop = false;
while (!stop && pc < program_size) {
OpcodeShort opcode = static_cast<OpcodeShort>(read_byte(pc));
if (static_cast<uint8_t>(opcode) & (1 << 7)) {
// Long opcode
OpcodeLong opcode = static_cast<OpcodeLong>(read_u16(pc));
pc += 2;
switch (opcode) {
default:
NRF_LOG_ERROR("Unknown opcode: 0x%04X", opcode);
break;
}
} else {
pc++;
switch (opcode) {
case OpcodeShort::WaitRefresh:
stop = true;
break;
case OpcodeShort::Push0:
push(std::make_shared<ValueInteger>(0));
break;
case OpcodeShort::PushU8:
push(std::make_shared<ValueInteger>(read_byte(pc)));
pc++;
break;
case OpcodeShort::PushU16:
push(std::make_shared<ValueInteger>(read_u16(pc)));
pc += 2;
break;
case OpcodeShort::PushU24:
push(std::make_shared<ValueInteger>(read_u24(pc)));
pc += 3;
break;
case OpcodeShort::PushU32:
push(std::make_shared<ValueInteger>(read_u32(pc)));
pc += 4;
break;
case OpcodeShort::PushEmptyString:
push(std::make_shared<ValueString>(new char[1] {0}, 1));
break;
case OpcodeShort::Duplicate:
push(stack[stack_pointer - 1]);
break;
case OpcodeShort::Pop:
pop();
break;
case OpcodeShort::LoadString: {
uint32_t ptr = pop_uint32();
int length = read_byte(ptr);
char* text = new char[length + 1];
text[length] = '\0';
for (int i = 0; i < length; i++) {
text[i] = read_byte(ptr + 1 + i);
}
push(std::make_shared<ValueString>(text, length + 1));
break;
}
case OpcodeShort::StoreLocal:
locals[read_byte(pc++)] = pop();
break;
case OpcodeShort::LoadLocal:
push(locals[read_byte(pc++)]);
break;
case OpcodeShort::Branch: {
if (doBranch(pop_uint32()))
stop = true;
break;
}
case OpcodeShort::BranchIfTrue: {
uint32_t to = pop_uint32();
auto cond = pop();
if (cond->isTruthy() && doBranch(to))
stop = true;
break;
}
case OpcodeShort::Call: {
uint32_t next = pc;
pc = pop_uint32();
push(std::make_shared<ValueInteger>(next));
break;
}
case OpcodeShort::StartPeriodicRefresh:
if (taskRefresh == nullptr) {
taskRefresh = lv_task_create(RefreshTaskCallback, LV_DISP_DEF_REFR_PERIOD, LV_TASK_PRIO_MID, this);
}
break;
case OpcodeShort::StopPeriodicRefresh:
if (taskRefresh != nullptr) {
lv_task_del(taskRefresh);
taskRefresh = nullptr;
}
break;
case OpcodeShort::SetLabelText: {
auto str = pop<ValueString>(String);
auto obj = pop<ValueLvglObject>(LvglObject);
lv_label_set_text(obj->obj, str->str);
push(obj);
break;
}
case OpcodeShort::SetArcRange: {
uint32_t max = pop_uint32();
uint32_t min = pop_uint32();
auto obj = pop<ValueLvglObject>(LvglObject);
lv_arc_set_range(obj->obj, min, max);
push(obj);
break;
}
case OpcodeShort::SetArcRotation: {
uint32_t rot = pop_uint32();
auto obj = pop<ValueLvglObject>(LvglObject);
lv_arc_set_rotation(obj->obj, rot);
push(obj);
break;
}
case OpcodeShort::SetArcBgAngles: {
uint32_t end = pop_uint32();
uint32_t start = pop_uint32();
auto obj = pop<ValueLvglObject>(LvglObject);
lv_arc_set_bg_angles(obj->obj, start, end);
push(obj);
break;
}
case OpcodeShort::SetArcAdjustable: {
auto val = pop();
auto obj = pop<ValueLvglObject>(LvglObject);
lv_arc_set_adjustable(obj->obj, val->isTruthy());
push(obj);
break;
}
case OpcodeShort::SetArcStartAngle: {
uint32_t angle = pop_uint32();
auto obj = pop<ValueLvglObject>(LvglObject);
lv_arc_set_start_angle(obj->obj, angle);
push(obj);
break;
}
case OpcodeShort::SetArcValue: {
uint32_t value = pop_uint32();
auto obj = pop<ValueLvglObject>(LvglObject);
lv_arc_set_value(obj->obj, value);
push(obj);
break;
}
case OpcodeShort::CreateLabel:
push(std::make_shared<ValueLvglObject>(lv_label_create(lv_scr_act(), NULL)));
break;
case OpcodeShort::CreateButton:
push(std::make_shared<ValueLvglObject>(lv_btn_create(lv_scr_act(), NULL)));
break;
case OpcodeShort::CreateArc:
push(std::make_shared<ValueLvglObject>(lv_arc_create(lv_scr_act(), NULL)));
break;
case OpcodeShort::SetObjectAlign: {
int16_t y = pop_uint32();
int16_t x = pop_uint32();
uint8_t align = pop_uint32();
auto obj = pop<ValueLvglObject>(LvglObject);
lv_obj_align(obj->obj, lv_scr_act(), align, x, y);
push(obj);
break;
}
case OpcodeShort::SetObjectSize: {
int16_t h = pop_uint32();
int16_t w = pop_uint32();
auto obj = pop<ValueLvglObject>(LvglObject);
lv_obj_set_size(obj->obj, w, h);
push(obj);
break;
}
case OpcodeShort::SetObjectParent: {
auto parent = pop<ValueLvglObject>(LvglObject);
auto child = pop<ValueLvglObject>(LvglObject);
lv_obj_set_parent(child->obj, parent->obj);
push(child);
break;
}
case OpcodeShort::SetStyleLocalInt:
case OpcodeShort::SetStyleLocalFont:
case OpcodeShort::SetStyleLocalColor: {
uint32_t value = pop_uint32();
uint32_t prop = pop_uint32();
uint32_t part = pop_uint32();
auto obj = pop<ValueLvglObject>(LvglObject);
switch (opcode) {
case OpcodeShort::SetStyleLocalInt:
_lv_obj_set_style_local_int(obj->obj, part, prop, value);
break;
case OpcodeShort::SetStyleLocalColor:
_lv_obj_set_style_local_color(obj->obj, part, prop, lv_color_hex(value));
break;
case OpcodeShort::SetStyleLocalFont: {
if (value < num_fonts) {
_lv_obj_set_style_local_ptr(obj->obj, part, prop, fonts[value]);
}
break;
}
default:
break;
}
push(obj);
break;
}
case OpcodeShort::SetEventHandler: {
uint32_t cb_pc = pop_uint32();
auto obj = pop<ValueLvglObject>(LvglObject);
CallbackInfo* cb = new CallbackInfo {this, cb_pc};
lv_obj_set_user_data(obj->obj, cb);
lv_obj_set_event_cb(obj->obj, event_handler);
push(obj);
break;
}
case OpcodeShort::Add:
push(std::make_shared<ValueInteger>(pop_uint32() + pop_uint32()));
break;
case OpcodeShort::Subtract: {
uint32_t b = pop_uint32();
uint32_t a = pop_uint32();
push(std::make_shared<ValueInteger>(a - b));
break;
}
case OpcodeShort::Multiply:
push(std::make_shared<ValueInteger>(pop_uint32() * pop_uint32()));
break;
case OpcodeShort::Divide: {
uint32_t b = pop_uint32();
uint32_t a = pop_uint32();
push(std::make_shared<ValueInteger>(a / b));
break;
}
case OpcodeShort::GrowString: {
auto len = pop_uint32();
auto str = pop<ValueString>(String);
size_t new_cap = len + str->capacity;
char* new_str = new char[new_cap];
memcpy(new_str, str->str, str->capacity);
push(std::make_shared<ValueString>(new_str, new_cap));
break;
}
case OpcodeShort::ClearString: {
auto str = pop<ValueString>(String);
if (str->capacity > 0)
str->str[0] = '\0';
push(str);
break;
}
case OpcodeShort::Concat: {
auto b = pop();
auto a = pop();
if (a->type() == String && b->type() == String) {
auto aString = static_cast<ValueString*>(a.get());
auto bString = static_cast<ValueString*>(b.get());
int len_a = strlen(aString->str);
int len_b = strlen(bString->str);
size_t new_len = len_a + len_b + 1;
if (aString->capacity >= new_len) {
strcat(aString->str, bString->str);
push(a);
} else {
char* s = new char[new_len + 1];
strcpy(s, aString->str);
strcat(s, bString->str);
push(std::make_shared<ValueString>(s, new_len + 1));
}
} else if (a->type() == String && b->type() == Integer) {
auto aString = static_cast<ValueString*>(a.get());
auto bInt = static_cast<ValueInteger*>(b.get());
size_t aLen = strlen(aString->str);
size_t need_cap = aLen + 12 + 1;
if (aString->capacity - aLen >= need_cap) {
snprintf(aString->str + aLen, aString->capacity - aLen, "%lu", bInt->i);
push(a);
} else {
char* s = new char[need_cap];
memcpy(s, aString->str, aLen);
snprintf(s + aLen, need_cap - aLen, "%lu", bInt->i);
push(std::make_shared<ValueString>(s, need_cap));
}
} else {
asm_assert(false);
}
break;
}
case OpcodeShort::PushCurrentDateTime: {
auto time = dateTimeController.CurrentDateTime();
std::tm tm {
.tm_sec = dateTimeController.Seconds(),
.tm_min = dateTimeController.Minutes(),
.tm_hour = dateTimeController.Hours(),
.tm_mday = dateTimeController.Day(),
.tm_mon = static_cast<int>(dateTimeController.Month()) - 1,
.tm_year = dateTimeController.Year() - 1900,
.tm_wday = static_cast<int>(dateTimeController.DayOfWeek()),
.tm_yday = dateTimeController.DayOfYear() - 1,
};
push(std::make_shared<ValueDateTime>(time, tm));
break;
}
case OpcodeShort::PushCurrentTicks:
push(std::make_shared<ValueInteger>((xTaskGetTickCount() * configTICK_RATE_HZ) / 1000));
break;
case OpcodeShort::FormatDateTime: {
auto fmt = pop<ValueString>(String);
auto time = pop<ValueDateTime>(DateTime);
constexpr int max_len = 16;
char* str = new char[max_len]; // TODO: Allow user to reuse string in stack
strftime(str, max_len, fmt->str, &time->tm);
push(std::make_shared<ValueString>(str, max_len));
break;
}
case OpcodeShort::RealignObject:
lv_obj_realign(pop<ValueLvglObject>(LvglObject)->obj);
break;
case OpcodeShort::ShowStatusIcons:
if (!statusIcons) {
statusIcons = std::make_unique<Widgets::StatusIcons>(batteryController, bleController);
statusIcons->Create();
}
break;
case OpcodeShort::Equals: {
auto b = pop();
auto a = pop();
push(std::make_shared<ValueInteger>(a.get()->compare(b.get()) == 0 ? 1 : 0));
break;
}
case OpcodeShort::Greater: {
auto b = pop();
auto a = pop();
push(std::make_shared<ValueInteger>(a.get()->compare(b.get()) > 0 ? 1 : 0));
break;
}
case OpcodeShort::Lesser: {
auto b = pop();
auto a = pop();
push(std::make_shared<ValueInteger>(a.get()->compare(b.get()) < 0 ? 1 : 0));
break;
}
case OpcodeShort::Negate:
push(std::make_shared<ValueInteger>(pop().get()->isTruthy() ? 0 : 1));
break;
case OpcodeShort::GetDateTimeHour: {
auto time = pop<ValueDateTime>(DateTime);
push(std::make_shared<ValueInteger>(time->tm.tm_hour));
break;
}
case OpcodeShort::GetDateTimeMinute: {
auto time = pop<ValueDateTime>(DateTime);
push(std::make_shared<ValueInteger>(time->tm.tm_min));
break;
}
case OpcodeShort::GetDateTimeSecond: {
auto time = pop<ValueDateTime>(DateTime);
push(std::make_shared<ValueInteger>(time->tm.tm_sec));
break;
}
default:
NRF_LOG_ERROR("Unknown opcode: 0x%02X", opcode);
break;
}
}
}
}
void ASM::Refresh() {
run();
if (statusIcons) {
statusIcons->Update();
}
}
void ASM::_asm_assert(bool condition, const char* msg) {
if (!condition) {
// TODO: Handle better
if (msg)
NRF_LOG_ERROR("Assertion failed: %s", msg);
for (;;) {
}
}
}
bool ASM::doBranch(uint32_t to) {
if ((to & handler_return_pc_mark) != 0) {
pc = to & ~handler_return_pc_mark;
return true;
}
pc = to;
return false;
}
void ASM::OnObjectEvent(lv_obj_t* obj, lv_event_t event) {
if (event != LV_EVENT_CLICKED)
return;
CallbackInfo* cb = static_cast<CallbackInfo*>(lv_obj_get_user_data(obj));
if (cb) {
push(std::make_shared<ValueInteger>(pc | handler_return_pc_mark));
pc = cb->callback_pc;
run();
}
}

View file

@ -0,0 +1,319 @@
#pragma once
#include "displayapp/screens/Screen.h"
#include "displayapp/apps/Apps.h"
#include "displayapp/Controllers.h"
#include "displayapp/widgets/StatusIcons.h"
#include "components/datetime/DateTimeController.h"
#include "Symbols.h"
#include <cassert>
#include <memory>
#include <chrono>
#if DEBUG
#define STRINGIZE_DETAIL(x) #x
#define STRINGIZE(x) STRINGIZE_DETAIL(x)
#define asm_assert(condition) _asm_assert(condition, __FILE__ ":" STRINGIZE(__LINE__) " " #condition)
#else
#define asm_assert(condition) _asm_assert(condition, NULL)
#endif
namespace Pinetime {
namespace Applications {
namespace Screens {
class ASM : public Screen {
public:
ASM(Controllers::DateTime&, const Controllers::Battery&, const Controllers::Ble&, Controllers::FS&);
~ASM();
void Refresh() override;
void OnObjectEvent(lv_obj_t* obj, lv_event_t event);
private:
static constexpr int num_slots = 16;
static constexpr int max_locals = 16;
static constexpr int stack_size = 32;
static constexpr int cache_size = 16;
enum DataType : uint8_t { Integer, String, LvglObject, DateTime };
// TODO: Use fancy C++ type stuff
struct Value {
virtual DataType type() = 0;
virtual int compare(Value* other) = 0;
virtual bool isTruthy() = 0;
};
struct ValueInteger : public Value {
uint32_t i;
ValueInteger(uint32_t i) : i(i) {
}
DataType type() override {
return Integer;
}
int compare(Value* other) override {
if (other->type() != Integer) {
return -1;
}
auto otherInt = static_cast<ValueInteger*>(other)->i;
if (i < otherInt) {
return -1;
} else if (i > otherInt) {
return 1;
}
return 0;
}
bool isTruthy() override {
return i != 0;
}
};
struct ValueString : public Value {
char* str;
uint16_t capacity;
ValueString(char* str, uint16_t cap) : str(str), capacity(cap) {
}
~ValueString() {
delete[] str;
}
DataType type() override {
return String;
}
int compare(Value* other) override {
if (other->type() != String) {
return -1;
}
return strcmp(str, static_cast<ValueString*>(other)->str);
}
bool isTruthy() override {
return capacity > 0 && str[0] != '\0';
}
};
struct ValueLvglObject : public Value {
lv_obj_t* obj;
ValueLvglObject(lv_obj_t* obj) : obj(obj) {
}
~ValueLvglObject() {
lv_obj_del(obj);
}
DataType type() override {
return LvglObject;
}
int compare(Value*) override {
return -1;
}
bool isTruthy() override {
return obj != nullptr;
}
};
struct ValueDateTime : public Value {
std::chrono::time_point<std::chrono::system_clock, std::chrono::nanoseconds> time;
std::tm tm;
ValueDateTime(std::chrono::time_point<std::chrono::system_clock, std::chrono::nanoseconds> time, std::tm tm)
: time(time), tm(tm) {
}
DataType type() override {
return DateTime;
}
int compare(Value* other) override {
if (other->type() != DateTime) {
return -1;
}
auto otherTime = static_cast<ValueDateTime*>(other)->time;
if (time < otherTime) {
return -1;
} else if (time > otherTime) {
return 1;
}
return 0;
}
bool isTruthy() override {
return true;
}
};
enum class OpcodeShort : uint8_t {
StoreLocal,
LoadLocal,
Branch,
BranchIfTrue,
Call,
Push0,
PushU8,
PushU16,
PushU24,
PushU32,
PushEmptyString,
PushCurrentDateTime,
PushCurrentTicks,
Duplicate,
Pop,
LoadString,
StartPeriodicRefresh,
StopPeriodicRefresh,
ShowStatusIcons,
CreateLabel,
CreateButton,
CreateArc,
SetLabelText,
SetArcRange,
SetArcRotation,
SetArcBgAngles,
SetArcAdjustable,
SetArcStartAngle,
SetArcValue,
SetObjectAlign,
SetObjectSize,
SetObjectParent,
SetStyleLocalInt,
SetStyleLocalColor,
SetStyleLocalOpa,
SetStyleLocalFont,
SetEventHandler,
RealignObject,
WaitRefresh,
Add,
Subtract,
Multiply,
Divide,
Equals,
Greater,
Lesser,
Negate,
GrowString,
ClearString,
Concat,
FormatDateTime,
GetDateTimeHour,
GetDateTimeMinute,
GetDateTimeSecond
};
enum class OpcodeLong : uint16_t {};
void populate_cache(size_t pos);
uint8_t read_byte(size_t pos) {
if (pos < cache_start || pos >= cache_start + cache_size) {
populate_cache(pos);
}
return cache[pos - cache_start];
}
uint16_t read_u16(size_t pos) {
return static_cast<uint16_t>(read_byte(pos + 1) << 8 | read_byte(pos));
}
uint32_t read_u24(size_t pos) {
return static_cast<uint32_t>(read_byte(pos + 2) << 16 | read_byte(pos + 1) << 8 | read_byte(pos));
}
uint32_t read_u32(size_t pos) {
return static_cast<uint32_t>(read_byte(pos + 3) << 24 | read_byte(pos + 2) << 16 | read_byte(pos + 1) << 8 | read_byte(pos));
}
lfs_file_t file;
uint8_t cache[cache_size];
size_t cache_start;
size_t program_size;
size_t pc = 0;
std::shared_ptr<Value> locals[max_locals] = {};
std::shared_ptr<Value> stack[stack_size] = {};
uint8_t stack_pointer = 0;
lv_task_t* taskRefresh = nullptr;
Controllers::DateTime& dateTimeController;
const Controllers::Battery& batteryController;
const Controllers::Ble& bleController;
Controllers::FS& fs;
std::unique_ptr<Widgets::StatusIcons> statusIcons;
void run();
void _asm_assert(bool condition, const char* msg);
bool doBranch(uint32_t to);
std::shared_ptr<Value> pop() {
asm_assert(stack_pointer > 0);
stack_pointer--;
auto v = stack[stack_pointer];
stack[stack_pointer] = nullptr;
return v;
}
template <typename T>
std::shared_ptr<T> pop(DataType wantType)
requires(std::is_base_of_v<Value, T>)
{
auto v = pop();
asm_assert(v->type() == wantType);
return std::static_pointer_cast<T>(v);
}
uint32_t pop_uint32() {
return pop<ValueInteger>(Integer)->i;
}
void push(std::shared_ptr<Value> v) {
asm_assert(stack_pointer < stack_size);
stack[stack_pointer++] = v;
}
};
}
template <>
struct AppTraits<Apps::ASM> {
static constexpr Apps app = Apps::ASM;
static constexpr const char* icon = Screens::Symbols::eye;
static Screens::Screen* Create(AppControllers& controllers) {
return new Screens::ASM(controllers.dateTimeController,
controllers.batteryController,
controllers.bleController,
controllers.filesystem);
};
};
};
}