Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions platformio.ini
Original file line number Diff line number Diff line change
Expand Up @@ -301,6 +301,15 @@ build_unflags = ${common.build_unflags}
build_flags = ${common.build_flags_esp32} -D WLED_RELEASE_NAME=ESP32_Ethernet -D RLYPIN=-1 -D WLED_USE_ETHERNET -D BTNPIN=-1
lib_deps = ${esp32.lib_deps}

[env:esp32_eth_sd]
board = esp32-poe
platform = espressif32@2.0
upload_speed = 230400
build_unflags = ${common.build_unflags}
build_flags = ${common.build_flags_esp32} -D WLED_RELEASE_NAME=ESP32_Ethernet_SD -D RLYPIN=-1 -D WLED_USE_ETHERNET -D BTNPIN=-1 -D WLED_DEBUG -D WLED_USE_SD
lib_deps = ${esp32.lib_deps}
board_build.partitions = tools/WLED_ESP32_4MB_1MB_FS.csv

[env:esp32s2_saola]
board = esp32dev
board_build.mcu = esp32s2
Expand Down
1 change: 1 addition & 0 deletions wled00/const.h
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,7 @@
#define REALTIME_MODE_ARTNET 6
#define REALTIME_MODE_TPM2NET 7
#define REALTIME_MODE_DDP 8
#define REALTIME_MODE_TPM2RECORD 9

//realtime override modes
#define REALTIME_OVERRIDE_NONE 0
Expand Down
1 change: 1 addition & 0 deletions wled00/data/edit.htm
Original file line number Diff line number Diff line change
Expand Up @@ -483,6 +483,7 @@
case "json":
case "xml":
case "ini":
case "tpm2":
lang = ext;
}
}
Expand Down
4 changes: 4 additions & 0 deletions wled00/fcn_declare.h
Original file line number Diff line number Diff line change
Expand Up @@ -193,6 +193,10 @@ bool handleSet(AsyncWebServerRequest *request, const String& req, bool apply=tru
int getNumVal(const String* req, uint16_t pos);
bool updateVal(const String* req, const char* key, byte* val, byte minv=0, byte maxv=255);

//tpm2record.cpp
void loadRecording(const char *filepath, uint16_t startLed = -1, uint16_t stopLed = -1);
void handlePlayRecording();

//udp.cpp
void notify(byte callMode, bool followUp=false);
uint8_t realtimeBroadcast(uint8_t type, IPAddress client, uint16_t length, byte *buffer, uint8_t bri=255, bool isRGBW=false);
Expand Down
40 changes: 31 additions & 9 deletions wled00/json.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -257,6 +257,26 @@ bool deserializeState(JsonObject root, byte callMode, byte presetId)
if (presetsModifiedTime == 0) presetsModifiedTime = timein;
}

JsonVariant tpm2Var = root["tpm2"];
if (tpm2Var.is<JsonObject>())
{
const char *recording_path = tpm2Var["file"].as<const char *>();
if(recording_path) {
int id = -1;

JsonVariant segVar = tpm2Var["seg"];
if(segVar) { // playback on segments
if (segVar.is<JsonObject>() ) { id = segVar["id"] | -1; } // passed as json object
else if(segVar.is<JsonInteger>()) { id = segVar; } // passed as integer
else
DEBUG_PRINTLN("TPM2: 'seg' either as integer or as json with 'id':'integer'");
};

WS2812FX::Segment sg = strip.getSegment(id);
loadRecording(recording_path, sg.start, sg.stop);
}
}

doReboot = root[F("rb")] | doReboot;

realtimeOverride = root[F("lor")] | realtimeOverride;
Expand Down Expand Up @@ -494,15 +514,17 @@ void serializeInfo(JsonObject root)
root["live"] = (bool)realtimeMode;

switch (realtimeMode) {
case REALTIME_MODE_INACTIVE: root["lm"] = ""; break;
case REALTIME_MODE_GENERIC: root["lm"] = ""; break;
case REALTIME_MODE_UDP: root["lm"] = F("UDP"); break;
case REALTIME_MODE_HYPERION: root["lm"] = F("Hyperion"); break;
case REALTIME_MODE_E131: root["lm"] = F("E1.31"); break;
case REALTIME_MODE_ADALIGHT: root["lm"] = F("USB Adalight/TPM2"); break;
case REALTIME_MODE_ARTNET: root["lm"] = F("Art-Net"); break;
case REALTIME_MODE_TPM2NET: root["lm"] = F("tpm2.net"); break;
case REALTIME_MODE_DDP: root["lm"] = F("DDP"); break;
case REALTIME_MODE_INACTIVE: root["lm"] = ""; break;
case REALTIME_MODE_GENERIC: root["lm"] = ""; break;
case REALTIME_MODE_UDP: root["lm"] = F("UDP"); break;
case REALTIME_MODE_HYPERION: root["lm"] = F("Hyperion"); break;
case REALTIME_MODE_E131: root["lm"] = F("E1.31"); break;
case REALTIME_MODE_ADALIGHT: root["lm"] = F("USB Adalight/TPM2"); break;
case REALTIME_MODE_ARTNET: root["lm"] = F("Art-Net"); break;
case REALTIME_MODE_TPM2NET: root["lm"] = F("tpm2.net"); break;
case REALTIME_MODE_DDP: root["lm"] = F("DDP"); break;
case REALTIME_MODE_TPM2RECORD: root["lm"] = F("TPM2 Recording (ROM)");
break;
}

if (realtimeIP[0] == 0)
Expand Down
292 changes: 292 additions & 0 deletions wled00/tpm2record.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,292 @@
#include "wled.h"

#ifdef WLED_USE_SD
#define USED_STORAGE_FILESYSTEMS "SD, LittleFS"
#include "SD_MMC.h"
#else
#define USED_STORAGE_FILESYSTEMS "LittleFS"
#endif

// This adds TPM2-file storing and playback capabilities to WLED.
//
// What does it mean:
// You can now store short recorded animations on the ESP32 (in the ROM: no SD required) with a connected LED stripe.
//
// How to transfer the animation:
// WLED offers a web file manager under <IP_OF_WLED>/edit here you can upload a recorded *.TPM2 file
//
// How to create a recording:
// You can record with tools like Jinx
//
// How to load the animation:
// You can specify a preset to playback this recording with the following API command
// {"tpm2":{"file":"/record.tpm2"}}
//
// You can specify a preset to playback this recording on a specific segment
// {"tpm2":{"file":"/record.tpm2", "seg":{"id":2}}
// {"tpm2":{"file":"/record.tpm2", "seg":2}
//
// How to trigger the animation:
// Presets can be triggered multiple interfaces e.g. via the json API, via the web interface or with a connected IR remote
//
// What next:
// - Playback from SD card is the next plan, here the length of the animation is less of a problem.
// - Playback and Recording of RGBW animations, as right now only RGB recordings are supported by WLED

// reference spec of TPM2: https://gist.github.com/jblang/89e24e2655be6c463c56
// - A packet contains any data of the TPM2 protocol, it
// starts with `TPM2_START` and ends with `TPM2_END`
// - A frame contains the visual data (the LEDs color's) of one moment

// --- CONSTANTS ---
#define TPM2_START 0xC9
#define TPM2_DATA_FRAME 0xDA
#define TPM2_COMMAND 0xC0
#define TPM2_END 0x36
#define TPM2_RESPONSE 0xAA

// infinite loop of animation
#define RECORDING_REPEAT_LOOP -1

// Default repeat count, when not specified by preset (-1=loop, 0=play once, 2=repeat two times)
#define RECORDING_REPEAT_DEFAULT 0

// --- Recording playback related ---
File recordingFile;
uint8_t colorData[4];
uint8_t colorChannels = 3;
uint32_t msFrameDelay = 33; // time between frames
int32_t recordingRepeats = RECORDING_REPEAT_LOOP;
unsigned long lastFrame = 0;
uint16_t playbackLedStart = 0; // first led to play animation on
uint16_t playbackLedStop = 0; // led after the last led to play animation on

// skips until a specific byte comes up
void skipUntil(uint8_t byteToStopAt)
{
uint8_t rb = 0;
do { rb = recordingFile.read(); }
while (recordingFile.available() && rb != byteToStopAt);
}

void skipUntilNextPacket() { skipUntil(TPM2_START); }

void skipUntilEndOfPacket() { skipUntil(TPM2_END); }

void getNextColorData(uint8_t data[])
{
data[0] = recordingFile.read();
data[1] = recordingFile.read();
data[2] = recordingFile.read();
data[3] = 0; // TODO add RGBW mode to TPM2
}

uint16_t getNextPacketLength()
{
if (!recordingFile.available()) { return 0; }
uint8_t highbyte_size = recordingFile.read();
uint8_t lowbyte_size = recordingFile.read();
uint16_t size = highbyte_size << 8 | lowbyte_size;
return size;
}

void processCommandData()
{
DEBUG_PRINTLN("processCommandData: not implemented yet");
skipUntilNextPacket();
}

void processResponseData()
{
DEBUG_PRINTLN("processResponseData: not implemented yet");
skipUntilNextPacket();
}

void processFrameData()
{
uint16_t packetLength = getNextPacketLength(); // opt-TODO maybe stretch recording to available leds
uint16_t lastLed = min(playbackLedStop, uint16_t(playbackLedStart + packetLength));

for (uint16_t i = playbackLedStart; i < lastLed; i++)
{
getNextColorData(colorData);
setRealtimePixel(i, colorData[0], colorData[1], colorData[2], colorData[3]);
}

skipUntilEndOfPacket();

strip.show();
// tell ui we are playing the recording right now
uint8_t mode = REALTIME_MODE_TPM2RECORD;
realtimeLock(realtimeTimeoutMs, mode);

lastFrame = millis();
}

void processUnknownData(uint8_t data)
{
DEBUG_PRINT("processUnknownData - received:");
DEBUG_PRINTLN(data);
skipUntilNextPacket();
}

void clearLastPlayback() {

for (uint16_t i = playbackLedStart; i < playbackLedStop; i++)
{
getNextColorData(colorData);
setRealtimePixel(i, 0,0,0,0);
}
}

bool stopBecauseAtTheEnd()
{
//If recording reached end loop or stop playback
if (!recordingFile.available())
{
if (recordingRepeats == RECORDING_REPEAT_LOOP)
{
recordingFile.seek(0); // go back the beginning of the recording
}
else if (recordingRepeats > 0)
{
recordingFile.seek(0); // go back the beginning of the recording
recordingRepeats--;
DEBUG_PRINT("Repeat recordind again for:");
DEBUG_PRINTLN(recordingRepeats);
}
else
{
uint8_t mode = REALTIME_MODE_INACTIVE;
realtimeLock(10, mode);
recordingFile.close();
clearLastPlayback();
return true;
}
}

return false;
}

// scan and forward until next frame was read (this will process commands)
void playNextRecordingFrame()
{
if(stopBecauseAtTheEnd()) return;

uint8_t rb = 0; // last read byte from file

// scan to next TPM2 packet start, should be the first attempt
do { rb = recordingFile.read(); }
while (recordingFile.available() && rb != TPM2_START);
if (!recordingFile.available()) { return; }

// process everything until (including) the next frame data
while(true)
{
rb = recordingFile.read();
if (rb == TPM2_COMMAND) processCommandData();
else if(rb == TPM2_RESPONSE) processResponseData();
else if(rb != TPM2_DATA_FRAME) processUnknownData(rb);
else {
processFrameData();
break;
}
}
}

void handlePlayRecording()
{
if (realtimeMode != REALTIME_MODE_TPM2RECORD) return;
if ( millis() - lastFrame < msFrameDelay) return;
playNextRecordingFrame();
}

void printWholeRecording()
{
while (recordingFile.available())
{
uint8_t rb = recordingFile.read();

switch (rb)
{
case 0xC9:
DEBUG_PRINTLN("");
DEBUG_PRINT(rb);
DEBUG_PRINT(" ");
break;
default:
{
DEBUG_PRINT(rb);
DEBUG_PRINT(" ");
}
}
}

recordingFile.close();
}

#ifdef WLED_USE_SD
//checks if the file is available on SD card
bool fileOnSD(const char *filepath)
{
if(!SD_MMC.begin()) return false; // mounting the card failed

uint8_t cardType = SD_MMC.cardType();
if(cardType == CARD_NONE) return false; // no SD card attached
if(cardType == CARD_MMC || cardType == CARD_SD || cardType == CARD_SDHC)
{
return SD_MMC.exists(filepath);
}

return false; // unknown card type
}
#endif

//checks if the file is available on LittleFS
bool fileOnFS(const char *filepath)
{
return WLED_FS.exists(filepath);
}

void loadRecording(const char *filepath, uint16_t startLed, uint16_t stopLed)
{
//close any potentially open file
if(recordingFile.available()) {
clearLastPlayback();
recordingFile.close();
}

playbackLedStart = startLed;
playbackLedStop = stopLed;

// No start/stop defined
if(playbackLedStart == uint16_t(-1) || playbackLedStop == uint16_t(-1)) {
WS2812FX::Segment sg = strip.getSegment(-1);
playbackLedStart = sg.start;
playbackLedStop = sg.stop;
}

DEBUG_PRINTF("TPM2 load animation on LED %d to %d\n", playbackLedStart, playbackLedStop);

#ifdef WLED_USE_SD
if(fileOnSD(filepath)){
DEBUG_PRINTF("Read file from SD: %s\n", filepath);
recordingFile = SD_MMC.open(filepath, "rb");
} else
#endif
if(fileOnFS(filepath)) {
DEBUG_PRINTF("Read file from FS: %s\n", filepath);
recordingFile = WLED_FS.open(filepath, "rb");
} else {
DEBUG_PRINTF("File %s not found (%s)\n", filepath, USED_STORAGE_FILESYSTEMS);
return;
}

if (realtimeOverride == REALTIME_OVERRIDE_ONCE)
{
realtimeOverride = REALTIME_OVERRIDE_NONE;
}

recordingRepeats = RECORDING_REPEAT_DEFAULT;
playNextRecordingFrame();
}
2 changes: 2 additions & 0 deletions wled00/wled.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -183,6 +183,8 @@ void WLED::loop()
else if (!noWifiSleep)
delay(1); //required to make sure ESP enters modem sleep (see #1184)
#endif
} else {
handlePlayRecording();
}
yield();
#ifdef ESP8266
Expand Down