Skip to content

Commit 83740a3

Browse files
committed
Add SSE sweep streaming, client & portal fixes
Implement Server-Sent-Events sweeping: add /sweep endpoint that streams frequency/intensity points (with averaging, settle time, point limit) and stops when the client disconnects. Update JS UI (messung.html and website copy) to use EventSource for real-time streaming, handle errors, stop behavior, and continuous mode. Improve captive-portal responses and notFound filtering to better support Android/iOS/Windows probes and reduce noisy OS requests. Change TSL2591 default gain to HIGH and update gain select defaults in HTML. Expand README with explicit local PlatformIO/esptool build & flash commands.
1 parent 497c66b commit 83740a3

6 files changed

Lines changed: 281 additions & 75 deletions

File tree

Production_Files/Software/ODMR_Server/README.md

Lines changed: 14 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -388,19 +388,28 @@ Use PlatformIO to build and flash the firmware:
388388
pio run -e seeed_xiao_esp32s3 --target upload
389389

390390
# For ESP32-C3 (with improved serial)
391-
pio run -e seeed_xiao_esp32c3 --target upload
392-
393-
391+
/Users/bene/.platformio/penv/bin/pio run -e seeed_xiao_esp32c3 --target upload
394392

395393
# 1. Alles bauen
396-
pio run -e seeed_xiao_esp32c3 && pio run -e seeed_xiao_esp32c3 -t buildfs
394+
cd /Users/bene/Dropbox/Dokumente/Promotion/PROJECTS/TechnicalDocs-openUC2-QBox/Production_Files/Software/ODMR_Server/
395+
/Users/bene/.platformio/penv/bin/pio run -e seeed_xiao_esp32c3 && /Users/bene/.platformio/penv/bin/pio run -e seeed_xiao_esp32c3 -t buildfs
397396

398397
# 2. Mergen
399-
pio run -e seeed_xiao_esp32c3 -t mergedbin
398+
/Users/bene/.platformio/penv/bin/pio run -e seeed_xiao_esp32c3 -t mergedbin
400399

401400
# Output: build/fw-images/seeed_xiao_esp32c3.bin → direkt an 0x0 flashen
402401
/Users/bene/.platformio/penv/bin/pio run -e seeed_xiao_esp32c3 -t upload_merged --upload-port /dev/cu.usbmodem101
403402

403+
/Users/bene/.platformio/penv/bin/python -m esptool \
404+
--chip esp32c3 \
405+
-p /dev/cu.usbmodem101 \
406+
-b 460800 \
407+
write-flash \
408+
--flash-mode keep \
409+
--flash-freq keep \
410+
--flash-size keep \
411+
0x0 /Users/bene/Dropbox/Dokumente/Promotion/PROJECTS/TechnicalDocs-openUC2-QBox/Production_Files/Software/ODMR_Server/build/fw-images/seeed_xiao_esp32c3.bin
412+
404413
/Users/bene/.platformio/penv/bin/python -m esptool \
405414
--chip esp32c3 -p /dev/cu.usbmodem101 -b 460800 \
406415
write-flash --flash-mode keep --flash-freq keep --flash-size keep \

Production_Files/Software/ODMR_Server/data/justage.html

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -118,8 +118,8 @@ <h5>Sensor-Einstellungen</h5>
118118
<select class="form-select" id="gainSelect">
119119
<option value="0x00">Low (1x)</option>
120120
<option value="0x10">Medium (25x)</option>
121-
<option value="0x20">High (428x)</option>
122-
<option value="0x30" selected>Max (9876x)</option>
121+
<option value="0x20" selected>High (428x)</option>
122+
<option value="0x30">Max (9876x)</option>
123123
</select>
124124
</div>
125125

Production_Files/Software/ODMR_Server/data/messung.html

Lines changed: 57 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,8 @@
3333
const fMin=2200, fMax=4400, stepMin=0.2, stepMax=5;
3434
const IntMin=0, IntMax=65535, IntRangeMin=200;
3535
var stopRequested = false; // Global stop flag (P0 #5)
36-
var continuousTimeout = null; // Track setTimeout for continuous mode
36+
var continuousTimeout = null; // Track setTimeout for continuous mode
37+
var currentEventSource = null; // Track SSE connection for streaming sweep
3738
const XPlotSize=1600, YPlotSize=800;
3839
var AutoMessung=0;
3940
const PlotViewBox="-200 -" + (YPlotSize+75) + " " + (XPlotSize+200+150) + " " + (YPlotSize+75+150);
@@ -52,48 +53,79 @@
5253
</script>
5354

5455
<script>
55-
async function MessungStarten(){
56+
// Start measurement using SSE (Server-Sent Events) streaming.
57+
// The ESP32 performs the frequency sweep and streams each data point
58+
// back in real time. No individual HTTP requests per frequency needed.
59+
function MessungStarten(){
5660
stopRequested = false;
5761
ToggleStart(0);
5862
setLEDStatus('measuring', 'Messung läuft');
59-
enableADF();
6063

6164
deleteScaleGroup("DataGroupID");
6265
AngezeigteDatenpunkte=[];
63-
_YMax=0, _YMin=65535;
66+
_YMax=0; _YMin=65535;
6467
prepareFreqs();
6568
prepareXSkala(fBegin,fEnd);
66-
while(Frequenzen.length!=0 && !stopRequested){
69+
70+
// Build SSE URL from current sweep parameters
71+
var url = '/sweep?f_begin=' + fBegin + '&f_end=' + fEnd + '&f_step=' + fStep;
72+
currentEventSource = new EventSource(url);
73+
74+
currentEventSource.onmessage = function(event) {
6775
try {
68-
var response=await freqMessung(Frequenzen.shift());
69-
Messreihe.push(response);
70-
_YMin=Math.min(_YMin,response[1]-100);
71-
_YMax=Math.max(_YMax,response[1]+100);
76+
var data = JSON.parse(event.data);
77+
if (data.done) {
78+
currentEventSource.close();
79+
currentEventSource = null;
80+
addDataGroup();
81+
Messreihe = [];
82+
setLEDStatus('ready', 'Messungen abgeschlossen');
83+
if (AutoMessung && !stopRequested) {
84+
continuousTimeout = setTimeout(MessungStarten, 600);
85+
} else {
86+
ToggleStart(1);
87+
disableADF();
88+
stopRequested = false;
89+
}
90+
return;
91+
}
92+
// Process streamed data point [freq, intensity, magnetic_field_placeholder]
93+
var response = [data.f, data.I, 0];
94+
Messreihe.push(response);
95+
_YMin = Math.min(_YMin, response[1] - 100);
96+
_YMax = Math.max(_YMax, response[1] + 100);
7297
autoScale();
73-
drawCircle(response,+200,"black");
98+
drawCircle(response, +200, "black");
7499
} catch(e) {
75-
console.warn('Skipped point:', e);
76-
Messreihe.push([NaN, NaN, NaN]);
100+
console.warn('SSE parse error:', e);
77101
}
78-
}
79-
addDataGroup();
80-
Messreihe=[];
102+
};
81103

82-
setLEDStatus('ready', 'Messungen abgeschlossen');
83-
84-
if(AutoMessung && !stopRequested){
85-
continuousTimeout = setTimeout(MessungStarten, 600);
86-
} else {
104+
currentEventSource.onerror = function(e) {
105+
console.warn('SSE connection error:', e);
106+
if (currentEventSource) {
107+
currentEventSource.close();
108+
currentEventSource = null;
109+
}
110+
addDataGroup();
111+
Messreihe = [];
112+
setLEDStatus('error', 'Verbindungsfehler');
87113
ToggleStart(1);
88-
disableADF();
89114
stopRequested = false;
90-
}
115+
};
91116
}
92-
// Stop measurement handler (P0 #5)
117+
// Stop measurement handler (P0 #5) - closes SSE connection which
118+
// signals the ESP32 to stop the sweep immediately
93119
function MessungStoppen(){
94120
stopRequested = true;
121+
if (currentEventSource) {
122+
currentEventSource.close();
123+
currentEventSource = null;
124+
}
95125
if(continuousTimeout) { clearTimeout(continuousTimeout); continuousTimeout = null; }
96126
AutoMessung = 0;
127+
addDataGroup();
128+
Messreihe = [];
97129
setLEDStatus('ready', 'Messung gestoppt');
98130
ToggleStart(1);
99131
disableADF();
@@ -631,8 +663,8 @@ <h6 class="mb-0" data-lang-key="sensor_settings">Sensor-Einstellungen</h6>
631663
data-bs-toggle="tooltip">
632664
<option value="0x00">Low (1x)</option>
633665
<option value="0x10">Medium (25x)</option>
634-
<option value="0x20">High (428x)</option>
635-
<option value="0x30" selected>Max (9876x)</option>
666+
<option value="0x20" selected>High (428x)</option>
667+
<option value="0x30">Max (9876x)</option>
636668
</select>
637669
</div>
638670
<div class="mb-3">

Production_Files/Software/ODMR_Server/src/main.cpp

Lines changed: 149 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -79,7 +79,7 @@ ADF4351 adf(clock, data, LE, CE);
7979
Adafruit_TSL2591 tsl = Adafruit_TSL2591(2591);
8080

8181
// TSL2591 settings storage
82-
tsl2591Gain_t currentGain = TSL2591_GAIN_MAX;
82+
tsl2591Gain_t currentGain = TSL2591_GAIN_HIGH;
8383
tsl2591IntegrationTime_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+
626732
void 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>&#128300; 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

Comments
 (0)