diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 5cd2e656a4..94c9fe5699 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -428,6 +428,7 @@ list(APPEND SOURCE_FILES displayapp/screens/WatchFacePineTimeStyle.cpp displayapp/screens/WatchFaceCasioStyleG7710.cpp displayapp/screens/WatchFacePrideFlag.cpp + displayapp/screens/WatchFaceQRCode.cpp ## diff --git a/src/displayapp/UserApps.h b/src/displayapp/UserApps.h index 8dc114429f..1ed4b089ac 100644 --- a/src/displayapp/UserApps.h +++ b/src/displayapp/UserApps.h @@ -15,6 +15,7 @@ #include "displayapp/screens/WatchFacePineTimeStyle.h" #include "displayapp/screens/WatchFaceTerminal.h" #include "displayapp/screens/WatchFacePrideFlag.h" +#include "displayapp/screens/WatchFaceQRCode.h" namespace Pinetime { namespace Applications { diff --git a/src/displayapp/apps/Apps.h.in b/src/displayapp/apps/Apps.h.in index f6feeb7b6d..a5f9653a4f 100644 --- a/src/displayapp/apps/Apps.h.in +++ b/src/displayapp/apps/Apps.h.in @@ -54,6 +54,7 @@ namespace Pinetime { Infineat, CasioStyleG7710, PrideFlag, + QRCode, }; template diff --git a/src/displayapp/apps/CMakeLists.txt b/src/displayapp/apps/CMakeLists.txt index 93196ed6a0..b79f904f6e 100644 --- a/src/displayapp/apps/CMakeLists.txt +++ b/src/displayapp/apps/CMakeLists.txt @@ -29,6 +29,7 @@ else() set(DEFAULT_WATCHFACE_TYPES "${DEFAULT_WATCHFACE_TYPES}, WatchFace::Infineat") set(DEFAULT_WATCHFACE_TYPES "${DEFAULT_WATCHFACE_TYPES}, WatchFace::CasioStyleG7710") set(DEFAULT_WATCHFACE_TYPES "${DEFAULT_WATCHFACE_TYPES}, WatchFace::PrideFlag") + set(DEFAULT_WATCHFACE_TYPES "${DEFAULT_WATCHFACE_TYPES}, WatchFace::QRCode") set(WATCHFACE_TYPES "${DEFAULT_WATCHFACE_TYPES}" CACHE STRING "List of watch faces to build into the firmware") endif() diff --git a/src/displayapp/screens/WatchFaceQRCode.cpp b/src/displayapp/screens/WatchFaceQRCode.cpp new file mode 100644 index 0000000000..34f752dbf4 --- /dev/null +++ b/src/displayapp/screens/WatchFaceQRCode.cpp @@ -0,0 +1,514 @@ +#include "displayapp/screens/WatchFaceQRCode.h" + +using namespace Pinetime::Applications::Screens; + +WatchFaceQRCode::WatchFaceQRCode(Components::LittleVgl& lvgl, + Controllers::DateTime& dateTimeController, + const Controllers::Battery& batteryController, + const Controllers::Ble& bleController, + Controllers::Settings& settingsController, + Controllers::MotorController& motor) + : lvgl {lvgl}, + dateTimeController {dateTimeController}, + batteryController {batteryController}, + bleController {bleController}, + settingsController {settingsController}, + motor {motor} { + + altTextIndex = 0; + altTextIndexUpdated = false; + altTextLastChangedTime = xTaskGetTickCount(); + + // White background for entire screen + lv_obj_t* whiteBorder = lv_obj_create(lv_scr_act(), nullptr); + lv_obj_set_style_local_bg_color(whiteBorder, LV_OBJ_PART_MAIN, LV_STATE_DEFAULT, LV_COLOR_WHITE); + lv_obj_set_size(whiteBorder, LV_HOR_RES, LV_VER_RES); + lv_obj_set_style_local_radius(whiteBorder, LV_OBJ_PART_MAIN, LV_STATE_DEFAULT, 0); + + // Gray placeholder to show before the qr code gets fully drawn + lv_obj_t* grayPlaceholder = lv_obj_create(whiteBorder, nullptr); + lv_obj_set_style_local_bg_color(grayPlaceholder, LV_OBJ_PART_MAIN, LV_STATE_DEFAULT, LV_COLOR_GRAY); + lv_obj_set_size(grayPlaceholder, + (lv_coord_t) ((LV_HOR_RES * 33. + quietZoneSize) / (33. + 2 * quietZoneSize)), + (lv_coord_t) ((LV_VER_RES * 33. + quietZoneSize) / (33. + 2 * quietZoneSize))); + lv_obj_align(grayPlaceholder, nullptr, LV_ALIGN_CENTER, 0, 0); + lv_obj_set_style_local_radius(grayPlaceholder, LV_OBJ_PART_MAIN, LV_STATE_DEFAULT, 0); + + // Populate buffers for qr code drawing + const int bufSize = (int) (ceil(LV_HOR_RES / (33. + 2 * quietZoneSize)) * ceil(LV_VER_RES / (33. + 2 * quietZoneSize))); + whiteBuffer = std::unique_ptr(new lv_color_t[bufSize]); + lv_color_fill(whiteBuffer.get(), LV_COLOR_WHITE, bufSize); + blackBuffer = std::unique_ptr(new lv_color_t[bufSize]); + lv_color_fill(blackBuffer.get(), LV_COLOR_BLACK, bufSize); + + taskRefresh = lv_task_create(RefreshTaskCallback, LV_DISP_DEF_REFR_PERIOD, LV_TASK_PRIO_MID, this); +} + +WatchFaceQRCode::~WatchFaceQRCode() { + lv_task_del(taskRefresh); + lv_obj_clean(lv_scr_act()); +} + +/// Long tap to move to next alternate text +bool WatchFaceQRCode::OnTouchEvent(TouchEvents event) { + if (event == TouchEvents::LongTap) { + altTextIndex = (altTextIndex + 1) % (altTexts.size() + 1); + altTextLastChangedTime = xTaskGetTickCount(); + altTextIndexUpdated = true; + BuzzBinary(altTextIndex); + return true; + } + return false; +} + +/// Reset back to normal time QR code if displaying alternate text +bool WatchFaceQRCode::OnButtonPushed() { + if (altTextIndex != 0) { + altTextIndex = 0; + altTextIndexUpdated = true; + BuzzBinary(0); + return true; + } + return false; +} + +void WatchFaceQRCode::Refresh() { + bool timeTextNeedsRefresh = false; + + powerPresent = batteryController.IsPowerPresent(); + batteryPercentRemaining = batteryController.PercentRemaining(); + if (batteryPercentRemaining.IsUpdated() || powerPresent.IsUpdated()) { + timeTextNeedsRefresh = true; + // Needs to be called to reset isUpdated flag on powerPresent, else screen updates twice on object creation + powerPresent.IsUpdated(); + } + + bleState = bleController.IsConnected(); + if (bleState.IsUpdated()) { + timeTextNeedsRefresh = true; + } + + currentDateTime = std::chrono::time_point_cast(dateTimeController.CurrentDateTime()); + if (currentDateTime.IsUpdated()) { + timeTextNeedsRefresh = true; + } + + if (altTextIndex != 0 && xTaskGetTickCount() - altTextLastChangedTime >= altTextTimeout) { + altTextIndex = 0; + altTextIndexUpdated = true; + BuzzBinary(0); + } + + if ((timeTextNeedsRefresh && altTextIndex == 0) || altTextIndexUpdated) { + altTextIndexUpdated = false; + RefreshQRCode(); + } +} + +/// Buzzes the provided number out in binary. Long buzz = 1, short buzz = 0. +void WatchFaceQRCode::BuzzBinary(const uint8_t number) const { + static constexpr uint8_t longBuzzLenMS = 50; + static constexpr uint8_t shortBuzzLenMS = 20; + static constexpr uint8_t pauseLenMS = 250; + + if (number == 0) { + motor.RunForDuration(shortBuzzLenMS); + return; + } + + uint8_t mask = 0x01; + while (mask << 1 <= number && mask < 0x80) { + mask <<= 1; + } + + while (mask > 0) { + if ((bool) (number & mask)) { + motor.RunForDuration(longBuzzLenMS); + } else { + motor.RunForDuration(shortBuzzLenMS); + } + vTaskDelay(pdMS_TO_TICKS(pauseLenMS)); + mask >>= 1; + } +} + +/// Create and return the time text to be displayed as the main watchface. +// ReSharper disable once CppDFAUnreachableFunctionCall +std::string WatchFaceQRCode::MakeQRTimeText() const { + /* + - FORMAT: + { + "time": "12:30 PM", + "date": "11 Jan 2025", + "bt" : false, + "batt": "+100" + } + - NOTES: + Time respects 12/24 hour format + Battery level is a string, and has a plus in front of it when charging + */ + + auto textToEncode = std::unique_ptr(new char[80]); + int textLength = 0; + int printReturn = 0; + + if (settingsController.GetClockType() == Controllers::Settings::ClockType::H12) { + printReturn = snprintf(&textToEncode[textLength], + 80 - textLength, + "{\n\"time\": \"%i:%.2i %cM\",\n", + dateTimeController.Hours() > 12 ? dateTimeController.Hours() - 12 : dateTimeController.Hours(), + dateTimeController.Minutes(), + dateTimeController.Hours() > 12 ? 'P' : 'A'); + } else { + printReturn = snprintf(&textToEncode[textLength], + 80 - textLength, + "{\n\"time\": \"%i:%.2i\",\n", + dateTimeController.Hours(), + dateTimeController.Minutes()); + } + + if (printReturn < 0) { + return "{\n\"error\": \"Failed to encode watch: TIME\",\n\"apology\": \"Sowwy :3\"\n}"; + } + textLength += printReturn; + + printReturn = snprintf(&textToEncode[textLength], + 80 - textLength, + "\"date\": \"%i %s %.4i\",\n\"bt\" : %s,\n", + dateTimeController.Day(), + Controllers::DateTime::MonthShortToStringLow(dateTimeController.Month()), + dateTimeController.Year(), + bleController.IsConnected() ? "true" : "false"); + + if (printReturn < 0) { + return "{\n\"error\": \"Failed to encode watch: DATE/BT\",\n\"apology\": \"Sowwy :3\"\n}"; + } + textLength += printReturn; + + if (batteryController.IsPowerPresent()) { + printReturn = snprintf(&textToEncode[textLength], 80 - textLength, "\"batt\": \"+%i\"\n}", batteryController.PercentRemaining()); + } else { + printReturn = snprintf(&textToEncode[textLength], 80 - textLength, "\"batt\": \"%i\"\n}", batteryController.PercentRemaining()); + } + + if (printReturn < 0) { + return "{\n\"error\": \"Failed to encode watch: BATTERY\",\n\"apology\": \"Sowwy :3\"\n}"; + } + + return std::string(textToEncode.get()); +} + +/// Refresh the entire QR code being displayed, including reevaluating text. +void WatchFaceQRCode::RefreshQRCode() const { + std::string workingText; + if (altTextIndex == 0) { + workingText = MakeQRTimeText(); + } else { + workingText = altTexts[altTextIndex - 1]; + } + + const std::shared_ptr qrCodeImg = QRCodeGenerator::GenerateCode(workingText); + + lv_area_t area; + for (int x = 0; x < 33; x++) { + for (int y = 0; y < 33; y++) { + area.x1 = (lv_coord_t) (LV_HOR_RES * (x + quietZoneSize) / (33. + 2 * quietZoneSize)); + area.y1 = (lv_coord_t) (LV_VER_RES * (y + quietZoneSize) / (33. + 2 * quietZoneSize)); + area.x2 = (lv_coord_t) (LV_HOR_RES * (x + quietZoneSize + 1) / (33. + 2 * quietZoneSize) - 1); + area.y2 = (lv_coord_t) (LV_VER_RES * (y + quietZoneSize + 1) / (33. + 2 * quietZoneSize) - 1); + lvgl.SetFullRefresh(Components::LittleVgl::FullRefreshDirections::None); + + if ((int) qrCodeImg->getBit2D(x, y) == 0) { + lvgl.FlushDisplay(&area, whiteBuffer.get()); + } else { + lvgl.FlushDisplay(&area, blackBuffer.get()); + } + } + } +} + +BitByteArray::BitByteArray(const uint16_t sizeBytes) : size {sizeBytes} { + this->byteArray = std::unique_ptr(new uint8_t[sizeBytes]); + std::fill_n(byteArray.get(), sizeBytes, 0); +} + +/// Size of array in bytes +uint16_t BitByteArray::length() const { + return size; +} + +uint8_t& BitByteArray::operator[](const uint16_t index) { + assert(index < this->size); + return byteArray[index]; +} + +uint8_t BitByteArray::getByte(const uint16_t index) const { + assert(index < this->size); + return byteArray[index]; +} + +void BitByteArray::setByte(const uint16_t index, const uint8_t value) { + assert(index < this->size); + byteArray[index] = value; +} + +bool BitByteArray::getBit(const uint16_t index) const { + assert(index < this->size * 8); + return (bool) (byteArray[index / 8] & (0x80 >> index % 8)); +} + +void BitByteArray::setBit(const uint16_t index, const bool value) { + assert(index < this->size * 8); + if (!value) { + byteArray[index / 8] &= 0xFF - (0x80 >> index % 8); + } else { + byteArray[index / 8] |= 0x80 >> index % 8; + } +} + +uint8_t BitByteArray::getNybble(const uint16_t index) const { + assert(index < this->size * 2); + if (index % 2 == 1) { + return byteArray[index / 2] & 0x0F; + } + return byteArray[index / 2] & 0xF0; +} + +void BitByteArray::setNybble(const uint16_t index, const uint8_t value) { + assert(index < this->size * 2); + if (index % 2 == 1) { + byteArray[index / 2] = (byteArray[index / 2] & 0xF0) | (value & 0x0F); + } else { + byteArray[index / 2] = (byteArray[index / 2] & 0x0F) | ((value & 0x0F) << 4); + } +} + +BitByteArray2D::BitByteArray2D(const uint16_t width, const uint16_t height) : BitByteArray(width * height), width {width}, height {height} { +} + +bool BitByteArray2D::getBit2D(const uint16_t indexX, const uint16_t indexY) const { + assert(indexX < width); + assert(indexY < height); + return getBit((indexY * width) + indexX); +} + +void BitByteArray2D::setBit2D(const uint16_t indexX, const uint16_t indexY, const bool value) { + assert(indexX < width); + assert(indexY < height); + setBit((indexY * width) + indexX, value); +} + +/// Fills the area with the given value. Inclusive on all sides. +void BitByteArray2D::FillBits(const uint16_t x1, const uint16_t y1, const uint16_t x2, const uint16_t y2, const bool value) { + assert(x1 <= x2); + assert(x2 < width); + assert(y1 <= y2); + assert(y2 < height); + for (uint16_t xCoord = x1; xCoord <= x2; xCoord++) { + for (uint16_t yCoord = y1; yCoord <= y2; yCoord++) { + setBit((yCoord * width) + xCoord, value); + } + } +} + +/** + * @param text The text to convert to a QR code. Must be encoded in ISO-8859-1 (which is ASCII compatible). + * @return a 33x33 BitByteArray2D representing the modules of the code. 0 is white, 1 is black. + */ +std::shared_ptr QRCodeGenerator::GenerateCode(const std::string& text) { + std::string workingText; + if (text.length() > 78) { + workingText = "{\n\"error\": \"DATA TOO LONG\",\n\"apology\": \"Sowwy :3\"\n}"; + } else { + workingText = text; + } + + const std::shared_ptr codeData = GenerateCodeData(workingText); + AppendCodeEC(codeData); + return GenerateCodeImage(codeData); +} + +/** + * @param text The text to encode. Must be encoded in ISO-8859-1 and not be longer than 78 bytes. + * @return a 101-length BitByteArray (80 bytes populated) containing the base data, to append EC data to. + */ +std::shared_ptr QRCodeGenerator::GenerateCodeData(const std::string& text) { + auto data = std::make_shared(101); + + // QR code type and length + data->setNybble(0, 0b0100); + data->setNybble(1, ((uint8_t) text.length() & 0xF0) >> 4); + data->setNybble(2, (uint8_t) text.length() & 0x0F); + + // Insert user provided text + uint16_t nybbleIndex = 3; + for (int i = 0; i < std::min((int) text.length(), 78); i++) { + data->setNybble(nybbleIndex, (text[i] & 0xF0) >> 4); + data->setNybble(nybbleIndex + 1, text[i] & 0x0F); + nybbleIndex += 2; + } + + // Pad remainder with alternating 0xEC and 0x11 bytes + uint16_t byteIndex = (nybbleIndex + 1) / 2; + while (true) { + if (byteIndex >= 80) { + break; + } + data->setByte(byteIndex, 0xEC); + byteIndex++; + if (byteIndex >= 80) { + break; + } + data->setByte(byteIndex, 0x11); + byteIndex++; + } + + return data; +} + +/** + * Modifies the passed BitByteArray in-place to add 20 bytes of Reed-Solomon error correction at the end. + * @param baseData A BitByteArray with <=80 populated bytes and size >=100 bytes. + */ +void QRCodeGenerator::AppendCodeEC(const std::shared_ptr baseData) { + // https://www.thonky.com/qr-code-tutorial/error-correction-coding + + BitByteArray dataPolynomialInt = BitByteArray(100); + + // Copy initial polynomial state to new BitByteArray for holding the long division state + for (int i = 0; i < 80; i++) { + dataPolynomialInt[i] = baseData->getByte(i); + } + + // Polynomial long division: dataPolynomialInt / generatorPolynomialAlpha + for (uint16_t i = 0; i < 80; i++) { + // if got extra 0 in the lead, pass over it + if (dataPolynomialInt[i] == 0) { + continue; + } + // multiplier to put onto generator polynomial + const uint16_t multiplierAlpha = intToAlpha[dataPolynomialInt[i]]; + // add multiplied generator polynomial to the message polynomial (by xor) + for (uint16_t j = 0; j < 21; j++) { + dataPolynomialInt[i + j] ^= alphaToInt[(generatorPolynomialAlpha[j] + multiplierAlpha) % 255]; + } + } + + // dataPolynomialInt now only has 20 digits at the tail containing the EC data, copy it over + for (int i = 80; i < 100; i++) { + baseData->setByte(i, dataPolynomialInt.getByte(i)); + } +} + +/** + * @param codeData a >=101 byte BitByteArray containing the base data + error correction data to put in the code. + * @return a 33x33 BitByteArray2D representing the modules of the code. 0 is white, 1 is black. + */ +std::shared_ptr QRCodeGenerator::GenerateCodeImage(const std::shared_ptr codeData) { + auto canvas = std::make_shared(33, 33); + + // Finder patterns + // top left + canvas->FillBits(0, 0, 7, 7, false); + canvas->FillBits(0, 0, 6, 6, true); + canvas->FillBits(1, 1, 5, 5, false); + canvas->FillBits(2, 2, 4, 4, true); + // top right + canvas->FillBits(25, 0, 32, 7, false); + canvas->FillBits(26, 0, 32, 6, true); + canvas->FillBits(27, 1, 31, 5, false); + canvas->FillBits(28, 2, 30, 4, true); + // bottom left + canvas->FillBits(0, 25, 7, 32, false); + canvas->FillBits(0, 26, 6, 32, true); + canvas->FillBits(1, 27, 5, 31, false); + canvas->FillBits(2, 28, 4, 30, true); + // alignment pattern + canvas->FillBits(24, 24, 28, 28, true); + canvas->FillBits(25, 25, 27, 27, false); + canvas->setBit2D(26, 26, true); + + // Timing patterns + for (uint16_t x = 6; x < 25; x++) { + canvas->setBit2D(x, 6, x % 2 == 0); + } + for (uint16_t y = 6; y < 25; y++) { + canvas->setBit2D(6, y, y % 2 == 0); + } + + // Dark module + canvas->setBit2D(8, 25, true); + + // Version information + BitByteArray versionInfo = BitByteArray(2); + versionInfo[0] = 0xE5; + versionInfo[1] = 0xE6; + // Version info around top left finder pattern + uint8_t versionIdx = 0; + for (uint8_t x = 0; x < 6; x++) { + canvas->setBit2D(x, 8, versionInfo.getBit(versionIdx)); + versionIdx++; + } + canvas->setBit2D(7, 8, versionInfo.getBit(6)); + canvas->setBit2D(8, 8, versionInfo.getBit(7)); + canvas->setBit2D(8, 7, versionInfo.getBit(8)); + versionIdx += 3; + for (int8_t y = 5; y > -1; y--) { + canvas->setBit2D(8, y, versionInfo.getBit(versionIdx)); + versionIdx++; + } + // Version info around bottom left and top right finder patterns + versionIdx = 0; + for (uint8_t y = 32; y > 25; y--) { + canvas->setBit2D(8, y, versionInfo.getBit(versionIdx)); + versionIdx++; + } + for (uint8_t x = 25; x < 33; x++) { + canvas->setBit2D(x, 8, versionInfo.getBit(versionIdx)); + versionIdx++; + } + + // Add data into the code + BitByteArray2D mask = BitByteArray2D(33, 33); + mask.FillBits(0, 0, 32, 32, true); + mask.FillBits(0, 0, 8, 8, false); + mask.FillBits(0, 25, 8, 32, false); + mask.FillBits(25, 0, 32, 8, false); + mask.FillBits(9, 6, 24, 6, false); + mask.FillBits(6, 9, 6, 24, false); + mask.FillBits(24, 24, 28, 28, false); + + uint16_t dataIdx = 0; + int8_t wideColX = 32; + while (wideColX > 0) { + if ((wideColX > 5 && (wideColX / 2) % 2 == 0) || (wideColX <= 5 && wideColX == 3)) { + // going up + for (int8_t y = 32; y > -1; y--) { + for (int8_t x = wideColX; x > wideColX - 2; x--) { + if (!mask.getBit2D(x, y)) { + continue; + } + canvas->setBit2D(x, y, (int) codeData->getBit(dataIdx) == y % 2); + dataIdx++; + } + } + } else { + // going down + for (int8_t y = 0; y < 33; y++) { + for (int8_t x = wideColX; x > wideColX - 2; x--) { + if (!mask.getBit2D(x, y)) { + continue; + } + canvas->setBit2D(x, y, (int) codeData->getBit(dataIdx) == y % 2); + dataIdx++; + } + } + } + wideColX -= 2; + if (wideColX == 6) { + wideColX--; + } + } + + return canvas; +} \ No newline at end of file diff --git a/src/displayapp/screens/WatchFaceQRCode.h b/src/displayapp/screens/WatchFaceQRCode.h new file mode 100644 index 0000000000..faac3de9d0 --- /dev/null +++ b/src/displayapp/screens/WatchFaceQRCode.h @@ -0,0 +1,167 @@ +#pragma once + +#include "displayapp/apps/Apps.h" +#include "displayapp/screens/Screen.h" +#include "displayapp/Controllers.h" +#include "components/datetime/DateTimeController.h" +#include "components/battery/BatteryController.h" +#include "components/ble/BleController.h" +#include "utility/DirtyValue.h" + +namespace Pinetime { + namespace Applications { + namespace Screens { + + class WatchFaceQRCode : public Screen { + public: + WatchFaceQRCode(Components::LittleVgl& lvgl, + Controllers::DateTime& dateTimeController, + const Controllers::Battery& batteryController, + const Controllers::Ble& bleController, + Controllers::Settings& settingsController, + Controllers::MotorController& motor); + ~WatchFaceQRCode() override; + + bool OnTouchEvent(Pinetime::Applications::TouchEvents event) override; + bool OnButtonPushed() override; + + void Refresh() override; + + private: + void BuzzBinary(uint8_t number) const; + void RefreshQRCode() const; + std::string MakeQRTimeText() const; + + Utility::DirtyValue batteryPercentRemaining {}; + Utility::DirtyValue powerPresent {}; + Utility::DirtyValue bleState {}; + Utility::DirtyValue> currentDateTime {}; + + // Add more strings to this to be able to cycle through them with long taps. + // Strings must be <=78 bytes long and encoded in ISO-8859-1. + // ISO-8859-1 is ASCII compatible so if you don't use unusual characters you don't need to worry about that. + // Has a limit of 255 items. + std::vector altTexts = {"https://www.youtube.com/watch?v=dQw4w9WgXcQ", + "++++[>++++<-]>+[>+++++>++++>++<<<-]>-.>++++.---.>--.<++.------.<-------.>++++.", + "While you were reading this message, I snuck a blueberry into your pocket."}; + // altTextIndex 0 means main watch screen + unsigned int altTextIndex; + bool altTextIndexUpdated; + uint32_t altTextLastChangedTime; + constexpr static uint32_t altTextTimeout = pdMS_TO_TICKS(60000); + + /// Quiet zone is the empty space around the QR code. This is expressed in pixels of the QR code itself. + /// Should not be set to 0, that will render the code unscannable. + constexpr static uint16_t quietZoneSize = 2; + std::unique_ptr blackBuffer; + std::unique_ptr whiteBuffer; + + Components::LittleVgl& lvgl; + Controllers::DateTime& dateTimeController; + const Controllers::Battery& batteryController; + const Controllers::Ble& bleController; + Controllers::Settings& settingsController; + Controllers::MotorController& motor; + + lv_task_t* taskRefresh; + }; + + /// Class for interacting with bits and nybbles in a byte array + class BitByteArray { + public: + BitByteArray(uint16_t sizeBytes); + + uint16_t length() const; + + uint8_t& operator[](uint16_t index); + uint8_t getByte(uint16_t index) const; + void setByte(uint16_t index, uint8_t value); + bool getBit(uint16_t index) const; + void setBit(uint16_t index, bool value); + uint8_t getNybble(uint16_t index) const; + void setNybble(uint16_t index, uint8_t value); + + private: + std::unique_ptr byteArray; + uint16_t size; + }; + + /// Small wrapper class to BitByteArray to interpret its bits as a 2D canvas. + /// Can be used as a normal BitByteArray. Coordinates start with (0,0) at top left. + class BitByteArray2D : BitByteArray { + public: + BitByteArray2D(uint16_t width, uint16_t height); + + bool getBit2D(uint16_t indexX, uint16_t indexY) const; + void setBit2D(uint16_t indexX, uint16_t indexY, bool value); + + void FillBits(uint16_t x1, uint16_t y1, uint16_t x2, uint16_t y2, bool value); + + private: + uint16_t width; + uint16_t height; + }; + + /// QR code generator class. Only need to call QRCodeGenerator::GenerateCode("text"). + /// Can only generate 4L codes (33x33 modules, 78 byte contents). + class QRCodeGenerator { + public: + // Not instantiable. More of a holder for data and functions related to GenerateCode(). + QRCodeGenerator() = delete; + ~QRCodeGenerator() = delete; + + static std::shared_ptr GenerateCode(const std::string& text); + + private: + static std::shared_ptr GenerateCodeData(const std::string& text); + static void AppendCodeEC(std::shared_ptr baseData); + static std::shared_ptr GenerateCodeImage(std::shared_ptr codeData); + + // intToAlpha[0] is junk, do not use + constexpr static uint8_t intToAlpha[256] = { + 0, 0, 1, 25, 2, 50, 26, 198, 3, 223, 51, 238, 27, 104, 199, 75, 4, 100, 224, 14, 52, 141, 239, 129, 28, 193, + 105, 248, 200, 8, 76, 113, 5, 138, 101, 47, 225, 36, 15, 33, 53, 147, 142, 218, 240, 18, 130, 69, 29, 181, 194, 125, + 106, 39, 249, 185, 201, 154, 9, 120, 77, 228, 114, 166, 6, 191, 139, 98, 102, 221, 48, 253, 226, 152, 37, 179, 16, 145, + 34, 136, 54, 208, 148, 206, 143, 150, 219, 189, 241, 210, 19, 92, 131, 56, 70, 64, 30, 66, 182, 163, 195, 72, 126, 110, + 107, 58, 40, 84, 250, 133, 186, 61, 202, 94, 155, 159, 10, 21, 121, 43, 78, 212, 229, 172, 115, 243, 167, 87, 7, 112, + 192, 247, 140, 128, 99, 13, 103, 74, 222, 237, 49, 197, 254, 24, 227, 165, 153, 119, 38, 184, 180, 124, 17, 68, 146, 217, + 35, 32, 137, 46, 55, 63, 209, 91, 149, 188, 207, 205, 144, 135, 151, 178, 220, 252, 190, 97, 242, 86, 211, 171, 20, 42, + 93, 158, 132, 60, 57, 83, 71, 109, 65, 162, 31, 45, 67, 216, 183, 123, 164, 118, 196, 23, 73, 236, 127, 12, 111, 246, + 108, 161, 59, 82, 41, 157, 85, 170, 251, 96, 134, 177, 187, 204, 62, 90, 203, 89, 95, 176, 156, 169, 160, 81, 11, 245, + 22, 235, 122, 117, 44, 215, 79, 174, 213, 233, 230, 231, 173, 232, 116, 214, 244, 234, 168, 80, 88, 175}; + constexpr static uint8_t alphaToInt[256] = { + 1, 2, 4, 8, 16, 32, 64, 128, 29, 58, 116, 232, 205, 135, 19, 38, 76, 152, 45, 90, 180, 117, 234, 201, 143, 3, + 6, 12, 24, 48, 96, 192, 157, 39, 78, 156, 37, 74, 148, 53, 106, 212, 181, 119, 238, 193, 159, 35, 70, 140, 5, 10, + 20, 40, 80, 160, 93, 186, 105, 210, 185, 111, 222, 161, 95, 190, 97, 194, 153, 47, 94, 188, 101, 202, 137, 15, 30, 60, + 120, 240, 253, 231, 211, 187, 107, 214, 177, 127, 254, 225, 223, 163, 91, 182, 113, 226, 217, 175, 67, 134, 17, 34, 68, 136, + 13, 26, 52, 104, 208, 189, 103, 206, 129, 31, 62, 124, 248, 237, 199, 147, 59, 118, 236, 197, 151, 51, 102, 204, 133, 23, + 46, 92, 184, 109, 218, 169, 79, 158, 33, 66, 132, 21, 42, 84, 168, 77, 154, 41, 82, 164, 85, 170, 73, 146, 57, 114, + 228, 213, 183, 115, 230, 209, 191, 99, 198, 145, 63, 126, 252, 229, 215, 179, 123, 246, 241, 255, 227, 219, 171, 75, 150, 49, + 98, 196, 149, 55, 110, 220, 165, 87, 174, 65, 130, 25, 50, 100, 200, 141, 7, 14, 28, 56, 112, 224, 221, 167, 83, 166, + 81, 162, 89, 178, 121, 242, 249, 239, 195, 155, 43, 86, 172, 69, 138, 9, 18, 36, 72, 144, 61, 122, 244, 245, 247, 243, + 251, 235, 203, 139, 11, 22, 44, 88, 176, 125, 250, 233, 207, 131, 27, 54, 108, 216, 173, 71, 142, 1}; + constexpr static uint8_t generatorPolynomialAlpha[21] = {0, 17, 60, 79, 50, 61, 163, 26, 187, 202, 180, + 221, 225, 83, 239, 156, 164, 212, 212, 188, 190}; + }; + } + + template <> + struct WatchFaceTraits { + static constexpr WatchFace watchFace = WatchFace::QRCode; + static constexpr const char* name = "QRCode"; + + static Screens::Screen* Create(AppControllers& controllers) { + return new Screens::WatchFaceQRCode(controllers.lvgl, + controllers.dateTimeController, + controllers.batteryController, + controllers.bleController, + controllers.settingsController, + controllers.motorController); + }; + + static bool IsAvailable(Pinetime::Controllers::FS& /*filesystem*/) { + return true; + } + }; + } +} \ No newline at end of file