added code from gh

This commit is contained in:
2023-12-23 20:37:34 +01:00
parent 675fda89a1
commit 757ad51899
27 changed files with 2932 additions and 0 deletions

View 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());
}

View 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
View 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
View 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
View 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
View 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
View 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
View 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, &notify_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
View 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_;
};

View 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
View 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;
};