// Arduino-ESP32 core 3.x + ArduinoJson 7. Run build-kit.mjs to generate controller.h. // ESP32 DevKit example GPIO pins. Outputs disabled by default. #include #include #include #include "controller.h" const bool ENABLE_OUTPUTS=false; const int ESC_PIN=25, STEERING_PIN=26; const char* AP_NAME="skyvison-rc"; WebServer server(80); String key,apPassword,session;volatile uint32_t lastFrame=0;long lastSeq=-1;volatile bool armed=false; void pulse(int pin,int us){ledcWrite(pin,(uint32_t)((uint64_t)us*65535/20000));} void stopOutputs(){armed=false;if(ENABLE_OUTPUTS){pulse(ESC_PIN,1000);pulse(STEERING_PIN,1500);}} void reply(int code,const String& body){server.sendHeader("Cache-Control","no-store");server.send(code,"application/json",body);} bool authorized(){if(server.header("X-RC-Key")!=key){reply(403,"{\"error\":\"Pairing key rejected\"}");return false;}if(server.header("Content-Type").indexOf("application/json")!=0){reply(415,"{\"error\":\"JSON required\"}");return false;}return true;} void setup(){ Serial.begin(115200); if(ENABLE_OUTPUTS){if(!ledcAttach(ESC_PIN,50,16)||!ledcAttach(STEERING_PIN,50,16)){while(true)delay(1000);}} stopOutputs(); // Separate watchdog task: WebServer may block while reading a slow HTTP client. xTaskCreate([](void*){for(;;){if(millis()-lastFrame>500)stopOutputs();vTaskDelay(pdMS_TO_TICKS(10));}},"rc-watchdog",2048,nullptr,2,nullptr); char token[40],password[24];snprintf(token,sizeof(token),"%08lx%08lx%08lx",(unsigned long)esp_random(),(unsigned long)esp_random(),(unsigned long)esp_random());key=token;snprintf(password,sizeof(password),"rc%08lx%04lx",(unsigned long)esp_random(),(unsigned long)(esp_random()&65535));apPassword=password; WiFi.mode(WIFI_AP);WiFi.softAP(AP_NAME,apPassword.c_str(),1,false,1); const char* headers[]={"X-RC-Key","Content-Type"};server.collectHeaders(headers,2); server.on("/",HTTP_GET,[]{server.sendHeader("Cache-Control","no-store");server.sendHeader("Content-Security-Policy","default-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline'; img-src 'self' data:; connect-src 'self'; frame-ancestors 'none'; base-uri 'none'");server.send_P(200,"text/html",CONTROLLER_HTML);}); server.on("/api/session",HTTP_POST,[]{ if(!authorized())return; if(session.length()&&millis()-lastFrame<1000){reply(409,"{\"error\":\"Receiver busy; wait one second\"}");return;} stopOutputs();char id[32];snprintf(id,sizeof(id),"%08lx%08lx",(unsigned long)esp_random(),(unsigned long)esp_random());session=id;lastSeq=-1;lastFrame=millis(); reply(200,"{\"session\":\""+session+"\",\"watchdogMs\":500,\"outputs\":\""+(ENABLE_OUTPUTS?"enabled":"dry-run")+"\"}"); }); server.on("/api/control",HTTP_POST,[]{ if(!authorized())return; if(server.arg("plain").length()>2048){stopOutputs();reply(400,"{\"error\":\"Frame too large\"}");return;} JsonDocument d;auto error=deserializeJson(d,server.arg("plain")); if(error||!d["session"].is()||d["session"].as()!=session||session.length()==0){reply(403,"{\"error\":\"Session rejected\"}");return;} if(!d["seq"].is()||d["seq"].as()<=lastSeq){reply(409,"{\"error\":\"Stale sequence\"}");return;}lastSeq=d["seq"].as(); bool valid=d["armed"].is()&&d["deadman"].is(); String profile=d["profile"]|"";valid=valid&&(profile=="car"||profile=="boat"); for(const char* name:{"throttle","yaw","pitch","roll"}){if(!d[name].is()||!isfinite(d[name].as())||abs(d[name].as())>1)valid=false;} float t=d["throttle"]|0.0f,r=d["roll"]|0.0f;bool requested=d["armed"]|false; if(millis()-lastFrame>500)stopOutputs(); if(t<0)valid=false; if(requested&&!armed&&(t!=0||r!=0||d["yaw"].as()!=0||d["pitch"].as()!=0))valid=false; if(!valid){stopOutputs();reply(400,"{\"error\":\"Invalid profile, channel or neutral arming state\"}");return;} armed=requested;bool active=armed&&d["deadman"].as();lastFrame=millis(); if(ENABLE_OUTPUTS){pulse(ESC_PIN,active?1000+(int)(t*1000):1000);pulse(STEERING_PIN,active?1500+(int)(r*500):1500);} reply(200,"{\"armed\":"+String(armed?"true":"false")+",\"seq\":"+String(lastSeq)+"}"); }); server.onNotFound([]{reply(404,"{\"error\":\"Not found\"}");});server.begin(); Serial.printf("\nWi-Fi: %s\nWi-Fi password: %s\nController: http://192.168.4.1\nPairing key: %s\nOutputs: %s\n",AP_NAME,apPassword.c_str(),key.c_str(),ENABLE_OUTPUTS?"ENABLED":"DRY RUN"); } void loop(){if(millis()-lastFrame>500)stopOutputs();server.handleClient();if(millis()-lastFrame>500)stopOutputs();delay(1);}