Compare commits

..
5 Commits
Author SHA1 Message Date
mitchell 562b6cea8b start bbnow and deps 2026-07-19 17:49:47 -05:00
mitchell 59d038efc2 add tft 2026-07-19 16:05:07 -05:00
mitchell 0d761b048f scaffold for bbnow 2026-07-19 15:59:00 -05:00
mitchell 30244c6d7b correct button colors 2026-04-10 17:14:49 +00:00
mitchell 75052174a6 game timers 2026-04-04 16:35:45 -05:00
18 changed files with 1389 additions and 20 deletions
+13
View File
@@ -1,10 +1,23 @@
idf_component_register(
SRCS "blk_box.cpp"
INCLUDE_DIRS "include" "."
REQUIRES
lvgl
PRIV_REQUIRES
led_strip
esp_driver_gpio
esp_driver_i2c
esp_driver_spi
esp_timer
esp_lcd
esp_event
esp_netif
esp_wifi
nvs_flash
)
target_compile_options(${COMPONENT_LIB} PRIVATE
-Wno-missing-field-initializers
)
add_subdirectory(drivers)
+11
View File
@@ -4,10 +4,21 @@
#include "blk_box_drivers/inputs.hpp"
#include "blk_box_drivers/leds.hpp"
#include "blk_box_drivers/char_lcd.hpp"
#include "blk_box_drivers/ssegs.hpp"
#include "blk_box_drivers/tft.hpp"
#include "blk_box_drivers/nvs.hpp"
#include "blk_box_drivers/radio.hpp"
#include "blk_box_drivers/bbnow.hpp"
void init_blk_box(BlkBoxInitConfig cfg) {
init_main_i2c();
init_expander();
init_leds();
init_lcd();
init_ssegs();
init_tft();
init_nvs();
init_radio();
init_espnow();
}
+6
View File
@@ -1,4 +1,5 @@
set(SOURCES
"bbnow.cpp"
"char_lcd_headers.cpp"
"char_lcd.cpp"
"helpers.cpp"
@@ -6,6 +7,11 @@ set(SOURCES
"i2c.cpp"
"lcd2004.cpp"
"leds.cpp"
"nvs.cpp"
"radio.cpp"
"ssegs.cpp"
"tft.cpp"
"tm1640.cpp"
)
target_sources(${COMPONENT_LIB} PRIVATE ${SOURCES})
+66
View File
@@ -0,0 +1,66 @@
#include "blk_box_drivers/bbnow.hpp"
#include "esp_now.h"
#include "esp_log.h"
#include "esp_mac.h"
#include <memory.h>
const static char TAG[] = "bbnow";
static void example_espnow_send_cb(const esp_now_send_info_t *tx_info, esp_now_send_status_t status);
static void example_espnow_recv_cb(const esp_now_recv_info_t *recv_info, const uint8_t *data, int len);
/* ESPNOW sending or receiving callback function is called in WiFi task.
* Users should not do lengthy operations from this task. Instead, post
* necessary data to a queue and handle it from a lower priority task. */
static void example_espnow_send_cb(const esp_now_send_info_t *tx_info, esp_now_send_status_t status)
{
if (tx_info == NULL) {
ESP_LOGE(TAG, "Send cb arg error");
return;
}
ESP_LOGI(TAG, "sending espnow packet of size: %d to " MACSTR, tx_info->data_len, MAC2STR(tx_info->des_addr));
}
static void example_espnow_recv_cb(const esp_now_recv_info_t *recv_info, const uint8_t *data, int len)
{
uint8_t * src_addr = recv_info->src_addr;
uint8_t * des_addr = recv_info->des_addr;
if (src_addr == NULL || data == NULL || len <= 0) {
ESP_LOGE(TAG, "Receive cb arg error");
return;
}
ESP_LOGI(TAG, "got espnow packet of size: %d from " MACSTR " to " MACSTR, MAC2STR(src_addr), MAC2STR(des_addr));
// if (IS_BROADCAST_ADDR(des_addr)) {
// /* If added a peer with encryption before, the receive packets may be
// * encrypted as peer-to-peer message or unencrypted over the broadcast channel.
// * Users can check the destination address to distinguish it.
// */
// ESP_LOGD(TAG, "Receive broadcast ESPNOW data");
// } else {
// ESP_LOGD(TAG, "Receive unicast ESPNOW data");
// }
}
// TODO: add "initializing _____..."
// and "____ initialized!" logs to all driver init functions.
void init_espnow(void) {
ESP_ERROR_CHECK( esp_now_init() );
ESP_ERROR_CHECK( esp_now_register_send_cb(example_espnow_send_cb) );
ESP_ERROR_CHECK( esp_now_register_recv_cb(example_espnow_recv_cb) );
/* Set primary master key. */
// TODO: add provisions for encryption
// ESP_ERROR_CHECK( esp_now_set_pmk(ESPNOW_PMK));
// Add broadcast peer information to peer list, allowing us to send to it
esp_now_peer_info_t peer = {0};
peer.channel = BBNOW_DEFAULT_CHANNEL;
peer.ifidx = WIFI_IF_AP;
peer.encrypt = false;
memcpy(peer.peer_addr, BROADCAST_MAC, ESP_NOW_ETH_ALEN);
ESP_ERROR_CHECK( esp_now_add_peer(&peer) );
}
+13
View File
@@ -0,0 +1,13 @@
#include "blk_box_drivers/nvs.hpp"
#include "nvs_flash.h"
void init_nvs() {
// TODO: do more once we are doing more with nvs.
esp_err_t ret = nvs_flash_init();
if (ret == ESP_ERR_NVS_NO_FREE_PAGES || ret == ESP_ERR_NVS_NEW_VERSION_FOUND) {
// TODO: dont just erase, but also handle the case where we have to upgrade the nvs partition.
ESP_ERROR_CHECK( nvs_flash_erase() );
ret = nvs_flash_init();
}
ESP_ERROR_CHECK( ret );
}
+18
View File
@@ -0,0 +1,18 @@
#include "blk_box_drivers/radio.hpp"
#include "blk_box_drivers/bbnow.hpp"
#include "esp_wifi.h"
void init_radio() {
// TODO: Do more once we are doing wifi in addition to espnow
ESP_ERROR_CHECK(esp_netif_init());
ESP_ERROR_CHECK(esp_event_loop_create_default());
wifi_init_config_t cfg = WIFI_INIT_CONFIG_DEFAULT();
ESP_ERROR_CHECK( esp_wifi_init(&cfg) );
ESP_ERROR_CHECK( esp_wifi_set_storage(WIFI_STORAGE_RAM) );
ESP_ERROR_CHECK( esp_wifi_set_mode(WIFI_MODE_AP) );
ESP_ERROR_CHECK( esp_wifi_start());
ESP_ERROR_CHECK( esp_wifi_set_channel(BBNOW_DEFAULT_CHANNEL, WIFI_SECOND_CHAN_NONE));
// enable long range
// ESP_ERROR_CHECK( esp_wifi_set_protocol(ESPNOW_WIFI_IF, WIFI_PROTOCOL_11B|WIFI_PROTOCOL_11G|WIFI_PROTOCOL_11N|WIFI_PROTOCOL_LR) );
}
+485
View File
@@ -0,0 +1,485 @@
#include "blk_box_drivers/ssegs.hpp"
#include "tm1640.hpp"
#include "pins.h"
#include "freertos/FreeRTOS.h"
#include "freertos/queue.h"
#include "freertos/event_groups.h"
#include "esp_log.h"
#include <atomic>
#include <cmath>
#include <tuple>
const static uint32_t TICKER_PERIOD_MS = 100;
const static uint8_t MODULE_IDX = 0;
const static uint8_t GAME_IDX = 4;
TM1640 ssegs(PIN_SSEG_CLK, PIN_SSEG_DAT);
const static size_t CMD_QUEUE_SIZE = 10;
QueueHandle_t cmd_queue;
std::atomic<int32_t> game_time = 0;
std::atomic<int32_t> module_time = 0;
// for notifying users of events
const static uint32_t EVENT_CMDS_FLUSHED = (1 << 0);
const static uint32_t EVENT_MODULE_POSITIVE = (1 << 1);
const static uint32_t EVENT_MODULE_ZERO_NEG = (1 << 2);
const static uint32_t EVENT_GAME_POSITIVE = (1 << 3);
const static uint32_t EVENT_GAME_ZERO_NEG = (1 << 4);
EventGroupHandle_t ssegs_event_group;
const static char* TAG = "ssegs";
/// Uses a compare-exchange loop to do a saturating subtraction on an atomic int32_t. Returns the old and new values.
std::pair<int32_t, int32_t> saturating_sub(std::atomic<int32_t>& v, int32_t sub) {
int32_t cur = v.load(std::memory_order_relaxed);
while (true) {
int32_t desired = (cur <= sub) ? 0 : cur - sub;
if (v.compare_exchange_weak(cur, desired,
std::memory_order_relaxed)) {
return {cur, desired}; // {old value, new value}
}
// cur is updated automatically with latest value on failure
}
}
/// Updates the segment buffer to reflect the current time.
///
/// Returns `true` if the buffer has changed and needs to be redrawn.
///
/// `seg_buf.len()` should be >= 4.
bool update_segments(int32_t last_time, int32_t current_time, uint8_t seg_buf[4]) {
const uint32_t MILLIS_10S = 100;
const uint32_t SECOND = 1000;
const uint32_t SECOND_10S = SECOND * 10;
const uint32_t MINUTE = 60 * SECOND;
const uint32_t MINUTE_10S = 10 * MINUTE;
const uint32_t HOUR = 60 * MINUTE;
const uint32_t HOUR_10S = 10 * HOUR;
uint32_t time = std::abs(current_time);
if (time > HOUR) {
// HH.MM
if ((current_time / MINUTE) == (last_time / MINUTE)) {
// no change neccesary
return false;
}
uint8_t h1 = (time / HOUR_10S) % 10;
uint8_t h0 = (time / HOUR) % 10;
uint8_t minutes = (time / MINUTE) % 60;
uint8_t m1 = minutes / 10;
uint8_t m0 = minutes % 10;
seg_buf[0] = SSegController::FONT_HEX[h1];
seg_buf[1] = SSegController::FONT_HEX[h0] | SSegController::BIT_MASK_DP;
seg_buf[2] = SSegController::FONT_HEX[m1];
seg_buf[3] = SSegController::FONT_HEX[m0];
return true;
} else if (time > MINUTE) {
// MM.SS
if ((current_time / SECOND) == (last_time / SECOND)) {
// no change neccesary
return false;
}
uint8_t m1 = (time / MINUTE_10S) % 10;
uint8_t m0 = (time / MINUTE) % 10;
uint8_t seconds = (time / SECOND) % 60;
uint8_t s1 = seconds / 10;
uint8_t s0 = seconds % 10;
seg_buf[0] = SSegController::FONT_HEX[m1];
seg_buf[1] = SSegController::FONT_HEX[m0] | SSegController::BIT_MASK_DP;
seg_buf[2] = SSegController::FONT_HEX[s1];
seg_buf[3] = SSegController::FONT_HEX[s0];
return true;
} else {
// SS.m
if ((current_time / MILLIS_10S) == (last_time / MILLIS_10S)) {
// no change neccesary
return false;
}
uint8_t s1 = (time / SECOND_10S) % 10;
uint8_t s0 = (time / SECOND) % 10;
uint8_t m1 = (time / MILLIS_10S) % 10;
seg_buf[0] = 0; // unused digit
seg_buf[1] = SSegController::FONT_HEX[s1];
seg_buf[2] = SSegController::FONT_HEX[s0] | SSegController::BIT_MASK_DP;
seg_buf[3] = SSegController::FONT_HEX[m1];
return true;
}
return true;
}
static void timer_task(void* arg) {
(void) arg;
const TickType_t ticker_period_ticks = pdMS_TO_TICKS(TICKER_PERIOD_MS);
ESP_LOGI(TAG, "sseg timer task starting...");
bool game_en = false;
bool game_running = false;
bool game_rollover = true;
bool module_en = false;
bool module_running = false;
bool module_rollover = false;
uint8_t seg_buf[4] = {0};
TickType_t last_wake_time = xTaskGetTickCount();
SSegCommand cmd;
while (true) {
TickType_t elapsed = xTaskGetTickCount() - last_wake_time;
if ((ticker_period_ticks > elapsed) && (xQueueReceive(cmd_queue, &cmd, ticker_period_ticks - elapsed) == pdPASS)) {
// command received
ESP_LOGI(TAG, "sseg command received");
switch (cmd.type) {
case SSegCommand::Type::SetIntensity: {
uint8_t intensity = std::get<uint8_t>(cmd.data);
ssegs.set_intensity(intensity);
break;
}
case SSegCommand::Type::EnableGameTimer: {
game_en = true;
int32_t game_time_val = game_time.load(std::memory_order_acquire);
if (update_segments(std::numeric_limits<int32_t>::max(), game_time_val, seg_buf)) {
ssegs.set_digits(GAME_IDX, seg_buf, 4);
}
break;
}
case SSegCommand::Type::DisableGameTimer: {
game_en = false;
game_running = false;
game_time.store(0, std::memory_order_release);
xEventGroupClearBits(ssegs_event_group, EVENT_GAME_POSITIVE | EVENT_GAME_ZERO_NEG);
for (uint8_t& seg : seg_buf) {
seg = 0;
}
ssegs.set_digits(GAME_IDX, seg_buf, 4);
break;
}
case SSegCommand::Type::StartGameTimer:
game_running = true;
break;
case SSegCommand::Type::StopGameTimer:
game_running = false;
break;
case SSegCommand::Type::SetGameTime: {
int32_t new_time = std::get<int32_t>(cmd.data);
int32_t last_time = game_time.exchange(new_time, std::memory_order_acq_rel);
if (new_time > 0) {
xEventGroupSetBits(ssegs_event_group, EVENT_GAME_POSITIVE);
xEventGroupClearBits(ssegs_event_group, EVENT_GAME_ZERO_NEG);
} else {
xEventGroupSetBits(ssegs_event_group, EVENT_GAME_ZERO_NEG);
xEventGroupClearBits(ssegs_event_group, EVENT_GAME_POSITIVE);
}
if (game_en) {
if (update_segments(last_time, new_time, seg_buf)) {
ssegs.set_digits(GAME_IDX, seg_buf, 4);
}
}
break;
}
case SSegCommand::Type::EnableModuleTimer: {
module_en = true;
int32_t module_time_val = module_time.load(std::memory_order_acquire);
if (update_segments(std::numeric_limits<int32_t>::max(), module_time_val, seg_buf)) {
ssegs.set_digits(MODULE_IDX, seg_buf, 4);
}
break;
}
case SSegCommand::Type::DisableModuleTimer: {
module_en = false;
module_running = false;
module_time.store(0, std::memory_order_release);
xEventGroupClearBits(ssegs_event_group, EVENT_MODULE_POSITIVE | EVENT_MODULE_ZERO_NEG);
for (uint8_t& seg : seg_buf) {
seg = 0;
}
ssegs.set_digits(MODULE_IDX, seg_buf, 4);
break;
}
case SSegCommand::Type::StartModuleTimer:
module_running = true;
break;
case SSegCommand::Type::StopModuleTimer:
module_running = false;
break;
case SSegCommand::Type::SetModuleTime: {
int32_t new_time = std::get<int32_t>(cmd.data);
int32_t last_time = module_time.exchange(new_time, std::memory_order_acq_rel);
if (new_time > 0) {
xEventGroupSetBits(ssegs_event_group, EVENT_MODULE_POSITIVE);
xEventGroupClearBits(ssegs_event_group, EVENT_MODULE_ZERO_NEG);
} else {
xEventGroupSetBits(ssegs_event_group, EVENT_MODULE_ZERO_NEG);
xEventGroupClearBits(ssegs_event_group, EVENT_MODULE_POSITIVE);
}
if (module_en) {
if (update_segments(last_time, new_time, seg_buf)) {
ssegs.set_digits(MODULE_IDX, seg_buf, 4);
}
}
break;
}
case SSegCommand::Type::SetGameRaw: {
std::array<uint8_t, 4> raw = std::get<std::array<uint8_t, 4>>(cmd.data);
ssegs.set_digits(GAME_IDX, raw.data(), 4);
break;
}
case SSegCommand::Type::SetGameDigit: {
auto [digit, value] = std::get<std::pair<uint8_t, uint8_t>>(cmd.data);
ssegs.set_digit(GAME_IDX + digit, value);
break;
}
case SSegCommand::Type::SetModuleRaw: {
std::array<uint8_t, 4> raw = std::get<std::array<uint8_t, 4>>(cmd.data);
ssegs.set_digits(MODULE_IDX, raw.data(), 4);
break;
}
case SSegCommand::Type::SetModuleDigit: {
auto [digit, value] = std::get<std::pair<uint8_t, uint8_t>>(cmd.data);
ssegs.set_digit(MODULE_IDX + digit, value);
break;
}
case SSegCommand::Type::SetGameRollover: {
bool rollover = std::get<bool>(cmd.data);
game_rollover = rollover;
break;
}
case SSegCommand::Type::SetModuleRollover: {
bool rollover = std::get<bool>(cmd.data);
module_rollover = rollover;
break;
}
}
if (uxQueueMessagesWaiting(cmd_queue) == 0) {
xEventGroupSetBits(ssegs_event_group, EVENT_CMDS_FLUSHED);
}
} else {
// ticker finished
last_wake_time += pdMS_TO_TICKS(TICKER_PERIOD_MS);
bool update_module = module_en && module_running;
bool update_game = game_en && game_running;
// ESP_LOGI(TAG, "ticker ticked: update_game=%d, update_module=%d", update_game, update_module);
if (update_module) {
int32_t old_time;
int32_t new_time;
if (module_rollover) {
old_time = module_time.fetch_sub(TICKER_PERIOD_MS);
new_time = old_time - TICKER_PERIOD_MS; // fetch_sub returns old value
} else {
std::tie(old_time, new_time) = saturating_sub(module_time, TICKER_PERIOD_MS);
}
if (new_time > 0) {
xEventGroupSetBits(ssegs_event_group, EVENT_MODULE_POSITIVE);
xEventGroupClearBits(ssegs_event_group, EVENT_MODULE_ZERO_NEG);
} else {
xEventGroupSetBits(ssegs_event_group, EVENT_MODULE_ZERO_NEG);
xEventGroupClearBits(ssegs_event_group, EVENT_MODULE_POSITIVE);
}
if (update_segments(old_time, new_time, seg_buf)) {
ssegs.set_digits(MODULE_IDX, seg_buf, 4);
}
if (new_time == 0 && !module_rollover) {
// we've hit 0 and are not rolling over
module_running = false;
}
}
if (update_game) {
int32_t old_time;
int32_t new_time;
if (game_rollover) {
old_time = game_time.fetch_sub(TICKER_PERIOD_MS);
new_time = old_time - TICKER_PERIOD_MS; // fetch_sub returns old value
} else {
std::tie(old_time, new_time) = saturating_sub(game_time, TICKER_PERIOD_MS);
}
if (new_time > 0) {
xEventGroupSetBits(ssegs_event_group, EVENT_GAME_POSITIVE);
xEventGroupClearBits(ssegs_event_group, EVENT_GAME_ZERO_NEG);
} else {
xEventGroupSetBits(ssegs_event_group, EVENT_GAME_ZERO_NEG);
xEventGroupClearBits(ssegs_event_group, EVENT_GAME_POSITIVE);
}
if (update_segments(old_time, new_time, seg_buf)) {
ssegs.set_digits(GAME_IDX, seg_buf, 4);
}
if (new_time == 0 && !game_rollover) {
// we've hit 0 and are not rolling over
game_running = false;
}
}
}
}
}
// SSegController static method implementations
void SSegController::enable_game_timer() {
xEventGroupClearBits(ssegs_event_group, EVENT_CMDS_FLUSHED);
SSegCommand cmd = SSegCommand::EnableGameTimer();
xQueueSend(cmd_queue, &cmd, portMAX_DELAY);
}
void SSegController::disable_game_timer() {
xEventGroupClearBits(ssegs_event_group, EVENT_CMDS_FLUSHED);
SSegCommand cmd = SSegCommand::DisableGameTimer();
xQueueSend(cmd_queue, &cmd, portMAX_DELAY);
}
void SSegController::start_game_timer() {
xEventGroupClearBits(ssegs_event_group, EVENT_CMDS_FLUSHED);
SSegCommand cmd = SSegCommand::StartGameTimer();
xQueueSend(cmd_queue, &cmd, portMAX_DELAY);
}
void SSegController::stop_game_timer() {
xEventGroupClearBits(ssegs_event_group, EVENT_CMDS_FLUSHED);
SSegCommand cmd = SSegCommand::StopGameTimer();
xQueueSend(cmd_queue, &cmd, portMAX_DELAY);
}
void SSegController::set_game_time(int32_t millis) {
// Align to TICKER_PERIOD_MS
millis = millis - (millis % TICKER_PERIOD_MS);
xEventGroupClearBits(ssegs_event_group, EVENT_CMDS_FLUSHED);
SSegCommand cmd = SSegCommand::SetGameTime(millis);
xQueueSend(cmd_queue, &cmd, portMAX_DELAY);
}
void SSegController::enable_module_timer() {
xEventGroupClearBits(ssegs_event_group, EVENT_CMDS_FLUSHED);
SSegCommand cmd = SSegCommand::EnableModuleTimer();
xQueueSend(cmd_queue, &cmd, portMAX_DELAY);
}
void SSegController::disable_module_timer() {
xEventGroupClearBits(ssegs_event_group, EVENT_CMDS_FLUSHED);
SSegCommand cmd = SSegCommand::DisableModuleTimer();
xQueueSend(cmd_queue, &cmd, portMAX_DELAY);
}
void SSegController::start_module_timer() {
xEventGroupClearBits(ssegs_event_group, EVENT_CMDS_FLUSHED);
SSegCommand cmd = SSegCommand::StartModuleTimer();
xQueueSend(cmd_queue, &cmd, portMAX_DELAY);
}
void SSegController::stop_module_timer() {
xEventGroupClearBits(ssegs_event_group, EVENT_CMDS_FLUSHED);
SSegCommand cmd = SSegCommand::StopModuleTimer();
xQueueSend(cmd_queue, &cmd, portMAX_DELAY);
}
void SSegController::set_module_time(int32_t millis) {
// Align to TICKER_PERIOD_MS
millis = millis - (millis % TICKER_PERIOD_MS);
xEventGroupClearBits(ssegs_event_group, EVENT_CMDS_FLUSHED);
SSegCommand cmd = SSegCommand::SetModuleTime(millis);
xQueueSend(cmd_queue, &cmd, portMAX_DELAY);
}
void SSegController::set_game_raw(const std::array<uint8_t, 4>& segments) {
xEventGroupClearBits(ssegs_event_group, EVENT_CMDS_FLUSHED);
SSegCommand cmd = SSegCommand::SetGameRaw(segments);
xQueueSend(cmd_queue, &cmd, portMAX_DELAY);
}
void SSegController::set_game_digit_raw(uint8_t digit, uint8_t segments) {
xEventGroupClearBits(ssegs_event_group, EVENT_CMDS_FLUSHED);
SSegCommand cmd = SSegCommand::SetGameDigit(digit, segments);
xQueueSend(cmd_queue, &cmd, portMAX_DELAY);
}
void SSegController::set_module_raw(const std::array<uint8_t, 4>& segments) {
xEventGroupClearBits(ssegs_event_group, EVENT_CMDS_FLUSHED);
SSegCommand cmd = SSegCommand::SetModuleRaw(segments);
xQueueSend(cmd_queue, &cmd, portMAX_DELAY);
}
void SSegController::set_module_digit_raw(uint8_t digit, uint8_t segments) {
xEventGroupClearBits(ssegs_event_group, EVENT_CMDS_FLUSHED);
SSegCommand cmd = SSegCommand::SetModuleDigit(digit, segments);
xQueueSend(cmd_queue, &cmd, portMAX_DELAY);
}
void SSegController::game_timer_rollover(bool rollover) {
xEventGroupClearBits(ssegs_event_group, EVENT_CMDS_FLUSHED);
SSegCommand cmd = SSegCommand::SetGameRollover(rollover);
xQueueSend(cmd_queue, &cmd, portMAX_DELAY);
}
void SSegController::module_timer_rollover(bool rollover) {
xEventGroupClearBits(ssegs_event_group, EVENT_CMDS_FLUSHED);
SSegCommand cmd = SSegCommand::SetModuleRollover(rollover);
xQueueSend(cmd_queue, &cmd, portMAX_DELAY);
}
int32_t SSegController::get_game_time() {
return game_time.load(std::memory_order_acquire);
}
int32_t SSegController::get_module_time() {
return module_time.load(std::memory_order_acquire);
}
// TODO: take timeout to these functions \/
void SSegController::flush() {
xEventGroupWaitBits(ssegs_event_group, EVENT_CMDS_FLUSHED, pdFALSE, pdTRUE, portMAX_DELAY);
}
void SSegController::wait_game_timer_done() {
xEventGroupWaitBits(ssegs_event_group, EVENT_GAME_ZERO_NEG, pdTRUE, pdFALSE, portMAX_DELAY);
}
void SSegController::wait_module_timer_done() {
xEventGroupWaitBits(ssegs_event_group, EVENT_MODULE_ZERO_NEG, pdTRUE, pdFALSE, portMAX_DELAY);
}
void SSegController::set_intensity(uint8_t intensity) {
xEventGroupClearBits(ssegs_event_group, EVENT_CMDS_FLUSHED);
SSegCommand cmd = SSegCommand::SetIntensity(intensity);
xQueueSend(cmd_queue, &cmd, portMAX_DELAY);
}
void init_ssegs() {
ssegs.init();
cmd_queue = xQueueCreate(CMD_QUEUE_SIZE, sizeof(SSegCommand));
if (cmd_queue == NULL) {
ESP_LOGE(TAG, "Failed to create command queue!");
return;
}
ssegs_event_group = xEventGroupCreate();
if (ssegs_event_group == NULL) {
ESP_LOGE(TAG, "Failed to create event group!");
return;
}
xTaskCreate(timer_task, "ssegs_timer_task", 4096, NULL, 4, NULL);
}
+249
View File
@@ -0,0 +1,249 @@
#include "blk_box_drivers/tft.hpp"
#include "pins.h"
#include <freertos/FreeRTOS.h>
#include <esp_log.h>
#include <esp_lcd_panel_io.h>
#include <esp_lcd_panel_ops.h>
#include <esp_lcd_ili9488.h>
#include <esp_log.h>
#include <esp_timer.h>
static const char* TAG = "tft";
static esp_lcd_panel_io_handle_t lcd_io_handle = NULL;
static esp_lcd_panel_handle_t lcd_handle = NULL;
static lv_disp_draw_buf_t lv_disp_buf;
static lv_disp_drv_t lv_disp_drv;
static lv_disp_t *lv_display = NULL;
static lv_color_t *lv_buf_1 = NULL;
static lv_color_t *lv_buf_2 = NULL;
lv_obj_t* screen;
static lv_style_t style_screen;
SemaphoreHandle_t xGuiSemaphore;
// static bool replay_handler(const char* event, char* arg) {
// return false;
// }
static bool notify_lvgl_flush_ready(
esp_lcd_panel_io_handle_t panel_io,
esp_lcd_panel_io_event_data_t *edata,
void *user_ctx
) {
lv_disp_drv_t *disp_driver = (lv_disp_drv_t *)user_ctx;
lv_disp_flush_ready(disp_driver);
return false;
}
// const char base64_chars[] = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
// /// Base 64 encodes a u16... sort of. This doesn't do any of the fancy padding stuff.
// static void encode_base64(char* buf, size_t start_idx, uint16_t value) {
// buf[start_idx+0] = base64_chars[(value >> 10) & 0x3F];
// buf[start_idx+1] = base64_chars[(value >> 4) & 0x3F];
// buf[start_idx+2] = base64_chars[(value << 2) & 0x3F];
// }
static void lvgl_flush_cb(lv_disp_drv_t *drv, const lv_area_t *area, lv_color_t *color_map) {
esp_lcd_panel_handle_t panel_handle = (esp_lcd_panel_handle_t) drv->user_data;
int offsetx1 = area->x1;
int offsetx2 = area->x2;
int offsety1 = area->y1;
int offsety2 = area->y2;
esp_lcd_panel_draw_bitmap(panel_handle, offsetx1, offsety1, offsetx2 + 1, offsety2 + 1, color_map);
// TODO: change this to be a kconfig value
#if false
if (is_state_tracking()) {
size_t size = (offsetx2 + 1 - offsetx1) * (offsety2 + 1 - offsety1) + 1;
// if (size > 1024) {
// ESP_LOGW("tft_track_state", "Write too big (%d)! truncating to 1024!", size);
// }
// size = MIN(1024, size);
// 24 bytes for the offsets
// 3 bytes per encoded color
// 1 byte for null terminator
size_t alloc_size = 24 + size * 3 + 1;
char* buf = (char*)malloc(alloc_size);
if (buf != nullptr) {
size_t initial_offset = sprintf(buf, "%d,%d,%d,%d:", offsetx1, offsety1, offsetx2 + 1, offsety2 + 1);
for (size_t i = 0; i < size; i++) {
size_t index = initial_offset + i * 3;
// we assume that the size of the color data is 16b
static_assert(sizeof(lv_color_t) == sizeof(uint16_t), "lv_color_t must be 16b wide");
encode_base64(buf, index, color_map[i].full);
}
buf[initial_offset + (size-1) * 3 + 1] = '\0';
event_occured("TFT_W", buf);
free(buf);
} else {
ESP_LOGE("tft_track_state", "buffer alloc failed!");
}
}
#endif
}
static void IRAM_ATTR lv_tick_task(void *param) {
lv_tick_inc(LVGL_UPDATE_PERIOD_MS);
}
static void initialize_spi() {
ESP_LOGI(TAG, "Initializing SPI bus (MOSI:%d, MISO:%d, CLK:%d)",
PIN_TFT_MOSI, PIN_TFT_MISO, PIN_TFT_CLK);
spi_bus_config_t bus = {
.mosi_io_num = PIN_TFT_MOSI,
.miso_io_num = PIN_TFT_MISO,
.sclk_io_num = PIN_TFT_CLK,
.quadwp_io_num = GPIO_NUM_NC,
.quadhd_io_num = GPIO_NUM_NC,
.data4_io_num = GPIO_NUM_NC,
.data5_io_num = GPIO_NUM_NC,
.data6_io_num = GPIO_NUM_NC,
.data7_io_num = GPIO_NUM_NC,
.max_transfer_sz = SPI_MAX_TRANSFER_SIZE,
.flags = SPICOMMON_BUSFLAG_SCLK | SPICOMMON_BUSFLAG_MISO |
SPICOMMON_BUSFLAG_MOSI | SPICOMMON_BUSFLAG_MASTER,
.isr_cpu_id = ESP_INTR_CPU_AFFINITY_AUTO,
.intr_flags = ESP_INTR_FLAG_LOWMED | ESP_INTR_FLAG_IRAM
};
ESP_ERROR_CHECK(spi_bus_initialize(SPI2_HOST, &bus, SPI_DMA_CH_AUTO));
}
static void initialize_display() {
const esp_lcd_panel_io_spi_config_t io_config = {
.cs_gpio_num = PIN_TFT_CS,
.dc_gpio_num = PIN_TFT_RS,
.spi_mode = 0,
.pclk_hz = DISPLAY_REFRESH_HZ,
.trans_queue_depth = DISPLAY_SPI_QUEUE_LEN,
.on_color_trans_done = notify_lvgl_flush_ready,
.user_ctx = &lv_disp_drv,
.lcd_cmd_bits = DISPLAY_COMMAND_BITS,
.lcd_param_bits = DISPLAY_PARAMETER_BITS,
.flags = {
.dc_high_on_cmd = 0, /*!< If enabled, DC level = 1 indicates command transfer */
.dc_low_on_data = 0, /*!< If enabled, DC level = 0 indicates color data transfer */
.dc_low_on_param = 0, /*!< If enabled, DC level = 0 indicates parameter transfer */
.octal_mode = 0, /*!< transmit with octal mode (8 data lines), this mode is used to simulate Intel 8080 timing */
.quad_mode = 0, /*!< transmit with quad mode (4 data lines), this mode is useful when transmitting LCD parameters (Only use one line for command) */
.sio_mode = 0, /*!< Read and write through a single data line (MOSI) */
.lsb_first = 0, /*!< transmit LSB bit first */
.cs_high_active = 0, /*!< CS line is high active */
}
};
const esp_lcd_panel_dev_config_t lcd_config = {
.rgb_ele_order = LCD_RGB_ELEMENT_ORDER_BGR,
.data_endian = LCD_RGB_DATA_ENDIAN_BIG,
.bits_per_pixel = 18,
.reset_gpio_num = PIN_TFT_RST,
.flags = {
.reset_active_high = 0
},
};
ESP_ERROR_CHECK(esp_lcd_new_panel_io_spi((esp_lcd_spi_bus_handle_t)SPI2_HOST, &io_config, &lcd_io_handle));
ESP_ERROR_CHECK(esp_lcd_new_panel_ili9488_ips(lcd_io_handle, &lcd_config, LV_BUFFER_SIZE, &lcd_handle));
// ESP_ERROR_CHECK(esp_lcd_new_panel_ili9488(lcd_io_handle, &lcd_config, LV_BUFFER_SIZE, &lcd_handle));
ESP_ERROR_CHECK(esp_lcd_panel_reset(lcd_handle));
ESP_ERROR_CHECK(esp_lcd_panel_init(lcd_handle));
ESP_ERROR_CHECK(esp_lcd_panel_invert_color(lcd_handle, true));
ESP_ERROR_CHECK(esp_lcd_panel_swap_xy(lcd_handle, true));
ESP_ERROR_CHECK(esp_lcd_panel_mirror(lcd_handle, false, true));
ESP_ERROR_CHECK(esp_lcd_panel_set_gap(lcd_handle, 0, 0));
#if ESP_IDF_VERSION < ESP_IDF_VERSION_VAL(5, 0, 0)
ESP_ERROR_CHECK(esp_lcd_panel_disp_off(lcd_handle, false));
#else
ESP_ERROR_CHECK(esp_lcd_panel_disp_on_off(lcd_handle, true));
#endif
}
static void guiTask(void *pvParameter) {
xGuiSemaphore = xSemaphoreCreateRecursiveMutex();
ESP_LOGI(TAG, "Initializing LVGL");
lv_init();
ESP_LOGI(TAG, "Allocating %zu bytes for LVGL buffer", LV_BUFFER_SIZE * sizeof(lv_color_t));
lv_buf_1 = (lv_color_t *)heap_caps_malloc(LV_BUFFER_SIZE * sizeof(lv_color_t), MALLOC_CAP_DMA);
#if USE_DOUBLE_BUFFERING
ESP_LOGI(TAG, "Allocating %zu bytes for second LVGL buffer", LV_BUFFER_SIZE * sizeof(lv_color_t));
lv_buf_2 = (lv_color_t *)heap_caps_malloc(LV_BUFFER_SIZE * sizeof(lv_color_t), MALLOC_CAP_DMA);
#endif
ESP_LOGI(TAG, "Creating LVLG display buffer");
lv_disp_draw_buf_init(&lv_disp_buf, lv_buf_1, lv_buf_2, LV_BUFFER_SIZE);
ESP_LOGI(TAG, "Initializing %dx%d display", DISPLAY_HORIZONTAL_PIXELS, DISPLAY_VERTICAL_PIXELS);
lv_disp_drv_init(&lv_disp_drv);
lv_disp_drv.hor_res = DISPLAY_HORIZONTAL_PIXELS;
lv_disp_drv.ver_res = DISPLAY_VERTICAL_PIXELS;
lv_disp_drv.flush_cb = lvgl_flush_cb;
lv_disp_drv.draw_buf = &lv_disp_buf;
lv_disp_drv.user_data = lcd_handle;
// lv_disp_drv.rotated = LV_DISP_ROT_90;
lv_display = lv_disp_drv_register(&lv_disp_drv);
ESP_LOGI(TAG, "Creating LVGL tick timer");
const esp_timer_create_args_t lvgl_tick_timer_args = {
.callback = &lv_tick_task,
// .dispatch_method = ESP_TIMER_TASK,
.name = "periodic_gui",
// .skip_unhandled_events = false
};
esp_timer_handle_t periodic_timer;
ESP_ERROR_CHECK(esp_timer_create(&lvgl_tick_timer_args, &periodic_timer));
ESP_ERROR_CHECK(esp_timer_start_periodic(periodic_timer, LVGL_UPDATE_PERIOD_MS * 1000));
screen = lv_scr_act();
lv_style_init(&style_screen);
lv_style_set_bg_color(&style_screen, lv_color_black());
lv_obj_add_style(screen, &style_screen, LV_STATE_DEFAULT);
while (1) {
/* Delay 1 tick (assumes FreeRTOS tick is 10ms */
vTaskDelay(pdMS_TO_TICKS(10));
/* Try to take the semaphore, call lvgl related function on success */
if (pdTRUE == xSemaphoreTakeRecursive(xGuiSemaphore, portMAX_DELAY)) {
lv_task_handler();
xSemaphoreGiveRecursive(xGuiSemaphore);
}
}
vTaskDelete(NULL);
}
void init_tft() {
ESP_LOGI(TAG, "Initializing TFT...");
initialize_spi();
initialize_display();
xTaskCreatePinnedToCore(guiTask, "gui", 4096*2, NULL, 5, NULL, 1);
// register_replay_fn(replay_handler);
ESP_LOGI(TAG, "TFT initialized!");
}
bool lvgl_lock(TickType_t ticks_to_wait) {
return xSemaphoreTakeRecursive(xGuiSemaphore, ticks_to_wait) == pdTRUE;
}
void lvgl_unlock() {
xSemaphoreGiveRecursive(xGuiSemaphore);
}
+117
View File
@@ -0,0 +1,117 @@
#include "tm1640.hpp"
#include <vector>
#include "driver/gpio.h"
#include "esp_rom_sys.h"
// Constants
static const uint8_t CMD_DATA_AUTO = 0x40;
static const uint8_t CMD_DATA_FIXED = 0x44;
static const uint8_t CMD_DISPLAY = 0x80;
static const uint8_t CMD_ADDRESS = 0xC0;
// TODO: we could use the RMT interface to do this more efficiently.
TM1640::TM1640(gpio_num_t clk_pin, gpio_num_t dio_pin) : clk_pin(clk_pin), dio_pin(dio_pin), intensity(0x0F) {
// Configure pins as output
gpio_config_t io_conf = {};
io_conf.intr_type = GPIO_INTR_DISABLE;
io_conf.mode = GPIO_MODE_OUTPUT;
io_conf.pin_bit_mask = (1ULL << clk_pin) | (1ULL << dio_pin);
io_conf.pull_down_en = GPIO_PULLDOWN_DISABLE;
io_conf.pull_up_en = GPIO_PULLUP_DISABLE;
gpio_config(&io_conf);
// Set pins high
gpio_set_level(clk_pin, 1);
gpio_set_level(dio_pin, 1);
}
void TM1640::bit_delay() {
esp_rom_delay_us(1);
}
void TM1640::start() {
gpio_set_level(dio_pin, 0);
gpio_set_level(clk_pin, 0);
bit_delay();
}
void TM1640::stop() {
gpio_set_level(dio_pin, 0);
bit_delay();
gpio_set_level(clk_pin, 1);
gpio_set_level(dio_pin, 1);
bit_delay();
}
void TM1640::shift_out(uint8_t data) {
for (int i = 0; i < 8; i++) {
gpio_set_level(dio_pin, data & 1);
data >>= 1;
bit_delay();
gpio_set_level(clk_pin, 1);
bit_delay();
gpio_set_level(clk_pin, 0);
bit_delay();
}
}
void TM1640::send(uint8_t* data, size_t len) {
start();
for (size_t i = 0; i < len; i++) {
shift_out(data[i]);
}
stop();
}
void TM1640::init() {
clear_display();
}
void TM1640::clear_display() {
uint8_t data1[] = {CMD_DATA_AUTO};
send(data1, 1);
uint8_t data2[] = {CMD_ADDRESS, 0, 0, 0, 0, 0, 0, 0, 0};
send(data2, 9);
uint8_t data3 = CMD_DISPLAY | intensity;
send(&data3, 1);
}
// TODO: can these become all one send??
// other functions too
void TM1640::set_digit(uint8_t digit, uint8_t segments) {
uint8_t data1[] = {CMD_DATA_FIXED};
send(data1, 1);
uint8_t cmd = CMD_ADDRESS | digit;
uint8_t data2[] = {cmd, segments};
send(data2, 2);
uint8_t data3 = CMD_DISPLAY | intensity;
send(&data3, 1);
}
void TM1640::set_digits(uint8_t starting_pos, uint8_t* segments, size_t len) {
uint8_t data1[] = {CMD_DATA_AUTO};
send(data1, 1);
std::vector<uint8_t> data;
data.push_back(CMD_ADDRESS | starting_pos);
for (size_t i = 0; i < len; i++) {
data.push_back(segments[i]);
}
send(data.data(), data.size());
uint8_t data3 = CMD_DISPLAY | intensity;
send(&data3, 1);
}
void TM1640::set_intensity(uint8_t intensity) {
uint8_t new_intensity = intensity & 0x07; // 0-7
this->intensity = (this->intensity & 0xF8) | new_intensity;
uint8_t cmd = CMD_DISPLAY | this->intensity;
send(&cmd, 1);
}
void TM1640::set_display(bool on) {
uint8_t display_bit = on ? 0x08 : 0x00;
this->intensity = (this->intensity & 0xF7) | display_bit;
uint8_t cmd = CMD_DISPLAY | this->intensity;
send(&cmd, 1);
}
+47
View File
@@ -0,0 +1,47 @@
#ifndef TM1640_HPP
#define TM1640_HPP
#include "driver/gpio.h"
#include <stdint.h>
class TM1640 {
gpio_num_t clk_pin;
gpio_num_t dio_pin;
/// The intensity and display on/off setting.
uint8_t intensity;
void bit_delay();
void start();
void stop();
void shift_out(uint8_t data);
void send(uint8_t* data, size_t len);
public:
TM1640(gpio_num_t clk_pin, gpio_num_t dio_pin);
/// Initializes the TM1640 7-segment display.
void init();
/// Clears the display by setting all segments to off.
void clear_display();
/// Sets the segments of a single digit.
void set_digit(uint8_t digit, uint8_t segments);
/// Sets the segments of multiple digits starting at `starting_pos`.
void set_digits(uint8_t starting_pos, uint8_t* segments, size_t len);
/// Sets the intensity from 0-7.
///
/// intensity 0 is still on. To turn the display off, use `set_display`().
void set_intensity(uint8_t intensity);
/// Turns the display on or off.
void set_display(bool on);
};
#endif // TM1640_HPP
+5 -12
View File
@@ -3,16 +3,9 @@ dependencies:
## Required IDF version
idf:
version: '>=6.0.0'
# # Put list of dependencies here
# # For components maintained by Espressif:
# component: "~1.0.0"
# # For 3rd party components:
# username/component: ">=1.0.0,<2.0.0"
# username2/component2:
# version: "~1.0.0"
# # For transient dependencies `public` flag can be set.
# # `public` flag doesn't have an effect dependencies of the `main` component.
# # All dependencies of `main` are public by default.
# public: true
espressif/led_strip: ^3.0.3
espressif/led_strip: ^3.0.3
# atanisoft/esp_lcd_ili9488: ^1.1.1
atanisoft/esp_lcd_ili9488:
path: ../../../esp_lcd_ili9488
lvgl/lvgl: ^8.4
+12
View File
@@ -0,0 +1,12 @@
#ifndef BBNOW_HPP
#define BBNOW_HPP
#include "esp_now.h"
/// The channel to use if not on a WIFI network
#define BBNOW_DEFAULT_CHANNEL 6
const static uint8_t BROADCAST_MAC[ESP_NOW_ETH_ALEN] = { 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF };
void init_espnow();
#endif /* BBNOW_HPP */
+8 -3
View File
@@ -26,10 +26,16 @@ enum class Button: uint8_t {
B2 = 1,
B3 = 2,
B4 = 3,
GREEN = 0,
YELLOW = 1,
RED = 2,
RED = 1,
YELLOW = 2,
BLUE = 3,
LEFT = 0,
DOWN = 1,
UP = 2,
RIGHT = 3,
};
constexpr uint8_t raw_value(Button v) { return static_cast<uint8_t>(v); }
@@ -85,7 +91,6 @@ constexpr char keypad_key_to_char(KeypadKey key) {
return lookup[static_cast<uint8_t>(key) & 0b1111];
}
struct SwitchFlip {
private:
// [bit2: up] [bit1-0: switch]
+6
View File
@@ -0,0 +1,6 @@
#ifndef NVS_HPP
#define NVS_HPP
void init_nvs();
#endif /* NVS_HPP */
+6
View File
@@ -0,0 +1,6 @@
#ifndef RADIO_HPP
#define RADIO_HPP
void init_radio();
#endif /* RADIO_HPP */
+282
View File
@@ -0,0 +1,282 @@
#ifndef SSEGS_HPP
#define SSEGS_HPP
#include <variant>
#include <array>
#include <cstdint>
#include <utility>
/// A command to send to the sseg timer controller.
struct SSegCommand {
enum class Type {
SetIntensity,
EnableGameTimer,
DisableGameTimer,
StartGameTimer,
StopGameTimer,
SetGameTime,
EnableModuleTimer,
DisableModuleTimer,
StartModuleTimer,
StopModuleTimer,
SetModuleTime,
SetGameRaw,
SetGameDigit,
SetModuleRaw,
SetModuleDigit,
SetGameRollover,
SetModuleRollover,
};
Type type;
std::variant<
std::monostate, // for commands without data
uint8_t, // SetIntensity
int32_t, // SetGameTime, SetModuleTime
std::array<uint8_t, 4>, // SetGameRaw, SetModuleRaw
std::pair<uint8_t, uint8_t>, // SetGameDigit, SetModuleDigit
bool // SetGameRollover, SetModuleRollover
> data;
// Constructors for each variant
static SSegCommand SetIntensity(uint8_t intensity) {
return {Type::SetIntensity, intensity};
}
static SSegCommand EnableGameTimer() {
return {Type::EnableGameTimer, std::monostate{}};
}
static SSegCommand DisableGameTimer() {
return {Type::DisableGameTimer, std::monostate{}};
}
static SSegCommand StartGameTimer() {
return {Type::StartGameTimer, std::monostate{}};
}
static SSegCommand StopGameTimer() {
return {Type::StopGameTimer, std::monostate{}};
}
static SSegCommand SetGameTime(int32_t time) {
return {Type::SetGameTime, time};
}
static SSegCommand EnableModuleTimer() {
return {Type::EnableModuleTimer, std::monostate{}};
}
static SSegCommand DisableModuleTimer() {
return {Type::DisableModuleTimer, std::monostate{}};
}
static SSegCommand StartModuleTimer() {
return {Type::StartModuleTimer, std::monostate{}};
}
static SSegCommand StopModuleTimer() {
return {Type::StopModuleTimer, std::monostate{}};
}
static SSegCommand SetModuleTime(int32_t time) {
return {Type::SetModuleTime, time};
}
static SSegCommand SetGameRaw(std::array<uint8_t, 4> raw) {
return {Type::SetGameRaw, raw};
}
static SSegCommand SetGameDigit(uint8_t digit, uint8_t value) {
return {Type::SetGameDigit, std::make_pair(digit, value)};
}
static SSegCommand SetModuleRaw(std::array<uint8_t, 4> raw) {
return {Type::SetModuleRaw, raw};
}
static SSegCommand SetModuleDigit(uint8_t digit, uint8_t value) {
return {Type::SetModuleDigit, std::make_pair(digit, value)};
}
static SSegCommand SetGameRollover(bool rollover) {
return {Type::SetGameRollover, rollover};
}
static SSegCommand SetModuleRollover(bool rollover) {
return {Type::SetModuleRollover, rollover};
}
};
class SSegController {
public:
/// A hexidecimal font for the seven segment displays.
constexpr static uint8_t FONT_HEX[16] = {
0b00111111, 0b00000110, 0b01011011, 0b01001111, 0b01100110, 0b01101101, 0b01111101,
0b00000111, 0b01111111, 0b01101111, 0b01110111, 0b01111100, 0b00111001, 0b01011110,
0b01111001, 0b01110001,
};
/// The mask for the 'A' segment of the display.
constexpr static uint8_t BIT_MASK_A = 0b0000'0001;
/// The mask for the 'B' segment of the display.
constexpr static uint8_t BIT_MASK_B = 0b0000'0010;
/// The mask for the 'C' segment of the display.
constexpr static uint8_t BIT_MASK_C = 0b0000'0100;
/// The mask for the 'D' segment of the display.
constexpr static uint8_t BIT_MASK_D = 0b0000'1000;
/// The mask for the 'E' segment of the display.
constexpr static uint8_t BIT_MASK_E = 0b0001'0000;
/// The mask for the 'F' segment of the display.
constexpr static uint8_t BIT_MASK_F = 0b0010'0000;
/// The mask for the 'G' segment of the display.
constexpr static uint8_t BIT_MASK_G = 0b0100'0000;
/// The mask for the 'DP' (decimal point) segment of the display.
constexpr static uint8_t BIT_MASK_DP = 0b1000'0000;
/// Enables the game timer.
///
/// This "gives control" of the game timer over to the
/// timer task.
///
/// This does not start the game timer, only enables it.
static void enable_game_timer();
/// Disables the game timer.
///
/// This "takes control" of the game timer away from the
/// timer task.
///
/// This also stops the timer, resets the time to 0, and clears
/// the display.
static void disable_game_timer();
/// Starts the game timer.
///
/// This can be called while the game timer is disabled,
/// but the timer will not start until it is enabled.
///
/// Calling this while the timer is disabled can be useful
/// if you want it to start counting right away.
static void start_game_timer();
/// Stops the game timer.
///
/// This can be called while the game timer is disabled,
/// but the timer only counts while it is enabled regardless.
static void stop_game_timer();
/// Sets the game time.
///
/// This can be called even when the game timer is disabled.
///
/// A negative number will cause the timer to count up.
static void set_game_time(int32_t millis);
/// Enables the module timer.
///
/// This "gives control" of the module timer over to the
/// timer task.
///
/// This does not start the module timer, only enables it.
static void enable_module_timer();
/// Disables the module timer.
///
/// This "takes control" of the module timer away from the
/// timer task.
///
/// This also stops the timer, resets the time to 0, and clears
/// the display.
static void disable_module_timer();
/// Starts the module timer.
///
/// This can be called while the module timer is disabled,
/// but the timer will not start until it is enabled.
///
/// Calling this while the timer is disabled can be useful
/// if you want it to start counting right away.
static void start_module_timer();
/// Stops the module timer.
///
/// This can be called while the module timer is disabled,
/// but the timer only counts while it is enabled regardless.
static void stop_module_timer();
/// Sets the module time.
///
/// This can be called even when the module timer is disabled.
///
/// A negative number will cause the timer to count up.
static void set_module_time(int32_t millis);
/// Sets the game timer to the given raw segments.
///
/// You should ensure the game timer is disabled before
/// calling this, otherwise, the data will be overwritten.
static void set_game_raw(const std::array<uint8_t, 4>& segments);
/// Sets the game timer digit to the given raw segments.
///
/// You should ensure the game timer is disabled before
/// calling this, otherwise, the data will be overwritten.
///
/// `digit` should be in the range 0..=3.
static void set_game_digit_raw(uint8_t digit, uint8_t segments);
/// Sets the module timer to the given raw segments.
///
/// You should ensure the module timer is disabled before
/// calling this, otherwise, the data will be overwritten.
static void set_module_raw(const std::array<uint8_t, 4>& segments);
/// Sets the module timer digit to the given raw segments.
///
/// You should ensure the module timer is disabled before
/// calling this, otherwise, the data will be overwritten.
///
/// `digit` should be in the range 0..=3.
static void set_module_digit_raw(uint8_t digit, uint8_t segments);
/// Sets the rollover logic for the game timer.
///
/// If `true`, when the timer reaches zero, it will go
/// negative, and start counting up.
/// If `false`, when the timer reaches zero, it will stop
/// the timer.
static void game_timer_rollover(bool rollover);
/// Sets the rollover logic for the module timer.
///
/// If `true`, when the timer reaches zero, it will go
/// negative, and start counting up.
/// If `false`, when the timer reaches zero, it will stop
/// the timer.
static void module_timer_rollover(bool rollover);
/// Gets the current game time in millis.
static int32_t get_game_time();
/// Gets the current module time in millis.
static int32_t get_module_time();
/// Waits until all commands are flushed to the seven segments.
static void flush();
/// Waits until the game timer is zero (or negative).
static void wait_game_timer_done();
/// Waits until the module timer is zero (or negative).
static void wait_module_timer_done();
/// Sets the intensity of the display.
///
/// `intensity` gets clamped to the range `0..=7`
static void set_intensity(uint8_t intensity);
};
void init_ssegs();
#endif // SSEGS_HPP
+39
View File
@@ -0,0 +1,39 @@
#ifndef TFT_HPP
#define TFT_HPP
#include <lvgl.h>
#include <freertos/FreeRTOS.h>
// Uncomment the following line to enable using double buffering of LVGL color
// data.
// #define USE_DOUBLE_BUFFERING 1
// rotation swaps the horizontal and vertical pixel counts
#define DISPLAY_HORIZONTAL_PIXELS 480
#define DISPLAY_VERTICAL_PIXELS 320
#define DISPLAY_COMMAND_BITS 8
#define DISPLAY_PARAMETER_BITS 8
#define DISPLAY_REFRESH_HZ 40000000
#define DISPLAY_SPI_QUEUE_LEN 10
#define SPI_MAX_TRANSFER_SIZE 32768
#define TFT_INVERT_COLOR false
// Default to 50 lines of color data
#define LV_BUFFER_SIZE DISPLAY_HORIZONTAL_PIXELS * 50
#define LVGL_UPDATE_PERIOD_MS 5
#define BACKLIGHT_LEDC_MODE LEDC_LOW_SPEED_MODE
#define BACKLIGHT_LEDC_CHANNEL LEDC_CHANNEL_0
#define BACKLIGHT_LEDC_TIMER LEDC_TIMER_1
#define BACKLIGHT_LEDC_TIMER_RESOLUTION LEDC_TIMER_10_BIT
#define BACKLIGHT_LEDC_FRQUENCY 5000
extern lv_obj_t* screen;
void init_tft();
bool lvgl_lock(TickType_t ticks_to_wait);
void lvgl_unlock();
#endif // TFT_HPP
+6 -5
View File
@@ -6,11 +6,12 @@
#define PIN_SDA (GPIO_NUM_7)
#define PIN_SCL (GPIO_NUM_15)
#define PIN_LCD_MISO (GPIO_NUM_16)
#define PIN_LCD_MOSI (GPIO_NUM_17)
#define PIN_LCD_CLK (GPIO_NUM_18)
#define PIN_LCD_RS (GPIO_NUM_8)
#define PIN_LCD_RST (GPIO_NUM_9)
#define PIN_TFT_CS (GPIO_NUM_NC)
#define PIN_TFT_MISO (GPIO_NUM_16)
#define PIN_TFT_MOSI (GPIO_NUM_17)
#define PIN_TFT_CLK (GPIO_NUM_18)
#define PIN_TFT_RS (GPIO_NUM_8)
#define PIN_TFT_RST (GPIO_NUM_9)
#define PIN_USB_DM (GPIO_NUM_19)
#define PIN_USB_DP (GPIO_NUM_20)