Captive Portal Blog Teil 4 : BMP Dateienanzeige auf 8x8 Matrix Display - AZ-Delivery

Hola y bienvenidos a otra parte de la serie ESP Captive Portal.

 En la parte de hoy, estamos expandiendo nuestro portal cautivo con un servidor de archivos integrado por otra aplicación de seguimiento muy interesante: ¡Conectamos una matriz WS1812 de 8x8 al ESP32 y mostramos archivos BMP de 8x8 píxeles almacenados en el servidor de archivos! Actualmente hay una limitación de que el archivo BMP debe tener exactamente 8x8 píxeles para poder visualizarse. Todos los demás formatos o tamaños no se ofrecen para la selección de pantallas. La selección de qué archivo BMP se mostrará se realiza en el sitio web principal:

En primer lugarsi  tenemos que agregar una pantalla de matriz de puntos de 8x8 al hardware y conectarlo. La estructura es muy simple gracias al bus de un solo cable de los LED WS2812:

*** Tenga en cuenta también las instrucciones de seguridad de nuestro libro electrónico para la puesta en marcha de la matriz de 8x8 ***

Subimos el código extendido al ESP 32:

 

#include <WiFi.h>
#include <WiFiClient.h> 
#include <Servidor web.h>
#include <ESPmDNS.h>
#include <SPIFFS.h>
#include <Servidor DNS.h>
#include <EEPROM.h>
#include <FastLED.h>

#definir GPIO_OUT_W1TS_REG (DR_REG_GPIO_BASE + 0x0008)
#definir GPIO_OUT_W1TC_REG (DR_REG_GPIO_BASE + 0x000c)

#definir LED_PIN  17
#definir COLOR_ORDER GRB
#definir CHIPSET     WS2812

estática const byte WiFiPwdLen = 25;
estática const byte APSTANAMELEN = 20;

estructura WiFiEEPromData   {     bool APSTA = cierto; // Punto de acceso o modo de estación - modo AP verdadero     bool PwDReq = falso; // Contraseña requerida     bool CapPortal = cierto ; // CaptivePortal activado en modo AP     char APSTAName[APSTANAMELEN]; // ESTACIÓN / AP Nombre del punto PARA CONECTAR, si está definido     char WiFiPwd[WiFiPwdLen]; // WiFiPAssword, si está definido     char ConfigValid[3]; // Si Config es Vaild, se requiere la etiqueta "TK" "   };

estructura BMPHeader // BitMapStucture   {     uint32_t fileSize;  //       uint32_t creatorBytes; //       uint32_t imageOffset; // Inicio de datos de imagen "Desplazamiento de imagen:      uint32_t headerSize;     //       uint32_t ancho;     uint32_t altura;     uint16_t aviones;     uint16_t profundidad; // bits por píxel     uint32_t formatear;     };

/ * nombre de host para mDNS. Debería funcionar al menos en Windows. Prueba http://esp8266.local */
const char *ESPHostname = "ESP32";

// servidor DNS
const byte DNS_PORT = 53;
Servidor DNS dnsServer;

// Conmmon Paramenters
bool SoftAccOK  = falso;

// servidor web
Servidor web servidor(80);

/ * Parámetros de red de Soft AP * /
IPAddress apIP(172, 20, 0, 1);
IPAddress netMsk(255, 255, 255, 0);

sin firmar largo currentMillis = 0;
sin firmar largo startMillis;

/ ** Estado actual de la WLAN * /
corta estado = WL_IDLE_STATUS;

Archivo fsUploadFile;              // un objeto File para almacenar temporalmente el archivo recibido
WiFiEEPromData MyWiFiConfig;
Cadena getContentType(Cadena nombre de archivo); // convierte la extensión del archivo al tipo MIME
bool handleFileRead(Cadena camino);       // envía el archivo correcto al cliente (si existe)
nulo handleFileUpload();                // cargar un nuevo archivo a SPIFFS
Cadena temp ="";                 byte Brillo = 100;              // PresetBrightness
// Parámetros para LED
const uint8_t kMatrixWidth = 8;
const uint8_t kMatrixHeight = 8;
// const bool kMatrixSerpentineLayout = false;

#definir NUM_LEDS (kMatrixWidth * kMatrixHeight)
CRGB leds_plus_safety_pixel[ NUM_LEDS + 1];
CRGB* const leds( leds_plus_safety_pixel + 1);

nulo configuración() 
{   REG_WRITE(GPIO_OUT_W1TS_REG, BIT(GPIO_NUM_16));     // Conjunto de corrección de errores de meditación Guru   retrasar(1);   REG_WRITE(GPIO_OUT_W1TC_REG, BIT(GPIO_NUM_16));     // Solución de error de meditación Guru claro   bool ConnectSuccess = falso;   bool CreateSoftAPSucc  = falso;   bool Sistema CInitFS  = falso;   bool CInitHTTPServer  = falso;   byte Len;    De serie.comenzar(9600);    mientras que (!De serie) {     ; // espera a que se conecte el puerto serie. Necesario para USB nativo   }   De serie.println(F("Interfaz serie inicializada a 9600 baudios".));    FastLED.addLeds<CHIPSET, LED_PIN, COLOR_ORDER>(leds, NUM_LEDS).setCorrection(Típico SMD5050);   FastLED.conjuntoBrillo(BRILLO);   FastLED.espectáculo();   Wifi.setAutoReconnect (falso);   Wifi.persistente(falso);   Wifi.desconectar();    Wifi.setHostname(ESPHostname); // Establece el nombre de host DHCP asignado a la estación ESP.   Si (loadCredentials()) // Cargue las credenciales de WLAN para la configuración de WiFi   {       De serie.println(F("Credenciales válidas encontradas".));         Si (MyWiFiConfig.APSTA == cierto)  // Modo AP       {          De serie.println(F("Modo de punto de acceso seleccionado".));          Len = Strlen(MyWiFiConfig.APSTAName);         MyWiFiConfig.APSTAName[Len+1] = '\0';          Len = Strlen(MyWiFiConfig.WiFiPwd);         MyWiFiConfig.WiFiPwd[Len+1] = '\0';           CreateSoftAPSucc = CreateWifiSoftAP();       } más       {         De serie.println(F("Modo de estación seleccionado".));                Len = Strlen(MyWiFiConfig.APSTAName);         MyWiFiConfig.APSTAName[Len+1] = '\0';          Len = Strlen(MyWiFiConfig.WiFiPwd);         MyWiFiConfig.WiFiPwd[Len+1] = '\0';         Len = ConnectWifiAP();              Si ( Len == 3 ) { ConnectSuccess = cierto; } más { ConnectSuccess = falso; }            }   } más   { // Establecer configuración predeterminada - Crear AP      De serie.println(F("NO se encontraron credenciales válidas".));       SetDefaultWiFiConfig ();      CreateSoftAPSucc = CreateWifiSoftAP();       guardar credenciales();      // Parpadeo      retrasar(500);      }       // Inicializar sistema de archivos   CInitFSSystem = InitalizeFileSystem();    Si (!(CInitFSSystem)) {De serie.println(F("¡Sistema de archivos no inicializado!")); }    Si ((ConnectSuccess o CreateSoftAPSucc))     {                De serie.impresión (F("Dirección IP: "));       Si (CreateSoftAPSucc) { De serie.println(Wifi.softAPIP());}          Si (ConnectSuccess) { De serie.println(Wifi.localIP());}       InitalizeHTTPServer();          }     más     {       De serie.setDebugOutput(cierto); // Salida de depuración para WLAN en la interfaz en serie.       De serie.println(F("Error: no se puede conectar a la WLAN. Establezca la configuración POR DEFECTO".));       SetDefaultWiFiConfig();       CreateSoftAPSucc = CreateWifiSoftAP();       InitalizeHTTPServer();         SetDefaultWiFiConfig();       guardar credenciales();        }    para ( En t yo = 0; yo < NUM_LEDS; yo++) // Pantalla LED clara     {           leds[yo]=  0x000000;             }   FastLED.espectáculo();   // Borrar pantalla :)
}

vacío InitalizeHTTPServer() 
 {   bool initok = falso;   / * Configurar páginas web: raíz, páginas de configuración wifi, detectores de portal cautivo SO y no encontrado. * /   servidor.en("/", manejarRoot);   servidor.en("/Wifi", handleWifi);   servidor.en("/ sistema de archivos", HTTP_GET,handleDisplayFS);   servidor.en("/subir", HTTP_POST, []() {   servidor.enviar(200, "Texto sin formato", "");   }, handleFileUpload);    // if (MyWiFiConfig.CapPortal) {server.on ("/ generate_204", handleRoot); } // Portal cautivo de Android. Quizás no sea necesario. Puede ser manejado por el controlador notFound.   // if (MyWiFiConfig.CapPortal) {server.on ("/ favicon.ico", handleRoot); } // Otro portal cautivo de Android. Quizás no sea necesario. Puede ser manejado por el controlador notFound. Comprobado en Sony Handy   // if (MyWiFiConfig.CapPortal) {server.on ("/ fwlink", handleRoot); } // portal cautivo de Microsoft. Quizás no sea necesario. Puede ser manejado por el controlador notFound.   servidor.en("/ generate_204", manejarRoot);  // Portal cautivo de Android. Quizás no sea necesario. Puede ser manejado por el controlador notFound.   servidor.en("/favicon.ico", manejarRoot);    // Otro portal cautivo de Android. Quizás no sea necesario. Puede ser manejado por el controlador notFound. Comprobado en Sony Handy   servidor.en("/ fwlink", manejarRoot);   // portal cautivo de Microsoft. Quizás no sea necesario. Puede ser manejado por el controlador notFound.    servidor.onNotFound ( handleNotFound );   servidor.empezar(); // Inicio del servidor web
 }

booleano InitalizeFileSystem() {   bool initok = falso;   initok = SPIFFS.empezar();   retrasar(200);   Si (!(initok))   {     De serie.println(F("Formatear SPIFFS"));     SPIFFS.formato();     initok = SPIFFS.empezar();   }   regreso initok;
}

booleano CreateWifiSoftAP() 
{   Wifi.desconectar();   De serie.impresión(F("Inicializar SoftAP"));   Si (MyWiFiConfig.PwDReq)      {       SoftAccOK  =  Wifi.softAP(MyWiFiConfig.APSTAName, MyWiFiConfig.WiFiPwd); // Passwortlänge mindestens 8 Zeichen!     } más     {       SoftAccOK  =  Wifi.softAP(MyWiFiConfig.APSTAName); // Punto de acceso SIN contraseña       // Función de sobrecarga :; WiFi.softAP (ssid, contraseña, canal, oculto)     }   retrasar(2000); // Sin demora he visto la dirección IP en blanco   Wifi.softAPConfig(apIP, apIP, netMsk);   Si (SoftAccOK)   {    / * Configurar el servidor DNS redirigiendo todos los dominios a la apIP * /     servidor DNS.setErrorReplyCode(DNSReplyCode::No hay error);   servidor DNS.comienzo(DNS_PORT, "*", apIP);   De serie.println(F("exitoso."));   } más   {   De serie.println(F("Error de AP suave".));   De serie.println(MyWiFiConfig.APSTAName);   De serie.println(MyWiFiConfig.WiFiPwd);   }   regreso SoftAccOK;
}

byte ConnectWifiAP() 
{   De serie.println(F("Inicializando el Cliente Wifi".));     byte connRes = 0;   byte yo = 0;   Wifi.desconectar();   Wifi.softAPdisconnect(cierto); // La función establecerá el SSID y la contraseña configurados actualmente del soft-AP en valores nulos. El parámetro es opcional. Si se establece en verdadero, desactivará el modo soft-AP.   retrasar(500);   Wifi.empezar(MyWiFiConfig.APSTAName, MyWiFiConfig.WiFiPwd);   connRes  = Wifi.waitForConnectResult();   mientras (( connRes == 0 ) y (yo != 10))  // if connRes == 0 "IDLE_STATUS - cambiar Statius"     {        connRes  = Wifi.waitForConnectResult();       retrasar(2000);       yo++;       De serie.impresión(F("."));       // declaración (s)     }   mientras (( connRes == 1 ) y (yo != 10))  // if connRes == 1 NO_SSID_AVAILin - SSID no puede ser alcanzado     {        connRes  = Wifi.waitForConnectResult();       retrasar(2000);       yo++;       De serie.impresión(F("."));       // declaración (s)     }     Si (connRes == 3 ) {                          Wifi.setAutoReconnect(cierto); // Establezca si el módulo intentará volver a conectarse a un punto de acceso en caso de que esté desconectado.                         // Configurar el respondedor MDNS                             Si (!MDNS.empezar(ESPHostname)) {                                 De serie.println(F("Error: MDNS"));                                 } más { MDNS.addService("http", "tcp", 80); }                      }   mientras (( connRes == 4 ) y (yo != 10))  // if connRes == 4 Contraseña incorrecta. A veces sucede esto con la PCD correcta     {        Wifi.empezar(MyWiFiConfig.APSTAName, MyWiFiConfig.WiFiPwd);        connRes = Wifi.waitForConnectResult();       retrasar(2000);       yo++;       De serie.impresión(F("."));                      }   Si (connRes == 4 ) {                           De serie.println(F("STA Pwd Err"));                                              De serie.println(MyWiFiConfig.APSTAName);                         De serie.println(MyWiFiConfig.WiFiPwd);                          Wifi.desconectar();                       }
De serie.println(F(""));
regreso connRes;
}

uint16_t leer16(Archivo F)
{   // Los datos BMP se almacenan little-endian, igual que Arduino.   uint16_t resultado;   ((uint8_t *)&resultado)[0] = F.leer(); // LSB   ((uint8_t *)&resultado)[1] = F.leer(); // MSB   regreso resultado;
}

uint32_t leer32(Archivo F)
{   // Los datos BMP se almacenan little-endian, igual que Arduino.   uint32_t resultado;   ((uint8_t *)&resultado)[0] = F.leer(); // LSB   ((uint8_t *)&resultado)[1] = F.leer();   ((uint8_t *)&resultado)[2] = F.leer();   ((uint8_t *)&resultado)[3] = F.leer(); // MSB   regreso resultado;
}

BMPHeader ReadBitmapSpecs(Cuerda nombre del archivo)
{   Archivo archivo;   BMPHeader BMPData;   archivo =SPIFFS.abierto(nombre del archivo, "r");   Si (!archivo)   {     archivo.cerca();     regreso BMPData;   }   // Parse BMP header   Si (leer16(archivo) == 0x4D42) // firma BMP   {     BMPData.tamaño del archivo = leer32(archivo);     BMPData.creatorBytes = leer32(archivo);     BMPData.imageOffset = leer32(archivo); // Inicio de datos de imagen     BMPData.headerSize = leer32(archivo);     BMPData.anchura  = leer32(archivo);     BMPData.altura = leer32(archivo);     BMPData.aviones = leer16(archivo);     BMPData.profundidad = leer16(archivo); // bits por píxel     BMPData.formato = leer32(archivo);   }
archivo.cerca();
regreso BMPData;
}

#definir SD_BUFFER_PIXELS 20

vacío drawBitmap_SPIFFS(Cuerda nombre del archivo, uint8_t X, uint8_t y)
{   Archivo archivo;   uint8_t buffer[3 * SD_BUFFER_PIXELS]; // buffer de píxeles, tamaño para r, g, b   bool válido = falso; // formato válido para ser manejado   bool dar la vuelta = cierto; // mapa de bits se almacena de abajo hacia arriba   uint32_t pos = 0;   archivo =SPIFFS.abierto(nombre del archivo, "r");   Si (!archivo)   {     De serie.impresión(F("Error de sistema de archivos"));     regreso;   }   // Parse BMP header   Si (leer16(archivo) == 0x4D42) // firma BMP   {     uint32_t tamaño del archivo = leer32(archivo);     uint32_t creatorBytes = leer32(archivo);     uint32_t imageOffset = leer32(archivo); // Inicio de datos de imagen     uint32_t headerSize = leer32(archivo);     uint32_t anchura  = leer32(archivo);     uint32_t altura = leer32(archivo);     uint16_t aviones = leer16(archivo);     uint16_t profundidad = leer16(archivo); // bits por píxel     uint32_t formato = leer32(archivo);     Si ((aviones == 1) && (formato == 0)) // se maneja sin comprimir     {       De serie.impresión(F("Tamaño del archivo: "));        De serie.println(tamaño del archivo);       De serie.impresión(F("Desplazamiento de imagen:"));        De serie.println(imageOffset);       De serie.impresión(F("Tamaño del encabezado:"));        De serie.println(headerSize);       De serie.impresión(F("Profundidad de bits: "));        De serie.println(profundidad);       De serie.impresión(F("Tamaño de la imagen: "));       De serie.impresión(anchura);       De serie.impresión('X');       De serie.println(altura);       uint32_t rowSize = (anchura * profundidad / 8 + 3) & ~3;       Si (altura < 0)       {         altura = -altura;         dar la vuelta = falso;       }       uint16_t w = anchura;       uint16_t h = altura;       tamaño_t buffidx = tamaño de(buffer); // forzar la carga del búfer       para (uint16_t fila = 0; fila < h; fila++) // para cada línea       {         Si (dar la vuelta) // El mapa de bits se almacena de abajo hacia arriba (BMP normal)           pos = imageOffset + (altura - 1 - fila) * rowSize;         más     // El mapa de bits se almacena de arriba a abajo           pos = imageOffset + fila * rowSize;         Si (archivo.posición() != pos)         { // ¿Necesitas buscar?           archivo.buscar(pos,SeekSet);  // si el modo es SeekSet, la posición se establece para compensar los bytes desde el principio.                                     // si el modo es SeekCur, la posición actual se mueve por bytes de desplazamiento.                                     // si el modo es SeekEnd, la posición se establece para compensar bytes desde el final del            buffidx = tamaño de(buffer); // forzar la recarga del búfer         }         uint8_t pedacitos;         para (uint16_t columna = 0; columna < w; columna++) // por cada píxel         {           // ¿Es hora de leer más datos de píxeles?           Si (buffidx >= tamaño de(buffer))           {             archivo.leer(buffer, tamaño de(buffer));             buffidx = 0; // Establecer índice al principio           }           cambiar (profundidad)           {             caso 1: // un bit por píxel en formato b / n               {                 válido = cierto;                 Si (0 == columna % 8)                 {                   pedacitos = buffer[buffidx++];                 }                 uint16_t bw_color = pedacitos & 0x80;                               uint16_t PixelNum = (fila*8)+columna;                 leds[PixelNum].rojo  = bw_color;                 leds[PixelNum].verde = bw_color;                 leds[PixelNum].azul  = bw_color;                 pedacitos <<= 1;               }               descanso;             caso 24: // formato BMP estándar               {                 válido = cierto;                 uint16_t si = buffer[buffidx++];                 uint16_t sol = buffer[buffidx++];                 uint16_t r = buffer[buffidx++];                 uint16_t PixelNum = (fila*8)+columna;                 leds[PixelNum].rojo  = r;                 leds[PixelNum].verde  = sol;                 leds[PixelNum].azul  = si;               }               descanso;           }         } // final de píxel       } // línea final      FastLED.espectáculo();   // Mostrar resultados :)     }   }     archivo.cerca();   Si (!(válido))   {     De serie.println(F("Err: BMP"));   }
}

vacío handleFileUpload() {    Si (servidor.uri() != "/subir") regreso;    HTTPUpload& subir = servidor.subir();    Si (subir.estado == UPLOAD_FILE_START) {      Cuerda nombre del archivo = subir.nombre del archivo;      Si (subir.nombre del archivo.longitud() > 30) {       subir.nombre del archivo = subir.nombre del archivo.subcadena(subir.nombre del archivo.longitud() - 30, subir.nombre del archivo.longitud());  // Dateinamen auf 30 Zeichen kürzen     }      De serie.println("Nombre de FileUpload:" + subir.nombre del archivo);        Si (!nombre del archivo.comienza con("/")) nombre del archivo = "/" + nombre del archivo;       fsUploadFile = SPIFFS.abierto("/" + servidor.urlDecode(subir.nombre del archivo), "w");      nombre del archivo = Cuerda();    } más Si (subir.estado == UPLOAD_FILE_WRITE) {      Si (fsUploadFile)        fsUploadFile.escribir(subir.buf, subir.tamaño actual);    } más Si (subir.estado == UPLOAD_FILE_END) {      Si (fsUploadFile)        fsUploadFile.cerca();      handleDisplayFS();    }
 }

vacío handleDisplayFS() {                     Sistema de archivos // HTML   // Página: /filesystem   temp ="";   // HTML Header   servidor.sendHeader("Cache-Control", "sin caché, sin tienda, debe-revalidar");   servidor.sendHeader("Pragma", "no-cache");   servidor.sendHeader("Caduca", "-1");   servidor.setContentLength(CONTENT_LENGTH_UNKNOWN);   Contenido // HTML   servidor.enviar ( 200, "text/html", temp );    temp += "head>";   servidor.enviarContenido(temp);   temp = "";   temp += "title>File System Manager";   temp += "

Serial Peripheral Interface Flash Filesystem

"
;  servidor.enviarContenido(temp);  temp = "";  si (servidor.args() > 0) // Parámetro wurden ubergeben    {      si (servidor.hasArg("delete"))          {              Cadena FToDel = servidor.arg("delete");          si (SPIFFS.existe(FToDel))              {              SPIFFS.eliminar(FToDel);                temp += "Archivo" + FToDel + "Suprimido con éxito.";            } de lo contrario            {              temp += "Archivo" + FToDel + "no se puede eliminar.";            }          servidor.enviarContenido(temp);          temp = "";        }      si (servidor.hasArg("format") y servidor.arg("on"))          {             SPIFFS.formato();           temp += "Sistema de archivos SPI con formato exitoso".;           servidor.enviarContenido(temp);           temp = "";        } // server.client().stop(); // Stop es necesario porque no enviamos longitud de contenido    }  temp += ">Current SPIFFS Status: ";    temp+=formatBytes(SPIFFS.usedBytes()*1.05)+" de "+formatBytes(SPIFFS.totalBytes())+" used.
"
;  temp+=formatBytes((SPIFFS.totalBytes()-(SPIFFS.usedBytes()*1.05)))+"free.
"
;  temp+="

"
;  servidor.enviarContenido(temp);  temp = "";  // Compruebe los parámetros del sitio  temp += ">";  temp+="

Publiced files on SPIFFS:

>>>Filename>>>td>S ize>Action>>>";  servidor.enviarContenido(temp);  temp="";  Archivoraíz=SPIFFS.abierto("/");  Archivoarchivo=raíz.openNextFile();  mientras(archivo)  {     temp+="";     temp+="";     temp+="";        archivo=raíz.openNextFile();  }  temp+="";  temp+="
+ Cadena(archivo.nombre()) + "\" download=\" + Cadena(archivo.nombre()) + "\">" + Cadena(archivo.nombre()) + "
>";     temp += "
"+ formatBytes(archivo.tamaño())+ " + Cadena(archivo.nombre()) + "> Delete
>
;  temp += "

Upload>/h4>";  temp += "";  temp += "<form method='POST' action='/upload' enctype='multipart/form-data' style='height:35px;'><input type='file' name='upload' style='height:35px; font-size:13px;' required>\r\n<input type= 'submit' value='Upload' class='button'>";  temp += "


"
;  servidor.enviarContenido(temp);  temp = "";  temp += " Format SPIFFS Filesystem. (Toma hasta 30 segundos) >";  temp += "

Systemlinks:

>

>>> ;th>
";  temp+="Main Page>/a>

>

"
;  servidor.enviarContenido(temp);  temp = "";  temp += "

Programmed and designed by: Tobias Kuch

>p>Contacto con información:
tobias.kuch@googlemail.com'>tobias.kuch@googlemail.com.

>>";    //server.send ( 200, "", temp );  servidor.enviarContenido(temp);  servidor.cliente().parar(); // Parar es necesario porque no enviamos longitud de contenido  temp = ""; } /** Cargar credenciales WLAN de EEPROM */ bool carga Credenciales() { bool RetValue; EEPROM.comenzar(512); EEPROM.obtener(0, MyWiFiConfig); EEPROM.final(); si (Cadena(MyWiFiConfig.ConfigValid) == Cadena("TK"))  {    RetValue = verdadero;  } de lo contrario  {    RetValue = falso; // Configuración WLAN no encontrada.  }  regresar RetValue; } /** Conservar credenciales WLAN a EEPROM */ bool salvar credenciales() { bool RetValue; // Verificar errores lógicos RetValue = verdadero; si  (MyWiFiConfig.APSTA == verdadero ) Modo //AP  {   si (MyWiFiConfig.PwDReq y (tamaño de(Cadena(MyWiFiConfig.WiFiPwd)) < 8))    {          RetValue = falso;  // Configuración inválida    }   si (tamaño de(Cadena(MyWiFiConfig.APSTAName)) < 1)    {      RetValue = falso;  // Configuración inválida    }  } si (RetValue)  {  EEPROM.comenzar(512);  para (int yo = 0 ; yo < tamaño de(MyWiFiConfig) ; yo++)     {      EEPROM.escribir(yo, 0);     }  strncpy( MyWiFiConfig.ConfigValid , "TK", tamaño de(MyWiFiConfig.ConfigValid) );  EEPROM.poner(0, MyWiFiConfig);  EEPROM.cometer();  EEPROM.fin();  }  volver RetValue; } nulo SetDefaultWiFiConfig() {   byte Len;   MyWiFiConfig.APSTA = cierto;   MyWiFiConfig.PwDReq = cierto;  // se requiere PW predeterminado   MyWiFiConfig.CapPortal = cierto;   strncpy( MyWiFiConfig.APSTAName, "ESP_Config", tamaño de(MyWiFiConfig.APSTAName) );   Len = barrer(MyWiFiConfig.APSTAName);   MyWiFiConfig.APSTAName[Len+1] = '\0';      strncpy( MyWiFiConfig.WiFiPwd, "12345678", tamaño de(MyWiFiConfig.WiFiPwd) );   Len = barrer(MyWiFiConfig.WiFiPwd);   MyWiFiConfig.WiFiPwd[Len+1] = '\0';     strncpy( MyWiFiConfig.ConfigValid, "TK", tamaño de(MyWiFiConfig.ConfigValid) );   Len = barrer(MyWiFiConfig.ConfigValid);   MyWiFiConfig.ConfigValid[Len+1] = '\0';   De serie.println(F("Restablecer las credenciales WiFi".)); } vacío handleRoot() { // Página principal: temp = ""; corto PicCount = 0; byte ServArgs = 0; //Página de construcción  // HTML Header  servidor.sendHeader("Cache-Control", "sin caché, sin tienda, debe-revalidar");  servidor.sendHeader("Pragma", "no-cache");  servidor.sendHeader("Caduca", "-1");  servidor.setContentLength(CONTENT_LENGTH_UNKNOWN); Contenido // HTML  servidor.enviar ( 200, "text/html", temp );   // Speichersparen - Schon mal dem Client senden  temp = "";  temp += "head>";  temp += "";  temp += "Tobi's LED Display";  temp += "

LED Display

"
;  temp += "";  servidor.enviarContenido(temp);  temp = ""; // Solicitud de usuario de procesamiento si (servidor.args() > 0) // Parámetro wurden ubergeben  {  temp += "
Eingaben werden verarbeitet. Bitte warten..

"
;;  servidor.enviarContenido(temp);  temp = ""; // Actualizar el documento de antecedentes si (servidor.arg("PicSelect") == "off")  // Borrar pantalla LED  {      temp = "";    para ( int i = 0; i < NUM_LEDS; i++)      {            lideres[i]=  0x000000;              }    Rápido.mostrar();  } de lo contrario  {    temp = servidor.arg("PicSelect"); // Bild gewählt. Muestra inhalt por Picselect hergstellt    drawBitmap_SPIFFS(temp,0,0);    temp = "";     } }  temp += "

>Disponible Pictures in SPIIFS for 8x8 Display>/h2>

>>/caption>";  temp+="
"
;  temp+="> Clear LED Display
";  temp+=">";  //Lista de archivos BMP disponibles en SPIFFS  Archivoraíz=SPIFFS.abierto("/");  Archivoarchivo=raíz.openNextFile();  PicCount=1;  mientras(archivo)   {    si(Cadena(archivo.nombre()).terminaCon(".bmp")oCadena(archivo.nombre()).terminaCon(".BMP"))    {      BMPHeaderPicData=ReadBitmapSpecs(archivo.nombre());      si((PicData.ancho<kMatrixWidth+1)y(PicData.altura<kMatrixHeight+1))  // Mostrar solo en la lista, cuando Bitmap no exceda la Resolución de visualización. Las imágenes más grandes no están listadas.        {          temp+="+Cadena(archivo.nombre())+"'alt='"+Cadena(archivo.nombre())+"' border='3' bordercolor=green> Imagen "+PicCount+"
"
;          temp+=Cadena(archivo.nombre())+" Res: "+Cadena(PicData.ancho)+"x"+Cadena(PicData.altura)+"px Filesize: "+formatBytes(archivo.tamaño())+"";          PicCount ++;        }    }   archivo = raíz.openNextFile();        }  servidor.enviarContenido(temp);  temp = "";    temp = "<button type='submit' name='action' value='0' style='height: 50px; width: 280px'>Show Image on Led Display</button>";  temp += "";  temp += "

table border=2 bgcolor = white width = 280 cellpadding =5<

Systemlinks:

<

>>>/caption>"
;  temp += "
>
";  temp+="WIFI Settings
"
;  temp+="Filemanager
"
;  temp+="


"
;  temp += "

Programmed and designed by: Tobias Kuch

>p>Contacto con información: tobias.kuch@googlemail.com'>tobias.kuch@googlemail.com.

"
;  temp += "";  servidor.enviarContenido(temp);  temp = "";  servidor.cliente().parar(); // Parar es necesario porque no enviamos longitud de contenido } vacío handleNotFound() {    si (portal cautivo())      { // Si el portal caprive redirige en lugar de mostrar la página de error.        regresar;      }  si (!handleFileRead(servidor.uri()))    {    temp = "";    // HTML Header    servidor.sendHeader("Cache-Control", "sin caché, sin tienda, debe-revalidar");    servidor.sendHeader("Pragma", "no-cache");    servidor.sendHeader("Caduca", "-1");    servidor.setContentLength(CONTENT_LENGTH_UNKNOWN);    Contenido // HTML    temp += "head>";    temp += "";    temp += "< título>File not found>";    temp += "

404 Archivo no encontrado


"
;    temp += "

Debug Information:


"
;    temp += "";    temp += "URI: ";    temp += servidor.uri();    temp += "\nMethod: ";    temp+= ( servidor.método() == HTTP_GET ) ? "GET" : "POST";    temp += "
Arguments: "
;    temp += servidor.args();    temp += "\n";      para ( uint8_t i = 0; i < servidor.args(); i++ ) {        temp += " " + servidor.arg ( i ) + ": " + servidor.arg ( i ) + "\n";        }    temp += "
Server Hostheader: "
+ servidor.hostHeader();    para ( uint8_t i = 0; i < servidor.encabezados(); i++ ) {        temp += " " + servidor.encabezado ( i ) + ": " + servidor.encabezado ( i ) + "\n<br>";        }      temp += "



You may want to browse to: h2>

";    temp+=">";    temp+="Main Page>/a>
"
;    temp += "WIFI Settings
";    temp+="Filemanager
"
;    temp+="


"
;    temp += "

Programmed and designed by: Tobias Kuch

>p>Contacto con información: tobias.kuch@googlemail.com'>tobias.kuch@googlemail.com.

"
;    temp += "";    servidor.enviar ( 404, "", temp );    servidor.cliente().parar(); // Parar es necesario porque no enviamos longitud de contenido    temp = "";    } } /** Redirigir al portal cautivo si recibimos una solicitud de otro dominio. Devuelve la verdad en ese caso para que el controlador de página no intente manejar la solicitud de nuevo. */ booleano portal cautivo() {  si (!isIp(servidor.hostHeader()) && servidor.hostHeader() != (Cadena(Nombre de ESP)+".local")) {    // Serial.println("Petición redireccionada al portal cautivo");    servidor.sendHeader("Localización", Cadena("http://") + toStringIp(servidor.cliente().localIP()), verdadero);    servidor.enviar ( 302, "texto/llano", ""); // El contenido vacío inhibe el encabezado de longitud de contenido, por lo que tenemos que cerrar el enchufe nosotros mismos.    servidor.cliente().parar(); // Parar es necesario porque no enviamos longitud de contenido    regresar verdadero;  }  regresar falso; } /** Manejador de página de configuración de Wifi */ vacío handleWifi() {  // Página: /wifi  byte i;  byte len ;  temp = "";  // Compruebe los parámetros del sitio      si (servidor.hasArg("Reiniciar") )  // Sistema de reinicio       {         temp = "Reiniciando el sistema en 5 segundos ...";         servidor.enviar ( 200, "text/html", temp );         retraso(5000);         servidor.cliente().parar();               WiFi.desconectar();         retraso(1000);             }      si (servidor.hasArg("WiFiMode") y (servidor.arg("WiFiMode") == "1")  )  // Modo de estación STA Conectar a otra estación WIFI       {        startMillis = millis(); // Restablecer el contador de tiempo para evitar el funcionamiento de la legumbre de modo inactivo        // Conectar a la STATION existente        si ( tamaño de(servidor.arg("WiFi_Network")) > 0  )          {            Serial.println("Modo de la STA");            MyWiFiConfig.APSTA = falso; // Punto de acceso o modo de estación - modo de estación falso            temp = "";                      para ( i = 0; i < APSTANameLen;i++) { MyWiFiConfig.APSTAName[i] =  0; }            temp = servidor.arg("WiFi_Network");            len =  temp.longitud();            para ( i = 0; i < len;i++)            {                  MyWiFiConfig.APSTAName[i] =  temp[i];                          }         // MyWiFiConfig.APSTAName[len+1] = '\0';            temp = "";            para ( i = 0; i < WiFiPwdLen;i++)  { MyWiFiConfig.WiFiPwd[i] =  0; }                      temp = servidor.arg("STAWLanPW");            len =  temp.longitud();                                 para ( i = 0; i < len;i++)                {                si (temp[i] > 32) //Steuerzeichen raus                  {                   MyWiFiConfig.WiFiPwd[i] =  temp[i];                    }                }        // MyWiFiConfig.WiFiPwd[len+1] = '\0';            temp = "WiFi Connect to AP: -";            temp += MyWiFiConfig.APSTAName;            temp += "-
WiFi PW: -"
;            temp += MyWiFiConfig.WiFiPwd;            temp += "-
"
;                      temp += "Conectando al modo STA en 2 segundos ... <br>";            servidor.enviar ( 200, "text/html", temp );            servidor.enviarContenido(temp);            retraso(2000);            servidor.cliente().parar();            servidor.parar();            temp = "";            WiFi.desconectar();            WiFi.softAPdisconnect(verdadero);            retraso(500);           // ConnectWifiAP           bool SaveOk = salvar credenciales();                      i = ConnectWifiAP();            retraso(700);            si (i != 3) // 4: WL_CONNECT_FAILED - La contraseña es incorrecta 1: WL_NO_SSID_AVAILin - SSID configurado no se puede alcanzar              {                 Serial.imprimir(F("No se puede conectar a la red especificada. Razón: "));                 Serial.println(i);                 servidor.cliente().parar();                 retraso(100);                              WiFi.setAutoReconnect (falso);                 retraso(100);                      WiFi.desconectar();                              retraso(1000);                 Configuración de WiFiWiFiConfig predeterminado();                 Crear WifiSoftAP();                 regresar;              } de lo contrario              {                 // Config seguro                 bool SaveOk = salvar credenciales();                 InitalizeHTTPServer();                 regresar;              }          }       }               si (servidor.hasArg("WiFiMode") y (servidor.arg("WiFiMode") == "2")  )  // Cambiar modo AP       {        startMillis = millis(); // Restablecer el contador de tiempo para evitar el funcionamiento de la legumbre de modo inactivo        // Configurar punto de acceso        temp = servidor.arg("APPoint");              len =  temp.longitud();        temp =servidor.arg("APPW");        si (servidor.hasArg("PasswordReq"))            {                    i =  temp.longitud();            } de lo contrario { i = 8; }                si (  ( len > 1 ) y (servidor.arg("APPW") == servidor.arg("APPWRepeat")) y ( i > 7)          )          {            temp = "";            Serial.println(F("APMode"));            MyWiFiConfig.APSTA = verdadero; // Punto de acceso o modo de almacenamiento - verdadero modo AP            si (servidor.hasArg("Portal Captivo"))            {              MyWiFiConfig.CapPortal = verdadero ; //CaptivePortal en modo AP            } de lo contrario { MyWiFiConfig.CapPortal = falso ; }                       si (servidor.hasArg("PasswordReq"))            {              MyWiFiConfig.PwDReq = verdadero ; //Password Required in AP Mode            } de lo contrario { MyWiFiConfig.PwDReq = falso ; }            para ( i = 0; i < APSTANameLen;i++) { MyWiFiConfig.APSTAName[i] =  0; }            temp = servidor.arg("APPoint");            len =  temp.longitud();            para ( i = 0; i < len;i++) { MyWiFiConfig.APSTAName[i] =  temp[i]; }            MyWiFiConfig.APSTAName[len+1] = '\0';              temp = "";            para ( i = 0; i < WiFiPwdLen;i++)  {  MyWiFiConfig.WiFiPwd[i] =  0; }                      temp = servidor.arg("APPW");            len =  temp.longitud();                                 para ( i = 0; i < len;i++)  { MyWiFiConfig.WiFiPwd[i] =  temp[i];  }            MyWiFiConfig.WiFiPwd[len+1] = '\0';              temp = "";                     si (salvar credenciales()) // Guardar AP ConfigCongfig              {                        temp = "Daten des AP Modes erfolgreich gespeichert. Reboot notwendig.";              } de lo contrario  { temp = "Daten des AP Modes fehlerhaft.";  }          } de lo contrario si (servidor.arg("APPW") != servidor.arg("APPWRepeat"))                {                  temp = "";                  temp = "WLAN Passwort nicht gleich. Abgebrochen.";                } de lo contrario                {                              temp = "";                  temp = "WLAN Passwort oder AP Name zu kurz. Abgebrochen.";                }                     // End WifiAP       }  // HTML Header  servidor.sendHeader("Cache-Control", "sin caché, sin tienda, debe-revalidar");  servidor.sendHeader("Pragma", "no-cache");  servidor.sendHeader("Caduca", "-1");  servidor.setContentLength(CONTENT_LENGTH_UNKNOWN); Contenido // HTML  temp += "head>";  servidor.enviar ( 200, "text/html", temp );  temp = "";  temp += "Smartes Tuerschild - WiFi Settings";  servidor.enviarContenido(temp);  temp = "";  temp += "

WiFi Settings

"
;  temp += "

Current WiFi Settings:

";  si(servidor.cliente().localIP()==apIP){     temp+="Modo : Punto de acceso suave (AP)
"
;     temp+="SSID : "+Cadena(MyWiFiConfig.APSTAName)+"

"
;  }de lo contrario{     temp+="Mode : Station (STA)
"
;     temp+="SSID : "+Cadena(MyWiFiConfig.APSTAName)+"
"
;     temp+="BSSID : "+WiFi.BSSIDstr()+"

"
;  }  temp+="

"
;  servidor.enviarContenido(temp);  temp = "";  temp += "<form action='/wifi' method='post'>";  temp += ">>";  si(MyWiFiConfig.APSTA==1)    {      temp+=";    } de lo contrario    {      temp += ";    }  temp += "WiFi Networks:
td>Encryption< ;/td>";  servidor.enviarContenido(temp);  temp="";  WiFi.scanDelete();  intn=WiFi.scanNetworks(falso,falso);//WiFi.scanNetworks(async, show_hidden)  si(n>0){    para(inti=0;i<n;i++){    temp+="";    CadenaNrb=Cadena(i);    temp+="";    temp+="";       Nrb=Obtener tipo de cifrado(WiFi.tipo de cifrado(i));    temp+="";    temp+="";       }  }de lo contrario{    temp+="";    temp+="";    temp+="";    temp+="";    temp+="";  }  temp+="
NumberSSIDWiFi Strength
" + Nrb + " " + WiFi.SSID(i) +" "+ Nrb + " " + Cadena(WiFi.RSSI(i)) + "
1 No WLAN found --- ---
table border=2 bgcolor = white >Connect to WiFi SSID:>WiFi Password: "
;  temp += "<input type='text' name='STAWLanPW' maxlength='40' size='40'>";  temp += ">table>table border=2 bgcolor = white width = 500t> >>>
"
;  servidor.enviarContenido(temp);  temp = "";  si (MyWiFiConfig.APSTA == verdadero)    {      temp += " WiFi Access Point Mode
"
;    } de lo contrario    {      temp += "";    }  temp += "";    }de lo contrario    {      temp+="<input type='text' name='APPointName' maxlength='"+Cadena(APSTANameLen-1)+"' size='30' >";    }  servidor.enviarContenido(temp);  temp="";         si(MyWiFiConfig.APSTA==verdadero)    {      temp+="";      temp+="";      temp+="";    }de lo contrario    {      temp+="";      temp+="";      temp+="";    }      temp+="
WiFi Access Point Name: ";    servidor.enviarContenido(temp);  temp = "";              si (MyWiFiConfig.APSTA == verdadero)    {      temp += "<input type='text' name='APPointName' maxlength='"+Cadena(APSTANameLen-1)+"' size='30' value='" + Cadena(MyWiFiConfig.APSTAName) + "'>
WiFi Password: ";      temp += "<input type='password' name='APPW' maxlength='"+Cadena(WiFiPwdLen-1)+"' size='30' value='" + Cadena(MyWiFiConfig.WiFiPwd) + "'>
Repeat WiFi Password:
WiFi Password: ";      temp += "<input type='password' name='APPW' maxlength='"+Cadena(WiFiPwdLen-1)+"' size='30'>
Repeat WiFi Password:
"
;  servidor.enviarContenido(temp);  temp = "";        si (MyWiFiConfig.PwDReq)    {      temp += " Contraseña para el inicio de sesión requerido. ";    } de lo contrario    {      temp += "<input type='checkbox' name='PasswordReq' > Contraseña para el inicio de sesión requerido".;    }  servidor.enviarContenido(temp);  temp = "";    si (MyWiFiConfig.CapPortal)    {      temp += " Activar portal cautivo";    } de lo contrario    {      temp += "<input type='checkbox' name='CaptivePortal' > Activar portal cautivo";    }  servidor.enviarContenido(temp);  temp = "";    temp += "
>>>
<<"
;  temp += "<button type='submit' name='Reboot' value='1' style='height: 50px; width: 200px' >Reboot System</button>";  servidor.enviarContenido(temp);  temp = "";  temp += "<button type='reset' name='action' value='1' style='height: 50px; width: 100px' >Reset</button</form>";  temp += "

Systemlinks:

>

>>> ;th>
";  servidor.enviarContenido(temp);  temp="";  temp+="Main Page>>


"
;  temp += "

Programmed and designed by: Tobias Kuch

>p>Contacto con información:
tobias.kuch@googlemail.com'>tobias.kuch@googlemail.com.

";  temp += "";    servidor.enviarContenido(temp);  servidor.cliente().parar(); // Parar es necesario porque no enviamos longitud de contenido  temp = ""; } vacío handleUploadSave() {  Cadena FileData ;    temp = "";              para (byte i = 0; i < servidor.args(); i++)  {    temp += "Arg" + (Cadena)i + " –> ";   //Incluir el valor de iteración actual    temp += servidor.arg(i) + ": ";     // Obtener el nombre del parámetro    temp += servidor.arg(i) + "\n";              // Obtener el valor del parámetro  }  // server.send(200, "text/plain", temp); //Respuesta a la solicitud HTTP  FileData = servidor.arg("datei");  servidor.sendHeader("Localización", "filesystem", verdadero);  servidor.sendHeader("Cache-Control", "sin caché, sin tienda, debe-revalidar");  servidor.sendHeader("Pragma", "no-cache");  servidor.sendHeader("Caduca", "-1");  servidor.enviar ( 302, "texto/llano", "");  // El contenido vacío inhibe el encabezado de longitud de contenido, por lo que tenemos que cerrar el enchufe nosotros mismos.  servidor.cliente().parar(); // Parar es necesario porque no enviamos longitud de contenido } /** ¿Es esto una IP? */ booleano isIp(Cadena str) {  para (int i = 0; i < str.longitud(); i++) {    int c = str.charAt(i);    si (c != '.' && (c < '0' || c > '9')) {      regresar falso;    }  }  regresar verdadero; } Cadena Obtener tipo de cifrado(byte este tipo) {  Cadena Producto = "";   // leer el tipo de cifrado e imprimir el nombre:   interruptor (este tipo) {     caso 5:       Producto = "WEP";       regresar Producto;       romper;     caso 2:       Producto = "WPA";       regresar Producto;       romper;     caso 4:       Producto = "WPA2";       regresar Producto;       romper;     caso 7:       Producto = "Ninguno";       regresar Producto;       romper;     caso 8:       Producto = "Auto";       regresar Producto;      romper;   } } /** IP a String? */ Cadena toStringIp(IPAddress ip) {  Cadena res = "";  para (int i = 0; i < 3; i++) {    res += Cadena((ip >> (8 * i)) & 0xFF) + ".";  }  res += Cadena(((ip >> 8 * 3)) & 0xFF);  regresar res; } Cadena formatBytes(size_t bytes) {            // lesbare Anzeige der Speichergröen   si (bytes < 1024) {     regresar Cadena(bytes) + "Byte";   } de lo contrario si (bytes < (1024 * 1024)) {     regresar Cadena(bytes / 1024.0) + "KB";   } de lo contrario si (bytes < (1024 * 1024 * 1024)) {     regresar Cadena(bytes / 1024.0 / 1024.0) + "MB";   } } Cadena getContentType(Cadena nombre de archivo) { // convertir la extensión de archivo al tipo MIME  si (nombre de archivo.terminaCon(".htm")) regresar "text/html";  de lo contrario si (nombre de archivo.terminaCon(".css")) regresar "texto/css";  de lo contrario si (nombre de archivo.terminaCon(".js")) regresar "applicación/javascripto";  de lo contrario si (nombre de archivo.terminaCon(".ico")) regresar "Imagen/x-icon";  de lo contrario si (nombre de archivo.terminaCon(".gz")) regresar "applicación/x-gzip";  de lo contrario si (nombre de archivo.terminaCon(".bmp")) regresar "Imagen/bmp";  de lo contrario si (nombre de archivo.terminaCon(".tif")) regresar "imágene/tipo";  de lo contrario si (nombre de archivo.terminaCon(".pbm")) regresar "image/x-portable-bitmap";  de lo contrario si (nombre de archivo.terminaCon(".jpg")) regresar "image/jpeg";  de lo contrario si (nombre de archivo.terminaCon(".gif")) regresar "image/gif";  de lo contrario si (nombre de archivo.terminaCon(".png")) regresar "imagen/png";  de lo contrario si (nombre de archivo.terminaCon(".svg")) regresar "imagen/svg+xml";  de lo contrario si (nombre de archivo.terminaCon(".html")) regresar "text/html";  de lo contrario si (nombre de archivo.terminaCon(".wav")) regresar "audio/x-wav";  de lo contrario si (nombre de archivo.terminaCon(".zip")) regresar "Aplicación/zip";  de lo contrario si (nombre de archivo.terminaCon(".rgb")) regresar "image/x-rg"; // Lista completa en https://wiki.selfhtml.org/wiki/MIME-Type/bersicht  regresar "texto/llano"; } bool handleFileRead(Cadena camino) { // enviar el archivo correcto al cliente (si existe)  si (camino.terminaCon("/")) camino += "index.html";          // Si se solicita una carpeta, envíe el archivo de índice  Cadena tipo de contenido = getContentType(camino);             // Obtener el tipo MIME  Cadena pathWithGz = camino + ".gz";  si (SPIFFS.existe(pathWithGz) || SPIFFS.existe(camino)) { // Si el archivo existe, ya sea como archivo comprimido, o normal    si (SPIFFS.existe(pathWithGz))                         // Si hay una versión comprimida disponible      camino += ".gz";                                         // Usa el verión comprimido    Archivo archivo = SPIFFS.abierto(camino, "r");                    // Abrir el archivo    size_t enviado = servidor.streamFile(archivo, tipo de contenido);    // Enviarlo al cliente    archivo.cerca();                                          // Cierre el archivo de nuevo    regresar verdadero;  }  regresar falso; } vacío bucle() {  si (SoftAccOK)  {    dnsServer.procesoNextRequest(); //DNS  }  //HTTP  servidor.handleClient(); }

 

Ich habe ein paar 8x8 BMP Grafiken in verschiedenen Farbtiefe für euch zum Testen erstellt:

 Descargar BMP Grafiken

Im nächsten Teil kümmern wir uns um eine höhere Auflösung unseres LED Displays. Bis dahin wünsche viel Spa mit dem Anzeigen eigener BMP Dateien auf dem LED-Display.

Esp-8266Projekte für fortgeschritteneSensoren

3 comentarios

aschommer

aschommer

Es hat doch etwas länger gedauert, aber jetzt habe ich “meinen Fork” erstellt: https://github.com/a-schommer/Tobis-General-Display
Rückmeldungen würden mich freuen – ich habe (noch) keine Routine damit, Software zu “veröffentlichen”, also glaube ich selbst nicht so ganz, dass es fehlerfrei ist.

Tobias

Tobias

Hallo aschommer

Sämtliche Codes hier von mir stehen unter der GPL 3, wenn nicht anders im Code angegeben. Die Codes werden zuätzlich von mir auf GitHub unter https://github.com/kuchto zeitverzögert veröffentlich. Gerne kann für die einzelnen Projekte ein Fork angelegt werden.
Viele Grüße

aschommer

aschommer

Unter welcher Lizenz stehen die Programme hier eigentlich? Ich würde gerne eine Variante für SSD1306-OLEDs (via u8g2-Library) beisteuern.

Deja un comentario

Todos los comentarios son moderados antes de ser publicados