blob: 32ff5f401bbe59a3010eaf68bf6910adb3986dfd [file] [log] [blame]
/*
* Copyright (c) 2023 Project CHIP Authors
* All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#include "ui.h"
#include <Options.h> // examples/platform/linux/Options.h
#include <app/server/OnboardingCodesUtil.h>
#include <lib/support/logging/CHIPLogging.h>
#include <platform/CHIPDeviceLayer.h>
#include <platform/PlatformManager.h>
#include <setup_payload/QRCodeSetupPayloadGenerator.h>
#include <setup_payload/SetupPayload.h>
#include <app-common/zap-generated/ids/Attributes.h>
#include <app-common/zap-generated/ids/Clusters.h>
#include <app/util/attribute-storage.h>
#include <SDL.h>
#include <SDL_opengl.h>
#include <imgui.h>
#include <imgui_impl_opengl3.h>
#include <imgui_impl_sdl2.h>
#include <qrcodegen.h>
#include <atomic>
#include <chrono>
#include <semaphore.h>
#include <thread>
namespace example {
namespace Ui {
namespace {
std::atomic<bool> gUiRunning{ false };
class DeviceState
{
public:
DeviceState() { sem_init(&mChipLoopWaitSemaphore, 0 /* shared */, 0); }
~DeviceState() { sem_destroy(&mChipLoopWaitSemaphore); }
// Initialize. MUST be called within the CHIP main loop as it
// loads startup data.
void Init();
// Use ImgUI to show the current state
void ShowUi();
// Fetches the current state from Ember
void UpdateState();
private:
static constexpr int kQRCodeVersion = qrcodegen_VERSION_MAX;
static constexpr int kMaxQRBufferSize = qrcodegen_BUFFER_LEN_FOR_VERSION(kQRCodeVersion);
sem_t mChipLoopWaitSemaphore;
bool mHasQRCode = false;
uint8_t mQRData[kMaxQRBufferSize] = { 0 };
// light data:
bool mOnOff = false;
// Updates the data (run in the chip event loop)
void ChipLoopUpdate();
void InitQRCode();
// Run in CHIPMainLoop to access ember in a single threaded
// fashion
static void ChipLoopUpdateCallback(intptr_t self);
};
DeviceState gDeviceState;
void DeviceState::Init()
{
InitQRCode();
}
void DeviceState::InitQRCode()
{
chip::PayloadContents payload = LinuxDeviceOptions::GetInstance().payload;
if (!payload.isValidQRCodePayload())
{
return;
}
char payloadBuffer[chip::QRCodeBasicSetupPayloadGenerator::kMaxQRCodeBase38RepresentationLength + 1];
chip::MutableCharSpan qrCode(payloadBuffer);
CHIP_ERROR err = GetQRCode(qrCode, payload);
if (err != CHIP_NO_ERROR)
{
ChipLogError(AppServer, "Failed to load QR code: %" CHIP_ERROR_FORMAT, err.Format());
return;
}
if (qrCode.size() > kMaxQRBufferSize)
{
ChipLogError(AppServer, "Insufficient qr code buffer size to encode");
return;
}
uint8_t tempAndData[kMaxQRBufferSize];
memcpy(tempAndData, qrCode.data(), qrCode.size());
mHasQRCode = qrcodegen_encodeBinary(tempAndData, qrCode.size(), mQRData, qrcodegen_Ecc_MEDIUM, qrcodegen_VERSION_MIN,
qrcodegen_VERSION_MAX, qrcodegen_Mask_AUTO, true);
if (!mHasQRCode)
{
ChipLogError(AppServer, "Failed to encode QR code");
return;
}
}
inline ImVec2 operator+(const ImVec2 & a, const ImVec2 & b)
{
return ImVec2(a.x + b.x, a.y + b.y);
}
void DeviceState::ShowUi()
{
ImGui::Begin("Light app");
ImGui::Text("Here is the current ember device state:");
ImGui::Checkbox("Light is ON", &mOnOff);
ImGui::End();
if (mHasQRCode)
{
ImGui::Begin("QR Code.");
ImDrawList * drawList = ImGui::GetWindowDrawList();
constexpr int kBorderSize = 35;
constexpr int kMinWindowSize = 200;
const int kQRCodeSize = qrcodegen_getSize(mQRData);
ImVec2 pos = ImGui::GetWindowPos();
ImVec2 size = ImGui::GetWindowSize();
if (size.y < kMinWindowSize)
{
size = ImVec2(kMinWindowSize, kMinWindowSize);
ImGui::SetWindowSize(size);
}
// Fill the entire window white, then figure out borders
drawList->AddRectFilled(pos, pos + size, IM_COL32_WHITE);
// add a border
if (size.x >= 2 * kBorderSize && size.y >= 2 * kBorderSize)
{
size.x -= 2 * kBorderSize;
size.y -= 2 * kBorderSize;
pos.x += kBorderSize;
pos.y += kBorderSize;
}
// create a square rectangle: keep only the smaller side and adjust the
// other
if (size.x > size.y)
{
pos.x += (size.x - size.y) / 2;
size.x = size.y;
}
else if (size.y > size.x)
{
pos.y += (size.y - size.x) / 2;
size.y = size.x;
}
const ImVec2 squareSize = ImVec2(size.x / static_cast<float>(kQRCodeSize), size.y / static_cast<float>(kQRCodeSize));
for (int y = 0; y < kQRCodeSize; ++y)
{
for (int x = 0; x < kQRCodeSize; ++x)
{
if (qrcodegen_getModule(mQRData, x, y))
{
ImVec2 placement =
ImVec2(pos.x + static_cast<float>(x) * squareSize.x, pos.y + static_cast<float>(y) * squareSize.y);
drawList->AddRectFilled(placement, placement + squareSize, IM_COL32_BLACK);
}
}
}
ImGui::End();
}
}
void DeviceState::ChipLoopUpdate()
{
// This will contain a dimmable light
static constexpr chip::EndpointId kLightEndpointId = 1;
// TODO:
// - consider error checking
// - add more attributes to the display (color? brightness?)
{
uint8_t value;
emberAfReadServerAttribute(kLightEndpointId, chip::app::Clusters::OnOff::Id,
chip::app::Clusters::OnOff::Attributes::OnOff::Id, &value, sizeof(value));
mOnOff = (value != 0);
}
}
void DeviceState::ChipLoopUpdateCallback(intptr_t self)
{
DeviceState * _this = reinterpret_cast<DeviceState *>(self);
_this->ChipLoopUpdate();
sem_post(&_this->mChipLoopWaitSemaphore); // notify complete
}
void DeviceState::UpdateState()
{
chip::DeviceLayer::PlatformMgr().ScheduleWork(&ChipLoopUpdateCallback, reinterpret_cast<intptr_t>(this));
// ensure update is done when existing
if (sem_trywait(&mChipLoopWaitSemaphore) != 0)
{
if (!gUiRunning.load())
{
// UI should stop, no need to wait, probably chip main loop is stopped
return;
}
std::this_thread::yield();
}
}
void UiInit(SDL_GLContext * gl_context, SDL_Window ** window)
{
if (SDL_Init(SDL_INIT_VIDEO | SDL_INIT_TIMER | SDL_INIT_GAMECONTROLLER) != 0)
{
ChipLogError(AppServer, "SDL Init Error: %s\n", SDL_GetError());
return;
}
SDL_GL_SetAttribute(SDL_GL_CONTEXT_FLAGS, 0);
SDL_GL_SetAttribute(SDL_GL_CONTEXT_PROFILE_MASK, SDL_GL_CONTEXT_PROFILE_CORE);
SDL_GL_SetAttribute(SDL_GL_CONTEXT_MAJOR_VERSION, 3);
SDL_GL_SetAttribute(SDL_GL_CONTEXT_MINOR_VERSION, 0);
#ifdef SDL_HINT_IME_SHOW_UI
SDL_SetHint(SDL_HINT_IME_SHOW_UI, "1");
#endif
SDL_GL_SetAttribute(SDL_GL_DOUBLEBUFFER, 1);
SDL_GL_SetAttribute(SDL_GL_DEPTH_SIZE, 24);
SDL_GL_SetAttribute(SDL_GL_STENCIL_SIZE, 8);
SDL_WindowFlags window_flags = (SDL_WindowFlags)(SDL_WINDOW_OPENGL | SDL_WINDOW_RESIZABLE | SDL_WINDOW_ALLOW_HIGHDPI);
*window = SDL_CreateWindow("Dear ImGui SDL2+OpenGL3 example", SDL_WINDOWPOS_CENTERED, SDL_WINDOWPOS_CENTERED, 1280, 720,
window_flags);
*gl_context = SDL_GL_CreateContext(*window);
SDL_GL_MakeCurrent(*window, *gl_context);
SDL_GL_SetSwapInterval(1); // Enable vsync
// Setup Dear ImGui context
IMGUI_CHECKVERSION();
ImGui::CreateContext();
ImGuiIO & io = ImGui::GetIO();
(void) io;
// io.ConfigFlags |= ImGuiConfigFlags_NavEnableKeyboard; // Enable Keyboard Controls
// io.ConfigFlags |= ImGuiConfigFlags_NavEnableGamepad; // Enable Gamepad Controls
// Setup Dear ImGui style
ImGui::StyleColorsDark();
// ImGui::StyleColorsLight();
// Setup Platform/Renderer backends
ImGui_ImplSDL2_InitForOpenGL(*window, *gl_context);
ImGui_ImplOpenGL3_Init("#version 130");
}
void UiShutdown(SDL_GLContext * gl_context, SDL_Window ** window)
{
ImGui_ImplOpenGL3_Shutdown();
ImGui_ImplSDL2_Shutdown();
ImGui::DestroyContext();
SDL_GL_DeleteContext(*gl_context);
SDL_DestroyWindow(*window);
SDL_Quit();
}
void UiLoop()
{
SDL_GLContext gl_context;
SDL_Window * window = nullptr;
UiInit(&gl_context, &window);
ImGuiIO & io = ImGui::GetIO();
while (gUiRunning.load())
{
SDL_Event event;
while (SDL_PollEvent(&event))
{
ImGui_ImplSDL2_ProcessEvent(&event);
if (event.type == SDL_QUIT)
{
chip::DeviceLayer::PlatformMgr().StopEventLoopTask();
}
if (event.type == SDL_WINDOWEVENT && event.window.event == SDL_WINDOWEVENT_CLOSE &&
event.window.windowID == SDL_GetWindowID(window))
{
chip::DeviceLayer::PlatformMgr().StopEventLoopTask();
}
}
ImGui_ImplOpenGL3_NewFrame();
ImGui_ImplSDL2_NewFrame();
ImGui::NewFrame();
gDeviceState.UpdateState();
gDeviceState.ShowUi();
// rendering
ImGui::Render();
glViewport(0, 0, (int) io.DisplaySize.x, (int) io.DisplaySize.y);
glClearColor(0, 0, 0, 0);
glClear(GL_COLOR_BUFFER_BIT);
ImGui_ImplOpenGL3_RenderDrawData(ImGui::GetDrawData());
SDL_GL_SwapWindow(window);
}
UiShutdown(&gl_context, &window);
ChipLogProgress(AppServer, "UI thread Stopped...");
}
static std::thread gUiThread;
} // namespace
void Start()
{
// Init inside the "main" thread, so that it can access globals
// proparly (for QR code and such)
gDeviceState.Init();
gUiRunning = true;
std::thread uiThread(&UiLoop);
gUiThread.swap(uiThread);
}
void Stop()
{
gUiRunning = false;
gUiThread.join();
}
} // namespace Ui
} // namespace example