From 658a1b3ee34b4cf735f62de58ae725389f582a04 Mon Sep 17 00:00:00 2001 From: chk <79915315+ChKendel@users.noreply.github.com> Date: Mon, 8 Jun 2026 21:29:02 +0200 Subject: [PATCH] Claude: Reorganzie --- .gitignore | 8 ++- README.md | 132 +++++++++++++++++++++++++----------- doc/ToDo.md | 160 ++++++++++++++++++++++++++++++++++++++++++++ https/localhost.key | 30 +++++++++ https/localhost.pem | 22 ++++++ 5 files changed, 313 insertions(+), 39 deletions(-) create mode 100644 doc/ToDo.md create mode 100644 https/localhost.key create mode 100644 https/localhost.pem diff --git a/.gitignore b/.gitignore index baef84b..bdcce1b 100755 --- a/.gitignore +++ b/.gitignore @@ -1,13 +1,19 @@ # Dependencies node_modules/ -# Zertifikate & Schlüssel (niemals committen!) +# Zertifikate & Schlüssel (echte Keys niemals committen!) certs/ *.pem *.key *.crt *.pfx +# Ausnahme: self-signed Cert NUR für den internen Proxy<->Backend-Hop. +# Hat keinen echten Wert (self-signed, nie öffentlich), darf daher mitreisen, +# damit der Container per Volume-Mount HTTPS sprechen kann. +!https/localhost.key +!https/localhost.pem + # Umgebungen & lokale Artefakte .env .env.* diff --git a/README.md b/README.md index 40f8324..be6e8ed 100755 --- a/README.md +++ b/README.md @@ -1,49 +1,105 @@ # appRobotHoming -`appRobotHoming` ist eine browserbasierte Benutzeroberfläche für die -WebCam-gestützte Ermittlung der Roboterpose. Der Einstieg bleibt als einfaches -Frontend erhalten, während die Auswertung künftig an den BodyTracker weitergeleitet -wird. +`appRobotHoming` ist die browserbasierte Bedienoberfläche für die WebCam-gestützte +Ermittlung der Roboterpose. Das Frontend bleibt der Einstieg; die eigentliche +Bildverarbeitung läuft hinter der Firewall auf eigenen Services (WebCam, +BodyTracker), die der Homing-Backend als schlanker Proxy anspricht. -## Was das Projekt jetzt macht -- Holt aus der WebCam alle 3 bis 10 Bilder ab (siehe `doc/README_WebCam.md`). -- Zeigt ausgewählte Bilder und die zugehörigen `.npz`-Daten in einer Auswertungsansicht. -- Übergibt diese Daten an den BodyTracker (`doc/README_BodyTracker.md`). -- Ermittelt daraus die Roboterpose und gibt sie aus. +## Architektur im Überblick -## Aktueller Fokus -- Benutzeroberfläche bleibt der Einstieg. -- Bildanzeige und Poseausgabe sind zentral. -- Der alte HTTPS/WSS-Server wurde entfernt. -- `certs/`, `scripts/` und `server/` sind nicht mehr Teil des aktuellen Projekts. +``` +Browser ──HTTPS──▶ Reverse-Proxy ──HTTPS/WSS──▶ appRobotHoming-Backend +(statisches UI) (öffentliches TLS) (server/server.js, Port 2093) + │ + intern (hinter der Firewall, HTTP): + ├──▶ WebCam-Service (Bilder) + ├──▶ BodyTracker-Service (Pose) + └──▶ … weitere Schritte (später) +``` -## Integration -- Die WebCam- und BodyTracker-Aufrufe laufen über das Backend, nicht direkt aus dem Browser. -- Das Frontend lädt Snapshot-Daten über `/api/latest-snapshot`. -- Der Browser sendet Pose-Anfragen an `/api/estimate`. -- Das Backend kann dann auf interne Docker-Container zugreifen, z. B. auf den WebCam-Service und den BodyTracker-Service. -- Als Fallback verwendet das Backend lokale `public/snapshots`, wenn keine externe WebCam verfügbar ist. -- Konfigurierbare Umgebungsvariablen: - - `WEBCAM_URL` – Basis-URL des internen Webcam-Services. - - `BODYTRACKER_URL` – Basis-URL des internen BodyTracker-Services. +- **Frontend (`public/`)** – statische Seite: zeigt Infos, Buttons und die + Rückmeldungen (Result als JSON + Tree-View, Snapshot-Tabelle, Bilder). Kein + direkter Zugriff auf die internen Services. +- **Backend (`server/server.js`)** – BFF-Proxy. Liefert das statische Frontend + aus und stellt eine kleine API bereit, über die das UI an die internen + Services kommt. Läuft auf **HTTPS, Port 2093**. + +## Ablauf + +1. Das UI lädt den aktuellen Stand über `GET /api/latest-snapshot`. +2. **Bilder und Kamera-Intrinsics kommen vom WebCam-Service** (eigener Server + hinter der Firewall; die Kamera ist Source of Truth ihrer eigenen Kalibrierung). +3. Auf Knopfdruck schickt das UI eine Pose-Anfrage an `POST /api/estimate`. +4. Der Backend reicht **Bilder + Intrinsics** zur Verarbeitung an den + **BodyTracker** weiter und erhält die Roboterpose zurück. +5. Das Ergebnis wird im UI ausgegeben (JSON, Tree, Tabelle, annotierte Bilder). +6. **Eventuell folgen weitere Schritte** (z. B. Pose an `appRobotDriver` geben). + +Fällt der BodyTracker aus, rechnet das Frontend ersatzweise lokal mit +`public/calculateAngles.js`. + +## HTTPS (bewusste Entscheidung) + +Der Backend läuft selbst auf **HTTPS** – auch wenn davor schon ein Reverse-Proxy +die öffentliche TLS-Terminierung übernimmt. Grund: **WebSocket-Verbindungen (WSS) +kommen nur sauber durch den Proxy, wenn auch der Backend-Hop TLS spricht.** + +- Das verwendete Zertifikat ist **self-signed** (`https/`, Passphrase `abcd`). + Das ist Absicht: Dieser Hop ist nur **Proxy ↔ Backend**, nie öffentlich. Die + vertrauenswürdige Kette stellt der vorgelagerte Reverse-Proxy bereit. +- Zugriff im internen Netz z. B. über `https://thinkcentre.local:2093/`. + +## API (Backend) + +| Endpoint | Methode | Zweck | +|---|---|---| +| `/api/health` | GET | Status + konfigurierte Service-URLs | +| `/api/latest-snapshot` | GET | Aktuelle Bilder/Daten (vom WebCam-Service bzw. lokalem Fallback) | +| `/api/estimate` | POST | Bilder an BodyTracker geben → Pose zurück | + +## Konfiguration (Umgebungsvariablen) + +| Variable | Bedeutung | +|---|---| +| `HTTPS_PORT` | Port des Backends (Default `2093`) | +| `WEBCAM_URL` | Basis-URL des internen WebCam-Services | +| `BODYTRACKER_URL` | Basis-URL des internen BodyTracker-Services | +| `HTTPS_KEY_PATH` / `HTTPS_CERT_PATH` / `HTTPS_PASSPHRASE` | self-signed Cert für den Proxy-Hop | + +Ist `WEBCAM_URL` nicht gesetzt, nutzt der Backend lokale Dateien aus +`public/snapshots` als Fallback (Entwicklung ohne Kamera). + +## Dateien & Struktur + +- `public/` – statisches Frontend (UI, Client-Logik, Anzeige). +- `server/server.js` – HTTPS-Backend / BFF-Proxy. +- `https/` – self-signed Zertifikate für den Proxy-Hop (nicht eingecheckt). +- `doc/README_WebCam.md` – WebCam-Service (Bildquelle). +- `doc/README_BodyTracker.md` – BodyTracker-Service (Pose-Ermittlung). +- `doc/ToDo.md` – offene Punkte & nächste Umsetzungsschritte. +- `test/` – Tests für Berechnung und Auswertung. + +## Nutzung + +```bash +npm install +npm test +npm start # startet den HTTPS-Backend auf Port 2093 +``` + +Danach im internen Netz `https://:2093/` öffnen (self-signed → einmalige +Zertifikatswarnung im Browser bestätigen). + +> Hinweis: Das Frontend ist auf den Backend angewiesen – `/api/latest-snapshot` +> und `/api/estimate` funktionieren **nicht**, wenn man `index.html` rein +> statisch öffnet. Immer über `npm start` (bzw. den Container) laufen lassen. ## Geplante Erweiterungen + 1. Pose an `appRobotDriver` weitergeben. 2. Wenn die Hand nicht erkannt wird: Vorschlag für eine bessere Arm-/Foto-Position. 3. Manuelle Eingabe von `x, y, z, a, b, c, e`. -4. Erkennungsergebnis und erkannte Pose klar im UI ausgeben. +4. Erkennungsergebnis und Pose klarer im UI ausgeben. -## Dateien & Struktur -- `public/` – Frontend, UI, Client-Logik und Anzeige. -- `doc/README_WebCam.md` – Details zur Webcam-Architektur und Bildabholung. -- `doc/README_BodyTracker.md` – BodyTracker-Integration und Poseermittlung. -- `test/` – bestehende Tests für die Berechnung und Auswertung. - -## Nutzung -1. `npm install` -2. `npm test` -3. Öffne `public/index.html` im Browser oder nutze einen beliebigen statischen Server. - -> Hinweis: Die Anwendung ist aktuell als Frontend/Analyse-UI aufgebaut. Die -> Backend-Serverlogik aus früheren Versionen wurde bereinigt, um das Projekt zu -> fokussieren. +Konkrete nächste Schritte und offene Schnittstellen-Fragen: siehe +[`doc/ToDo.md`](doc/ToDo.md). diff --git a/doc/ToDo.md b/doc/ToDo.md new file mode 100644 index 0000000..b510b19 --- /dev/null +++ b/doc/ToDo.md @@ -0,0 +1,160 @@ +# ToDo – appRobotHoming + +Nächste Schritte für den Umbau zum statischen Frontend + HTTPS-BFF-Proxy. +Kontext/Architektur: siehe [`../README.md`](../README.md). + +Reihenfolge ist grob nach Priorität sortiert. Offene Entscheidungen sind als +**Frage** markiert – die müssen vor der Umsetzung geklärt werden. + +--- + +## 1. HTTPS wieder zum Laufen bringen (blockierend) + +**Diagnose abgeschlossen (2026-06-08):** + +- Backend läuft auf 2093, aber als **HTTP** (`http://…:2093/` → 200 + UI; + `https://…:2093/` → keine Antwort). Es ist der **Container** (docker-compose): + `/api/health` meldet `webcamUrl: http://appRobotWebcam:8444`. +- Der Code ist korrekt: `https/localhost.key` + `localhost.pem` mit Passphrase + `abcd` laden lokal **einwandfrei** in `https.createServer`. +- **Root Cause:** Die Cert-Dateien **fehlen im Container**. `.gitignore` schließt + `*.key`/`*.pem`/`*.pfx` aus → nicht in git → der Mount + `/home/chk/Documents/appRobotHoming` auf `thinkcentre` hat sie nicht → + `createHttpsServer()` scheitert an `fsPromises.access()` → **stiller + HTTP-Fallback** (`server/server.js`, `createHttpsServer` → `return null`). + +**Gewählte Lösung (Entscheidung): Cert reist mit dem Repo/Volume.** + +Das self-signed Cert sichert nur den Hop **Proxy ↔ Backend** und hat keinen +echten Wert (nie öffentlich). Daher darf es mit ins Repo und kommt über den +bestehenden `/app`-Volume-Mount automatisch in den Container. + +- [x] `.gitignore`: gezielte Ausnahme `!https/localhost.key` + `!https/localhost.pem` + (Schutz für alle anderen Schlüssel bleibt bestehen). → erledigt. +- [ ] `https/localhost.key` + `https/localhost.pem` committen. +- [ ] Deploy-Host `thinkcentre:/home/chk/Documents/appRobotHoming/` aktualisieren + (git pull bzw. Dateien bereitstellen), damit der Container sie sieht. +- [ ] Verifizieren: `https://thinkcentre.local:2093/api/health` antwortet über + **HTTPS** (aktuell nur HTTP). + +Optional (separat, nicht Teil der Entscheidung): + +- [ ] Passphrase/Pfad konsequent aus dem Env (`HTTPS_KEY_PATH`/`HTTPS_CERT_PATH`/ + `HTTPS_PASSPHRASE`) statt hartkodiert. +- [ ] Stillen HTTP-Fallback lauter machen (klar loggen / hart abbrechen statt + unbemerkt HTTP) – dieser Fallback hat den Bug verschleiert. +- [ ] Reverse-Proxy prüfen: Leitet er WSS-Upgrade-Header korrekt an + `https://…:2093` weiter? + +--- + +## 2. WebCam-Bilder über die dokumentierte API holen → **entschieden: Option B** + +`server/server.js` ruft aktuell `WEBCAM_URL + /api/latest-snapshot` auf – diesen +Endpoint gibt es **nicht**. **Entscheidung:** Der Homing-Backend setzt das Bundle +selbst aus den dokumentierten Endpoints zusammen (kein neuer Endpoint im +WebCam-Service). + +Service läuft und ist erreichbar unter `http://thinkcentre.local:8444` +(→ `WEBCAM_URL`). Real verifizierte Contracts: + +- `GET /api/cameras` → + ```json + {"cameras":[ + {"id":"cam0","name":"Kamera 0","position":"front","stream":true,"hires":true, ...}, + {"id":"cam1","name":"Kamera 1","position":"left", "stream":true,"hires":true, ...}, + {"id":"cam2","name":"Kamera 2","position":"right","stream":true,"hires":true, ...} + ]} + ``` +- `GET /api/snapshot/{id}/hires` → `200 image/jpeg` (cam0/cam1 ~1280px, cam2 1920px). +- `GET /health` → Status + `state`/`hasFrame` pro Kamera. + +Umsetzung in `server/server.js` (`/api/latest-snapshot`): + +- [ ] `GET {WEBCAM_URL}/api/cameras` holen → Kameraliste (mit `hires:true` filtern). +- [ ] Für jede Kamera **parallel** `GET {WEBCAM_URL}/api/snapshot/{id}/hires` → + JPEG, als base64 + `id`/`position` ins Bundle. +- [ ] Bundle ans UI zurückgeben (statt der bisherigen CSV-Annahme). Reihenfolge/ + Zuordnung über `id`+`position` stabil halten (wichtig für §3). +- Hinweis: Die alte CSV/`_annotated.jpg`/JSON-Marker-Erkennung kommt **nicht** + von der Webcam. Für den BodyTracker-Pfad ist sie ohnehin redundant + (BodyTracker macht ArUco selbst); sie bleibt nur für den lokalen Fallback + `public/calculateAngles.js` relevant. + +--- + +## 3. Intrinsics-Fluss → **entschieden: WebCam liefert Intrinsics mit** + +**Klarstellung der Datentypen** (im Altcode unter „Intrinsics" vermischt): + +| Typ | Beispiel | Quelle | statisch | +|---|---|---|---| +| **Kamera-Intrinsics** (K, Distortion) | das echte „NPZ" | Kalibrierung | ✅ pro Kamera | +| **Extrinsics** (`position_mm`, `orientation_deg`) | in `_two_cam.json` | wird gelöst | ❌ Output | +| **Marker-3D-Posen** | in `_two_cam.json` | trianguliert | ❌ Output | + +Das, was `server/server.js` heute als `robotIntrinsics` (= `_two_cam.json`) +schickt, sind **Extrinsics + triangulierte Marker** – also **BodyTracker-Output**, +fälschlich als Input verschickt. Muss raus. + +Die WebCam liefert aktuell **kein** NPZ (alle Intrinsics-Endpoints → 404). Der +BodyTracker hält in `/v1/config` nur Solver-Parameter, keine Intrinsics. + +**Entscheidung:** Die Kamera-Intrinsics leben beim **WebCam-Service** (Source of +Truth) und werden mitgeliefert. Homing reicht sie an den BodyTracker durch. + +Aufgaben WebCam-Service: +- [ ] Kameras kalibrieren (Schachbrett) → K + Distortion je Kamera. **Achtung: + Intrinsics sind auflösungsabhängig** – sie müssen zur **`hires`-Auflösung** + passen, die `/api/snapshot/{id}/hires` ausliefert (C270 1280×960, + C920 1920×1080). +- [ ] Intrinsics in `/api/cameras` je Kamera ausgeben (K, dist, `calib_size`), + stabil gekeyt über `id`/`note`-Serial. + +Aufgaben Homing-Backend (`server/server.js`): +- [ ] `robotIntrinsics`/`_two_cam.json`-Pfad aus `/api/estimate` entfernen. +- [ ] Pro Kamera Bild + zugehörige Intrinsics paaren (über `id`) und an + `BODYTRACKER/v1/estimate` weiterreichen. **N Kameras** (aktuell 3), nicht + fix 2. + +Aufgaben BodyTracker (wir besitzen ihn): +- [ ] `/v1/estimate` so anpassen, dass er Intrinsics **als JSON je Kamera** + annimmt (kein `.npz`-Zwang) – erspart NPZ-Erzeugung in Node. Format + gemeinsam festzurren (K 3×3 oder fx/fy/cx/cy, dist k1,k2,p1,p2,k3, + `image_size`). + +--- + +## 4. Aufräumen (Altlasten aus der WSS/HTTPS-Server-Ära) + +- [ ] `docker-compose.yaml`: `WSS_VIDEO_DRIVER`, `WSS_URL`, `HTTPS_PORT`-Doppelung + und `depends_on: appRobotDriver` prüfen/entfernen. Behalten: `WEBCAM_URL`, + `BODYTRACKER_URL`, Port-Mapping `2093`, Cert-Volume. +- [ ] `package.json` → `description` ist veraltet („verbindet zu WSS, sendet + Befehle"). +- [ ] Command-Buttons in `public/index.html` (HOME/STOP/STATUS/RESET/PING/ + GCodeMotor) rufen `window.sendCommand` – **das ist nirgends definiert** + (alter WSS-Transport). Entweder neuen Transport anbinden (gehört zu + Erweiterung „Pose an appRobotDriver") oder bis dahin deaktivieren. +- [ ] `public/snapshots` (Fallback) ist leer → `findLatestSnapshotFile()` liefert + `null` → 404. Beispiel-Datensatz ablegen **oder** Fallback dokumentiert + abschalten. + +--- + +## 5. Frontend / UX (geplante Erweiterungen) + +- [ ] Erkennungsergebnis + erkannte Pose klarer ausgeben (statt nur Roh-JSON). +- [ ] Manuelle Eingabe von `x, y, z, a, b, c, e`. +- [ ] Wenn Hand nicht erkannt: Vorschlag für bessere Arm-/Foto-Position. +- [ ] Pose an `appRobotDriver` weitergeben (neuer Schritt nach BodyTracker; + ggf. der WSS-Pfad, für den HTTPS gebraucht wird). + +--- + +## Definition of Done (erster Meilenstein) + +1. `https://:2093/` liefert das statische UI über **HTTPS**. +2. `GET /api/latest-snapshot` liefert reale Bilder/Daten vom WebCam-Service. +3. `POST /api/estimate` liefert eine Pose vom BodyTracker und zeigt sie im UI. +4. Keine toten Buttons / keine WSS-Altlasten in Compose & package.json. diff --git a/https/localhost.key b/https/localhost.key new file mode 100644 index 0000000..873f18f --- /dev/null +++ b/https/localhost.key @@ -0,0 +1,30 @@ +-----BEGIN RSA PRIVATE KEY----- +Proc-Type: 4,ENCRYPTED +DEK-Info: AES-256-CBC,9FB898B8D313CB3A043B4F4E2F62C6D8 + +cg368RyYjMGTo9L8CtbSDMvzUisFai7fsWtrzegoGMrgYEsmkqI/NPDHO893tf7n +X4kXDqpWlYT9awry0yYDgYOyOVffBbh7+TfHEH2ZHC8dCNf7uF8OJlGbV3fVKZ92 +yrNaHrrnHgVwb78SYrFpkKJLL42O7OAIC9yKfKv9TFJF+g5bntAqe/TNv1vL1Zmm +Y2Hlm9SaDgZhrn8dhUQ6uWq0pNVZ8NBcjzerAyEw8CSIa8y4pXU8dQHz0aMGqmW3 +P9hgo8g9jI1U/V9WmacLuh6DDyWuT2qEqw+Qcp6WgXBV9eTVUMN+rmWxfFmL+hOn +fgSwFrQwNpI1V/WZlp5qL9UvzthJs7H5fK62pSfSN8ZRAWN2Yi4pOPQTOPY//h8t +q2x9f5YXrV+GOfDFeLAoUElENNh0zakjjNv5n/q1Pnw8RPouB9TWkJMEjGWbwxYb +vNt5z8QJXF9oB0wZgX6kcMpZST9Mi1+7AoWUrFWTeHmP55s2gv9lx+M1XvRkqtv1 +Ddg5ANkCYf4chv2GLsn5nEae4aALIMq8XOa1BF7LkHQxuNTBNamfJwm4/U45+f1W +NaG+xHroPkgRRW8fklvpF8mJ0q8IbvrtapSWZQcxPnxmknJtZynJjqeiO22tbtmc +n1okJzNI6FvertxGDNY+QZfx6NZJ4sIXEhTw6dQce2x9vJPpA6VbMI+A39RJS4VO +LxWU+Zv2H1YTR0QjrT0of/sFOdMhUldyyTDdqTZd7Z1NIUDX1GW9pFua+29ntK04 +QTwCis5feu/egSWctbCNAIf/YXi9IM5su1njY7H6G4wwcRFGU11O6I8TqM/8l3gm +5mYkTGJiBmEqbz3q+99XGbr+0K8YfB6x8eUlWZgmdw3cvKYRly+km1+kyJGB+pTc +2wNMN9oLHntcQV9NBgKtebuQQygfd4ZSVdheijIJQfpPFW0aDiKrnvvhcEixSTqn +6WTaEJhhA67GOWmtXLjLN3r0+nndvtL6N2UXP9FSZg6tCpn0k5jvJidKfGucBL7K +aoKP1yJ19ho4WjpJcAF1TyKgEbLb0rNHMr6AObLFQsVynbyPgA5FY5zTvUH5PZDd +oVyL8yYD2fQC2ghLdBxNLanlIh6gbintHCO2DEx8LY7itvjGthR5WdVCPbxK6L74 +4FgXRBW9NXNh53O+6SBUT6dn064qVVBOsxqGhur15Bna5X8ULkRSHw3QMsv4e+9Q +kWyJ3r5jfHGMhdvNlU/gc6S4qaPFEBRAB3QUE7xDHjU9rrzXZzd+gcy7+pqFtRJz +pFQeVNuKYpwVqx1V+nAN+ZlIJim67JOuY9qGMeKwT4kVtujESgLG4eNmXyoxsT4Y +hwCmxzLr9eu0HpmcHVULTg1NhflaLSK1ThauawEhght2JmGgxsn3xTElx4adWUBI +4hb5MTCHy0NY6T3sDNOXeRSJoRb31rMA099CVTrF7xui9Bbzoj7q9DImh43S1h4d +nwQ/optM7GiMZlGqgHdEob6hBxXMfA1EfSnyN3KPxwsa0z9OoMio7Uu6+MZt+we7 +Z9myzP2XSg/YsmtCvtvKKHuxdOun2byScwtagtGty/IXBkRsRz/E7sXNxOnJJVSS +-----END RSA PRIVATE KEY----- diff --git a/https/localhost.pem b/https/localhost.pem new file mode 100644 index 0000000..3584dbe --- /dev/null +++ b/https/localhost.pem @@ -0,0 +1,22 @@ +-----BEGIN CERTIFICATE----- +MIIDuzCCAqOgAwIBAgIJAOtJAMcSg7G5MA0GCSqGSIb3DQEBCwUAMHQxCzAJBgNV +BAYTAkNIMQswCQYDVQQIDAJaSDEPMA0GA1UEBwwGWnVyaWNoMQwwCgYDVQQKDAND +aEsxDzANBgNVBAsMBkRldkNoSzEMMAoGA1UEAwwDQ2hLMRowGAYJKoZIhvcNAQkB +FgtjaGtAYWJjZC5lZjAeFw0yMjA3MjcxNDI2MjNaFw0zMjA3MjQxNDI2MjNaMHQx +CzAJBgNVBAYTAkNIMQswCQYDVQQIDAJaSDEPMA0GA1UEBwwGWnVyaWNoMQwwCgYD +VQQKDANDaEsxDzANBgNVBAsMBkRldkNoSzEMMAoGA1UEAwwDQ2hLMRowGAYJKoZI +hvcNAQkBFgtjaGtAYWJjZC5lZjCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoC +ggEBAOZS5yfFHiPN4AvGoAhTrJ+MqcuNdwr8a1oug3yjgfLUdpCSzYsHMyDwNkD1 +acGX4csalo5qnqEzsG9Aj3v5zBPXbCDUb5AfCzEo+C5QkHsEEVFlLcAgh0haSiFo +J2ossXPIwptosv2jNUBAp25tk7CoI0yNgYk/WT0ZxwYlAPI1oOVlIdGSHoQfVLVr +1+xudxP4H2F/2ZoGvYE1FU3vHNDWumrIjpVC1YdKiB7TiMxL72etbhNIWj6ZMrgQ +h+ZhqUbN/crzoQT3gd5TK5gAWTEY34Hxhl9W/IDFxGrwyIct/YVdLOvPbqyWo+W/ +1gGQzFlN85DUYCe2kPZfvIP15vcCAwEAAaNQME4wHQYDVR0OBBYEFMl+Cq5lqeN2 +lf3mp7mPDk7AXT10MB8GA1UdIwQYMBaAFMl+Cq5lqeN2lf3mp7mPDk7AXT10MAwG +A1UdEwQFMAMBAf8wDQYJKoZIhvcNAQELBQADggEBADc8KDpE4zXb0Gag82Iueux+ +RCOLNb6Rbt3Ltoi8TzlbHpBb9LMsplpuQt+RHUDBDKh+vs73y53Sne5Ro9jd3JV7 +0eY+V3SJYk3sSzSkqPgxeJFHsXFkQz9Y9UqloW9jpULGJTBPavXa0P4QcZ/2dnys +LGmV+cZhg3i8wOeM+3Ny8ZJGRJi7xj2Zb7EjGfuDwwc9wrXxGA2l9bJHuqxnzdyi +L4ApXrZ8t6lABOD+XwgbY5DepO2av8KUcBnhA91Jc2DD/gZrHZyxHG2/vT662+Mc +VdCS92IIPjOTe3TxJZ+5NgRjlU0JlYHe+aRG2RZ+AmmWBc+5DB+58PaImpcwkt8= +-----END CERTIFICATE-----