Skip to content

Improve ODMR_Server stability and web handling#80

Merged
beniroquai merged 11 commits into
mainfrom
fix-listoferrors
Mar 25, 2026
Merged

Improve ODMR_Server stability and web handling#80
beniroquai merged 11 commits into
mainfrom
fix-listoferrors

Conversation

@beniroquai
Copy link
Copy Markdown
Contributor

Add ISSUES.md and multiple server improvements to reduce client stalls and captive-portal churn: stop redirecting OS probe endpoints (return success codes), return 404 for unknown non-HTML assets and 204 for favicon, remove external CDN dependency from HTML, and collect Accept headers for smarter routing. Implement cached sensor reads (periodic sampler + instant /intensity response) to avoid blocking on long sensor integration times. Fix SoftAP channel selection by passing channel into WiFi.softAP(...), add /version endpoint (uses updated version_info.h), add simple ADF enable/disable endpoints, and minor logging/diagnostic prints. Also remove the ODMR_CAMERA example files and update website HTML headers to match the offline-friendly changes.

Add ISSUES.md and multiple server improvements to reduce client stalls and captive-portal churn: stop redirecting OS probe endpoints (return success codes), return 404 for unknown non-HTML assets and 204 for favicon, remove external CDN dependency from HTML, and collect Accept headers for smarter routing. Implement cached sensor reads (periodic sampler + instant /intensity response) to avoid blocking on long sensor integration times. Fix SoftAP channel selection by passing channel into WiFi.softAP(...), add /version endpoint (uses updated version_info.h), add simple ADF enable/disable endpoints, and minor logging/diagnostic prints. Also remove the ODMR_CAMERA example files and update website HTML headers to match the offline-friendly changes.
Copilot AI review requested due to automatic review settings March 4, 2026 05:08
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR aims to improve the ESP32-based ODMR_Server’s stability in offline SoftAP mode by reducing captive-portal churn, preventing browser retry storms, and making UI interactions work without external Bootstrap JS/CDN dependencies. It also adds firmware-side endpoints and sensor read caching to reduce perceived stalls.

Changes:

  • Firmware: adjust captive-portal probe endpoints to return “success” codes, add /version + ADF enable/disable endpoints, add cached intensity sampling, and change unknown-asset handling to prefer 404s (unless HTML navigation).
  • Web UI: remove laser controls, unify intensity range constants to 16-bit (65535), add “Stop measurement”, add fetch timeouts/retries, and add lightweight JS to replace Bootstrap JS behaviors (navbar/dropdown/tabs).
  • Repo hygiene/docs: add ISSUES.md and remove the ODMR_CAMERA example project files.

Reviewed changes

Copilot reviewed 19 out of 21 changed files in this pull request and generated 7 comments.

Show a summary per file
File Description
Production_Files/Software/ODMR_Server/src/website_html/ratio.html Adds local JS for navbar/dropdown behavior without Bootstrap JS.
Production_Files/Software/ODMR_Server/src/website_html/messung_webserial.html Updates intensity range constants, removes laser UI/control, removes Bootstrap CDN JS, adds local JS replacements (incl. tabs).
Production_Files/Software/ODMR_Server/src/website_html/messung.html Adds stop/timeout/retry logic for WiFi measurements, removes laser UI/control, removes Bootstrap CDN JS, adds local JS replacement.
Production_Files/Software/ODMR_Server/src/website_html/justage.html Prevents overlapping /intensity requests, removes Bootstrap CDN JS, switches WebSerial gating logic, adds local JS replacement.
Production_Files/Software/ODMR_Server/src/website_html/infos.html Adds partners section, removes Bootstrap CDN JS, switches WebSerial gating logic, adds local JS replacement.
Production_Files/Software/ODMR_Server/src/website_html/index.html Switches WebSerial gating logic and adds local JS replacement (but still includes Bootstrap CDN JS).
Production_Files/Software/ODMR_Server/src/website/ratio_html.h Updates embedded ratio page HTML and adds local JS replacement (but drops WebSerial nav item).
Production_Files/Software/ODMR_Server/src/website/messung_html.h Updates embedded measurement page HTML for stop/timeout/retry + offline-friendly JS.
Production_Files/Software/ODMR_Server/src/website/justage_html.h Updates embedded alignment page HTML for offline-friendly behavior and request throttling.
Production_Files/Software/ODMR_Server/src/website/infos_html.h Updates embedded info page HTML for partners + offline-friendly behavior.
Production_Files/Software/ODMR_Server/src/website/index_html.h Updates embedded index page HTML for WebSerial gating + local JS replacement (but still includes Bootstrap CDN JS).
Production_Files/Software/ODMR_Server/src/version_info.h Updates build metadata constants used by /version.
Production_Files/Software/ODMR_Server/src/main.cpp Implements firmware stability changes (probe endpoints, 404 routing, cached sensor reads, SoftAP channel param, /version, ADF endpoints).
Production_Files/Software/ODMR_CAMERA/src/main.cpp Removed (camera example project cleanup).
Production_Files/Software/ODMR_CAMERA/platformio.ini Removed (camera example project cleanup).
Production_Files/Software/ODMR_CAMERA/display.py Removed (camera example project cleanup).
Production_Files/Software/ODMR_CAMERA/README.md Removed (camera example project cleanup).
Production_Files/Software/ODMR_CAMERA/.vscode/extensions.json Removed (camera example project cleanup).
Production_Files/Software/ODMR_CAMERA/.gitignore Removed (camera example project cleanup).
ISSUES.md Adds consolidated issue/fix tracking and rationale for changes.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

You can also share your feedback on Copilot code review. Take the survey.

server.send(200, "application/json", json); });

// Collect Accept header for 404 logic
server.collectHeaders("Accept");
Copy link

Copilot AI Mar 4, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

server.collectHeaders("Accept") is not a valid WebServer::collectHeaders call in the Arduino-ESP32 WebServer API (it expects an array of header keys + count). As written this is likely a compile error, and server.header("Accept") will remain empty.

Update this to use the correct overload (e.g., const char* keys[] = {"Accept"}; server.collectHeaders(keys, 1);).

Suggested change
server.collectHeaders("Accept");
const char *headerKeys[] = {"Accept"};
server.collectHeaders(headerKeys, 1);

Copilot uses AI. Check for mistakes.
Comment on lines +238 to +252
// Return 404 for unknown assets to prevent browser retries (P0 #3)
// Only serve index for HTML navigation requests
String accept = server.header("Accept");
if (actualPath.endsWith(".html") || accept.indexOf("text/html") >= 0)
{
Serial.print("Unknown HTML path redirected to index: ");
Serial.println(actualPath);
server.send_P(200, "text/html", INDEX_HTML);
}
else
{
Serial.print("404 Not Found: ");
Serial.println(actualPath);
server.send(404, "text/plain", "Not found");
}
Copy link

Copilot AI Mar 4, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

With the new 404-on-unknown-asset behavior, requests for NVGitter.png (referenced by the index page) will now return 404 and the image will never load. Since the repo already contains website/nvgitter_png.h, consider adding an explicit mapping/route in handleFileRequest() for /NVGitter.png and serving the binary data with the correct content-type and length.

Copilot uses AI. Check for mistakes.
Comment on lines 677 to +680
int wifiChannel = random(1, 12);
Serial.print("Using WiFi channel: ");
Serial.println(wifiChannel);
WiFi.channel(wifiChannel);
// Note: channel is passed to WiFi.softAP() directly (P1 #7)
Copy link

Copilot AI Mar 4, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

random(1, 12) generates values in the range [1, 11] (upper bound is exclusive), so channel 12 is never selected. If the intent is to randomize across channels 1–12, change this to random(1, 13) (or otherwise include 12).

Copilot uses AI. Check for mistakes.
Comment on lines +420 to +431
<script>
/* Bootstrap-JS replacement: navbar collapse + dropdown */
(function(){
document.querySelectorAll('.navbar-toggler').forEach(function(b){
b.addEventListener('click',function(){var t=document.querySelector(b.getAttribute('data-bs-target'));if(t)t.classList.toggle('show');});
});
document.querySelectorAll('.dropdown-toggle').forEach(function(b){
b.addEventListener('click',function(e){e.preventDefault();var m=b.nextElementSibling;if(m&&m.classList.contains('dropdown-menu'))m.classList.toggle('show');});
});
document.addEventListener('click',function(e){if(!e.target.closest('.dropdown'))document.querySelectorAll('.dropdown-menu.show').forEach(function(m){m.classList.remove('show');});});
})();
</script>
Copy link

Copilot AI Mar 4, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This page adds a custom “Bootstrap-JS replacement” script, but it still loads Bootstrap JS from the external CDN earlier in the document. In offline AP mode that CDN request can still stall page load, and you may also get duplicate/conflicting behavior.

Remove the CDN <script src="https://cdn.jsdelivr.net/...bootstrap.bundle.min.js"> from this page if the replacement is intended to fully replace Bootstrap JS.

Copilot uses AI. Check for mistakes.
#define __INDEX_HTML_H__

const char INDEX_HTML[] PROGMEM = "<!DOCTYPE html>\n<html lang=\"de\">\n<head>\n <meta charset=\"UTF-8\">\n <meta name=\"viewport\" content=\"width=device-width, initial-scale=1\">\n <title>NV-Experimente / ODMR</title>\n\n <!-- Offline-capable CSS framework -->\n <link rel=\"stylesheet\" href=\"style.css\">\n</head>\n\n<body>\n <!-- ░░ Navbar ░░----------------------------------------------------------- -->\n <nav class=\"navbar navbar-expand-lg shadow-sm mb-4\">\n <div class=\"container\">\n <a class=\"navbar-brand fw-bold\" href=\"index.html\">NV-Experimente</a>\n <button class=\"navbar-toggler\" type=\"button\" data-bs-toggle=\"collapse\"\n data-bs-target=\"#nvNav\" aria-controls=\"nvNav\" aria-expanded=\"false\">\n <span class=\"navbar-toggler-icon\"></span>\n </button>\n\n <div class=\"collapse navbar-collapse\" id=\"nvNav\">\n <ul class=\"navbar-nav ms-auto\">\n <li class=\"nav-item\"><a class=\"nav-link active\" href=\"index.html\" data-lang-key=\"nav_start\">Start</a></li>\n <li class=\"nav-item\"><a class=\"nav-link\" href=\"messung.html\" data-lang-key=\"nav_measurement_device\">Messung (on Device)</a></li>\n <li class=\"nav-item\" id=\"webSerialNavItem\"><a class=\"nav-link\" href=\"messung_webserial.html\" data-lang-key=\"nav_measurement_webserial\">Messung (WebSerial)</a></li>\n <li class=\"nav-item\"><a class=\"nav-link\" href=\"ratio.html\" data-lang-key=\"nav_ratio\">B-Field Monitor</a></li>\n <li class=\"nav-item\"><a class=\"nav-link\" href=\"justage.html\" data-lang-key=\"nav_alignment\">Justage</a></li>\n <li class=\"nav-item\"><a class=\"nav-link\" href=\"infos.html\" data-lang-key=\"nav_info\">Weitere Infos</a></li>\n <li class=\"nav-item dropdown\">\n <a class=\"nav-link dropdown-toggle\" href=\"#\" id=\"langDropdown\" role=\"button\" data-bs-toggle=\"dropdown\" aria-expanded=\"false\">\n 🌐 DE\n </a>\n <ul class=\"dropdown-menu\" aria-labelledby=\"langDropdown\">\n <li><a class=\"dropdown-item\" href=\"#\" onclick=\"setLanguage('de')\">🇩🇪 Deutsch</a></li>\n <li><a class=\"dropdown-item\" href=\"#\" onclick=\"setLanguage('en')\">🇺🇸 English</a></li>\n </ul>\n </li>\n </ul>\n </div>\n </div>\n </nav>\n\n <!-- ░░ Main content ░░------------------------------------------------------ -->\n <main class=\"container\">\n <section class=\"mb-5\">\n <h1 class=\"display-5 mb-3\" data-lang-key=\"welcome_title\">Low-Cost Experimente mit NV-Zentren</h1>\n \n <!-- Introduction Section -->\n <div class=\"row\">\n <div class=\"col-lg-8\">\n <h2 class=\"h4 mb-3\" data-lang-key=\"odmr_intro_title\">Tutorial: Building an ODMR Setup</h2>\n <p class=\"lead\" data-lang-key=\"odmr_intro_text\">\n In diesem Workshop konstruieren wir ein ODMR (Optically Detected Magnetic Resonance) System mit dem UC2 modularen Mikroskop-Toolbox und NV (Nitrogen-Vacancy) Diamanten. ODMR ist eine Quantensensing-Technik, die es uns ermöglicht, Magnetfelder durch Beobachtung von Fluoreszenzänderungen in Quantensystemen zu messen.\n </p>\n </div>\n <div class=\"col-lg-4\">\n <div class=\"card\">\n <div class=\"card-body text-center\">\n <h6 class=\"card-title\" data-lang-key=\"quick_start\">Quick Start</h6>\n <div class=\"d-grid gap-2\">\n <a href=\"messung.html\" class=\"btn btn-primary\" data-lang-key=\"start_measurement\">Messung starten</a>\n <a href=\"justage.html\" class=\"btn btn-secondary\" data-lang-key=\"alignment\">Aufbau justieren</a>\n </div>\n </div>\n </div>\n </div>\n </div>\n </section>\n\n <!-- Theory Section -->\n <section class=\"mb-5\">\n <h2 class=\"h4 mb-3\" data-lang-key=\"theory_title\">Theoretischer Hintergrund</h2>\n \n <div class=\"row\">\n <div class=\"col-lg-6\">\n <h3 class=\"h5 mb-3\" data-lang-key=\"nv_centers_title\">Was sind NV-Zentren?</h3>\n <p data-lang-key=\"nv_centers_text\">\n NV-Zentren sind Fehlstellen in Diamanten, bestehend aus einem Stickstoffatom neben einer Vakanz (Leerstelle). Diese Quantensysteme haben einzigartige Eigenschaften, die sie ideal für Sensoranwendungen machen:\n </p>\n <ul>\n <li data-lang-key=\"nv_property_1\">Spin-1 Grundzustand mit drei möglichen Projektionen</li>\n <li data-lang-key=\"nv_property_2\">Optische Anregung bei 532 nm (grün)</li>\n <li data-lang-key=\"nv_property_3\">Fluoreszenz im roten Spektralbereich</li>\n <li data-lang-key=\"nv_property_4\">Raumtemperatur-stabile Quantenkohärenz</li>\n </ul>\n </div>\n <div class=\"col-lg-6\">\n <figure class=\"text-center\">\n <img src=\"NVGitter.png\" class=\"img-fluid rounded shadow\" alt=\"NV Center Structure\" data-lang-key=\"nv_structure_alt\">\n <figcaption class=\"mt-2 small text-muted\" data-lang-key=\"nv_structure_caption\">\n Struktur des NV-Zentrums im Diamantgitter\n </figcaption>\n </figure>\n </div>\n </div>\n \n <div class=\"row mt-4\">\n <div class=\"col-12\">\n <h3 class=\"h5 mb-3\" data-lang-key=\"odmr_principle_title\">ODMR-Prinzip</h3>\n <p data-lang-key=\"odmr_principle_text\">\n Der ODMR-Effekt basiert auf spinabhängiger Fluoreszenz. Wenn Mikrowellenstrahlung bei der Resonanzfrequenz (~2,87 GHz) angewendet wird, verursacht sie Übergänge zwischen Quantenspinzuständen, was zu einer messbaren Abnahme der Fluoreszenzintensität führt.\n </p>\n \n <div class=\"alert alert-info\">\n <h6 data-lang-key=\"key_concept\">Schlüsselkonzept:</h6>\n <p class=\"mb-0\" data-lang-key=\"zeeman_effect_text\">\n Externe Magnetfelder verschieben die Resonanzfrequenzen durch den Zeeman-Effekt, wodurch präzise Magnetfeldmessungen ermöglicht werden.\n </p>\n </div>\n </div>\n </div>\n </section>\n\n <!-- Applications Section -->\n <section class=\"mb-5\">\n <h2 class=\"h4 mb-3\" data-lang-key=\"applications_title\">Moderne Anwendungen</h2>\n \n <div class=\"row\">\n <div class=\"col-md-6 col-lg-3 mb-3\">\n <div class=\"card h-100\">\n <div class=\"card-body text-center\">\n <h6 class=\"card-title\" data-lang-key=\"app_biomedical\">Biomedizinische Bildgebung</h6>\n <p class=\"card-text small\" data-lang-key=\"app_biomedical_desc\">\n Kartierung von Magnetfeldern in lebenden Zellen und Geweben\n </p>\n </div>\n </div>\n </div>\n <div class=\"col-md-6 col-lg-3 mb-3\">\n <div class=\"card h-100\">\n <div class=\"card-body text-center\">\n <h6 class=\"card-title\" data-lang-key=\"app_materials\">Materialwissenschaft</h6>\n <p class=\"card-text small\" data-lang-key=\"app_materials_desc\">\n Untersuchung magnetischer Domänen und Spintransport\n </p>\n </div>\n </div>\n </div>\n <div class=\"col-md-6 col-lg-3 mb-3\">\n <div class=\"card h-100\">\n <div class=\"card-body text-center\">\n <h6 class=\"card-title\" data-lang-key=\"app_quantum\">Quanteninformation</h6>\n <p class=\"card-text small\" data-lang-key=\"app_quantum_desc\">\n Bausteine für Quantencomputer und -netzwerke\n </p>\n </div>\n </div>\n </div>\n <div class=\"col-md-6 col-lg-3 mb-3\">\n <div class=\"card h-100\">\n <div class=\"card-body text-center\">\n <h6 class=\"card-title\" data-lang-key=\"app_physics\">Fundamentale Physik</h6>\n <p class=\"card-text small\" data-lang-key=\"app_physics_desc\">\n Test der Quantenmechanik und Messung von Fundamentalkonstanten\n </p>\n </div>\n </div>\n </div>\n </div>\n </section>\n\n <!-- Setup Overview Section -->\n <section class=\"mb-5\">\n <h2 class=\"h4 mb-3\" data-lang-key=\"setup_overview_title\">Versuchsaufbau</h2>\n \n <div class=\"row\">\n <div class=\"col-lg-8\">\n <p data-lang-key=\"setup_overview_text\">\n Der ODMR-Aufbau folgt konfokalen Mikroskopieprinzipien und kombiniert optische Anregung, Mikrowellenmanipulation und Fluoreszenzdetektion für hochpräzise Quantensensing.\n </p>\n \n <h3 class=\"h6 mb-2\" data-lang-key=\"components_needed\">Benötigte Komponenten:</h3>\n <div class=\"row\">\n <div class=\"col-md-6\">\n <ul class=\"list-unstyled\">\n <li data-lang-key=\"component_1\">• Grundplatte für Montage</li>\n <li data-lang-key=\"component_2\">• Grüne Laserdiode (532 nm)</li>\n <li data-lang-key=\"component_3\">• 45° Spiegel für Strahlführung</li>\n <li data-lang-key=\"component_4\">• Strahlteiler mit Filter</li>\n <li data-lang-key=\"component_5\">• Konvergente Linse</li>\n </ul>\n </div>\n <div class=\"col-md-6\">\n <ul class=\"list-unstyled\">\n <li data-lang-key=\"component_6\">• Lichtsensor (Photodiode)</li>\n <li data-lang-key=\"component_7\">• Elektronik-Box mit Mikrowellenerzeugung</li>\n <li data-lang-key=\"component_8\">• XY-Bühnensystem mit NV-Diamant</li>\n <li data-lang-key=\"component_9\">• Magnet für externes Magnetfeld</li>\n <li data-lang-key=\"component_10\">• Mikrowellenantenne</li>\n </ul>\n </div>\n </div>\n </div>\n <div class=\"col-lg-4\">\n <div class=\"alert alert-warning\">\n <h6 class=\"alert-heading\" data-lang-key=\"safety_title\">⚠️ Sicherheitshinweise</h6>\n <ul class=\"mb-0 small\">\n <li data-lang-key=\"safety_laser\">Niemals direkt in den Laser blicken</li>\n <li data-lang-key=\"safety_magnet\">Vorsicht bei Implantaten und elektronischen Geräten</li>\n <li data-lang-key=\"safety_power\">Stromversorgung vor Verkabelungsänderungen trennen</li>\n </ul>\n </div>\n </div>\n </div>\n </section>\n\n <!-- QuantumMiniLabs Section -->\n <section class=\"mb-5\">\n <div class=\"card\">\n <div class=\"card-body\">\n <h2 class=\"h4 mb-3\" data-lang-key=\"quantumminilabs_title\">Das QuantumMiniLabs Projekt</h2>\n <p data-lang-key=\"quantumminilabs_text\">\n Das QuantumMiniLabs-Projekt entwickelt ein Open-Source-Ökosystem, das kostengünstige, skalierbare, modulare und reparable Quantentechnologie-Experimente ermöglicht. Das Ziel ist es, das System an 100 Bildungsstandorten in Deutschland einzusetzen.\n </p>\n <p data-lang-key=\"quantumminilabs_vision\">\n QuantumMiniLabs bietet die erste erschwingliche DIY-Plattform für Experimente mit Quantensystemen der zweiten Generation. NV-Diamanten ermöglichen stabile Experimente bei Raumtemperatur.\n </p>\n </div>\n </div>\n </section>\n </main>\n\n <!-- ░░ Footer ░░----------------------------------------------------------- -->\n <footer class=\"py-3 mt-auto\">\n <div class=\"container text-center small\">\n <div>Uni Münster · openUC2 GmbH – <a class=\"text-white text-decoration-none\" href=\"mailto:hello@openuc2.com\">hello@openuc2.com</a></div>\n <div class=\"mt-1\" id=\"versionInfo\" style=\"opacity: 0.7; font-size: 0.85em;\">Loading version...</div>\n </div>\n </footer>\n\n <!-- Bootstrap JS from CDN -->\n <script src=\"https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js\"></script>\n \n <script>\n // Multi-language support\n const translations = {\n de: {\n nav_start: \"Start\",\n nav_measurement_device: \"Messung (on Device)\",\n nav_measurement_webserial: \"Messung (WebSerial)\",\n nav_alignment: \"Justage\",\n nav_info: \"Weitere Infos\",\n title: \"NV-Experimente / ODMR\",\n welcome_title: \"Low-Cost Experimente mit NV-Zentren\",\n odmr_intro_title: \"Tutorial: Building an ODMR Setup\",\n odmr_intro_text: \"In diesem Workshop konstruieren wir ein ODMR (Optically Detected Magnetic Resonance) System mit dem UC2 modularen Mikroskop-Toolbox und NV (Nitrogen-Vacancy) Diamanten. ODMR ist eine Quantensensing-Technik, die es uns ermöglicht, Magnetfelder durch Beobachtung von Fluoreszenzänderungen in Quantensystemen zu messen.\",\n quick_start: \"Quick Start\",\n start_measurement: \"Messung starten\",\n alignment: \"Aufbau justieren\",\n theory_title: \"Theoretischer Hintergrund\",\n nv_centers_title: \"Was sind NV-Zentren?\",\n nv_centers_text: \"NV-Zentren sind Fehlstellen in Diamanten, bestehend aus einem Stickstoffatom neben einer Vakanz (Leerstelle). Diese Quantensysteme haben einzigartige Eigenschaften, die sie ideal für Sensoranwendungen machen:\",\n nv_property_1: \"Spin-1 Grundzustand mit drei möglichen Projektionen\",\n nv_property_2: \"Optische Anregung bei 532 nm (grün)\",\n nv_property_3: \"Fluoreszenz im roten Spektralbereich\",\n nv_property_4: \"Raumtemperatur-stabile Quantenkohärenz\",\n nv_structure_alt: \"NV-Zentrum Struktur\",\n nv_structure_caption: \"Struktur des NV-Zentrums im Diamantgitter\",\n odmr_principle_title: \"ODMR-Prinzip\",\n odmr_principle_text: \"Der ODMR-Effekt basiert auf spinabhängiger Fluoreszenz. Wenn Mikrowellenstrahlung bei der Resonanzfrequenz (~2,87 GHz) angewendet wird, verursacht sie Übergänge zwischen Quantenspinzuständen, was zu einer messbaren Abnahme der Fluoreszenzintensität führt.\",\n key_concept: \"Schlüsselkonzept:\",\n zeeman_effect_text: \"Externe Magnetfelder verschieben die Resonanzfrequenzen durch den Zeeman-Effekt, wodurch präzise Magnetfeldmessungen ermöglicht werden.\",\n applications_title: \"Moderne Anwendungen\",\n app_biomedical: \"Biomedizinische Bildgebung\",\n app_biomedical_desc: \"Kartierung von Magnetfeldern in lebenden Zellen und Geweben\",\n app_materials: \"Materialwissenschaft\",\n app_materials_desc: \"Untersuchung magnetischer Domänen und Spintransport\",\n app_quantum: \"Quanteninformation\",\n app_quantum_desc: \"Bausteine für Quantencomputer und -netzwerke\",\n app_physics: \"Fundamentale Physik\",\n app_physics_desc: \"Test der Quantenmechanik und Messung von Fundamentalkonstanten\",\n setup_overview_title: \"Versuchsaufbau\",\n setup_overview_text: \"Der ODMR-Aufbau folgt konfokalen Mikroskopieprinzipien und kombiniert optische Anregung, Mikrowellenmanipulation und Fluoreszenzdetektion für hochpräzise Quantensensing.\",\n components_needed: \"Benötigte Komponenten:\",\n component_1: \"• Grundplatte für Montage\",\n component_2: \"• Grüne Laserdiode (532 nm)\",\n component_3: \"• 45° Spiegel für Strahlführung\",\n component_4: \"• Strahlteiler mit Filter\",\n component_5: \"• Konvergente Linse\",\n component_6: \"• Lichtsensor (Photodiode)\",\n component_7: \"• Elektronik-Box mit Mikrowellenerzeugung\",\n component_8: \"• XY-Bühnensystem mit NV-Diamant\",\n component_9: \"• Magnet für externes Magnetfeld\",\n component_10: \"• Mikrowellenantenne\",\n safety_title: \"⚠️ Sicherheitshinweise\",\n safety_laser: \"Niemals direkt in den Laser blicken\",\n safety_magnet: \"Vorsicht bei Implantaten und elektronischen Geräten\",\n safety_power: \"Stromversorgung vor Verkabelungsänderungen trennen\",\n quantumminilabs_title: \"Das QuantumMiniLabs Projekt\",\n quantumminilabs_text: \"Das QuantumMiniLabs-Projekt entwickelt ein Open-Source-Ökosystem, das kostengünstige, skalierbare, modulare und reparable Quantentechnologie-Experimente ermöglicht. Das Ziel ist es, das System an 100 Bildungsstandorten in Deutschland einzusetzen.\",\n quantumminilabs_vision: \"QuantumMiniLabs bietet die erste erschwingliche DIY-Plattform für Experimente mit Quantensystemen der zweiten Generation. NV-Diamanten ermöglichen stabile Experimente bei Raumtemperatur.\"\n },\n en: {\n nav_start: \"Start\",\n nav_measurement_device: \"Measurement (on Device)\",\n nav_measurement_webserial: \"Measurement (WebSerial)\",\n nav_alignment: \"Alignment\",\n nav_info: \"More Info\",\n title: \"NV Experiments / ODMR\",\n welcome_title: \"Low-Cost Experiments with NV Centers\",\n odmr_intro_title: \"Tutorial: Building an ODMR Setup\",\n odmr_intro_text: \"In this workshop, we will construct an ODMR (Optically Detected Magnetic Resonance) system using the UC2 modular microscope toolbox and NV (Nitrogen-Vacancy) diamonds. ODMR is a quantum sensing technique that allows us to measure magnetic fields by observing changes in fluorescence from quantum systems.\",\n quick_start: \"Quick Start\",\n start_measurement: \"Start Measurement\",\n alignment: \"Align Setup\",\n theory_title: \"Theoretical Background\",\n nv_centers_title: \"What are NV Centers?\",\n nv_centers_text: \"NV centers are point defects in diamond consisting of a nitrogen atom adjacent to a vacant lattice site. These quantum systems have unique properties that make them ideal for sensing applications:\",\n nv_property_1: \"Spin-1 ground state with three possible projections\",\n nv_property_2: \"Optical excitation at 532 nm (green)\",\n nv_property_3: \"Fluorescence in red spectral range\",\n nv_property_4: \"Room-temperature stable quantum coherence\",\n nv_structure_alt: \"NV Center Structure\",\n nv_structure_caption: \"Structure of NV center in diamond lattice\",\n odmr_principle_title: \"ODMR Principle\",\n odmr_principle_text: \"The ODMR effect relies on spin-dependent fluorescence. When microwave radiation at the resonant frequency (~2.87 GHz) is applied, it causes transitions between quantum spin states, resulting in a measurable decrease in fluorescence intensity.\",\n key_concept: \"Key Concept:\",\n zeeman_effect_text: \"External magnetic fields shift these resonance frequencies via the Zeeman effect, allowing precise magnetic field measurements.\",\n applications_title: \"Modern Applications\",\n app_biomedical: \"Biomedical Imaging\",\n app_biomedical_desc: \"Mapping magnetic fields in living cells and tissues\",\n app_materials: \"Materials Science\",\n app_materials_desc: \"Studying magnetic domains and spin transport\",\n app_quantum: \"Quantum Information\",\n app_quantum_desc: \"Building blocks for quantum computers and networks\",\n app_physics: \"Fundamental Physics\",\n app_physics_desc: \"Testing quantum mechanics and measuring fundamental constants\",\n setup_overview_title: \"Experimental Setup\",\n setup_overview_text: \"The ODMR setup follows confocal microscopy principles and combines optical excitation, microwave manipulation, and fluorescence detection for high-precision quantum sensing.\",\n components_needed: \"Required Components:\",\n component_1: \"• Base plate for mounting\",\n component_2: \"• Green laser diode (532 nm)\",\n component_3: \"• 45° mirrors for beam steering\",\n component_4: \"• Beam splitter with filter\",\n component_5: \"• Converging lens\",\n component_6: \"• Light sensor (photodiode)\",\n component_7: \"• Electronics box with microwave generation\",\n component_8: \"• XY-stage with NV diamond sample\",\n component_9: \"• Magnet for external magnetic field\",\n component_10: \"• Microwave antenna\",\n safety_title: \"⚠️ Safety Instructions\",\n safety_laser: \"Never look directly into the laser\",\n safety_magnet: \"Caution with implants and electronic devices\",\n safety_power: \"Disconnect power before changing connections\",\n quantumminilabs_title: \"The QuantumMiniLabs Project\",\n quantumminilabs_text: \"The QuantumMiniLabs project is developing an open-source ecosystem that enables low-cost, scalable, modular, and repairable quantum tech experiments. The goal is to deploy the system at 100 educational locations across Germany.\",\n quantumminilabs_vision: \"QuantumMiniLabs offer the first affordable DIY platform for experimenting with second-generation quantum systems. NV diamonds allow for stable experiments at room temperature.\"\n }\n };\n \n function setLanguage(lang) {\n localStorage.setItem('language', lang);\n updateLanguage(lang);\n \n // Update dropdown display\n const langDropdown = document.getElementById('langDropdown');\n langDropdown.innerHTML = `🌐 ${lang.toUpperCase()}`;\n }\n \n function updateLanguage(lang) {\n const elements = document.querySelectorAll('[data-lang-key]');\n elements.forEach(element => {\n const key = element.getAttribute('data-lang-key');\n if (translations[lang] && translations[lang][key]) {\n element.textContent = translations[lang][key];\n }\n });\n \n // Update title\n if (translations[lang] && translations[lang].title) {\n document.title = translations[lang].title;\n }\n }\n \n // Initialize language on page load\n document.addEventListener('DOMContentLoaded', function() {\n const savedLang = localStorage.getItem('language') || 'de';\n setLanguage(savedLang);\n \n // Hide WebSerial navigation item if on ESP32\n const isLocalDevice = window.location.hostname === '192.168.4.1' || \n window.location.hostname.includes('ODMR_') || \n window.location.protocol === 'file:';\n \n if (isLocalDevice) {\n const webSerialNavItem = document.getElementById('webSerialNavItem');\n if (webSerialNavItem) {\n webSerialNavItem.style.display = 'none';\n }\n }\n \n // Fetch and display version information\n fetch('/version')\n .then(response => {\n if (!response.ok) {\n throw new Error(`HTTP error! status: ${response.status}`);\n }\n const contentType = response.headers.get('content-type');\n if (!contentType || !contentType.includes('application/json')) {\n throw new Error('Version endpoint not available');\n }\n return response.json();\n })\n .then(data => {\n const versionInfo = document.getElementById('versionInfo');\n if (versionInfo) {\n versionInfo.textContent = `v${data.version} | Build: ${data.build_date} | ${data.git_hash}`;\n }\n })\n .catch(error => {\n console.error('Error fetching version:', error);\n const versionInfo = document.getElementById('versionInfo');\n if (versionInfo) {\n versionInfo.textContent = 'Version: ESP32 ODMR Server';\n }\n });\n });\n </script>\n</body>\n</html>\n";
const char INDEX_HTML[] PROGMEM = "<!DOCTYPE html>\n<html lang=\"de\">\n<head>\n <meta charset=\"UTF-8\">\n <meta name=\"viewport\" content=\"width=device-width, initial-scale=1\">\n <title>NV-Experimente / ODMR</title>\n\n <!-- Offline-capable CSS framework -->\n <link rel=\"stylesheet\" href=\"style.css\">\n</head>\n\n<body>\n <!-- ░░ Navbar ░░----------------------------------------------------------- -->\n <nav class=\"navbar navbar-expand-lg shadow-sm mb-4\">\n <div class=\"container\">\n <a class=\"navbar-brand fw-bold\" href=\"index.html\">NV-Experimente</a>\n <button class=\"navbar-toggler\" type=\"button\" data-bs-toggle=\"collapse\"\n data-bs-target=\"#nvNav\" aria-controls=\"nvNav\" aria-expanded=\"false\">\n <span class=\"navbar-toggler-icon\"></span>\n </button>\n\n <div class=\"collapse navbar-collapse\" id=\"nvNav\">\n <ul class=\"navbar-nav ms-auto\">\n <li class=\"nav-item\"><a class=\"nav-link active\" href=\"index.html\" data-lang-key=\"nav_start\">Start</a></li>\n <li class=\"nav-item\"><a class=\"nav-link\" href=\"messung.html\" data-lang-key=\"nav_measurement_device\">Messung (on Device)</a></li>\n <li class=\"nav-item\" id=\"webSerialNavItem\"><a class=\"nav-link\" href=\"messung_webserial.html\" data-lang-key=\"nav_measurement_webserial\">Messung (WebSerial)</a></li>\n <li class=\"nav-item\"><a class=\"nav-link\" href=\"ratio.html\" data-lang-key=\"nav_ratio\">B-Field Monitor</a></li>\n <li class=\"nav-item\"><a class=\"nav-link\" href=\"justage.html\" data-lang-key=\"nav_alignment\">Justage</a></li>\n <li class=\"nav-item\"><a class=\"nav-link\" href=\"infos.html\" data-lang-key=\"nav_info\">Weitere Infos</a></li>\n <li class=\"nav-item dropdown\">\n <a class=\"nav-link dropdown-toggle\" href=\"#\" id=\"langDropdown\" role=\"button\" data-bs-toggle=\"dropdown\" aria-expanded=\"false\">\n 🌐 DE\n </a>\n <ul class=\"dropdown-menu\" aria-labelledby=\"langDropdown\">\n <li><a class=\"dropdown-item\" href=\"#\" onclick=\"setLanguage('de')\">🇩🇪 Deutsch</a></li>\n <li><a class=\"dropdown-item\" href=\"#\" onclick=\"setLanguage('en')\">🇺🇸 English</a></li>\n </ul>\n </li>\n </ul>\n </div>\n </div>\n </nav>\n\n <!-- ░░ Main content ░░------------------------------------------------------ -->\n <main class=\"container\">\n <section class=\"mb-5\">\n <h1 class=\"display-5 mb-3\" data-lang-key=\"welcome_title\">Low-Cost Experimente mit NV-Zentren</h1>\n \n <!-- Introduction Section -->\n <div class=\"row\">\n <div class=\"col-lg-8\">\n <h2 class=\"h4 mb-3\" data-lang-key=\"odmr_intro_title\">Tutorial: Building an ODMR Setup</h2>\n <p class=\"lead\" data-lang-key=\"odmr_intro_text\">\n In diesem Workshop konstruieren wir ein ODMR (Optically Detected Magnetic Resonance) System mit dem UC2 modularen Mikroskop-Toolbox und NV (Nitrogen-Vacancy) Diamanten. ODMR ist eine Quantensensing-Technik, die es uns ermöglicht, Magnetfelder durch Beobachtung von Fluoreszenzänderungen in Quantensystemen zu messen.\n </p>\n </div>\n <div class=\"col-lg-4\">\n <div class=\"card\">\n <div class=\"card-body text-center\">\n <h6 class=\"card-title\" data-lang-key=\"quick_start\">Quick Start</h6>\n <div class=\"d-grid gap-2\">\n <a href=\"messung.html\" class=\"btn btn-primary\" data-lang-key=\"start_measurement\">Messung starten</a>\n <a href=\"justage.html\" class=\"btn btn-secondary\" data-lang-key=\"alignment\">Aufbau justieren</a>\n </div>\n </div>\n </div>\n </div>\n </div>\n </section>\n\n <!-- Theory Section -->\n <section class=\"mb-5\">\n <h2 class=\"h4 mb-3\" data-lang-key=\"theory_title\">Theoretischer Hintergrund</h2>\n \n <div class=\"row\">\n <div class=\"col-lg-6\">\n <h3 class=\"h5 mb-3\" data-lang-key=\"nv_centers_title\">Was sind NV-Zentren?</h3>\n <p data-lang-key=\"nv_centers_text\">\n NV-Zentren sind Fehlstellen in Diamanten, bestehend aus einem Stickstoffatom neben einer Vakanz (Leerstelle). Diese Quantensysteme haben einzigartige Eigenschaften, die sie ideal für Sensoranwendungen machen:\n </p>\n <ul>\n <li data-lang-key=\"nv_property_1\">Spin-1 Grundzustand mit drei möglichen Projektionen</li>\n <li data-lang-key=\"nv_property_2\">Optische Anregung bei 532 nm (grün)</li>\n <li data-lang-key=\"nv_property_3\">Fluoreszenz im roten Spektralbereich</li>\n <li data-lang-key=\"nv_property_4\">Raumtemperatur-stabile Quantenkohärenz</li>\n </ul>\n </div>\n <div class=\"col-lg-6\">\n <figure class=\"text-center\">\n <img src=\"NVGitter.png\" class=\"img-fluid rounded shadow\" alt=\"NV Center Structure\" data-lang-key=\"nv_structure_alt\">\n <figcaption class=\"mt-2 small text-muted\" data-lang-key=\"nv_structure_caption\">\n Struktur des NV-Zentrums im Diamantgitter\n </figcaption>\n </figure>\n </div>\n </div>\n \n <div class=\"row mt-4\">\n <div class=\"col-12\">\n <h3 class=\"h5 mb-3\" data-lang-key=\"odmr_principle_title\">ODMR-Prinzip</h3>\n <p data-lang-key=\"odmr_principle_text\">\n Der ODMR-Effekt basiert auf spinabhängiger Fluoreszenz. Wenn Mikrowellenstrahlung bei der Resonanzfrequenz (~2,87 GHz) angewendet wird, verursacht sie Übergänge zwischen Quantenspinzuständen, was zu einer messbaren Abnahme der Fluoreszenzintensität führt.\n </p>\n \n <div class=\"alert alert-info\">\n <h6 data-lang-key=\"key_concept\">Schlüsselkonzept:</h6>\n <p class=\"mb-0\" data-lang-key=\"zeeman_effect_text\">\n Externe Magnetfelder verschieben die Resonanzfrequenzen durch den Zeeman-Effekt, wodurch präzise Magnetfeldmessungen ermöglicht werden.\n </p>\n </div>\n </div>\n </div>\n </section>\n\n <!-- Applications Section -->\n <section class=\"mb-5\">\n <h2 class=\"h4 mb-3\" data-lang-key=\"applications_title\">Moderne Anwendungen</h2>\n \n <div class=\"row\">\n <div class=\"col-md-6 col-lg-3 mb-3\">\n <div class=\"card h-100\">\n <div class=\"card-body text-center\">\n <h6 class=\"card-title\" data-lang-key=\"app_biomedical\">Biomedizinische Bildgebung</h6>\n <p class=\"card-text small\" data-lang-key=\"app_biomedical_desc\">\n Kartierung von Magnetfeldern in lebenden Zellen und Geweben\n </p>\n </div>\n </div>\n </div>\n <div class=\"col-md-6 col-lg-3 mb-3\">\n <div class=\"card h-100\">\n <div class=\"card-body text-center\">\n <h6 class=\"card-title\" data-lang-key=\"app_materials\">Materialwissenschaft</h6>\n <p class=\"card-text small\" data-lang-key=\"app_materials_desc\">\n Untersuchung magnetischer Domänen und Spintransport\n </p>\n </div>\n </div>\n </div>\n <div class=\"col-md-6 col-lg-3 mb-3\">\n <div class=\"card h-100\">\n <div class=\"card-body text-center\">\n <h6 class=\"card-title\" data-lang-key=\"app_quantum\">Quanteninformation</h6>\n <p class=\"card-text small\" data-lang-key=\"app_quantum_desc\">\n Bausteine für Quantencomputer und -netzwerke\n </p>\n </div>\n </div>\n </div>\n <div class=\"col-md-6 col-lg-3 mb-3\">\n <div class=\"card h-100\">\n <div class=\"card-body text-center\">\n <h6 class=\"card-title\" data-lang-key=\"app_physics\">Fundamentale Physik</h6>\n <p class=\"card-text small\" data-lang-key=\"app_physics_desc\">\n Test der Quantenmechanik und Messung von Fundamentalkonstanten\n </p>\n </div>\n </div>\n </div>\n </div>\n </section>\n\n <!-- Setup Overview Section -->\n <section class=\"mb-5\">\n <h2 class=\"h4 mb-3\" data-lang-key=\"setup_overview_title\">Versuchsaufbau</h2>\n \n <div class=\"row\">\n <div class=\"col-lg-8\">\n <p data-lang-key=\"setup_overview_text\">\n Der ODMR-Aufbau folgt konfokalen Mikroskopieprinzipien und kombiniert optische Anregung, Mikrowellenmanipulation und Fluoreszenzdetektion für hochpräzise Quantensensing.\n </p>\n \n <h3 class=\"h6 mb-2\" data-lang-key=\"components_needed\">Benötigte Komponenten:</h3>\n <div class=\"row\">\n <div class=\"col-md-6\">\n <ul class=\"list-unstyled\">\n <li data-lang-key=\"component_1\">• Grundplatte für Montage</li>\n <li data-lang-key=\"component_2\">• Grüne Laserdiode (532 nm)</li>\n <li data-lang-key=\"component_3\">• 45° Spiegel für Strahlführung</li>\n <li data-lang-key=\"component_4\">• Strahlteiler mit Filter</li>\n <li data-lang-key=\"component_5\">• Konvergente Linse</li>\n </ul>\n </div>\n <div class=\"col-md-6\">\n <ul class=\"list-unstyled\">\n <li data-lang-key=\"component_6\">• Lichtsensor (Photodiode)</li>\n <li data-lang-key=\"component_7\">• Elektronik-Box mit Mikrowellenerzeugung</li>\n <li data-lang-key=\"component_8\">• XY-Bühnensystem mit NV-Diamant</li>\n <li data-lang-key=\"component_9\">• Magnet für externes Magnetfeld</li>\n <li data-lang-key=\"component_10\">• Mikrowellenantenne</li>\n </ul>\n </div>\n </div>\n </div>\n <div class=\"col-lg-4\">\n <div class=\"alert alert-warning\">\n <h6 class=\"alert-heading\" data-lang-key=\"safety_title\">⚠️ Sicherheitshinweise</h6>\n <ul class=\"mb-0 small\">\n <li data-lang-key=\"safety_laser\">Niemals direkt in den Laser blicken</li>\n <li data-lang-key=\"safety_magnet\">Vorsicht bei Implantaten und elektronischen Geräten</li>\n <li data-lang-key=\"safety_power\">Stromversorgung vor Verkabelungsänderungen trennen</li>\n </ul>\n </div>\n </div>\n </div>\n </section>\n\n <!-- QuantumMiniLabs Section -->\n <section class=\"mb-5\">\n <div class=\"card\">\n <div class=\"card-body\">\n <h2 class=\"h4 mb-3\" data-lang-key=\"quantumminilabs_title\">Das QuantumMiniLabs Projekt</h2>\n <p data-lang-key=\"quantumminilabs_text\">\n Das QuantumMiniLabs-Projekt entwickelt ein Open-Source-Ökosystem, das kostengünstige, skalierbare, modulare und reparable Quantentechnologie-Experimente ermöglicht. Das Ziel ist es, das System an 100 Bildungsstandorten in Deutschland einzusetzen.\n </p>\n <p data-lang-key=\"quantumminilabs_vision\">\n QuantumMiniLabs bietet die erste erschwingliche DIY-Plattform für Experimente mit Quantensystemen der zweiten Generation. NV-Diamanten ermöglichen stabile Experimente bei Raumtemperatur.\n </p>\n </div>\n </div>\n </section>\n </main>\n\n <!-- ░░ Footer ░░----------------------------------------------------------- -->\n <footer class=\"py-3 mt-auto\">\n <div class=\"container text-center small\">\n <div>Uni Münster · openUC2 GmbH – <a class=\"text-white text-decoration-none\" href=\"mailto:hello@openuc2.com\">hello@openuc2.com</a></div>\n <div class=\"mt-1\" id=\"versionInfo\" style=\"opacity: 0.7; font-size: 0.85em;\">Loading version...</div>\n </div>\n </footer>\n\n <!-- Bootstrap JS from CDN -->\n <script src=\"https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js\"></script>\n \n <script>\n // Multi-language support\n const translations = {\n de: {\n nav_start: \"Start\",\n nav_measurement_device: \"Messung (on Device)\",\n nav_measurement_webserial: \"Messung (WebSerial)\",\n nav_alignment: \"Justage\",\n nav_info: \"Weitere Infos\",\n title: \"NV-Experimente / ODMR\",\n welcome_title: \"Low-Cost Experimente mit NV-Zentren\",\n odmr_intro_title: \"Tutorial: Building an ODMR Setup\",\n odmr_intro_text: \"In diesem Workshop konstruieren wir ein ODMR (Optically Detected Magnetic Resonance) System mit dem UC2 modularen Mikroskop-Toolbox und NV (Nitrogen-Vacancy) Diamanten. ODMR ist eine Quantensensing-Technik, die es uns ermöglicht, Magnetfelder durch Beobachtung von Fluoreszenzänderungen in Quantensystemen zu messen.\",\n quick_start: \"Quick Start\",\n start_measurement: \"Messung starten\",\n alignment: \"Aufbau justieren\",\n theory_title: \"Theoretischer Hintergrund\",\n nv_centers_title: \"Was sind NV-Zentren?\",\n nv_centers_text: \"NV-Zentren sind Fehlstellen in Diamanten, bestehend aus einem Stickstoffatom neben einer Vakanz (Leerstelle). Diese Quantensysteme haben einzigartige Eigenschaften, die sie ideal für Sensoranwendungen machen:\",\n nv_property_1: \"Spin-1 Grundzustand mit drei möglichen Projektionen\",\n nv_property_2: \"Optische Anregung bei 532 nm (grün)\",\n nv_property_3: \"Fluoreszenz im roten Spektralbereich\",\n nv_property_4: \"Raumtemperatur-stabile Quantenkohärenz\",\n nv_structure_alt: \"NV-Zentrum Struktur\",\n nv_structure_caption: \"Struktur des NV-Zentrums im Diamantgitter\",\n odmr_principle_title: \"ODMR-Prinzip\",\n odmr_principle_text: \"Der ODMR-Effekt basiert auf spinabhängiger Fluoreszenz. Wenn Mikrowellenstrahlung bei der Resonanzfrequenz (~2,87 GHz) angewendet wird, verursacht sie Übergänge zwischen Quantenspinzuständen, was zu einer messbaren Abnahme der Fluoreszenzintensität führt.\",\n key_concept: \"Schlüsselkonzept:\",\n zeeman_effect_text: \"Externe Magnetfelder verschieben die Resonanzfrequenzen durch den Zeeman-Effekt, wodurch präzise Magnetfeldmessungen ermöglicht werden.\",\n applications_title: \"Moderne Anwendungen\",\n app_biomedical: \"Biomedizinische Bildgebung\",\n app_biomedical_desc: \"Kartierung von Magnetfeldern in lebenden Zellen und Geweben\",\n app_materials: \"Materialwissenschaft\",\n app_materials_desc: \"Untersuchung magnetischer Domänen und Spintransport\",\n app_quantum: \"Quanteninformation\",\n app_quantum_desc: \"Bausteine für Quantencomputer und -netzwerke\",\n app_physics: \"Fundamentale Physik\",\n app_physics_desc: \"Test der Quantenmechanik und Messung von Fundamentalkonstanten\",\n setup_overview_title: \"Versuchsaufbau\",\n setup_overview_text: \"Der ODMR-Aufbau folgt konfokalen Mikroskopieprinzipien und kombiniert optische Anregung, Mikrowellenmanipulation und Fluoreszenzdetektion für hochpräzise Quantensensing.\",\n components_needed: \"Benötigte Komponenten:\",\n component_1: \"• Grundplatte für Montage\",\n component_2: \"• Grüne Laserdiode (532 nm)\",\n component_3: \"• 45° Spiegel für Strahlführung\",\n component_4: \"• Strahlteiler mit Filter\",\n component_5: \"• Konvergente Linse\",\n component_6: \"• Lichtsensor (Photodiode)\",\n component_7: \"• Elektronik-Box mit Mikrowellenerzeugung\",\n component_8: \"• XY-Bühnensystem mit NV-Diamant\",\n component_9: \"• Magnet für externes Magnetfeld\",\n component_10: \"• Mikrowellenantenne\",\n safety_title: \"⚠️ Sicherheitshinweise\",\n safety_laser: \"Niemals direkt in den Laser blicken\",\n safety_magnet: \"Vorsicht bei Implantaten und elektronischen Geräten\",\n safety_power: \"Stromversorgung vor Verkabelungsänderungen trennen\",\n quantumminilabs_title: \"Das QuantumMiniLabs Projekt\",\n quantumminilabs_text: \"Das QuantumMiniLabs-Projekt entwickelt ein Open-Source-Ökosystem, das kostengünstige, skalierbare, modulare und reparable Quantentechnologie-Experimente ermöglicht. Das Ziel ist es, das System an 100 Bildungsstandorten in Deutschland einzusetzen.\",\n quantumminilabs_vision: \"QuantumMiniLabs bietet die erste erschwingliche DIY-Plattform für Experimente mit Quantensystemen der zweiten Generation. NV-Diamanten ermöglichen stabile Experimente bei Raumtemperatur.\"\n },\n en: {\n nav_start: \"Start\",\n nav_measurement_device: \"Measurement (on Device)\",\n nav_measurement_webserial: \"Measurement (WebSerial)\",\n nav_alignment: \"Alignment\",\n nav_info: \"More Info\",\n title: \"NV Experiments / ODMR\",\n welcome_title: \"Low-Cost Experiments with NV Centers\",\n odmr_intro_title: \"Tutorial: Building an ODMR Setup\",\n odmr_intro_text: \"In this workshop, we will construct an ODMR (Optically Detected Magnetic Resonance) system using the UC2 modular microscope toolbox and NV (Nitrogen-Vacancy) diamonds. ODMR is a quantum sensing technique that allows us to measure magnetic fields by observing changes in fluorescence from quantum systems.\",\n quick_start: \"Quick Start\",\n start_measurement: \"Start Measurement\",\n alignment: \"Align Setup\",\n theory_title: \"Theoretical Background\",\n nv_centers_title: \"What are NV Centers?\",\n nv_centers_text: \"NV centers are point defects in diamond consisting of a nitrogen atom adjacent to a vacant lattice site. These quantum systems have unique properties that make them ideal for sensing applications:\",\n nv_property_1: \"Spin-1 ground state with three possible projections\",\n nv_property_2: \"Optical excitation at 532 nm (green)\",\n nv_property_3: \"Fluorescence in red spectral range\",\n nv_property_4: \"Room-temperature stable quantum coherence\",\n nv_structure_alt: \"NV Center Structure\",\n nv_structure_caption: \"Structure of NV center in diamond lattice\",\n odmr_principle_title: \"ODMR Principle\",\n odmr_principle_text: \"The ODMR effect relies on spin-dependent fluorescence. When microwave radiation at the resonant frequency (~2.87 GHz) is applied, it causes transitions between quantum spin states, resulting in a measurable decrease in fluorescence intensity.\",\n key_concept: \"Key Concept:\",\n zeeman_effect_text: \"External magnetic fields shift these resonance frequencies via the Zeeman effect, allowing precise magnetic field measurements.\",\n applications_title: \"Modern Applications\",\n app_biomedical: \"Biomedical Imaging\",\n app_biomedical_desc: \"Mapping magnetic fields in living cells and tissues\",\n app_materials: \"Materials Science\",\n app_materials_desc: \"Studying magnetic domains and spin transport\",\n app_quantum: \"Quantum Information\",\n app_quantum_desc: \"Building blocks for quantum computers and networks\",\n app_physics: \"Fundamental Physics\",\n app_physics_desc: \"Testing quantum mechanics and measuring fundamental constants\",\n setup_overview_title: \"Experimental Setup\",\n setup_overview_text: \"The ODMR setup follows confocal microscopy principles and combines optical excitation, microwave manipulation, and fluorescence detection for high-precision quantum sensing.\",\n components_needed: \"Required Components:\",\n component_1: \"• Base plate for mounting\",\n component_2: \"• Green laser diode (532 nm)\",\n component_3: \"• 45° mirrors for beam steering\",\n component_4: \"• Beam splitter with filter\",\n component_5: \"• Converging lens\",\n component_6: \"• Light sensor (photodiode)\",\n component_7: \"• Electronics box with microwave generation\",\n component_8: \"• XY-stage with NV diamond sample\",\n component_9: \"• Magnet for external magnetic field\",\n component_10: \"• Microwave antenna\",\n safety_title: \"⚠️ Safety Instructions\",\n safety_laser: \"Never look directly into the laser\",\n safety_magnet: \"Caution with implants and electronic devices\",\n safety_power: \"Disconnect power before changing connections\",\n quantumminilabs_title: \"The QuantumMiniLabs Project\",\n quantumminilabs_text: \"The QuantumMiniLabs project is developing an open-source ecosystem that enables low-cost, scalable, modular, and repairable quantum tech experiments. The goal is to deploy the system at 100 educational locations across Germany.\",\n quantumminilabs_vision: \"QuantumMiniLabs offer the first affordable DIY platform for experimenting with second-generation quantum systems. NV diamonds allow for stable experiments at room temperature.\"\n }\n };\n \n function setLanguage(lang) {\n localStorage.setItem('language', lang);\n updateLanguage(lang);\n \n // Update dropdown display\n const langDropdown = document.getElementById('langDropdown');\n langDropdown.innerHTML = `🌐 ${lang.toUpperCase()}`;\n }\n \n function updateLanguage(lang) {\n const elements = document.querySelectorAll('[data-lang-key]');\n elements.forEach(element => {\n const key = element.getAttribute('data-lang-key');\n if (translations[lang] && translations[lang][key]) {\n element.textContent = translations[lang][key];\n }\n });\n \n // Update title\n if (translations[lang] && translations[lang].title) {\n document.title = translations[lang].title;\n }\n }\n \n // Initialize language on page load\n document.addEventListener('DOMContentLoaded', function() {\n const savedLang = localStorage.getItem('language') || 'de';\n setLanguage(savedLang);\n \n // WebSerial nav visibility: use /webserial_check + browser capability (P2 #12)\n (async function(){\n const wsNav = document.getElementById('webSerialNavItem');\n if (!wsNav) return;\n if (!('serial' in navigator)) { wsNav.style.display = 'none'; return; }\n try {\n const r = await fetch('/webserial_check');\n const d = await r.json();\n if (!d.webserial_enabled) wsNav.style.display = 'none';\n } catch(e) { wsNav.style.display = 'none'; }\n })();\n \n // Fetch and display version information\n fetch('/version')\n .then(response => {\n if (!response.ok) {\n throw new Error(`HTTP error! status: ${response.status}`);\n }\n const contentType = response.headers.get('content-type');\n if (!contentType || !contentType.includes('application/json')) {\n throw new Error('Version endpoint not available');\n }\n return response.json();\n })\n .then(data => {\n const versionInfo = document.getElementById('versionInfo');\n if (versionInfo) {\n versionInfo.textContent = `v${data.version} | Build: ${data.build_date} | ${data.git_hash}`;\n }\n })\n .catch(error => {\n console.error('Error fetching version:', error);\n const versionInfo = document.getElementById('versionInfo');\n if (versionInfo) {\n versionInfo.textContent = 'Version: ESP32 ODMR Server';\n }\n });\n });\n </script>\n <script>\n /* Bootstrap-JS replacement: navbar collapse + dropdown */\n (function(){\n document.querySelectorAll('.navbar-toggler').forEach(function(b){\n b.addEventListener('click',function(){var t=document.querySelector(b.getAttribute('data-bs-target'));if(t)t.classList.toggle('show');});\n });\n document.querySelectorAll('.dropdown-toggle').forEach(function(b){\n b.addEventListener('click',function(e){e.preventDefault();var m=b.nextElementSibling;if(m&&m.classList.contains('dropdown-menu'))m.classList.toggle('show');});\n });\n document.addEventListener('click',function(e){if(!e.target.closest('.dropdown'))document.querySelectorAll('.dropdown-menu.show').forEach(function(m){m.classList.remove('show');});});\n })();\n </script>\n</body>\n</html>\n";
Copy link

Copilot AI Mar 4, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

INDEX_HTML still includes a Bootstrap JS CDN <script src="https://cdn.jsdelivr.net/...bootstrap.bundle.min.js"> even though a custom “Bootstrap-JS replacement” was added. In offline AP mode this external request can stall page load, and it also risks duplicate/conflicting navbar/dropdown handling.

If the goal is fully offline-friendly pages, remove the CDN Bootstrap JS reference from the embedded HTML.

Copilot uses AI. Check for mistakes.
#define __RATIO_HTML_H__

const char RATIO_HTML[] PROGMEM = "<!DOCTYPE html>\n<html lang=\"de\">\n<head>\n <meta charset=\"UTF-8\">\n <meta name=\"viewport\" content=\"width=device-width, initial-scale=1\">\n <title>NV Experimente - B-Field Monitor</title>\n \n <!-- Offline-capable CSS framework -->\n <link rel=\"stylesheet\" href=\"style.css\">\n \n <!-- Page-specific styles -->\n <style>\n #ratioPlot {\n width: 100%;\n height: 280px;\n border: 1px solid var(--uc2-border);\n border-radius: 8px;\n background: #fff;\n }\n .plot-controls { display: flex; gap: 1rem; margin-top: 0.5rem; flex-wrap: wrap; }\n .plot-controls > div { flex: 1; min-width: 120px; }\n .plot-controls select { padding: 0.4rem; }\n .data-table { font-family: monospace; }\n .data-table td:last-child { text-align: right; font-weight: 600; font-size: 1.2rem; }\n h1 { text-align: center; }\n </style>\n</head>\n\n<body>\n <!-- ░░ Navbar ░░----------------------------------------------------------- -->\n <nav class=\"navbar navbar-expand-lg shadow-sm mb-4\">\n <div class=\"container\">\n <a class=\"navbar-brand fw-bold\" href=\"index.html\">NV-Experimente</a>\n <button class=\"navbar-toggler\" type=\"button\" data-bs-toggle=\"collapse\"\n data-bs-target=\"#nvNav\" aria-controls=\"nvNav\" aria-expanded=\"false\">\n <span class=\"navbar-toggler-icon\"></span>\n </button>\n\n <div class=\"collapse navbar-collapse\" id=\"nvNav\">\n <ul class=\"navbar-nav ms-auto\">\n <li class=\"nav-item\"><a class=\"nav-link\" href=\"index.html\" data-lang-key=\"nav_start\">Start</a></li>\n <li class=\"nav-item\"><a class=\"nav-link active\" href=\"messung.html\" data-lang-key=\"nav_measurement_device\">Messung (on Device)</a></li>\n <li class=\"nav-item\" id=\"webSerialNavItem\"><a class=\"nav-link\" href=\"messung_webserial.html\" data-lang-key=\"nav_measurement_webserial\">Messung (WebSerial)</a></li>\n <li class=\"nav-item\"><a class=\"nav-link\" href=\"ratio.html\" data-lang-key=\"nav_ratio\">B-Field Monitor</a></li>\n <li class=\"nav-item\"><a class=\"nav-link\" href=\"justage.html\" data-lang-key=\"nav_alignment\">Justage</a></li>\n <li class=\"nav-item\"><a class=\"nav-link\" href=\"infos.html\" data-lang-key=\"nav_info\">Weitere Infos</a></li>\n <li class=\"nav-item dropdown\">\n <a class=\"nav-link dropdown-toggle\" href=\"#\" id=\"langDropdown\" role=\"button\" data-bs-toggle=\"dropdown\" aria-expanded=\"false\">\n 🌐 DE\n </a>\n <ul class=\"dropdown-menu\" aria-labelledby=\"langDropdown\">\n <li><a class=\"dropdown-item\" href=\"#\" onclick=\"setLanguage('de'); return false;\">🇩🇪 Deutsch</a></li>\n <li><a class=\"dropdown-item\" href=\"#\" onclick=\"setLanguage('en'); return false;\">🇺🇸 English</a></li>\n </ul>\n </li>\n </ul>\n </div>\n </div>\n </nav>\n \n <div class=\"container\">\n <h1>Fast B-Field Monitoring (Ratio Mode)</h1>\n \n <div class=\"row\">\n <!-- Left column: Controls -->\n <div class=\"col-left\">\n <!-- Frequency Settings -->\n <div class=\"card\">\n <div class=\"card-header\">📡 Frequenz-Einstellungen</div>\n <div class=\"card-body\">\n <!-- Mode Toggle -->\n <div class=\"form-group\">\n <label>Modus:</label>\n <div class=\"btn-group\">\n <input type=\"radio\" name=\"modeSelect\" id=\"mode2pt\" value=\"2\" checked>\n <label for=\"mode2pt\">2-Punkt</label>\n <input type=\"radio\" name=\"modeSelect\" id=\"mode3pt\" value=\"3\">\n <label for=\"mode3pt\">3-Punkt</label>\n </div>\n </div>\n \n <!-- Frequency 1 -->\n <div class=\"form-group\">\n <label for=\"freq1\">f₁ (MHz)</label>\n <div class=\"input-group\">\n <input type=\"number\" id=\"freq1\" value=\"2865\" min=\"2200\" max=\"4400\" step=\"0.1\">\n <span class=\"suffix\">MHz</span>\n </div>\n <small>Linke Flanke des Dips</small>\n </div>\n \n <!-- Frequency 2 -->\n <div class=\"form-group\">\n <label for=\"freq2\">f₂ (MHz)</label>\n <div class=\"input-group\">\n <input type=\"number\" id=\"freq2\" value=\"2875\" min=\"2200\" max=\"4400\" step=\"0.1\">\n <span class=\"suffix\">MHz</span>\n </div>\n <small>Rechte Flanke des Dips</small>\n </div>\n \n <!-- Frequency 3 (optional) -->\n <div class=\"form-group\" id=\"freq3Container\" style=\"display: none;\">\n <label for=\"freq3\">f₃ (MHz)</label>\n <div class=\"input-group\">\n <input type=\"number\" id=\"freq3\" value=\"2870\" min=\"2200\" max=\"4400\" step=\"0.1\">\n <span class=\"suffix\">MHz</span>\n </div>\n <small>Zentrum oder Referenz</small>\n </div>\n \n <!-- Averaging -->\n <div class=\"form-group\">\n <label>Mittelungen: <span id=\"avgValue\">3</span></label>\n <input type=\"range\" id=\"avgCount\" min=\"1\" max=\"10\" value=\"3\">\n </div>\n \n <!-- Interval -->\n <div class=\"form-group\">\n <label for=\"intervalMs\">Messintervall (ms)</label>\n <input type=\"number\" id=\"intervalMs\" value=\"500\" min=\"200\" max=\"5000\" step=\"100\">\n <small>Min. 200ms empfohlen</small>\n </div>\n </div>\n </div>\n \n <!-- Control Buttons -->\n <div class=\"card\">\n <div class=\"card-header\">🎮 Steuerung</div>\n <div class=\"card-body\">\n <button class=\"btn btn-success\" id=\"startMonitoring\">▶ Monitoring starten</button>\n <button class=\"btn btn-danger\" id=\"stopMonitoring\" disabled>⏹ Monitoring stoppen</button>\n \n <div class=\"status-row\">\n <div class=\"status-dot\" id=\"statusIndicator\"></div>\n <span id=\"statusText\">Bereit</span>\n </div>\n <div class=\"status-row\">\n <small>Messungen: <span id=\"measurementCount\">0</span> | Fehler: <span id=\"errorCount\">0</span></small>\n </div>\n </div>\n </div>\n </div>\n \n <!-- Right column: Display -->\n <div class=\"col-right\">\n <!-- Current Values -->\n <div class=\"card\">\n <div class=\"card-header\">📊 Aktuelle Werte</div>\n <div class=\"card-body\">\n <div style=\"display: flex; flex-wrap: wrap; gap: 1rem;\">\n <!-- Main Ratio Display -->\n <div style=\"flex: 1; min-width: 200px;\">\n <small>Ratio r₁₂ = (I₁-I₂)/(I₁+I₂)</small>\n <div class=\"ratio-display\" id=\"ratioDisplay\">---</div>\n </div>\n \n <!-- Intensity Values -->\n <div style=\"flex: 1; min-width: 200px;\">\n <table class=\"data-table\">\n <tr>\n <td><strong>I₁</strong> @ <span id=\"f1Display\">---</span> MHz</td>\n <td id=\"i1Display\">---</td>\n </tr>\n <tr>\n <td><strong>I₂</strong> @ <span id=\"f2Display\">---</span> MHz</td>\n <td id=\"i2Display\">---</td>\n </tr>\n <tr id=\"i3Row\" style=\"display: none;\">\n <td><strong>I₃</strong> @ <span id=\"f3Display\">---</span> MHz</td>\n <td id=\"i3Display\">---</td>\n </tr>\n </table>\n \n <!-- Additional ratios for 3-point mode -->\n <div id=\"additionalRatios\" style=\"display: none; margin-top: 0.5rem; text-align: center;\">\n <small>r₁₃: <strong id=\"r13Display\">---</strong> | r₂₃: <strong id=\"r23Display\">---</strong></small>\n </div>\n </div>\n </div>\n </div>\n </div>\n \n <!-- Live Plot -->\n <div class=\"card\">\n <div class=\"card-header\" style=\"display: flex; justify-content: space-between; align-items: center;\">\n <span>📈 Live-Plot</span>\n <div>\n <button class=\"btn btn-outline\" style=\"width: auto; padding: 0.25rem 0.75rem; margin: 0;\" id=\"clearPlot\">Leeren</button>\n <button class=\"btn btn-outline\" style=\"width: auto; padding: 0.25rem 0.75rem; margin: 0;\" id=\"downloadData\">CSV ↓</button>\n </div>\n </div>\n <div class=\"card-body\">\n <canvas id=\"ratioPlot\"></canvas>\n <div class=\"plot-controls\">\n <div>\n <small>Zeitfenster:</small>\n <select id=\"timeWindow\">\n <option value=\"10\">10 s</option>\n <option value=\"30\" selected>30 s</option>\n <option value=\"60\">60 s</option>\n <option value=\"120\">120 s</option>\n </select>\n </div>\n <div>\n <small>Y-Bereich:</small>\n <select id=\"yRange\">\n <option value=\"auto\" selected>Auto</option>\n <option value=\"0.1\">±0.1</option>\n <option value=\"0.5\">±0.5</option>\n <option value=\"1\">±1.0</option>\n </select>\n </div>\n </div>\n </div>\n </div>\n \n <!-- Theory -->\n <div class=\"info-box\">\n <strong>ℹ️ So funktioniert es:</strong><br>\n Das normalisierte Verhältnis <strong>r = (I₁-I₂)/(I₁+I₂)</strong> ist proportional zur Magnetfeldverschiebung.\n Wählen Sie f₁ und f₂ symmetrisch um das ODMR-Dip-Minimum.\n </div>\n </div>\n </div>\n </div>\n\n <!-- Footer -->\n <footer>\n Uni Münster · openUC2 GmbH – <a href=\"mailto:hello@openuc2.com\">hello@openuc2.com</a>\n <div id=\"versionInfo\" style=\"opacity: 0.7; font-size: 0.8em; margin-top: 0.25rem;\">ESP32 ODMR Server</div>\n </footer>\n\n <!-- JavaScript -->\n <script>\n // ========================================================================\n // Global State\n // ========================================================================\n let monitoringInterval = null;\n let measurementCount = 0;\n let errorCount = 0;\n let ratioData = [];\n let startTime = null;\n let canvas, ctx;\n let isFetching = false; // Prevent overlapping requests\n \n const MAX_DATA_POINTS = 500;\n const FETCH_TIMEOUT = 3000; // 3 second timeout\n \n // DOM helper\n const ById = (id) => document.getElementById(id);\n \n // ========================================================================\n // Initialization\n // ========================================================================\n document.addEventListener('DOMContentLoaded', function() {\n canvas = ById('ratioPlot');\n ctx = canvas.getContext('2d');\n resizeCanvas();\n window.addEventListener('resize', resizeCanvas);\n drawPlot();\n \n // Event Listeners\n ById('startMonitoring').addEventListener('click', startMonitoring);\n ById('stopMonitoring').addEventListener('click', stopMonitoring);\n ById('clearPlot').addEventListener('click', clearPlot);\n ById('downloadData').addEventListener('click', downloadCSV);\n \n // Mode toggle\n document.querySelectorAll('input[name=\"modeSelect\"]').forEach(radio => {\n radio.addEventListener('change', handleModeChange);\n });\n \n // Averaging slider\n ById('avgCount').addEventListener('input', function() {\n ById('avgValue').textContent = this.value;\n });\n \n // Plot controls\n ById('timeWindow').addEventListener('change', drawPlot);\n ById('yRange').addEventListener('change', drawPlot);\n \n // Load version\n loadVersionInfo();\n });\n \n // ========================================================================\n // Mode Handling\n // ========================================================================\n function handleModeChange() {\n const is3pt = ById('mode3pt').checked;\n ById('freq3Container').style.display = is3pt ? 'block' : 'none';\n ById('i3Row').style.display = is3pt ? 'table-row' : 'none';\n ById('additionalRatios').style.display = is3pt ? 'block' : 'none';\n }\n \n // ========================================================================\n // Monitoring Control\n // ========================================================================\n function startMonitoring() {\n const f1 = parseFloat(ById('freq1').value);\n const f2 = parseFloat(ById('freq2').value);\n const interval = Math.max(200, parseInt(ById('intervalMs').value)); // Min 200ms\n \n if (isNaN(f1) || isNaN(f2) || f1 < 2200 || f1 > 4400 || f2 < 2200 || f2 > 4400) {\n alert('Ungültige Frequenzwerte! Bereich: 2200-4400 MHz');\n return;\n }\n \n // Update UI\n ById('startMonitoring').disabled = true;\n ById('stopMonitoring').disabled = false;\n ById('statusIndicator').classList.add('running');\n ById('statusIndicator').classList.remove('error');\n ById('statusText').textContent = 'Läuft...';\n \n // Reset state\n startTime = Date.now();\n measurementCount = 0;\n errorCount = 0;\n ById('measurementCount').textContent = '0';\n ById('errorCount').textContent = '0';\n \n // Start periodic measurements with proper interval\n monitoringInterval = setInterval(fetchRatioDataSafe, interval);\n \n // First measurement after short delay\n setTimeout(fetchRatioDataSafe, 100);\n }\n \n function stopMonitoring() {\n if (monitoringInterval) {\n clearInterval(monitoringInterval);\n monitoringInterval = null;\n }\n isFetching = false;\n \n ById('startMonitoring').disabled = false;\n ById('stopMonitoring').disabled = true;\n ById('statusIndicator').classList.remove('running');\n ById('statusText').textContent = 'Gestoppt';\n }\n \n // ========================================================================\n // Data Fetching with timeout and error handling\n // ========================================================================\n async function fetchRatioDataSafe() {\n // Skip if already fetching (prevents overlapping requests)\n if (isFetching) {\n console.log('Skipping fetch - previous request still pending');\n return;\n }\n \n isFetching = true;\n \n const f1 = parseFloat(ById('freq1').value);\n const f2 = parseFloat(ById('freq2').value);\n const avg = parseInt(ById('avgCount').value);\n const is3pt = ById('mode3pt').checked;\n \n let url = `/ratio?f1=${f1}&f2=${f2}&avg=${avg}`;\n if (is3pt) {\n const f3 = parseFloat(ById('freq3').value);\n url += `&f3=${f3}`;\n }\n \n try {\n // Fetch with timeout\n const controller = new AbortController();\n const timeoutId = setTimeout(() => controller.abort(), FETCH_TIMEOUT);\n \n const response = await fetch(url, { signal: controller.signal });\n clearTimeout(timeoutId);\n \n if (!response.ok) throw new Error(`HTTP ${response.status}`);\n \n const data = await response.json();\n \n if (data.status === 'ok') {\n processRatioData(data);\n measurementCount++;\n ById('measurementCount').textContent = measurementCount;\n ById('statusIndicator').classList.remove('error');\n ById('statusText').textContent = 'Läuft...';\n }\n } catch (error) {\n errorCount++;\n ById('errorCount').textContent = errorCount;\n ById('statusIndicator').classList.add('error');\n \n if (error.name === 'AbortError') {\n ById('statusText').textContent = 'Timeout';\n } else {\n ById('statusText').textContent = 'Fehler';\n }\n console.warn('Fetch error:', error.message);\n } finally {\n isFetching = false;\n }\n }\n \n // ========================================================================\n // Data Processing\n // ========================================================================\n function processRatioData(data) {\n const points = data.points;\n const ratio = data.ratio;\n const timestamp = (Date.now() - startTime) / 1000;\n \n const I1 = points[0].I;\n const I2 = points[1].I;\n const I3 = points.length > 2 ? points[2].I : null;\n \n const dataPoint = {\n time: timestamp,\n r12: ratio.r12,\n r13: ratio.r13 || null,\n r23: ratio.r23 || null,\n I1, I2, I3,\n f1: points[0].f,\n f2: points[1].f,\n f3: points.length > 2 ? points[2].f : null\n };\n \n ratioData.push(dataPoint);\n if (ratioData.length > MAX_DATA_POINTS) ratioData.shift();\n \n updateDisplay(dataPoint);\n drawPlot();\n }\n \n // ========================================================================\n // Display Update\n // ========================================================================\n function updateDisplay(data) {\n const ratioDisplay = ById('ratioDisplay');\n ratioDisplay.textContent = data.r12.toFixed(4);\n ratioDisplay.classList.remove('positive', 'negative');\n if (data.r12 > 0.005) ratioDisplay.classList.add('positive');\n else if (data.r12 < -0.005) ratioDisplay.classList.add('negative');\n \n ById('f1Display').textContent = data.f1.toFixed(1);\n ById('f2Display').textContent = data.f2.toFixed(1);\n ById('i1Display').textContent = data.I1;\n ById('i2Display').textContent = data.I2;\n \n if (data.I3 !== null) {\n ById('f3Display').textContent = data.f3.toFixed(1);\n ById('i3Display').textContent = data.I3;\n ById('r13Display').textContent = data.r13.toFixed(4);\n ById('r23Display').textContent = data.r23.toFixed(4);\n }\n }\n \n // ========================================================================\n // Canvas Drawing\n // ========================================================================\n function resizeCanvas() {\n const container = canvas.parentElement;\n canvas.width = container.clientWidth;\n canvas.height = 280;\n drawPlot();\n }\n \n function drawPlot() {\n if (!ctx) return;\n \n const width = canvas.width;\n const height = canvas.height;\n const pad = { top: 20, right: 20, bottom: 35, left: 55 };\n const plotW = width - pad.left - pad.right;\n const plotH = height - pad.top - pad.bottom;\n \n // Clear\n ctx.fillStyle = '#fff';\n ctx.fillRect(0, 0, width, height);\n \n // Time window\n const timeWindow = parseInt(ById('timeWindow').value);\n const currentTime = ratioData.length > 0 ? ratioData[ratioData.length - 1].time : 0;\n const minTime = Math.max(0, currentTime - timeWindow);\n \n const visibleData = ratioData.filter(d => d.time >= minTime);\n \n // Y range\n const yRangeSetting = ById('yRange').value;\n let yMin, yMax;\n \n if (yRangeSetting === 'auto' && visibleData.length > 0) {\n const ratios = visibleData.map(d => d.r12);\n const margin = 0.05;\n yMin = Math.min(...ratios) - margin;\n yMax = Math.max(...ratios) + margin;\n if (yMax - yMin < 0.05) {\n const mid = (yMax + yMin) / 2;\n yMin = mid - 0.025;\n yMax = mid + 0.025;\n }\n } else {\n const range = yRangeSetting === 'auto' ? 1 : parseFloat(yRangeSetting);\n yMin = -range;\n yMax = range;\n }\n \n // Grid\n ctx.strokeStyle = '#eee';\n ctx.lineWidth = 1;\n for (let i = 0; i <= 5; i++) {\n const y = pad.top + (plotH * i / 5);\n ctx.beginPath();\n ctx.moveTo(pad.left, y);\n ctx.lineTo(width - pad.right, y);\n ctx.stroke();\n }\n for (let i = 0; i <= 6; i++) {\n const x = pad.left + (plotW * i / 6);\n ctx.beginPath();\n ctx.moveTo(x, pad.top);\n ctx.lineTo(x, height - pad.bottom);\n ctx.stroke();\n }\n \n // Zero line\n if (yMin < 0 && yMax > 0) {\n const zeroY = pad.top + plotH * (yMax / (yMax - yMin));\n ctx.strokeStyle = '#999';\n ctx.setLineDash([4, 4]);\n ctx.beginPath();\n ctx.moveTo(pad.left, zeroY);\n ctx.lineTo(width - pad.right, zeroY);\n ctx.stroke();\n ctx.setLineDash([]);\n }\n \n // Axes\n ctx.strokeStyle = '#023773';\n ctx.lineWidth = 2;\n ctx.beginPath();\n ctx.moveTo(pad.left, pad.top);\n ctx.lineTo(pad.left, height - pad.bottom);\n ctx.lineTo(width - pad.right, height - pad.bottom);\n ctx.stroke();\n \n // Labels\n ctx.fillStyle = '#023773';\n ctx.font = '11px sans-serif';\n ctx.textAlign = 'center';\n \n for (let i = 0; i <= 6; i++) {\n const x = pad.left + (plotW * i / 6);\n const time = minTime + (timeWindow * i / 6);\n ctx.fillText(time.toFixed(0) + 's', x, height - 8);\n }\n \n ctx.textAlign = 'right';\n for (let i = 0; i <= 5; i++) {\n const y = pad.top + (plotH * i / 5);\n const value = yMax - ((yMax - yMin) * i / 5);\n ctx.fillText(value.toFixed(2), pad.left - 5, y + 4);\n }\n \n // Data line\n if (visibleData.length > 1) {\n ctx.strokeStyle = '#023773';\n ctx.lineWidth = 2;\n ctx.beginPath();\n \n let first = true;\n for (const point of visibleData) {\n const x = pad.left + plotW * ((point.time - minTime) / timeWindow);\n const y = pad.top + plotH * ((yMax - point.r12) / (yMax - yMin));\n \n if (first) { ctx.moveTo(x, y); first = false; }\n else { ctx.lineTo(x, y); }\n }\n ctx.stroke();\n \n // Current point\n const last = visibleData[visibleData.length - 1];\n const lx = pad.left + plotW * ((last.time - minTime) / timeWindow);\n const ly = pad.top + plotH * ((yMax - last.r12) / (yMax - yMin));\n ctx.fillStyle = '#85b918';\n ctx.beginPath();\n ctx.arc(lx, ly, 5, 0, Math.PI * 2);\n ctx.fill();\n }\n \n // No data message\n if (visibleData.length === 0) {\n ctx.fillStyle = '#999';\n ctx.font = '14px sans-serif';\n ctx.textAlign = 'center';\n ctx.fillText('Keine Daten - Monitoring starten', width / 2, height / 2);\n }\n }\n \n // ========================================================================\n // Data Management\n // ========================================================================\n function clearPlot() {\n ratioData = [];\n measurementCount = 0;\n errorCount = 0;\n ById('measurementCount').textContent = '0';\n ById('errorCount').textContent = '0';\n ById('ratioDisplay').textContent = '---';\n ById('ratioDisplay').classList.remove('positive', 'negative');\n ById('i1Display').textContent = '---';\n ById('i2Display').textContent = '---';\n ById('i3Display').textContent = '---';\n ById('f1Display').textContent = '---';\n ById('f2Display').textContent = '---';\n ById('f3Display').textContent = '---';\n ById('r13Display').textContent = '---';\n ById('r23Display').textContent = '---';\n startTime = Date.now();\n drawPlot();\n }\n \n function downloadCSV() {\n if (ratioData.length === 0) {\n alert('Keine Daten vorhanden.');\n return;\n }\n \n let csv = 'Zeit_s;f1_MHz;I1;f2_MHz;I2;r12';\n if (ratioData[0].f3 !== null) csv += ';f3_MHz;I3;r13;r23';\n csv += '\\n';\n \n for (const p of ratioData) {\n csv += `${p.time.toFixed(3)};${p.f1};${p.I1};${p.f2};${p.I2};${p.r12.toFixed(6)}`;\n if (p.f3 !== null) csv += `;${p.f3};${p.I3};${p.r13.toFixed(6)};${p.r23.toFixed(6)}`;\n csv += '\\n';\n }\n \n const blob = new Blob([csv], { type: 'text/csv;charset=utf-8;' });\n const link = document.createElement('a');\n link.href = URL.createObjectURL(blob);\n link.download = `ratio_${new Date().toISOString().slice(0,19).replace(/:/g,'-')}.csv`;\n link.click();\n }\n \n // ========================================================================\n // Version Info\n // ========================================================================\n function loadVersionInfo() {\n fetch('/version')\n .then(r => r.ok ? r.json() : Promise.reject())\n .then(data => {\n ById('versionInfo').textContent = `v${data.version} | ${data.build_date}`;\n })\n .catch(() => {});\n }\n </script>\n</body>\n</html>\n";
const char RATIO_HTML[] PROGMEM = "<!DOCTYPE html>\n<html lang=\"de\">\n<head>\n <meta charset=\"UTF-8\">\n <meta name=\"viewport\" content=\"width=device-width, initial-scale=1\">\n <title>NV Experimente - B-Field Monitor</title>\n \n <!-- Offline-capable CSS framework -->\n <link rel=\"stylesheet\" href=\"style.css\">\n \n <!-- Page-specific styles -->\n <style>\n #ratioPlot {\n width: 100%;\n height: 280px;\n border: 1px solid var(--uc2-border);\n border-radius: 8px;\n background: #fff;\n }\n .plot-controls { display: flex; gap: 1rem; margin-top: 0.5rem; flex-wrap: wrap; }\n .plot-controls > div { flex: 1; min-width: 120px; }\n .plot-controls select { padding: 0.4rem; }\n .data-table { font-family: monospace; }\n .data-table td:last-child { text-align: right; font-weight: 600; font-size: 1.2rem; }\n h1 { text-align: center; }\n </style>\n</head>\n\n<body>\n <!-- ░░ Navbar ░░----------------------------------------------------------- -->\n <nav class=\"navbar navbar-expand-lg shadow-sm mb-4\">\n <div class=\"container\">\n <a class=\"navbar-brand fw-bold\" href=\"index.html\">NV-Experimente</a>\n <button class=\"navbar-toggler\" type=\"button\" data-bs-toggle=\"collapse\"\n data-bs-target=\"#nvNav\" aria-controls=\"nvNav\" aria-expanded=\"false\">\n <span class=\"navbar-toggler-icon\"></span>\n </button>\n\n <div class=\"collapse navbar-collapse\" id=\"nvNav\">\n <ul class=\"navbar-nav ms-auto\">\n <li class=\"nav-item\"><a class=\"nav-link\" href=\"index.html\" data-lang-key=\"nav_start\">Start</a></li>\n <li class=\"nav-item\"><a class=\"nav-link \" href=\"messung.html\" data-lang-key=\"nav_measurement_device\">Messung (on Device)</a></li>\n <li class=\"nav-item\"><a class=\"nav-link active\" href=\"ratio.html\" data-lang-key=\"nav_ratio\">B-Field Monitor</a></li>\n <li class=\"nav-item\"><a class=\"nav-link\" href=\"justage.html\" data-lang-key=\"nav_alignment\">Justage</a></li>\n <li class=\"nav-item\"><a class=\"nav-link\" href=\"infos.html\" data-lang-key=\"nav_info\">Weitere Infos</a></li>\n <li class=\"nav-item dropdown\">\n <a class=\"nav-link dropdown-toggle\" href=\"#\" id=\"langDropdown\" role=\"button\" data-bs-toggle=\"dropdown\" aria-expanded=\"false\">\n 🌐 DE\n </a>\n <ul class=\"dropdown-menu\" aria-labelledby=\"langDropdown\">\n <li><a class=\"dropdown-item\" href=\"#\" onclick=\"setLanguage('de'); return false;\">🇩🇪 Deutsch</a></li>\n <li><a class=\"dropdown-item\" href=\"#\" onclick=\"setLanguage('en'); return false;\">🇺🇸 English</a></li>\n </ul>\n </li>\n </ul>\n </div>\n </div>\n </nav>\n \n <div class=\"container\">\n <h1>Fast B-Field Monitoring (Ratio Mode)</h1>\n \n <div class=\"row\">\n <!-- Left column: Controls -->\n <div class=\"col-left\">\n <!-- Frequency Settings -->\n <div class=\"card\">\n <div class=\"card-header\">📡 Frequenz-Einstellungen</div>\n <div class=\"card-body\">\n <!-- Mode Toggle -->\n <div class=\"form-group\">\n <label>Modus:</label>\n <div class=\"btn-group\">\n <input type=\"radio\" name=\"modeSelect\" id=\"mode2pt\" value=\"2\" checked>\n <label for=\"mode2pt\">2-Punkt</label>\n <input type=\"radio\" name=\"modeSelect\" id=\"mode3pt\" value=\"3\">\n <label for=\"mode3pt\">3-Punkt</label>\n </div>\n </div>\n \n <!-- Frequency 1 -->\n <div class=\"form-group\">\n <label for=\"freq1\">f₁ (MHz)</label>\n <div class=\"input-group\">\n <input type=\"number\" id=\"freq1\" value=\"2865\" min=\"2200\" max=\"4400\" step=\"0.1\">\n <span class=\"suffix\">MHz</span>\n </div>\n <small>Linke Flanke des Dips</small>\n </div>\n \n <!-- Frequency 2 -->\n <div class=\"form-group\">\n <label for=\"freq2\">f₂ (MHz)</label>\n <div class=\"input-group\">\n <input type=\"number\" id=\"freq2\" value=\"2875\" min=\"2200\" max=\"4400\" step=\"0.1\">\n <span class=\"suffix\">MHz</span>\n </div>\n <small>Rechte Flanke des Dips</small>\n </div>\n \n <!-- Frequency 3 (optional) -->\n <div class=\"form-group\" id=\"freq3Container\" style=\"display: none;\">\n <label for=\"freq3\">f₃ (MHz)</label>\n <div class=\"input-group\">\n <input type=\"number\" id=\"freq3\" value=\"2870\" min=\"2200\" max=\"4400\" step=\"0.1\">\n <span class=\"suffix\">MHz</span>\n </div>\n <small>Zentrum oder Referenz</small>\n </div>\n \n <!-- Averaging -->\n <div class=\"form-group\">\n <label>Mittelungen: <span id=\"avgValue\">3</span></label>\n <input type=\"range\" id=\"avgCount\" min=\"1\" max=\"10\" value=\"3\">\n </div>\n \n <!-- Interval -->\n <div class=\"form-group\">\n <label for=\"intervalMs\">Messintervall (ms)</label>\n <input type=\"number\" id=\"intervalMs\" value=\"500\" min=\"200\" max=\"5000\" step=\"100\">\n <small>Min. 200ms empfohlen</small>\n </div>\n </div>\n </div>\n \n <!-- Control Buttons -->\n <div class=\"card\">\n <div class=\"card-header\">🎮 Steuerung</div>\n <div class=\"card-body\">\n <button class=\"btn btn-success\" id=\"startMonitoring\">▶ Monitoring starten</button>\n <button class=\"btn btn-danger\" id=\"stopMonitoring\" disabled>⏹ Monitoring stoppen</button>\n \n <div class=\"status-row\">\n <div class=\"status-dot\" id=\"statusIndicator\"></div>\n <span id=\"statusText\">Bereit</span>\n </div>\n <div class=\"status-row\">\n <small>Messungen: <span id=\"measurementCount\">0</span> | Fehler: <span id=\"errorCount\">0</span></small>\n </div>\n </div>\n </div>\n </div>\n \n <!-- Right column: Display -->\n <div class=\"col-right\">\n <!-- Current Values -->\n <div class=\"card\">\n <div class=\"card-header\">📊 Aktuelle Werte</div>\n <div class=\"card-body\">\n <div style=\"display: flex; flex-wrap: wrap; gap: 1rem;\">\n <!-- Main Ratio Display -->\n <div style=\"flex: 1; min-width: 200px;\">\n <small>Ratio r₁₂ = (I₁-I₂)/(I₁+I₂)</small>\n <div class=\"ratio-display\" id=\"ratioDisplay\">---</div>\n </div>\n \n <!-- Intensity Values -->\n <div style=\"flex: 1; min-width: 200px;\">\n <table class=\"data-table\">\n <tr>\n <td><strong>I₁</strong> @ <span id=\"f1Display\">---</span> MHz</td>\n <td id=\"i1Display\">---</td>\n </tr>\n <tr>\n <td><strong>I₂</strong> @ <span id=\"f2Display\">---</span> MHz</td>\n <td id=\"i2Display\">---</td>\n </tr>\n <tr id=\"i3Row\" style=\"display: none;\">\n <td><strong>I₃</strong> @ <span id=\"f3Display\">---</span> MHz</td>\n <td id=\"i3Display\">---</td>\n </tr>\n </table>\n \n <!-- Additional ratios for 3-point mode -->\n <div id=\"additionalRatios\" style=\"display: none; margin-top: 0.5rem; text-align: center;\">\n <small>r₁₃: <strong id=\"r13Display\">---</strong> | r₂₃: <strong id=\"r23Display\">---</strong></small>\n </div>\n </div>\n </div>\n </div>\n </div>\n \n <!-- Live Plot -->\n <div class=\"card\">\n <div class=\"card-header\" style=\"display: flex; justify-content: space-between; align-items: center;\">\n <span>📈 Live-Plot</span>\n <div>\n <button class=\"btn btn-outline\" style=\"width: auto; padding: 0.25rem 0.75rem; margin: 0;\" id=\"clearPlot\">Leeren</button>\n <button class=\"btn btn-outline\" style=\"width: auto; padding: 0.25rem 0.75rem; margin: 0;\" id=\"downloadData\">CSV ↓</button>\n </div>\n </div>\n <div class=\"card-body\">\n <canvas id=\"ratioPlot\"></canvas>\n <div class=\"plot-controls\">\n <div>\n <small>Zeitfenster:</small>\n <select id=\"timeWindow\">\n <option value=\"10\">10 s</option>\n <option value=\"30\" selected>30 s</option>\n <option value=\"60\">60 s</option>\n <option value=\"120\">120 s</option>\n </select>\n </div>\n <div>\n <small>Y-Bereich:</small>\n <select id=\"yRange\">\n <option value=\"auto\" selected>Auto</option>\n <option value=\"0.1\">±0.1</option>\n <option value=\"0.5\">±0.5</option>\n <option value=\"1\">±1.0</option>\n </select>\n </div>\n </div>\n </div>\n </div>\n \n <!-- Theory -->\n <div class=\"info-box\">\n <strong>ℹ️ So funktioniert es:</strong><br>\n Das normalisierte Verhältnis <strong>r = (I₁-I₂)/(I₁+I₂)</strong> ist proportional zur Magnetfeldverschiebung.\n Wählen Sie f₁ und f₂ symmetrisch um das ODMR-Dip-Minimum.\n </div>\n </div>\n </div>\n </div>\n\n <!-- Footer -->\n <footer>\n Uni Münster · openUC2 GmbH – <a href=\"mailto:hello@openuc2.com\">hello@openuc2.com</a>\n <div id=\"versionInfo\" style=\"opacity: 0.7; font-size: 0.8em; margin-top: 0.25rem;\">ESP32 ODMR Server</div>\n </footer>\n\n <!-- JavaScript -->\n <script>\n // ========================================================================\n // Global State\n // ========================================================================\n let monitoringInterval = null;\n let measurementCount = 0;\n let errorCount = 0;\n let ratioData = [];\n let startTime = null;\n let canvas, ctx;\n let isFetching = false; // Prevent overlapping requests\n \n const MAX_DATA_POINTS = 500;\n const FETCH_TIMEOUT = 3000; // 3 second timeout\n \n // DOM helper\n const ById = (id) => document.getElementById(id);\n \n // ========================================================================\n // Initialization\n // ========================================================================\n document.addEventListener('DOMContentLoaded', function() {\n canvas = ById('ratioPlot');\n ctx = canvas.getContext('2d');\n resizeCanvas();\n window.addEventListener('resize', resizeCanvas);\n drawPlot();\n \n // Event Listeners\n ById('startMonitoring').addEventListener('click', startMonitoring);\n ById('stopMonitoring').addEventListener('click', stopMonitoring);\n ById('clearPlot').addEventListener('click', clearPlot);\n ById('downloadData').addEventListener('click', downloadCSV);\n \n // Mode toggle\n document.querySelectorAll('input[name=\"modeSelect\"]').forEach(radio => {\n radio.addEventListener('change', handleModeChange);\n });\n \n // Averaging slider\n ById('avgCount').addEventListener('input', function() {\n ById('avgValue').textContent = this.value;\n });\n \n // Plot controls\n ById('timeWindow').addEventListener('change', drawPlot);\n ById('yRange').addEventListener('change', drawPlot);\n \n // Load version\n loadVersionInfo();\n });\n \n // ========================================================================\n // Mode Handling\n // ========================================================================\n function handleModeChange() {\n const is3pt = ById('mode3pt').checked;\n ById('freq3Container').style.display = is3pt ? 'block' : 'none';\n ById('i3Row').style.display = is3pt ? 'table-row' : 'none';\n ById('additionalRatios').style.display = is3pt ? 'block' : 'none';\n }\n \n // ========================================================================\n // Monitoring Control\n // ========================================================================\n function startMonitoring() {\n const f1 = parseFloat(ById('freq1').value);\n const f2 = parseFloat(ById('freq2').value);\n const interval = Math.max(200, parseInt(ById('intervalMs').value)); // Min 200ms\n \n if (isNaN(f1) || isNaN(f2) || f1 < 2200 || f1 > 4400 || f2 < 2200 || f2 > 4400) {\n alert('Ungültige Frequenzwerte! Bereich: 2200-4400 MHz');\n return;\n }\n \n // Update UI\n ById('startMonitoring').disabled = true;\n ById('stopMonitoring').disabled = false;\n ById('statusIndicator').classList.add('running');\n ById('statusIndicator').classList.remove('error');\n ById('statusText').textContent = 'Läuft...';\n \n // Reset state\n startTime = Date.now();\n measurementCount = 0;\n errorCount = 0;\n ById('measurementCount').textContent = '0';\n ById('errorCount').textContent = '0';\n \n // Start periodic measurements with proper interval\n monitoringInterval = setInterval(fetchRatioDataSafe, interval);\n \n // First measurement after short delay\n setTimeout(fetchRatioDataSafe, 100);\n }\n \n function stopMonitoring() {\n if (monitoringInterval) {\n clearInterval(monitoringInterval);\n monitoringInterval = null;\n }\n isFetching = false;\n \n ById('startMonitoring').disabled = false;\n ById('stopMonitoring').disabled = true;\n ById('statusIndicator').classList.remove('running');\n ById('statusText').textContent = 'Gestoppt';\n }\n \n // ========================================================================\n // Data Fetching with timeout and error handling\n // ========================================================================\n async function fetchRatioDataSafe() {\n // Skip if already fetching (prevents overlapping requests)\n if (isFetching) {\n console.log('Skipping fetch - previous request still pending');\n return;\n }\n \n isFetching = true;\n \n const f1 = parseFloat(ById('freq1').value);\n const f2 = parseFloat(ById('freq2').value);\n const avg = parseInt(ById('avgCount').value);\n const is3pt = ById('mode3pt').checked;\n \n let url = `/ratio?f1=${f1}&f2=${f2}&avg=${avg}`;\n if (is3pt) {\n const f3 = parseFloat(ById('freq3').value);\n url += `&f3=${f3}`;\n }\n \n try {\n // Fetch with timeout\n const controller = new AbortController();\n const timeoutId = setTimeout(() => controller.abort(), FETCH_TIMEOUT);\n \n const response = await fetch(url, { signal: controller.signal });\n clearTimeout(timeoutId);\n \n if (!response.ok) throw new Error(`HTTP ${response.status}`);\n \n const data = await response.json();\n \n if (data.status === 'ok') {\n processRatioData(data);\n measurementCount++;\n ById('measurementCount').textContent = measurementCount;\n ById('statusIndicator').classList.remove('error');\n ById('statusText').textContent = 'Läuft...';\n }\n } catch (error) {\n errorCount++;\n ById('errorCount').textContent = errorCount;\n ById('statusIndicator').classList.add('error');\n \n if (error.name === 'AbortError') {\n ById('statusText').textContent = 'Timeout';\n } else {\n ById('statusText').textContent = 'Fehler';\n }\n console.warn('Fetch error:', error.message);\n } finally {\n isFetching = false;\n }\n }\n \n // ========================================================================\n // Data Processing\n // ========================================================================\n function processRatioData(data) {\n const points = data.points;\n const ratio = data.ratio;\n const timestamp = (Date.now() - startTime) / 1000;\n \n const I1 = points[0].I;\n const I2 = points[1].I;\n const I3 = points.length > 2 ? points[2].I : null;\n \n const dataPoint = {\n time: timestamp,\n r12: ratio.r12,\n r13: ratio.r13 || null,\n r23: ratio.r23 || null,\n I1, I2, I3,\n f1: points[0].f,\n f2: points[1].f,\n f3: points.length > 2 ? points[2].f : null\n };\n \n ratioData.push(dataPoint);\n if (ratioData.length > MAX_DATA_POINTS) ratioData.shift();\n \n updateDisplay(dataPoint);\n drawPlot();\n }\n \n // ========================================================================\n // Display Update\n // ========================================================================\n function updateDisplay(data) {\n const ratioDisplay = ById('ratioDisplay');\n ratioDisplay.textContent = data.r12.toFixed(4);\n ratioDisplay.classList.remove('positive', 'negative');\n if (data.r12 > 0.005) ratioDisplay.classList.add('positive');\n else if (data.r12 < -0.005) ratioDisplay.classList.add('negative');\n \n ById('f1Display').textContent = data.f1.toFixed(1);\n ById('f2Display').textContent = data.f2.toFixed(1);\n ById('i1Display').textContent = data.I1;\n ById('i2Display').textContent = data.I2;\n \n if (data.I3 !== null) {\n ById('f3Display').textContent = data.f3.toFixed(1);\n ById('i3Display').textContent = data.I3;\n ById('r13Display').textContent = data.r13.toFixed(4);\n ById('r23Display').textContent = data.r23.toFixed(4);\n }\n }\n \n // ========================================================================\n // Canvas Drawing\n // ========================================================================\n function resizeCanvas() {\n const container = canvas.parentElement;\n canvas.width = container.clientWidth;\n canvas.height = 280;\n drawPlot();\n }\n \n function drawPlot() {\n if (!ctx) return;\n \n const width = canvas.width;\n const height = canvas.height;\n const pad = { top: 20, right: 20, bottom: 35, left: 55 };\n const plotW = width - pad.left - pad.right;\n const plotH = height - pad.top - pad.bottom;\n \n // Clear\n ctx.fillStyle = '#fff';\n ctx.fillRect(0, 0, width, height);\n \n // Time window\n const timeWindow = parseInt(ById('timeWindow').value);\n const currentTime = ratioData.length > 0 ? ratioData[ratioData.length - 1].time : 0;\n const minTime = Math.max(0, currentTime - timeWindow);\n \n const visibleData = ratioData.filter(d => d.time >= minTime);\n \n // Y range\n const yRangeSetting = ById('yRange').value;\n let yMin, yMax;\n \n if (yRangeSetting === 'auto' && visibleData.length > 0) {\n const ratios = visibleData.map(d => d.r12);\n const margin = 0.05;\n yMin = Math.min(...ratios) - margin;\n yMax = Math.max(...ratios) + margin;\n if (yMax - yMin < 0.05) {\n const mid = (yMax + yMin) / 2;\n yMin = mid - 0.025;\n yMax = mid + 0.025;\n }\n } else {\n const range = yRangeSetting === 'auto' ? 1 : parseFloat(yRangeSetting);\n yMin = -range;\n yMax = range;\n }\n \n // Grid\n ctx.strokeStyle = '#eee';\n ctx.lineWidth = 1;\n for (let i = 0; i <= 5; i++) {\n const y = pad.top + (plotH * i / 5);\n ctx.beginPath();\n ctx.moveTo(pad.left, y);\n ctx.lineTo(width - pad.right, y);\n ctx.stroke();\n }\n for (let i = 0; i <= 6; i++) {\n const x = pad.left + (plotW * i / 6);\n ctx.beginPath();\n ctx.moveTo(x, pad.top);\n ctx.lineTo(x, height - pad.bottom);\n ctx.stroke();\n }\n \n // Zero line\n if (yMin < 0 && yMax > 0) {\n const zeroY = pad.top + plotH * (yMax / (yMax - yMin));\n ctx.strokeStyle = '#999';\n ctx.setLineDash([4, 4]);\n ctx.beginPath();\n ctx.moveTo(pad.left, zeroY);\n ctx.lineTo(width - pad.right, zeroY);\n ctx.stroke();\n ctx.setLineDash([]);\n }\n \n // Axes\n ctx.strokeStyle = '#023773';\n ctx.lineWidth = 2;\n ctx.beginPath();\n ctx.moveTo(pad.left, pad.top);\n ctx.lineTo(pad.left, height - pad.bottom);\n ctx.lineTo(width - pad.right, height - pad.bottom);\n ctx.stroke();\n \n // Labels\n ctx.fillStyle = '#023773';\n ctx.font = '11px sans-serif';\n ctx.textAlign = 'center';\n \n for (let i = 0; i <= 6; i++) {\n const x = pad.left + (plotW * i / 6);\n const time = minTime + (timeWindow * i / 6);\n ctx.fillText(time.toFixed(0) + 's', x, height - 8);\n }\n \n ctx.textAlign = 'right';\n for (let i = 0; i <= 5; i++) {\n const y = pad.top + (plotH * i / 5);\n const value = yMax - ((yMax - yMin) * i / 5);\n ctx.fillText(value.toFixed(2), pad.left - 5, y + 4);\n }\n \n // Data line\n if (visibleData.length > 1) {\n ctx.strokeStyle = '#023773';\n ctx.lineWidth = 2;\n ctx.beginPath();\n \n let first = true;\n for (const point of visibleData) {\n const x = pad.left + plotW * ((point.time - minTime) / timeWindow);\n const y = pad.top + plotH * ((yMax - point.r12) / (yMax - yMin));\n \n if (first) { ctx.moveTo(x, y); first = false; }\n else { ctx.lineTo(x, y); }\n }\n ctx.stroke();\n \n // Current point\n const last = visibleData[visibleData.length - 1];\n const lx = pad.left + plotW * ((last.time - minTime) / timeWindow);\n const ly = pad.top + plotH * ((yMax - last.r12) / (yMax - yMin));\n ctx.fillStyle = '#85b918';\n ctx.beginPath();\n ctx.arc(lx, ly, 5, 0, Math.PI * 2);\n ctx.fill();\n }\n \n // No data message\n if (visibleData.length === 0) {\n ctx.fillStyle = '#999';\n ctx.font = '14px sans-serif';\n ctx.textAlign = 'center';\n ctx.fillText('Keine Daten - Monitoring starten', width / 2, height / 2);\n }\n }\n \n // ========================================================================\n // Data Management\n // ========================================================================\n function clearPlot() {\n ratioData = [];\n measurementCount = 0;\n errorCount = 0;\n ById('measurementCount').textContent = '0';\n ById('errorCount').textContent = '0';\n ById('ratioDisplay').textContent = '---';\n ById('ratioDisplay').classList.remove('positive', 'negative');\n ById('i1Display').textContent = '---';\n ById('i2Display').textContent = '---';\n ById('i3Display').textContent = '---';\n ById('f1Display').textContent = '---';\n ById('f2Display').textContent = '---';\n ById('f3Display').textContent = '---';\n ById('r13Display').textContent = '---';\n ById('r23Display').textContent = '---';\n startTime = Date.now();\n drawPlot();\n }\n \n function downloadCSV() {\n if (ratioData.length === 0) {\n alert('Keine Daten vorhanden.');\n return;\n }\n \n let csv = 'Zeit_s;f1_MHz;I1;f2_MHz;I2;r12';\n if (ratioData[0].f3 !== null) csv += ';f3_MHz;I3;r13;r23';\n csv += '\\n';\n \n for (const p of ratioData) {\n csv += `${p.time.toFixed(3)};${p.f1};${p.I1};${p.f2};${p.I2};${p.r12.toFixed(6)}`;\n if (p.f3 !== null) csv += `;${p.f3};${p.I3};${p.r13.toFixed(6)};${p.r23.toFixed(6)}`;\n csv += '\\n';\n }\n \n const blob = new Blob([csv], { type: 'text/csv;charset=utf-8;' });\n const link = document.createElement('a');\n link.href = URL.createObjectURL(blob);\n link.download = `ratio_${new Date().toISOString().slice(0,19).replace(/:/g,'-')}.csv`;\n link.click();\n }\n \n // ========================================================================\n // Version Info\n // ========================================================================\n function loadVersionInfo() {\n fetch('/version')\n .then(r => r.ok ? r.json() : Promise.reject())\n .then(data => {\n ById('versionInfo').textContent = `v${data.version} | ${data.build_date}`;\n })\n .catch(() => {});\n }\n </script>\n <script>\n /* Bootstrap-JS replacement: navbar collapse + dropdown */\n (function(){\n document.querySelectorAll('.navbar-toggler').forEach(function(b){\n b.addEventListener('click',function(){var t=document.querySelector(b.getAttribute('data-bs-target'));if(t)t.classList.toggle('show');});\n });\n document.querySelectorAll('.dropdown-toggle').forEach(function(b){\n b.addEventListener('click',function(e){e.preventDefault();var m=b.nextElementSibling;if(m&&m.classList.contains('dropdown-menu'))m.classList.toggle('show');});\n });\n document.addEventListener('click',function(e){if(!e.target.closest('.dropdown'))document.querySelectorAll('.dropdown-menu.show').forEach(function(m){m.classList.remove('show');});});\n })();\n </script>\n</body>\n</html>\n";
Copy link

Copilot AI Mar 4, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The ratio page navbar no longer includes the Messung (WebSerial) nav item (webSerialNavItem) while other pages still do (and now hide/show it dynamically). This makes navigation inconsistent and prevents reaching the WebSerial page from ratio.html.

Consider restoring the WebSerial nav item here and applying the same /webserial_check + navigator.serial gating used on the other pages.

Copilot uses AI. Check for mistakes.
Comment on lines +1091 to +1096
b.addEventListener('click',function(){var t=document.querySelector(b.getAttribute('data-bs-target'));if(t)t.classList.toggle('show');});
});
document.querySelectorAll('.dropdown-toggle').forEach(function(b){
b.addEventListener('click',function(e){e.preventDefault();var m=b.nextElementSibling;if(m&&m.classList.contains('dropdown-menu'))m.classList.toggle('show');});
});
document.addEventListener('click',function(e){if(!e.target.closest('.dropdown'))document.querySelectorAll('.dropdown-menu.show').forEach(function(m){m.classList.remove('show');});});
Copy link

Copilot AI Mar 4, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The custom navbar/dropdown toggle script toggles visibility but does not update aria-expanded on the toggler/dropdown elements. That leaves ARIA state incorrect for assistive technologies.

When toggling the show class, also update the relevant aria-expanded attributes (and consider keyboard/focus handling if replacing Bootstrap behaviors).

Suggested change
b.addEventListener('click',function(){var t=document.querySelector(b.getAttribute('data-bs-target'));if(t)t.classList.toggle('show');});
});
document.querySelectorAll('.dropdown-toggle').forEach(function(b){
b.addEventListener('click',function(e){e.preventDefault();var m=b.nextElementSibling;if(m&&m.classList.contains('dropdown-menu'))m.classList.toggle('show');});
});
document.addEventListener('click',function(e){if(!e.target.closest('.dropdown'))document.querySelectorAll('.dropdown-menu.show').forEach(function(m){m.classList.remove('show');});});
b.addEventListener('click', function(){
var t = document.querySelector(b.getAttribute('data-bs-target'));
if (t) {
var isShown = t.classList.toggle('show');
b.setAttribute('aria-expanded', isShown ? 'true' : 'false');
}
});
});
document.querySelectorAll('.dropdown-toggle').forEach(function(b){
b.addEventListener('click', function(e){
e.preventDefault();
var m = b.nextElementSibling;
if (m && m.classList.contains('dropdown-menu')) {
var isShown = m.classList.toggle('show');
b.setAttribute('aria-expanded', isShown ? 'true' : 'false');
m.setAttribute('aria-hidden', isShown ? 'false' : 'true');
}
});
});
document.addEventListener('click', function(e){
if (!e.target.closest('.dropdown')) {
document.querySelectorAll('.dropdown-menu.show').forEach(function(m){
m.classList.remove('show');
m.setAttribute('aria-hidden', 'true');
var parent = m.parentElement;
if (parent) {
var toggle = parent.querySelector('.dropdown-toggle');
if (toggle) {
toggle.setAttribute('aria-expanded', 'false');
}
}
});
}
});

Copilot uses AI. Check for mistakes.
Add static web assets for ODMR_Server (HTML/CSS/JS/image files) and update the project to include them. Update CI workflow to trigger on Production_Files/Software/ODMR_Server/** and the new branch 'fix-listoferrors'; add spiffs_offset values for ESP32S3/C3, run pio buildfs, and merge spiffs.bin into the final firmware binary. Also update PlatformIO project files (platformio.ini, src/main.cpp) to accommodate the website/SPIFFS changes.
Add a PlatformIO extra script (scripts/merge_espwebtools.py) that merges bootloader, partitions, app and SPIFFS into a single firmware image and provides custom targets mergedbin and upload_merged for creating and flashing the combined .bin. Update platformio.ini to register the post build script for relevant envs, add the generated build/fw-images/seeed_xiao_esp32c3.bin, and extend README with build/merge/flash usage examples (esptool commands and PIO targets). The merge script parses partition CSV to locate SPIFFS offset, locates required binaries (boot_app0, app, spiffs), and preserves board flash settings when invoking esptool.
Expanded README with detailed build/flash/development guide, merged-binary workflow, esptool and web-flasher instructions, partition layout and troubleshooting. Updated prebuilt firmware image (build/fw-images/seeed_xiao_esp32c3.bin).

Frontend changes: added null-guards when registering event listeners in messung.html to avoid errors if elements are missing; added data-lang-key attributes and an i18n script to ratio.html (DE/EN translations, language persistence and dynamic updates); improved navbar toggler visuals and responsive collapse background in style.css.

Firmware changes: updated captive-portal endpoints in src/main.cpp to serve a small HTML portal page (PORTAL_HTML) so mobile OSes show the captive-portal browser/prompt reliably; retained Windows plain-text probes where appropriate. These changes improve UX for initial device setup and make the web UI more robust.
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.
Embed generated website assets into firmware and prefer PROGMEM pages over SPIFFS; add helpers to serve PROGMEM strings and update root handler. Implement LED breathing brightness (sinusoidal pulse) and adjust LED behavior per status. Lower alignment/intensity thresholds in the Justage page and JS logic (Optimal: 50000->4000, Gut: 20000->1500). Add extra startup/version logging, handle captive-portal URI variants (/generate_204*), and update build/version metadata. Minor README esptool port adjustment and regenerated HTML header files.
Canan Gallitschke and others added 5 commits March 11, 2026 11:20
Implement sweep recovery and stop controls: add a SweepDataPoint buffer (MAX_SWEEP_BUFFER) to store recent sweep points, track sweepInProgress and sweepStopRequested, and persist points during a sweep. Introduce HTTP endpoints /sweep_buffer (GET) to retrieve buffered data and /sweep_stop (POST) to request stopping a running sweep. Update sweep loop to check for client disconnect or explicit stop request and improve logging; ensure buffer reset at sweep start and clear flags at completion.

Other updates: bump build metadata (date/time/git hash), update README esptool path, add an image width attribute in index.html, reorder a serial unknown-command log branch, update many embedded website HTML assets, and include an updated firmware binary.
@beniroquai beniroquai merged commit b846413 into main Mar 25, 2026
3 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants