diff --git a/companion-app/capacitor.config.ts b/companion-app/capacitor.config.ts
new file mode 100644
index 0000000..8f906c1
--- /dev/null
+++ b/companion-app/capacitor.config.ts
@@ -0,0 +1,51 @@
+/**
+ * Capacitor-Konfiguration für die native Hülle.
+ *
+ * **Noch nicht in Betrieb.** Die Pakete sind bewusst nicht installiert; diese
+ * Datei liegt fertig da, damit die Inbetriebnahme nur noch aus den Befehlen in
+ * `../UMSETZUNGSPLAN.md` Phase 10 Stufe 2 besteht:
+ *
+ * npm i -D @capacitor/cli
+ * npm i @capacitor/core @capacitor/ios @capacitor/android
+ * npx cap add ios && npx cap add android
+ * npm run build && npx cap sync
+ * npx cap open ios # in Xcode signieren und aufs Gerät bauen
+ *
+ * Nur Sideload, nie App Store: die Audi-Schrift und die Typenschilder sind
+ * ausschließlich für diese private Installation freigegeben.
+ *
+ * Der Typ ist absichtlich nicht aus @capacitor/cli importiert — das Paket
+ * fehlt ja noch, und ein toter Import würde die Typprüfung brechen.
+ */
+
+interface CapacitorKonfiguration {
+ appId: string
+ appName: string
+ webDir: string
+ server?: { androidScheme?: string; iosScheme?: string; cleartext?: boolean }
+ ios?: { contentInset?: string; backgroundColor?: string }
+ android?: { backgroundColor?: string }
+}
+
+const konfiguration: CapacitorKonfiguration = {
+ appId: "app.datametric360",
+ appName: "DataMetric360",
+ webDir: "dist",
+ server: {
+ // https, damit die App als sicherer Ursprung gilt — sonst verweigern
+ // Browser-Bausteine wie die Zwischenablage ihren Dienst.
+ androidScheme: "https",
+ iosScheme: "https",
+ },
+ ios: {
+ // Die App zeichnet bis unter die Statusleiste und hält den sicheren
+ // Bereich selbst über env(safe-area-inset-*) frei.
+ contentInset: "never",
+ backgroundColor: "#161b23",
+ },
+ android: {
+ backgroundColor: "#161b23",
+ },
+}
+
+export default konfiguration
diff --git a/companion-app/index.html b/companion-app/index.html
index 1599a53..8862b87 100644
--- a/companion-app/index.html
+++ b/companion-app/index.html
@@ -8,6 +8,15 @@
/>
+
+
+
+
+
+
+
DataMetric360
diff --git a/companion-app/public/apple-touch-icon.png b/companion-app/public/apple-touch-icon.png
new file mode 100644
index 0000000..29cdfe1
Binary files /dev/null and b/companion-app/public/apple-touch-icon.png differ
diff --git a/companion-app/public/manifest.webmanifest b/companion-app/public/manifest.webmanifest
new file mode 100644
index 0000000..aae28cb
--- /dev/null
+++ b/companion-app/public/manifest.webmanifest
@@ -0,0 +1,33 @@
+{
+ "name": "DataMetric360",
+ "short_name": "DM360",
+ "description": "Fahrzeugdaten, Fahrtenbuch und Tankvorgänge vom eigenen Server.",
+ "lang": "de",
+ "dir": "ltr",
+ "start_url": "./",
+ "scope": "./",
+ "display": "standalone",
+ "orientation": "portrait-primary",
+ "background_color": "#161b23",
+ "theme_color": "#161b23",
+ "icons": [
+ {
+ "src": "./symbol.svg",
+ "sizes": "any",
+ "type": "image/svg+xml",
+ "purpose": "any"
+ },
+ {
+ "src": "./symbol-192.png",
+ "sizes": "192x192",
+ "type": "image/png",
+ "purpose": "any maskable"
+ },
+ {
+ "src": "./symbol-512.png",
+ "sizes": "512x512",
+ "type": "image/png",
+ "purpose": "any maskable"
+ }
+ ]
+}
diff --git a/companion-app/public/symbol-192.png b/companion-app/public/symbol-192.png
new file mode 100644
index 0000000..108e79b
Binary files /dev/null and b/companion-app/public/symbol-192.png differ
diff --git a/companion-app/public/symbol-512.png b/companion-app/public/symbol-512.png
new file mode 100644
index 0000000..8e4b1c2
Binary files /dev/null and b/companion-app/public/symbol-512.png differ
diff --git a/companion-app/public/symbol.svg b/companion-app/public/symbol.svg
new file mode 100644
index 0000000..764a621
--- /dev/null
+++ b/companion-app/public/symbol.svg
@@ -0,0 +1,5 @@
+
\ No newline at end of file
diff --git a/companion-app/src/api/ablageNativ.ts b/companion-app/src/api/ablageNativ.ts
new file mode 100644
index 0000000..6de861e
--- /dev/null
+++ b/companion-app/src/api/ablageNativ.ts
@@ -0,0 +1,88 @@
+/* ================================================================
+ Sichere Ablage auf dem Gerät (iOS Keychain / Android Keystore)
+ ================================================================
+ Der Zugangstoken hat vollen Zugriff auf die Home-Assistant-Schnittstelle.
+ Im Browser bleibt nur localStorage; in der nativen Hülle gehört er in den
+ Schlüsselbund des Betriebssystems.
+
+ **Noch nicht in Betrieb**: das Plugin ist bewusst keine Abhängigkeit,
+ solange die Capacitor-Hülle nicht gebaut ist. Der Adapter greift deshalb
+ defensiv auf die Laufzeit zu, statt zu importieren — fehlt das Plugin,
+ bleibt es bei der Browser-Variante, und die App läuft trotzdem.
+
+ In Betrieb nehmen (UMSETZUNGSPLAN.md Phase 10 Stufe 2):
+ npm i @aparajita/capacitor-secure-storage
+ Danach ruft main.tsx `nativeAblageEinrichten()` auf. */
+
+import { BrowserAblage, ablageSetzen, umgebungErkennen, type Ablage } from "./umgebung.ts";
+
+/** Form des Plugins, so weit die App sie braucht. */
+interface SicherheitsPlugin {
+ get(optionen: { key: string }): Promise<{ value: string | null }>;
+ set(optionen: { key: string; value: string }): Promise;
+ remove(optionen: { key: string }): Promise;
+}
+
+function pluginHolen(): SicherheitsPlugin | null {
+ if (typeof window === "undefined") return null;
+ const global = window as unknown as {
+ Capacitor?: { Plugins?: Record };
+ };
+ const plugin = global.Capacitor?.Plugins?.["SecureStorage"];
+ if (!plugin) return null;
+ const kandidat = plugin as Partial;
+ return typeof kandidat.get === "function" &&
+ typeof kandidat.set === "function" &&
+ typeof kandidat.remove === "function"
+ ? (plugin as SicherheitsPlugin)
+ : null;
+}
+
+export class NativeAblage implements Ablage {
+ readonly #plugin: SicherheitsPlugin;
+ /* Fällt einzeln auf den Browser zurück, wenn ein Aufruf scheitert - besser
+ eine Sitzung ohne Schlüsselbund als eine App, die nicht startet. */
+ readonly #ersatz = new BrowserAblage();
+
+ constructor(plugin: SicherheitsPlugin) {
+ this.#plugin = plugin;
+ }
+
+ async lesen(schluessel: string): Promise {
+ try {
+ const { value } = await this.#plugin.get({ key: schluessel });
+ return value ?? null;
+ } catch {
+ return this.#ersatz.lesen(schluessel);
+ }
+ }
+
+ async schreiben(schluessel: string, wert: string): Promise {
+ try {
+ await this.#plugin.set({ key: schluessel, value: wert });
+ } catch {
+ await this.#ersatz.schreiben(schluessel, wert);
+ }
+ }
+
+ async loeschen(schluessel: string): Promise {
+ try {
+ await this.#plugin.remove({ key: schluessel });
+ } catch {
+ await this.#ersatz.loeschen(schluessel);
+ }
+ }
+}
+
+/**
+ * Schaltet auf die sichere Ablage um, sofern die App nativ läuft und das
+ * Plugin vorhanden ist. Gibt zurück, ob umgeschaltet wurde — praktisch für
+ * eine Anzeige in den Einstellungen.
+ */
+export function nativeAblageEinrichten(): boolean {
+ if (umgebungErkennen() !== "capacitor") return false;
+ const plugin = pluginHolen();
+ if (!plugin) return false;
+ ablageSetzen(new NativeAblage(plugin));
+ return true;
+}
diff --git a/companion-app/src/api/index.ts b/companion-app/src/api/index.ts
index 42ee522..0ef1451 100644
--- a/companion-app/src/api/index.ts
+++ b/companion-app/src/api/index.ts
@@ -11,6 +11,7 @@ export * from "./umgebung.ts";
export * from "./rest.ts";
export * from "./live.ts";
export * from "./warteschlange.ts";
+export * from "./ablageNativ.ts";
import { ENTITAETEN } from "./types.ts";
import type { Fahrt, Fahrzeugstatus, Profil, Tankvorgang } from "./types.ts";
diff --git a/companion-app/src/main.tsx b/companion-app/src/main.tsx
index 749d4e9..bcbd8dc 100644
--- a/companion-app/src/main.tsx
+++ b/companion-app/src/main.tsx
@@ -4,8 +4,13 @@ import { createRoot } from "react-dom/client"
import "@audi-dash/ui/styles.css"
import "./stile/grundlage.css"
import "./stile/audi-schrift.css"
+import { nativeAblageEinrichten } from "./api"
import { App } from "./App"
+// In der nativen Hülle den Token in den Schlüsselbund legen statt in
+// localStorage. Ohne Hülle passiert hier nichts.
+nativeAblageEinrichten()
+
const wurzel = document.getElementById("wurzel")
if (!wurzel) throw new Error("Wurzelelement #wurzel fehlt in index.html")
diff --git a/homeassistant/FMM003_MAPPING.md b/homeassistant/FMM003_MAPPING.md
new file mode 100644
index 0000000..5a70b6b
--- /dev/null
+++ b/homeassistant/FMM003_MAPPING.md
@@ -0,0 +1,136 @@
+# FMM003 → Home Assistant: Zuordnungstabelle
+
+**Gerüst, noch nicht ausgefüllt.** Gehört zu Phase 13 des
+`../UMSETZUNGSPLAN.md`; die Architektur dahinter steht in
+`../COMPANION_APP_ARCHITECTURE.md` §2b.
+
+> **Grundregel: nicht raten.** Wie die CAN/FMS-Werte im Codec JSON heißen, ist
+> unbekannt und je nach Firmware verschieden. Die Tabelle unten wird aus einer
+> **echten mitgeschnittenen Nachricht** gefüllt, nicht aus der Dokumentation
+> abgeleitet. Bis dahin bleiben die Felder leer.
+
+## Datenweg
+
+```
+FMM003 (CAN/FMS) ──MQTT über TLS──▶ Mosquitto-Add-on ──▶ MQTT-Integration ──▶ Entitäten
+```
+
+Traccar entfällt: Das Gerät liefert bereits JSON, es gibt nichts zu dekodieren.
+
+## Schritt 1 — Gerät einstellen
+
+Im Teltonika Configurator:
+
+| Ort | Einstellung |
+|---|---|
+| System → System Settings → Data Protocol | **Codec JSON** |
+| GPRS → Server Settings | Broker-IP und Port **8883** |
+| GPRS → Server Settings | Benutzername und Passwort des Brokers |
+| Security | Wurzelzertifikat, Gerätezertifikat, privater Schlüssel |
+
+**Firmware-Version hier eintragen:** `________________`
+
+Codec JSON ist nicht in jeder Firmware enthalten (bei mehreren Modellen erst ab
+03.28.00). Ohne hochgeladene Zertifikate verweigert das Gerät die
+MQTT-Verbindung — das ist kein Fehler, sondern Absicht.
+
+## Schritt 2 — Nachricht mitschneiden
+
+```bash
+mosquitto_sub -h -p 8883 --cafile ca.crt \
+ -u -P -t '#' -v | tee mitschnitt.txt
+```
+
+Dabei nacheinander: Zündung an, kurz fahren, anhalten, Zündung aus. Danach den
+Mitschnitt sichten und die Tabelle unten füllen.
+
+**Nicht ins Repository legen** — der Mitschnitt enthält echte Positionsdaten.
+
+## Schritt 3 — Zuordnungstabelle
+
+Aus dem Mitschnitt zu füllen. Die Spalte „Entität" nennt den Namen, unter dem
+das Backend den Wert erwartet.
+
+| JSON-Feld | Bedeutung | Einheit | Entität in HA | Gefunden am |
+|---|---|---|---|---|
+| | Zündung an/aus | 0/1 | `binary_sensor.audi_zuendung` | |
+| | Geschwindigkeit | km/h | `sensor.audi_geschwindigkeit` | |
+| | Position Breite | Grad | `device_tracker.audi_position` | |
+| | Position Länge | Grad | `device_tracker.audi_position` | |
+| | Kilometerstand | km | `sensor.audi_kilometerstand` | |
+| | Tankfüllstand | % | `sensor.audi_tankfuellstand` | |
+| | Drehzahl | min⁻¹ | `sensor.audi_drehzahl` | |
+| | Kühlmitteltemperatur | °C | `sensor.audi_kuehlmittel` | |
+| | Bordspannung | V | `sensor.audi_bordspannung` | |
+| | Zeitstempel | ISO/Epoch | — (Attribut) | |
+
+Teltonika nennt CAN-Werte oft nur über eine AVL-Kennung (etwa `"12"` für
+Kraftstoffstand). Deshalb hier immer **beides** notieren: die Kennung aus der
+Nachricht und ihre Bedeutung aus der AVL-Liste des Geräts.
+
+## Schritt 4 — MQTT-Integration
+
+Aus der ausgefüllten Tabelle entstehen die Entitäten. Gerüst für
+`configuration.yaml`, die Feldnamen in den Vorlagen sind erst danach einsetzbar:
+
+```yaml
+mqtt:
+ binary_sensor:
+ - name: "Audi Zündung"
+ state_topic: "teltonika//data"
+ value_template: "{{ 'ON' if value_json. | int == 1 else 'OFF' }}"
+ device_class: power
+
+ sensor:
+ - name: "Audi Geschwindigkeit"
+ state_topic: "teltonika//data"
+ value_template: "{{ value_json. }}"
+ unit_of_measurement: "km/h"
+ state_class: measurement
+
+ device_tracker:
+ - name: "Audi Position"
+ state_topic: "teltonika//data"
+ json_attributes_topic: "teltonika//data"
+ # latitude/longitude erwartet HA als Attribute
+```
+
+## Schritt 5 — Fahrterkennung umstellen
+
+Neues `pyscript/fahrterkennung_fmm003.py` nach dem Muster von
+`pyscript/fahrterkennung.py`, mit zwei Unterschieden:
+
+1. Der Auslöser ist die **Zündungs-Entität**, nicht der WLAN-Sensor.
+2. Der Startzeitpunkt darf nicht mehr nur im Arbeitsspeicher liegen — beim
+ FMM003 gibt es keinen Grund mehr für diese Einschränkung, weil der Zustand
+ jederzeit vom Gerät neu gelesen werden kann.
+
+**Unverändert übernehmen:** die Pausentoleranz (`fahrten_pausenzeit_min` aus dem
+Profil, Standard 15 Minuten) über `task.unique()` und `task.sleep()`. Das ist
+eine bewusste Produktentscheidung, dreimal durchdacht: Springt der Motor
+innerhalb der Toleranz wieder an, ist es **eine** Fahrt, nicht zwei. Die Zündung
+fällt bei Start-Stopp nicht ab, ein zusätzlicher Filter dafür ist also
+unnötig.
+
+Vorgehen: erst parallel laufen lassen und in eine Testdatei schreiben, mit der
+bisherigen Erkennung vergleichen, dann scharf schalten. Danach
+`WLAN_SENSOR` aus `pyscript/modules/einstellungen.py` entfernen.
+
+## Schritt 6 — Live-Ansicht einschalten
+
+In `companion-app/src/funktionen.ts` den Schalter `LIVE_VERFUEGBAR` auf `true`
+setzen und in `companion-app/src/screens/LiveFahrt.tsx` die Feldnamen in
+`liveZustandLesen()` an die tatsächlichen Entitäten anpassen.
+
+Damit werden auch die Fahrtdetails erstmals echte Routen zeigen, statt nur
+Start- und Zielpunkt.
+
+## Noch zu entscheiden
+
+- **Erreichbarkeit des Brokers für das Fahrzeug:** Portfreigabe 8883 direkt auf
+ Mosquitto gegen VPS-Broker mit Mosquitto-Bridge über Tailscale. Ersteres ist
+ einfacher, aber eine echte Freigabe; letzteres öffnet zuhause nichts, kostet
+ aber einen zusätzlichen Server.
+- **Datenvolumen:** Codec JSON braucht deutlich mehr Bytes als das binäre
+ Codec8E. Bei einem privat genutzten Fahrzeug vernachlässigbar, bei knappem
+ Mobilfunktarif im Blick behalten.
diff --git a/homeassistant/REVERSE_PROXY.md b/homeassistant/REVERSE_PROXY.md
new file mode 100644
index 0000000..3a32b94
--- /dev/null
+++ b/homeassistant/REVERSE_PROXY.md
@@ -0,0 +1,126 @@
+# Reverse-Proxy: Pfad-Freigabeliste für DataMetric360
+
+**Vorbereitung, noch nicht in Betrieb.** Gehört zu Phase 12 des
+`../UMSETZUNGSPLAN.md`; die Entscheidung dahinter steht in
+`../COMPANION_APP_ARCHITECTURE.md` §4.
+
+Ziel: Die App erreicht Home Assistant von unterwegs, **ohne dass Home Assistant
+selbst im Internet steht**. Dazu ein Cloudflare-Tunnel (baut nur nach außen auf,
+kein Port am Router) auf einen Reverse Proxy, der ausschließlich die unten
+aufgeführten Pfade durchlässt.
+
+## Was die App wirklich braucht
+
+Aus dem tatsächlichen Code der Datenschicht (`companion-app/src/api/`)
+abgeleitet, nicht geschätzt:
+
+| Methode | Pfad | Wofür |
+|---|---|---|
+| `GET` | `/api/` | Verbindungsprüfung bei der Ersteinrichtung |
+| `GET` | `/api/states/pyscript.audi_dashboard_*` | Profil, Fahrten, Tankvorgänge, Status, Batterieverlauf, Belegergebnis, Updatestatus |
+| `GET` | `/api/states/pyscript.reifen_*` | Reifensatz-Kilometerstände |
+| `POST` | `/api/services/pyscript/audi_dashboard_*` | alle schreibenden Vorgänge |
+| `GET` (Upgrade) | `/api/websocket` | Live-Aktualisierung |
+
+Alles andere wird geblockt, insbesondere `/auth/*`, `/lovelace*`, `/config*`,
+`/api/config`, `/api/history*`, `/developer-tools*` und die Oberfläche selbst.
+
+## Ehrliche Einschränkung
+
+**Der WebSocket lässt sich nicht pfadgenau beschneiden.** Nach `auth_ok` kann
+über `/api/websocket` grundsätzlich jeder Zustand gelesen werden, nicht nur die
+`pyscript.*`-Entitäten. Die Freigabeliste ist an dieser Stelle also grobkörniger
+als beim REST-Zugriff.
+
+Was bleibt: Ohne gültigen Token kommt gar keine Verbindung zustande, und ein
+verlorenes Gerät wird durch Zurückziehen genau seines Tokens ausgesperrt. Das
+ist die im Architekturdokument bewusst akzeptierte Abwägung — sie sollte nur
+nicht in Vergessenheit geraten.
+
+Wer sie nicht eingehen will, hat eine Alternative: den WebSocket weglassen und
+die App auf regelmäßiges Abfragen umstellen. Kostet Akku und Datenvolumen,
+verkleinert die Angriffsfläche aber auf die exakt aufgeführten REST-Pfade.
+
+## Nginx Proxy Manager
+
+Im Add-on unter *Hosts → Proxy Hosts → Edit → Advanced* eintragen. Ziel ist der
+interne Name der Home-Assistant-Instanz (im Supervisor-Netz `homeassistant:8123`).
+
+```nginx
+# Reihenfolge zählt: die erlaubenden Blöcke stehen vor dem pauschalen Verbot.
+
+location = /api/ {
+ proxy_pass http://homeassistant:8123;
+ include conf.d/include/proxy.conf;
+}
+
+location ~ ^/api/states/pyscript\.(audi_dashboard_[a-z_]+|reifen_[a-z_]+)$ {
+ limit_except GET { deny all; }
+ proxy_pass http://homeassistant:8123;
+ include conf.d/include/proxy.conf;
+}
+
+location ~ ^/api/services/pyscript/audi_dashboard_[a-z_]+$ {
+ limit_except POST { deny all; }
+ proxy_pass http://homeassistant:8123;
+ include conf.d/include/proxy.conf;
+}
+
+location = /api/websocket {
+ proxy_pass http://homeassistant:8123;
+ proxy_http_version 1.1;
+ proxy_set_header Upgrade $http_upgrade;
+ proxy_set_header Connection "upgrade";
+ proxy_read_timeout 3600s;
+ include conf.d/include/proxy.conf;
+}
+
+# Die Fotos der Fahrzeuge, die die App anzeigt. Nur Lesen, nur Bilder.
+location ~ ^/local/bilder/[a-z0-9_-]+\.(webp|png|jpg|svg)$ {
+ limit_except GET { deny all; }
+ proxy_pass http://homeassistant:8123;
+ include conf.d/include/proxy.conf;
+}
+
+# Alles Übrige: nicht durchlassen.
+location / {
+ return 404;
+}
+```
+
+Wird die App selbst unter derselben Adresse ausgeliefert, braucht sie einen
+eigenen Block auf ihr Verzeichnis — sauberer ist ein getrennter Hostname
+(`datametric360.app` für die App, `api.datametric360.app` für die
+Schnittstelle), dann bleibt diese Liste unverändert.
+
+## Nach der Einrichtung prüfen
+
+Von einem Netz ohne VPN, etwa über Mobilfunk:
+
+```bash
+API=https://api.datametric360.app
+T=
+
+# muss gehen
+curl -s -o /dev/null -w '%{http_code}\n' -H "Authorization: Bearer $T" "$API/api/"
+curl -s -o /dev/null -w '%{http_code}\n' -H "Authorization: Bearer $T" \
+ "$API/api/states/pyscript.audi_dashboard_profil"
+
+# muss 404 oder 403 liefern
+curl -s -o /dev/null -w '%{http_code}\n' "$API/auth/authorize"
+curl -s -o /dev/null -w '%{http_code}\n' -H "Authorization: Bearer $T" "$API/api/config"
+curl -s -o /dev/null -w '%{http_code}\n' -H "Authorization: Bearer $T" \
+ "$API/api/states/sensor.audi_rs_4_avant_mileage"
+
+# ohne Token: 401, nicht 200
+curl -s -o /dev/null -w '%{http_code}\n' "$API/api/states/pyscript.audi_dashboard_profil"
+```
+
+Zusätzlich: Port 8123 darf von außen **gar nicht** antworten.
+
+## Offen bis zur Einrichtung
+
+- Nameserver von `datametric360.app` auf Cloudflare umstellen („Full setup")
+- Entscheidung Nginx Proxy Manager gegen Traefik (die Liste oben ist für NPM
+ geschrieben und für Traefik sinngemäß zu übertragen)
+- Endgültige Hostnamen festlegen und hier eintragen
diff --git a/homeassistant/www/dm360-qr.html b/homeassistant/www/dm360-qr.html
new file mode 100644
index 0000000..d1189b6
--- /dev/null
+++ b/homeassistant/www/dm360-qr.html
@@ -0,0 +1,115 @@
+
+
+
+
+
+ DataMetric360 — Gerät verbinden
+
+
+
+
+
+
+
Gerät verbinden
+
+ Trag die von außen erreichbare Adresse und einen langlebigen Zugriffstoken ein.
+ Der Code entsteht hier auf dem Gerät; nichts davon wird irgendwohin gesendet.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/homeassistant/www/dm360-qrcode-lib.js b/homeassistant/www/dm360-qrcode-lib.js
new file mode 100644
index 0000000..df13f82
--- /dev/null
+++ b/homeassistant/www/dm360-qrcode-lib.js
@@ -0,0 +1,2297 @@
+//---------------------------------------------------------------------
+//
+// QR Code Generator for JavaScript
+//
+// Copyright (c) 2009 Kazuhiko Arase
+//
+// URL: http://www.d-project.com/
+//
+// Licensed under the MIT license:
+// http://www.opensource.org/licenses/mit-license.php
+//
+// The word 'QR Code' is registered trademark of
+// DENSO WAVE INCORPORATED
+// http://www.denso-wave.com/qrcode/faqpatent-e.html
+//
+//---------------------------------------------------------------------
+
+var qrcode = function() {
+
+ //---------------------------------------------------------------------
+ // qrcode
+ //---------------------------------------------------------------------
+
+ /**
+ * qrcode
+ * @param typeNumber 1 to 40
+ * @param errorCorrectionLevel 'L','M','Q','H'
+ */
+ var qrcode = function(typeNumber, errorCorrectionLevel) {
+
+ var PAD0 = 0xEC;
+ var PAD1 = 0x11;
+
+ var _typeNumber = typeNumber;
+ var _errorCorrectionLevel = QRErrorCorrectionLevel[errorCorrectionLevel];
+ var _modules = null;
+ var _moduleCount = 0;
+ var _dataCache = null;
+ var _dataList = [];
+
+ var _this = {};
+
+ var makeImpl = function(test, maskPattern) {
+
+ _moduleCount = _typeNumber * 4 + 17;
+ _modules = function(moduleCount) {
+ var modules = new Array(moduleCount);
+ for (var row = 0; row < moduleCount; row += 1) {
+ modules[row] = new Array(moduleCount);
+ for (var col = 0; col < moduleCount; col += 1) {
+ modules[row][col] = null;
+ }
+ }
+ return modules;
+ }(_moduleCount);
+
+ setupPositionProbePattern(0, 0);
+ setupPositionProbePattern(_moduleCount - 7, 0);
+ setupPositionProbePattern(0, _moduleCount - 7);
+ setupPositionAdjustPattern();
+ setupTimingPattern();
+ setupTypeInfo(test, maskPattern);
+
+ if (_typeNumber >= 7) {
+ setupTypeNumber(test);
+ }
+
+ if (_dataCache == null) {
+ _dataCache = createData(_typeNumber, _errorCorrectionLevel, _dataList);
+ }
+
+ mapData(_dataCache, maskPattern);
+ };
+
+ var setupPositionProbePattern = function(row, col) {
+
+ for (var r = -1; r <= 7; r += 1) {
+
+ if (row + r <= -1 || _moduleCount <= row + r) continue;
+
+ for (var c = -1; c <= 7; c += 1) {
+
+ if (col + c <= -1 || _moduleCount <= col + c) continue;
+
+ if ( (0 <= r && r <= 6 && (c == 0 || c == 6) )
+ || (0 <= c && c <= 6 && (r == 0 || r == 6) )
+ || (2 <= r && r <= 4 && 2 <= c && c <= 4) ) {
+ _modules[row + r][col + c] = true;
+ } else {
+ _modules[row + r][col + c] = false;
+ }
+ }
+ }
+ };
+
+ var getBestMaskPattern = function() {
+
+ var minLostPoint = 0;
+ var pattern = 0;
+
+ for (var i = 0; i < 8; i += 1) {
+
+ makeImpl(true, i);
+
+ var lostPoint = QRUtil.getLostPoint(_this);
+
+ if (i == 0 || minLostPoint > lostPoint) {
+ minLostPoint = lostPoint;
+ pattern = i;
+ }
+ }
+
+ return pattern;
+ };
+
+ var setupTimingPattern = function() {
+
+ for (var r = 8; r < _moduleCount - 8; r += 1) {
+ if (_modules[r][6] != null) {
+ continue;
+ }
+ _modules[r][6] = (r % 2 == 0);
+ }
+
+ for (var c = 8; c < _moduleCount - 8; c += 1) {
+ if (_modules[6][c] != null) {
+ continue;
+ }
+ _modules[6][c] = (c % 2 == 0);
+ }
+ };
+
+ var setupPositionAdjustPattern = function() {
+
+ var pos = QRUtil.getPatternPosition(_typeNumber);
+
+ for (var i = 0; i < pos.length; i += 1) {
+
+ for (var j = 0; j < pos.length; j += 1) {
+
+ var row = pos[i];
+ var col = pos[j];
+
+ if (_modules[row][col] != null) {
+ continue;
+ }
+
+ for (var r = -2; r <= 2; r += 1) {
+
+ for (var c = -2; c <= 2; c += 1) {
+
+ if (r == -2 || r == 2 || c == -2 || c == 2
+ || (r == 0 && c == 0) ) {
+ _modules[row + r][col + c] = true;
+ } else {
+ _modules[row + r][col + c] = false;
+ }
+ }
+ }
+ }
+ }
+ };
+
+ var setupTypeNumber = function(test) {
+
+ var bits = QRUtil.getBCHTypeNumber(_typeNumber);
+
+ for (var i = 0; i < 18; i += 1) {
+ var mod = (!test && ( (bits >> i) & 1) == 1);
+ _modules[Math.floor(i / 3)][i % 3 + _moduleCount - 8 - 3] = mod;
+ }
+
+ for (var i = 0; i < 18; i += 1) {
+ var mod = (!test && ( (bits >> i) & 1) == 1);
+ _modules[i % 3 + _moduleCount - 8 - 3][Math.floor(i / 3)] = mod;
+ }
+ };
+
+ var setupTypeInfo = function(test, maskPattern) {
+
+ var data = (_errorCorrectionLevel << 3) | maskPattern;
+ var bits = QRUtil.getBCHTypeInfo(data);
+
+ // vertical
+ for (var i = 0; i < 15; i += 1) {
+
+ var mod = (!test && ( (bits >> i) & 1) == 1);
+
+ if (i < 6) {
+ _modules[i][8] = mod;
+ } else if (i < 8) {
+ _modules[i + 1][8] = mod;
+ } else {
+ _modules[_moduleCount - 15 + i][8] = mod;
+ }
+ }
+
+ // horizontal
+ for (var i = 0; i < 15; i += 1) {
+
+ var mod = (!test && ( (bits >> i) & 1) == 1);
+
+ if (i < 8) {
+ _modules[8][_moduleCount - i - 1] = mod;
+ } else if (i < 9) {
+ _modules[8][15 - i - 1 + 1] = mod;
+ } else {
+ _modules[8][15 - i - 1] = mod;
+ }
+ }
+
+ // fixed module
+ _modules[_moduleCount - 8][8] = (!test);
+ };
+
+ var mapData = function(data, maskPattern) {
+
+ var inc = -1;
+ var row = _moduleCount - 1;
+ var bitIndex = 7;
+ var byteIndex = 0;
+ var maskFunc = QRUtil.getMaskFunction(maskPattern);
+
+ for (var col = _moduleCount - 1; col > 0; col -= 2) {
+
+ if (col == 6) col -= 1;
+
+ while (true) {
+
+ for (var c = 0; c < 2; c += 1) {
+
+ if (_modules[row][col - c] == null) {
+
+ var dark = false;
+
+ if (byteIndex < data.length) {
+ dark = ( ( (data[byteIndex] >>> bitIndex) & 1) == 1);
+ }
+
+ var mask = maskFunc(row, col - c);
+
+ if (mask) {
+ dark = !dark;
+ }
+
+ _modules[row][col - c] = dark;
+ bitIndex -= 1;
+
+ if (bitIndex == -1) {
+ byteIndex += 1;
+ bitIndex = 7;
+ }
+ }
+ }
+
+ row += inc;
+
+ if (row < 0 || _moduleCount <= row) {
+ row -= inc;
+ inc = -inc;
+ break;
+ }
+ }
+ }
+ };
+
+ var createBytes = function(buffer, rsBlocks) {
+
+ var offset = 0;
+
+ var maxDcCount = 0;
+ var maxEcCount = 0;
+
+ var dcdata = new Array(rsBlocks.length);
+ var ecdata = new Array(rsBlocks.length);
+
+ for (var r = 0; r < rsBlocks.length; r += 1) {
+
+ var dcCount = rsBlocks[r].dataCount;
+ var ecCount = rsBlocks[r].totalCount - dcCount;
+
+ maxDcCount = Math.max(maxDcCount, dcCount);
+ maxEcCount = Math.max(maxEcCount, ecCount);
+
+ dcdata[r] = new Array(dcCount);
+
+ for (var i = 0; i < dcdata[r].length; i += 1) {
+ dcdata[r][i] = 0xff & buffer.getBuffer()[i + offset];
+ }
+ offset += dcCount;
+
+ var rsPoly = QRUtil.getErrorCorrectPolynomial(ecCount);
+ var rawPoly = qrPolynomial(dcdata[r], rsPoly.getLength() - 1);
+
+ var modPoly = rawPoly.mod(rsPoly);
+ ecdata[r] = new Array(rsPoly.getLength() - 1);
+ for (var i = 0; i < ecdata[r].length; i += 1) {
+ var modIndex = i + modPoly.getLength() - ecdata[r].length;
+ ecdata[r][i] = (modIndex >= 0)? modPoly.getAt(modIndex) : 0;
+ }
+ }
+
+ var totalCodeCount = 0;
+ for (var i = 0; i < rsBlocks.length; i += 1) {
+ totalCodeCount += rsBlocks[i].totalCount;
+ }
+
+ var data = new Array(totalCodeCount);
+ var index = 0;
+
+ for (var i = 0; i < maxDcCount; i += 1) {
+ for (var r = 0; r < rsBlocks.length; r += 1) {
+ if (i < dcdata[r].length) {
+ data[index] = dcdata[r][i];
+ index += 1;
+ }
+ }
+ }
+
+ for (var i = 0; i < maxEcCount; i += 1) {
+ for (var r = 0; r < rsBlocks.length; r += 1) {
+ if (i < ecdata[r].length) {
+ data[index] = ecdata[r][i];
+ index += 1;
+ }
+ }
+ }
+
+ return data;
+ };
+
+ var createData = function(typeNumber, errorCorrectionLevel, dataList) {
+
+ var rsBlocks = QRRSBlock.getRSBlocks(typeNumber, errorCorrectionLevel);
+
+ var buffer = qrBitBuffer();
+
+ for (var i = 0; i < dataList.length; i += 1) {
+ var data = dataList[i];
+ buffer.put(data.getMode(), 4);
+ buffer.put(data.getLength(), QRUtil.getLengthInBits(data.getMode(), typeNumber) );
+ data.write(buffer);
+ }
+
+ // calc num max data.
+ var totalDataCount = 0;
+ for (var i = 0; i < rsBlocks.length; i += 1) {
+ totalDataCount += rsBlocks[i].dataCount;
+ }
+
+ if (buffer.getLengthInBits() > totalDataCount * 8) {
+ throw 'code length overflow. ('
+ + buffer.getLengthInBits()
+ + '>'
+ + totalDataCount * 8
+ + ')';
+ }
+
+ // end code
+ if (buffer.getLengthInBits() + 4 <= totalDataCount * 8) {
+ buffer.put(0, 4);
+ }
+
+ // padding
+ while (buffer.getLengthInBits() % 8 != 0) {
+ buffer.putBit(false);
+ }
+
+ // padding
+ while (true) {
+
+ if (buffer.getLengthInBits() >= totalDataCount * 8) {
+ break;
+ }
+ buffer.put(PAD0, 8);
+
+ if (buffer.getLengthInBits() >= totalDataCount * 8) {
+ break;
+ }
+ buffer.put(PAD1, 8);
+ }
+
+ return createBytes(buffer, rsBlocks);
+ };
+
+ _this.addData = function(data, mode) {
+
+ mode = mode || 'Byte';
+
+ var newData = null;
+
+ switch(mode) {
+ case 'Numeric' :
+ newData = qrNumber(data);
+ break;
+ case 'Alphanumeric' :
+ newData = qrAlphaNum(data);
+ break;
+ case 'Byte' :
+ newData = qr8BitByte(data);
+ break;
+ case 'Kanji' :
+ newData = qrKanji(data);
+ break;
+ default :
+ throw 'mode:' + mode;
+ }
+
+ _dataList.push(newData);
+ _dataCache = null;
+ };
+
+ _this.isDark = function(row, col) {
+ if (row < 0 || _moduleCount <= row || col < 0 || _moduleCount <= col) {
+ throw row + ',' + col;
+ }
+ return _modules[row][col];
+ };
+
+ _this.getModuleCount = function() {
+ return _moduleCount;
+ };
+
+ _this.make = function() {
+ if (_typeNumber < 1) {
+ var typeNumber = 1;
+
+ for (; typeNumber < 40; typeNumber++) {
+ var rsBlocks = QRRSBlock.getRSBlocks(typeNumber, _errorCorrectionLevel);
+ var buffer = qrBitBuffer();
+
+ for (var i = 0; i < _dataList.length; i++) {
+ var data = _dataList[i];
+ buffer.put(data.getMode(), 4);
+ buffer.put(data.getLength(), QRUtil.getLengthInBits(data.getMode(), typeNumber) );
+ data.write(buffer);
+ }
+
+ var totalDataCount = 0;
+ for (var i = 0; i < rsBlocks.length; i++) {
+ totalDataCount += rsBlocks[i].dataCount;
+ }
+
+ if (buffer.getLengthInBits() <= totalDataCount * 8) {
+ break;
+ }
+ }
+
+ _typeNumber = typeNumber;
+ }
+
+ makeImpl(false, getBestMaskPattern() );
+ };
+
+ _this.createTableTag = function(cellSize, margin) {
+
+ cellSize = cellSize || 2;
+ margin = (typeof margin == 'undefined')? cellSize * 4 : margin;
+
+ var qrHtml = '';
+
+ qrHtml += '
';
+ qrHtml += '';
+
+ for (var r = 0; r < _this.getModuleCount(); r += 1) {
+
+ qrHtml += '
';
+
+ for (var c = 0; c < _this.getModuleCount(); c += 1) {
+ qrHtml += '