Added support for SPIFFS

Fixed ESP32
This commit is contained in:
cschwinne 2019-03-16 02:09:37 +01:00
parent d4bf1cb23d
commit c8a7537157
11 changed files with 705 additions and 200 deletions

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -24,15 +24,21 @@
//#define WLED_DISABLE_INFRARED //there is no pin left for this on ESP8266-01 //#define WLED_DISABLE_INFRARED //there is no pin left for this on ESP8266-01
//#define WLED_DISABLE_MOBILE_UI //#define WLED_DISABLE_MOBILE_UI
//to toggle usb serial debug (un)comment following line(s) #define WLED_DISABLE_FILESYSTEM //SPIFFS is not used by any WLED feature yet
//#define WLED_ENABLE_FS_SERVING //Enable sending html file from SPIFFS before serving progmem version
//#define WLED_ENABLE_FS_EDITOR //enable /edit page for editing SPIFFS content. Will also be disabled with OTA lock
//to toggle usb serial debug (un)comment the following line
//#define WLED_DEBUG //#define WLED_DEBUG
//library inclusions //library inclusions
#include <Arduino.h> #include <Arduino.h>
#ifdef ARDUINO_ARCH_ESP32 #ifdef ARDUINO_ARCH_ESP32
#include <WiFi.h> #include <WiFi.h>
#include <ESPmDNS.h> #include <ESPmDNS.h>
#include <AsyncTCP.h> #include <AsyncTCP.h>
#include "SPIFFS.h"
#else #else
#include <ESP8266WiFi.h> #include <ESP8266WiFi.h>
#include <ESP8266mDNS.h> #include <ESP8266mDNS.h>
@ -46,6 +52,7 @@
#ifndef WLED_DISABLE_OTA #ifndef WLED_DISABLE_OTA
#include <ArduinoOTA.h> #include <ArduinoOTA.h>
#endif #endif
#include <SPIFFSEditor.h>
#include "src/dependencies/time/Time.h" #include "src/dependencies/time/Time.h"
#include "src/dependencies/time/TimeLib.h" #include "src/dependencies/time/TimeLib.h"
#include "src/dependencies/timezone/Timezone.h" #include "src/dependencies/timezone/Timezone.h"
@ -69,6 +76,7 @@
#include "WS2812FX.h" #include "WS2812FX.h"
#include "ir_codes.h" #include "ir_codes.h"
#if IR_PIN < 0 #if IR_PIN < 0
#ifndef WLED_DISABLE_INFRARED #ifndef WLED_DISABLE_INFRARED
#define WLED_DISABLE_INFRARED #define WLED_DISABLE_INFRARED
@ -89,7 +97,7 @@
//version code in format yymmddb (b = daily build) //version code in format yymmddb (b = daily build)
#define VERSION 1903131 #define VERSION 1903161
char versionString[] = "0.8.4-dev"; char versionString[] = "0.8.4-dev";
@ -98,10 +106,6 @@ char apPass[65] = "wled1234";
char otaPass[33] = "wledota"; char otaPass[33] = "wledota";
//spiffs FS only useful for debug (only ESP8266)
//#define USEFS
//Hardware CONFIG (only changeble HERE, not at runtime) //Hardware CONFIG (only changeble HERE, not at runtime)
//LED strip pin, button pin and IR pin changeable in NpbWrapper.h! //LED strip pin, button pin and IR pin changeable in NpbWrapper.h!
@ -438,9 +442,12 @@ WS2812FX strip = WS2812FX();
#endif #endif
//filesystem //filesystem
#ifdef USEFS #ifndef WLED_DISABLE_FILESYSTEM
#include <FS.h>; #include <FS.h>
File fsUploadFile; #ifdef ARDUINO_ARCH_ESP32
#include "SPIFFS.h"
#endif
#include "SPIFFSEditor.h"
#endif #endif
//gamma 2.4 lookup table used for color correction //gamma 2.4 lookup table used for color correction
@ -465,6 +472,7 @@ const byte gamma8[] = {
//function prototypes //function prototypes
void serveMessage(AsyncWebServerRequest*,uint16_t,String,String,byte); void serveMessage(AsyncWebServerRequest*,uint16_t,String,String,byte);
//turns all LEDs off and restarts ESP //turns all LEDs off and restarts ESP
void reset() void reset()
{ {
@ -514,7 +522,7 @@ void loop() {
userLoop(); userLoop();
yield(); yield();
handleButton(); handleIO();
handleIR(); handleIR();
handleNetworkTime(); handleNetworkTime();
if (!onlyAP) handleAlexa(); if (!onlyAP) handleAlexa();

View File

@ -577,13 +577,11 @@ void savePreset(byte index)
} }
char* loadMacro(byte index) void loadMacro(byte index, char* m)
{ {
index-=1; index-=1;
char m[65]; if (index > 15) return;
if (index > 15) return m;
readStringFromEEPROM(1024+64*index, m, 64); readStringFromEEPROM(1024+64*index, m, 64);
return m;
} }
@ -592,7 +590,9 @@ void applyMacro(byte index)
index-=1; index-=1;
if (index > 15) return; if (index > 15) return;
String mc="win&"; String mc="win&";
mc += loadMacro(index+1); char m[65];
loadMacro(index+1, m);
mc += m;
mc += "&IN"; //internal, no XML response mc += "&IN"; //internal, no XML response
if (!notifyMacro) mc += "&NN"; if (!notifyMacro) mc += "&NN";
String forbidden = "&M="; //dont apply if called by the macro itself to prevent loop String forbidden = "&M="; //dont apply if called by the macro itself to prevent loop

View File

@ -3,10 +3,17 @@
*/ */
//build XML response to HTTP /win API request //build XML response to HTTP /win API request
char* XML_response(AsyncWebServerRequest *request, bool includeTheme) char* XML_response(AsyncWebServerRequest *request, bool includeTheme, char* dest = nullptr)
{
if (dest == nullptr) //allocate local buffer if none passed
{ {
char sbuf[1024]; char sbuf[1024];
olen = 0; obuf = sbuf; obuf = sbuf;
} else {
obuf = dest;
}
olen = 0;
oappend("<?xml version=\"1.0\" ?><vs><ac>"); oappend("<?xml version=\"1.0\" ?><vs><ac>");
oappendi((nightlightActive && nightlightFade) ? briT : bri); oappendi((nightlightActive && nightlightFade) ? briT : bri);
oappend("</ac>"); oappend("</ac>");
@ -98,8 +105,7 @@ char* XML_response(AsyncWebServerRequest *request, bool includeTheme)
oappend("</cf></th>"); oappend("</cf></th>");
} }
oappend("</vs>"); oappend("</vs>");
if (request != nullptr) request->send(200, "text/xml", sbuf); if (request != nullptr) request->send(200, "text/xml", obuf);
return sbuf;
} }
//append a numeric setting to string buffer //append a numeric setting to string buffer
@ -157,15 +163,15 @@ void sappends(char stype, char* key, char* val)
//get values for settings form in javascript //get values for settings form in javascript
char* getSettingsJS(byte subPage) void getSettingsJS(byte subPage, char* dest)
{ {
//0: menu 1: wifi 2: leds 3: ui 4: sync 5: time 6: sec //0: menu 1: wifi 2: leds 3: ui 4: sync 5: time 6: sec
DEBUG_PRINT("settings resp"); DEBUG_PRINT("settings resp");
DEBUG_PRINTLN(subPage); DEBUG_PRINTLN(subPage);
char sbuf[2048]; obuf = dest;
olen = 0; obuf = sbuf; olen = 0;
if (subPage <1 || subPage >6) return sbuf; if (subPage <1 || subPage >6) return;
if (subPage == 1) { if (subPage == 1) {
sappends('s',"CS",clientSSID); sappends('s',"CS",clientSSID);
@ -328,7 +334,9 @@ char* getSettingsJS(byte subPage)
sappend('c',"CF",!useAMPM); sappend('c',"CF",!useAMPM);
sappend('i',"TZ",currentTimezone); sappend('i',"TZ",currentTimezone);
sappend('v',"UO",utcOffsetSecs); sappend('v',"UO",utcOffsetSecs);
sappends('m',"(\"times\")[0]",getTimeString()); char tm[32];
getTimeString(tm);
sappends('m',"(\"times\")[0]",tm);
sappend('i',"OL",overlayCurrent); sappend('i',"OL",overlayCurrent);
sappend('v',"O1",overlayMin); sappend('v',"O1",overlayMin);
sappend('v',"O2",overlayMax); sappend('v',"O2",overlayMax);
@ -344,12 +352,13 @@ char* getSettingsJS(byte subPage)
sappend('v',"CH",countdownHour); sappend('v',"CH",countdownHour);
sappend('v',"CM",countdownMin); sappend('v',"CM",countdownMin);
sappend('v',"CS",countdownSec); sappend('v',"CS",countdownSec);
char k[4]; k[0]= 'M'; char k[4]; k[0]= 'M';
for (int i=1;i<17;i++) for (int i=1;i<17;i++)
{ {
char m[65];
loadMacro(i, m);
sprintf(k+1,"%i",i); sprintf(k+1,"%i",i);
sappends('s',k,loadMacro(i)); sappends('s',k,m);
} }
sappend('v',"MB",macroBoot); sappend('v',"MB",macroBoot);
@ -386,7 +395,6 @@ char* getSettingsJS(byte subPage)
oappend(") OK\";"); oappend(") OK\";");
} }
oappend("}</script>"); oappend("}</script>");
return sbuf;
} }

View File

@ -37,129 +37,43 @@ void handleSerial()
} }
} }
#ifdef USEFS
String formatBytes(size_t bytes){ #if !defined WLED_DISABLE_FILESYSTEM && defined WLED_ENABLE_FS_SERVING
if (bytes < 1024){ //Un-comment any file types you need
return String(bytes)+"B"; String getContentType(AsyncWebServerRequest* request, String filename){
} else if(bytes < (1024 * 1024)){ if(request->hasArg("download")) return "application/octet-stream";
return String(bytes/1024.0)+"KB";
} else if(bytes < (1024 * 1024 * 1024)){
return String(bytes/1024.0/1024.0)+"MB";
} else {
return String(bytes/1024.0/1024.0/1024.0)+"GB";
}
}
String getContentType(String filename){
if(server->hasArg("download")) return "application/octet-stream";
else if(filename.endsWith(".htm")) return "text/html"; else if(filename.endsWith(".htm")) return "text/html";
else if(filename.endsWith(".html")) return "text/html"; else if(filename.endsWith(".html")) return "text/html";
else if(filename.endsWith(".css")) return "text/css"; // else if(filename.endsWith(".css")) return "text/css";
else if(filename.endsWith(".js")) return "application/javascript"; // else if(filename.endsWith(".js")) return "application/javascript";
else if(filename.endsWith(".json")) return "application/json";
else if(filename.endsWith(".png")) return "image/png"; else if(filename.endsWith(".png")) return "image/png";
else if(filename.endsWith(".gif")) return "image/gif"; // else if(filename.endsWith(".gif")) return "image/gif";
else if(filename.endsWith(".jpg")) return "image/jpeg"; else if(filename.endsWith(".jpg")) return "image/jpeg";
else if(filename.endsWith(".ico")) return "image/x-icon"; else if(filename.endsWith(".ico")) return "image/x-icon";
else if(filename.endsWith(".xml")) return "text/xml"; // else if(filename.endsWith(".xml")) return "text/xml";
else if(filename.endsWith(".pdf")) return "application/x-pdf"; // else if(filename.endsWith(".pdf")) return "application/x-pdf";
else if(filename.endsWith(".zip")) return "application/x-zip"; // else if(filename.endsWith(".zip")) return "application/x-zip";
else if(filename.endsWith(".gz")) return "application/x-gzip"; // else if(filename.endsWith(".gz")) return "application/x-gzip";
return "text/plain"; return "text/plain";
} }
bool handleFileRead(String path){ bool handleFileRead(AsyncWebServerRequest* request, String path){
DEBUG_PRINTLN("handleFileRead: " + path); DEBUG_PRINTLN("FileRead: " + path);
if(path.endsWith("/")) path += "index.htm"; if(path.endsWith("/")) path += "index.htm";
String contentType = getContentType(path); String contentType = getContentType(request, path);
String pathWithGz = path + ".gz"; String pathWithGz = path + ".gz";
if(SPIFFS.exists(pathWithGz) || SPIFFS.exists(path)){ if(SPIFFS.exists(pathWithGz)){
if(SPIFFS.exists(pathWithGz)) request->send(SPIFFS, pathWithGz, contentType);
path += ".gz"; return true;
File file = SPIFFS.open(path, "r"); }
size_t sent = server->streamFile(file, contentType); if(SPIFFS.exists(path)) {
file.close(); request->send(SPIFFS, path, contentType);
return true; return true;
} }
return false; return false;
} }
void handleFileUpload(){
if(server->uri() != "/edit") return;
HTTPUpload& upload = server->upload();
if(upload.status == UPLOAD_FILE_START){
String filename = upload.filename;
if(!filename.startsWith("/")) filename = "/"+filename;
DEBUG_PRINT("handleFileUpload Name: "); DEBUG_PRINTLN(filename);
fsUploadFile = SPIFFS.open(filename, "w");
filename = String();
} else if(upload.status == UPLOAD_FILE_WRITE){
//DEBUG_PRINT("handleFileUpload Data: "); DEBUG_PRINTLN(upload.currentSize);
if(fsUploadFile)
fsUploadFile.write(upload.buf, upload.currentSize);
} else if(upload.status == UPLOAD_FILE_END){
if(fsUploadFile)
fsUploadFile.close();
DEBUG_PRINT("handleFileUpload Size: "); DEBUG_PRINTLN(upload.totalSize);
}
}
void handleFileDelete(){
if(server->args() == 0) return server->send(500, "text/plain", "BAD ARGS");
String path = server->arg(0);
DEBUG_PRINTLN("handleFileDelete: " + path);
if(path == "/")
return server->send(500, "text/plain", "BAD PATH");
if(!SPIFFS.exists(path))
return server->send(404, "text/plain", "FileNotFound");
SPIFFS.remove(path);
server->send(200, "text/plain", "");
path = String();
}
void handleFileList() {
if(!server->hasArg("dir")) {server->send(500, "text/plain", "BAD ARGS"); return;}
String path = server->arg("dir");
DEBUG_PRINTLN("handleFileList: " + path);
Dir dir = SPIFFS.openDir(path);
path = String();
String output = "[";
while(dir.next()){
File entry = dir.openFile("r");
if (output != "[") output += ',';
bool isDir = false;
output += "{\"type\":\"";
output += (isDir)?"dir":"file";
output += "\",\"name\":\"";
output += String(entry.name()).substring(1);
output += "\"}";
entry.close();
}
output += "]";
server->send(200, "text/json", output);
}
void handleFileCreate(){
if(server->args() == 0)
return server->send(500, "text/plain", "BAD ARGS");
String path = server->arg(0);
DEBUG_PRINTLN("handleFileCreate: " + path);
if(path == "/")
return server->send(500, "text/plain", "BAD PATH");
if(SPIFFS.exists(path))
return server->send(500, "text/plain", "FILE EXISTS");
File file = SPIFFS.open(path, "w");
if(file)
file.close();
else
return server->send(500, "text/plain", "CREATE FAILED");
server->send(200, "text/plain", "");
path = String();
}
#else #else
bool handleFileRead(String path){return false;} bool handleFileRead(AsyncWebServerRequest*, String path){return false;}
#endif #endif

View File

@ -30,7 +30,10 @@ void wledInit()
DEBUG_PRINT("LEDs inited. heap usage ~"); DEBUG_PRINT("LEDs inited. heap usage ~");
DEBUG_PRINTLN(heapPreAlloc - ESP.getFreeHeap()); DEBUG_PRINTLN(heapPreAlloc - ESP.getFreeHeap());
#ifdef USEFS #ifndef WLED_DISABLE_FILESYSTEM
#ifdef ARDUINO_ARCH_ESP32
SPIFFS.begin(true);
#endif
SPIFFS.begin(); SPIFFS.begin();
#endif #endif

View File

@ -123,10 +123,9 @@ void updateLocalTime()
local = timezones[currentTimezone]->toLocal(tmc); local = timezones[currentTimezone]->toLocal(tmc);
} }
char* getTimeString() void getTimeString(char* out)
{ {
updateLocalTime(); updateLocalTime();
char out[32];
sprintf(out,"%i-%i-%i, %i:%s%i:%s%i",year(local), month(local), day(local), sprintf(out,"%i-%i-%i, %i:%s%i:%s%i",year(local), month(local), day(local),
(useAMPM)? hour(local)%12:hour(local), (useAMPM)? hour(local)%12:hour(local),
(minute(local)<10)?"0":"",minute(local), (minute(local)<10)?"0":"",minute(local),
@ -135,26 +134,6 @@ char* getTimeString()
{ {
strcat(out,(hour(local) > 11)? " PM":" AM"); strcat(out,(hour(local) > 11)? " PM":" AM");
} }
return out;
/*
String ret = year(local) + "-";
ret = ret + month(local);
ret = ret + "-";
ret = ret + day(local);
ret = ret + ", ";
ret += (useAMPM)? hour(local)%12:hour(local);
ret = ret + ":";
if (minute(local) < 10) ret = ret + "0";
ret = ret + minute(local);
ret = ret + ":";
if (second(local) < 10) ret = ret + "0";
ret = ret + second(local);
if (useAMPM)
{
ret += (hour(local) > 11)? " PM":" AM";
}
return ret;
*/
} }
void setCountdown() void setCountdown()

View File

@ -88,9 +88,11 @@ void publishMqtt()
strcat(subuf, "/c"); strcat(subuf, "/c");
mqtt->publish(subuf, 0, true, s); mqtt->publish(subuf, 0, true, s);
char apires[1024];
XML_response(nullptr, false, apires);
strcpy(subuf, mqttDeviceTopic); strcpy(subuf, mqttDeviceTopic);
strcat(subuf, "/v"); strcat(subuf, "/v");
mqtt->publish(subuf, 0, true, XML_response(nullptr, false)); mqtt->publish(subuf, 0, true, apires);
} }

View File

@ -15,7 +15,7 @@ void initServer()
}); });
server.on("/favicon.ico", HTTP_GET, [](AsyncWebServerRequest *request){ server.on("/favicon.ico", HTTP_GET, [](AsyncWebServerRequest *request){
if(!handleFileRead("/favicon.ico")) if(!handleFileRead(request, "/favicon.ico"))
{ {
request->send_P(200, "image/x-icon", favicon, 156); request->send_P(200, "image/x-icon", favicon, 156);
} }
@ -102,14 +102,16 @@ void initServer()
//if OTA is allowed //if OTA is allowed
if (!otaLock){ if (!otaLock){
#if !defined WLED_DISABLE_FILESYSTEM && defined WLED_ENABLE_FS_EDITOR
#ifdef ARDUINO_ARCH_ESP32
server.addHandler(new SPIFFSEditor(SPIFFS));//http_username,http_password));
#else
server.addHandler(new SPIFFSEditor());//http_username,http_password));
#endif
#else
server.on("/edit", HTTP_GET, [](AsyncWebServerRequest *request){ server.on("/edit", HTTP_GET, [](AsyncWebServerRequest *request){
request->send(200, "text/html", PAGE_edit); serveMessage(request, 501, "Not implemented", "The SPIFFS editor is disabled in this build.", 254);
}); });
#ifdef USEFS
server.on("/edit", HTTP_PUT, handleFileCreate);
server.on("/edit", HTTP_DELETE, handleFileDelete);
server.on("/edit", HTTP_POST, [](){ server->send(200, "text/plain", ""); }, handleFileUpload);
server.on("/list", HTTP_GET, handleFileList);
#endif #endif
//init ota page //init ota page
#ifndef WLED_DISABLE_OTA #ifndef WLED_DISABLE_OTA
@ -146,7 +148,7 @@ void initServer()
#else #else
server.on("/update", HTTP_GET, [](AsyncWebServerRequest *request){ server.on("/update", HTTP_GET, [](AsyncWebServerRequest *request){
serveMessage(request, 500, "Not implemented", "OTA updates are unsupported in this build.", 254); serveMessage(request, 501, "Not implemented", "OTA updates are disabled in this build.", 254);
}); });
#endif #endif
} else } else
@ -157,9 +159,6 @@ void initServer()
server.on("/update", HTTP_GET, [](AsyncWebServerRequest *request){ server.on("/update", HTTP_GET, [](AsyncWebServerRequest *request){
serveMessage(request, 500, "Access Denied", "Please unlock OTA in security settings!", 254); serveMessage(request, 500, "Access Denied", "Please unlock OTA in security settings!", 254);
}); });
server.on("/list", HTTP_GET, [](AsyncWebServerRequest *request){
serveMessage(request, 500, "Access Denied", "Please unlock OTA in security settings!", 254);
});
} }
server.on("/", HTTP_GET, [](AsyncWebServerRequest *request){ server.on("/", HTTP_GET, [](AsyncWebServerRequest *request){
@ -177,12 +176,14 @@ void initServer()
request->send(200); return; request->send(200); return;
} }
if(!handleSet(request, request->url())){ if(handleSet(request, request->url())) return;
#ifndef WLED_DISABLE_ALEXA #ifndef WLED_DISABLE_ALEXA
if(!espalexa.handleAlexaApiCall(request)) if(espalexa.handleAlexaApiCall(request)) return;
#endif
#ifdef WLED_ENABLE_FS_SERVING
if(handleFileRead(request, request->url())) return;
#endif #endif
request->send(404, "text/plain", "Not Found"); request->send(404, "text/plain", "Not Found");
}
}); });
} }
@ -218,6 +219,16 @@ void serveIndex(AsyncWebServerRequest* request)
if (uiConfiguration == 0 && request->hasHeader("User-Agent")) serveMobile = checkClientIsMobile(request->getHeader("User-Agent")->value()); if (uiConfiguration == 0 && request->hasHeader("User-Agent")) serveMobile = checkClientIsMobile(request->getHeader("User-Agent")->value());
else if (uiConfiguration == 2) serveMobile = true; else if (uiConfiguration == 2) serveMobile = true;
#ifdef WLED_ENABLE_FS_SERVING
if (serveMobile)
{
if (handleFileRead(request, "/index_mobile.htm")) return;
} else
{
if (handleFileRead(request, "/index.htm")) return;
}
#endif
AsyncWebServerResponse *response = request->beginResponse_P(200, "text/html", AsyncWebServerResponse *response = request->beginResponse_P(200, "text/html",
(serveMobile) ? (uint8_t*)PAGE_indexM : PAGE_index, (serveMobile) ? (uint8_t*)PAGE_indexM : PAGE_index,
(serveMobile) ? PAGE_indexM_L : PAGE_index_L); (serveMobile) ? PAGE_indexM_L : PAGE_index_L);
@ -235,7 +246,13 @@ void serveIndex(AsyncWebServerRequest* request)
String msgProcessor(const String& var) String msgProcessor(const String& var)
{ {
if (var == "CSS") return String(obuf); if (var == "CSS") {
char css[512];
obuf = css;
olen = 0;
getCSSColors();
return String(obuf);
}
if (var == "MSG") { if (var == "MSG") {
String messageBody = messageHead; String messageBody = messageHead;
messageBody += "</h2>"; messageBody += "</h2>";
@ -266,8 +283,10 @@ String msgProcessor(const String& var)
void serveMessage(AsyncWebServerRequest* request, uint16_t code, String headl, String subl="", byte optionT=255) void serveMessage(AsyncWebServerRequest* request, uint16_t code, String headl, String subl="", byte optionT=255)
{ {
char buf[512]; #ifndef ARDUINO_ARCH_ESP32
char buf[256];
obuf = buf; obuf = buf;
#endif
olen = 0; olen = 0;
getCSSColors(); getCSSColors();
messageHead = headl; messageHead = headl;
@ -281,9 +300,10 @@ void serveMessage(AsyncWebServerRequest* request, uint16_t code, String headl, S
String settingsProcessor(const String& var) String settingsProcessor(const String& var)
{ {
if (var == "CSS") { if (var == "CSS") {
char* buf = getSettingsJS(optionType); char buf[2048];
getSettingsJS(optionType, buf);
getCSSColors(); getCSSColors();
return buf; return String(buf);
} }
if (var == "SCSS") return String(PAGE_settingsCss); if (var == "SCSS") return String(PAGE_settingsCss);
return String(); return String();

View File

@ -195,7 +195,7 @@ void serializeInfo(JsonObject& root)
#ifndef WLED_DISABLE_CRONIXIE #ifndef WLED_DISABLE_CRONIXIE
os += 0x10; os += 0x10;
#endif #endif
#ifdef USEFS #ifndef WLED_DISABLE_FILESYSTEM
os += 0x08; os += 0x08;
#endif #endif
#ifndef WLED_DISABLE_HUESYNC #ifndef WLED_DISABLE_HUESYNC