added code from gh
This commit is contained in:
419
Firmware/src/display_task.cpp
Normal file
419
Firmware/src/display_task.cpp
Normal file
@@ -0,0 +1,419 @@
|
||||
#include "display_task.h"
|
||||
|
||||
#include <AceButton.h>
|
||||
#include <SD_MMC.h>
|
||||
#include <TFT_eSPI.h>
|
||||
#include <Update.h>
|
||||
#include <WiFi.h>
|
||||
|
||||
#include <json11.hpp>
|
||||
|
||||
#include "gif_player.h"
|
||||
|
||||
using namespace json11;
|
||||
|
||||
#define PIN_LCD_BACKLIGHT 27
|
||||
|
||||
#define PIN_SD_DAT1 4
|
||||
#define PIN_SD_DAT2 12
|
||||
|
||||
DisplayTask::DisplayTask(MainTask& main_task, const uint8_t task_core) : Task{"Display", 8192, 1, task_core}, Logger(), main_task_(main_task) {
|
||||
log_queue_ = xQueueCreate(10, sizeof(std::string *));
|
||||
assert(log_queue_ != NULL);
|
||||
|
||||
event_queue_ = xQueueCreate(10, sizeof(Event));
|
||||
assert(event_queue_ != NULL);
|
||||
}
|
||||
|
||||
int DisplayTask::enumerateGifs(const char* basePath, std::vector<std::string>& out_files) {
|
||||
int amount = 0;
|
||||
File GifRootFolder = SD_MMC.open(basePath);
|
||||
if(!GifRootFolder){
|
||||
log_n("Failed to open directory");
|
||||
return 0;
|
||||
}
|
||||
|
||||
if(!GifRootFolder.isDirectory()){
|
||||
log_n("Not a directory");
|
||||
return 0;
|
||||
}
|
||||
|
||||
File file = GifRootFolder.openNextFile();
|
||||
|
||||
while( file ) {
|
||||
if(!file.isDirectory()) {
|
||||
if(file.name()[0] != '.')
|
||||
{
|
||||
String FullPath;
|
||||
FullPath = String(basePath) + "/" + String(file.name());
|
||||
out_files.push_back( FullPath.c_str() );
|
||||
log_d("got file: %s",file.name());
|
||||
amount++;
|
||||
}
|
||||
file.close();
|
||||
}
|
||||
file = GifRootFolder.openNextFile();
|
||||
}
|
||||
GifRootFolder.close();
|
||||
log_n("Found %d GIF files", amount);
|
||||
return amount;
|
||||
}
|
||||
|
||||
|
||||
// perform the actual update from a given stream
|
||||
bool DisplayTask::performUpdate(Stream &updateSource, size_t updateSize) {
|
||||
if (Update.begin(updateSize)) {
|
||||
size_t written = Update.writeStream(updateSource);
|
||||
if (written == updateSize) {
|
||||
Serial.println("Written : " + String(written) + " successfully");
|
||||
}
|
||||
else {
|
||||
Serial.println("Written only : " + String(written) + "/" + String(updateSize) + ". Retry?");
|
||||
}
|
||||
if (Update.end()) {
|
||||
Serial.println("OTA done!");
|
||||
if (Update.isFinished()) {
|
||||
Serial.println("Update successfully completed. Rebooting.");
|
||||
tft_.fillScreen(TFT_BLACK);
|
||||
tft_.drawString("Update successful!", 0, 0);
|
||||
return true;
|
||||
}
|
||||
else {
|
||||
Serial.println("Update not finished? Something went wrong!");
|
||||
tft_.fillScreen(TFT_BLACK);
|
||||
tft_.drawString("Update error: unknown", 0, 0);
|
||||
}
|
||||
}
|
||||
else {
|
||||
uint8_t error = Update.getError();
|
||||
Serial.println("Error Occurred. Error #: " + String(error));
|
||||
tft_.fillScreen(TFT_BLACK);
|
||||
tft_.drawString("Update error: " + String(error), 0, 0);
|
||||
}
|
||||
|
||||
}
|
||||
else
|
||||
{
|
||||
Serial.println("Not enough space to begin OTA");
|
||||
tft_.fillScreen(TFT_BLACK);
|
||||
tft_.drawString("Not enough space", 0, 0);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// check given FS for valid firmware.bin and perform update if available
|
||||
bool DisplayTask::updateFromFS(fs::FS &fs) {
|
||||
tft_.fillScreen(TFT_BLACK);
|
||||
tft_.setTextDatum(TL_DATUM);
|
||||
|
||||
File updateBin = fs.open("/firmware.bin");
|
||||
if (updateBin) {
|
||||
if(updateBin.isDirectory()){
|
||||
Serial.println("Error, firmware.bin is not a file");
|
||||
updateBin.close();
|
||||
return false;
|
||||
}
|
||||
|
||||
size_t updateSize = updateBin.size();
|
||||
|
||||
bool update_successful = false;
|
||||
if (updateSize > 0) {
|
||||
Serial.println("Try to start update");
|
||||
digitalWrite(PIN_LCD_BACKLIGHT, HIGH);
|
||||
tft_.fillScreen(TFT_BLACK);
|
||||
tft_.drawString("Starting update...", 0, 0);
|
||||
delay(1000);
|
||||
update_successful = performUpdate(updateBin, updateSize);
|
||||
}
|
||||
else {
|
||||
Serial.println("Error, file is empty");
|
||||
}
|
||||
|
||||
updateBin.close();
|
||||
fs.remove("/firmware.bin");
|
||||
|
||||
// Leave some time to read the update result message
|
||||
delay(5000);
|
||||
return update_successful;
|
||||
}
|
||||
else {
|
||||
Serial.println("No firmware.bin at sd root");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
void DisplayTask::run() {
|
||||
pinMode(PIN_LCD_BACKLIGHT, OUTPUT);
|
||||
pinMode(PIN_SD_DAT1, INPUT_PULLUP);
|
||||
pinMode(PIN_SD_DAT2, INPUT_PULLUP);
|
||||
|
||||
tft_.begin();
|
||||
#ifdef USE_DMA
|
||||
tft_.initDMA();
|
||||
#endif
|
||||
tft_.setRotation(1);
|
||||
tft_.fillScreen(TFT_BLACK);
|
||||
|
||||
bool isblinked = false;
|
||||
while(! SD_MMC.begin("/sdcard", false) ) {
|
||||
digitalWrite(PIN_LCD_BACKLIGHT, HIGH);
|
||||
log_n("SD Card mount failed!");
|
||||
isblinked = !isblinked;
|
||||
if( isblinked ) {
|
||||
tft_.setTextColor( TFT_WHITE, TFT_BLACK );
|
||||
} else {
|
||||
tft_.setTextColor( TFT_BLACK, TFT_WHITE );
|
||||
}
|
||||
tft_.setTextDatum(TC_DATUM);
|
||||
tft_.drawString( "INSERT SD", tft_.width()/2, tft_.height()/2 );
|
||||
|
||||
delay( 300 );
|
||||
}
|
||||
|
||||
log_n("SD Card mounted!");
|
||||
|
||||
if (updateFromFS(SD_MMC)) {
|
||||
ESP.restart();
|
||||
}
|
||||
|
||||
// #####################################################
|
||||
// CHANGES ABOVE THIS LINE MAY BREAK FIRMWARE UPDATES!!!
|
||||
// #####################################################
|
||||
|
||||
main_task_.setLogger(this);
|
||||
|
||||
// Load config from SD card
|
||||
File configFile = SD_MMC.open("/config.json");
|
||||
if (configFile) {
|
||||
if(configFile.isDirectory()){
|
||||
log("Error, config.json is not a file");
|
||||
} else {
|
||||
char data[512];
|
||||
size_t data_len = configFile.readBytes(data, sizeof(data) - 1);
|
||||
data[data_len] = 0;
|
||||
|
||||
std::string err;
|
||||
Json json = Json::parse(data, err);
|
||||
if (err.empty()) {
|
||||
show_log_ = json["show_log"].bool_value();
|
||||
const char* ssid = json["ssid"].string_value().c_str();
|
||||
const char* password = json["password"].string_value().c_str();
|
||||
Serial.printf("Wifi info: %s %s\n", ssid, password);
|
||||
|
||||
const char* tz = json["timezone"].string_value().c_str();
|
||||
Serial.printf("Timezone: %s\n", tz);
|
||||
|
||||
main_task_.setConfig(ssid, password, tz);
|
||||
} else {
|
||||
log("Error parsing wifi credentials! " + String(err.c_str()));
|
||||
}
|
||||
}
|
||||
configFile.close();
|
||||
} else {
|
||||
log("Missing config file!");
|
||||
}
|
||||
|
||||
// Delay to avoid brownout while wifi is starting
|
||||
delay(500);
|
||||
|
||||
GifPlayer::begin(&tft_);
|
||||
|
||||
if (GifPlayer::start("/gifs/boot.gif")) {
|
||||
GifPlayer::play_frame(nullptr);
|
||||
delay(50);
|
||||
digitalWrite(PIN_LCD_BACKLIGHT, HIGH);
|
||||
delay(200);
|
||||
while (GifPlayer::play_frame(nullptr)) {
|
||||
yield();
|
||||
}
|
||||
digitalWrite(PIN_LCD_BACKLIGHT, LOW);
|
||||
delay(500);
|
||||
GifPlayer::stop();
|
||||
}
|
||||
|
||||
std::vector<std::string> main_gifs;
|
||||
std::vector<std::string> christmas_gifs;
|
||||
|
||||
int num_main_gifs = enumerateGifs( "/gifs/main", main_gifs);
|
||||
int num_christmas_gifs = enumerateGifs( "/gifs/christmas", christmas_gifs);
|
||||
int current_file = -1;
|
||||
const char* current_file_name = "";
|
||||
uint32_t minimum_loop_duration = 0;
|
||||
uint32_t start_millis = UINT32_MAX;
|
||||
|
||||
bool last_christmas; // I gave you my heart...
|
||||
|
||||
main_task_.registerEventQueue(event_queue_);
|
||||
|
||||
State state = State::CHOOSE_GIF;
|
||||
int frame_delay = 0;
|
||||
uint32_t last_frame = 0;
|
||||
while (1) {
|
||||
bool left_button = false;
|
||||
bool right_button = false;
|
||||
Event event;
|
||||
if (xQueueReceive(event_queue_, &event, 0)) {
|
||||
switch (event.type) {
|
||||
case EventType::BUTTON:
|
||||
if (event.button.event == ace_button::AceButton::kEventPressed) {
|
||||
if (event.button.button_id == BUTTON_ID_LEFT) {
|
||||
left_button = true;
|
||||
} else if (event.button.button_id == BUTTON_ID_RIGHT) {
|
||||
right_button = true;
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
handleLogRendering();
|
||||
switch (state) {
|
||||
case State::CHOOSE_GIF:
|
||||
Serial.println("Choose gif");
|
||||
if (millis() - start_millis > minimum_loop_duration) {
|
||||
// Only change the file if we've exceeded the minimum loop duration
|
||||
if (isChristmas()) {
|
||||
if (num_christmas_gifs > 0) {
|
||||
current_file_name = christmas_gifs[current_file++ % num_christmas_gifs].c_str();
|
||||
minimum_loop_duration = 30000;
|
||||
Serial.printf("Chose christmas gif: %s\n", current_file_name);
|
||||
} else {
|
||||
continue;
|
||||
}
|
||||
} else {
|
||||
if (num_main_gifs > 0) {
|
||||
int next_file = current_file;
|
||||
while (num_main_gifs > 1 && next_file == current_file) {
|
||||
next_file = random(num_main_gifs);
|
||||
}
|
||||
current_file = next_file;
|
||||
current_file_name = main_gifs[current_file].c_str();
|
||||
minimum_loop_duration = 0;
|
||||
Serial.printf("Chose gif: %s\n", current_file_name);
|
||||
} else {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
start_millis = millis();
|
||||
}
|
||||
if (!GifPlayer::start(current_file_name)) {
|
||||
continue;
|
||||
}
|
||||
last_frame = millis();
|
||||
GifPlayer::play_frame(&frame_delay);
|
||||
delay(50);
|
||||
digitalWrite(PIN_LCD_BACKLIGHT, HIGH);
|
||||
state = State::PLAY_GIF;
|
||||
break;
|
||||
case State::PLAY_GIF: {
|
||||
if (right_button) {
|
||||
GifPlayer::stop();
|
||||
int center = tft_.width()/2;
|
||||
tft_.fillScreen(TFT_BLACK);
|
||||
tft_.setTextSize(2);
|
||||
tft_.setTextDatum(TC_DATUM);
|
||||
tft_.drawString("Merry Christmas!", center, 10);
|
||||
tft_.setTextSize(1);
|
||||
tft_.drawString("Designed and handmade", center, 50);
|
||||
tft_.drawString("by Scott Bezek", center, 60);
|
||||
tft_.drawString("Oakland, 2021", center, 80);
|
||||
|
||||
if (WiFi.status() == WL_CONNECTED) {
|
||||
tft_.setTextDatum(BL_DATUM);
|
||||
tft_.drawString(String("IP: ") + WiFi.localIP().toString(), 5, tft_.height());
|
||||
}
|
||||
main_task_.setOtaEnabled(true);
|
||||
delay(200);
|
||||
state = State::SHOW_CREDITS;
|
||||
break;
|
||||
}
|
||||
bool is_christmas = isChristmas();
|
||||
bool christmas_changed = false;
|
||||
if (is_christmas != last_christmas) {
|
||||
last_christmas = is_christmas;
|
||||
christmas_changed = true;
|
||||
}
|
||||
|
||||
if (left_button || christmas_changed) {
|
||||
// Force select new gif, even if we hadn't met the minimum loop duration yet
|
||||
minimum_loop_duration = 0;
|
||||
GifPlayer::stop();
|
||||
state = State::CHOOSE_GIF;
|
||||
break;
|
||||
}
|
||||
uint32_t time_since_last_frame = millis() - last_frame;
|
||||
if (time_since_last_frame > frame_delay) {
|
||||
// Time for the next frame; play it
|
||||
last_frame = millis();
|
||||
if (!GifPlayer::play_frame(&frame_delay)) {
|
||||
GifPlayer::stop();
|
||||
state = State::CHOOSE_GIF;
|
||||
break;
|
||||
}
|
||||
} else {
|
||||
// Wait until it's time for the next frame, but up to 50ms max at a time to avoid stalling UI thread
|
||||
delay(min((uint32_t)50, frame_delay - time_since_last_frame));
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
case State::SHOW_CREDITS:
|
||||
if (right_button) {
|
||||
// Exit credits
|
||||
main_task_.setOtaEnabled(false);
|
||||
state = State::CHOOSE_GIF;
|
||||
tft_.fillScreen(TFT_BLACK);
|
||||
delay(200);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
bool DisplayTask::isChristmas() {
|
||||
tm local;
|
||||
return main_task_.getLocalTime(&local) && local.tm_mon == 11 && local.tm_mday == 25;
|
||||
}
|
||||
|
||||
void DisplayTask::handleLogRendering() {
|
||||
uint32_t now = millis();
|
||||
// Check for new message
|
||||
bool force_redraw = false;
|
||||
if (now - last_message_millis_ > 100) {
|
||||
std::string* log_string;
|
||||
if (xQueueReceive(log_queue_, &log_string, 0) == pdTRUE) {
|
||||
last_message_millis_ = now;
|
||||
force_redraw = true;
|
||||
strncpy(current_message_, log_string->c_str(), sizeof(current_message_));
|
||||
delete log_string;
|
||||
}
|
||||
}
|
||||
|
||||
bool show = show_log_ && (now - last_message_millis_ < 3000);
|
||||
|
||||
if (show && (!message_visible_ || force_redraw)) {
|
||||
GifPlayer::set_max_line(124);
|
||||
tft_.fillRect(0, 124, DISPLAY_WIDTH, 11, TFT_BLACK);
|
||||
tft_.setTextSize(1);
|
||||
tft_.setTextDatum(TL_DATUM);
|
||||
tft_.drawString(current_message_, 3, 126);
|
||||
} else if (!show && message_visible_) {
|
||||
tft_.fillRect(0, 124, DISPLAY_WIDTH, 11, TFT_BLACK);
|
||||
GifPlayer::set_max_line(-1);
|
||||
}
|
||||
message_visible_ = show;
|
||||
}
|
||||
|
||||
void DisplayTask::log(const char* msg) {
|
||||
Serial.println(msg);
|
||||
// Allocate a string for the duration it's in the queue; it is free'd by the queue consumer
|
||||
std::string* msg_str = new std::string(msg);
|
||||
|
||||
// Put string in queue (or drop if full to avoid blocking)
|
||||
if (xQueueSendToBack(log_queue_, &msg_str, 0) != pdTRUE) {
|
||||
delete msg_str;
|
||||
}
|
||||
}
|
||||
|
||||
void DisplayTask::log(String msg) {
|
||||
log(msg.c_str());
|
||||
}
|
||||
48
Firmware/src/display_task.h
Normal file
48
Firmware/src/display_task.h
Normal file
@@ -0,0 +1,48 @@
|
||||
#pragma once
|
||||
|
||||
#include <Arduino.h>
|
||||
#include <SD_MMC.h>
|
||||
#include <TFT_eSPI.h>
|
||||
|
||||
#include "logger.h"
|
||||
#include "main_task.h"
|
||||
#include "task.h"
|
||||
|
||||
enum class State {
|
||||
CHOOSE_GIF,
|
||||
PLAY_GIF,
|
||||
SHOW_CREDITS,
|
||||
};
|
||||
|
||||
class DisplayTask : public Task<DisplayTask>, public Logger {
|
||||
friend class Task<DisplayTask>; // Allow base Task to invoke protected run()
|
||||
|
||||
public:
|
||||
DisplayTask(MainTask& main_task, const uint8_t task_core);
|
||||
virtual ~DisplayTask() {};
|
||||
|
||||
void log(const char* msg) override;
|
||||
|
||||
protected:
|
||||
void run();
|
||||
|
||||
private:
|
||||
bool performUpdate(Stream &updateSource, size_t updateSize);
|
||||
bool updateFromFS(fs::FS &fs);
|
||||
int enumerateGifs( const char* basePath, std::vector<std::string>& out_files);
|
||||
bool isChristmas();
|
||||
void handleLogRendering();
|
||||
|
||||
void log(String msg);
|
||||
|
||||
TFT_eSPI tft_ = TFT_eSPI();
|
||||
MainTask& main_task_;
|
||||
QueueHandle_t log_queue_;
|
||||
QueueHandle_t event_queue_;
|
||||
|
||||
bool show_log_ = false;
|
||||
bool message_visible_ = false;
|
||||
char current_message_[200];
|
||||
uint32_t last_message_millis_ = UINT32_MAX;
|
||||
|
||||
};
|
||||
22
Firmware/src/event.h
Normal file
22
Firmware/src/event.h
Normal file
@@ -0,0 +1,22 @@
|
||||
#pragma once
|
||||
|
||||
#include <stdint.h>
|
||||
|
||||
#define BUTTON_ID_LEFT 0
|
||||
#define BUTTON_ID_RIGHT 1
|
||||
|
||||
enum class EventType {
|
||||
BUTTON,
|
||||
};
|
||||
|
||||
struct EventButton {
|
||||
uint8_t button_id;
|
||||
uint8_t event;
|
||||
};
|
||||
|
||||
struct Event {
|
||||
EventType type;
|
||||
union {
|
||||
EventButton button;
|
||||
};
|
||||
};
|
||||
225
Firmware/src/gif_player.cpp
Normal file
225
Firmware/src/gif_player.cpp
Normal file
@@ -0,0 +1,225 @@
|
||||
#include "gif_player.h"
|
||||
|
||||
#include <AnimatedGIF.h>
|
||||
#include <SD_MMC.h>
|
||||
#include <TFT_eSPI.h>
|
||||
|
||||
AnimatedGIF GifPlayer::gif;
|
||||
TFT_eSPI* GifPlayer::tft;
|
||||
|
||||
File GifPlayer::FSGifFile; // temp gif file holder
|
||||
|
||||
#ifdef USE_DMA
|
||||
uint16_t GifPlayer::usTemp[2][BUFFER_SIZE]; // Global to support DMA use
|
||||
#else
|
||||
uint16_t GifPlayer::usTemp[1][BUFFER_SIZE]; // Global to support DMA use
|
||||
#endif
|
||||
bool GifPlayer::dmaBuf;
|
||||
|
||||
int GifPlayer::frame_delay;
|
||||
int GifPlayer::max_line = -1;
|
||||
|
||||
|
||||
void * GifPlayer::GIFOpenFile(const char *fname, int32_t *pSize)
|
||||
{
|
||||
//log_d("GIFOpenFile( %s )\n", fname );
|
||||
FSGifFile = SD_MMC.open(fname);
|
||||
if (FSGifFile) {
|
||||
*pSize = FSGifFile.size();
|
||||
return (void *)&FSGifFile;
|
||||
}
|
||||
return NULL;
|
||||
}
|
||||
|
||||
|
||||
void GifPlayer::GIFCloseFile(void *pHandle)
|
||||
{
|
||||
File *f = static_cast<File *>(pHandle);
|
||||
if (f != NULL)
|
||||
f->close();
|
||||
}
|
||||
|
||||
|
||||
int32_t GifPlayer::GIFReadFile(GIFFILE *pFile, uint8_t *pBuf, int32_t iLen)
|
||||
{
|
||||
int32_t iBytesRead;
|
||||
iBytesRead = iLen;
|
||||
File *f = static_cast<File *>(pFile->fHandle);
|
||||
// Note: If you read a file all the way to the last byte, seek() stops working
|
||||
if ((pFile->iSize - pFile->iPos) < iLen)
|
||||
iBytesRead = pFile->iSize - pFile->iPos - 1; // <-- ugly work-around
|
||||
if (iBytesRead <= 0)
|
||||
return 0;
|
||||
iBytesRead = (int32_t)f->read(pBuf, iBytesRead);
|
||||
pFile->iPos = f->position();
|
||||
return iBytesRead;
|
||||
}
|
||||
|
||||
|
||||
int32_t GifPlayer::GIFSeekFile(GIFFILE *pFile, int32_t iPosition)
|
||||
{
|
||||
int i = micros();
|
||||
File *f = static_cast<File *>(pFile->fHandle);
|
||||
f->seek(iPosition);
|
||||
pFile->iPos = (int32_t)f->position();
|
||||
i = micros() - i;
|
||||
//log_d("Seek time = %d us\n", i);
|
||||
return pFile->iPos;
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
// From AnimatedGIF TFT_eSPI_memory example
|
||||
|
||||
// Draw a line of image directly on the LCD
|
||||
void GifPlayer::GIFDraw(GIFDRAW *pDraw)
|
||||
{
|
||||
uint8_t *s;
|
||||
uint16_t *d, *usPalette;
|
||||
int x, y, iWidth, iCount;
|
||||
|
||||
// Displ;ay bounds chech and cropping
|
||||
iWidth = pDraw->iWidth;
|
||||
if (iWidth + pDraw->iX > DISPLAY_WIDTH)
|
||||
iWidth = DISPLAY_WIDTH - pDraw->iX;
|
||||
usPalette = pDraw->pPalette;
|
||||
y = pDraw->iY + pDraw->y; // current line
|
||||
if (y >= DISPLAY_HEIGHT || pDraw->iX >= DISPLAY_WIDTH || iWidth < 1 || (max_line > -1 && y > max_line))
|
||||
return;
|
||||
|
||||
// Old image disposal
|
||||
s = pDraw->pPixels;
|
||||
if (pDraw->ucDisposalMethod == 2) // restore to background color
|
||||
{
|
||||
for (x = 0; x < iWidth; x++)
|
||||
{
|
||||
if (s[x] == pDraw->ucTransparent)
|
||||
s[x] = pDraw->ucBackground;
|
||||
}
|
||||
pDraw->ucHasTransparency = 0;
|
||||
}
|
||||
|
||||
// Apply the new pixels to the main image
|
||||
if (pDraw->ucHasTransparency) // if transparency used
|
||||
{
|
||||
uint8_t *pEnd, c, ucTransparent = pDraw->ucTransparent;
|
||||
pEnd = s + iWidth;
|
||||
x = 0;
|
||||
iCount = 0; // count non-transparent pixels
|
||||
while (x < iWidth)
|
||||
{
|
||||
c = ucTransparent - 1;
|
||||
d = &usTemp[0][0];
|
||||
while (c != ucTransparent && s < pEnd && iCount < BUFFER_SIZE )
|
||||
{
|
||||
c = *s++;
|
||||
if (c == ucTransparent) // done, stop
|
||||
{
|
||||
s--; // back up to treat it like transparent
|
||||
}
|
||||
else // opaque
|
||||
{
|
||||
*d++ = usPalette[c];
|
||||
iCount++;
|
||||
}
|
||||
} // while looking for opaque pixels
|
||||
if (iCount) // any opaque pixels?
|
||||
{
|
||||
// DMA would degrtade performance here due to short line segments
|
||||
tft->setAddrWindow(pDraw->iX + x, y, iCount, 1);
|
||||
tft->pushPixels(usTemp, iCount);
|
||||
x += iCount;
|
||||
iCount = 0;
|
||||
}
|
||||
// no, look for a run of transparent pixels
|
||||
c = ucTransparent;
|
||||
while (c == ucTransparent && s < pEnd)
|
||||
{
|
||||
c = *s++;
|
||||
if (c == ucTransparent)
|
||||
x++;
|
||||
else
|
||||
s--;
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
s = pDraw->pPixels;
|
||||
|
||||
// Unroll the first pass to boost DMA performance
|
||||
// Translate the 8-bit pixels through the RGB565 palette (already byte reversed)
|
||||
if (iWidth <= BUFFER_SIZE)
|
||||
for (iCount = 0; iCount < iWidth; iCount++) usTemp[dmaBuf][iCount] = usPalette[*s++];
|
||||
else
|
||||
for (iCount = 0; iCount < BUFFER_SIZE; iCount++) usTemp[dmaBuf][iCount] = usPalette[*s++];
|
||||
|
||||
#ifdef USE_DMA // 71.6 fps (ST7796 84.5 fps)
|
||||
tft->dmaWait();
|
||||
tft->setAddrWindow(pDraw->iX, y, iWidth, 1);
|
||||
tft->pushPixelsDMA(&usTemp[dmaBuf][0], iCount);
|
||||
dmaBuf = !dmaBuf;
|
||||
#else // 57.0 fps
|
||||
tft->setAddrWindow(pDraw->iX, y, iWidth, 1);
|
||||
tft->pushPixels(&usTemp[0][0], iCount);
|
||||
#endif
|
||||
|
||||
iWidth -= iCount;
|
||||
// Loop if pixel buffer smaller than width
|
||||
while (iWidth > 0)
|
||||
{
|
||||
// Translate the 8-bit pixels through the RGB565 palette (already byte reversed)
|
||||
if (iWidth <= BUFFER_SIZE)
|
||||
for (iCount = 0; iCount < iWidth; iCount++) usTemp[dmaBuf][iCount] = usPalette[*s++];
|
||||
else
|
||||
for (iCount = 0; iCount < BUFFER_SIZE; iCount++) usTemp[dmaBuf][iCount] = usPalette[*s++];
|
||||
|
||||
#ifdef USE_DMA
|
||||
tft->dmaWait();
|
||||
tft->pushPixelsDMA(&usTemp[dmaBuf][0], iCount);
|
||||
dmaBuf = !dmaBuf;
|
||||
#else
|
||||
tft->pushPixels(&usTemp[0][0], iCount);
|
||||
#endif
|
||||
iWidth -= iCount;
|
||||
}
|
||||
}
|
||||
} /* GIFDraw() */
|
||||
|
||||
|
||||
|
||||
|
||||
bool GifPlayer::start(const char* path) {
|
||||
gif.begin(BIG_ENDIAN_PIXELS);
|
||||
|
||||
if( ! gif.open( path, GIFOpenFile, GIFCloseFile, GIFReadFile, GIFSeekFile, GIFDraw ) ) {
|
||||
log_n("Could not open gif %s", path );
|
||||
return false;
|
||||
}
|
||||
|
||||
tft->startWrite();
|
||||
return true;
|
||||
}
|
||||
|
||||
bool GifPlayer::play_frame(int* frame_delay) {
|
||||
bool sync = frame_delay == nullptr;
|
||||
return gif.playFrame(sync, frame_delay) == 1;
|
||||
|
||||
}
|
||||
|
||||
void GifPlayer::stop() {
|
||||
gif.close();
|
||||
tft->endWrite();
|
||||
gif.reset();
|
||||
}
|
||||
|
||||
void GifPlayer::begin(TFT_eSPI* tft) {
|
||||
GifPlayer::tft = tft;
|
||||
|
||||
dmaBuf = 0;
|
||||
}
|
||||
|
||||
void GifPlayer::set_max_line(int l) {
|
||||
max_line = l;
|
||||
}
|
||||
45
Firmware/src/gif_player.h
Normal file
45
Firmware/src/gif_player.h
Normal file
@@ -0,0 +1,45 @@
|
||||
#pragma once
|
||||
|
||||
#include <AnimatedGIF.h>
|
||||
#include <Arduino.h>
|
||||
#include <SD_MMC.h>
|
||||
#include <TFT_eSPI.h>
|
||||
|
||||
#define DISPLAY_WIDTH 240
|
||||
#define DISPLAY_HEIGHT 135
|
||||
// #define USE_DMA 1
|
||||
#define BUFFER_SIZE 256 // Optimum is >= GIF width or integral division of width
|
||||
|
||||
class GifPlayer {
|
||||
private:
|
||||
static AnimatedGIF gif;
|
||||
static TFT_eSPI* tft;
|
||||
|
||||
static File FSGifFile; // temp gif file holder
|
||||
|
||||
#ifdef USE_DMA
|
||||
static uint16_t usTemp[2][BUFFER_SIZE]; // Global to support DMA use
|
||||
#else
|
||||
static uint16_t usTemp[1][BUFFER_SIZE]; // Global to support DMA use
|
||||
#endif
|
||||
static bool dmaBuf;
|
||||
|
||||
static int frame_delay;
|
||||
static int max_line;
|
||||
|
||||
static void * GIFOpenFile(const char *fname, int32_t *pSize);
|
||||
static void GIFCloseFile(void *pHandle);
|
||||
static int32_t GIFReadFile(GIFFILE *pFile, uint8_t *pBuf, int32_t iLen);
|
||||
static int32_t GIFSeekFile(GIFFILE *pFile, int32_t iPosition);
|
||||
static void GIFDraw(GIFDRAW *pDraw);
|
||||
|
||||
public:
|
||||
static void begin(TFT_eSPI* tft);
|
||||
|
||||
static bool start(const char* path);
|
||||
static bool play_frame(int* frame_delay);
|
||||
static void stop();
|
||||
|
||||
static void set_max_line(int l);
|
||||
|
||||
};
|
||||
9
Firmware/src/logger.h
Normal file
9
Firmware/src/logger.h
Normal file
@@ -0,0 +1,9 @@
|
||||
#pragma once
|
||||
|
||||
class Logger {
|
||||
public:
|
||||
Logger() {};
|
||||
virtual ~Logger() {};
|
||||
virtual void log(const char* msg) = 0;
|
||||
|
||||
};
|
||||
21
Firmware/src/main.cpp
Normal file
21
Firmware/src/main.cpp
Normal file
@@ -0,0 +1,21 @@
|
||||
#include <Arduino.h>
|
||||
|
||||
#include "display_task.h"
|
||||
#include "main_task.h"
|
||||
|
||||
MainTask main_task = MainTask(0);
|
||||
DisplayTask display_task = DisplayTask(main_task, 1);
|
||||
|
||||
void setup() {
|
||||
Serial.begin(115200);
|
||||
|
||||
main_task.begin();
|
||||
display_task.begin();
|
||||
|
||||
vTaskDelete(NULL);
|
||||
}
|
||||
|
||||
|
||||
void loop() {
|
||||
assert(false);
|
||||
}
|
||||
219
Firmware/src/main_task.cpp
Normal file
219
Firmware/src/main_task.cpp
Normal file
@@ -0,0 +1,219 @@
|
||||
#include "main_task.h"
|
||||
|
||||
#include <AceButton.h>
|
||||
#include <ArduinoOTA.h>
|
||||
#include <lwip/apps/sntp.h>
|
||||
#include <time.h>
|
||||
#include <WiFi.h>
|
||||
|
||||
#include "semaphore_guard.h"
|
||||
|
||||
#define TASK_NOTIFY_SET_CONFIG (1 << 0)
|
||||
|
||||
#define MDNS_NAME "switchOrnament"
|
||||
#define OTA_PASSWORD "hunter2"
|
||||
|
||||
#define PIN_LEFT_BUTTON 32
|
||||
#define PIN_RIGHT_BUTTON 26
|
||||
|
||||
using namespace ace_button;
|
||||
|
||||
MainTask::MainTask(const uint8_t task_core) : Task{"Main", 8192, 1, task_core}, semaphore_(xSemaphoreCreateMutex()) {
|
||||
assert(semaphore_ != NULL);
|
||||
xSemaphoreGive(semaphore_);
|
||||
}
|
||||
|
||||
MainTask::~MainTask() {
|
||||
if (semaphore_ != NULL) {
|
||||
vSemaphoreDelete(semaphore_);
|
||||
}
|
||||
}
|
||||
|
||||
void MainTask::run() {
|
||||
WiFi.mode(WIFI_STA);
|
||||
|
||||
AceButton left_button(PIN_LEFT_BUTTON, 1, BUTTON_ID_LEFT);
|
||||
AceButton right_button(PIN_RIGHT_BUTTON, 1, BUTTON_ID_RIGHT);
|
||||
|
||||
pinMode(PIN_LEFT_BUTTON, INPUT_PULLUP);
|
||||
pinMode(PIN_RIGHT_BUTTON, INPUT_PULLUP);
|
||||
|
||||
ButtonConfig* config = ButtonConfig::getSystemButtonConfig();
|
||||
config->setIEventHandler(this);
|
||||
|
||||
ArduinoOTA
|
||||
.onStart([this]() {
|
||||
String type;
|
||||
if (ArduinoOTA.getCommand() == U_FLASH)
|
||||
type = "(flash)";
|
||||
else // U_SPIFFS
|
||||
type = "(filesystem)";
|
||||
|
||||
// NOTE: if updating SPIFFS this would be the place to unmount SPIFFS using SPIFFS.end()
|
||||
log("Start OTA " + type);
|
||||
})
|
||||
.onEnd([this]() {
|
||||
log("OTA End");
|
||||
})
|
||||
.onProgress([this](unsigned int progress, unsigned int total) {
|
||||
static uint32_t last_progress;
|
||||
if (millis() - last_progress > 1000) {
|
||||
log("OTA Progress: " + String((int)(progress * 100 / total)) + "%");
|
||||
last_progress = millis();
|
||||
}
|
||||
})
|
||||
.onError([this](ota_error_t error) {
|
||||
log("Error[%u]: " + String(error));
|
||||
if (error == OTA_AUTH_ERROR) log("Auth Failed");
|
||||
else if (error == OTA_BEGIN_ERROR) log("Begin Failed");
|
||||
else if (error == OTA_CONNECT_ERROR) log("Connect Failed");
|
||||
else if (error == OTA_RECEIVE_ERROR) log("Receive Failed");
|
||||
else if (error == OTA_END_ERROR) log("End Failed");
|
||||
});
|
||||
ArduinoOTA.setHostname(MDNS_NAME);
|
||||
ArduinoOTA.setPassword(OTA_PASSWORD);
|
||||
|
||||
wl_status_t wifi_status = WL_DISCONNECTED;
|
||||
while (1) {
|
||||
uint32_t notify_value = 0;
|
||||
if (xTaskNotifyWait(0, ULONG_MAX, ¬ify_value, 0) == pdTRUE) {
|
||||
if (notify_value && TASK_NOTIFY_SET_CONFIG) {
|
||||
String wifi_ssid, wifi_password, timezone;
|
||||
{
|
||||
SemaphoreGuard lock(semaphore_);
|
||||
wifi_ssid = wifi_ssid_;
|
||||
wifi_password = wifi_password_;
|
||||
timezone = timezone_;
|
||||
}
|
||||
setenv("TZ", timezone.c_str(), 1);
|
||||
tzset();
|
||||
|
||||
char buf[200];
|
||||
snprintf(buf, sizeof(buf), "Connecting to %s...", wifi_ssid.c_str());
|
||||
log(buf);
|
||||
WiFi.begin(wifi_ssid.c_str(), wifi_password.c_str());
|
||||
}
|
||||
}
|
||||
|
||||
wl_status_t new_status = WiFi.status();
|
||||
if (new_status != wifi_status) {
|
||||
char buf[200];
|
||||
snprintf(buf, sizeof(buf), "Wifi status changed to %d\n", new_status);
|
||||
log(buf);
|
||||
if (new_status == WL_CONNECTED) {
|
||||
snprintf(buf, sizeof(buf), "IP: %s", WiFi.localIP().toString().c_str());
|
||||
log(buf);
|
||||
|
||||
delay(100);
|
||||
// Sync SNTP
|
||||
sntp_setoperatingmode(SNTP_OPMODE_POLL);
|
||||
|
||||
char server[] = "time.nist.gov"; // sntp_setservername takes a non-const char*, so use a non-const variable to avoid warning
|
||||
sntp_setservername(0, server);
|
||||
sntp_init();
|
||||
}
|
||||
|
||||
wifi_status = new_status;
|
||||
}
|
||||
|
||||
|
||||
time_t now = 0;
|
||||
bool ntp_just_synced = false;
|
||||
{
|
||||
SemaphoreGuard lock(semaphore_);
|
||||
if (!ntp_synced_) {
|
||||
// Check if NTP has synced yet
|
||||
time(&now);
|
||||
if (now > 1625099485) {
|
||||
ntp_just_synced = true;
|
||||
ntp_synced_ = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (ntp_just_synced) {
|
||||
// We do this separately from above to avoid deadlock: log() requires semaphore_ and we're non-reentrant-locking
|
||||
char buf[200];
|
||||
strftime(buf, sizeof(buf), "Got time: %Y-%m-%d %H:%M:%S", localtime(&now));
|
||||
Serial.printf("%s\n", buf);
|
||||
log(buf);
|
||||
}
|
||||
|
||||
ArduinoOTA.handle();
|
||||
left_button.check();
|
||||
right_button.check();
|
||||
delay(1);
|
||||
}
|
||||
}
|
||||
|
||||
void MainTask::setConfig(const char* wifi_ssid, const char* wifi_password, const char* timezone) {
|
||||
{
|
||||
SemaphoreGuard lock(semaphore_);
|
||||
wifi_ssid_ = String(wifi_ssid);
|
||||
wifi_password_ = String(wifi_password);
|
||||
timezone_ = String(timezone);
|
||||
}
|
||||
xTaskNotify(getHandle(), TASK_NOTIFY_SET_CONFIG, eSetBits);
|
||||
}
|
||||
|
||||
bool MainTask::getLocalTime(tm* t) {
|
||||
SemaphoreGuard lock(semaphore_);
|
||||
if (!ntp_synced_) {
|
||||
return false;
|
||||
}
|
||||
time_t now = 0;
|
||||
time(&now);
|
||||
localtime_r(&now, t);
|
||||
return true;
|
||||
}
|
||||
|
||||
void MainTask::setLogger(Logger* logger) {
|
||||
SemaphoreGuard lock(semaphore_);
|
||||
logger_ = logger;
|
||||
}
|
||||
|
||||
void MainTask::setOtaEnabled(bool enabled) {
|
||||
if (enabled) {
|
||||
ArduinoOTA.begin();
|
||||
} else {
|
||||
ArduinoOTA.end();
|
||||
}
|
||||
}
|
||||
|
||||
void MainTask::log(const char* message) {
|
||||
SemaphoreGuard lock(semaphore_);
|
||||
if (logger_ != nullptr) {
|
||||
logger_->log(message);
|
||||
} else {
|
||||
Serial.println(message);
|
||||
}
|
||||
}
|
||||
|
||||
void MainTask::log(String message) {
|
||||
log(message.c_str());
|
||||
}
|
||||
|
||||
void MainTask::registerEventQueue(QueueHandle_t queue) {
|
||||
SemaphoreGuard lock(semaphore_);
|
||||
event_queues_.push_back(queue);
|
||||
}
|
||||
|
||||
void MainTask::publishEvent(Event event) {
|
||||
SemaphoreGuard lock(semaphore_);
|
||||
for (QueueHandle_t queue : event_queues_) {
|
||||
xQueueSend(queue, &event, 0);
|
||||
}
|
||||
}
|
||||
|
||||
void MainTask::handleEvent(AceButton* button, uint8_t event_type, uint8_t button_state) {
|
||||
Event event = {
|
||||
.type = EventType::BUTTON,
|
||||
{
|
||||
.button = {
|
||||
.button_id = button->getId(),
|
||||
.event = event_type,
|
||||
},
|
||||
}
|
||||
};
|
||||
publishEvent(event);
|
||||
}
|
||||
48
Firmware/src/main_task.h
Normal file
48
Firmware/src/main_task.h
Normal file
@@ -0,0 +1,48 @@
|
||||
#pragma once
|
||||
|
||||
#include <Arduino.h>
|
||||
#include <vector>
|
||||
|
||||
#include <AceButton.h>
|
||||
|
||||
#include "event.h"
|
||||
#include "logger.h"
|
||||
#include "task.h"
|
||||
|
||||
class MainTask : public Task<MainTask>, public ace_button::IEventHandler {
|
||||
friend class Task<MainTask>; // Allow base Task to invoke protected run()
|
||||
|
||||
public:
|
||||
MainTask(const uint8_t task_core);
|
||||
virtual ~MainTask();
|
||||
|
||||
void setConfig(const char* wifi_ssid, const char* wifi_password, const char* timezone);
|
||||
bool getLocalTime(tm* t);
|
||||
void setLogger(Logger* logger);
|
||||
void setOtaEnabled(bool enabled);
|
||||
void registerEventQueue(QueueHandle_t queue);
|
||||
|
||||
void handleEvent(ace_button::AceButton* button, uint8_t event_type, uint8_t button_state) override;
|
||||
|
||||
protected:
|
||||
void run();
|
||||
|
||||
private:
|
||||
|
||||
void log(const char* message);
|
||||
void log(String message);
|
||||
|
||||
void publishEvent(Event event);
|
||||
|
||||
SemaphoreHandle_t semaphore_;
|
||||
|
||||
String wifi_ssid_;
|
||||
String wifi_password_;
|
||||
String timezone_;
|
||||
|
||||
bool ntp_synced_ = false;
|
||||
|
||||
Logger* logger_ = nullptr;
|
||||
|
||||
std::vector<QueueHandle_t> event_queues_;
|
||||
};
|
||||
33
Firmware/src/semaphore_guard.h
Normal file
33
Firmware/src/semaphore_guard.h
Normal file
@@ -0,0 +1,33 @@
|
||||
/*
|
||||
Copyright 2020 Scott Bezek and the splitflap contributors
|
||||
|
||||
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.
|
||||
*/
|
||||
#pragma once
|
||||
|
||||
#include <Arduino.h>
|
||||
|
||||
class SemaphoreGuard {
|
||||
public:
|
||||
SemaphoreGuard(SemaphoreHandle_t handle) : handle_{handle} {
|
||||
xSemaphoreTake(handle_, portMAX_DELAY);
|
||||
}
|
||||
~SemaphoreGuard() {
|
||||
xSemaphoreGive(handle_);
|
||||
}
|
||||
SemaphoreGuard(SemaphoreGuard const&)=delete;
|
||||
SemaphoreGuard& operator=(SemaphoreGuard const&)=delete;
|
||||
|
||||
private:
|
||||
SemaphoreHandle_t handle_;
|
||||
};
|
||||
54
Firmware/src/task.h
Normal file
54
Firmware/src/task.h
Normal file
@@ -0,0 +1,54 @@
|
||||
/*
|
||||
Copyright 2020 Scott Bezek and the splitflap contributors
|
||||
|
||||
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.
|
||||
*/
|
||||
#pragma once
|
||||
|
||||
#include<Arduino.h>
|
||||
|
||||
// Static polymorphic abstract base class for a FreeRTOS task using CRTP pattern. Concrete implementations
|
||||
// should implement a run() method.
|
||||
// Inspired by https://fjrg76.wordpress.com/2018/05/23/objectifying-task-creation-in-freertos-ii/
|
||||
template<class T>
|
||||
class Task {
|
||||
public:
|
||||
Task(const char* name, uint32_t stackDepth, UBaseType_t priority, const BaseType_t coreId = tskNO_AFFINITY) :
|
||||
name { name },
|
||||
stackDepth {stackDepth},
|
||||
priority { priority },
|
||||
coreId { coreId }
|
||||
{}
|
||||
virtual ~Task() {};
|
||||
|
||||
TaskHandle_t getHandle() {
|
||||
return taskHandle;
|
||||
}
|
||||
|
||||
void begin() {
|
||||
BaseType_t result = xTaskCreatePinnedToCore(taskFunction, name, stackDepth, this, priority, &taskHandle, coreId);
|
||||
assert("Failed to create task" && result == pdPASS);
|
||||
}
|
||||
|
||||
private:
|
||||
static void taskFunction(void* params) {
|
||||
T* t = static_cast<T*>(params);
|
||||
t->run();
|
||||
}
|
||||
|
||||
const char* name;
|
||||
uint32_t stackDepth;
|
||||
UBaseType_t priority;
|
||||
TaskHandle_t taskHandle;
|
||||
const BaseType_t coreId;
|
||||
};
|
||||
Reference in New Issue
Block a user