Skip to content
This repository was archived by the owner on May 19, 2026. It is now read-only.
Draft
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
249 changes: 99 additions & 150 deletions Apps/GlanceStrain/Software/Libs/Source/Service.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -20,145 +20,119 @@
#include "icon_30x30.h"

Service::Service(SDK::Kernel &kernel)
: mKernel(kernel), mName("Strain Score"), mGlanceUI(), mGlanceTitle(), mGlanceValue(), mSensorHR(SDK::Sensor::Type::HEART_RATE), mSensorActivity(SDK::Sensor::Type::ACTIVITY)
{
}
: mKernel(kernel),
mName("Strain Score"),
mGlanceUI(),
mGlanceTitle(),
mGlanceValue(),
mSensorHR(SDK::Sensor::Type::HEART_RATE),
mSensorActivity(SDK::Sensor::Type::ACTIVITY) {}

Service::~Service()
{
disconnect();
}
Service::~Service() { disconnect(); }

void Service::run()
{
void Service::run() {
LOG_INFO("Started\n");

createGuiControls();
LOG_DEBUG("GUI controls created\n");
if (configGui())
{
LOG_DEBUG("GUI configured successfully\n");
connect();
checkDayRollover();
}
else
{
return;
}

while (true)
{

while (true) {
SDK::MessageBase *msg;

if (!mKernel.comm.getMessage(msg))
{
if (!mKernel.comm.getMessage(msg)) {
continue;
}

switch (msg->getType())
{
case SDK::MessageType::EVENT_GLANCE_START:
LOG_INFO("GLANCE is now running\n");

case SDK::MessageType::COMMAND_APP_STOP:
LOG_INFO("Force exit from the application\n");
case SDK::MessageType::EVENT_GLANCE_STOP:
LOG_INFO("GLANCE has stopped\n");
saveJson();
// disconnect();
// mKernel.comm.releaseMessage(msg);
// return;

case SDK::MessageType::EVENT_GLANCE_TICK:
LOG_DEBUG("Glance tick event\n");
onGlanceTick();
break;

case SDK::MessageType::EVENT_SENSOR_LAYER_DATA:
{
LOG_DEBUG("Sensor data event\n");
auto event = static_cast<SDK::Message::Sensor::EventData *>(msg);
this->onSdlNewData(event->handle,
event->data,
event->count,
event->stride);
}
break;

default:
break;
switch (msg->getType()) {
case SDK::MessageType::EVENT_GLANCE_START:
LOG_INFO("GLANCE is now running\n");
if (configGui()) {
LOG_DEBUG("GUI configured successfully\n");
connect();
checkDayRollover();
}
break;

case SDK::MessageType::COMMAND_APP_STOP:
LOG_INFO("Force exit from the application\n");
// saveJson();
// disconnect();
// mKernel.comm.releaseMessage(msg);
// return;
break;
case SDK::MessageType::EVENT_GLANCE_STOP:
LOG_INFO("GLANCE has stopped\n");
saveJson();

case SDK::MessageType::EVENT_GLANCE_TICK:
LOG_DEBUG("Glance tick event\n");
onGlanceTick();
break;

case SDK::MessageType::EVENT_SENSOR_LAYER_DATA: {
LOG_DEBUG("Sensor data event\n");
auto event = static_cast<SDK::Message::Sensor::EventData *>(msg);
this->onSdlNewData(event->handle, event->data, event->count, event->stride);
} break;

default:
break;
}

mKernel.comm.releaseMessage(msg);
}
}

void Service::connect()
{
if (!mSensorHR.isConnected())
{
void Service::connect() {
if (!mSensorHR.isConnected()) {
LOG_DEBUG("Connect to HR sensors...\n");
mSensorHR.connect();
mSensorHR.connect(2000);
}
if (!mSensorActivity.isConnected())
{
if (!mSensorActivity.isConnected()) {
LOG_DEBUG("Connect to Activity sensor...\n");
mSensorActivity.connect();
mSensorActivity.connect(2000);
}
}

void Service::disconnect()
{
void Service::disconnect() {
LOG_DEBUG("Disconnect from sensors...\n");
saveJson();
if (mSensorHR.isConnected())
{
if (mSensorHR.isConnected()) {
mSensorHR.disconnect();
}
if (mSensorActivity.isConnected())
{
if (mSensorActivity.isConnected()) {
mSensorActivity.disconnect();
}
}

void Service::onSdlNewData(uint16_t handle,
const SDK::Sensor::Data *data,
uint16_t count,
uint16_t stride)
{
void Service::onSdlNewData(uint16_t handle, const SDK::Sensor::Data *data, uint16_t count, uint16_t stride) {
SDK::Sensor::DataBatch batch(data, count, stride);

if (mSensorActivity.matchesDriver(handle))
{
if (count > 0)
{

LOG_DEBUG("New data from SDL...\n");
if (mSensorActivity.matchesDriver(handle)) {
if (count > 0) {
SDK::SensorDataParser::Activity p(batch[0]);
if (p.isDataValid())
{
if (p.isDataValid()) {
mActiveMin = p.getDuration();
}
}
}

if (mSensorHR.matchesDriver(handle))
{
for (uint16_t i = 0; i < count; ++i)
{
if (mSensorHR.matchesDriver(handle)) {
for (uint16_t i = 0; i < count; ++i) {
SDK::SensorDataParser::HeartRate p(batch[i]);
if (p.isDataValid())
{
if (p.isDataValid()) {
uint16_t hr = p.getBpm();
if (hr >= 50 && hr <= 220)
{
if (hr >= 50 && hr <= 220) {
float norm = (static_cast<float>(hr) - 60.0f) / 120.0f;
float delta = std::max(0.0f, norm) * 0.75f;
mTotalStrain += delta;
mSumHR += static_cast<float>(hr);
mMaxHR = std::max(mMaxHR, hr);
mSampleCount++;
uint64_t ts = p.getTimestamp();
if (mSamples.size() < 1440u)
{
if (mSamples.size() < 1440u) {
mSamples.push_back({ts, hr, delta});
}
}
Expand All @@ -167,10 +141,7 @@ void Service::onSdlNewData(uint16_t handle,
}
}

void Service::onGlanceTick()
{
checkDayRollover();

void Service::onGlanceTick() {
// float avg_hr = (mSampleCount > 0) ? (mSumHR / static_cast<float>(mSampleCount)) : 0.0f;

mGlanceValue.print("%.1f", mTotalStrain);
Expand All @@ -179,21 +150,16 @@ void Service::onGlanceTick()

std::time_t now = std::time(nullptr);
bool need_save = false;
if (mTickCount % 60 == 0)
need_save = true;
if (now - mLastSaveTime > 3600)
need_save = true;
if (need_save)
{
if (mTickCount % 60 == 0) need_save = true;
if (now - mLastSaveTime > 3600) need_save = true;
if (need_save) {
saveJson();
mLastSaveTime = now;
}

if (mGlanceUI.isInvalid())
{
if (mGlanceUI.isInvalid()) {
auto *upd = mKernel.comm.allocateMessage<SDK::Message::RequestGlanceUpdate>();
if (upd)
{
if (upd) {
upd->name = mName;
upd->controls = mGlanceUI.data();
upd->controlsNumber = static_cast<uint32_t>(mGlanceUI.size());
Expand All @@ -206,16 +172,12 @@ void Service::onGlanceTick()
}
}

bool Service::configGui()
{
bool Service::configGui() {
bool status = false;
auto *gc = mKernel.comm.allocateMessage<SDK::Message::RequestGlanceConfig>();
if (gc)
{
if (mKernel.comm.sendMessage(gc, 100) && gc->getResult() == SDK::MessageResult::SUCCESS)
{
if (gc->maxControls >= 3)
{
if (gc) {
if (mKernel.comm.sendMessage(gc, 100) && gc->getResult() == SDK::MessageResult::SUCCESS) {
if (gc->maxControls >= 3) {
mGlanceUI.setWidth(gc->width);
mGlanceUI.setHeight(gc->height);
status = true;
Expand All @@ -227,8 +189,7 @@ bool Service::configGui()
return status;
}

void Service::createGuiControls()
{
void Service::createGuiControls() {
mGlanceUI.createImage().init({31, 15}, {60, 60}, ICON_60X60_ABGR2222);

mGlanceTitle = mGlanceUI.createText();
Expand All @@ -246,8 +207,7 @@ void Service::createGuiControls()
.alignment(GlanceAlignH_t::GLANCE_ALIGN_H_CENTER);
}

void Service::checkDayRollover()
{
void Service::checkDayRollover() {
LOG_DEBUG("Checking day rollover\n");
std::time_t now_t = std::time(nullptr);
std::tm tm_now;
Expand All @@ -256,11 +216,9 @@ void Service::checkDayRollover()
std::strftime(now_date, sizeof(now_date), "%Y-%m-%d", &tm_now);
LOG_DEBUG("Current date: %s\n", now_date);

if (std::strlen(mCurrentDate) == 0 || std::strcmp(mCurrentDate, now_date) != 0)
{
if (std::strlen(mCurrentDate) == 0 || std::strcmp(mCurrentDate, now_date) != 0) {
LOG_DEBUG("Date changed from '%s' to '%s'\n", mCurrentDate, now_date);
if (std::strlen(mCurrentDate) > 0)
{
if (std::strlen(mCurrentDate) > 0) {
saveJson();
}
std::strncpy(mCurrentDate, now_date, 10);
Expand All @@ -280,64 +238,55 @@ void Service::checkDayRollover()
}
}

void Service::saveJson()
{
void Service::saveJson() {
checkDayRollover();

LOG_DEBUG("Saving JSON to %s\n", mJsonPath);
auto json_buf = std::make_shared<std::array<uint8_t, 65536>>();
size_t offset = 0;
float avg_hr = (mSampleCount > 0) ? (mSumHR / static_cast<float>(mSampleCount)) : 0.0f;

offset = std::snprintf(reinterpret_cast<char *>(json_buf->data()), json_buf->size(),
"{\"date\":\"%s\",\"total_strain\":%.1f,\"avg_hr\":%.1f,\"max_hr\":%u,\"active_min\":%lu,\"samples\":[",
mCurrentDate, mTotalStrain, avg_hr, mMaxHR, mActiveMin);
offset = std::snprintf(
reinterpret_cast<char *>(json_buf->data()), json_buf->size(),
"{\"date\":\"%s\",\"total_strain\":%.1f,\"avg_hr\":%.1f,\"max_hr\":%u,\"active_min\":%lu,\"samples\":[",
mCurrentDate, mTotalStrain, avg_hr, mMaxHR, mActiveMin);
LOG_DEBUG("Initial JSON offset: %zu\n", offset);

bool first_sample = true;
for (const auto &s : mSamples)
{
if (!first_sample)
{
offset += std::snprintf(reinterpret_cast<char *>(json_buf->data()) + offset, json_buf->size() - offset, ",");
}
else
{
for (const auto &s : mSamples) {
if (!first_sample) {
offset +=
std::snprintf(reinterpret_cast<char *>(json_buf->data()) + offset, json_buf->size() - offset, ",");
} else {
first_sample = false;
}
offset += std::snprintf(reinterpret_cast<char *>(json_buf->data()) + offset, json_buf->size() - offset,
"{\"timestamp\":%llu,\"hr\":%hu,\"strain_delta\":%.3f}",
static_cast<unsigned long long>(s.timestamp), static_cast<unsigned short>(s.hr), s.strain_delta);
static_cast<unsigned long long>(s.timestamp), static_cast<unsigned short>(s.hr),
s.strain_delta);
}
LOG_DEBUG("Samples processed, offset: %zu\n", offset);
offset += std::snprintf(reinterpret_cast<char *>(json_buf->data()) + offset, json_buf->size() - offset, "]}");
LOG_DEBUG("Final JSON offset: %zu\n", offset);

if (offset > 0 && offset < json_buf->size())
{
if (offset > 0 && offset < json_buf->size()) {
// mKernel.fs.mkdir("/Apps");
// mKernel.fs.mkdir("/Apps/strain");
// mKernel.fs.mkdir("/Apps/strain/storage");
auto file = mKernel.fs.file(mJsonPath);
if (file && file->open(true, true))
{ // write mode, create/override
if (file && file->open(true, true)) { // write mode, create/override
size_t bw;
file->write(reinterpret_cast<const char *>(json_buf->data()), offset, bw);
if (bw == offset)
{
if (bw == offset) {
LOG_INFO("Saved strain data to %s\n", mJsonPath);
}
else
{
} else {
LOG_ERROR("Failed to write complete JSON data\n");
}
file->close();
}
else
{
} else {
LOG_ERROR("Failed to open file %s for writing\n", mJsonPath);
}
}
else
{
} else {
LOG_ERROR("JSON buffer overflow or empty\n");
}
}