blob: fc6a12ea443b139c08cecbb2db5517a8eb0f3b59 [file] [log] [blame]
// Copyright 2022 The Pigweed Authors
//
// 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
//
// https://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 <array>
#include <cstdint>
#include <cwchar>
#include <forward_list>
#include <memory>
#include <string_view>
#include <utility>
#define PW_LOG_LEVEL PW_LOG_LEVEL_DEBUG
#include "ansi.h"
#include "app_common/common.h"
#include "pw_assert/assert.h"
#include "pw_assert/check.h"
#include "pw_board_led/led.h"
#include "pw_color/color.h"
#include "pw_color/colors_endesga32.h"
#include "pw_color/colors_pico8.h"
#include "pw_coordinates/vec2.h"
#include "pw_coordinates/vec_int.h"
#include "pw_draw/draw.h"
#include "pw_draw/font_set.h"
#include "pw_draw/pigweed_farm.h"
#include "pw_framebuffer/framebuffer.h"
#include "pw_log/log.h"
#include "pw_ring_buffer/prefixed_entry_ring_buffer.h"
#include "pw_spin_delay/delay.h"
#include "pw_string/string_builder.h"
#include "pw_sys_io/sys_io.h"
#include "pw_touchscreen/touchscreen.h"
#include "text_buffer.h"
#if defined(USE_FREERTOS)
#include "FreeRTOS.h"
#include "task.h"
#endif // if defined(USE_FREERTOS)
using pw::color::color_rgb565_t;
using pw::color::colors_pico8_rgb565;
using pw::coordinates::Size;
using pw::coordinates::Vector2;
using pw::display::Display;
using pw::draw::FontSet;
using pw::framebuffer::Framebuffer;
using pw::ring_buffer::PrefixedEntryRingBuffer;
// TODO(cmumford): move this code into a pre_init section (i.e. boot.cc) which
// is part of the target. Not all targets currently have this.
#if defined(DEFINE_FREERTOS_MEMORY_FUNCTIONS)
std::array<StackType_t, 100 /*configMINIMAL_STACK_SIZE*/> freertos_idle_stack;
StaticTask_t freertos_idle_tcb;
std::array<StackType_t, configTIMER_TASK_STACK_DEPTH> freertos_timer_stack;
StaticTask_t freertos_timer_tcb;
extern "C" {
// Required for configUSE_TIMERS.
void vApplicationGetTimerTaskMemory(StaticTask_t** ppxTimerTaskTCBBuffer,
StackType_t** ppxTimerTaskStackBuffer,
uint32_t* pulTimerTaskStackSize) {
*ppxTimerTaskTCBBuffer = &freertos_timer_tcb;
*ppxTimerTaskStackBuffer = freertos_timer_stack.data();
*pulTimerTaskStackSize = freertos_timer_stack.size();
}
void vApplicationGetIdleTaskMemory(StaticTask_t** ppxIdleTaskTCBBuffer,
StackType_t** ppxIdleTaskStackBuffer,
uint32_t* pulIdleTaskStackSize) {
*ppxIdleTaskTCBBuffer = &freertos_idle_tcb;
*ppxIdleTaskStackBuffer = freertos_idle_stack.data();
*pulIdleTaskStackSize = freertos_idle_stack.size();
}
} // extern "C"
#endif // defined(DEFINE_FREERTOS_MEMORY_FUNCTIONS)
namespace {
constexpr color_rgb565_t kBlack = 0U;
constexpr color_rgb565_t kWhite = 0xffff;
class DemoDecoder : public AnsiDecoder {
public:
DemoDecoder(TextBuffer& log_text_buffer)
: log_text_buffer_(log_text_buffer) {}
protected:
void SetFgColor(uint8_t r, uint8_t g, uint8_t b) override {
fg_color_ = pw::color::ColorRGBA(r, g, b).ToRgb565();
}
void SetBgColor(uint8_t r, uint8_t g, uint8_t b) override {
bg_color_ = pw::color::ColorRGBA(r, g, b).ToRgb565();
}
void EmitChar(char c) override {
log_text_buffer_.DrawCharacter(TextBuffer::Char{c, fg_color_, bg_color_});
}
private:
color_rgb565_t fg_color_ = kWhite;
color_rgb565_t bg_color_ = kBlack;
TextBuffer& log_text_buffer_;
};
// A simple implementation of a UI button.
class Button {
public:
// The label ptr must be valid throughout the lifetime of this object.
Button(const wchar_t* label, const Vector2<int>& tl, const Size<int>& size)
: label_(label), tl_(tl), size_(size) {}
bool Contains(Vector2<int> pt) const {
return pt.x >= tl_.x && pt.x < (tl_.x + size_.width) && pt.y >= tl_.y &&
pt.y < (tl_.y + size_.height);
}
const wchar_t* label_;
const Vector2<int> tl_;
const Size<int> size_;
};
constexpr const wchar_t* kButtonLabel = L"Click to add logs";
constexpr int kButtonWidth = 108;
constexpr Vector2<int> kButtonTL = {320 - kButtonWidth, 0};
constexpr Size<int> kButtonSize = {kButtonWidth, 12};
TextBuffer s_log_text_buffer;
DemoDecoder s_demo_decoder(s_log_text_buffer);
Button g_button(kButtonLabel, kButtonTL, kButtonSize);
#if defined(USE_FREERTOS)
std::array<StackType_t, configMINIMAL_STACK_SIZE> s_freertos_stack;
StaticTask_t s_freertos_tcb;
#endif // defined(USE_FREERTOS)
void DrawButton(const Button& button,
color_rgb565_t bg_color,
Framebuffer& framebuffer) {
pw::draw::DrawRectWH(framebuffer,
button.tl_.x,
button.tl_.y,
button.size_.width,
button.size_.height,
bg_color,
/*filled=*/true);
constexpr int kMargin = 2;
Vector2<int> tl{button.tl_.x + kMargin, button.tl_.y + kMargin};
DrawString(
button.label_, tl, kBlack, bg_color, pw::draw::font6x8, framebuffer);
}
// Draw a font sheet starting at the given top-left screen coordinates.
Vector2<int> DrawTestFontSheet(Vector2<int> tl,
int num_columns,
color_rgb565_t fg_color,
color_rgb565_t bg_color,
const FontSet& font,
Framebuffer& framebuffer) {
Vector2<int> max_extents = tl;
const int initial_x = tl.x;
for (int c = font.starting_character; c <= font.ending_character; c++) {
int char_idx = c - font.starting_character;
if (char_idx % num_columns == 0) {
tl.x = initial_x;
tl.y += font.height;
}
auto char_size =
DrawCharacter(c, tl, fg_color, bg_color, font, framebuffer);
tl.x += char_size.width;
max_extents.x = std::max(tl.x, max_extents.x);
max_extents.y = std::max(tl.y, max_extents.y);
}
max_extents.y += font.height;
return max_extents;
}
Vector2<int> DrawColorFontSheet(Vector2<int> tl,
int num_columns,
color_rgb565_t fg_color,
const FontSet& font,
Framebuffer& framebuffer) {
constexpr int kNumColors = sizeof(pw::color::colors_endesga32_rgb565) /
sizeof(pw::color::colors_endesga32_rgb565[0]);
const int initial_x = tl.x;
Vector2<int> max_extents = tl;
for (int c = font.starting_character; c <= font.ending_character; c++) {
int char_idx = c - font.starting_character;
if (char_idx % num_columns == 0) {
tl.x = initial_x;
tl.y += font.height;
}
auto char_size =
DrawCharacter(c,
tl,
fg_color,
pw::color::colors_endesga32_rgb565[char_idx % kNumColors],
font,
framebuffer);
tl.x += char_size.width;
max_extents.x = std::max(tl.x, max_extents.x);
max_extents.y = std::max(tl.y, max_extents.y);
}
max_extents.y += font.height;
return max_extents;
}
// The logging callback used to capture log messages sent to pw_log.
void LogCallback(std::string_view log) {
for (auto c : log) {
s_demo_decoder.ProcessChar(c);
}
s_demo_decoder.ProcessChar('\n');
pw::sys_io::WriteLine(log).IgnoreError();
};
// Draw the Pigweed sprite and artwork at the top of the display.
// Returns the bottom Y coordinate drawn.
int DrawPigweedSprite(Framebuffer& framebuffer) {
int sprite_pos_x = 10;
int sprite_pos_y = 24;
int sprite_scale = 4;
int border_size = 8;
// Draw the dark blue border
pw::draw::DrawRectWH(
framebuffer,
sprite_pos_x - border_size,
sprite_pos_y - border_size,
pigweed_farm_sprite_sheet.width * sprite_scale + (border_size * 2),
pigweed_farm_sprite_sheet.height * sprite_scale + (border_size * 2),
colors_pico8_rgb565[COLOR_DARK_BLUE],
true);
// Shrink the border
border_size = 4;
// Draw the light blue background
pw::draw::DrawRectWH(
framebuffer,
sprite_pos_x - border_size,
sprite_pos_y - border_size,
pigweed_farm_sprite_sheet.width * sprite_scale + (border_size * 2),
pigweed_farm_sprite_sheet.height * sprite_scale + (border_size * 2),
colors_pico8_rgb565[COLOR_BLUE],
true);
static Vector2<int> sun_offset;
static int motion_dir = -1;
static int frame_num = 0;
frame_num++;
if ((frame_num % 5) == 0)
sun_offset.x += motion_dir;
if ((frame_num % 15) == 0)
sun_offset.y -= motion_dir;
if (sun_offset.x < -60)
motion_dir = 1;
else if (sun_offset.x > 10)
motion_dir = -1;
// Draw the Sun
pw::draw::DrawCircle(framebuffer,
sun_offset.x + sprite_pos_x +
(pigweed_farm_sprite_sheet.width * sprite_scale) -
32,
sun_offset.y + sprite_pos_y,
20,
colors_pico8_rgb565[COLOR_ORANGE],
true);
pw::draw::DrawCircle(framebuffer,
sun_offset.x + sprite_pos_x +
(pigweed_farm_sprite_sheet.width * sprite_scale) -
32,
sun_offset.y + sprite_pos_y,
18,
colors_pico8_rgb565[COLOR_YELLOW],
true);
// Draw the farm sprite's shadow
pigweed_farm_sprite_sheet.current_index = 1;
pw::draw::DrawSprite(framebuffer,
sprite_pos_x + 2,
sprite_pos_y + 2,
&pigweed_farm_sprite_sheet,
4);
// Draw the farm sprite
pigweed_farm_sprite_sheet.current_index = 0;
pw::draw::DrawSprite(
framebuffer, sprite_pos_x, sprite_pos_y, &pigweed_farm_sprite_sheet, 4);
return 76;
}
void DrawFPS(Vector2<int> tl,
Framebuffer& framebuffer,
std::wstring_view fps_msg) {
if (fps_msg.empty())
return;
DrawString(fps_msg,
tl,
colors_pico8_rgb565[COLOR_PEACH],
kBlack,
pw::draw::font6x8,
framebuffer);
}
// Draw the pigweed text banner.
// Returns the bottom Y coordinate of the bottommost pixel set.
int DrawPigweedBanner(Vector2<int> tl, Framebuffer& framebuffer) {
constexpr std::array<std::wstring_view, 5> pigweed_banner = {
L"▒█████▄ █▓ ▄███▒ ▒█ ▒█ ░▓████▒ ░▓████▒ ▒▓████▄",
L" ▒█░ █░ ░█▒ ██▒ ▀█▒ ▒█░ █ ▒█ ▒█ ▀ ▒█ ▀ ▒█ ▀█▌",
L" ▒█▄▄▄█░ ░█▒ █▓░ ▄▄░ ▒█░ █ ▒█ ▒███ ▒███ ░█ █▌",
L" ▒█▀ ░█░ ▓█ █▓ ░█░ █ ▒█ ▒█ ▄ ▒█ ▄ ░█ ▄█▌",
L" ▒█ ░█░ ░▓███▀ ▒█▓▀▓█░ ░▓████▒ ░▓████▒ ▒▓████▀"};
// Draw the Pigweed "ASCII" banner.
for (auto text_row : pigweed_banner) {
Size<int> string_dims = DrawString(text_row,
tl,
colors_pico8_rgb565[COLOR_PINK],
kBlack,
pw::draw::font6x8_box_chars,
framebuffer);
tl.y += string_dims.height;
}
return tl.y - pw::draw::font6x8_box_chars.height;
}
// Draw the font sheets.
// Returns the bottom Y coordinate drawn.
int DrawFontSheets(Vector2<int> tl, Framebuffer& framebuffer) {
constexpr int kFontSheetVerticalPadding = 4;
constexpr int kFontSheetNumColumns = 48;
int initial_x = tl.x;
tl = DrawColorFontSheet(tl,
kFontSheetNumColumns,
/*fg_color=*/kBlack,
pw::draw::font6x8,
framebuffer);
tl.x = initial_x;
tl.y -= pw::draw::font6x8.height;
tl.y += kFontSheetVerticalPadding;
tl = DrawTestFontSheet(tl,
kFontSheetNumColumns,
/*fg_color=*/kWhite,
/*bg_color=*/kBlack,
pw::draw::font6x8,
framebuffer);
tl.x = initial_x;
tl.y += kFontSheetVerticalPadding;
Size<int> string_dims = DrawString(L"Box Characters:",
tl,
/*fg_color=*/kWhite,
/*bg_color=*/kBlack,
pw::draw::font6x8,
framebuffer);
tl.x += string_dims.width + pw::draw::font6x8.width;
tl.y -= pw::draw::font6x8.height;
tl = DrawTestFontSheet(tl,
/*num_columns=*/32,
/*fg_color=*/kWhite,
/*bg_color=*/kBlack,
pw::draw::font6x8_box_chars,
framebuffer);
return tl.y;
}
// Draw the application header section which is mostly static text/graphics.
// Return the height (in pixels) of the header.
int DrawHeader(Framebuffer& framebuffer, std::wstring_view fps_msg) {
DrawButton(
g_button, /*bg_color=*/colors_pico8_rgb565[COLOR_BLUE], framebuffer);
Vector2<int> tl = {0, 0};
tl.y = DrawPigweedSprite(framebuffer);
tl.y = DrawPigweedBanner(tl, framebuffer);
constexpr int kFontSheetMargin = 4;
tl.y += kFontSheetMargin;
DrawFPS({1, 2}, framebuffer, fps_msg);
return DrawFontSheets(tl, framebuffer);
}
void DrawLogTextBuffer(int top, const FontSet& font, Framebuffer& framebuffer) {
constexpr int kLeft = 0;
Vector2<int> loc;
Vector2<int> pos{kLeft, top};
Size<int> buffer_size = s_log_text_buffer.GetSize();
for (loc.y = 0; loc.y < buffer_size.height; loc.y++) {
for (loc.x = 0; loc.x < buffer_size.width; loc.x++) {
auto ch = s_log_text_buffer.GetChar(loc);
if (!ch.ok())
continue;
Size<int> char_size = DrawCharacter(ch->ch,
pos,
ch->foreground_color,
ch->background_color,
font,
framebuffer);
pos.x += char_size.width;
}
pos.y += font.height;
pos.x = kLeft;
}
}
void DrawFrame(Framebuffer& framebuffer, std::wstring_view fps_msg) {
constexpr int kHeaderMargin = 4;
int header_bottom = DrawHeader(framebuffer, fps_msg);
DrawLogTextBuffer(
header_bottom + kHeaderMargin, pw::draw::font6x8, framebuffer);
}
void CreateDemoLogMessages() {
PW_LOG_CRITICAL("An irrecoverable error has occurred!");
PW_LOG_ERROR("There was an error on our last operation");
PW_LOG_WARN("Looks like something is amiss; consider investigating");
PW_LOG_INFO("The operation went as expected");
PW_LOG_DEBUG("Debug output");
}
// Given a ring buffer full of uint32_t values, return the average value
// or zero if empty (or iteration error).
uint32_t CalcAverageUint32Value(PrefixedEntryRingBuffer& ring_buffer) {
uint64_t sum = 0;
uint32_t count = 0;
for (const auto& entry_info : ring_buffer) {
PW_ASSERT(entry_info.buffer.size() == sizeof(uint32_t));
uint32_t val;
std::memcpy(&val, entry_info.buffer.data(), sizeof(val));
sum += val;
count++;
}
return count == 0 ? 0 : sum / count;
}
} // namespace
void MainTask(void* pvParameters) {
// Timing variables
uint32_t frame_start_millis = pw::spin_delay::Millis();
uint32_t frames = 0;
int frames_per_second = 0;
std::array<wchar_t, 40> fps_buffer = {0};
std::wstring_view fps_view(fps_buffer.data(), 0);
std::byte draw_buffer[30 * sizeof(uint32_t)];
std::byte flush_buffer[30 * sizeof(uint32_t)];
PrefixedEntryRingBuffer draw_times;
PrefixedEntryRingBuffer flush_times;
draw_times.SetBuffer(draw_buffer);
flush_times.SetBuffer(flush_buffer);
pw::log_basic::SetOutput(LogCallback);
pw::board_led::Init();
PW_CHECK_OK(Common::Init());
Display& display = Common::GetDisplay();
Framebuffer framebuffer = display.GetFramebuffer();
PW_ASSERT(framebuffer.IsValid());
pw::draw::Fill(framebuffer, kBlack);
PW_LOG_INFO("pw::touchscreen::Init()");
pw::touchscreen::Init();
pw::coordinates::Vec3Int last_frame_touch_state(0, 0, 0);
DrawFrame(framebuffer, fps_view);
// Push the frame buffer to the screen.
display.ReleaseFramebuffer(std::move(framebuffer));
// The display loop.
while (1) {
pw::coordinates::Vec3Int point = display.GetTouchPoint();
// Check for touchscreen events.
if (display.TouchscreenAvailable() && display.NewTouchEvent()) {
if (point.z > 0) {
bool button_just_pressed = false;
if (point.z != last_frame_touch_state.z)
button_just_pressed = true;
// New touch event
Vector2<int> touch_location{point.x, point.y};
PW_LOG_DEBUG("Touch: x:%d, y:%d, z:%d", point.x, point.y, point.z);
// If a button was just pressed, call CreateDemoLogMessages.
if (button_just_pressed && g_button.Contains(touch_location)) {
CreateDemoLogMessages();
}
}
}
last_frame_touch_state.x = point.x;
last_frame_touch_state.y = point.y;
last_frame_touch_state.z = point.z;
uint32_t start = pw::spin_delay::Millis();
framebuffer = display.GetFramebuffer();
if (!framebuffer.IsValid())
continue;
pw::draw::Fill(framebuffer, kBlack);
DrawFrame(framebuffer, fps_view);
uint32_t end = pw::spin_delay::Millis();
uint32_t time = end - start;
draw_times.PushBack(pw::as_bytes(pw::span{std::addressof(time), 1}));
start = end;
display.ReleaseFramebuffer(std::move(framebuffer));
time = pw::spin_delay::Millis() - start;
flush_times.PushBack(pw::as_bytes(pw::span{std::addressof(time), 1}));
// Every second make a log message.
frames++;
if (pw::spin_delay::Millis() > frame_start_millis + 1000) {
frames_per_second = frames;
frames = 0;
PW_LOG_INFO("Time: %lu - FPS: %d",
static_cast<unsigned long>(pw::spin_delay::Millis()),
frames_per_second);
int len = std::swprintf(fps_buffer.data(),
fps_buffer.size(),
L"FPS:%d, Draw:%dms, Flush:%dms",
frames_per_second,
CalcAverageUint32Value(draw_times),
CalcAverageUint32Value(flush_times));
fps_view = std::wstring_view(fps_buffer.data(), len);
frame_start_millis = pw::spin_delay::Millis();
}
}
}
int main(void) {
#if defined(USE_FREERTOS)
TaskHandle_t task_handle = xTaskCreateStatic(MainTask,
"main",
s_freertos_stack.size(),
/*pvParameters=*/nullptr,
tskIDLE_PRIORITY,
s_freertos_stack.data(),
&s_freertos_tcb);
PW_CHECK_NOTNULL(task_handle); // Ensure it succeeded.
vTaskStartScheduler();
#else
MainTask(/*pvParameters=*/nullptr);
#endif
return 0;
}