Initial public release of the 2024A software.

This commit is contained in:
Joe Kearney 2025-01-25 14:04:42 -06:00
parent 7b9ad3edfd
commit 303e9e1dad
361 changed files with 60083 additions and 2 deletions

View file

@ -0,0 +1,11 @@
idf_component_register(
SRCS
"I2S_Audio.c"
INCLUDE_DIRS
"."
REQUIRES
"SystemK"
"driver"
"spiffs"
"esp-audio-player"
)

View file

@ -0,0 +1,462 @@
#include <SystemK.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <dirent.h>
#include <errno.h>
#include <driver/i2s_std.h>
#include <esp_spiffs.h>
#include <audio_player.h>
// I2S Configuration
#define I2S_DOUT GPIO_NUM_11
#define I2S_BCLK GPIO_NUM_12
#define I2S_LRC GPIO_NUM_13
#define I2S_DIN I2S_GPIO_UNUSED
#define I2S_SCLK I2S_GPIO_UNUSED
#define AUDIO_TASK_STACK_SIZE 4096
static StackType_t xStack[AUDIO_TASK_STACK_SIZE];
static StaticTask_t xTaskBuffer;
static QueueHandle_t xQueueAudio;
static TaskHandle_t xAudioTaskHandle;
void I2SAudioTask(void *pvParameters);
static const char *TAG = "I2S Audio";
static i2s_chan_handle_t tx_handle;
#define I2S_GPIO_CFG \
{ \
.mclk = I2S_SCLK, \
.bclk = I2S_BCLK, \
.ws = I2S_LRC, \
.dout = I2S_DOUT, \
.din = I2S_DIN, \
.invert_flags = { \
.mclk_inv = false, \
.bclk_inv = false, \
.ws_inv = false, \
}, \
}
#define I2S_DUPLEX_STEREO_CFG(_sample_rate) \
{ \
.clk_cfg = I2S_STD_CLK_DEFAULT_CONFIG(_sample_rate), \
.slot_cfg = I2S_STD_PHILIP_SLOT_DEFAULT_CONFIG(I2S_DATA_BIT_WIDTH_16BIT, I2S_SLOT_MODE_STEREO), \
.gpio_cfg = I2S_GPIO_CFG, \
}
static esp_err_t I2S_Audio_Set_Mute(AUDIO_PLAYER_MUTE_SETTING setting)
{
if (setting == AUDIO_PLAYER_MUTE)
{
KLOG_DEBUG(TAG, "Muted.");
}
else
{
KLOG_DEBUG(TAG, "Muted.");
}
return ESP_OK;
}
static esp_err_t I2S_Audio_Write(void *audio_buffer, size_t len, size_t *bytes_written, uint32_t timeout_ms)
{
esp_err_t ret = ESP_OK;
ret = i2s_channel_write(tx_handle, (char *)audio_buffer, len, bytes_written, timeout_ms);
return ret;
}
static esp_err_t I2S_Audio_Reconfigure_Clock(uint32_t rate, uint32_t bits_cfg, i2s_slot_mode_t ch)
{
KLOG_INFO(TAG, "Reconfiguring clock for %lu sps, %lu bps, %d channels.", rate, bits_cfg, ch);
esp_err_t ret = ESP_OK;
i2s_std_config_t std_cfg = {
.clk_cfg = I2S_STD_CLK_DEFAULT_CONFIG(rate),
.slot_cfg = I2S_STD_PHILIP_SLOT_DEFAULT_CONFIG((i2s_data_bit_width_t)bits_cfg, (i2s_slot_mode_t)ch),
.gpio_cfg = I2S_GPIO_CFG};
ret |= i2s_channel_disable(tx_handle);
ret |= i2s_channel_reconfig_std_clock(tx_handle, &std_cfg.clk_cfg);
ret |= i2s_channel_reconfig_std_slot(tx_handle, &std_cfg.slot_cfg);
ret |= i2s_channel_enable(tx_handle);
return ret;
}
void Initialize_Audio(void)
{
KLOG_INFO(TAG, "Initializing I2S Audio...");
// Setup a standard configuration and the channel.
i2s_chan_config_t chan_cfg = I2S_CHANNEL_DEFAULT_CONFIG(I2S_NUM_AUTO, I2S_ROLE_MASTER);
chan_cfg.auto_clear = true; // Auto clear the legacy data in the DMA buffer.
ESP_ERROR_CHECK(i2s_new_channel(&chan_cfg, &tx_handle, NULL));
// Setup the I2S configuration.
i2s_std_config_t std_cfg = I2S_DUPLEX_STEREO_CFG(44100);
esp_err_t ret = i2s_channel_init_std_mode(tx_handle, &std_cfg);
if (ret != ESP_OK)
{
if (ret == ESP_ERR_NO_MEM)
{
KLOG_ERROR(TAG, "No memory for storing the channel information");
}
else if (ret == ESP_ERR_INVALID_ARG)
{
KLOG_ERROR(TAG, "NULL pointer or invalid configuration");
}
else if (ret == ESP_ERR_INVALID_STATE)
{
KLOG_ERROR(TAG, "This channel is not registered");
}
else
{
KLOG_ERROR(TAG, "Failed to initialize I2S Audio (%s)", esp_err_to_name(ret));
}
}
i2s_channel_enable(tx_handle);
xQueueAudio = xQueueCreate(5, sizeof(AudioAction_T));
if (xQueueAudio == NULL)
{
KLOG_ERROR(TAG, "Couldn't create the I2S Audio queue.");
}
audio_player_config_t config = {.mute_fn = I2S_Audio_Set_Mute,
.write_fn = I2S_Audio_Write,
.clk_set_fn = I2S_Audio_Reconfigure_Clock,
.priority = 10,
.coreID = APP_CPU_NUM};
ret = audio_player_new(config);
if (ret != ESP_OK)
{
KLOG_ERROR(TAG, "Couldn't create the audio player.");
}
xAudioTaskHandle = xTaskCreateStaticPinnedToCore(
I2SAudioTask, // Function that implements the task.
"I2S Audio", // Text name for the task.
AUDIO_TASK_STACK_SIZE, // Stack size in words, not bytes.
NULL, // Parameter passed into the task.
1, // Priority at which the task is created.
xStack, // Array to use as the task's stack.
&xTaskBuffer, // Variable to hold the task's data structure.
APP_CPU_NUM); // Core where the task should run.
if (xAudioTaskHandle == NULL)
{
KLOG_ERROR(TAG, "Couldn't create the I2S Audio task.");
}
}
esp_err_t Play_Audio_File(const char *filename)
{
KLOG_INFO(TAG, "Attempting to play file: %s", filename);
KLOG_DEBUG(TAG, "Free heap: %ld bytes", esp_get_free_heap_size());
FILE *fh = fopen(filename, "rb");
if (fh == NULL)
{
KLOG_ERROR(TAG, "Failed to open audio file %s! Error: %s", filename, strerror(errno));
return ESP_ERR_NOT_FOUND;
}
fseek(fh, 0, SEEK_END);
long file_size = ftell(fh);
fseek(fh, 0, SEEK_SET);
KLOG_DEBUG(TAG, "File size: %ld bytes", file_size);
esp_err_t ret = audio_player_play(fh);
if (ret != ESP_OK)
{
KLOG_ERROR(TAG, "Couldn't play audio file %s! Error: %d", filename, ret);
fclose(fh);
return ret;
}
return ESP_OK;
}
SystemKResult_T Perform_Audio_Action(AudioAction_T *action)
{
if (xQueueSend(xQueueAudio, action, 0) == pdTRUE)
{
return SYSTEMK_RESULT_SUCCESS;
}
else
{
return SYSTEMK_RESULT_QUEUE_IS_FULL;
}
}
char *concat_path(const char *directory_path, const char *filename)
{
size_t needed = snprintf(NULL, 0, "%s/%s", directory_path, filename) + 1;
char *full_path = malloc(needed);
if (full_path != NULL)
{
snprintf(full_path, needed, "%s/%s", directory_path, filename);
}
return full_path;
}
char *find_filename(const char *directory_path, const char *prefix)
{
DIR *dir = opendir(directory_path);
if (dir == NULL)
{
KLOG_ERROR(TAG, "Failed to open directory '%s': %s!", directory_path, strerror(errno));
return NULL;
}
struct dirent *entry;
char *filename = NULL;
while ((entry = readdir(dir)) != NULL)
{
if (entry->d_type == DT_REG && strncmp(entry->d_name, prefix, strlen(prefix)) == 0)
{
filename = strdup(entry->d_name);
if (filename == NULL)
{
KLOG_ERROR(TAG, "Memory allocation failed!");
}
else
{
KLOG_DEBUG(TAG, "Found matching file: %s", filename);
}
break;
}
}
closedir(dir);
if (filename == NULL)
{
KLOG_WARN(TAG, "No file beginning with '%s' found in directory '%s'!", prefix, directory_path);
}
return filename;
}
SystemKResult_T Play_Sound_By_Prefix(const char *prefix)
{
SystemKResult_T result = SYSTEMK_RESULT_SUCCESS;
const char *sounds_dir = "/usb/01";
char *filename = find_filename(sounds_dir, prefix);
if (filename != NULL)
{
if (audio_player_get_state() == AUDIO_PLAYER_STATE_PLAYING)
{
audio_player_stop();
}
Play_Audio_File(concat_path(sounds_dir, filename));
}
else
{
result = SYSTEMK_RESULT_FILE_NOT_FOUND;
}
return result;
}
SystemKResult_T Play_Number_Sound(uint8_t number)
{
SystemKResult_T result = SYSTEMK_RESULT_SUCCESS;
char number_str[4];
snprintf(number_str, 4, "%03u", number);
const char *sounds_dir = "/usb/10";
char *filename = find_filename(sounds_dir, (const char *)number_str);
if (filename != NULL)
{
if (audio_player_get_state() == AUDIO_PLAYER_STATE_PLAYING)
{
audio_player_stop();
}
Play_Audio_File(concat_path(sounds_dir, filename));
}
else
{
result = SYSTEMK_RESULT_FILE_NOT_FOUND;
}
return result;
}
void I2SAudioTask(void *pvParameters)
{
// Wait for system initialization.
// TODO: Make this closed-loop!
vTaskDelay(1000 / portTICK_PERIOD_MS);
while (true)
{
AudioAction_T action;
if (xQueueReceive(xQueueAudio, &action, portMAX_DELAY) == pdPASS)
{
switch (action.ID)
{
case AUDIO_SET_VOLUME:
break;
case AUDIO_SILENCE:
if (audio_player_get_state() == AUDIO_PLAYER_STATE_PLAYING)
{
audio_player_stop();
}
break;
case AUDIO_PLAY_STARTUP_SOUND:
Play_Sound_By_Prefix("001");
break;
case AUDIO_PLAY_SHOT_FIRED:
Play_Sound_By_Prefix("002");
break;
case AUDIO_PLAY_TAG_RECEIVED:
Play_Sound_By_Prefix("003");
break;
case AUDIO_PLAY_TAGGED_OUT:
Play_Sound_By_Prefix("004");
break;
case AUDIO_PLAY_MISFIRE:
Play_Sound_By_Prefix("005");
break;
case AUDIO_PRONOUNCE_NUMBER_0_TO_100:
uint8_t number = *((uint8_t *)action.Data);
if (number > 100)
{
number = 100;
}
else if (number == 0)
{
number = 101;
}
Play_Number_Sound(number);
break;
case AUDIO_PLAY_MENU_PROMPT:
Play_Sound_By_Prefix("006");
break;
case AUDIO_PLAY_SELECTION_INDICATOR:
Play_Sound_By_Prefix("007");
break;
case AUDIO_PLAY_HEALTH_REMAINING:
Play_Sound_By_Prefix("008");
break;
case AUDIO_PLAY_ELECTRONIC_DANCE_MUSIC:
Play_Sound_By_Prefix("009");
break;
case AUDIO_PLAY_GENERIC_ERROR:
Play_Sound_By_Prefix("010");
break;
case AUDIO_PLAY_VOLUME_PROMPT:
Play_Sound_By_Prefix("011");
break;
case AUDIO_PLAY_RIGHT_HANDED:
Play_Sound_By_Prefix("012");
break;
case AUDIO_PLAY_LEFT_HANDED:
Play_Sound_By_Prefix("013");
break;
case AUDIO_PLAY_GAME_ON:
Play_Sound_By_Prefix("014");
break;
case AUDIO_PLAY_HARDWARE_SETTINGS_PROMPT:
Play_Sound_By_Prefix("015");
break;
case AUDIO_PLAY_GAME_SETTINGS_PROMPT:
Play_Sound_By_Prefix("016");
break;
case AUDIO_PLAY_BONK:
Play_Sound_By_Prefix("017");
break;
case AUDIO_PLAY_NEAR_MISS:
Play_Sound_By_Prefix("018");
break;
case AUDIO_PLAY_PLAYER_ID_PROMPT:
Play_Sound_By_Prefix("019");
break;
case AUDIO_PLAY_TEAM_ID_PROMPT:
Play_Sound_By_Prefix("020");
break;
case AUDIO_PLAY_FRIENDLY_FIRE:
KLOG_WARN(TAG, "\"Friendly Fire\" audio is disabled in this build.");
//Play_Sound_By_Prefix("021");
break;
case AUDIO_PLAY_STARTING_THEME:
Play_Sound_By_Prefix("022");
break;
case AUDIO_PLAY_BOOP:
Play_Sound_By_Prefix("023");
break;
case AUDIO_PLAY_BEEP:
Play_Sound_By_Prefix("024");
break;
case AUDIO_PLAY_REPROGRAMMING:
Play_Sound_By_Prefix("025");
break;
case AUDIO_PLAY_BOMB:
Play_Sound_By_Prefix("026");
break;
case AUDIO_PLAY_GAME_OVER:
Play_Sound_By_Prefix("027");
break;
default:
Play_Audio_File("/spiffs/bad.wav");
break;
}
if (action.Play_To_Completion == true)
{
// Allow some time for the audio to start.
vTaskDelay(100 / portTICK_PERIOD_MS);
while (audio_player_get_state() != AUDIO_PLAYER_STATE_IDLE)
{
vTaskDelay(100 / portTICK_PERIOD_MS);
}
KEvent_T command_received_event = {.ID = KEVENT_AUDIO_COMPLETED, .Data = (void *)action.ID};
Post_KEvent(&command_received_event);
}
}
}
}

View file

@ -0,0 +1,4 @@
#include "esp_err.h"
void Initialize_Audio(void);
esp_err_t Play_WAV_File(char * filename);

276
components/BLE/BLE.c Normal file
View file

@ -0,0 +1,276 @@
#include <SystemK.h>
#include "esp_log.h"
/* BLE */
#include "esp_nimble_hci.h"
#include "nimble/nimble_port.h"
#include "nimble/nimble_port_freertos.h"
#include "host/ble_hs.h"
#include "host/util/util.h"
#include "console/console.h"
#include "services/gap/ble_svc_gap.h"
static const char *TAG = "BLE";
static bool Host_And_Controller_Synced = false;
static bool Is_Scanning_And_Advertising = false;
static uint8_t Advertising_Data[BLE_KTAG_PACKET_TOTAL_SIZE] = {0x1E, 0xFF, 0xFF, 0xFF, 'K', 'T', 'a', 'g'};
// Forward declarations of NimBLE functions.
void ble_store_config_init(void);
static int ble_gap_event(struct ble_gap_event *event, void *arg);
/**
* Initiates the GAP general discovery procedure.
*/
static void ble_scan(void)
{
uint8_t own_addr_type;
struct ble_gap_disc_params disc_params;
int rc;
/* Figure out address to use while advertising (no privacy for now) */
rc = ble_hs_id_infer_auto(0, &own_addr_type);
if (rc != 0)
{
KLOG_ERROR(TAG, "Error determining address type; rc=%d", rc);
return;
}
/* Tell the controller not to filter duplicates; we want to process
* repeated advertisements from the same device.
*/
disc_params.filter_duplicates = 0;
/**
* Perform a passive scan. I.e., don't send follow-up scan requests to
* each advertiser.
*/
disc_params.passive = 1;
/* Use defaults for the rest of the parameters. */
disc_params.itvl = BLE_GAP_SCAN_ITVL_MS(40);
disc_params.window = BLE_GAP_SCAN_WIN_MS(30);
disc_params.filter_policy = 0;
disc_params.limited = 0;
rc = ble_gap_disc(own_addr_type, BLE_HS_FOREVER, &disc_params, ble_gap_event, NULL);
if (rc != 0)
{
KLOG_ERROR(TAG, "Error initiating GAP discovery procedure; rc=%d", rc);
}
}
static void ble_advertise(void)
{
struct ble_gap_adv_params adv_params;
int rc = ble_gap_adv_set_data((const uint8_t *)&Advertising_Data, sizeof(Advertising_Data));
if (rc != 0)
{
KLOG_ERROR(TAG, "Error setting advertisement data; rc=%d", rc);
return;
}
/* Begin advertising. */
memset(&adv_params, 0, sizeof adv_params);
adv_params.conn_mode = BLE_GAP_CONN_MODE_NON;
adv_params.disc_mode = BLE_GAP_DISC_MODE_NON;
// N.B. High duty-cycle mode requires modification to NimBLE, or you will get an "Error enabling advertisement; rc=530"
// adv_params.high_duty_cycle = 1;
// adv_params.itvl_min = BLE_GAP_ADV_ITVL_MS(20);
// adv_params.itvl_max = BLE_GAP_ADV_ITVL_MS(25);
adv_params.itvl_min = BLE_GAP_ADV_ITVL_MS(32);
adv_params.itvl_max = BLE_GAP_ADV_ITVL_MS(37);
rc = ble_gap_adv_start(BLE_OWN_ADDR_PUBLIC, NULL, BLE_HS_FOREVER,
&adv_params, ble_gap_event, NULL);
if (rc != 0)
{
KLOG_ERROR(TAG, "Error enabling advertisement; rc=%d", rc);
return;
}
}
// https://mynewt.apache.org/latest/network/ble_setup/ble_sync_cb.html#reset
static void ble_on_reset(int reason)
{
KLOG_ERROR(TAG, "Resetting state for reason %d.", reason);
Host_And_Controller_Synced = false;
}
// https://mynewt.apache.org/latest/network/ble_setup/ble_sync_cb.html#sync
static void ble_on_sync(void)
{
KLOG_INFO(TAG, "Host and controller synchronized.");
/* Make sure we have proper identity address set (public preferred) */
int rc = ble_hs_util_ensure_addr(0);
assert(rc == 0);
Host_And_Controller_Synced = true;
}
/**
* The NimBLE host executes this callback when a GAP event occurs. The
* application associates a GAP event callback with each connection that is
* established. KTag uses the same callback for all connections.
*
* @param event The event being signalled.
* @param arg Application-specified argument; unused by KTag.
*
* @return 0 if the application successfully handled the
* event; nonzero on failure. The semantics
* of the return code is specific to the
* particular GAP event being signalled.
*/
static int ble_gap_event(struct ble_gap_event *event, void *arg)
{
struct ble_hs_adv_fields fields;
int rc;
switch (event->type)
{
case BLE_GAP_EVENT_DISC:
rc = ble_hs_adv_parse_fields(&fields, event->disc.data,
event->disc.length_data);
if (rc != 0)
{
return 0;
}
BLE_Packet_T *packet = BLE_DecodeKTagPacket(event->disc.data,
event->disc.length_data,
&(event->disc.addr.val[0]),
event->disc.rssi);
if (packet != NULL)
{
if (packet->Generic.type == BLE_PACKET_TYPE_CONSOLE)
{
packet->Console.console_data[BLE_KTAG_PACKET_DATA_SIZE - 1] = '\0';
HW_Execute_Console_Command(packet->Console.console_data);
}
else
{
KEvent_T packet_received_event = {.ID = KEVENT_BLE_PACKET_RECEIVED, .Data = packet};
Post_KEvent(&packet_received_event);
}
}
return 0;
case BLE_GAP_EVENT_ADV_COMPLETE:
KLOG_INFO(TAG, "Advertise complete; reason=%d", event->adv_complete.reason);
ble_advertise();
return 0;
default:
return 0;
}
}
void ble_host_task(void *param)
{
KLOG_INFO(TAG, "BLE Host Task Started");
// This function will return only after nimble_port_stop() is called:
nimble_port_run();
nimble_port_freertos_deinit();
}
void Initialize_BLE(void)
{
KLOG_INFO(TAG, "Initializing BLE...");
esp_err_t ret = nimble_port_init();
if (ret != ESP_OK)
{
KLOG_ERROR(TAG, "Failed to initialize NimBLE for reason %d.", ret);
return;
}
/* Configure the host. */
ble_hs_cfg.reset_cb = ble_on_reset;
ble_hs_cfg.sync_cb = ble_on_sync;
ble_hs_cfg.store_status_cb = ble_store_util_status_rr;
ble_store_config_init();
nimble_port_freertos_init(ble_host_task);
}
void Disable_BLE(void)
{
KLOG_INFO(TAG, "Disabling BLE...");
esp_err_t ret = nimble_port_deinit();
if (ret != ESP_OK)
{
KLOG_ERROR(TAG, "Failed to deinitialize NimBLE for reason %d.", ret);
}
else
{
KLOG_INFO(TAG, "NimBLE deinitialized--BLE disabled.");
}
}
SystemKResult_T BLE_GetMyAddress(uint8_t *BD_ADDR)
{
/* Try to load a public address. */
int result = ble_hs_id_copy_addr(BLE_ADDR_PUBLIC, BD_ADDR, NULL);
if (result == BLE_HS_ENOADDR)
{
return SYSTEMK_RESULT_NOT_IMPLEMENTED;
}
else
{
return SYSTEMK_RESULT_SUCCESS;
}
}
SystemKResult_T BLE_SetAdvertisingData(BLE_AdvertisingData_T *data)
{
SystemKResult_T result = SYSTEMK_RESULT_SUCCESS;
if (data->length > BLE_MAX_ADVERTISING_BYTES)
{
result = SYSTEMK_RESULT_TOO_MANY_DATA;
}
else if (data->length < BLE_KTAG_PACKET_TOTAL_SIZE)
{
result = SYSTEMK_RESULT_TOO_FEW_DATA;
}
else
{
memcpy(Advertising_Data, data, BLE_KTAG_PACKET_TOTAL_SIZE);
}
if (Is_Scanning_And_Advertising == true)
{
// Restart advertising to put the new data into the advertisement.
ble_gap_adv_stop();
ble_advertise();
}
return result;
}
SystemKResult_T BLE_ScanAndAdvertise(void)
{
SystemKResult_T result = SYSTEMK_RESULT_NOT_READY;
if (Host_And_Controller_Synced == true)
{
if (Is_Scanning_And_Advertising == false)
{
ble_scan();
ble_advertise();
Is_Scanning_And_Advertising = true;
}
result = SYSTEMK_RESULT_SUCCESS;
}
return result;
}

4
components/BLE/BLE.h Normal file
View file

@ -0,0 +1,4 @@
#include "esp_err.h"
void Initialize_BLE(void);
void Disable_BLE(void);

View file

@ -0,0 +1,10 @@
idf_component_register(
SRCS
"BLE.c"
INCLUDE_DIRS
"."
REQUIRES
"SystemK"
"driver"
"bt"
)

View file

@ -0,0 +1,9 @@
idf_component_register(
SRCS
"IR.c"
INCLUDE_DIRS
"."
REQUIRES
"SystemK"
"driver"
)

362
components/IR/IR.c Normal file
View file

@ -0,0 +1,362 @@
#include <SystemK.h>
#include <driver/gpio.h>
#include <driver/rmt_tx.h>
#include <driver/rmt_rx.h>
#define IR_RESOLUTION_HZ (1 * 1000 * 1000) // 1MHz resolution, 1 tick = 1us
#define IR_TX_PIN GPIO_NUM_6
#define IR_TX_RIGHT_ENABLE GPIO_NUM_7
#define IR_TX_LEFT_ENABLE GPIO_NUM_5
#define IR_RX_LEFT_PIN GPIO_NUM_2
#define IR_RX_FORWARD_PIN GPIO_NUM_15
#define IR_RX_RIGHT_PIN GPIO_NUM_42
static const char *TAG = "IR";
static rmt_channel_handle_t Tx_Channel = NULL;
static gpio_config_t tx_right_enable_gpio_config = {
.pin_bit_mask = (1ULL << IR_TX_RIGHT_ENABLE),
.mode = GPIO_MODE_OUTPUT, // Set mode to output
.pull_up_en = GPIO_PULLUP_ENABLE,
.pull_down_en = GPIO_PULLDOWN_DISABLE,
.intr_type = GPIO_INTR_DISABLE};
static gpio_config_t tx_left_enable_gpio_config = {
.pin_bit_mask = (1ULL << IR_TX_LEFT_ENABLE),
.mode = GPIO_MODE_OUTPUT, // Set mode to output
.pull_up_en = GPIO_PULLUP_ENABLE,
.pull_down_en = GPIO_PULLDOWN_DISABLE,
.intr_type = GPIO_INTR_DISABLE};
// Create simple encoder
rmt_copy_encoder_config_t Copy_Encoder_Config;
rmt_encoder_handle_t Tx_Encoder;
rmt_transmit_config_t Tx_Config = {
.loop_count = 0, // no transfer loop
};
rmt_receive_config_t Rx_Config = {
.signal_range_min_ns = 5 * 1000, // A pulse whose width is smaller than this threshold will be treated as glitch and ignored.
.signal_range_max_ns = 20 * 1000 * 1000, // RMT will stop receiving if one symbol level has kept longer than this value.
.flags.en_partial_rx = false};
static rmt_channel_handle_t Left_Rx_Channel = NULL;
static rmt_channel_handle_t Forward_Rx_Channel = NULL;
static rmt_channel_handle_t Right_Rx_Channel = NULL;
static QueueHandle_t Receive_Queue;
static TimedPulseTrain_T Left_Rxd_RMT_Data;
static TimedPulseTrain_T Forward_Rxd_RMT_Data;
static TimedPulseTrain_T Right_Rxd_RMT_Data;
#define IR_RX_STACK_SIZE 4096
static StaticTask_t xTaskBuffer;
static StackType_t xStack[IR_RX_STACK_SIZE];
static TaskHandle_t IR_Rx_Task_Handle;
static TagPacket_T Shot_Packet;
typedef struct
{
uint8_t count;
TagSensorLocation_T location;
} RxNotification_T;
static bool RMT_Rx_Left_Done_Callback(rmt_channel_handle_t channel, const rmt_rx_done_event_data_t *edata, void *user_data)
{
BaseType_t xHigherPriorityTaskWoken = pdFALSE;
RxNotification_T notice;
notice.count = edata->num_symbols * 2;
notice.location = TAG_SENSOR_LEFT;
xQueueSendFromISR(Receive_Queue, &notice, &xHigherPriorityTaskWoken);
return xHigherPriorityTaskWoken;
}
static bool RMT_Rx_Forward_Done_Callback(rmt_channel_handle_t channel, const rmt_rx_done_event_data_t *edata, void *user_data)
{
BaseType_t xHigherPriorityTaskWoken = pdFALSE;
RxNotification_T notice;
notice.count = edata->num_symbols * 2;
notice.location = TAG_SENSOR_FORWARD;
xQueueSendFromISR(Receive_Queue, &notice, &xHigherPriorityTaskWoken);
return xHigherPriorityTaskWoken;
}
static bool RMT_Rx_Right_Done_Callback(rmt_channel_handle_t channel, const rmt_rx_done_event_data_t *edata, void *user_data)
{
BaseType_t xHigherPriorityTaskWoken = pdFALSE;
RxNotification_T notice;
notice.count = edata->num_symbols * 2;
notice.location = TAG_SENSOR_RIGHT;
xQueueSendFromISR(Receive_Queue, &notice, &xHigherPriorityTaskWoken);
return xHigherPriorityTaskWoken;
}
static const rmt_rx_event_callbacks_t left_cbs = {
.on_recv_done = RMT_Rx_Left_Done_Callback,
};
static const rmt_rx_event_callbacks_t forward_cbs = {
.on_recv_done = RMT_Rx_Forward_Done_Callback,
};
static const rmt_rx_event_callbacks_t right_cbs = {
.on_recv_done = RMT_Rx_Right_Done_Callback,
};
static void IR_Receive_Task(void *param)
{
KLOG_INFO(TAG, "IR Receive Task Started");
while (true)
{
RxNotification_T notice;
DecodedPacket_T *result = NULL;
if (xQueueReceive(Receive_Queue, &notice, portMAX_DELAY) == pdPASS)
{
if (notice.location == TAG_SENSOR_FORWARD)
{
KLOG_INFO(TAG, "TAG_SENSOR_FORWARD Rx'd");
Forward_Rxd_RMT_Data.count = notice.count;
result = PROTOCOLS_MaybeDecodePacket(&Forward_Rxd_RMT_Data);
}
else if (notice.location == TAG_SENSOR_LEFT)
{
KLOG_INFO(TAG, "TAG_SENSOR_LEFT Rx'd");
Left_Rxd_RMT_Data.count = notice.count;
result = PROTOCOLS_MaybeDecodePacket(&Left_Rxd_RMT_Data);
}
else if (notice.location == TAG_SENSOR_RIGHT)
{
KLOG_INFO(TAG, "TAG_SENSOR_RIGHT Rx'd");
Right_Rxd_RMT_Data.count = notice.count;
result = PROTOCOLS_MaybeDecodePacket(&Right_Rxd_RMT_Data);
}
if (result != NULL)
{
if (result->Generic.type == DECODED_PACKET_TYPE_TAG_RECEIVED)
{
KEvent_T tag_received_event = {.ID = KEVENT_TAG_RECEIVED, .Data = result};
Post_KEvent(&tag_received_event);
}
else if (result->Generic.type == DECODED_PACKET_TYPE_COMMAND_RECEIVED)
{
KEvent_T command_received_event = {.ID = KEVENT_COMMAND_RECEIVED, .Data = result};
Post_KEvent(&command_received_event);
}
}
else
{
KEvent_T near_miss_event = {.ID = KEVENT_NEAR_MISS, .Data = NULL};
Post_KEvent(&near_miss_event);
}
// Start receiving again.
// We need to reset all three channels for some reason--why?
{
ESP_ERROR_CHECK(rmt_disable(Forward_Rx_Channel));
ESP_ERROR_CHECK(rmt_enable(Forward_Rx_Channel));
ESP_ERROR_CHECK(rmt_receive(Forward_Rx_Channel, &Forward_Rxd_RMT_Data, sizeof(Forward_Rxd_RMT_Data.pulsetrain), &Rx_Config));
}
{
ESP_ERROR_CHECK(rmt_disable(Left_Rx_Channel));
ESP_ERROR_CHECK(rmt_enable(Left_Rx_Channel));
ESP_ERROR_CHECK(rmt_receive(Left_Rx_Channel, &Left_Rxd_RMT_Data, sizeof(Left_Rxd_RMT_Data.pulsetrain), &Rx_Config));
}
{
ESP_ERROR_CHECK(rmt_disable(Right_Rx_Channel));
ESP_ERROR_CHECK(rmt_enable(Right_Rx_Channel));
ESP_ERROR_CHECK(rmt_receive(Right_Rx_Channel, &Right_Rxd_RMT_Data, sizeof(Right_Rxd_RMT_Data.pulsetrain), &Rx_Config));
}
}
}
}
static void Initialize_Receive_Task(void)
{
IR_Rx_Task_Handle = xTaskCreateStaticPinnedToCore(
IR_Receive_Task, // Function that implements the task.
"IR Rx", // Text name for the task.
IR_RX_STACK_SIZE, // Stack size in words, not bytes.
0, // Parameter passed into the task.
tskIDLE_PRIORITY + 1, // Priority at which the task is created.
xStack, // Array to use as the task's stack.
&xTaskBuffer, // Variable to hold the task's data structure.
APP_CPU_NUM); // Core where the task should run.
if (IR_Rx_Task_Handle == NULL)
{
KLOG_ERROR(TAG, "Failed to create IR Receive task!");
}
}
void Initialize_IR(SemaphoreHandle_t init_complete)
{
KLOG_INFO(TAG, "Initializing IR...");
KLOG_INFO(TAG, "Creating RMT TX channel...");
rmt_tx_channel_config_t tx_channel_cfg = {
.gpio_num = IR_TX_PIN, // GPIO number used by RMT TX channel.
.clk_src = RMT_CLK_SRC_XTAL, // Clock source of RMT TX channel, channels in the same group must use the same clock source
.resolution_hz = IR_RESOLUTION_HZ, // Channel clock resolution, in Hz.
.mem_block_symbols = 48, // If DMA is not used, this field controls the size of the dedicated memory block owned by the channel.
// THIS IS WRONG IN THE DOCS! See https://github.com/espressif/esp-idf/issues/12084#issuecomment-1679881770.
.trans_queue_depth = 1, // Depth of the internal transaction queue.
.flags.invert_out = false, // Should the signal be inverted before sending it to the GPIO?
.flags.with_dma = true, // Should this channel use the DMA backend?
.flags.io_loop_back = false, // Should the signal output from the GPIO be fed to the input path as well?
.flags.io_od_mode = false, // Should the GPIO be configured in open-drain mode?
.intr_priority = 1 // RMT interrupt priority. If set to 0 , then the driver will use a interrupt with low or medium priority (priority level may be one of 1, 2, or 3).
};
ESP_ERROR_CHECK(rmt_new_tx_channel(&tx_channel_cfg, &Tx_Channel));
KLOG_INFO(TAG, "Modulate carrier to TX channel");
rmt_carrier_config_t carrier_cfg = {
.duty_cycle = 0.30, // 30% duty cycle
.frequency_hz = 38000, // 38KHz
};
ESP_ERROR_CHECK(rmt_apply_carrier(Tx_Channel, &carrier_cfg));
ESP_ERROR_CHECK(rmt_enable(Tx_Channel));
ESP_ERROR_CHECK(gpio_config(&tx_right_enable_gpio_config));
KLOG_INFO(TAG, "Initialized right IR Tx enable as GPIO[%d].", IR_TX_RIGHT_ENABLE);
ESP_ERROR_CHECK(gpio_config(&tx_left_enable_gpio_config));
KLOG_INFO(TAG, "Initialized left IR Tx enable as GPIO[%d].", IR_TX_LEFT_ENABLE);
ESP_ERROR_CHECK(rmt_new_copy_encoder(&Copy_Encoder_Config, &Tx_Encoder));
KLOG_INFO(TAG, "Creating RMT Rx channels...");
rmt_rx_channel_config_t rx_left_channel_cfg = {
.gpio_num = IR_RX_LEFT_PIN, // GPIO number used by RMT RX channel. Set to -1 if unused.
.clk_src = RMT_CLK_SRC_XTAL, // Clock source of RMT RX channel; channels in the same group must use the same clock source.
.resolution_hz = IR_RESOLUTION_HZ, // Channel clock resolution, in Hz.
.mem_block_symbols = 48, // Size of memory block, in number of `rmt_symbol_word_t`, must be an even.
// In the DMA mode, this field controls the DMA buffer size, it can be set to a large value (e.g. 1024);
// In the normal mode, this field controls the number of RMT memory block that will be used by the channel.
.flags.invert_in = true, // Should the incoming signal be inverted before processing?
.flags.with_dma = false, // Should this channel use the DMA backend?
.flags.io_loop_back = false, // Should the signal output from the GPIO be fed to the input path as well?
.intr_priority = 1 // RMT interrupt priority. If set to 0 , then the driver will use a interrupt with low or medium priority (priority level may be one of 1, 2, or 3).
};
ESP_ERROR_CHECK(rmt_new_rx_channel(&rx_left_channel_cfg, &Left_Rx_Channel));
rmt_rx_channel_config_t rx_forward_channel_cfg = {
.gpio_num = IR_RX_FORWARD_PIN, // GPIO number used by RMT RX channel. Set to -1 if unused.
.clk_src = RMT_CLK_SRC_XTAL, // Clock source of RMT RX channel; channels in the same group must use the same clock source.
.resolution_hz = IR_RESOLUTION_HZ, // Channel clock resolution, in Hz.
.mem_block_symbols = 48, // Size of memory block, in number of `rmt_symbol_word_t`, must be an even.
// In the DMA mode, this field controls the DMA buffer size, it can be set to a large value (e.g. 1024);
// In the normal mode, this field controls the number of RMT memory block that will be used by the channel.
.flags.invert_in = true, // Should the incoming signal be inverted before processing?
.flags.with_dma = false, // Should this channel use the DMA backend?
.flags.io_loop_back = false, // Should the signal output from the GPIO be fed to the input path as well?
.intr_priority = 1 // RMT interrupt priority. If set to 0 , then the driver will use a interrupt with low or medium priority (priority level may be one of 1, 2, or 3).
};
ESP_ERROR_CHECK(rmt_new_rx_channel(&rx_forward_channel_cfg, &Forward_Rx_Channel));
rmt_rx_channel_config_t rx_right_channel_cfg = {
.gpio_num = IR_RX_RIGHT_PIN, // GPIO number used by RMT RX channel. Set to -1 if unused.
.clk_src = RMT_CLK_SRC_XTAL, // Clock source of RMT RX channel; channels in the same group must use the same clock source.
.resolution_hz = IR_RESOLUTION_HZ, // Channel clock resolution, in Hz.
.mem_block_symbols = 48, // Size of memory block, in number of `rmt_symbol_word_t`, must be an even.
// In the DMA mode, this field controls the DMA buffer size, it can be set to a large value (e.g. 1024);
// In the normal mode, this field controls the number of RMT memory block that will be used by the channel.
.flags.invert_in = true, // Should the incoming signal be inverted before processing?
.flags.with_dma = false, // Should this channel use the DMA backend?
.flags.io_loop_back = false, // Should the signal output from the GPIO be fed to the input path as well?
.intr_priority = 1 // RMT interrupt priority. If set to 0 , then the driver will use a interrupt with low or medium priority (priority level may be one of 1, 2, or 3).
};
ESP_ERROR_CHECK(rmt_new_rx_channel(&rx_right_channel_cfg, &Right_Rx_Channel));
ESP_LOGI(TAG, "register RX done callbacks");
Receive_Queue = xQueueCreate(1, sizeof(RxNotification_T));
assert(Receive_Queue);
ESP_ERROR_CHECK(rmt_rx_register_event_callbacks(Left_Rx_Channel, &left_cbs, 0));
ESP_ERROR_CHECK(rmt_rx_register_event_callbacks(Forward_Rx_Channel, &forward_cbs, 0));
ESP_ERROR_CHECK(rmt_rx_register_event_callbacks(Right_Rx_Channel, &right_cbs, 0));
Initialize_Receive_Task();
Left_Rxd_RMT_Data.receiver = TAG_SENSOR_LEFT;
Forward_Rxd_RMT_Data.receiver = TAG_SENSOR_FORWARD;
Right_Rxd_RMT_Data.receiver = TAG_SENSOR_RIGHT;
ESP_ERROR_CHECK(rmt_enable(Left_Rx_Channel));
ESP_ERROR_CHECK(rmt_receive(Left_Rx_Channel, &Left_Rxd_RMT_Data, sizeof(Left_Rxd_RMT_Data.pulsetrain), &Rx_Config));
ESP_ERROR_CHECK(rmt_enable(Forward_Rx_Channel));
ESP_ERROR_CHECK(rmt_receive(Forward_Rx_Channel, &Forward_Rxd_RMT_Data, sizeof(Forward_Rxd_RMT_Data.pulsetrain), &Rx_Config));
ESP_ERROR_CHECK(rmt_enable(Right_Rx_Channel));
ESP_ERROR_CHECK(rmt_receive(Right_Rx_Channel, &Right_Rxd_RMT_Data, sizeof(Right_Rxd_RMT_Data.pulsetrain), &Rx_Config));
xSemaphoreGive(init_complete);
}
static inline void PrintPulseTrainToConsole(TimedPulseTrain_T *train)
{
for (uint_fast16_t i = 0; i < train->count; i += 2)
{
KLOG_INFO(TAG, "%2d: (%d, %4d) (%d, %4d)", i + 1, train->bitstream[i].symbol, train->bitstream[i].duration, train->bitstream[i + 1].symbol, train->bitstream[i + 1].duration);
vTaskDelay(pdMS_TO_TICKS(10));
}
}
SystemKResult_T Prepare_Tag(void)
{
TimedPulseTrain_T *Shot_Buffer;
uint8_t team_ID;
uint8_t player_ID;
uint8_t weapon_ID;
(void)SETTINGS_get_uint8_t(SYSTEMK_SETTING_TEAMID, &team_ID);
(void)SETTINGS_get_uint8_t(SYSTEMK_SETTING_PLAYERID, &player_ID);
(void)SETTINGS_get_uint8_t(SYSTEMK_SETTING_WEAPONID, &weapon_ID);
Weapon_t weapon = GetWeaponFromID(weapon_ID);
Shot_Packet.player_ID = player_ID;
Shot_Packet.team_ID = team_ID;
Shot_Packet.color = (uint32_t)PROTOCOLS_GetColor(weapon.Protocol, team_ID, player_ID);
Shot_Packet.protocol = weapon.Protocol;
Shot_Packet.damage = weapon.Damage_Per_Shot;
Shot_Buffer = PROTOCOLS_EncodePacket(&Shot_Packet);
KLOG_INFO(TAG, "Tag prepared (%u pulses):", Shot_Buffer->count);
PrintPulseTrainToConsole(Shot_Buffer);
gpio_set_level(IR_TX_RIGHT_ENABLE, 0);
gpio_set_level(IR_TX_LEFT_ENABLE, 0);
return SYSTEMK_RESULT_SUCCESS;
}
SystemKResult_T Send_Tag(void)
{
TimedPulseTrain_T *Shot_Buffer;
Shot_Buffer = PROTOCOLS_EncodePacket(&Shot_Packet);
ESP_ERROR_CHECK(rmt_transmit(Tx_Channel, Tx_Encoder, Shot_Buffer->pulsetrain, sizeof(rmt_symbol_word_t) * Shot_Buffer->count, &Tx_Config));
KEvent_T tag_sent_event = {.ID = KEVENT_TAG_SENT, .Data = (void *)0x00};
Post_KEvent(&tag_sent_event);
return SYSTEMK_RESULT_SUCCESS;
}

3
components/IR/IR.h Normal file
View file

@ -0,0 +1,3 @@
#include "esp_err.h"
void Initialize_IR(SemaphoreHandle_t init_complete);

View file

@ -0,0 +1,38 @@
## 2.5.0
- Enabled support for IDF4.4 and above
- with RMT backend only
- Added API `led_strip_set_pixel_hsv`
## 2.4.0
- Support configurable SPI mode to control leds
- recommend enabling DMA when using SPI mode
## 2.3.0
- Support configurable RMT channel size by setting `mem_block_symbols`
## 2.2.0
- Support for 4 components RGBW leds (SK6812):
- in led_strip_config_t new fields
led_pixel_format, controlling byte format (LED_PIXEL_FORMAT_GRB, LED_PIXEL_FORMAT_GRBW)
led_model, used to configure bit timing (LED_MODEL_WS2812, LED_MODEL_SK6812)
- new API led_strip_set_pixel_rgbw
- new interface type set_pixel_rgbw
## 2.1.0
- Support DMA feature, which offloads the CPU by a lot when it comes to drive a bunch of LEDs
- Support various RMT clock sources
- Acquire and release the power management lock before and after each refresh
- New driver flag: `invert_out` which can invert the led control signal by hardware
## 2.0.0
- Reimplemented the driver using the new RMT driver (`driver/rmt_tx.h`)
## 1.0.0
- Initial driver version, based on the legacy RMT driver (`driver/rmt.h`)

View file

@ -0,0 +1,22 @@
include($ENV{IDF_PATH}/tools/cmake/version.cmake)
set(srcs "src/led_strip_api.c" "src/muxed_led_strip_api.c")
if("${IDF_VERSION_MAJOR}.${IDF_VERSION_MINOR}" VERSION_GREATER_EQUAL "5.0")
if(CONFIG_SOC_RMT_SUPPORTED)
list(APPEND srcs "src/led_strip_rmt_dev.c" "src/muxed_led_strip_rmt_dev.c" "src/led_strip_rmt_encoder.c")
endif()
else()
list(APPEND srcs "src/led_strip_rmt_dev_idf4.c")
endif()
# the SPI backend driver relies on something that was added in IDF 5.1
if("${IDF_VERSION_MAJOR}.${IDF_VERSION_MINOR}" VERSION_GREATER_EQUAL "5.1")
if(CONFIG_SOC_GPSPI_SUPPORTED)
list(APPEND srcs "src/led_strip_spi_dev.c")
endif()
endif()
idf_component_register(SRCS ${srcs}
INCLUDE_DIRS "include" "interface"
REQUIRES "driver")

View file

@ -0,0 +1,202 @@
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "[]"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright [yyyy] [name of copyright owner]
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.

View file

@ -0,0 +1,97 @@
# LED Strip Driver
[![Component Registry](https://components.espressif.com/components/espressif/led_strip/badge.svg)](https://components.espressif.com/components/espressif/led_strip)
This driver is designed for addressable LEDs like [WS2812](http://www.world-semi.com/Certifications/WS2812B.html), where each LED is controlled by a single data line.
## Backend Controllers
### The [RMT](https://docs.espressif.com/projects/esp-idf/en/latest/esp32/api-reference/peripherals/rmt.html) Peripheral
This is the most economical way to drive the LEDs because it only consumes one RMT channel, leaving other channels free to use. However, the memory usage increases dramatically with the number of LEDs. If the RMT hardware can't be assist by DMA, the driver will going into interrupt very frequently, thus result in a high CPU usage. What's worse, if the RMT interrupt is delayed or not serviced in time (e.g. if Wi-Fi interrupt happens on the same CPU core), the RMT transaction will be corrupted and the LEDs will display incorrect colors. If you want to use RMT to drive a large number of LEDs, you'd better to enable the DMA feature if possible [^1].
#### Allocate LED Strip Object with RMT Backend
```c
#define BLINK_GPIO 0
led_strip_handle_t led_strip;
/* LED strip initialization with the GPIO and pixels number*/
led_strip_config_t strip_config = {
.strip_gpio_num = BLINK_GPIO, // The GPIO that connected to the LED strip's data line
.max_leds = 1, // The number of LEDs in the strip,
.led_pixel_format = LED_PIXEL_FORMAT_GRB, // Pixel format of your LED strip
.led_model = LED_MODEL_WS2812, // LED strip model
.flags.invert_out = false, // whether to invert the output signal (useful when your hardware has a level inverter)
};
led_strip_rmt_config_t rmt_config = {
#if ESP_IDF_VERSION < ESP_IDF_VERSION_VAL(5, 0, 0)
.rmt_channel = 0,
#else
.clk_src = RMT_CLK_SRC_DEFAULT, // different clock source can lead to different power consumption
.resolution_hz = 10 * 1000 * 1000, // 10MHz
.flags.with_dma = false, // whether to enable the DMA feature
#endif
};
ESP_ERROR_CHECK(led_strip_new_rmt_device(&strip_config, &rmt_config, &led_strip));
```
You can create multiple LED strip objects with different GPIOs and pixel numbers. The backend driver will automatically allocate the RMT channel for you if there is more available.
### The [SPI](https://docs.espressif.com/projects/esp-idf/en/latest/esp32/api-reference/peripherals/spi_master.html) Peripheral
SPI peripheral can also be used to generate the timing required by the LED strip. However this backend is not as economical as the RMT one, because it will take up the whole **bus**, unlike the RMT just takes one **channel**. You **CANT** connect other devices to the same SPI bus if it's been used by the led_strip, because the led_strip doesn't have the concept of "Chip Select".
Please note, the SPI backend has a dependency of **ESP-IDF >= 5.1**
#### Allocate LED Strip Object with SPI Backend
```c
#define BLINK_GPIO 0
led_strip_handle_t led_strip;
/* LED strip initialization with the GPIO and pixels number*/
led_strip_config_t strip_config = {
.strip_gpio_num = BLINK_GPIO, // The GPIO that connected to the LED strip's data line
.max_leds = 1, // The number of LEDs in the strip,
.led_pixel_format = LED_PIXEL_FORMAT_GRB, // Pixel format of your LED strip
.led_model = LED_MODEL_WS2812, // LED strip model
.flags.invert_out = false, // whether to invert the output signal (useful when your hardware has a level inverter)
};
led_strip_spi_config_t spi_config = {
.clk_src = SPI_CLK_SRC_DEFAULT, // different clock source can lead to different power consumption
.flags.with_dma = true, // Using DMA can improve performance and help drive more LEDs
.spi_bus = SPI2_HOST, // SPI bus ID
};
ESP_ERROR_CHECK(led_strip_new_spi_device(&strip_config, &spi_config, &led_strip));
```
The number of LED strip objects can be created depends on how many free SPI buses are free to use in your project.
## FAQ
* Which led_strip backend should I choose?
* It depends on your application requirement and target chip's ability.
```mermaid
flowchart LR
A{Is RMT supported?}
A --> |No| B[SPI backend]
B --> C{Does the led strip has \n a larger number of LEDs?}
C --> |No| D[Don't have to enable the DMA of the backend]
C --> |Yes| E[Enable the DMA of the backend]
A --> |Yes| F{Does the led strip has \n a larger number of LEDs?}
F --> |Yes| G{Does RMT support DMA?}
G --> |Yes| E
G --> |No| B
F --> |No| H[RMT backend] --> D
```
* How to set the brightness of the LED strip?
* You can tune the brightness by scaling the value of each R-G-B element with a **same** factor. But pay attention to the overflow of the value.
[^1]: The RMT DMA feature is not available on all ESP chips. Please check the data sheet before using it.

View file

@ -0,0 +1,454 @@
# API Reference
## Header files
- [include/led_strip.h](#file-includeled_striph)
- [include/led_strip_rmt.h](#file-includeled_strip_rmth)
- [include/led_strip_spi.h](#file-includeled_strip_spih)
- [include/led_strip_types.h](#file-includeled_strip_typesh)
- [interface/led_strip_interface.h](#file-interfaceled_strip_interfaceh)
## File include/led_strip.h
## Functions
| Type | Name |
| ---: | :--- |
| esp\_err\_t | [**led\_strip\_clear**](#function-led_strip_clear) ([**led\_strip\_handle\_t**](#struct-led_strip_t) strip) <br>_Clear LED strip (turn off all LEDs)_ |
| esp\_err\_t | [**led\_strip\_del**](#function-led_strip_del) ([**led\_strip\_handle\_t**](#struct-led_strip_t) strip) <br>_Free LED strip resources._ |
| esp\_err\_t | [**led\_strip\_refresh**](#function-led_strip_refresh) ([**led\_strip\_handle\_t**](#struct-led_strip_t) strip) <br>_Refresh memory colors to LEDs._ |
| esp\_err\_t | [**led\_strip\_set\_pixel**](#function-led_strip_set_pixel) ([**led\_strip\_handle\_t**](#struct-led_strip_t) strip, uint32\_t index, uint32\_t red, uint32\_t green, uint32\_t blue) <br>_Set RGB for a specific pixel._ |
| esp\_err\_t | [**led\_strip\_set\_pixel\_hsv**](#function-led_strip_set_pixel_hsv) ([**led\_strip\_handle\_t**](#struct-led_strip_t) strip, uint32\_t index, uint16\_t hue, uint8\_t saturation, uint8\_t value) <br>_Set HSV for a specific pixel._ |
| esp\_err\_t | [**led\_strip\_set\_pixel\_rgbw**](#function-led_strip_set_pixel_rgbw) ([**led\_strip\_handle\_t**](#struct-led_strip_t) strip, uint32\_t index, uint32\_t red, uint32\_t green, uint32\_t blue, uint32\_t white) <br>_Set RGBW for a specific pixel._ |
## Functions Documentation
### function `led_strip_clear`
_Clear LED strip (turn off all LEDs)_
```c
esp_err_t led_strip_clear (
led_strip_handle_t strip
)
```
**Parameters:**
- `strip` LED strip
**Returns:**
- ESP\_OK: Clear LEDs successfully
- ESP\_FAIL: Clear LEDs failed because some other error occurred
### function `led_strip_del`
_Free LED strip resources._
```c
esp_err_t led_strip_del (
led_strip_handle_t strip
)
```
**Parameters:**
- `strip` LED strip
**Returns:**
- ESP\_OK: Free resources successfully
- ESP\_FAIL: Free resources failed because error occurred
### function `led_strip_refresh`
_Refresh memory colors to LEDs._
```c
esp_err_t led_strip_refresh (
led_strip_handle_t strip
)
```
**Parameters:**
- `strip` LED strip
**Returns:**
- ESP\_OK: Refresh successfully
- ESP\_FAIL: Refresh failed because some other error occurred
**Note:**
: After updating the LED colors in the memory, a following invocation of this API is needed to flush colors to strip.
### function `led_strip_set_pixel`
_Set RGB for a specific pixel._
```c
esp_err_t led_strip_set_pixel (
led_strip_handle_t strip,
uint32_t index,
uint32_t red,
uint32_t green,
uint32_t blue
)
```
**Parameters:**
- `strip` LED strip
- `index` index of pixel to set
- `red` red part of color
- `green` green part of color
- `blue` blue part of color
**Returns:**
- ESP\_OK: Set RGB for a specific pixel successfully
- ESP\_ERR\_INVALID\_ARG: Set RGB for a specific pixel failed because of invalid parameters
- ESP\_FAIL: Set RGB for a specific pixel failed because other error occurred
### function `led_strip_set_pixel_hsv`
_Set HSV for a specific pixel._
```c
esp_err_t led_strip_set_pixel_hsv (
led_strip_handle_t strip,
uint32_t index,
uint16_t hue,
uint8_t saturation,
uint8_t value
)
```
**Parameters:**
- `strip` LED strip
- `index` index of pixel to set
- `hue` hue part of color (0 - 360)
- `saturation` saturation part of color (0 - 255)
- `value` value part of color (0 - 255)
**Returns:**
- ESP\_OK: Set HSV color for a specific pixel successfully
- ESP\_ERR\_INVALID\_ARG: Set HSV color for a specific pixel failed because of an invalid argument
- ESP\_FAIL: Set HSV color for a specific pixel failed because other error occurred
### function `led_strip_set_pixel_rgbw`
_Set RGBW for a specific pixel._
```c
esp_err_t led_strip_set_pixel_rgbw (
led_strip_handle_t strip,
uint32_t index,
uint32_t red,
uint32_t green,
uint32_t blue,
uint32_t white
)
```
**Note:**
Only call this function if your led strip does have the white component (e.g. SK6812-RGBW)
**Note:**
Also see `led_strip_set_pixel` if you only want to specify the RGB part of the color and bypass the white component
**Parameters:**
- `strip` LED strip
- `index` index of pixel to set
- `red` red part of color
- `green` green part of color
- `blue` blue part of color
- `white` separate white component
**Returns:**
- ESP\_OK: Set RGBW color for a specific pixel successfully
- ESP\_ERR\_INVALID\_ARG: Set RGBW color for a specific pixel failed because of an invalid argument
- ESP\_FAIL: Set RGBW color for a specific pixel failed because other error occurred
## File include/led_strip_rmt.h
## Structures and Types
| Type | Name |
| ---: | :--- |
| struct | [**led\_strip\_rmt\_config\_t**](#struct-led_strip_rmt_config_t) <br>_LED Strip RMT specific configuration._ |
## Functions
| Type | Name |
| ---: | :--- |
| esp\_err\_t | [**led\_strip\_new\_rmt\_device**](#function-led_strip_new_rmt_device) (const [**led\_strip\_config\_t**](#struct-led_strip_config_t) \*led\_config, const [**led\_strip\_rmt\_config\_t**](#struct-led_strip_rmt_config_t) \*rmt\_config, [**led\_strip\_handle\_t**](#struct-led_strip_t) \*ret\_strip) <br>_Create LED strip based on RMT TX channel._ |
## Structures and Types Documentation
### struct `led_strip_rmt_config_t`
_LED Strip RMT specific configuration._
Variables:
- rmt\_clock\_source\_t clk_src <br>RMT clock source
- struct [**led\_strip\_rmt\_config\_t**](#struct-led_strip_rmt_config_t) flags <br>Extra driver flags
- size\_t mem_block_symbols <br>How many RMT symbols can one RMT channel hold at one time. Set to 0 will fallback to use the default size.
- uint32\_t resolution_hz <br>RMT tick resolution, if set to zero, a default resolution (10MHz) will be applied
- uint32\_t with_dma <br>Use DMA to transmit data
## Functions Documentation
### function `led_strip_new_rmt_device`
_Create LED strip based on RMT TX channel._
```c
esp_err_t led_strip_new_rmt_device (
const led_strip_config_t *led_config,
const led_strip_rmt_config_t *rmt_config,
led_strip_handle_t *ret_strip
)
```
**Parameters:**
- `led_config` LED strip configuration
- `rmt_config` RMT specific configuration
- `ret_strip` Returned LED strip handle
**Returns:**
- ESP\_OK: create LED strip handle successfully
- ESP\_ERR\_INVALID\_ARG: create LED strip handle failed because of invalid argument
- ESP\_ERR\_NO\_MEM: create LED strip handle failed because of out of memory
- ESP\_FAIL: create LED strip handle failed because some other error
## File include/led_strip_spi.h
## Structures and Types
| Type | Name |
| ---: | :--- |
| struct | [**led\_strip\_spi\_config\_t**](#struct-led_strip_spi_config_t) <br>_LED Strip SPI specific configuration._ |
## Functions
| Type | Name |
| ---: | :--- |
| esp\_err\_t | [**led\_strip\_new\_spi\_device**](#function-led_strip_new_spi_device) (const [**led\_strip\_config\_t**](#struct-led_strip_config_t) \*led\_config, const [**led\_strip\_spi\_config\_t**](#struct-led_strip_spi_config_t) \*spi\_config, [**led\_strip\_handle\_t**](#struct-led_strip_t) \*ret\_strip) <br>_Create LED strip based on SPI MOSI channel._ |
## Structures and Types Documentation
### struct `led_strip_spi_config_t`
_LED Strip SPI specific configuration._
Variables:
- spi\_clock\_source\_t clk_src <br>SPI clock source
- struct [**led\_strip\_spi\_config\_t**](#struct-led_strip_spi_config_t) flags <br>Extra driver flags
- spi\_host\_device\_t spi_bus <br>SPI bus ID. Which buses are available depends on the specific chip
- uint32\_t with_dma <br>Use DMA to transmit data
## Functions Documentation
### function `led_strip_new_spi_device`
_Create LED strip based on SPI MOSI channel._
```c
esp_err_t led_strip_new_spi_device (
const led_strip_config_t *led_config,
const led_strip_spi_config_t *spi_config,
led_strip_handle_t *ret_strip
)
```
**Note:**
Although only the MOSI line is used for generating the signal, the whole SPI bus can't be used for other purposes.
**Parameters:**
- `led_config` LED strip configuration
- `spi_config` SPI specific configuration
- `ret_strip` Returned LED strip handle
**Returns:**
- ESP\_OK: create LED strip handle successfully
- ESP\_ERR\_INVALID\_ARG: create LED strip handle failed because of invalid argument
- ESP\_ERR\_NOT\_SUPPORTED: create LED strip handle failed because of unsupported configuration
- ESP\_ERR\_NO\_MEM: create LED strip handle failed because of out of memory
- ESP\_FAIL: create LED strip handle failed because some other error
## File include/led_strip_types.h
## Structures and Types
| Type | Name |
| ---: | :--- |
| enum | [**led\_model\_t**](#enum-led_model_t) <br>_LED strip model._ |
| enum | [**led\_pixel\_format\_t**](#enum-led_pixel_format_t) <br>_LED strip pixel format._ |
| struct | [**led\_strip\_config\_t**](#struct-led_strip_config_t) <br>_LED Strip Configuration._ |
| typedef struct [**led\_strip\_t**](#struct-led_strip_t) \* | [**led\_strip\_handle\_t**](#typedef-led_strip_handle_t) <br>_LED strip handle._ |
## Structures and Types Documentation
### enum `led_model_t`
_LED strip model._
```c
enum led_model_t {
LED_MODEL_WS2812,
LED_MODEL_SK6812,
LED_MODEL_INVALID
};
```
**Note:**
Different led model may have different timing parameters, so we need to distinguish them.
### enum `led_pixel_format_t`
_LED strip pixel format._
```c
enum led_pixel_format_t {
LED_PIXEL_FORMAT_GRB,
LED_PIXEL_FORMAT_GRBW,
LED_PIXEL_FORMAT_INVALID
};
```
### struct `led_strip_config_t`
_LED Strip Configuration._
Variables:
- struct [**led\_strip\_config\_t**](#struct-led_strip_config_t) flags <br>Extra driver flags
- uint32\_t invert_out <br>Invert output signal
- led\_model\_t led_model <br>LED model
- led\_pixel\_format\_t led_pixel_format <br>LED pixel format
- uint32\_t max_leds <br>Maximum LEDs in a single strip
- int strip_gpio_num <br>GPIO number that used by LED strip
### typedef `led_strip_handle_t`
_LED strip handle._
```c
typedef struct led_strip_t* led_strip_handle_t;
```
## File interface/led_strip_interface.h
## Structures and Types
| Type | Name |
| ---: | :--- |
| struct | [**led\_strip\_t**](#struct-led_strip_t) <br>_LED strip interface definition._ |
| typedef struct [**led\_strip\_t**](#struct-led_strip_t) | [**led\_strip\_t**](#typedef-led_strip_t) <br> |
## Structures and Types Documentation
### struct `led_strip_t`
_LED strip interface definition._
Variables:
- esp\_err\_t(\* clear <br>_Clear LED strip (turn off all LEDs)_<br>**Parameters:**
- `strip` LED strip
- `timeout_ms` timeout value for clearing task
**Returns:**
- ESP\_OK: Clear LEDs successfully
- ESP\_FAIL: Clear LEDs failed because some other error occurred
- esp\_err\_t(\* del <br>_Free LED strip resources._<br>**Parameters:**
- `strip` LED strip
**Returns:**
- ESP\_OK: Free resources successfully
- ESP\_FAIL: Free resources failed because error occurred
- esp\_err\_t(\* refresh <br>_Refresh memory colors to LEDs._<br>**Parameters:**
- `strip` LED strip
- `timeout_ms` timeout value for refreshing task
**Returns:**
- ESP\_OK: Refresh successfully
- ESP\_FAIL: Refresh failed because some other error occurred
**Note:**
: After updating the LED colors in the memory, a following invocation of this API is needed to flush colors to strip.
- esp\_err\_t(\* set_pixel <br>_Set RGB for a specific pixel._<br>**Parameters:**
- `strip` LED strip
- `index` index of pixel to set
- `red` red part of color
- `green` green part of color
- `blue` blue part of color
**Returns:**
- ESP\_OK: Set RGB for a specific pixel successfully
- ESP\_ERR\_INVALID\_ARG: Set RGB for a specific pixel failed because of invalid parameters
- ESP\_FAIL: Set RGB for a specific pixel failed because other error occurred
- esp\_err\_t(\* set_pixel_rgbw <br>_Set RGBW for a specific pixel. Similar to_ `set_pixel`_but also set the white component._<br>**Parameters:**
- `strip` LED strip
- `index` index of pixel to set
- `red` red part of color
- `green` green part of color
- `blue` blue part of color
- `white` separate white component
**Returns:**
- ESP\_OK: Set RGBW color for a specific pixel successfully
- ESP\_ERR\_INVALID\_ARG: Set RGBW color for a specific pixel failed because of an invalid argument
- ESP\_FAIL: Set RGBW color for a specific pixel failed because other error occurred
### typedef `led_strip_t`
```c
typedef struct led_strip_t led_strip_t;
```
Type of LED strip

View file

@ -0,0 +1,8 @@
# For more information about build system see
# https://docs.espressif.com/projects/esp-idf/en/latest/api-guides/build-system.html
# The following five lines of boilerplate have to be in your project's
# CMakeLists in this exact order for cmake to work correctly
cmake_minimum_required(VERSION 3.16)
include($ENV{IDF_PATH}/tools/cmake/project.cmake)
project(led_strip_rmt_ws2812)

View file

@ -0,0 +1,31 @@
# LED Strip Example (RMT backend + WS2812)
This example demonstrates how to blink the WS2812 LED using the [led_strip](https://components.espressif.com/component/espressif/led_strip) component.
## How to Use Example
### Hardware Required
* A development board with Espressif SoC
* A USB cable for Power supply and programming
* WS2812 LED strip
### Configure the Example
Before project configuration and build, be sure to set the correct chip target using `idf.py set-target <chip_name>`. Then assign the proper GPIO in the [source file](main/led_strip_rmt_ws2812_main.c). If your led strip has multiple LEDs, don't forget update the number.
### Build and Flash
Run `idf.py -p PORT build flash monitor` to build, flash and monitor the project.
(To exit the serial monitor, type ``Ctrl-]``.)
See the [Getting Started Guide](https://docs.espressif.com/projects/esp-idf/en/latest/get-started/index.html) for full steps to configure and use ESP-IDF to build projects.
## Example Output
```text
I (299) gpio: GPIO[8]| InputEn: 0| OutputEn: 1| OpenDrain: 0| Pullup: 1| Pulldown: 0| Intr:0
I (309) example: Created LED strip object with RMT backend
I (309) example: Start blinking LED strip
```

View file

@ -0,0 +1,2 @@
idf_component_register(SRCS "led_strip_rmt_ws2812_main.c"
INCLUDE_DIRS ".")

View file

@ -0,0 +1,5 @@
## IDF Component Manager Manifest File
dependencies:
espressif/led_strip:
version: '^2'
override_path: '../../../'

View file

@ -0,0 +1,75 @@
/*
* SPDX-FileCopyrightText: 2023 Espressif Systems (Shanghai) CO LTD
*
* SPDX-License-Identifier: Unlicense OR CC0-1.0
*/
#include <stdio.h>
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "led_strip.h"
#include "esp_log.h"
#include "esp_err.h"
// GPIO assignment
#define LED_STRIP_BLINK_GPIO 2
// Numbers of the LED in the strip
#define LED_STRIP_LED_NUMBERS 24
// 10MHz resolution, 1 tick = 0.1us (led strip needs a high resolution)
#define LED_STRIP_RMT_RES_HZ (10 * 1000 * 1000)
static const char *TAG = "example";
led_strip_handle_t configure_led(void)
{
// LED strip general initialization, according to your led board design
led_strip_config_t strip_config = {
.strip_gpio_num = LED_STRIP_BLINK_GPIO, // The GPIO that connected to the LED strip's data line
.max_leds = LED_STRIP_LED_NUMBERS, // The number of LEDs in the strip,
.led_pixel_format = LED_PIXEL_FORMAT_GRB, // Pixel format of your LED strip
.led_model = LED_MODEL_WS2812, // LED strip model
.flags.invert_out = false, // whether to invert the output signal
};
// LED strip backend configuration: RMT
led_strip_rmt_config_t rmt_config = {
#if ESP_IDF_VERSION < ESP_IDF_VERSION_VAL(5, 0, 0)
.rmt_channel = 0,
#else
.clk_src = RMT_CLK_SRC_DEFAULT, // different clock source can lead to different power consumption
.resolution_hz = LED_STRIP_RMT_RES_HZ, // RMT counter clock frequency
.flags.with_dma = false, // DMA feature is available on ESP target like ESP32-S3
#endif
};
// LED Strip object handle
led_strip_handle_t led_strip;
ESP_ERROR_CHECK(led_strip_new_rmt_device(&strip_config, &rmt_config, &led_strip));
ESP_LOGI(TAG, "Created LED strip object with RMT backend");
return led_strip;
}
void app_main(void)
{
led_strip_handle_t led_strip = configure_led();
bool led_on_off = false;
ESP_LOGI(TAG, "Start blinking LED strip");
while (1) {
if (led_on_off) {
/* Set the LED pixel using RGB from 0 (0%) to 255 (100%) for each color */
for (int i = 0; i < LED_STRIP_LED_NUMBERS; i++) {
ESP_ERROR_CHECK(led_strip_set_pixel(led_strip, i, 5, 5, 5));
}
/* Refresh the strip to send data */
ESP_ERROR_CHECK(led_strip_refresh(led_strip));
ESP_LOGI(TAG, "LED ON!");
} else {
/* Set all LED off to clear all pixels */
ESP_ERROR_CHECK(led_strip_clear(led_strip));
ESP_LOGI(TAG, "LED OFF!");
}
led_on_off = !led_on_off;
vTaskDelay(pdMS_TO_TICKS(500));
}
}

View file

@ -0,0 +1,8 @@
# For more information about build system see
# https://docs.espressif.com/projects/esp-idf/en/latest/api-guides/build-system.html
# The following five lines of boilerplate have to be in your project's
# CMakeLists in this exact order for cmake to work correctly
cmake_minimum_required(VERSION 3.16)
include($ENV{IDF_PATH}/tools/cmake/project.cmake)
project(led_strip_spi_ws2812)

View file

@ -0,0 +1,31 @@
# LED Strip Example (SPI backend + WS2812)
This example demonstrates how to blink the WS2812 LED using the [led_strip](https://components.espressif.com/component/espressif/led_strip) component.
## How to Use Example
### Hardware Required
* A development board with Espressif SoC
* A USB cable for Power supply and programming
* WS2812 LED strip
### Configure the Example
Before project configuration and build, be sure to set the correct chip target using `idf.py set-target <chip_name>`. Then assign the proper GPIO in the [source file](main/led_strip_spi_ws2812_main.c). If your led strip has multiple LEDs, don't forget update the number.
### Build and Flash
Run `idf.py -p PORT build flash monitor` to build, flash and monitor the project.
(To exit the serial monitor, type ``Ctrl-]``.)
See the [Getting Started Guide](https://docs.espressif.com/projects/esp-idf/en/latest/get-started/index.html) for full steps to configure and use ESP-IDF to build projects.
## Example Output
```text
I (299) gpio: GPIO[14]| InputEn: 0| OutputEn: 1| OpenDrain: 0| Pullup: 1| Pulldown: 0| Intr:0
I (309) example: Created LED strip object with SPI backend
I (309) example: Start blinking LED strip
```

View file

@ -0,0 +1,2 @@
idf_component_register(SRCS "led_strip_spi_ws2812_main.c"
INCLUDE_DIRS ".")

View file

@ -0,0 +1,6 @@
## IDF Component Manager Manifest File
dependencies:
espressif/led_strip:
version: '^2.4'
override_path: '../../../'
idf: ">=5.1"

View file

@ -0,0 +1,69 @@
/*
* SPDX-FileCopyrightText: 2023 Espressif Systems (Shanghai) CO LTD
*
* SPDX-License-Identifier: Unlicense OR CC0-1.0
*/
#include <stdio.h>
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "led_strip.h"
#include "esp_log.h"
#include "esp_err.h"
// GPIO assignment
#define LED_STRIP_BLINK_GPIO 2
// Numbers of the LED in the strip
#define LED_STRIP_LED_NUMBERS 24
static const char *TAG = "example";
led_strip_handle_t configure_led(void)
{
// LED strip general initialization, according to your led board design
led_strip_config_t strip_config = {
.strip_gpio_num = LED_STRIP_BLINK_GPIO, // The GPIO that connected to the LED strip's data line
.max_leds = LED_STRIP_LED_NUMBERS, // The number of LEDs in the strip,
.led_pixel_format = LED_PIXEL_FORMAT_GRB, // Pixel format of your LED strip
.led_model = LED_MODEL_WS2812, // LED strip model
.flags.invert_out = false, // whether to invert the output signal
};
// LED strip backend configuration: SPI
led_strip_spi_config_t spi_config = {
.clk_src = SPI_CLK_SRC_DEFAULT, // different clock source can lead to different power consumption
.flags.with_dma = true, // Using DMA can improve performance and help drive more LEDs
.spi_bus = SPI2_HOST, // SPI bus ID
};
// LED Strip object handle
led_strip_handle_t led_strip;
ESP_ERROR_CHECK(led_strip_new_spi_device(&strip_config, &spi_config, &led_strip));
ESP_LOGI(TAG, "Created LED strip object with SPI backend");
return led_strip;
}
void app_main(void)
{
led_strip_handle_t led_strip = configure_led();
bool led_on_off = false;
ESP_LOGI(TAG, "Start blinking LED strip");
while (1) {
if (led_on_off) {
/* Set the LED pixel using RGB from 0 (0%) to 255 (100%) for each color */
for (int i = 0; i < LED_STRIP_LED_NUMBERS; i++) {
ESP_ERROR_CHECK(led_strip_set_pixel(led_strip, i, 5, 5, 5));
}
/* Refresh the strip to send data */
ESP_ERROR_CHECK(led_strip_refresh(led_strip));
ESP_LOGI(TAG, "LED ON!");
} else {
/* Set all LED off to clear all pixels */
ESP_ERROR_CHECK(led_strip_clear(led_strip));
ESP_LOGI(TAG, "LED OFF!");
}
led_on_off = !led_on_off;
vTaskDelay(pdMS_TO_TICKS(500));
}
}

View file

@ -0,0 +1,6 @@
dependencies:
idf:
version: '>=4.4'
description: Multiplexed Driver for Addressable LED Strip (WS2812, etc)
url: https://github.com/espressif/idf-extra-components/tree/master/led_strip
version: 2.5.3

View file

@ -0,0 +1,111 @@
/*
* SPDX-FileCopyrightText: 2022-2023 Espressif Systems (Shanghai) CO LTD
*
* SPDX-License-Identifier: Apache-2.0
*/
#pragma once
#include <stdint.h>
#include "esp_err.h"
#include "led_strip_rmt.h"
#include "esp_idf_version.h"
#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 1, 0)
#include "led_strip_spi.h"
#endif
#ifdef __cplusplus
extern "C" {
#endif
/**
* @brief Set RGB for a specific pixel
*
* @param strip: LED strip
* @param index: index of pixel to set
* @param red: red part of color
* @param green: green part of color
* @param blue: blue part of color
*
* @return
* - ESP_OK: Set RGB for a specific pixel successfully
* - ESP_ERR_INVALID_ARG: Set RGB for a specific pixel failed because of invalid parameters
* - ESP_FAIL: Set RGB for a specific pixel failed because other error occurred
*/
esp_err_t led_strip_set_pixel(led_strip_handle_t strip, uint32_t index, uint32_t red, uint32_t green, uint32_t blue);
/**
* @brief Set RGBW for a specific pixel
*
* @note Only call this function if your led strip does have the white component (e.g. SK6812-RGBW)
* @note Also see `led_strip_set_pixel` if you only want to specify the RGB part of the color and bypass the white component
*
* @param strip: LED strip
* @param index: index of pixel to set
* @param red: red part of color
* @param green: green part of color
* @param blue: blue part of color
* @param white: separate white component
*
* @return
* - ESP_OK: Set RGBW color for a specific pixel successfully
* - ESP_ERR_INVALID_ARG: Set RGBW color for a specific pixel failed because of an invalid argument
* - ESP_FAIL: Set RGBW color for a specific pixel failed because other error occurred
*/
esp_err_t led_strip_set_pixel_rgbw(led_strip_handle_t strip, uint32_t index, uint32_t red, uint32_t green, uint32_t blue, uint32_t white);
/**
* @brief Set HSV for a specific pixel
*
* @param strip: LED strip
* @param index: index of pixel to set
* @param hue: hue part of color (0 - 360)
* @param saturation: saturation part of color (0 - 255)
* @param value: value part of color (0 - 255)
*
* @return
* - ESP_OK: Set HSV color for a specific pixel successfully
* - ESP_ERR_INVALID_ARG: Set HSV color for a specific pixel failed because of an invalid argument
* - ESP_FAIL: Set HSV color for a specific pixel failed because other error occurred
*/
esp_err_t led_strip_set_pixel_hsv(led_strip_handle_t strip, uint32_t index, uint16_t hue, uint8_t saturation, uint8_t value);
/**
* @brief Refresh memory colors to LEDs
*
* @param strip: LED strip
*
* @return
* - ESP_OK: Refresh successfully
* - ESP_FAIL: Refresh failed because some other error occurred
*
* @note:
* After updating the LED colors in the memory, a following invocation of this API is needed to flush colors to strip.
*/
esp_err_t led_strip_refresh(led_strip_handle_t strip);
/**
* @brief Clear LED strip (turn off all LEDs)
*
* @param strip: LED strip
*
* @return
* - ESP_OK: Clear LEDs successfully
* - ESP_FAIL: Clear LEDs failed because some other error occurred
*/
esp_err_t led_strip_clear(led_strip_handle_t strip);
/**
* @brief Free LED strip resources
*
* @param strip: LED strip
*
* @return
* - ESP_OK: Free resources successfully
* - ESP_FAIL: Free resources failed because error occurred
*/
esp_err_t led_strip_del(led_strip_handle_t strip);
#ifdef __cplusplus
}
#endif

View file

@ -0,0 +1,53 @@
/*
* SPDX-FileCopyrightText: 2022-2023 Espressif Systems (Shanghai) CO LTD
*
* SPDX-License-Identifier: Apache-2.0
*/
#pragma once
#include <stdint.h>
#include "esp_err.h"
#include "led_strip_types.h"
#include "esp_idf_version.h"
#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 0, 0)
#include "driver/rmt_types.h"
#endif
#ifdef __cplusplus
extern "C" {
#endif
/**
* @brief LED Strip RMT specific configuration
*/
typedef struct {
#if ESP_IDF_VERSION < ESP_IDF_VERSION_VAL(5, 0, 0)
uint8_t rmt_channel; /*!< Specify the channel number, the legacy RMT driver doesn't support channel allocator */
#else // new driver supports specify the clock source and clock resolution
rmt_clock_source_t clk_src; /*!< RMT clock source */
uint32_t resolution_hz; /*!< RMT tick resolution, if set to zero, a default resolution (10MHz) will be applied */
#endif
size_t mem_block_symbols; /*!< How many RMT symbols can one RMT channel hold at one time. Set to 0 will fallback to use the default size. */
struct {
uint32_t with_dma: 1; /*!< Use DMA to transmit data */
} flags; /*!< Extra driver flags */
} led_strip_rmt_config_t;
/**
* @brief Create LED strip based on RMT TX channel
*
* @param led_config LED strip configuration
* @param rmt_config RMT specific configuration
* @param ret_strip Returned LED strip handle
* @return
* - ESP_OK: create LED strip handle successfully
* - ESP_ERR_INVALID_ARG: create LED strip handle failed because of invalid argument
* - ESP_ERR_NO_MEM: create LED strip handle failed because of out of memory
* - ESP_FAIL: create LED strip handle failed because some other error
*/
esp_err_t led_strip_new_rmt_device(const led_strip_config_t *led_config, const led_strip_rmt_config_t *rmt_config, led_strip_handle_t *ret_strip);
#ifdef __cplusplus
}
#endif

View file

@ -0,0 +1,46 @@
/*
* SPDX-FileCopyrightText: 2022-2023 Espressif Systems (Shanghai) CO LTD
*
* SPDX-License-Identifier: Apache-2.0
*/
#pragma once
#include <stdint.h>
#include "esp_err.h"
#include "driver/spi_master.h"
#include "led_strip_types.h"
#ifdef __cplusplus
extern "C" {
#endif
/**
* @brief LED Strip SPI specific configuration
*/
typedef struct {
spi_clock_source_t clk_src; /*!< SPI clock source */
spi_host_device_t spi_bus; /*!< SPI bus ID. Which buses are available depends on the specific chip */
struct {
uint32_t with_dma: 1; /*!< Use DMA to transmit data */
} flags; /*!< Extra driver flags */
} led_strip_spi_config_t;
/**
* @brief Create LED strip based on SPI MOSI channel
* @note Although only the MOSI line is used for generating the signal, the whole SPI bus can't be used for other purposes.
*
* @param led_config LED strip configuration
* @param spi_config SPI specific configuration
* @param ret_strip Returned LED strip handle
* @return
* - ESP_OK: create LED strip handle successfully
* - ESP_ERR_INVALID_ARG: create LED strip handle failed because of invalid argument
* - ESP_ERR_NOT_SUPPORTED: create LED strip handle failed because of unsupported configuration
* - ESP_ERR_NO_MEM: create LED strip handle failed because of out of memory
* - ESP_FAIL: create LED strip handle failed because some other error
*/
esp_err_t led_strip_new_spi_device(const led_strip_config_t *led_config, const led_strip_spi_config_t *spi_config, led_strip_handle_t *ret_strip);
#ifdef __cplusplus
}
#endif

View file

@ -0,0 +1,54 @@
/*
* SPDX-FileCopyrightText: 2022-2023 Espressif Systems (Shanghai) CO LTD
*
* SPDX-License-Identifier: Apache-2.0
*/
#pragma once
#include <stdint.h>
#ifdef __cplusplus
extern "C" {
#endif
/**
* @brief LED strip pixel format
*/
typedef enum {
LED_PIXEL_FORMAT_GRB, /*!< Pixel format: GRB */
LED_PIXEL_FORMAT_GRBW, /*!< Pixel format: GRBW */
LED_PIXEL_FORMAT_INVALID /*!< Invalid pixel format */
} led_pixel_format_t;
/**
* @brief LED strip model
* @note Different led model may have different timing parameters, so we need to distinguish them.
*/
typedef enum {
LED_MODEL_WS2812, /*!< LED strip model: WS2812 */
LED_MODEL_SK6812, /*!< LED strip model: SK6812 */
LED_MODEL_INVALID /*!< Invalid LED strip model */
} led_model_t;
/**
* @brief LED strip handle
*/
typedef struct led_strip_t *led_strip_handle_t;
/**
* @brief LED Strip Configuration
*/
typedef struct {
int strip_gpio_num; /*!< GPIO number that used by LED strip */
uint32_t max_leds; /*!< Maximum LEDs in a single strip */
led_pixel_format_t led_pixel_format; /*!< LED pixel format */
led_model_t led_model; /*!< LED model */
struct {
uint32_t invert_out: 1; /*!< Invert output signal */
} flags; /*!< Extra driver flags */
} led_strip_config_t;
#ifdef __cplusplus
}
#endif

View file

@ -0,0 +1,116 @@
/*
* SPDX-FileCopyrightText: 2022-2023 Espressif Systems (Shanghai) CO LTD
*
* SPDX-License-Identifier: Apache-2.0
*/
#pragma once
#include <stdint.h>
#include "esp_err.h"
#include "muxed_led_strip_rmt.h"
#include "esp_idf_version.h"
#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 1, 0)
#include "led_strip_spi.h"
#endif
#ifdef __cplusplus
extern "C" {
#endif
/**
* @brief Set RGB for a specific pixel
*
* @param strip: LED strip
* @param channel: channel of pixel to set
* @param index: index of pixel to set
* @param red: red part of color
* @param green: green part of color
* @param blue: blue part of color
*
* @return
* - ESP_OK: Set RGB for a specific pixel successfully
* - ESP_ERR_INVALID_ARG: Set RGB for a specific pixel failed because of invalid parameters
* - ESP_FAIL: Set RGB for a specific pixel failed because other error occurred
*/
esp_err_t muxed_led_strip_set_pixel(muxed_led_strip_handle_t strip, uint32_t channel, uint32_t index, uint32_t red, uint32_t green, uint32_t blue);
/**
* @brief Set RGBW for a specific pixel
*
* @note Only call this function if your led strip does have the white component (e.g. SK6812-RGBW)
* @note Also see `led_strip_set_pixel` if you only want to specify the RGB part of the color and bypass the white component
*
* @param strip: LED strip
* @param channel: channel of pixel to set
* @param index: index of pixel to set
* @param red: red part of color
* @param green: green part of color
* @param blue: blue part of color
* @param white: separate white component
*
* @return
* - ESP_OK: Set RGBW color for a specific pixel successfully
* - ESP_ERR_INVALID_ARG: Set RGBW color for a specific pixel failed because of an invalid argument
* - ESP_FAIL: Set RGBW color for a specific pixel failed because other error occurred
*/
esp_err_t muxed_led_strip_set_pixel_rgbw(muxed_led_strip_handle_t strip, uint32_t channel, uint32_t index, uint32_t red, uint32_t green, uint32_t blue, uint32_t white);
/**
* @brief Set HSV for a specific pixel
*
* @param strip: LED strip
* @param channel: channel of pixel to set
* @param index: index of pixel to set
* @param hue: hue part of color (0 - 360)
* @param saturation: saturation part of color (0 - 255)
* @param value: value part of color (0 - 255)
*
* @return
* - ESP_OK: Set HSV color for a specific pixel successfully
* - ESP_ERR_INVALID_ARG: Set HSV color for a specific pixel failed because of an invalid argument
* - ESP_FAIL: Set HSV color for a specific pixel failed because other error occurred
*/
esp_err_t muxed_led_strip_set_pixel_hsv(muxed_led_strip_handle_t strip, uint32_t channel, uint32_t index, uint16_t hue, uint8_t saturation, uint8_t value);
/**
* @brief Refresh memory colors to LEDs
*
* @param strip: LED strip
* @param channel: which channel to refresh
*
* @return
* - ESP_OK: Refresh successfully
* - ESP_FAIL: Refresh failed because some other error occurred
*
* @note:
* After updating the LED colors in the memory, a following invocation of this API is needed to flush colors to strip.
*/
esp_err_t muxed_led_strip_refresh(muxed_led_strip_handle_t strip, uint32_t channel);
/**
* @brief Clear LED strip (turn off all LEDs)
*
* @param strip: LED strip
* @param channel: which channel to clear
*
* @return
* - ESP_OK: Clear LEDs successfully
* - ESP_FAIL: Clear LEDs failed because some other error occurred
*/
esp_err_t muxed_led_strip_clear(muxed_led_strip_handle_t strip, uint32_t channel);
/**
* @brief Free LED strip resources
*
* @param strip: LED strip
*
* @return
* - ESP_OK: Free resources successfully
* - ESP_FAIL: Free resources failed because error occurred
*/
esp_err_t muxed_led_strip_del(muxed_led_strip_handle_t strip);
#ifdef __cplusplus
}
#endif

View file

@ -0,0 +1,41 @@
/*
* SPDX-FileCopyrightText: 2022-2023 Espressif Systems (Shanghai) CO LTD
*
* SPDX-License-Identifier: Apache-2.0
*/
#pragma once
#include <stdint.h>
#include "esp_err.h"
#include "led_strip_types.h"
#include "muxed_led_strip_types.h"
#include "led_strip_rmt.h"
#include "esp_idf_version.h"
#include <driver/rmt_tx.h>
#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 0, 0)
#include "driver/rmt_types.h"
#endif
#ifdef __cplusplus
extern "C" {
#endif
/**
* @brief Create multiplexed LED strip based on RMT TX channel
*
* @param led_config LED strip configuration
* @param rmt_config RMT specific configuration
* @param cbs Group of RMT TX callback functions
* @param ret_strip Returned LED strip handle
* @return
* - ESP_OK: create LED strip handle successfully
* - ESP_ERR_INVALID_ARG: create LED strip handle failed because of invalid argument
* - ESP_ERR_NO_MEM: create LED strip handle failed because of out of memory
* - ESP_FAIL: create LED strip handle failed because some other error
*/
esp_err_t muxed_led_strip_new_rmt_device(const muxed_led_strip_config_t *led_config, const led_strip_rmt_config_t *rmt_config, const rmt_tx_event_callbacks_t *cbs, muxed_led_strip_handle_t *ret_strip);
#ifdef __cplusplus
}
#endif

View file

@ -0,0 +1,36 @@
/*
* SPDX-FileCopyrightText: 2022-2023 Espressif Systems (Shanghai) CO LTD
*
* SPDX-License-Identifier: Apache-2.0
*/
#pragma once
#include <stdint.h>
#ifdef __cplusplus
extern "C" {
#endif
/**
* @brief Multiplexed LED strip handle
*/
typedef struct muxed_led_strip_t *muxed_led_strip_handle_t;
/**
* @brief LED Strip Configuration
*/
typedef struct {
int strip_gpio_num; /*!< GPIO number that used by LED strip */
uint32_t channels; /*!< Number of multiplexed channels */
uint32_t max_leds; /*!< Maximum LEDs in a single strip */
led_pixel_format_t led_pixel_format; /*!< LED pixel format */
led_model_t led_model; /*!< LED model */
struct {
uint32_t invert_out: 1; /*!< Invert output signal */
} flags; /*!< Extra driver flags */
} muxed_led_strip_config_t;
#ifdef __cplusplus
}
#endif

View file

@ -0,0 +1,95 @@
/*
* SPDX-FileCopyrightText: 2022 Espressif Systems (Shanghai) CO LTD
*
* SPDX-License-Identifier: Apache-2.0
*/
#pragma once
#include <stdint.h>
#include "esp_err.h"
#ifdef __cplusplus
extern "C" {
#endif
typedef struct led_strip_t led_strip_t; /*!< Type of LED strip */
/**
* @brief LED strip interface definition
*/
struct led_strip_t {
/**
* @brief Set RGB for a specific pixel
*
* @param strip: LED strip
* @param index: index of pixel to set
* @param red: red part of color
* @param green: green part of color
* @param blue: blue part of color
*
* @return
* - ESP_OK: Set RGB for a specific pixel successfully
* - ESP_ERR_INVALID_ARG: Set RGB for a specific pixel failed because of invalid parameters
* - ESP_FAIL: Set RGB for a specific pixel failed because other error occurred
*/
esp_err_t (*set_pixel)(led_strip_t *strip, uint32_t index, uint32_t red, uint32_t green, uint32_t blue);
/**
* @brief Set RGBW for a specific pixel. Similar to `set_pixel` but also set the white component
*
* @param strip: LED strip
* @param index: index of pixel to set
* @param red: red part of color
* @param green: green part of color
* @param blue: blue part of color
* @param white: separate white component
*
* @return
* - ESP_OK: Set RGBW color for a specific pixel successfully
* - ESP_ERR_INVALID_ARG: Set RGBW color for a specific pixel failed because of an invalid argument
* - ESP_FAIL: Set RGBW color for a specific pixel failed because other error occurred
*/
esp_err_t (*set_pixel_rgbw)(led_strip_t *strip, uint32_t index, uint32_t red, uint32_t green, uint32_t blue, uint32_t white);
/**
* @brief Refresh memory colors to LEDs
*
* @param strip: LED strip
* @param timeout_ms: timeout value for refreshing task
*
* @return
* - ESP_OK: Refresh successfully
* - ESP_FAIL: Refresh failed because some other error occurred
*
* @note:
* After updating the LED colors in the memory, a following invocation of this API is needed to flush colors to strip.
*/
esp_err_t (*refresh)(led_strip_t *strip);
/**
* @brief Clear LED strip (turn off all LEDs)
*
* @param strip: LED strip
* @param timeout_ms: timeout value for clearing task
*
* @return
* - ESP_OK: Clear LEDs successfully
* - ESP_FAIL: Clear LEDs failed because some other error occurred
*/
esp_err_t (*clear)(led_strip_t *strip);
/**
* @brief Free LED strip resources
*
* @param strip: LED strip
*
* @return
* - ESP_OK: Free resources successfully
* - ESP_FAIL: Free resources failed because error occurred
*/
esp_err_t (*del)(led_strip_t *strip);
};
#ifdef __cplusplus
}
#endif

View file

@ -0,0 +1,97 @@
/*
* SPDX-FileCopyrightText: 2022 Espressif Systems (Shanghai) CO LTD
*
* SPDX-License-Identifier: Apache-2.0
*/
#pragma once
#include <stdint.h>
#include "esp_err.h"
#ifdef __cplusplus
extern "C" {
#endif
typedef struct muxed_led_strip_t muxed_led_strip_t; /*!< Type of LED strip */
/**
* @brief LED strip interface definition
*/
struct muxed_led_strip_t {
/**
* @brief Set RGB for a specific pixel
*
* @param strip: LED strip
* @param channel: channel of pixel to set
* @param index: index of pixel to set
* @param red: red part of color
* @param green: green part of color
* @param blue: blue part of color
*
* @return
* - ESP_OK: Set RGB for a specific pixel successfully
* - ESP_ERR_INVALID_ARG: Set RGB for a specific pixel failed because of invalid parameters
* - ESP_FAIL: Set RGB for a specific pixel failed because other error occurred
*/
esp_err_t (*set_pixel)(muxed_led_strip_t *strip, uint32_t channel, uint32_t index, uint32_t red, uint32_t green, uint32_t blue);
/**
* @brief Set RGBW for a specific pixel. Similar to `set_pixel` but also set the white component
*
* @param strip: LED strip
* @param channel: channel of pixel to set
* @param index: index of pixel to set
* @param red: red part of color
* @param green: green part of color
* @param blue: blue part of color
* @param white: separate white component
*
* @return
* - ESP_OK: Set RGBW color for a specific pixel successfully
* - ESP_ERR_INVALID_ARG: Set RGBW color for a specific pixel failed because of an invalid argument
* - ESP_FAIL: Set RGBW color for a specific pixel failed because other error occurred
*/
esp_err_t (*set_pixel_rgbw)(muxed_led_strip_t *strip, uint32_t channel, uint32_t index, uint32_t red, uint32_t green, uint32_t blue, uint32_t white);
/**
* @brief Refresh memory colors to LEDs
*
* @param strip: LED strip
* @param channel: which channel to refresh
*
* @return
* - ESP_OK: Refresh successfully
* - ESP_FAIL: Refresh failed because some other error occurred
*
* @note:
* After updating the LED colors in the memory, a following invocation of this API is needed to flush colors to strip.
*/
esp_err_t (*refresh)(muxed_led_strip_t *strip, uint32_t channel);
/**
* @brief Clear LED strip (turn off all LEDs)
*
* @param strip: LED strip
* @param channel: which channel to clear
*
* @return
* - ESP_OK: Clear LEDs successfully
* - ESP_FAIL: Clear LEDs failed because some other error occurred
*/
esp_err_t (*clear)(muxed_led_strip_t *strip, uint32_t channel);
/**
* @brief Free LED strip resources
*
* @param strip: LED strip
*
* @return
* - ESP_OK: Free resources successfully
* - ESP_FAIL: Free resources failed because error occurred
*/
esp_err_t (*del)(muxed_led_strip_t *strip);
};
#ifdef __cplusplus
}
#endif

View file

@ -0,0 +1,94 @@
/*
* SPDX-FileCopyrightText: 2022 Espressif Systems (Shanghai) CO LTD
*
* SPDX-License-Identifier: Apache-2.0
*/
#include "esp_log.h"
#include "esp_check.h"
#include "led_strip.h"
#include "led_strip_interface.h"
static const char *TAG = "led_strip";
esp_err_t led_strip_set_pixel(led_strip_handle_t strip, uint32_t index, uint32_t red, uint32_t green, uint32_t blue)
{
ESP_RETURN_ON_FALSE(strip, ESP_ERR_INVALID_ARG, TAG, "invalid argument");
return strip->set_pixel(strip, index, red, green, blue);
}
esp_err_t led_strip_set_pixel_hsv(led_strip_handle_t strip, uint32_t index, uint16_t hue, uint8_t saturation, uint8_t value)
{
ESP_RETURN_ON_FALSE(strip, ESP_ERR_INVALID_ARG, TAG, "invalid argument");
uint32_t red = 0;
uint32_t green = 0;
uint32_t blue = 0;
uint32_t rgb_max = value;
uint32_t rgb_min = rgb_max * (255 - saturation) / 255.0f;
uint32_t i = hue / 60;
uint32_t diff = hue % 60;
// RGB adjustment amount by hue
uint32_t rgb_adj = (rgb_max - rgb_min) * diff / 60;
switch (i) {
case 0:
red = rgb_max;
green = rgb_min + rgb_adj;
blue = rgb_min;
break;
case 1:
red = rgb_max - rgb_adj;
green = rgb_max;
blue = rgb_min;
break;
case 2:
red = rgb_min;
green = rgb_max;
blue = rgb_min + rgb_adj;
break;
case 3:
red = rgb_min;
green = rgb_max - rgb_adj;
blue = rgb_max;
break;
case 4:
red = rgb_min + rgb_adj;
green = rgb_min;
blue = rgb_max;
break;
default:
red = rgb_max;
green = rgb_min;
blue = rgb_max - rgb_adj;
break;
}
return strip->set_pixel(strip, index, red, green, blue);
}
esp_err_t led_strip_set_pixel_rgbw(led_strip_handle_t strip, uint32_t index, uint32_t red, uint32_t green, uint32_t blue, uint32_t white)
{
ESP_RETURN_ON_FALSE(strip, ESP_ERR_INVALID_ARG, TAG, "invalid argument");
return strip->set_pixel_rgbw(strip, index, red, green, blue, white);
}
esp_err_t led_strip_refresh(led_strip_handle_t strip)
{
ESP_RETURN_ON_FALSE(strip, ESP_ERR_INVALID_ARG, TAG, "invalid argument");
return strip->refresh(strip);
}
esp_err_t led_strip_clear(led_strip_handle_t strip)
{
ESP_RETURN_ON_FALSE(strip, ESP_ERR_INVALID_ARG, TAG, "invalid argument");
return strip->clear(strip);
}
esp_err_t led_strip_del(led_strip_handle_t strip)
{
ESP_RETURN_ON_FALSE(strip, ESP_ERR_INVALID_ARG, TAG, "invalid argument");
return strip->del(strip);
}

View file

@ -0,0 +1,164 @@
/*
* SPDX-FileCopyrightText: 2022-2023 Espressif Systems (Shanghai) CO LTD
*
* SPDX-License-Identifier: Apache-2.0
*/
#include <stdlib.h>
#include <string.h>
#include <sys/cdefs.h>
#include "esp_log.h"
#include "esp_check.h"
#include "driver/rmt_tx.h"
#include "led_strip.h"
#include "led_strip_interface.h"
#include "led_strip_rmt_encoder.h"
#define LED_STRIP_RMT_DEFAULT_RESOLUTION 10000000 // 10MHz resolution
#define LED_STRIP_RMT_DEFAULT_TRANS_QUEUE_SIZE 4
// the memory size of each RMT channel, in words (4 bytes)
#if CONFIG_IDF_TARGET_ESP32 || CONFIG_IDF_TARGET_ESP32S2
#define LED_STRIP_RMT_DEFAULT_MEM_BLOCK_SYMBOLS 64
#else
#define LED_STRIP_RMT_DEFAULT_MEM_BLOCK_SYMBOLS 48
#endif
static const char *TAG = "led_strip_rmt";
typedef struct {
led_strip_t base;
rmt_channel_handle_t rmt_chan;
rmt_encoder_handle_t strip_encoder;
uint32_t strip_len;
uint8_t bytes_per_pixel;
uint8_t pixel_buf[];
} led_strip_rmt_obj;
static esp_err_t led_strip_rmt_set_pixel(led_strip_t *strip, uint32_t index, uint32_t red, uint32_t green, uint32_t blue)
{
led_strip_rmt_obj *rmt_strip = __containerof(strip, led_strip_rmt_obj, base);
ESP_RETURN_ON_FALSE(index < rmt_strip->strip_len, ESP_ERR_INVALID_ARG, TAG, "index out of maximum number of LEDs");
uint32_t start = index * rmt_strip->bytes_per_pixel;
// In thr order of GRB, as LED strip like WS2812 sends out pixels in this order
rmt_strip->pixel_buf[start + 0] = green & 0xFF;
rmt_strip->pixel_buf[start + 1] = red & 0xFF;
rmt_strip->pixel_buf[start + 2] = blue & 0xFF;
if (rmt_strip->bytes_per_pixel > 3) {
rmt_strip->pixel_buf[start + 3] = 0;
}
return ESP_OK;
}
static esp_err_t led_strip_rmt_set_pixel_rgbw(led_strip_t *strip, uint32_t index, uint32_t red, uint32_t green, uint32_t blue, uint32_t white)
{
led_strip_rmt_obj *rmt_strip = __containerof(strip, led_strip_rmt_obj, base);
ESP_RETURN_ON_FALSE(index < rmt_strip->strip_len, ESP_ERR_INVALID_ARG, TAG, "index out of maximum number of LEDs");
ESP_RETURN_ON_FALSE(rmt_strip->bytes_per_pixel == 4, ESP_ERR_INVALID_ARG, TAG, "wrong LED pixel format, expected 4 bytes per pixel");
uint8_t *buf_start = rmt_strip->pixel_buf + index * 4;
// SK6812 component order is GRBW
*buf_start = green & 0xFF;
*++buf_start = red & 0xFF;
*++buf_start = blue & 0xFF;
*++buf_start = white & 0xFF;
return ESP_OK;
}
static esp_err_t led_strip_rmt_refresh(led_strip_t *strip)
{
led_strip_rmt_obj *rmt_strip = __containerof(strip, led_strip_rmt_obj, base);
rmt_transmit_config_t tx_conf = {
.loop_count = 0,
};
ESP_RETURN_ON_ERROR(rmt_enable(rmt_strip->rmt_chan), TAG, "enable RMT channel failed");
ESP_RETURN_ON_ERROR(rmt_transmit(rmt_strip->rmt_chan, rmt_strip->strip_encoder, rmt_strip->pixel_buf,
rmt_strip->strip_len * rmt_strip->bytes_per_pixel, &tx_conf), TAG, "transmit pixels by RMT failed");
ESP_RETURN_ON_ERROR(rmt_tx_wait_all_done(rmt_strip->rmt_chan, -1), TAG, "flush RMT channel failed");
ESP_RETURN_ON_ERROR(rmt_disable(rmt_strip->rmt_chan), TAG, "disable RMT channel failed");
return ESP_OK;
}
static esp_err_t led_strip_rmt_clear(led_strip_t *strip)
{
led_strip_rmt_obj *rmt_strip = __containerof(strip, led_strip_rmt_obj, base);
// Write zero to turn off all leds
memset(rmt_strip->pixel_buf, 0, rmt_strip->strip_len * rmt_strip->bytes_per_pixel);
return led_strip_rmt_refresh(strip);
}
static esp_err_t led_strip_rmt_del(led_strip_t *strip)
{
led_strip_rmt_obj *rmt_strip = __containerof(strip, led_strip_rmt_obj, base);
ESP_RETURN_ON_ERROR(rmt_del_channel(rmt_strip->rmt_chan), TAG, "delete RMT channel failed");
ESP_RETURN_ON_ERROR(rmt_del_encoder(rmt_strip->strip_encoder), TAG, "delete strip encoder failed");
free(rmt_strip);
return ESP_OK;
}
esp_err_t led_strip_new_rmt_device(const led_strip_config_t *led_config, const led_strip_rmt_config_t *rmt_config, led_strip_handle_t *ret_strip)
{
led_strip_rmt_obj *rmt_strip = NULL;
esp_err_t ret = ESP_OK;
ESP_GOTO_ON_FALSE(led_config && rmt_config && ret_strip, ESP_ERR_INVALID_ARG, err, TAG, "invalid argument");
ESP_GOTO_ON_FALSE(led_config->led_pixel_format < LED_PIXEL_FORMAT_INVALID, ESP_ERR_INVALID_ARG, err, TAG, "invalid led_pixel_format");
uint8_t bytes_per_pixel = 3;
if (led_config->led_pixel_format == LED_PIXEL_FORMAT_GRBW) {
bytes_per_pixel = 4;
} else if (led_config->led_pixel_format == LED_PIXEL_FORMAT_GRB) {
bytes_per_pixel = 3;
} else {
assert(false);
}
rmt_strip = calloc(1, sizeof(led_strip_rmt_obj) + led_config->max_leds * bytes_per_pixel);
ESP_GOTO_ON_FALSE(rmt_strip, ESP_ERR_NO_MEM, err, TAG, "no mem for rmt strip");
uint32_t resolution = rmt_config->resolution_hz ? rmt_config->resolution_hz : LED_STRIP_RMT_DEFAULT_RESOLUTION;
// for backward compatibility, if the user does not set the clk_src, use the default value
rmt_clock_source_t clk_src = RMT_CLK_SRC_DEFAULT;
if (rmt_config->clk_src) {
clk_src = rmt_config->clk_src;
}
size_t mem_block_symbols = LED_STRIP_RMT_DEFAULT_MEM_BLOCK_SYMBOLS;
// override the default value if the user sets it
if (rmt_config->mem_block_symbols) {
mem_block_symbols = rmt_config->mem_block_symbols;
}
rmt_tx_channel_config_t rmt_chan_config = {
.clk_src = clk_src,
.gpio_num = led_config->strip_gpio_num,
.mem_block_symbols = mem_block_symbols,
.resolution_hz = resolution,
.trans_queue_depth = LED_STRIP_RMT_DEFAULT_TRANS_QUEUE_SIZE,
.flags.with_dma = rmt_config->flags.with_dma,
.flags.invert_out = led_config->flags.invert_out,
};
ESP_GOTO_ON_ERROR(rmt_new_tx_channel(&rmt_chan_config, &rmt_strip->rmt_chan), err, TAG, "create RMT TX channel failed");
led_strip_encoder_config_t strip_encoder_conf = {
.resolution = resolution,
.led_model = led_config->led_model
};
ESP_GOTO_ON_ERROR(rmt_new_led_strip_encoder(&strip_encoder_conf, &rmt_strip->strip_encoder), err, TAG, "create LED strip encoder failed");
rmt_strip->bytes_per_pixel = bytes_per_pixel;
rmt_strip->strip_len = led_config->max_leds;
rmt_strip->base.set_pixel = led_strip_rmt_set_pixel;
rmt_strip->base.set_pixel_rgbw = led_strip_rmt_set_pixel_rgbw;
rmt_strip->base.refresh = led_strip_rmt_refresh;
rmt_strip->base.clear = led_strip_rmt_clear;
rmt_strip->base.del = led_strip_rmt_del;
*ret_strip = &rmt_strip->base;
return ESP_OK;
err:
if (rmt_strip) {
if (rmt_strip->rmt_chan) {
rmt_del_channel(rmt_strip->rmt_chan);
}
if (rmt_strip->strip_encoder) {
rmt_del_encoder(rmt_strip->strip_encoder);
}
free(rmt_strip);
}
return ret;
}

View file

@ -0,0 +1,194 @@
/*
* SPDX-FileCopyrightText: 2022-2023 Espressif Systems (Shanghai) CO LTD
*
* SPDX-License-Identifier: Apache-2.0
*/
#include <stdlib.h>
#include <string.h>
#include <sys/cdefs.h>
#include "esp_log.h"
#include "esp_check.h"
#include "driver/rmt.h"
#include "led_strip.h"
#include "led_strip_interface.h"
static const char *TAG = "led_strip_rmt";
#define WS2812_T0H_NS (300)
#define WS2812_T0L_NS (900)
#define WS2812_T1H_NS (900)
#define WS2812_T1L_NS (300)
#define SK6812_T0H_NS (300)
#define SK6812_T0L_NS (900)
#define SK6812_T1H_NS (600)
#define SK6812_T1L_NS (600)
#define LED_STRIP_RESET_MS (10)
// the memory size of each RMT channel, in words (4 bytes)
#if CONFIG_IDF_TARGET_ESP32 || CONFIG_IDF_TARGET_ESP32S2
#define LED_STRIP_RMT_DEFAULT_MEM_BLOCK_SYMBOLS 64
#else
#define LED_STRIP_RMT_DEFAULT_MEM_BLOCK_SYMBOLS 48
#endif
static uint32_t led_t0h_ticks = 0;
static uint32_t led_t1h_ticks = 0;
static uint32_t led_t0l_ticks = 0;
static uint32_t led_t1l_ticks = 0;
typedef struct {
led_strip_t base;
rmt_channel_t rmt_channel;
uint32_t strip_len;
uint8_t bytes_per_pixel;
uint8_t buffer[0];
} led_strip_rmt_obj;
static void IRAM_ATTR ws2812_rmt_adapter(const void *src, rmt_item32_t *dest, size_t src_size,
size_t wanted_num, size_t *translated_size, size_t *item_num)
{
if (src == NULL || dest == NULL) {
*translated_size = 0;
*item_num = 0;
return;
}
const rmt_item32_t bit0 = {{{ led_t0h_ticks, 1, led_t0l_ticks, 0 }}}; //Logical 0
const rmt_item32_t bit1 = {{{ led_t1h_ticks, 1, led_t1l_ticks, 0 }}}; //Logical 1
size_t size = 0;
size_t num = 0;
uint8_t *psrc = (uint8_t *)src;
rmt_item32_t *pdest = dest;
while (size < src_size && num < wanted_num) {
for (int i = 0; i < 8; i++) {
// MSB first
if (*psrc & (1 << (7 - i))) {
pdest->val = bit1.val;
} else {
pdest->val = bit0.val;
}
num++;
pdest++;
}
size++;
psrc++;
}
*translated_size = size;
*item_num = num;
}
static esp_err_t led_strip_rmt_set_pixel(led_strip_t *strip, uint32_t index, uint32_t red, uint32_t green, uint32_t blue)
{
led_strip_rmt_obj *rmt_strip = __containerof(strip, led_strip_rmt_obj, base);
ESP_RETURN_ON_FALSE(index < rmt_strip->strip_len, ESP_ERR_INVALID_ARG, TAG, "index out of the maximum number of leds");
uint32_t start = index * rmt_strip->bytes_per_pixel;
// In thr order of GRB
rmt_strip->buffer[start + 0] = green & 0xFF;
rmt_strip->buffer[start + 1] = red & 0xFF;
rmt_strip->buffer[start + 2] = blue & 0xFF;
if (rmt_strip->bytes_per_pixel > 3) {
rmt_strip->buffer[start + 3] = 0;
}
return ESP_OK;
}
static esp_err_t led_strip_rmt_refresh(led_strip_t *strip)
{
led_strip_rmt_obj *rmt_strip = __containerof(strip, led_strip_rmt_obj, base);
ESP_RETURN_ON_ERROR(rmt_write_sample(rmt_strip->rmt_channel, rmt_strip->buffer, rmt_strip->strip_len * rmt_strip->bytes_per_pixel, true), TAG,
"transmit RMT samples failed");
vTaskDelay(pdMS_TO_TICKS(LED_STRIP_RESET_MS));
return ESP_OK;
}
static esp_err_t led_strip_rmt_clear(led_strip_t *strip)
{
led_strip_rmt_obj *rmt_strip = __containerof(strip, led_strip_rmt_obj, base);
// Write zero to turn off all LEDs
memset(rmt_strip->buffer, 0, rmt_strip->strip_len * rmt_strip->bytes_per_pixel);
return led_strip_rmt_refresh(strip);
}
static esp_err_t led_strip_rmt_del(led_strip_t *strip)
{
led_strip_rmt_obj *rmt_strip = __containerof(strip, led_strip_rmt_obj, base);
ESP_RETURN_ON_ERROR(rmt_driver_uninstall(rmt_strip->rmt_channel), TAG, "uninstall RMT driver failed");
free(rmt_strip);
return ESP_OK;
}
esp_err_t led_strip_new_rmt_device(const led_strip_config_t *led_config, const led_strip_rmt_config_t *dev_config, led_strip_handle_t *ret_strip)
{
led_strip_rmt_obj *rmt_strip = NULL;
esp_err_t ret = ESP_OK;
ESP_RETURN_ON_FALSE(led_config && dev_config && ret_strip, ESP_ERR_INVALID_ARG, TAG, "invalid argument");
ESP_RETURN_ON_FALSE(led_config->led_pixel_format < LED_PIXEL_FORMAT_INVALID, ESP_ERR_INVALID_ARG, TAG, "invalid led_pixel_format");
ESP_RETURN_ON_FALSE(dev_config->flags.with_dma == 0, ESP_ERR_NOT_SUPPORTED, TAG, "DMA is not supported");
uint8_t bytes_per_pixel = 3;
if (led_config->led_pixel_format == LED_PIXEL_FORMAT_GRBW) {
bytes_per_pixel = 4;
} else if (led_config->led_pixel_format == LED_PIXEL_FORMAT_GRB) {
bytes_per_pixel = 3;
} else {
assert(false);
}
// allocate memory for led_strip object
rmt_strip = calloc(1, sizeof(led_strip_rmt_obj) + led_config->max_leds * bytes_per_pixel);
ESP_RETURN_ON_FALSE(rmt_strip, ESP_ERR_NO_MEM, TAG, "request memory for les_strip failed");
// install RMT channel driver
rmt_config_t config = RMT_DEFAULT_CONFIG_TX(led_config->strip_gpio_num, dev_config->rmt_channel);
// set the minimal clock division because the LED strip needs a high clock resolution
config.clk_div = 2;
uint8_t mem_block_num = 2;
// override the default value if the user specify the mem block size
if (dev_config->mem_block_symbols) {
mem_block_num = (dev_config->mem_block_symbols + LED_STRIP_RMT_DEFAULT_MEM_BLOCK_SYMBOLS / 2) / LED_STRIP_RMT_DEFAULT_MEM_BLOCK_SYMBOLS;
}
config.mem_block_num = mem_block_num;
ESP_GOTO_ON_ERROR(rmt_config(&config), err, TAG, "RMT config failed");
ESP_GOTO_ON_ERROR(rmt_driver_install(config.channel, 0, 0), err, TAG, "RMT install failed");
uint32_t counter_clk_hz = 0;
rmt_get_counter_clock((rmt_channel_t)dev_config->rmt_channel, &counter_clk_hz);
// ns -> ticks
float ratio = (float)counter_clk_hz / 1e9;
if (led_config->led_model == LED_MODEL_WS2812) {
led_t0h_ticks = (uint32_t)(ratio * WS2812_T0H_NS);
led_t0l_ticks = (uint32_t)(ratio * WS2812_T0L_NS);
led_t1h_ticks = (uint32_t)(ratio * WS2812_T1H_NS);
led_t1l_ticks = (uint32_t)(ratio * WS2812_T1L_NS);
} else if (led_config->led_model == LED_MODEL_SK6812) {
led_t0h_ticks = (uint32_t)(ratio * SK6812_T0H_NS);
led_t0l_ticks = (uint32_t)(ratio * SK6812_T0L_NS);
led_t1h_ticks = (uint32_t)(ratio * SK6812_T1H_NS);
led_t1l_ticks = (uint32_t)(ratio * SK6812_T1L_NS);
} else {
assert(false);
}
// adapter to translates the LES strip date frame into RMT symbols
rmt_translator_init((rmt_channel_t)dev_config->rmt_channel, ws2812_rmt_adapter);
rmt_strip->bytes_per_pixel = bytes_per_pixel;
rmt_strip->rmt_channel = (rmt_channel_t)dev_config->rmt_channel;
rmt_strip->strip_len = led_config->max_leds;
rmt_strip->base.set_pixel = led_strip_rmt_set_pixel;
rmt_strip->base.refresh = led_strip_rmt_refresh;
rmt_strip->base.clear = led_strip_rmt_clear;
rmt_strip->base.del = led_strip_rmt_del;
*ret_strip = &rmt_strip->base;
return ESP_OK;
err:
if (rmt_strip) {
free(rmt_strip);
}
return ret;
}

View file

@ -0,0 +1,146 @@
/*
* SPDX-FileCopyrightText: 2022 Espressif Systems (Shanghai) CO LTD
*
* SPDX-License-Identifier: Apache-2.0
*/
#include "esp_check.h"
#include "led_strip_rmt_encoder.h"
static const char *TAG = "led_rmt_encoder";
typedef struct {
rmt_encoder_t base;
rmt_encoder_t *bytes_encoder;
rmt_encoder_t *copy_encoder;
int state;
rmt_symbol_word_t reset_code;
} rmt_led_strip_encoder_t;
static size_t rmt_encode_led_strip(rmt_encoder_t *encoder, rmt_channel_handle_t channel, const void *primary_data, size_t data_size, rmt_encode_state_t *ret_state)
{
rmt_led_strip_encoder_t *led_encoder = __containerof(encoder, rmt_led_strip_encoder_t, base);
rmt_encoder_handle_t bytes_encoder = led_encoder->bytes_encoder;
rmt_encoder_handle_t copy_encoder = led_encoder->copy_encoder;
rmt_encode_state_t session_state = 0;
rmt_encode_state_t state = 0;
size_t encoded_symbols = 0;
switch (led_encoder->state) {
case 0: // send RGB data
encoded_symbols += bytes_encoder->encode(bytes_encoder, channel, primary_data, data_size, &session_state);
if (session_state & RMT_ENCODING_COMPLETE) {
led_encoder->state = 1; // switch to next state when current encoding session finished
}
if (session_state & RMT_ENCODING_MEM_FULL) {
state |= RMT_ENCODING_MEM_FULL;
goto out; // yield if there's no free space for encoding artifacts
}
// fall-through
case 1: // send reset code
encoded_symbols += copy_encoder->encode(copy_encoder, channel, &led_encoder->reset_code,
sizeof(led_encoder->reset_code), &session_state);
if (session_state & RMT_ENCODING_COMPLETE) {
led_encoder->state = 0; // back to the initial encoding session
state |= RMT_ENCODING_COMPLETE;
}
if (session_state & RMT_ENCODING_MEM_FULL) {
state |= RMT_ENCODING_MEM_FULL;
goto out; // yield if there's no free space for encoding artifacts
}
}
out:
*ret_state = state;
return encoded_symbols;
}
static esp_err_t rmt_del_led_strip_encoder(rmt_encoder_t *encoder)
{
rmt_led_strip_encoder_t *led_encoder = __containerof(encoder, rmt_led_strip_encoder_t, base);
rmt_del_encoder(led_encoder->bytes_encoder);
rmt_del_encoder(led_encoder->copy_encoder);
free(led_encoder);
return ESP_OK;
}
static esp_err_t rmt_led_strip_encoder_reset(rmt_encoder_t *encoder)
{
rmt_led_strip_encoder_t *led_encoder = __containerof(encoder, rmt_led_strip_encoder_t, base);
rmt_encoder_reset(led_encoder->bytes_encoder);
rmt_encoder_reset(led_encoder->copy_encoder);
led_encoder->state = 0;
return ESP_OK;
}
esp_err_t rmt_new_led_strip_encoder(const led_strip_encoder_config_t *config, rmt_encoder_handle_t *ret_encoder)
{
esp_err_t ret = ESP_OK;
rmt_led_strip_encoder_t *led_encoder = NULL;
ESP_GOTO_ON_FALSE(config && ret_encoder, ESP_ERR_INVALID_ARG, err, TAG, "invalid argument");
ESP_GOTO_ON_FALSE(config->led_model < LED_MODEL_INVALID, ESP_ERR_INVALID_ARG, err, TAG, "invalid led model");
led_encoder = calloc(1, sizeof(rmt_led_strip_encoder_t));
ESP_GOTO_ON_FALSE(led_encoder, ESP_ERR_NO_MEM, err, TAG, "no mem for led strip encoder");
led_encoder->base.encode = rmt_encode_led_strip;
led_encoder->base.del = rmt_del_led_strip_encoder;
led_encoder->base.reset = rmt_led_strip_encoder_reset;
rmt_bytes_encoder_config_t bytes_encoder_config;
if (config->led_model == LED_MODEL_SK6812) {
bytes_encoder_config = (rmt_bytes_encoder_config_t) {
.bit0 = {
.level0 = 1,
.duration0 = 0.3 * config->resolution / 1000000, // T0H=0.3us
.level1 = 0,
.duration1 = 0.9 * config->resolution / 1000000, // T0L=0.9us
},
.bit1 = {
.level0 = 1,
.duration0 = 0.6 * config->resolution / 1000000, // T1H=0.6us
.level1 = 0,
.duration1 = 0.6 * config->resolution / 1000000, // T1L=0.6us
},
.flags.msb_first = 1 // SK6812 transfer bit order: G7...G0R7...R0B7...B0(W7...W0)
};
} else if (config->led_model == LED_MODEL_WS2812) {
// different led strip might have its own timing requirements, following parameter is for WS2812
bytes_encoder_config = (rmt_bytes_encoder_config_t) {
.bit0 = {
.level0 = 1,
.duration0 = 0.3 * config->resolution / 1000000, // T0H=0.3us
.level1 = 0,
.duration1 = 0.9 * config->resolution / 1000000, // T0L=0.9us
},
.bit1 = {
.level0 = 1,
.duration0 = 0.9 * config->resolution / 1000000, // T1H=0.9us
.level1 = 0,
.duration1 = 0.3 * config->resolution / 1000000, // T1L=0.3us
},
.flags.msb_first = 1 // WS2812 transfer bit order: G7...G0R7...R0B7...B0
};
} else {
assert(false);
}
ESP_GOTO_ON_ERROR(rmt_new_bytes_encoder(&bytes_encoder_config, &led_encoder->bytes_encoder), err, TAG, "create bytes encoder failed");
rmt_copy_encoder_config_t copy_encoder_config = {};
ESP_GOTO_ON_ERROR(rmt_new_copy_encoder(&copy_encoder_config, &led_encoder->copy_encoder), err, TAG, "create copy encoder failed");
uint32_t reset_ticks = config->resolution / 1000000 * 280 / 2; // reset code duration defaults to 280us to accomodate WS2812B-V5
led_encoder->reset_code = (rmt_symbol_word_t) {
.level0 = 0,
.duration0 = reset_ticks,
.level1 = 0,
.duration1 = reset_ticks,
};
*ret_encoder = &led_encoder->base;
return ESP_OK;
err:
if (led_encoder) {
if (led_encoder->bytes_encoder) {
rmt_del_encoder(led_encoder->bytes_encoder);
}
if (led_encoder->copy_encoder) {
rmt_del_encoder(led_encoder->copy_encoder);
}
free(led_encoder);
}
return ret;
}

View file

@ -0,0 +1,38 @@
/*
* SPDX-FileCopyrightText: 2022 Espressif Systems (Shanghai) CO LTD
*
* SPDX-License-Identifier: Apache-2.0
*/
#pragma once
#include <stdint.h>
#include "driver/rmt_encoder.h"
#include "led_strip_types.h"
#ifdef __cplusplus
extern "C" {
#endif
/**
* @brief Type of led strip encoder configuration
*/
typedef struct {
uint32_t resolution; /*!< Encoder resolution, in Hz */
led_model_t led_model; /*!< LED model */
} led_strip_encoder_config_t;
/**
* @brief Create RMT encoder for encoding LED strip pixels into RMT symbols
*
* @param[in] config Encoder configuration
* @param[out] ret_encoder Returned encoder handle
* @return
* - ESP_ERR_INVALID_ARG for any invalid arguments
* - ESP_ERR_NO_MEM out of memory when creating led strip encoder
* - ESP_OK if creating encoder successfully
*/
esp_err_t rmt_new_led_strip_encoder(const led_strip_encoder_config_t *config, rmt_encoder_handle_t *ret_encoder);
#ifdef __cplusplus
}
#endif

View file

@ -0,0 +1,209 @@
/*
* SPDX-FileCopyrightText: 2022-2023 Espressif Systems (Shanghai) CO LTD
*
* SPDX-License-Identifier: Apache-2.0
*/
#include <stdlib.h>
#include <string.h>
#include <sys/cdefs.h>
#include "esp_log.h"
#include "esp_check.h"
#include "esp_rom_gpio.h"
#include "soc/spi_periph.h"
#include "led_strip.h"
#include "led_strip_interface.h"
#include "hal/spi_hal.h"
#define LED_STRIP_SPI_DEFAULT_RESOLUTION (2.5 * 1000 * 1000) // 2.5MHz resolution
#define LED_STRIP_SPI_DEFAULT_TRANS_QUEUE_SIZE 4
#define SPI_BYTES_PER_COLOR_BYTE 3
#define SPI_BITS_PER_COLOR_BYTE (SPI_BYTES_PER_COLOR_BYTE * 8)
static const char *TAG = "led_strip_spi";
typedef struct {
led_strip_t base;
spi_host_device_t spi_host;
spi_device_handle_t spi_device;
uint32_t strip_len;
uint8_t bytes_per_pixel;
uint8_t pixel_buf[];
} led_strip_spi_obj;
// please make sure to zero-initialize the buf before calling this function
static void __led_strip_spi_bit(uint8_t data, uint8_t *buf)
{
// Each color of 1 bit is represented by 3 bits of SPI, low_level:100 ,high_level:110
// So a color byte occupies 3 bytes of SPI.
*(buf + 2) |= data & BIT(0) ? BIT(2) | BIT(1) : BIT(2);
*(buf + 2) |= data & BIT(1) ? BIT(5) | BIT(4) : BIT(5);
*(buf + 2) |= data & BIT(2) ? BIT(7) : 0x00;
*(buf + 1) |= BIT(0);
*(buf + 1) |= data & BIT(3) ? BIT(3) | BIT(2) : BIT(3);
*(buf + 1) |= data & BIT(4) ? BIT(6) | BIT(5) : BIT(6);
*(buf + 0) |= data & BIT(5) ? BIT(1) | BIT(0) : BIT(1);
*(buf + 0) |= data & BIT(6) ? BIT(4) | BIT(3) : BIT(4);
*(buf + 0) |= data & BIT(7) ? BIT(7) | BIT(6) : BIT(7);
}
static esp_err_t led_strip_spi_set_pixel(led_strip_t *strip, uint32_t index, uint32_t red, uint32_t green, uint32_t blue)
{
led_strip_spi_obj *spi_strip = __containerof(strip, led_strip_spi_obj, base);
ESP_RETURN_ON_FALSE(index < spi_strip->strip_len, ESP_ERR_INVALID_ARG, TAG, "index out of maximum number of LEDs");
// LED_PIXEL_FORMAT_GRB takes 72bits(9bytes)
uint32_t start = index * spi_strip->bytes_per_pixel * SPI_BYTES_PER_COLOR_BYTE;
memset(spi_strip->pixel_buf + start, 0, spi_strip->bytes_per_pixel * SPI_BYTES_PER_COLOR_BYTE);
__led_strip_spi_bit(green, &spi_strip->pixel_buf[start]);
__led_strip_spi_bit(red, &spi_strip->pixel_buf[start + SPI_BYTES_PER_COLOR_BYTE]);
__led_strip_spi_bit(blue, &spi_strip->pixel_buf[start + SPI_BYTES_PER_COLOR_BYTE * 2]);
if (spi_strip->bytes_per_pixel > 3) {
__led_strip_spi_bit(0, &spi_strip->pixel_buf[start + SPI_BYTES_PER_COLOR_BYTE * 3]);
}
return ESP_OK;
}
static esp_err_t led_strip_spi_set_pixel_rgbw(led_strip_t *strip, uint32_t index, uint32_t red, uint32_t green, uint32_t blue, uint32_t white)
{
led_strip_spi_obj *spi_strip = __containerof(strip, led_strip_spi_obj, base);
ESP_RETURN_ON_FALSE(index < spi_strip->strip_len, ESP_ERR_INVALID_ARG, TAG, "index out of maximum number of LEDs");
ESP_RETURN_ON_FALSE(spi_strip->bytes_per_pixel == 4, ESP_ERR_INVALID_ARG, TAG, "wrong LED pixel format, expected 4 bytes per pixel");
// LED_PIXEL_FORMAT_GRBW takes 96bits(12bytes)
uint32_t start = index * spi_strip->bytes_per_pixel * SPI_BYTES_PER_COLOR_BYTE;
// SK6812 component order is GRBW
memset(spi_strip->pixel_buf + start, 0, spi_strip->bytes_per_pixel * SPI_BYTES_PER_COLOR_BYTE);
__led_strip_spi_bit(green, &spi_strip->pixel_buf[start]);
__led_strip_spi_bit(red, &spi_strip->pixel_buf[start + SPI_BYTES_PER_COLOR_BYTE]);
__led_strip_spi_bit(blue, &spi_strip->pixel_buf[start + SPI_BYTES_PER_COLOR_BYTE * 2]);
__led_strip_spi_bit(white, &spi_strip->pixel_buf[start + SPI_BYTES_PER_COLOR_BYTE * 3]);
return ESP_OK;
}
static esp_err_t led_strip_spi_refresh(led_strip_t *strip)
{
led_strip_spi_obj *spi_strip = __containerof(strip, led_strip_spi_obj, base);
spi_transaction_t tx_conf;
memset(&tx_conf, 0, sizeof(tx_conf));
tx_conf.length = spi_strip->strip_len * spi_strip->bytes_per_pixel * SPI_BITS_PER_COLOR_BYTE;
tx_conf.tx_buffer = spi_strip->pixel_buf;
tx_conf.rx_buffer = NULL;
ESP_RETURN_ON_ERROR(spi_device_transmit(spi_strip->spi_device, &tx_conf), TAG, "transmit pixels by SPI failed");
return ESP_OK;
}
static esp_err_t led_strip_spi_clear(led_strip_t *strip)
{
led_strip_spi_obj *spi_strip = __containerof(strip, led_strip_spi_obj, base);
//Write zero to turn off all leds
memset(spi_strip->pixel_buf, 0, spi_strip->strip_len * spi_strip->bytes_per_pixel * SPI_BYTES_PER_COLOR_BYTE);
uint8_t *buf = spi_strip->pixel_buf;
for (int index = 0; index < spi_strip->strip_len * spi_strip->bytes_per_pixel; index++) {
__led_strip_spi_bit(0, buf);
buf += SPI_BYTES_PER_COLOR_BYTE;
}
return led_strip_spi_refresh(strip);
}
static esp_err_t led_strip_spi_del(led_strip_t *strip)
{
led_strip_spi_obj *spi_strip = __containerof(strip, led_strip_spi_obj, base);
ESP_RETURN_ON_ERROR(spi_bus_remove_device(spi_strip->spi_device), TAG, "delete spi device failed");
ESP_RETURN_ON_ERROR(spi_bus_free(spi_strip->spi_host), TAG, "free spi bus failed");
free(spi_strip);
return ESP_OK;
}
esp_err_t led_strip_new_spi_device(const led_strip_config_t *led_config, const led_strip_spi_config_t *spi_config, led_strip_handle_t *ret_strip)
{
led_strip_spi_obj *spi_strip = NULL;
esp_err_t ret = ESP_OK;
ESP_GOTO_ON_FALSE(led_config && spi_config && ret_strip, ESP_ERR_INVALID_ARG, err, TAG, "invalid argument");
ESP_GOTO_ON_FALSE(led_config->led_pixel_format < LED_PIXEL_FORMAT_INVALID, ESP_ERR_INVALID_ARG, err, TAG, "invalid led_pixel_format");
uint8_t bytes_per_pixel = 3;
if (led_config->led_pixel_format == LED_PIXEL_FORMAT_GRBW) {
bytes_per_pixel = 4;
} else if (led_config->led_pixel_format == LED_PIXEL_FORMAT_GRB) {
bytes_per_pixel = 3;
} else {
assert(false);
}
uint32_t mem_caps = MALLOC_CAP_DEFAULT;
if (spi_config->flags.with_dma) {
// DMA buffer must be placed in internal SRAM
mem_caps |= MALLOC_CAP_INTERNAL | MALLOC_CAP_DMA;
}
spi_strip = heap_caps_calloc(1, sizeof(led_strip_spi_obj) + led_config->max_leds * bytes_per_pixel * SPI_BYTES_PER_COLOR_BYTE, mem_caps);
ESP_GOTO_ON_FALSE(spi_strip, ESP_ERR_NO_MEM, err, TAG, "no mem for spi strip");
spi_strip->spi_host = spi_config->spi_bus;
// for backward compatibility, if the user does not set the clk_src, use the default value
spi_clock_source_t clk_src = SPI_CLK_SRC_DEFAULT;
if (spi_config->clk_src) {
clk_src = spi_config->clk_src;
}
spi_bus_config_t spi_bus_cfg = {
.mosi_io_num = led_config->strip_gpio_num,
//Only use MOSI to generate the signal, set -1 when other pins are not used.
.miso_io_num = -1,
.sclk_io_num = -1,
.quadwp_io_num = -1,
.quadhd_io_num = -1,
.max_transfer_sz = led_config->max_leds * bytes_per_pixel * SPI_BYTES_PER_COLOR_BYTE,
};
ESP_GOTO_ON_ERROR(spi_bus_initialize(spi_strip->spi_host, &spi_bus_cfg, spi_config->flags.with_dma ? SPI_DMA_CH_AUTO : SPI_DMA_DISABLED), err, TAG, "create SPI bus failed");
if (led_config->flags.invert_out == true) {
esp_rom_gpio_connect_out_signal(led_config->strip_gpio_num, spi_periph_signal[spi_strip->spi_host].spid_out, true, false);
}
spi_device_interface_config_t spi_dev_cfg = {
.clock_source = clk_src,
.command_bits = 0,
.address_bits = 0,
.dummy_bits = 0,
.clock_speed_hz = LED_STRIP_SPI_DEFAULT_RESOLUTION,
.mode = 0,
//set -1 when CS is not used
.spics_io_num = -1,
.queue_size = LED_STRIP_SPI_DEFAULT_TRANS_QUEUE_SIZE,
};
ESP_GOTO_ON_ERROR(spi_bus_add_device(spi_strip->spi_host, &spi_dev_cfg, &spi_strip->spi_device), err, TAG, "Failed to add spi device");
int clock_resolution_khz = 0;
spi_device_get_actual_freq(spi_strip->spi_device, &clock_resolution_khz);
// TODO: ideally we should decide the SPI_BYTES_PER_COLOR_BYTE by the real clock resolution
// But now, let's fixed the resolution, the downside is, we don't support a clock source whose frequency is not multiple of LED_STRIP_SPI_DEFAULT_RESOLUTION
ESP_GOTO_ON_FALSE(clock_resolution_khz == LED_STRIP_SPI_DEFAULT_RESOLUTION / 1000, ESP_ERR_NOT_SUPPORTED, err,
TAG, "unsupported clock resolution:%dKHz", clock_resolution_khz);
spi_strip->bytes_per_pixel = bytes_per_pixel;
spi_strip->strip_len = led_config->max_leds;
spi_strip->base.set_pixel = led_strip_spi_set_pixel;
spi_strip->base.set_pixel_rgbw = led_strip_spi_set_pixel_rgbw;
spi_strip->base.refresh = led_strip_spi_refresh;
spi_strip->base.clear = led_strip_spi_clear;
spi_strip->base.del = led_strip_spi_del;
*ret_strip = &spi_strip->base;
return ESP_OK;
err:
if (spi_strip) {
if (spi_strip->spi_device) {
spi_bus_remove_device(spi_strip->spi_device);
}
if (spi_strip->spi_host) {
spi_bus_free(spi_strip->spi_host);
}
free(spi_strip);
}
return ret;
}

View file

@ -0,0 +1,97 @@
/*
* SPDX-FileCopyrightText: 2022 Espressif Systems (Shanghai) CO LTD
*
* SPDX-License-Identifier: Apache-2.0
*/
#include "esp_log.h"
#include "esp_check.h"
#include "led_strip_types.h"
#include "muxed_led_strip_types.h"
#include "muxed_led_strip.h"
#include "muxed_led_strip_interface.h"
static const char *TAG = "muxed_led_strip";
esp_err_t muxed_led_strip_set_pixel(muxed_led_strip_handle_t strip, uint32_t channel, uint32_t index, uint32_t red, uint32_t green, uint32_t blue)
{
ESP_RETURN_ON_FALSE(strip, ESP_ERR_INVALID_ARG, TAG, "invalid argument");
return strip->set_pixel(strip, channel, index, red, green, blue);
}
esp_err_t muxed_led_strip_set_pixel_hsv(muxed_led_strip_handle_t strip, uint32_t channel, uint32_t index, uint16_t hue, uint8_t saturation, uint8_t value)
{
ESP_RETURN_ON_FALSE(strip, ESP_ERR_INVALID_ARG, TAG, "invalid argument");
uint32_t red = 0;
uint32_t green = 0;
uint32_t blue = 0;
uint32_t rgb_max = value;
uint32_t rgb_min = rgb_max * (255 - saturation) / 255.0f;
uint32_t i = hue / 60;
uint32_t diff = hue % 60;
// RGB adjustment amount by hue
uint32_t rgb_adj = (rgb_max - rgb_min) * diff / 60;
switch (i) {
case 0:
red = rgb_max;
green = rgb_min + rgb_adj;
blue = rgb_min;
break;
case 1:
red = rgb_max - rgb_adj;
green = rgb_max;
blue = rgb_min;
break;
case 2:
red = rgb_min;
green = rgb_max;
blue = rgb_min + rgb_adj;
break;
case 3:
red = rgb_min;
green = rgb_max - rgb_adj;
blue = rgb_max;
break;
case 4:
red = rgb_min + rgb_adj;
green = rgb_min;
blue = rgb_max;
break;
default:
red = rgb_max;
green = rgb_min;
blue = rgb_max - rgb_adj;
break;
}
return strip->set_pixel(strip, channel, index, red, green, blue);
}
esp_err_t muxed_led_strip_set_pixel_rgbw(muxed_led_strip_handle_t strip, uint32_t channel, uint32_t index, uint32_t red, uint32_t green, uint32_t blue, uint32_t white)
{
ESP_RETURN_ON_FALSE(strip, ESP_ERR_INVALID_ARG, TAG, "invalid argument");
return strip->set_pixel_rgbw(strip, channel, index, red, green, blue, white);
}
esp_err_t muxed_led_strip_refresh(muxed_led_strip_handle_t strip, uint32_t channel)
{
ESP_RETURN_ON_FALSE(strip, ESP_ERR_INVALID_ARG, TAG, "invalid argument");
return strip->refresh(strip, channel);
}
esp_err_t muxed_led_strip_clear(muxed_led_strip_handle_t strip, uint32_t channel)
{
ESP_RETURN_ON_FALSE(strip, ESP_ERR_INVALID_ARG, TAG, "invalid argument");
return strip->clear(strip, channel);
}
esp_err_t muxed_led_strip_del(muxed_led_strip_handle_t strip)
{
ESP_RETURN_ON_FALSE(strip, ESP_ERR_INVALID_ARG, TAG, "invalid argument");
return strip->del(strip);
}

View file

@ -0,0 +1,186 @@
/*
* SPDX-FileCopyrightText: 2022-2023 Espressif Systems (Shanghai) CO LTD
*
* SPDX-License-Identifier: Apache-2.0
*/
#include <stdlib.h>
#include <string.h>
#include <sys/cdefs.h>
#include "esp_log.h"
#include "esp_check.h"
#include "driver/rmt_tx.h"
#include "muxed_led_strip.h"
#include "muxed_led_strip_interface.h"
#include "led_strip_rmt_encoder.h"
#define LED_STRIP_RMT_DEFAULT_RESOLUTION 10000000 // 10MHz resolution
#define LED_STRIP_RMT_DEFAULT_TRANS_QUEUE_SIZE 4
// the memory size of each RMT channel, in words (4 bytes)
#if CONFIG_IDF_TARGET_ESP32 || CONFIG_IDF_TARGET_ESP32S2
#define LED_STRIP_RMT_DEFAULT_MEM_BLOCK_SYMBOLS 64
#else
#define LED_STRIP_RMT_DEFAULT_MEM_BLOCK_SYMBOLS 48
#endif
static const char *TAG = "led_strip_rmt";
typedef struct
{
muxed_led_strip_t base;
rmt_channel_handle_t rmt_chan;
rmt_encoder_handle_t strip_encoder;
uint32_t strip_len;
uint8_t bytes_per_pixel;
uint8_t n_channels;
uint8_t pixel_buf[];
} muxed_led_strip_rmt_obj;
static esp_err_t muxed_led_strip_rmt_set_pixel(muxed_led_strip_t *strip, uint32_t channel, uint32_t index, uint32_t red, uint32_t green, uint32_t blue)
{
muxed_led_strip_rmt_obj *rmt_strip = __containerof(strip, muxed_led_strip_rmt_obj, base);
ESP_RETURN_ON_FALSE(channel < rmt_strip->n_channels, ESP_ERR_INVALID_ARG, TAG, "channel greater than total channels");
ESP_RETURN_ON_FALSE(index < rmt_strip->strip_len, ESP_ERR_INVALID_ARG, TAG, "index out of maximum number of LEDs");
uint32_t start = (channel * rmt_strip->strip_len * rmt_strip->bytes_per_pixel) + (index * rmt_strip->bytes_per_pixel);
// In thr order of GRB, as LED strip like WS2812 sends out pixels in this order
rmt_strip->pixel_buf[start + 0] = green & 0xFF;
rmt_strip->pixel_buf[start + 1] = red & 0xFF;
rmt_strip->pixel_buf[start + 2] = blue & 0xFF;
if (rmt_strip->bytes_per_pixel > 3)
{
rmt_strip->pixel_buf[start + 3] = 0;
}
return ESP_OK;
}
static esp_err_t muxed_led_strip_rmt_set_pixel_rgbw(muxed_led_strip_t *strip, uint32_t channel, uint32_t index, uint32_t red, uint32_t green, uint32_t blue, uint32_t white)
{
muxed_led_strip_rmt_obj *rmt_strip = __containerof(strip, muxed_led_strip_rmt_obj, base);
ESP_RETURN_ON_FALSE(channel < rmt_strip->n_channels, ESP_ERR_INVALID_ARG, TAG, "channel greater than total channels");
ESP_RETURN_ON_FALSE(index < rmt_strip->strip_len, ESP_ERR_INVALID_ARG, TAG, "index out of maximum number of LEDs");
ESP_RETURN_ON_FALSE(rmt_strip->bytes_per_pixel == 4, ESP_ERR_INVALID_ARG, TAG, "wrong LED pixel format, expected 4 bytes per pixel");
uint8_t *buf_start = rmt_strip->pixel_buf + (channel * rmt_strip->strip_len * rmt_strip->bytes_per_pixel) + (index * 4);
// SK6812 component order is GRBW
*buf_start = green & 0xFF;
*++buf_start = red & 0xFF;
*++buf_start = blue & 0xFF;
*++buf_start = white & 0xFF;
return ESP_OK;
}
static esp_err_t muxed_led_strip_rmt_refresh(muxed_led_strip_t *strip, uint32_t channel)
{
muxed_led_strip_rmt_obj *rmt_strip = __containerof(strip, muxed_led_strip_rmt_obj, base);
rmt_transmit_config_t tx_conf = {
.loop_count = 0,
};
ESP_RETURN_ON_FALSE(channel < rmt_strip->n_channels, ESP_ERR_INVALID_ARG, TAG, "channel greater than total channels");
ESP_RETURN_ON_ERROR(rmt_enable(rmt_strip->rmt_chan), TAG, "enable RMT channel failed");
ESP_RETURN_ON_ERROR(rmt_transmit(rmt_strip->rmt_chan, rmt_strip->strip_encoder, rmt_strip->pixel_buf + (channel * rmt_strip->strip_len * rmt_strip->bytes_per_pixel),
rmt_strip->strip_len * rmt_strip->bytes_per_pixel, &tx_conf),
TAG, "transmit pixels by RMT failed");
ESP_RETURN_ON_ERROR(rmt_tx_wait_all_done(rmt_strip->rmt_chan, -1), TAG, "flush RMT channel failed");
ESP_RETURN_ON_ERROR(rmt_disable(rmt_strip->rmt_chan), TAG, "disable RMT channel failed");
return ESP_OK;
}
static esp_err_t muxed_led_strip_rmt_clear(muxed_led_strip_t *strip, uint32_t channel)
{
muxed_led_strip_rmt_obj *rmt_strip = __containerof(strip, muxed_led_strip_rmt_obj, base);
ESP_RETURN_ON_FALSE(channel < rmt_strip->n_channels, ESP_ERR_INVALID_ARG, TAG, "channel greater than total channels");
// Write zero to turn off all leds
memset(rmt_strip->pixel_buf + (channel * rmt_strip->strip_len * rmt_strip->bytes_per_pixel), 0, rmt_strip->strip_len * rmt_strip->bytes_per_pixel);
return muxed_led_strip_rmt_refresh(strip, channel);
}
static esp_err_t muxed_led_strip_rmt_del(muxed_led_strip_t *strip)
{
muxed_led_strip_rmt_obj *rmt_strip = __containerof(strip, muxed_led_strip_rmt_obj, base);
ESP_RETURN_ON_ERROR(rmt_del_channel(rmt_strip->rmt_chan), TAG, "delete RMT channel failed");
ESP_RETURN_ON_ERROR(rmt_del_encoder(rmt_strip->strip_encoder), TAG, "delete strip encoder failed");
free(rmt_strip);
return ESP_OK;
}
esp_err_t muxed_led_strip_new_rmt_device(const muxed_led_strip_config_t *led_config,
const led_strip_rmt_config_t *rmt_config,
const rmt_tx_event_callbacks_t *cbs,
muxed_led_strip_handle_t *ret_strip)
{
muxed_led_strip_rmt_obj *rmt_strip = NULL;
esp_err_t ret = ESP_OK;
ESP_GOTO_ON_FALSE(led_config && rmt_config && ret_strip, ESP_ERR_INVALID_ARG, err, TAG, "invalid argument");
ESP_GOTO_ON_FALSE(led_config->led_pixel_format < LED_PIXEL_FORMAT_INVALID, ESP_ERR_INVALID_ARG, err, TAG, "invalid led_pixel_format");
uint8_t bytes_per_pixel = 3;
if (led_config->led_pixel_format == LED_PIXEL_FORMAT_GRBW)
{
bytes_per_pixel = 4;
}
else if (led_config->led_pixel_format == LED_PIXEL_FORMAT_GRB)
{
bytes_per_pixel = 3;
}
else
{
assert(false);
}
uint8_t number_of_multiplexed_channels = led_config->channels;
rmt_strip = calloc(1, sizeof(muxed_led_strip_rmt_obj) + led_config->max_leds * bytes_per_pixel * number_of_multiplexed_channels);
ESP_GOTO_ON_FALSE(rmt_strip, ESP_ERR_NO_MEM, err, TAG, "no mem for rmt strip");
uint32_t resolution = rmt_config->resolution_hz ? rmt_config->resolution_hz : LED_STRIP_RMT_DEFAULT_RESOLUTION;
// for backward compatibility, if the user does not set the clk_src, use the default value
rmt_clock_source_t clk_src = RMT_CLK_SRC_DEFAULT;
if (rmt_config->clk_src)
{
clk_src = rmt_config->clk_src;
}
size_t mem_block_symbols = LED_STRIP_RMT_DEFAULT_MEM_BLOCK_SYMBOLS;
// override the default value if the user sets it
if (rmt_config->mem_block_symbols)
{
mem_block_symbols = rmt_config->mem_block_symbols;
}
rmt_tx_channel_config_t rmt_chan_config = {
.clk_src = clk_src,
.gpio_num = led_config->strip_gpio_num,
.mem_block_symbols = mem_block_symbols,
.resolution_hz = resolution,
.trans_queue_depth = LED_STRIP_RMT_DEFAULT_TRANS_QUEUE_SIZE,
.flags.with_dma = rmt_config->flags.with_dma,
.flags.invert_out = led_config->flags.invert_out,
};
ESP_GOTO_ON_ERROR(rmt_new_tx_channel(&rmt_chan_config, &rmt_strip->rmt_chan), err, TAG, "create RMT TX channel failed");
led_strip_encoder_config_t strip_encoder_conf = {
.resolution = resolution,
.led_model = led_config->led_model};
ESP_GOTO_ON_ERROR(rmt_new_led_strip_encoder(&strip_encoder_conf, &rmt_strip->strip_encoder), err, TAG, "create LED strip encoder failed");
rmt_strip->bytes_per_pixel = bytes_per_pixel;
rmt_strip->strip_len = led_config->max_leds;
rmt_strip->n_channels = number_of_multiplexed_channels;
rmt_strip->base.set_pixel = muxed_led_strip_rmt_set_pixel;
rmt_strip->base.set_pixel_rgbw = muxed_led_strip_rmt_set_pixel_rgbw;
rmt_strip->base.refresh = muxed_led_strip_rmt_refresh;
rmt_strip->base.clear = muxed_led_strip_rmt_clear;
rmt_strip->base.del = muxed_led_strip_rmt_del;
ESP_GOTO_ON_ERROR(rmt_tx_register_event_callbacks(rmt_strip->rmt_chan, cbs, (void *)0), err, TAG, "register RMT TX callbacks failed");
*ret_strip = &rmt_strip->base;
return ESP_OK;
err:
if (rmt_strip)
{
if (rmt_strip->rmt_chan)
{
rmt_del_channel(rmt_strip->rmt_chan);
}
if (rmt_strip->strip_encoder)
{
rmt_del_encoder(rmt_strip->strip_encoder);
}
free(rmt_strip);
}
return ret;
}

View file

@ -0,0 +1,17 @@
idf_component_register(
SRCS
"Key_Value.c"
"Settings.c"
"SPIFFS.c"
"USB.c"
INCLUDE_DIRS
"."
REQUIRES
"SystemK"
"spiffs"
"driver"
"usb"
"usb_host_msc"
"esp_timer"
)

228
components/NVM/Key_Value.c Normal file
View file

@ -0,0 +1,228 @@
/*
* This program source code file is part of the KTag project.
*
* 🛡 <https://ktag.clubk.club> 🃞
*
* Copyright © 2024-2025 Joseph P. Kearney and the KTag developers.
*
* This program is free software: you can redistribute it and/or modify it under
* the terms of the GNU Affero General Public License as published by the Free
* Software Foundation, either version 3 of the License, or (at your option) any
* later version.
*
* This program is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
* FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
* details.
*
* There should be a copy of the GNU Affero General Public License in the LICENSE
* file in the root of this repository. If not, see <http://www.gnu.org/licenses/>.
*/
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>
#include <SystemK.h>
#define MAX_LINE_LENGTH 140
#define MAX_KEY_LENGTH 64
#define MAX_VALUE_LENGTH 64
static const char *TAG = "NVM KV";
static const char *TEMP_FILE = "/usb/esp/temp.txt";
// Trims the whitespace from both ends of a string.
static void KV_Trim(char *str)
{
char *start = str;
char *end = str + strlen(str) - 1;
while (*start && (*start == ' ' || *start == '\t'))
start++;
while (end > start && (*end == ' ' || *end == '\t' || *end == '\n'))
end--;
*(end + 1) = '\0';
memmove(str, start, end - start + 2);
}
// Splits a line around the '=' into a key and value pair.
static int KV_Parse_Line(char *line, char *key, char *value)
{
char *delimiter = strchr(line, '=');
if (delimiter == NULL)
return 0;
*delimiter = '\0';
strncpy(key, line, MAX_KEY_LENGTH - 1);
strncpy(value, delimiter + 1, MAX_VALUE_LENGTH - 1);
key[MAX_KEY_LENGTH - 1] = '\0';
value[MAX_VALUE_LENGTH - 1] = '\0';
KV_Trim(key);
KV_Trim(value);
return 1;
}
SystemKResult_T KV_Get_Value_string(const char *filename, const char *search_key, char *value)
{
FILE *file = fopen(filename, "r");
if (file == NULL)
{
KLOG_ERROR(TAG, "Couldn't open file %s for reading!", filename);
return SYSTEMK_RESULT_FILE_NOT_FOUND;
}
char line[MAX_LINE_LENGTH];
char key[MAX_KEY_LENGTH];
while (fgets(line, sizeof(line), file))
{
if (KV_Parse_Line(line, key, value) && strcmp(key, search_key) == 0)
{
fclose(file);
return SYSTEMK_RESULT_SUCCESS;
}
}
fclose(file);
KLOG_ERROR(TAG, "Couldn't find key %s in file %s!", search_key, filename);
return SYSTEMK_RESULT_KEY_NOT_FOUND;
}
SystemKResult_T KV_Set_Value_string(const char *filename, const char *set_key, const char *set_value)
{
FILE *file = fopen(filename, "r");
if (file == NULL)
{
KLOG_ERROR(TAG, "Couldn't open file %s for reading!", filename);
return SYSTEMK_RESULT_FILE_NOT_FOUND;
}
FILE *temp = fopen(TEMP_FILE, "w");
if (temp == NULL)
{
fclose(file);
KLOG_ERROR(TAG, "Couldn't open file %s for writing!", TEMP_FILE);
return SYSTEMK_RESULT_WRITE_FAILED;
}
char line[MAX_LINE_LENGTH];
char line_copy[MAX_LINE_LENGTH];
char key[MAX_KEY_LENGTH];
char value[MAX_VALUE_LENGTH];
int found = 0;
while (fgets(line, sizeof(line), file))
{
strncpy(line_copy, line, MAX_LINE_LENGTH);
line_copy[MAX_LINE_LENGTH - 1] = '\0'; // Ensure null-termination
if (KV_Parse_Line(line, key, value) && strcmp(key, set_key) == 0)
{
fprintf(temp, "%s = %s\n", set_key, set_value);
found = 1;
}
else
{
fputs(line_copy, temp);
}
}
if (!found)
{
fprintf(temp, "%s = %s\n", set_key, set_value);
}
fclose(file);
fclose(temp);
remove(filename);
rename(TEMP_FILE, filename);
return SYSTEMK_RESULT_SUCCESS;
}
SystemKResult_T KV_Get_Value_uint32(const char *filename, const char *search_key, uint32_t *value)
{
char value_str[MAX_VALUE_LENGTH];
SystemKResult_T result = KV_Get_Value_string(filename, search_key, value_str);
if (result != SYSTEMK_RESULT_SUCCESS)
{
return result;
}
char *endptr;
// Reset errno to check for conversion errors.
errno = 0;
*value = strtoul(value_str, &endptr, 10);
// Check for conversion errors
if ((errno == ERANGE && *value == ULONG_MAX) || (errno != 0))
{
KLOG_ERROR(TAG, "Error converting %s for key %s to uint32_t!", value_str, search_key);
return SYSTEMK_RESULT_WRONG_DATATYPE;
}
if (endptr == value_str)
{
KLOG_ERROR(TAG, "No digits were found in %s (for key %s)!", value_str, search_key);
return SYSTEMK_RESULT_WRONG_DATATYPE;
}
if (*value > UINT32_MAX)
{
KLOG_ERROR(TAG, "Value %s (for key %s) exceeds uint32_t range!", value_str, search_key);
return SYSTEMK_RESULT_OVERFLOW;
}
return SYSTEMK_RESULT_SUCCESS;
}
SystemKResult_T KV_Set_Value_uint32(const char *filename, const char *set_key, uint32_t *set_value)
{
char value_str[MAX_VALUE_LENGTH];
int written = snprintf(value_str, MAX_VALUE_LENGTH, "%lu", *set_value);
if (written < 0 || written >= MAX_VALUE_LENGTH)
{
KLOG_ERROR(TAG, "Error converting uint32_t to string for key %s!", set_key);
return SYSTEMK_RESULT_WRONG_DATATYPE;
}
return KV_Set_Value_string(filename, set_key, value_str);
}
SystemKResult_T KV_Get_Value_uint8(const char *filename, const char *search_key, uint8_t *value)
{
uint32_t value32 = 0;
SystemKResult_T result = KV_Get_Value_uint32(filename, search_key, &value32);
if (result != SYSTEMK_RESULT_SUCCESS)
{
return result;
}
// Check for conversion errors
if (value32 > UINT8_MAX)
{
KLOG_ERROR(TAG, "Value %lu (for key %s) exceeds uint32_t range!", value32, search_key);
return SYSTEMK_RESULT_OVERFLOW;
}
*value = (uint8_t)value32;
return SYSTEMK_RESULT_SUCCESS;
}
SystemKResult_T KV_Set_Value_uint8(const char *filename, const char *set_key, uint8_t *set_value)
{
char value_str[MAX_VALUE_LENGTH];
int written = snprintf(value_str, MAX_VALUE_LENGTH, "%u", *set_value);
if (written < 0 || written >= MAX_VALUE_LENGTH)
{
KLOG_ERROR(TAG, "Error converting uint8_t to string for key %s!", set_key);
return SYSTEMK_RESULT_WRONG_DATATYPE;
}
return KV_Set_Value_string(filename, set_key, value_str);
}

View file

@ -0,0 +1,29 @@
/*
* This program source code file is part of the KTag project.
*
* 🛡 <https://ktag.clubk.club> 🃞
*
* Copyright © 2024-2025 Joseph P. Kearney and the KTag developers.
*
* This program is free software: you can redistribute it and/or modify it under
* the terms of the GNU Affero General Public License as published by the Free
* Software Foundation, either version 3 of the License, or (at your option) any
* later version.
*
* This program is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
* FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
* details.
*
* There should be a copy of the GNU Affero General Public License in the LICENSE
* file in the root of this repository. If not, see <http://www.gnu.org/licenses/>.
*/
SystemKResult_T KV_Get_Value_string(const char *filename, const char *search_key, char *value);
SystemKResult_T KV_Set_Value_string(const char *filename, const char *set_key, const char *set_value);
SystemKResult_T KV_Get_Value_uint32(const char *filename, const char *search_key, uint32_t *value);
SystemKResult_T KV_Set_Value_uint32(const char *filename, const char *set_key, uint32_t *set_value);
SystemKResult_T KV_Get_Value_uint8(const char *filename, const char *search_key, uint8_t *value);
SystemKResult_T KV_Set_Value_uint8(const char *filename, const char *set_key, uint8_t *set_value);

26
components/NVM/NVM.h Normal file
View file

@ -0,0 +1,26 @@
/*
* This program source code file is part of the KTag project.
*
* 🛡 <https://ktag.clubk.club> 🃞
*
* Copyright © 2024-2025 Joseph P. Kearney and the KTag developers.
*
* This program is free software: you can redistribute it and/or modify it under
* the terms of the GNU Affero General Public License as published by the Free
* Software Foundation, either version 3 of the License, or (at your option) any
* later version.
*
* This program is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
* FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
* details.
*
* There should be a copy of the GNU Affero General Public License in the LICENSE
* file in the root of this repository. If not, see <http://www.gnu.org/licenses/>.
*/
#pragma once
#include "Key_Value.h"
#include "SPIFFS.h"
#include "USB.h"

84
components/NVM/SPIFFS.c Normal file
View file

@ -0,0 +1,84 @@
/*
* This program source code file is part of the KTag project.
*
* 🛡 <https://ktag.clubk.club> 🃞
*
* Copyright © 2024-2025 Joseph P. Kearney and the KTag developers.
*
* This program is free software: you can redistribute it and/or modify it under
* the terms of the GNU Affero General Public License as published by the Free
* Software Foundation, either version 3 of the License, or (at your option) any
* later version.
*
* This program is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
* FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
* details.
*
* There should be a copy of the GNU Affero General Public License in the LICENSE
* file in the root of this repository. If not, see <http://www.gnu.org/licenses/>.
*/
#include <SystemK.h>
#include <string.h>
#include "esp_spiffs.h"
static const char *TAG = "SPIFFS";
const char *DEFAULT_CONFIG_FILE = "/spiffs/default_config.txt";
void Initialize_SPIFFS(SemaphoreHandle_t init_complete)
{
KLOG_INFO(TAG, "Initializing SPI flash file system (SPIFFS)...");
esp_vfs_spiffs_conf_t conf = {
.base_path = "/spiffs", // File path prefix associated with the filesystem.
.partition_label = NULL, // If set to NULL, first partition with subtype=spiffs will be used.
.max_files = 5, // Maximum files that could be open at the same time.
.format_if_mount_failed = false}; // If true, it will format the file system if it fails to mount.
esp_err_t ret = esp_vfs_spiffs_register(&conf);
if (ret != ESP_OK)
{
if (ret == ESP_FAIL)
{
KLOG_ERROR(TAG, "Failed to mount or format filesystem!");
}
else if (ret == ESP_ERR_NOT_FOUND)
{
KLOG_ERROR(TAG, "Failed to find SPIFFS partition!");
}
else
{
KLOG_ERROR(TAG, "Failed to initialize SPIFFS (%s)!", esp_err_to_name(ret));
}
return;
}
size_t total = 0;
size_t used = 0;
ret = esp_spiffs_info(NULL, &total, &used);
if (ret != ESP_OK)
{
KLOG_ERROR(TAG, "Failed to get SPIFFS partition information (%s)!", esp_err_to_name(ret));
}
else
{
KLOG_INFO(TAG, "SPIFFS partition: %d bytes used out of %d total.", used, total);
}
FILE *f = fopen("/spiffs/boot_message.txt", "r");
if (f == NULL)
{
KLOG_ERROR(TAG, "Failed to open boot_message.txt!");
return;
}
char buf[64];
memset(buf, 0, sizeof(buf));
fread(buf, 1, sizeof(buf), f);
fclose(f);
KLOG_INFO(TAG, ">>> %s <<<", buf);
xSemaphoreGive(init_complete);
}

26
components/NVM/SPIFFS.h Normal file
View file

@ -0,0 +1,26 @@
/*
* This program source code file is part of the KTag project.
*
* 🛡 <https://ktag.clubk.club> 🃞
*
* Copyright © 2024-2025 Joseph P. Kearney and the KTag developers.
*
* This program is free software: you can redistribute it and/or modify it under
* the terms of the GNU Affero General Public License as published by the Free
* Software Foundation, either version 3 of the License, or (at your option) any
* later version.
*
* This program is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
* FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
* details.
*
* There should be a copy of the GNU Affero General Public License in the LICENSE
* file in the root of this repository. If not, see <http://www.gnu.org/licenses/>.
*/
#pragma once
void Initialize_SPIFFS(SemaphoreHandle_t init_complete);
extern const char *DEFAULT_CONFIG_FILE;

209
components/NVM/Settings.c Normal file
View file

@ -0,0 +1,209 @@
/*
* This program source code file is part of the KTag project.
*
* 🛡 <https://ktag.clubk.club> 🃞
*
* Copyright © 2024-2025 Joseph P. Kearney and the KTag developers.
*
* This program is free software: you can redistribute it and/or modify it under
* the terms of the GNU Affero General Public License as published by the Free
* Software Foundation, either version 3 of the License, or (at your option) any
* later version.
*
* This program is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
* FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
* details.
*
* There should be a copy of the GNU Affero General Public License in the LICENSE
* file in the root of this repository. If not, see <http://www.gnu.org/licenses/>.
*/
#include <SystemK.h>
#include <NVM.h>
static const uint8_t UNINTIALIZED_UINT8 = UINT8_MAX;
static uint8_t Cached_Team_ID = UNINTIALIZED_UINT8;
static uint8_t Cached_Player_ID = UNINTIALIZED_UINT8;
static uint8_t Cached_Weapon_ID = UNINTIALIZED_UINT8;
SystemKResult_T SETTINGS_get_uint8_t(SystemKSettingID_T id, uint8_t *value)
{
SystemKResult_T result = SYSTEMK_RESULT_UNSPECIFIED_FAILURE;
char *key = "";
uint8_t *cached_value = NULL;
switch (id)
{
case SYSTEMK_SETTING_IS_RIGHT_HANDED:
key = "Is_Right_Handed";
break;
case SYSTEMK_SETTING_AUDIO_VOLUME:
key = "Audio_Volume";
break;
case SYSTEMK_SETTING_TEAMID:
if (Cached_Team_ID != UNINTIALIZED_UINT8)
{
*value = Cached_Team_ID;
result = SYSTEMK_RESULT_SUCCESS;
}
else
{
key = "Team_ID";
cached_value = &Cached_Team_ID;
}
break;
case SYSTEMK_SETTING_PLAYERID:
if (Cached_Player_ID != UNINTIALIZED_UINT8)
{
*value = Cached_Player_ID;
result = SYSTEMK_RESULT_SUCCESS;
}
else
{
key = "Player_ID";
cached_value = &Cached_Player_ID;
}
break;
case SYSTEMK_SETTING_WEAPONID:
if (Cached_Weapon_ID != UNINTIALIZED_UINT8)
{
*value = Cached_Weapon_ID;
result = SYSTEMK_RESULT_SUCCESS;
}
else
{
key = "Weapon_ID";
cached_value = &Cached_Weapon_ID;
}
break;
default:
result = SYSTEMK_RESULT_WRONG_DATATYPE;
break;
}
if (result != SYSTEMK_RESULT_SUCCESS)
{
result = KV_Get_Value_uint8(CONFIG_FILE, key, value);
if (result != SYSTEMK_RESULT_SUCCESS)
{
result = KV_Get_Value_uint8(DEFAULT_CONFIG_FILE, key, value);
if (result == SYSTEMK_RESULT_SUCCESS)
{
(void)KV_Set_Value_uint8(CONFIG_FILE, key, value);
}
}
// Save the cached value, if necessary.
if ((cached_value != NULL) && (result == SYSTEMK_RESULT_SUCCESS))
{
*cached_value = *value;
}
}
return result;
}
SystemKResult_T SETTINGS_set_uint8_t(SystemKSettingID_T id, uint8_t value)
{
SystemKResult_T result = SYSTEMK_RESULT_SUCCESS;
switch (id)
{
case SYSTEMK_SETTING_IS_RIGHT_HANDED:
result = KV_Set_Value_uint8(CONFIG_FILE, "Is_Right_Handed", &value);
break;
case SYSTEMK_SETTING_AUDIO_VOLUME:
result = KV_Set_Value_uint8(CONFIG_FILE, "Audio_Volume", &value);
break;
case SYSTEMK_SETTING_TEAMID:
Cached_Team_ID = value;
result = KV_Set_Value_uint8(CONFIG_FILE, "Team_ID", &value);
break;
case SYSTEMK_SETTING_PLAYERID:
Cached_Player_ID = value;
result = KV_Set_Value_uint8(CONFIG_FILE, "Player_ID", &value);
break;
case SYSTEMK_SETTING_WEAPONID:
Cached_Weapon_ID = value;
result = KV_Set_Value_uint8(CONFIG_FILE, "Weapon_ID", &value);
break;
default:
result = SYSTEMK_RESULT_WRONG_DATATYPE;
break;
}
return result;
}
SystemKResult_T SETTINGS_get_uint32_t(SystemKSettingID_T id, uint32_t *value)
{
SystemKResult_T result = SYSTEMK_RESULT_SUCCESS;
char *key = "";
switch (id)
{
case SYSTEMK_SETTING_T_START_GAME_in_ms:
key = "T_Start_Game_in_ms";
break;
default:
result = SYSTEMK_RESULT_WRONG_DATATYPE;
break;
}
if (result == SYSTEMK_RESULT_SUCCESS)
{
result = KV_Get_Value_uint32(CONFIG_FILE, key, value);
if (result != SYSTEMK_RESULT_SUCCESS)
{
result = KV_Get_Value_uint32(DEFAULT_CONFIG_FILE, key, value);
if (result == SYSTEMK_RESULT_SUCCESS)
{
(void)KV_Set_Value_uint32(CONFIG_FILE, key, value);
}
}
}
return result;
}
SystemKResult_T SETTINGS_set_uint32_t(SystemKSettingID_T id, uint32_t value)
{
SystemKResult_T result = SYSTEMK_RESULT_SUCCESS;
switch (id)
{
case SYSTEMK_SETTING_T_START_GAME_in_ms:
result = KV_Set_Value_uint32(CONFIG_FILE, "T_Start_Game_in_ms", &value);
break;
default:
result = SYSTEMK_RESULT_WRONG_DATATYPE;
break;
}
return result;
}
// Settings are saved on change in this implementation.
SystemKResult_T SETTINGS_Save(void)
{
return SYSTEMK_RESULT_SUCCESS;
}

449
components/NVM/USB.c Normal file
View file

@ -0,0 +1,449 @@
/*
* This program source code file is part of the KTag project.
*
* 🛡 <https://ktag.clubk.club> 🃞
*
* Copyright © 2024-2025 Joseph P. Kearney and the KTag developers.
*
* This program is free software: you can redistribute it and/or modify it under
* the terms of the GNU Affero General Public License as published by the Free
* Software Foundation, either version 3 of the License, or (at your option) any
* later version.
*
* This program is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
* FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
* details.
*
* There should be a copy of the GNU Affero General Public License in the LICENSE
* file in the root of this repository. If not, see <http://www.gnu.org/licenses/>.
*/
// From https://github.com/espressif/esp-idf/blob/master/examples/peripherals/usb/host/msc/main/msc_example_main.c
#include <SystemK.h>
#include <stdlib.h>
#include <string.h>
#include <assert.h>
#include <sys/stat.h>
#include <dirent.h>
#include <inttypes.h>
#include <errno.h>
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "freertos/queue.h"
#include "usb/usb_host.h"
#include "usb/msc_host.h"
#include "usb/msc_host_vfs.h"
#include "esp_timer.h"
#include "esp_err.h"
#include "Key_Value.h"
#include "USB.h"
#define STACK_SIZE 4096
static StaticTask_t xTaskBuffer;
static StackType_t xStack[STACK_SIZE];
#define USB_HOST_TASK_PRIORITY 2
#define MSC_HOST_TASK_PRIORITY 3
#define MNT_PATH "/usb" // Path in the Virtual File System, where the USB flash drive is going to be mounted
#define BUFFER_SIZE 4096 // The read/write performance can be improved with larger buffer for the cost of RAM; 4kB is enough for most use cases.
static const char *TAG = "USB";
static const char *OTA_FILE = "/usb/esp/OTA_URL.txt";
const char *CONFIG_FILE = "/usb/esp/config.txt";
typedef enum
{
USB_STATE_UNINITIALIZED,
USB_STATE_IDLE,
USB_STATE_DEVICE_CONNECTED,
USB_STATE_VFS_REGISTERED,
USB_STATE_PROCESSING_DISCONNECTION
} USB_State_T;
static QueueHandle_t usb_queue;
typedef struct
{
enum
{
USB_HOST_INITIALIZED,
USB_DEVICE_CONNECTED, // USB device connect event
USB_DEVICE_DISCONNECTED, // USB device disconnect event
} id;
union
{
uint8_t new_dev_address; // Address of new USB device for APP_DEVICE_CONNECTED event
} data;
} usb_message_t;
/**
* @brief MSC driver callback
*
* Signal device connection/disconnection to the main task
*
* @param[in] event MSC event
* @param[in] arg MSC event data
*/
static void MSC_Event_Callback(const msc_host_event_t *event, void *arg)
{
if (event->event == MSC_DEVICE_CONNECTED)
{
usb_message_t message = {
.id = USB_DEVICE_CONNECTED,
.data.new_dev_address = event->device.address,
};
xQueueSend(usb_queue, &message, portMAX_DELAY);
}
else if (event->event == MSC_DEVICE_DISCONNECTED)
{
usb_message_t message = {
.id = USB_DEVICE_DISCONNECTED,
};
xQueueSend(usb_queue, &message, portMAX_DELAY);
}
else
{
KLOG_INFO(TAG, "Event: %d", event->event);
}
}
__attribute__((unused)) static void print_device_info(msc_host_device_info_t *info)
{
const size_t megabyte = 1024 * 1024;
uint64_t capacity = ((uint64_t)info->sector_size * info->sector_count) / megabyte;
printf("Device info:\n");
printf("\t Capacity: %llu MB\n", capacity);
printf("\t Sector size: %" PRIu32 "\n", info->sector_size);
printf("\t Sector count: %" PRIu32 "\n", info->sector_count);
printf("\t PID: 0x%04X \n", info->idProduct);
printf("\t VID: 0x%04X \n", info->idVendor);
wprintf(L"\t iProduct: %S \n", info->iProduct);
wprintf(L"\t iManufacturer: %S \n", info->iManufacturer);
wprintf(L"\t iSerialNumber: %S \n", info->iSerialNumber);
}
__attribute__((unused)) static void file_operations(void)
{
const char *directory = "/usb/esp";
const char *file_path = "/usb/esp/test.txt";
// Create /usb/esp directory
struct stat s = {0};
bool directory_exists = stat(directory, &s) == 0;
if (!directory_exists)
{
if (mkdir(directory, 0775) != 0)
{
KLOG_ERROR(TAG, "mkdir failed with errno: %s", strerror(errno));
}
}
// Create /usb/esp/test.txt file, if it doesn't exist
if (stat(file_path, &s) != 0)
{
KLOG_INFO(TAG, "Creating file");
FILE *f = fopen(file_path, "w");
if (f == NULL)
{
KLOG_ERROR(TAG, "Failed to open file for writing");
return;
}
fprintf(f, "Hello World!\n");
fclose(f);
}
// Read back the file
FILE *f;
KLOG_INFO(TAG, "Reading file");
f = fopen(file_path, "r");
if (f == NULL)
{
KLOG_ERROR(TAG, "Failed to open file for reading");
return;
}
char line[64];
fgets(line, sizeof(line), f);
fclose(f);
// strip newline
char *pos = strchr(line, '\n');
if (pos)
{
*pos = '\0';
}
KLOG_INFO(TAG, "Read from file '%s': '%s'", file_path, line);
}
__attribute__((unused)) void speed_test(void)
{
#define TEST_FILE "/usb/esp/dummy"
#define ITERATIONS 256 // 256 * 4kb = 1MB
int64_t test_start, test_end;
FILE *f = fopen(TEST_FILE, "wb+");
if (f == NULL)
{
KLOG_ERROR(TAG, "Failed to open file for writing");
return;
}
// Set larger buffer for this file. It results in larger and more effective USB transfers
setvbuf(f, NULL, _IOFBF, BUFFER_SIZE);
// Allocate application buffer used for read/write
uint8_t *data = malloc(BUFFER_SIZE);
assert(data);
KLOG_INFO(TAG, "Writing to file %s", TEST_FILE);
test_start = esp_timer_get_time();
for (int i = 0; i < ITERATIONS; i++)
{
if (fwrite(data, BUFFER_SIZE, 1, f) == 0)
{
return;
}
}
test_end = esp_timer_get_time();
KLOG_INFO(TAG, "Write speed %1.2f MiB/s", (BUFFER_SIZE * ITERATIONS) / (float)(test_end - test_start));
rewind(f);
KLOG_INFO(TAG, "Reading from file %s", TEST_FILE);
test_start = esp_timer_get_time();
for (int i = 0; i < ITERATIONS; i++)
{
if (0 == fread(data, BUFFER_SIZE, 1, f))
{
return;
}
}
test_end = esp_timer_get_time();
KLOG_INFO(TAG, "Read speed %1.2f MiB/s", (BUFFER_SIZE * ITERATIONS) / (float)(test_end - test_start));
fclose(f);
free(data);
}
static void usb_host_task(void *args)
{
usb_host_config_t host_config = {
.skip_phy_setup = false,
.intr_flags = ESP_INTR_FLAG_LEVEL1,
};
ESP_ERROR_CHECK(usb_host_install(&host_config));
const msc_host_driver_config_t msc_config = {
.create_backround_task = true,
.task_priority = MSC_HOST_TASK_PRIORITY,
.stack_size = 4096,
.callback = MSC_Event_Callback,
};
ESP_ERROR_CHECK(msc_host_install(&msc_config));
vTaskDelay(10); // Short delay to let MSC client task spin up.
usb_message_t message = {
.id = USB_HOST_INITIALIZED};
xQueueSend(usb_queue, &message, portMAX_DELAY);
while (true)
{
uint32_t event_flags;
// This function handles all of the USB Host Library's processing and should be called repeatedly in a loop.
ESP_ERROR_CHECK(usb_host_lib_handle_events(portMAX_DELAY, &event_flags));
// Release devices once all clients have deregistered.
if (event_flags & USB_HOST_LIB_EVENT_FLAGS_NO_CLIENTS)
{
if (usb_host_device_free_all() == ESP_OK)
{
KLOG_INFO(TAG, "USB_HOST_LIB_EVENT_FLAGS_NO_CLIENTS");
// break;
};
}
if (event_flags & USB_HOST_LIB_EVENT_FLAGS_ALL_FREE)
{
KLOG_INFO(TAG, "USB_HOST_LIB_EVENT_FLAGS_ALL_FREE");
// break;
}
}
}
static void app_usb_task(void *args)
{
SemaphoreHandle_t init_complete = args;
static USB_State_T Current_State = USB_STATE_UNINITIALIZED;
static msc_host_device_handle_t msc_device = NULL;
static msc_host_vfs_handle_t vfs_handle = NULL;
static uint8_t device_address = 1;
usb_message_t msg;
while (true)
{
switch (Current_State)
{
case USB_STATE_UNINITIALIZED:
{
if (xQueueReceive(usb_queue, &msg, portMAX_DELAY) == pdTRUE)
{
if (msg.id == USB_HOST_INITIALIZED)
{
KLOG_INFO(TAG, "Host initialized.");
Current_State = USB_STATE_IDLE;
}
}
}
break;
case USB_STATE_IDLE:
{
KLOG_TRACE(TAG, "USB_STATE_IDLE");
if (xQueueReceive(usb_queue, &msg, pdMS_TO_TICKS(1000)) == pdTRUE)
{
if (msg.id == USB_DEVICE_CONNECTED)
{
device_address = msg.data.new_dev_address;
Current_State = USB_STATE_DEVICE_CONNECTED;
KLOG_INFO(TAG, "Device connected.");
}
}
else
{
KLOG_ERROR(TAG, "No flash drive detected--rebooting.");
vTaskDelay(pdMS_TO_TICKS(100));
esp_restart();
}
}
break;
case USB_STATE_DEVICE_CONNECTED:
{
KLOG_TRACE(TAG, "USB_STATE_DEVICE_CONNECTED");
// USB device connected. Open it and attempt to map it to Virtual File System.
if (msc_host_install_device(device_address, &msc_device) != ESP_OK)
{
ESP_LOGW(TAG, "Problem connecting to flash drive.");
Current_State = USB_STATE_PROCESSING_DISCONNECTION;
}
else
{
const esp_vfs_fat_mount_config_t mount_config = {
.format_if_mount_failed = false,
.max_files = 3,
.allocation_unit_size = 8192,
};
if (msc_host_vfs_register(msc_device, MNT_PATH, &mount_config, &vfs_handle) != ESP_OK)
{
ESP_LOGW(TAG, "Problem mounting filesystem.");
Current_State = USB_STATE_PROCESSING_DISCONNECTION;
}
else
{
Current_State = USB_STATE_VFS_REGISTERED;
xSemaphoreGive(init_complete);
}
}
}
break;
case USB_STATE_VFS_REGISTERED:
{
KLOG_TRACE(TAG, "USB_STATE_VFS_REGISTERED");
if (xQueueReceive(usb_queue, &msg, pdMS_TO_TICKS(1000)) == pdTRUE)
{
if (msg.id == USB_DEVICE_DISCONNECTED)
{
Current_State = USB_STATE_PROCESSING_DISCONNECTION;
}
}
vTaskDelay(pdMS_TO_TICKS(1000));
}
break;
case USB_STATE_PROCESSING_DISCONNECTION:
{
KLOG_TRACE(TAG, "USB_STATE_PROCESSING_DISCONNECTION");
if (vfs_handle)
{
ESP_ERROR_CHECK(msc_host_vfs_unregister(vfs_handle));
vfs_handle = NULL;
}
if (msc_device)
{
ESP_ERROR_CHECK(msc_host_uninstall_device(msc_device));
msc_device = NULL;
device_address = 0;
}
Current_State = USB_STATE_IDLE;
}
break;
}
}
}
void Initialize_USB(SemaphoreHandle_t init_complete)
{
KLOG_INFO(TAG, "Initializing USB file system...");
// Create FreeRTOS primitives
usb_queue = xQueueCreate(5, sizeof(usb_message_t));
assert(usb_queue);
BaseType_t usb_host_task_created = xTaskCreatePinnedToCore(usb_host_task, "USB Host", 4096, NULL, USB_HOST_TASK_PRIORITY, NULL, 1);
assert(usb_host_task_created);
TaskHandle_t xHandle = xTaskCreateStaticPinnedToCore(
app_usb_task, // Function that implements the task.
"USB NVM", // Text name for the task.
STACK_SIZE, // Stack size in bytes, not words.
(void *)init_complete, // Parameter passed into the task.
tskIDLE_PRIORITY + 2, // Priority at which the task is created.
xStack, // Array to use as the task's stack.
&xTaskBuffer, // Variable to hold the task's data structure.
APP_CPU_NUM); // Specify the task's core affinity
assert(xHandle);
}
bool OTA_File_Exists(void)
{
struct stat s = {0};
return (stat(OTA_FILE, &s) == 0);
}
char *Get_OTA_Image_URL(void)
{
static char url[64] = "";
FILE *f;
f = fopen(OTA_FILE, "r");
if (f == NULL)
{
KLOG_ERROR(TAG, "The OTA file is missing!");
return url;
}
fgets(url, sizeof(url), f);
fclose(f);
// Strip the newline
char *pos = strchr(url, '\n');
if (pos)
{
*pos = '\0';
}
return url;
}
void Erase_OTA_File(void)
{
if (remove(OTA_FILE) != 0)
{
KLOG_ERROR(TAG, "Error erasing the OTA file: %s", strerror(errno));
}
}

31
components/NVM/USB.h Normal file
View file

@ -0,0 +1,31 @@
/*
* This program source code file is part of the KTag project.
*
* 🛡 <https://ktag.clubk.club> 🃞
*
* Copyright © 2024-2025 Joseph P. Kearney and the KTag developers.
*
* This program is free software: you can redistribute it and/or modify it under
* the terms of the GNU Affero General Public License as published by the Free
* Software Foundation, either version 3 of the License, or (at your option) any
* later version.
*
* This program is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
* FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
* details.
*
* There should be a copy of the GNU Affero General Public License in the LICENSE
* file in the root of this repository. If not, see <http://www.gnu.org/licenses/>.
*/
#pragma once
void Initialize_USB(SemaphoreHandle_t init_complete);
extern const char *CONFIG_FILE;
// OTA Reprogramming Helpers
bool OTA_File_Exists(void);
char * Get_OTA_Image_URL(void);
void Erase_OTA_File(void);

View file

@ -0,0 +1,14 @@
idf_component_register(
SRCS
"Reprogramming.c"
INCLUDE_DIRS
"."
REQUIRES
"SystemK"
"driver"
"usb"
"usb_host_msc"
"esp_https_ota"
"app_update"
"NVM"
)

View file

@ -0,0 +1,311 @@
/*
* This program source code file is part of the KTag project.
*
* 🛡 <https://ktag.clubk.club> 🃞
*
* Copyright © 2024-2025 Joseph P. Kearney and the KTag developers.
*
* This program is free software: you can redistribute it and/or modify it under
* the terms of the GNU Affero General Public License as published by the Free
* Software Foundation, either version 3 of the License, or (at your option) any
* later version.
*
* This program is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
* FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
* details.
*
* There should be a copy of the GNU Affero General Public License in the LICENSE
* file in the root of this repository. If not, see <http://www.gnu.org/licenses/>.
*/
#include <SystemK.h>
#include <stdlib.h>
#include <string.h>
#include <dirent.h>
#include <errno.h>
#include <NVM.h>
#include "esp_log.h"
#include "esp_ota_ops.h"
#include "esp_spiffs.h"
#include "esp_partition.h"
static const char *TAG = "Reprogramming";
static const char *FIRMWARE_DIR = "/usb/esp/firmware";
static const char *APP_IMAGE_PREFIX = "APP";
static const char *SPIFFS_IMAGE_PREFIX = "SPIFFS";
#define REPROGRAMMING_TASK_PRIORITY 1
static TaskHandle_t xReprogrammingTask = NULL;
static StaticTask_t xReprogrammingTaskBuffer;
static StackType_t xReprogrammingTaskStack[4096];
#define BUFFER_SIZE 1024
typedef struct
{
unsigned long total_size;
unsigned long remaining_size;
void *data;
} data_manager_t;
typedef enum {
IMAGE_TYPE_APP,
IMAGE_TYPE_SPIFFS
} image_type_t;
static char *find_image_filename(const char *prefix)
{
DIR *dir = opendir(FIRMWARE_DIR);
if (dir == NULL)
{
KLOG_ERROR(TAG, "Failed to open directory '%s': %s!", FIRMWARE_DIR, strerror(errno));
return NULL;
}
struct dirent *entry;
char *filename = NULL;
while ((entry = readdir(dir)) != NULL)
{
if (entry->d_type == DT_REG && strncmp(entry->d_name, prefix, strlen(prefix)) == 0)
{
filename = strdup(entry->d_name);
if (filename == NULL)
{
KLOG_ERROR(TAG, "Memory allocation failed!");
}
else
{
KLOG_DEBUG(TAG, "Found image: %s", filename);
}
break;
}
}
closedir(dir);
if (filename == NULL)
{
KLOG_WARN(TAG, "Couldn't find an image with prefix '%s' in directory '%s'!", prefix, FIRMWARE_DIR);
return NULL;
}
size_t needed = snprintf(NULL, 0, "%s/%s", FIRMWARE_DIR, filename) + 1;
char *full_path = malloc(needed);
if (full_path != NULL)
{
snprintf(full_path, needed, "%s/%s", FIRMWARE_DIR, filename);
}
free(filename);
return full_path;
}
static SystemKResult_T program_image(const char *filename, image_type_t image_type)
{
esp_err_t err;
FILE *file = fopen(filename, "rb");
if (file == NULL)
{
KLOG_ERROR(TAG, "Failed to open file %s", filename);
return SYSTEMK_RESULT_FILE_NOT_FOUND;
}
fseek(file, 0, SEEK_END);
long file_size = ftell(file);
fseek(file, 0, SEEK_SET);
KLOG_INFO(TAG, "File size: %ld bytes", file_size);
data_manager_t data_manager = {
.total_size = file_size,
.remaining_size = file_size,
.data = malloc(BUFFER_SIZE)
};
if (data_manager.data == NULL)
{
KLOG_ERROR(TAG, "Failed to allocate memory");
fclose(file);
return SYSTEMK_RESULT_NOT_ENOUGH_MEMORY;
}
if (image_type == IMAGE_TYPE_APP)
{
esp_ota_handle_t update_handle = 0;
const esp_partition_t *update_partition = esp_ota_get_next_update_partition(NULL);
KLOG_INFO(TAG, "Writing to partition subtype %d at offset 0x%" PRIx32, update_partition->subtype, update_partition->address);
err = esp_ota_begin(update_partition, OTA_SIZE_UNKNOWN, &update_handle);
if (err != ESP_OK)
{
KLOG_ERROR(TAG, "esp_ota_begin failed (%s)", esp_err_to_name(err));
free(data_manager.data);
fclose(file);
return SYSTEMK_RESULT_UNSPECIFIED_FAILURE;
}
while (data_manager.remaining_size > 0)
{
size_t size = (data_manager.remaining_size <= BUFFER_SIZE) ? data_manager.remaining_size : BUFFER_SIZE;
if (fread(data_manager.data, 1, size, file) != size)
{
KLOG_ERROR(TAG, "fread failed");
err = ESP_FAIL;
break;
}
err = esp_ota_write(update_handle, data_manager.data, size);
if (err != ESP_OK)
{
KLOG_ERROR(TAG, "esp_ota_write failed (%s)", esp_err_to_name(err));
break;
}
data_manager.remaining_size -= size;
}
if (err == ESP_OK)
{
err = esp_ota_end(update_handle);
if (err != ESP_OK)
{
KLOG_ERROR(TAG, "esp_ota_end failed (%s)", esp_err_to_name(err));
}
else
{
err = esp_ota_set_boot_partition(update_partition);
if (err != ESP_OK)
{
KLOG_ERROR(TAG, "esp_ota_set_boot_partition failed (%s)", esp_err_to_name(err));
}
}
}
}
else if (image_type == IMAGE_TYPE_SPIFFS)
{
const esp_partition_t *spiffs_partition = esp_partition_find_first(ESP_PARTITION_TYPE_DATA, ESP_PARTITION_SUBTYPE_DATA_SPIFFS, NULL);
if (spiffs_partition == NULL)
{
KLOG_ERROR(TAG, "SPIFFS partition not found");
free(data_manager.data);
fclose(file);
return SYSTEMK_RESULT_UNSPECIFIED_FAILURE;
}
err = esp_partition_erase_range(spiffs_partition, 0, spiffs_partition->size);
if (err != ESP_OK)
{
KLOG_ERROR(TAG, "Failed to erase SPIFFS partition (%s)", esp_err_to_name(err));
free(data_manager.data);
fclose(file);
return SYSTEMK_RESULT_UNSPECIFIED_FAILURE;
}
while (data_manager.remaining_size > 0)
{
size_t size = (data_manager.remaining_size <= BUFFER_SIZE) ? data_manager.remaining_size : BUFFER_SIZE;
if (fread(data_manager.data, 1, size, file) != size)
{
KLOG_ERROR(TAG, "fread failed");
err = ESP_FAIL;
break;
}
err = esp_partition_write(spiffs_partition, spiffs_partition->size - data_manager.remaining_size, data_manager.data, size);
if (err != ESP_OK)
{
KLOG_ERROR(TAG, "esp_partition_write failed (%s)", esp_err_to_name(err));
break;
}
data_manager.remaining_size -= size;
}
}
free(data_manager.data);
fclose(file);
return (err == ESP_OK) ? SYSTEMK_RESULT_SUCCESS : SYSTEMK_RESULT_UNSPECIFIED_FAILURE;
}
SystemKResult_T Maybe_Reprogram_From_USB(void)
{
SystemKResult_T result = SYSTEMK_RESULT_SUCCESS;
char *app_image_filename = find_image_filename(APP_IMAGE_PREFIX);
char *spiffs_image_filename = find_image_filename(SPIFFS_IMAGE_PREFIX);
if (app_image_filename)
{
KLOG_INFO(TAG, "Application image found: %s", app_image_filename);
result = program_image(app_image_filename, IMAGE_TYPE_APP);
free(app_image_filename);
if (result != SYSTEMK_RESULT_SUCCESS)
{
return result;
}
}
if (spiffs_image_filename)
{
KLOG_INFO(TAG, "SPIFFS image found: %s", spiffs_image_filename);
result = program_image(spiffs_image_filename, IMAGE_TYPE_SPIFFS);
free(spiffs_image_filename);
}
if ((app_image_filename == NULL) && (spiffs_image_filename == NULL))
{
KLOG_ERROR(TAG, "Reprogramming failed--there's nothing to reprogram!");
result = SYSTEMK_RESULT_FILE_NOT_FOUND;
}
return result;
}
static void Reprogramming_Task(void *pvParameters)
{
SystemKResult_T result = Maybe_Reprogram_From_USB();
static All_On_Data_T data;
if (result == SYSTEMK_RESULT_SUCCESS)
{
KLOG_INFO(TAG, "Reprogramming succeeded--rebooting...");
data.color = ApplyMask(COLOR_GREEN, 0x70);
data.style = DISPLAY_STYLE_SOLID;
}
else
{
KLOG_ERROR(TAG, "Reprogramming failed--rebooting...");
data.color = ApplyMask(COLOR_RED, 0x70);
data.style = DISPLAY_STYLE_SOLID;
}
NeoPixelsAction_T neopixels_action = {.ID = NEOPIXELS_ALL_ON, .Prominence = NEOPIXELS_FOREGROUND, .Data = (void *)&data};
xQueueSend(xQueueNeoPixels, &neopixels_action, 0);
vTaskDelay(5000 / portTICK_PERIOD_MS);
esp_restart();
vTaskDelete(NULL);
}
void Request_Reprogramming_From_USB(void)
{
if (xReprogrammingTask == NULL)
{
KLOG_INFO(TAG, "Attempting USB reprogramming...");
xReprogrammingTask = xTaskCreateStaticPinnedToCore(
Reprogramming_Task,
"Reprogramming",
sizeof(xReprogrammingTaskStack) / sizeof(StackType_t),
NULL,
REPROGRAMMING_TASK_PRIORITY,
xReprogrammingTaskStack,
&xReprogrammingTaskBuffer,
APP_CPU_NUM);
}
else
{
KLOG_WARN(TAG, "Reprogramming already in progress.");
}
}

View file

@ -0,0 +1,22 @@
/*
* This program source code file is part of the KTag project.
*
* 🛡 <https://ktag.clubk.club> 🃞
*
* Copyright © 2024-2025 Joseph P. Kearney and the KTag developers.
*
* This program is free software: you can redistribute it and/or modify it under
* the terms of the GNU Affero General Public License as published by the Free
* Software Foundation, either version 3 of the License, or (at your option) any
* later version.
*
* This program is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
* FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
* details.
*
* There should be a copy of the GNU Affero General Public License in the LICENSE
* file in the root of this repository. If not, see <http://www.gnu.org/licenses/>.
*/
void Request_Reprogramming_From_USB(void);

View file

@ -0,0 +1,10 @@
idf_component_register(
SRCS
"Switches.c"
INCLUDE_DIRS
"."
REQUIRES
"SystemK"
"driver"
"button"
)

View file

@ -0,0 +1,88 @@
#include <SystemK.h>
#include <driver/gpio.h>
#include <iot_button.h>
#include <freertos/FreeRTOS.h>
#include <freertos/portmacro.h>
static const char *TAG = "Switches";
// GPIO assignments
#define TRIGGER_GPIO GPIO_NUM_1
#define ACCESSORY_GPIO GPIO_NUM_4
static button_handle_t Trigger_Button;
static button_config_t Trigger_Button_Config = {
.type = BUTTON_TYPE_GPIO,
.long_press_time = CONFIG_BUTTON_LONG_PRESS_TIME_MS,
.short_press_time = CONFIG_BUTTON_SHORT_PRESS_TIME_MS,
.gpio_button_config = {
.gpio_num = TRIGGER_GPIO,
.active_level = 0,
},
};
static button_handle_t Accessory_Button;
static button_config_t Accessory_Button_Config = {
.type = BUTTON_TYPE_GPIO,
.long_press_time = CONFIG_BUTTON_LONG_PRESS_TIME_MS,
.short_press_time = CONFIG_BUTTON_SHORT_PRESS_TIME_MS,
.gpio_button_config = {
.gpio_num = ACCESSORY_GPIO,
.active_level = 0,
},
};
static TickType_t TicksAtTriggerPress = 0;
static TickType_t TicksAtAccessoryPress = 0;
static TickType_t TicksAtAccessoryRelease = 0;
static void trigger_press_cb(void *arg, void *usr_data)
{
portBASE_TYPE xHigherPriorityTaskWoken = pdFALSE;
TicksAtTriggerPress = xTaskGetTickCountFromISR();
KEvent_T switch_event = {.ID = KEVENT_TRIGGER_SWITCH_PRESSED, .Data = NULL};
Post_KEvent_From_ISR(&switch_event, &xHigherPriorityTaskWoken);
}
static void trigger_release_cb(void *arg, void *usr_data)
{
portBASE_TYPE xHigherPriorityTaskWoken = pdFALSE;
uint32_t triggerPressDurationInms = pdTICKS_TO_MS(xTaskGetTickCountFromISR() - TicksAtTriggerPress);
KEvent_T switch_event = {.ID = KEVENT_TRIGGER_SWITCH_RELEASED, .Data = (void *) triggerPressDurationInms};
Post_KEvent_From_ISR(&switch_event, &xHigherPriorityTaskWoken);
}
static void accessory_press_cb(void *arg, void *usr_data)
{
portBASE_TYPE xHigherPriorityTaskWoken = pdFALSE;
TicksAtAccessoryPress = xTaskGetTickCountFromISR();
uint32_t accessoryTimeReleasedInms = pdTICKS_TO_MS(xTaskGetTickCountFromISR() - TicksAtAccessoryRelease);
KEvent_T switch_event = {.ID = KEVENT_ACCESSORY_SWITCH_PRESSED, .Data = (void *) accessoryTimeReleasedInms};
Post_KEvent_From_ISR(&switch_event, &xHigherPriorityTaskWoken);
}
static void accessory_release_cb(void *arg, void *usr_data)
{
portBASE_TYPE xHigherPriorityTaskWoken = pdFALSE;
TicksAtAccessoryRelease = xTaskGetTickCountFromISR();
uint32_t accessoryPressDurationInms = pdTICKS_TO_MS(xTaskGetTickCountFromISR() - TicksAtAccessoryPress);
KEvent_T switch_event = {.ID = KEVENT_ACCESSORY_SWITCH_RELEASED, .Data = (void *) accessoryPressDurationInms};
Post_KEvent_From_ISR(&switch_event, &xHigherPriorityTaskWoken);
}
void Initialize_Switches(void)
{
KLOG_INFO(TAG, "Initializing Switches...");
Trigger_Button = iot_button_create(&Trigger_Button_Config);
assert(Trigger_Button);
Accessory_Button = iot_button_create(&Accessory_Button_Config);
assert(Accessory_Button);
iot_button_register_cb(Trigger_Button, BUTTON_PRESS_DOWN, trigger_press_cb, NULL);
iot_button_register_cb(Trigger_Button, BUTTON_PRESS_UP, trigger_release_cb, NULL);
iot_button_register_cb(Accessory_Button, BUTTON_PRESS_DOWN, accessory_press_cb, NULL);
iot_button_register_cb(Accessory_Button, BUTTON_PRESS_UP, accessory_release_cb, NULL);
}

View file

@ -0,0 +1,2 @@
void Initialize_Switches(void);

1
components/SystemK Submodule

@ -0,0 +1 @@
Subproject commit 6f51f5b006db81d75dd752456be264bc50ce1b9b

View file

@ -0,0 +1,20 @@
idf_component_register(
SRCS
"WiFi.c"
INCLUDE_DIRS
"."
REQUIRES
"SystemK"
"esp_wifi"
"nvs_flash"
"esp_netif"
"esp_http_client"
"esp_event"
"esp_system"
"freertos"
"esp-tls"
"wifi_provisioning"
"esp_https_ota"
"app_update"
"NVM"
)

253
components/WiFi/WiFi.c Normal file
View file

@ -0,0 +1,253 @@
#include <SystemK.h>
#include <USB.h>
#include "esp_log.h"
#include "nvs_flash.h"
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "freertos/event_groups.h"
#include "esp_event.h"
#include <string.h>
#include "esp_wifi.h"
#include "esp_http_client.h"
#include "esp_tls.h"
#include "esp_https_ota.h"
#include "esp_ota_ops.h"
#include "esp_crt_bundle.h"
#include "USB.h"
#include "Key_Value.h"
static const char *TAG = "WiFi";
static EventGroupHandle_t s_wifi_event_group;
#define WIFI_CONNECTED_BIT BIT0
#define WIFI_FAIL_BIT BIT1
#define KTAG_WIFI_TASK_PRIORITY 2
static TaskHandle_t xWiFiTask = NULL;
static StaticTask_t xWiFiTaskBuffer;
static StackType_t xWiFiTaskStack[4096];
static void event_handler(void *arg, esp_event_base_t event_base,
int32_t event_id, void *event_data)
{
if (event_base == WIFI_EVENT && event_id == WIFI_EVENT_STA_START)
{
KLOG_INFO(TAG, "Connecting to the WiFi Access Point...");
esp_wifi_connect();
}
else if (event_base == WIFI_EVENT && event_id == WIFI_EVENT_STA_CONNECTED)
{
KLOG_INFO(TAG, "Connected to the WiFi Access Point!");
}
else if (event_base == WIFI_EVENT && event_id == WIFI_EVENT_STA_DISCONNECTED)
{
KLOG_WARN(TAG, "Failed to connect to the WiFi Access Point!");
xEventGroupSetBits(s_wifi_event_group, WIFI_FAIL_BIT);
}
else if (event_base == IP_EVENT && event_id == IP_EVENT_STA_GOT_IP)
{
ip_event_got_ip_t *event = (ip_event_got_ip_t *)event_data;
KLOG_INFO(TAG, "Got an IP address:" IPSTR, IP2STR(&event->ip_info.ip));
xEventGroupSetBits(s_wifi_event_group, WIFI_CONNECTED_BIT);
}
else
{
KLOG_WARN(TAG, "Unhandled event: %s %ld", event_base, event_id);
}
}
esp_err_t _http_event_handler(esp_http_client_event_t *evt)
{
switch (evt->event_id)
{
case HTTP_EVENT_ERROR:
KLOG_DEBUG(TAG, "HTTP_EVENT_ERROR");
break;
case HTTP_EVENT_ON_CONNECTED:
KLOG_DEBUG(TAG, "HTTP_EVENT_ON_CONNECTED");
break;
case HTTP_EVENT_HEADER_SENT:
KLOG_DEBUG(TAG, "HTTP_EVENT_HEADER_SENT");
break;
case HTTP_EVENT_ON_HEADER:
KLOG_DEBUG(TAG, "HTTP_EVENT_ON_HEADER, key=%s, value=%s", evt->header_key, evt->header_value);
break;
case HTTP_EVENT_ON_DATA:
KLOG_DEBUG(TAG, "HTTP_EVENT_ON_DATA, %.*s", evt->data_len, (char *)evt->data);
break;
case HTTP_EVENT_ON_FINISH:
KLOG_DEBUG(TAG, "HTTP_EVENT_ON_FINISH");
break;
case HTTP_EVENT_DISCONNECTED:
KLOG_DEBUG(TAG, "HTTP_EVENT_DISCONNECTED");
break;
case HTTP_EVENT_REDIRECT:
KLOG_DEBUG(TAG, "HTTP_EVENT_REDIRECT");
break;
}
return ESP_OK;
}
bool wifi_init_sta(void)
{
s_wifi_event_group = xEventGroupCreate();
ESP_ERROR_CHECK(esp_netif_init());
ESP_ERROR_CHECK(esp_event_loop_create_default());
esp_netif_create_default_wifi_sta();
esp_event_handler_instance_t instance_any_id;
esp_event_handler_instance_t instance_got_ip;
ESP_ERROR_CHECK(esp_event_handler_instance_register(WIFI_EVENT,
ESP_EVENT_ANY_ID,
&event_handler,
NULL,
&instance_any_id));
ESP_ERROR_CHECK(esp_event_handler_instance_register(IP_EVENT,
IP_EVENT_STA_GOT_IP,
&event_handler,
NULL,
&instance_got_ip));
wifi_init_config_t cfg = WIFI_INIT_CONFIG_DEFAULT();
ESP_ERROR_CHECK(esp_wifi_init(&cfg));
wifi_config_t wifi_config = {
.sta = {
.failure_retry_cnt = 3,
},
};
char STA_SSID[32];
char STA_Password[64];
KV_Get_Value_string(CONFIG_FILE, "STA_SSID", STA_SSID);
KV_Get_Value_string(CONFIG_FILE, "STA_Password", STA_Password);
strlcpy((char *)wifi_config.sta.ssid, STA_SSID, sizeof(wifi_config.sta.ssid));
strlcpy((char *)wifi_config.sta.password, STA_Password, sizeof(wifi_config.sta.password));
ESP_ERROR_CHECK(esp_wifi_set_mode(WIFI_MODE_STA));
ESP_ERROR_CHECK(esp_wifi_set_config(WIFI_IF_STA, &wifi_config));
ESP_ERROR_CHECK(esp_wifi_start());
// Wait until either the connection is established (WIFI_CONNECTED_BIT) or connection failed for the maximum
// number of re-tries (WIFI_FAIL_BIT). The bits are set by event_handler() (see above).
EventBits_t bits = xEventGroupWaitBits(s_wifi_event_group,
WIFI_CONNECTED_BIT | WIFI_FAIL_BIT,
pdFALSE,
pdFALSE,
portMAX_DELAY);
if (bits & WIFI_CONNECTED_BIT)
{
KLOG_INFO(TAG, "Connected to AP with SSID: %s", wifi_config.sta.ssid);
return true;
}
else if (bits & WIFI_FAIL_BIT)
{
KLOG_WARN(TAG, "Failed to connect to SSID \"%s\"!", wifi_config.sta.ssid);
return false;
}
else
{
KLOG_ERROR(TAG, "Unexpected event!");
return false;
}
}
void download_test_file(void)
{
esp_http_client_config_t config = {
.url = "https://ktag.clubk.club/2024A.txt",
.crt_bundle_attach = esp_crt_bundle_attach,
.event_handler = _http_event_handler};
esp_http_client_handle_t client = esp_http_client_init(&config);
esp_err_t err = esp_http_client_perform(client);
if (err == ESP_OK)
{
KLOG_INFO(TAG, "HTTPS Status = %d, content_length = %lld",
esp_http_client_get_status_code(client),
esp_http_client_get_content_length(client));
}
else
{
KLOG_ERROR(TAG, "Error perform http request %s", esp_err_to_name(err));
}
esp_http_client_cleanup(client);
}
esp_http_client_config_t ota_http_config = {
.crt_bundle_attach = esp_crt_bundle_attach,
.event_handler = _http_event_handler,
.buffer_size = 8192,
.keep_alive_enable = true,
.timeout_ms = 30 * 1000};
esp_https_ota_config_t ota_config = {
.http_config = &ota_http_config,
.bulk_flash_erase = false,
};
static void WiFi_Task(void *pvParameters)
{
if (wifi_init_sta())
{
download_test_file();
vTaskDelay(5000 / portTICK_PERIOD_MS);
const esp_partition_t *running = esp_ota_get_running_partition();
esp_app_desc_t running_app_info;
if (esp_ota_get_partition_description(running, &running_app_info) == ESP_OK)
{
KLOG_INFO(TAG, "Running firmware version: %s", running_app_info.version);
}
ota_http_config.url = Get_OTA_Image_URL();
KLOG_INFO(TAG, "Attempting OTA reprogramming from %s", ota_http_config.url);
esp_log_level_set("wifi", ESP_LOG_VERBOSE);
esp_err_t err = esp_https_ota(&ota_config);
if (err == ESP_OK)
{
Erase_OTA_File();
KLOG_INFO(TAG, "OTA Succeed, Rebooting...\n");
vTaskDelay(5000 / portTICK_PERIOD_MS);
esp_restart();
}
else
{
KLOG_ERROR(TAG, "OTA Failed!\n");
}
}
vTaskDelete(NULL);
}
void Initialize_WiFi(void)
{
if (xWiFiTask == NULL)
{
KLOG_INFO(TAG, "Initializing WiFi...");
xWiFiTask = xTaskCreateStaticPinnedToCore(
WiFi_Task,
"KTag WiFI",
sizeof(xWiFiTaskStack) / sizeof(StackType_t),
NULL,
KTAG_WIFI_TASK_PRIORITY,
xWiFiTaskStack,
&xWiFiTaskBuffer,
APP_CPU_NUM);
}
else
{
KLOG_WARN(TAG, "WiFi already running.");
}
}

1
components/WiFi/WiFi.h Normal file
View file

@ -0,0 +1 @@
void Initialize_WiFi(void);