kopia lustrzana https://github.com/Aircoookie/WLED
fix POV Display usermod (#4427)
* POV Display usermod this usermod adds a new effect called "POV Image". To get it to work: - read the README :) - upload a bmp image to the ESP filesystem using "/edit" url. - select "POV Image" effect. - set the filename (ie: "/myimage.bmp") as segment name. - rotate the segment at approximately 20 RPM. - enjoy the show! * improve file extension checks * improve README, remove PNGdec reference, clean usermod * restrain to esp32 platform + reduce memory footprint with mallocpull/4874/head^2
rodzic
d5d7fde30f
commit
da7f107273
|
@ -28,7 +28,6 @@ lib_deps = ${esp8266.lib_deps}
|
||||||
; robtillaart/SHT85@~0.3.3
|
; robtillaart/SHT85@~0.3.3
|
||||||
; ;gmag11/QuickESPNow @ ~0.7.0 # will also load QuickDebug
|
; ;gmag11/QuickESPNow @ ~0.7.0 # will also load QuickDebug
|
||||||
; https://github.com/blazoncek/QuickESPNow.git#optional-debug ;; exludes debug library
|
; https://github.com/blazoncek/QuickESPNow.git#optional-debug ;; exludes debug library
|
||||||
; bitbank2/PNGdec@^1.0.1 ;; used for POV display uncomment following
|
|
||||||
; ${esp32.AR_lib_deps} ;; needed for USERMOD_AUDIOREACTIVE
|
; ${esp32.AR_lib_deps} ;; needed for USERMOD_AUDIOREACTIVE
|
||||||
|
|
||||||
build_unflags = ${common.build_unflags}
|
build_unflags = ${common.build_unflags}
|
||||||
|
|
|
@ -0,0 +1,48 @@
|
||||||
|
## POV Display usermod
|
||||||
|
|
||||||
|
This usermod adds a new effect called “POV Image”.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
###How does it work?
|
||||||
|
With proper configuration (see below) the main segment will display a single row of pixels from an image stored on the ESP.
|
||||||
|
It displays the image row by row at a high refresh rate.
|
||||||
|
If you move the pixel segment at the right speed, you will see the full image floating in the air thanks to the persistence of vision.
|
||||||
|
RGB LEDs only (no RGBW), with grouping set to 1 and spacing set to 0.
|
||||||
|
Best results with high-density strips (e.g., 144 LEDs/m).
|
||||||
|
|
||||||
|
To get it working:
|
||||||
|
- Resize your image. The height must match the number of LEDs in your strip/segment.
|
||||||
|
- Rotate your image 90° clockwise (height becomes width).
|
||||||
|
- Upload a BMP image (24-bit, uncompressed) to the ESP filesystem using the “/edit” URL.
|
||||||
|
- Select the “POV Image” effect.
|
||||||
|
- Set the segment name to the absolute filesystem path of the image (e.g., “/myimage.bmp”).
|
||||||
|
- The path is case-sensitive and must start with “/”.
|
||||||
|
- Rotate the pixel strip at approximately 20 RPM.
|
||||||
|
- Tune as needed so that one full revolution maps to the image width (if the image appears stretched or compressed, adjust RPM slightly).
|
||||||
|
- Enjoy the show!
|
||||||
|
|
||||||
|
Notes:
|
||||||
|
- Only 24-bit uncompressed BMP files are supported.
|
||||||
|
- The image must fit into ~64 KB of RAM (width × height × 3 bytes, plus row padding to a 4-byte boundary).
|
||||||
|
- Examples (approximate, excluding row padding):
|
||||||
|
- 128×128 (49,152 bytes) fits.
|
||||||
|
- 160×160 (76,800 bytes) does NOT fit.
|
||||||
|
- 96×192 (55,296 bytes) fits; padding may add a small overhead.
|
||||||
|
- If the rendered image appears mirrored or upside‑down, rotate 90° the other way or flip horizontally in your editor and try again.
|
||||||
|
- The path must be absolute.
|
||||||
|
|
||||||
|
### Requirements
|
||||||
|
- 1D rotating LED strip/segment (POV setup). Ensure the segment length equals the number of physical LEDs.
|
||||||
|
- BMP image saved as 24‑bit, uncompressed (no alpha, no palette).
|
||||||
|
- Sufficient free RAM (~64 KB) for the image buffer.
|
||||||
|
|
||||||
|
### Troubleshooting
|
||||||
|
- Nothing displays: verify the file exists at the exact absolute path (case‑sensitive) and is a 24‑bit uncompressed BMP.
|
||||||
|
- Garbled colors or wrong orientation: re‑export as 24‑bit BMP and retry the rotation/flip guidance above.
|
||||||
|
- Image too large: reduce width and/or height until it fits within ~64 KB (see examples).
|
||||||
|
- Path issues: confirm you uploaded the file via the “/edit” URL and can see it in the filesystem browser.
|
||||||
|
|
||||||
|
### Safety
|
||||||
|
- Secure the rotating assembly and keep clear of moving parts.
|
||||||
|
- Balance the strip/hub to minimize vibration before running at speed.
|
|
@ -0,0 +1,146 @@
|
||||||
|
#include "bmpimage.h"
|
||||||
|
#define BUF_SIZE 64000
|
||||||
|
|
||||||
|
byte * _buffer = nullptr;
|
||||||
|
|
||||||
|
uint16_t read16(File &f) {
|
||||||
|
uint16_t result;
|
||||||
|
f.read((uint8_t *)&result,2);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
uint32_t read32(File &f) {
|
||||||
|
uint32_t result;
|
||||||
|
f.read((uint8_t *)&result,4);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool BMPimage::init(const char * fn) {
|
||||||
|
File bmpFile;
|
||||||
|
int bmpDepth;
|
||||||
|
//first, check if filename exists
|
||||||
|
if (!WLED_FS.exists(fn)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
bmpFile = WLED_FS.open(fn);
|
||||||
|
if (!bmpFile) {
|
||||||
|
_valid=false;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
//so, the file exists and is opened
|
||||||
|
// Parse BMP header
|
||||||
|
uint16_t header = read16(bmpFile);
|
||||||
|
if(header != 0x4D42) { // BMP signature
|
||||||
|
_valid=false;
|
||||||
|
bmpFile.close();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
//read and ingnore file size
|
||||||
|
read32(bmpFile);
|
||||||
|
(void)read32(bmpFile); // Read & ignore creator bytes
|
||||||
|
_imageOffset = read32(bmpFile); // Start of image data
|
||||||
|
// Read DIB header
|
||||||
|
read32(bmpFile);
|
||||||
|
_width = read32(bmpFile);
|
||||||
|
_height = read32(bmpFile);
|
||||||
|
if(read16(bmpFile) != 1) { // # planes -- must be '1'
|
||||||
|
_valid=false;
|
||||||
|
bmpFile.close();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
bmpDepth = read16(bmpFile); // bits per pixel
|
||||||
|
if((bmpDepth != 24) || (read32(bmpFile) != 0)) { // 0 = uncompressed {
|
||||||
|
_width=0;
|
||||||
|
_valid=false;
|
||||||
|
bmpFile.close();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
// If _height is negative, image is in top-down order.
|
||||||
|
// This is not canon but has been observed in the wild.
|
||||||
|
if(_height < 0) {
|
||||||
|
_height = -_height;
|
||||||
|
}
|
||||||
|
//now, we have successfully got all the basics
|
||||||
|
// BMP rows are padded (if needed) to 4-byte boundary
|
||||||
|
_rowSize = (_width * 3 + 3) & ~3;
|
||||||
|
//check image size - if it is too large, it will be unusable
|
||||||
|
if (_rowSize*_height>BUF_SIZE) {
|
||||||
|
_valid=false;
|
||||||
|
bmpFile.close();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
bmpFile.close();
|
||||||
|
// Ensure filename fits our buffer (segment name length constraint).
|
||||||
|
size_t len = strlen(fn);
|
||||||
|
if (len > WLED_MAX_SEGNAME_LEN) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
strncpy(filename, fn, sizeof(filename));
|
||||||
|
filename[sizeof(filename) - 1] = '\0';
|
||||||
|
_valid = true;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void BMPimage::clear(){
|
||||||
|
strcpy(filename, "");
|
||||||
|
_width=0;
|
||||||
|
_height=0;
|
||||||
|
_rowSize=0;
|
||||||
|
_imageOffset=0;
|
||||||
|
_loaded=false;
|
||||||
|
_valid=false;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool BMPimage::load(){
|
||||||
|
const size_t size = (size_t)_rowSize * (size_t)_height;
|
||||||
|
if (size > BUF_SIZE) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
File bmpFile = WLED_FS.open(filename);
|
||||||
|
if (!bmpFile) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (_buffer != nullptr) free(_buffer);
|
||||||
|
_buffer = (byte*)malloc(size);
|
||||||
|
if (_buffer == nullptr) return false;
|
||||||
|
|
||||||
|
bmpFile.seek(_imageOffset);
|
||||||
|
const size_t readBytes = bmpFile.read(_buffer, size);
|
||||||
|
bmpFile.close();
|
||||||
|
if (readBytes != size) {
|
||||||
|
_loaded = false;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
_loaded = true;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
byte* BMPimage::line(uint16_t n){
|
||||||
|
if (_loaded) {
|
||||||
|
return (_buffer+n*_rowSize);
|
||||||
|
} else {
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
uint32_t BMPimage::pixelColor(uint16_t x, uint16_t y){
|
||||||
|
uint32_t pos;
|
||||||
|
byte b,g,r; //colors
|
||||||
|
if (! _loaded) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
if ( (x>=_width) || (y>=_height) ) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
pos=y*_rowSize + 3*x;
|
||||||
|
//get colors. Note that in BMP files, they go in BGR order
|
||||||
|
b= _buffer[pos++];
|
||||||
|
g= _buffer[pos++];
|
||||||
|
r= _buffer[pos];
|
||||||
|
return (r<<16|g<<8|b);
|
||||||
|
}
|
|
@ -0,0 +1,50 @@
|
||||||
|
#ifndef _BMPIMAGE_H
|
||||||
|
#define _BMPIMAGE_H
|
||||||
|
#include "Arduino.h"
|
||||||
|
#include "wled.h"
|
||||||
|
|
||||||
|
/*
|
||||||
|
* This class describes a bitmap image. Each object refers to a bmp file on
|
||||||
|
* filesystem fatfs.
|
||||||
|
* To initialize, call init(), passign to it name of a bitmap file
|
||||||
|
* at the root of fatfs filesystem:
|
||||||
|
*
|
||||||
|
* BMPimage myImage;
|
||||||
|
* myImage.init("logo.bmp");
|
||||||
|
*
|
||||||
|
* For performance reasons, before actually usign the image, you need to load
|
||||||
|
* it from filesystem to RAM:
|
||||||
|
* myImage.load();
|
||||||
|
* All load() operations use the same reserved buffer in RAM, so you can only
|
||||||
|
* have one file loaded at a time. Before loading a new file, always unload the
|
||||||
|
* previous one:
|
||||||
|
* myImage.unload();
|
||||||
|
*/
|
||||||
|
|
||||||
|
class BMPimage {
|
||||||
|
public:
|
||||||
|
int height() {return _height; }
|
||||||
|
int width() {return _width; }
|
||||||
|
int rowSize() {return _rowSize;}
|
||||||
|
bool isLoaded() {return _loaded; }
|
||||||
|
bool load();
|
||||||
|
void unload() {_loaded=false; }
|
||||||
|
byte * line(uint16_t n);
|
||||||
|
uint32_t pixelColor(uint16_t x,uint16_t y);
|
||||||
|
bool init(const char* fn);
|
||||||
|
void clear();
|
||||||
|
char * getFilename() {return filename;};
|
||||||
|
|
||||||
|
private:
|
||||||
|
char filename[WLED_MAX_SEGNAME_LEN+1]="";
|
||||||
|
int _width=0;
|
||||||
|
int _height=0;
|
||||||
|
int _rowSize=0;
|
||||||
|
int _imageOffset=0;
|
||||||
|
bool _loaded=false;
|
||||||
|
bool _valid=false;
|
||||||
|
};
|
||||||
|
|
||||||
|
extern byte * _buffer;
|
||||||
|
|
||||||
|
#endif
|
|
@ -1,7 +1,5 @@
|
||||||
{
|
{
|
||||||
"name:": "pov_display",
|
"name:": "pov_display",
|
||||||
"build": { "libArchive": false},
|
"build": { "libArchive": false},
|
||||||
"dependencies": {
|
"platforms": ["espressif32"]
|
||||||
"bitbank2/PNGdec":"^1.0.3"
|
|
||||||
}
|
|
||||||
}
|
}
|
|
@ -0,0 +1,47 @@
|
||||||
|
#include "pov.h"
|
||||||
|
|
||||||
|
POV::POV() {}
|
||||||
|
|
||||||
|
void POV::showLine(const byte * line, uint16_t size){
|
||||||
|
uint16_t i, pos;
|
||||||
|
uint8_t r, g, b;
|
||||||
|
if (!line) {
|
||||||
|
// All-black frame on null input
|
||||||
|
for (i = 0; i < SEGLEN; i++) {
|
||||||
|
SEGMENT.setPixelColor(i, CRGB::Black);
|
||||||
|
}
|
||||||
|
strip.show();
|
||||||
|
lastLineUpdate = micros();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
for (i = 0; i < SEGLEN; i++) {
|
||||||
|
if (i < size) {
|
||||||
|
pos = 3 * i;
|
||||||
|
// using bgr order
|
||||||
|
b = line[pos++];
|
||||||
|
g = line[pos++];
|
||||||
|
r = line[pos];
|
||||||
|
SEGMENT.setPixelColor(i, CRGB(r, g, b));
|
||||||
|
} else {
|
||||||
|
SEGMENT.setPixelColor(i, CRGB::Black);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
strip.show();
|
||||||
|
lastLineUpdate = micros();
|
||||||
|
}
|
||||||
|
|
||||||
|
bool POV::loadImage(const char * filename){
|
||||||
|
if(!image.init(filename)) return false;
|
||||||
|
if(!image.load()) return false;
|
||||||
|
currentLine=0;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
int16_t POV::showNextLine(){
|
||||||
|
if (!image.isLoaded()) return 0;
|
||||||
|
//move to next line
|
||||||
|
showLine(image.line(currentLine), image.width());
|
||||||
|
currentLine++;
|
||||||
|
if (currentLine == image.height()) {currentLine=0;}
|
||||||
|
return currentLine;
|
||||||
|
}
|
|
@ -0,0 +1,42 @@
|
||||||
|
#ifndef _POV_H
|
||||||
|
#define _POV_H
|
||||||
|
#include "bmpimage.h"
|
||||||
|
|
||||||
|
|
||||||
|
class POV {
|
||||||
|
public:
|
||||||
|
POV();
|
||||||
|
|
||||||
|
/* Shows one line. line should be pointer to array which holds pixel colors
|
||||||
|
* (3 bytes per pixel, in BGR order). Note: 3, not 4!!!
|
||||||
|
* size should be size of array (number of pixels, not number of bytes)
|
||||||
|
*/
|
||||||
|
void showLine(const byte * line, uint16_t size);
|
||||||
|
|
||||||
|
/* Reads from file an image and making it current image */
|
||||||
|
bool loadImage(const char * filename);
|
||||||
|
|
||||||
|
/* Show next line of active image
|
||||||
|
Retunrs the index of next line to be shown (not yet shown!)
|
||||||
|
If it retunrs 0, it means we have completed showing the image and
|
||||||
|
next call will start again
|
||||||
|
*/
|
||||||
|
int16_t showNextLine();
|
||||||
|
|
||||||
|
//time since strip was last updated, in micro sec
|
||||||
|
uint32_t timeSinceUpdate() {return (micros()-lastLineUpdate);}
|
||||||
|
|
||||||
|
|
||||||
|
BMPimage * currentImage() {return ℑ}
|
||||||
|
|
||||||
|
char * getFilename() {return image.getFilename();}
|
||||||
|
|
||||||
|
private:
|
||||||
|
BMPimage image;
|
||||||
|
int16_t currentLine=0; //next line to be shown
|
||||||
|
uint32_t lastLineUpdate=0; //time in microseconds
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
#endif
|
|
@ -1,88 +1,75 @@
|
||||||
#include "wled.h"
|
#include "wled.h"
|
||||||
#include <PNGdec.h>
|
#include "pov.h"
|
||||||
|
|
||||||
void * openFile(const char *filename, int32_t *size) {
|
static const char _data_FX_MODE_POV_IMAGE[] PROGMEM = "POV Image@!;;;;";
|
||||||
f = WLED_FS.open(filename);
|
|
||||||
*size = f.size();
|
|
||||||
return &f;
|
|
||||||
}
|
|
||||||
|
|
||||||
void closeFile(void *handle) {
|
static POV s_pov;
|
||||||
if (f) f.close();
|
|
||||||
}
|
|
||||||
|
|
||||||
int32_t readFile(PNGFILE *pFile, uint8_t *pBuf, int32_t iLen)
|
|
||||||
{
|
|
||||||
int32_t iBytesRead;
|
|
||||||
iBytesRead = iLen;
|
|
||||||
File *f = static_cast<File *>(pFile->fHandle);
|
|
||||||
// Note: If you read a file all the way to the last byte, seek() stops working
|
|
||||||
if ((pFile->iSize - pFile->iPos) < iLen)
|
|
||||||
iBytesRead = pFile->iSize - pFile->iPos - 1; // <-- ugly work-around
|
|
||||||
if (iBytesRead <= 0)
|
|
||||||
return 0;
|
|
||||||
iBytesRead = (int32_t)f->read(pBuf, iBytesRead);
|
|
||||||
pFile->iPos = f->position();
|
|
||||||
return iBytesRead;
|
|
||||||
}
|
|
||||||
|
|
||||||
int32_t seekFile(PNGFILE *pFile, int32_t iPosition)
|
|
||||||
{
|
|
||||||
int i = micros();
|
|
||||||
File *f = static_cast<File *>(pFile->fHandle);
|
|
||||||
f->seek(iPosition);
|
|
||||||
pFile->iPos = (int32_t)f->position();
|
|
||||||
i = micros() - i;
|
|
||||||
return pFile->iPos;
|
|
||||||
}
|
|
||||||
|
|
||||||
void draw(PNGDRAW *pDraw) {
|
|
||||||
uint16_t usPixels[SEGLEN];
|
|
||||||
png.getLineAsRGB565(pDraw, usPixels, PNG_RGB565_LITTLE_ENDIAN, 0xffffffff);
|
|
||||||
for(int x=0; x < SEGLEN; x++) {
|
|
||||||
uint16_t color = usPixels[x];
|
|
||||||
byte r = ((color >> 11) & 0x1F);
|
|
||||||
byte g = ((color >> 5) & 0x3F);
|
|
||||||
byte b = (color & 0x1F);
|
|
||||||
SEGMENT.setPixelColor(x, RGBW32(r,g,b,0));
|
|
||||||
}
|
|
||||||
strip.show();
|
|
||||||
}
|
|
||||||
|
|
||||||
uint16_t mode_pov_image(void) {
|
uint16_t mode_pov_image(void) {
|
||||||
const char * filepath = SEGMENT.name;
|
Segment& mainseg = strip.getMainSegment();
|
||||||
int rc = png.open(filepath, openFile, closeFile, readFile, seekFile, draw);
|
const char* segName = mainseg.name;
|
||||||
if (rc == PNG_SUCCESS) {
|
if (!segName) {
|
||||||
rc = png.decode(NULL, 0);
|
return FRAMETIME;
|
||||||
png.close();
|
}
|
||||||
return FRAMETIME;
|
// Only proceed for files ending with .bmp (case-insensitive)
|
||||||
}
|
size_t segLen = strlen(segName);
|
||||||
|
if (segLen < 4) return FRAMETIME;
|
||||||
|
const char* ext = segName + (segLen - 4);
|
||||||
|
// compare case-insensitive to ".bmp"
|
||||||
|
if (!((ext[0]=='.') &&
|
||||||
|
(ext[1]=='b' || ext[1]=='B') &&
|
||||||
|
(ext[2]=='m' || ext[2]=='M') &&
|
||||||
|
(ext[3]=='p' || ext[3]=='P'))) {
|
||||||
return FRAMETIME;
|
return FRAMETIME;
|
||||||
|
}
|
||||||
|
|
||||||
|
const char* current = s_pov.getFilename();
|
||||||
|
if (current && strcmp(segName, current) == 0) {
|
||||||
|
s_pov.showNextLine();
|
||||||
|
return FRAMETIME;
|
||||||
|
}
|
||||||
|
|
||||||
|
static unsigned long s_lastLoadAttemptMs = 0;
|
||||||
|
unsigned long nowMs = millis();
|
||||||
|
// Retry at most twice per second if the image is not yet loaded.
|
||||||
|
if (nowMs - s_lastLoadAttemptMs < 500) return FRAMETIME;
|
||||||
|
s_lastLoadAttemptMs = nowMs;
|
||||||
|
s_pov.loadImage(segName);
|
||||||
|
return FRAMETIME;
|
||||||
}
|
}
|
||||||
|
|
||||||
class PovDisplayUsermod : public Usermod
|
class PovDisplayUsermod : public Usermod {
|
||||||
{
|
protected:
|
||||||
public:
|
bool enabled = false; //WLEDMM
|
||||||
static const char _data_FX_MODE_POV_IMAGE[] PROGMEM = "POV Image@!;;;1";
|
const char *_name; //WLEDMM
|
||||||
|
bool initDone = false; //WLEDMM
|
||||||
|
unsigned long lastTime = 0; //WLEDMM
|
||||||
|
public:
|
||||||
|
|
||||||
PNG png;
|
PovDisplayUsermod(const char *name, bool enabled)
|
||||||
File f;
|
: enabled(enabled) , _name(name) {}
|
||||||
|
|
||||||
|
void setup() override {
|
||||||
|
strip.addEffect(255, &mode_pov_image, _data_FX_MODE_POV_IMAGE);
|
||||||
|
//initDone removed (unused)
|
||||||
|
}
|
||||||
|
|
||||||
void setup() {
|
|
||||||
strip.addEffect(255, &mode_pov_image, _data_FX_MODE_POV_IMAGE);
|
void loop() override {
|
||||||
|
// if usermod is disabled or called during strip updating just exit
|
||||||
|
// NOTE: on very long strips strip.isUpdating() may always return true so update accordingly
|
||||||
|
if (!enabled || strip.isUpdating()) return;
|
||||||
|
|
||||||
|
// do your magic here
|
||||||
|
if (millis() - lastTime > 1000) {
|
||||||
|
lastTime = millis();
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
void loop() {
|
uint16_t getId() override {
|
||||||
}
|
return USERMOD_ID_POV_DISPLAY;
|
||||||
|
}
|
||||||
uint16_t getId()
|
|
||||||
{
|
|
||||||
return USERMOD_ID_POV_DISPLAY;
|
|
||||||
}
|
|
||||||
|
|
||||||
void connected() {}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
static PovDisplayUsermod pov_display("POV Display", false);
|
||||||
static PovDisplayUsermod pov_display;
|
REGISTER_USERMOD(pov_display);
|
||||||
REGISTER_USERMOD(pov_display);
|
|
||||||
|
|
Plik binarny nie jest wyświetlany.
Po Szerokość: | Wysokość: | Rozmiar: 988 KiB |
Ładowanie…
Reference in New Issue