@@ -79,7 +79,7 @@ ADF4351 adf(clock, data, LE, CE);
7979Adafruit_TSL2591 tsl = Adafruit_TSL2591(2591 );
8080
8181// TSL2591 settings storage
82- tsl2591Gain_t currentGain = TSL2591_GAIN_MAX ;
82+ tsl2591Gain_t currentGain = TSL2591_GAIN_HIGH ;
8383tsl2591IntegrationTime_t currentIntegrationTime = TSL2591_INTEGRATIONTIME_100MS ;
8484
8585// Cached sensor value for non-blocking reads (P0 #6)
@@ -623,6 +623,112 @@ void handleMeasureRatio()
623623 setLEDStatus (LED_CONNECTED );
624624}
625625
626+ // SSE (Server-Sent Events) endpoint for streaming frequency sweep.
627+ // The ESP32 loops through frequencies, measures each point, and streams
628+ // results back in real-time. Frontend uses EventSource to receive data.
629+ // Closing the EventSource connection stops the sweep immediately.
630+ void handleSweep ()
631+ {
632+ if (!server.hasArg (" f_begin" ) || !server.hasArg (" f_end" ) || !server.hasArg (" f_step" ))
633+ {
634+ server.send (400 , " application/json" , " {\" error\" :\" need f_begin, f_end, f_step\" }" );
635+ return ;
636+ }
637+
638+ float fBegin = server.arg (" f_begin" ).toFloat ();
639+ float fEnd = server.arg (" f_end" ).toFloat ();
640+ float fStep = server.arg (" f_step" ).toFloat ();
641+
642+ uint8_t averages = 1 ;
643+ if (server.hasArg (" avg" ))
644+ {
645+ int tmp = server.arg (" avg" ).toInt ();
646+ averages = (uint8_t )constrain (tmp, 1 , 20 );
647+ }
648+ uint16_t settle_ms = 10 ;
649+ if (server.hasArg (" settle" ))
650+ {
651+ settle_ms = (uint16_t )constrain (server.arg (" settle" ).toInt (), 1 , 200 );
652+ }
653+
654+ if (fBegin < ADF_FREQ_MIN || fEnd > ADF_FREQ_MAX || fStep <= 0 || fBegin >= fEnd )
655+ {
656+ server.send (400 , " application/json" , " {\" error\" :\" invalid sweep parameters\" }" );
657+ return ;
658+ }
659+
660+ int totalPoints = (int )((fEnd - fBegin ) / fStep ) + 1 ;
661+ if (totalPoints > 500 )
662+ {
663+ server.send (400 , " application/json" , " {\" error\" :\" too many points (max 500)\" }" );
664+ return ;
665+ }
666+
667+ Serial.printf (" Sweep start: %.1f -> %.1f MHz, step %.1f, avg %d, settle %d ms, %d pts\n " ,
668+ fBegin , fEnd , fStep , averages, settle_ms, totalPoints);
669+
670+ setLEDStatus (LED_MEASURING );
671+ adf.begin ();
672+
673+ // Send SSE response headers
674+ server.sendHeader (" Cache-Control" , " no-cache" );
675+ server.sendHeader (" Connection" , " keep-alive" );
676+ server.setContentLength (CONTENT_LENGTH_UNKNOWN );
677+ server.send (200 , " text/event-stream" , " " );
678+
679+ WiFiClient client = server.client ();
680+ int pointIndex = 0 ;
681+
682+ for (float f = fBegin ; f <= fEnd + 0 .001f ; f += fStep )
683+ {
684+ // Check if client disconnected (user pressed stop / closed page)
685+ if (!client.connected ())
686+ {
687+ Serial.println (" Sweep: client disconnected, stopping" );
688+ break ;
689+ }
690+
691+ adf.updateFrequency (f * 1e6 ); // MHz -> Hz
692+ delay (settle_ms); // PLL settle time
693+
694+ // Average multiple readings for noise reduction
695+ uint64_t sum = 0 ;
696+ for (uint8_t i = 0 ; i < averages; i++)
697+ {
698+ sum += readIR ();
699+ if (averages > 1 )
700+ delay (1 );
701+ }
702+ uint32_t intensity = (uint32_t )(sum / averages);
703+
704+ // Send data point as SSE event
705+ String msg = " data: {\" f\" :" ;
706+ msg += String (f, 1 );
707+ msg += " ,\" I\" :" ;
708+ msg += String (intensity);
709+ msg += " ,\" idx\" :" ;
710+ msg += String (pointIndex);
711+ msg += " ,\" total\" :" ;
712+ msg += String (totalPoints);
713+ msg += " }\n\n " ;
714+ server.sendContent (msg);
715+
716+ pointIndex++;
717+
718+ // Keep captive portal DNS responsive during long sweeps
719+ dnsServer.processNextRequest ();
720+ updateLEDs ();
721+ }
722+
723+ // Signal sweep completion
724+ server.sendContent (" data: {\" done\" :true}\n\n " );
725+ server.sendContent (" " ); // End chunked transfer
726+
727+ adf.stop ();
728+ setLEDStatus (LED_CONNECTED );
729+ Serial.printf (" Sweep complete: %d points measured\n " , pointIndex);
730+ }
731+
626732void setup ()
627733{
628734
@@ -789,8 +895,15 @@ void setup()
789895 { handleFileRequest (" /index.html" ); });
790896
791897 // Captive portal detection endpoints (P0 #1)
792- // Returning a non-"Success" HTML page triggers the OS captive portal popup,
793- // which displays a button letting users open http://192.168.4.1 directly.
898+ // Returning a non-"Success" HTML page triggers the OS captive portal popup.
899+ // The page uses platform-specific tricks to open the real browser instead of
900+ // staying inside the restricted captive-portal WebView:
901+ // Android : intent:// URI → system fires an Intent → default browser opens
902+ // iOS CNA : window.open + location fallback; OS also shows its own "Done" /
903+ // "Open in Safari" controls in the status bar
904+ // Portal page for Android captive portal WebView.
905+ // Plain <a> link works reliably in all captive portal WebViews.
906+ // No intent:// URIs or window.open() which fail in restricted contexts.
794907 static const char PORTAL_HTML [] =
795908 " <!DOCTYPE html><html><head>"
796909 " <meta charset='UTF-8'>"
@@ -800,36 +913,45 @@ void setup()
800913 " body{font-family:sans-serif;text-align:center;padding:2.5rem 1rem;"
801914 " background:#1a4fa0;color:#fff;margin:0}"
802915 " h1{font-size:1.5rem;margin-bottom:.5rem}"
803- " p{opacity:.8 ;margin-bottom:2rem ;font-size:.95rem}"
804- " a .btn{display:inline-block;background:#fff;color:#1a4fa0;"
916+ " p{opacity:.85 ;margin-bottom:1.5rem ;font-size:.95rem}"
917+ " .btn{display:inline-block;background:#fff;color:#1a4fa0;"
805918 " text-decoration:none;padding:.75rem 2.5rem;border-radius:8px;"
806919 " font-weight:bold;font-size:1.1rem;box-shadow:0 2px 8px rgba(0,0,0,.2)}"
920+ " .hint{margin-top:1.5rem;font-size:.82rem;opacity:.6}"
807921 " </style></head><body>"
808922 " <h1>🔬 openUC2 ODMR</h1>"
809- " <p>Connected to NV-Experiment device.<br>Open the dashboard to start.</p>"
810- " <a class='btn' href='http://192.168.4.1'>Open Dashboard</a>"
923+ " <p>Connected to NV-Experiment device.</p>"
924+ " <a class='btn' href='http://192.168.4.1/'>Open Dashboard</a>"
925+ " <p class='hint'>If this page stays inside a small popup, open your<br>"
926+ " browser and navigate to: <b>http://192.168.4.1</b></p>"
811927 " </body></html>" ;
812928
813- // Android: return 200 + HTML so Android shows captive portal notification
929+
930+ // Android: return 200 + HTML so Android shows "Sign in to network" notification.
931+ // When user taps it, the captive portal WebView opens and shows our portal page.
814932 server.on (" /generate_204" , HTTP_GET , []()
815933 { server.send (200 , " text/html" , PORTAL_HTML ); });
816934
817- // Microsoft Windows captive portal detection (keep plain text – Windows does
818- // not show a popup browser; it just checks for internet connectivity)
935+ // Microsoft Windows captive portal detection – return expected plain-text
936+ // responses so Windows does not flag the network as limited.
819937 server.on (" /connecttest.txt" , HTTP_GET , []()
820938 { server.send (200 , " text/plain" , " Microsoft Connect Test" ); });
821939 server.on (" /ncsi.txt" , HTTP_GET , []()
822940 { server.send (200 , " text/plain" , " Microsoft NCSI" ); });
941+ server.on (" /redirect" , HTTP_GET , []()
942+ { server.send (200 , " text/html" , PORTAL_HTML ); });
823943
824- // Apple iOS/macOS: body != "Success" triggers the captive portal mini-browser
944+ // Apple iOS / macOS: returning "Success" tells the OS the network has internet.
945+ // This prevents the CNA (Captive Network Assistant) mini-browser from opening
946+ // and keeps the WiFi connection stable. Users open Safari and go to 192.168.4.1.
825947 server.on (" /hotspot-detect.html" , HTTP_GET , []()
826- { server.send (200 , " text/html" , PORTAL_HTML ); });
948+ { server.send (200 , " text/html" , " Success " ); });
827949 server.on (" /library/test/success.html" , HTTP_GET , []()
828- { server.send (200 , " text/html" , PORTAL_HTML ); });
950+ { server.send (200 , " text/html" , " Success " ); });
829951
830952 // Additional OS probe endpoints
831953 server.on (" /success.txt" , HTTP_GET , []()
832- { server.send (200 , " text/html " , PORTAL_HTML ); });
954+ { server.send (200 , " text/plain " , " success " ); });
833955 server.on (" /canonical.html" , HTTP_GET , []()
834956 { server.send (200 , " text/html" , PORTAL_HTML ); });
835957
@@ -838,8 +960,18 @@ void setup()
838960 server.on (" /favicon.ico" , HTTP_GET , []()
839961 { server.send (204 , " image/x-icon" , " " ); });
840962
841- server.onNotFound ([]()
842- { handleFileRequest (server.uri ()); });
963+ server.onNotFound ([]() {
964+ String uri = server.uri ();
965+ // Silently return 404 for known OS background requests (Windows Update, etc.)
966+ // to avoid log spam from e.g. /msdownload/update/... certificate fetches
967+ if (uri.startsWith (" /msdownload/" ) || uri.endsWith (" .cab" ) ||
968+ uri.startsWith (" /GTSLT" ) || uri.startsWith (" /ocsp" ))
969+ {
970+ server.send (404 , " text/plain" , " " );
971+ return ;
972+ }
973+ handleFileRequest (uri);
974+ });
843975 server.on (" /odmr_act" , HTTP_POST , handleOdmrAct);
844976 server.on (" /measure" , HTTP_GET , handleMeasure);
845977 server.on (" /intensity" , HTTP_GET , handleIntensity);
@@ -848,6 +980,7 @@ void setup()
848980 server.on (" /tsl/gain" , HTTP_POST , handleSetTSLGain);
849981 server.on (" /tsl/integration_time" , HTTP_POST , handleSetTSLIntegrationTime);
850982 server.on (" /ratio" , HTTP_GET , handleMeasureRatio);
983+ server.on (" /sweep" , HTTP_GET , handleSweep);
851984
852985 // ADF4351 enable/disable endpoints (P1 #9)
853986 server.on (" /ADF_Enable" , HTTP_POST , []()
0 commit comments