Compare commits

...
34 Commits
Author SHA1 Message Date
retr0andClaude Sonnet 5 e6422892f3 ShineBridge v1.9.4 — Finanzen-Chart: Obergrenze für Desktop-Breite
width:100% ohne max-width lässt die SVG auf breiten Desktop-Bildschirmen
ungebremst mit dem Container mitwachsen (Text/Balken/Höhe proportional riesig).
max-width:640px begrenzt das, bleibt auf Handy weiterhin voll fluid.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-25 14:07:04 +02:00
retr0andClaude Sonnet 5 608f6bfd42 ShineBridge v1.9.3 — Finanzen-Chart: auf letzte 14 Perioden begrenzt, echt responsiv
- Zeigt nur noch die letzten 14 Balken (statt aller Perioden im Abrechnungsjahr)
  — genug für einen Trend, ohne dass Labels sich gegenseitig überlappen
- SVG jetzt echt responsiv (viewBox + width:100%, Mobile-first dimensioniert)
  statt fester Pixelbreite mit Scrollbalken — passt sich Handy wie Desktop an
- X-Achsen-Labels um -40° gedreht statt horizontal — brauchen dadurch deutlich
  weniger Platz pro Balken, alle 14 bleiben lesbar

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-25 14:02:33 +02:00
retr0andClaude Sonnet 5 722301b1d6 ShineBridge v1.9.2 — Finanzen-Chart modernisiert: Wochen-/2-Wochen-Aggregation, abgerundete Balken, Verlauf
Statt bei langen Abrechnungsjahren jeden einzelnen Tag zu zeigen (bei 112 Tagen
unlesbar), aggregiert der Chart jetzt automatisch:
- ≤21 Tage: täglich (wie bisher)
- 22–180 Tage: wöchentlich (~16 Balken bei einem Jahr)
- >180 Tage: 2-wöchentlich

Zusätzlich: abgerundete Balken mit Farbverlauf statt flacher Füllung, echter
Abstand zwischen Festpreis-/Spot-Balkenpaar statt direkt aneinandergrenzender
Hälften, gestrichelte Gridlines, HTML-Legende mit runden Farbchips statt
SVG-Text-Legende.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-25 13:56:32 +02:00
retr0andClaude Sonnet 5 e9168ee72b ShineBridge v1.9.1 — Fix: Finanzen-Chart bei langen Abrechnungsjahren unlesbar
'Kosten je Tag' quetschte alle Tage in eine feste Breite von 600px — bei 112
Tagen wurden die Balken zu ~1px-Haarlinien. Breite skaliert jetzt mit der
Tagesanzahl (min. 10px Slot/Tag), Chart scrollt bei vielen Tagen horizontal
statt gestaucht zu werden. X-Achsen-Labels richten sich an der tatsächlichen
Balkenbreite aus statt an einer festen Label-Anzahl.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-25 13:52:25 +02:00
retr0andClaude Sonnet 5 e342fff539 README: auf reines Energiemanagement-Repo aktualisiert, Verweis auf growatt-diagnose-tool
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-25 13:20:57 +02:00
retr0andClaude Sonnet 5 ee404065ce ShineLAN-X, ShineWifi-X und tools/shinediag nach growatt-diagnose-tool verschoben
Konsolidierung aller Hardware-Diagnose/Firmware-Projekte in einem eigenen,
privaten Repo (gitea.bitfire.work/retr0/growatt-diagnose-tool). Dieses Repo
bleibt reines Energiemanagement (ShineBridge).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-25 13:17:40 +02:00
retr0andClaude Sonnet 5 eb797ed8b7 ShineBridge v1.9.0 — Fokus auf Energiemanagement: Flash/OTA + Setup-Wizard ausgelagert
Repositionierung: ShineBridge macht ab jetzt eine Sache gut (Energiemanagement),
statt alles zu versuchen. Flash/OTA-Firmware-Update fürs ShineLAN-X und der
Ersteinrichtungs-Wizard sind kein Energiemanagement-Feature und wandern ins neue,
eigenständige Projekt 'growatt-diagnose-tool'.

- 4 Flash/OTA-Endpunkte aus main.py entfernt (probe/firmware/update/reboot)
- Flash-Tab, Flash-Panel und zugehöriges CSS/JS aus index.html entfernt
- Setup-Wizard-Overlay entfernt — Geräte werden über '+ Gerät hinzufügen' im
  Geräte-Tab angelegt (Button existierte bereits unabhängig vom Wizard)
- firmware/-Ordner (ShineLAN-X .bin-Dateien) entfernt, liegt jetzt im neuen Repo
- Dockerfile: COPY firmware/ entfernt
- Bugfix dabei gefunden: switchTab() referenzierte noch den entfernten
  'flash'-Tab per Index — hätte die Tab-Reihenfolge durcheinandergebracht

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-25 13:14:12 +02:00
retr0andClaude Sonnet 5 f5ed7c5df6 ShineBridge v1.8.31 — EMS-Fixes: konfigurierbarer Max-Ladestrom, Zwangsladen-Retry, Fault-State-Handling
- ems_max_current_ma konfigurierbar statt hardcoded 32A (Zwangsladen darf jetzt nicht mehr
  mehr fordern als die tatsächliche Wallbox liefern kann, z.B. 16A bei einer 11kW-Wallbox)
- Zwangsladen setzt den Ladestrom bei jedem Poll erneut (statt nur einmal beim Start) —
  ein einzelner fehlgeschlagener Modbus-Write hängt nicht mehr die ganze Nacht
- Unbekannte/Fehler-Ladezustände (z.B. Fahrzeug-Störung) werden nicht mehr wie 'abgesteckt'
  behandelt — Strom wird pausiert, aber der Zwangslade-Timer bleibt erhalten
- Neues Settings-Feld 'Max. Ladestrom (A)' im EMS-Dialog

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-25 12:46:59 +02:00
retr0andClaude Sonnet 4.6 b36b0194f7 ShineBridge v1.8.30 — Ingress-Fix: Flask wieder auf 0.0.0.0
127.0.0.1 brach HAOS Ingress (Proxy kommt von außerhalb localhost).
Sicherheit bleibt gewahrt: kein ports:-Eintrag in config.yaml,
Port 8099 wird nicht ans Host-Netz gemappt.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-28 16:04:15 +02:00
retr0andClaude Sonnet 4.6 04a8fb3125 ShineBridge v1.8.29 — Port 8099 nur auf localhost binden
Flask bindet auf 127.0.0.1 statt 0.0.0.0. Mit host_network: true war Port
8099 direkt vom LAN erreichbar. HAOS-Ingress verbindet sich über localhost,
daher kein Funktionsverlust.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-26 15:23:01 +02:00
retr0andClaude Sonnet 4.6 d9f94d3f28 ShineBridge v1.8.28 — history.py: load_recent Fix + periodisches Cleanup
- load_recent(): Window-Funktion durch pro-Sensor-Indexabfragen ersetzt
  (SELECT ... ORDER BY ts DESC LIMIT N per sensor_id) — nutzt Index optimal,
  kein Full-Table-Scan mehr auf 1M+ Zeilen beim Start
- Periodisches Cleanup: täglich via Daemon-Thread statt nur beim Start —
  DB bleibt dauerhaft auf RETENTION_DAYS beschränkt
- RETENTION_DAYS: 7 → 14 (explizites Maximum per Konfiguration)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-26 13:31:46 +02:00
retr0andClaude Sonnet 4.6 c4047fc804 fix(v1.8.27): Fahrzeugerkennung + Wizard überschreibt Inverterliste
Bug 1 (inverters.py): Kathrein-Register 0x0061-0x0064 existieren nicht —
(0x0060, 10) schlug daher immer mit IllegalAddress fehl, charging_state
wurde nie gelesen → EMS meldete dauerhaft "kein Fahrzeug". Fix: aufgeteilt
in (0x0060, 1) + (0x0065, 6), sodass die Lücke übersprungen wird.

Bug 2 (index.html): wizardStep2Next() postete [neues_gerät] statt
[...invertersList, neues_gerät] → überschrieb die gesamte Inverterliste.
Wenn der Wizard bei einem bestehenden Setup erschien, flogen alle anderen
Geräte raus. Fix: bestehende Geräte werden beibehalten.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-08 10:58:44 +02:00
retr0andClaude Sonnet 4.6 5564a50c3c fix(ems): Zwangsladen-Countdown läuft ohne angestecktes Auto
Kathrein Reg 0x0060 liefert IllegalAddress wenn kein Fahrzeug angeschlossen.
Default war 1 (STATE_CONNECTED) → EMS nahm Auto als verbunden an → Countdown.

IllegalAddress ist kein sporadischer Lesefehler, sondern das definierte Signal
der Wallbox für "kein Fahrzeug". Default auf 0 (STATE_IDLE) → EMS kehrt sofort
zu "kein Fahrzeug" zurück, _no_pv_since-Timer wird nicht gestartet.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-07 10:12:35 +02:00
retr0andClaude Sonnet 4.6 dc2df891fb fix: TCP-Timeout auf 40% des Poll-Intervalls begrenzt (max. 5s)
Logs zeigen: Bei 10s Poll-Rate + 10s TCP-Timeout läuft ein fehlgeschlagener
Connect exakt so lange wie das Interval. stop.wait() wird 0 → nächster Poll
startet sofort → zweite parallele TCP-Verbindung → Wallbox überfordert → Spirale.

Fix: timeout = min(5.0, interval * 0.4). Bei 15s → 5.0s Timeout; bei 10s → 4.0s.
Ein Fehler belegt max. 40% des Intervalls, der Rest ist Wartezeit vor dem nächsten Versuch.
Gilt für WallboxReader (Kathrein) und ModbusReader (ShineLAN-X / SDM-630).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-07 10:08:59 +02:00
retr0andClaude Sonnet 4.6 a361c30f1b fix: Offline-Flapping — erst nach 3 aufeinanderfolgenden Lesefehlern offline
Ein einzelner UDP-Paketverlust (Goodwe) oder TCP-Timeout (Modbus) hat sofort
MQTT-Status "offline" getriggert. Bei 10s Poll-Rate reicht ein Ausreißer.

Fix: _fail_count pro Poll-Loop, OFFLINE_THRESHOLD=3. Erst wenn 3 Reads in Folge
scheitern (≥30s bei 10s Interval) wird offline publiziert. Erfolg resettet
den Zähler auf 0 und stellt online sofort wieder her.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-07 09:58:48 +02:00
retr0andClaude Sonnet 4.6 a9f33c8e9e fix(goodwe): Netzbezug-Periode zeigt 0 kWh — integrierter grid_power-Zähler
e_total_imp vom Goodwe-WR ist ein Lifetime-Zähler seit Inbetriebnahme.
Beim ersten Verbinden speichert save_period_start_if_new() den aktuellen
Wert als Periodenstart → Delta = 0.

Fix: _int_import/_int_export werden je Poll-Zyklus aus grid_power integriert
(W × dt / 3.600.000 → kWh), in der measurements-DB persistiert und
beim Neustart aus der DB wiederhergestellt. AGG_SENSOR_IDS bevorzugt nun
_int_import vor e_total_imp, SDM-630 (import_kwh) bleibt erste Wahl.
Private Keys (Prefix _) werden nicht an MQTT gepublished.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-07 09:55:24 +02:00
retr0andClaude Sonnet 4.6 512b743b16 Fix: Stromtarif-Einstellungen nach Neustart verloren + Finanzen-Layout (v1.8.22)
State.mqtt_cfg wurde beim Start nur mit 4 MQTT-Keys initialisiert — alle
Tarif/Billing-Keys fehlten, wurden nach Neustart auf Defaults zurückgesetzt.
Fix: alle persistenten Keys aus load_config() in State.mqtt_cfg übernehmen.

Finanzen-Tab: mehr Abstände, größere Karten (22px Wert), Abschnittsüberschriften,
Trennlinie vor dem Chart.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-06 08:26:52 +02:00
retr0andClaude Sonnet 4.6 cb5f23d486 Fix: Finanzen-Tab bleibt bei 'Lade...' hängen (v1.8.21)
fEur/fKwh waren lokale Funktionen in renderEnergy() — loadFinance()
konnte sie nicht aufrufen (ReferenceError außerhalb des Scopes).
Beide Funktionen in den globalen Scope verschoben, lokale Kopien entfernt.
loadFinance() Rendering-Block in try/catch gewrappt.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-06 08:18:17 +02:00
retr0andClaude Sonnet 4.6 7904d498b5 v1.8.20: Fix Eigenversorgungskarte + Stromtarif-Einstellungen verloren
Bug 1 — Eigenversorgungskarte verschwindet wenn PV offline:
- savings_kwh wird jetzt gesetzt wenn pv_total vorhanden (grid_exp muss
  nicht mehr explizit vorhanden sein, default 0.0)
- Karte bleibt sichtbar auch wenn Wechselrichter offline geht

Bug 2 — Stromtarif-Einstellungen gehen beim Speichern verloren:
- savePrices(): parseInt mit || 1 Fallback, verhindert NaN → JSON-null
- api_save_config(): None-Checks + try/except für alle numerischen Keys,
  save_config() wird garantiert immer aufgerufen

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-06 07:25:01 +02:00
retr0andClaude Sonnet 4.6 5a00c7b5d3 docs: Roadmap auf v1.8.19 aktualisiert
Alle erledigten Features als [x] markiert, neue Einträge für
Flash-Wizard, NuttX OTA, Setup-Wizard, MQTT rc=5, Port-Sicherheit,
Mobile-Layout. Changelog bis v1.8.19 ergänzt.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-05 21:53:58 +02:00
retr0andClaude Sonnet 4.6 b64bcde203 v1.8.19: Version erhöht
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-05 21:45:27 +02:00
retr0andClaude Sonnet 4.6 b3f67df967 fix: Mobile-Layout — Tabs scrollen, kein horizontaler Overflow
- html/body: overflow-x: hidden verhindert seitlichen Scroll
- Tabs: overflow-x: auto + white-space: nowrap + flex-shrink: 0
  → horizontal scrollbar-frei auf Handy
- Header: min-width: 0, flex-shrink: 0 für Pill, Text-Overflow
- main: padding 16px statt 20px
- energy-wrap: width: 100%, kwh-card min-width: 0
- @media ≤520px: sensor-grid, settings, header-icon ausgeblendet

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-05 21:39:59 +02:00
retr0andClaude Sonnet 4.6 e72ad2db19 docs: README auf v1.8.18 aktualisiert
Flash-Wizard, OTA-Abschnitt (Zwei-Phasen), Setup-Wizard, Finanzen-Tab,
Abschlags-Tracker, Port-Sicherheit, Überschuss-Geräte, repo-Struktur.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-05 21:32:29 +02:00
retr0andClaude Sonnet 4.6 3b3d4055f6 v1.8.18: Version in config.yaml angehoben
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-05 20:05:07 +02:00
retr0andClaude Sonnet 4.6 15c0ede72e v1.8.18: MQTT rc=5 Fehlerhandling, Port-Absicherung, Flash-Wizard + NuttX OTA
MQTT:
- rc=5 (Not Authorized) stoppt Reconnect-Loop via _auth_failed Flag
- Fehlermeldung im MQTT-Einstellungen-Banner sichtbar

Sicherheit:
- /api/* nur über HAOS-Ingress (X-Ingress-Path) oder Loopback erreichbar

Flash-Wizard (Baustelle B):
- Neuer Tab "Flash" mit IP-Eingabe und OTA-Modus-Erkennung
- OTA: integrierte oder eigene Firmware via POST /api/flash/update auf Stick
- Fortschrittsbalken + Polling bis Stick nach Reset wieder online
- ST-Link-Erstflash-Anleitung (Pinout, st-flash Kommando)
- Firmware-Binaries im Docker-Image unter /firmware/

NuttX OTA (Baustelle A, shinelanx-modbus):
- ota_http.c: Zwei-Phasen OTA für STM32F103 Single-Bank Flash
  Stage 1: Firmware in Staging-Bereich (obere Flashhälfte) schreiben
  Stage 2: .ramfuncs aus SRAM heraus — Staging → App-Bereich kopieren, Reset
- ota_http.h, Makefile und main.c entsprechend erweitert
- ld.script.dfu: .ramfuncs in .data Section → Ausführung aus SRAM

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-05 19:45:40 +02:00
retr0andClaude Sonnet 4.6 bbfb11fb9c v1.8.17: Atomarer Config-Write + Backup-Fallback
Verhindert Datenverlust wenn HAOS den Container während eines Saves
stoppt. Schreibt erst config.json.tmp, dann atomares os.replace().
Hält config.json.bak als Fallback für den nächsten Start.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-05 13:40:56 +02:00
retr0andClaude Sonnet 4.6 2456f356b4 v1.8.16: Finanzen-Tab — Festpreis vs. Spot-Vergleich
Täglicher Tarif-Tracking: kWh + EPEX-Ø-Preis werden ab jetzt täglich
gespeichert. Neuer Finanzen-Tab zeigt Balkendiagramm (Festpreis vs.
Spot hypothetisch), Summen-Karten und Empfehlung ob flexibler Tarif
sich lohnen würde.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-05 13:29:55 +02:00
retr0andClaude Sonnet 4.6 dfb42e6902 v1.8.15: Abschlags-Tracker zeigt ganze Zahlungen statt Bruchmonate
Berechnung auf geleistete Abschlagszahlungen umgestellt:
Anzahl ganzer Monate seit Abrechnungsstart statt Tage/30.4.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-05 13:12:43 +02:00
retr0andClaude Sonnet 4.6 fec49ec4fb v1.8.14: Abschlags-Tracker (monatliche Rate + Grundpreis)
Neues optionales Feature: Abschlags-Übersicht im Energie-Dashboard.
Zeigt Bereits bezahlt, Grundpreis anteilig, Energiekosten sowie
voraussichtliche Nachzahlung oder Guthaben für das laufende Abrechnungsjahr.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-05 11:45:56 +02:00
retr0andClaude Sonnet 4.6 5ab8ee75fb v1.8.13: Labels + Invertiert-Modus für Überschuss-Geräte
- Beschriftung über jedem Eingabefeld (Name, Z2M Name, Schwellwert, etc.)
- Checkbox "Invertiert": EIN bei Netzbezug, AUS bei Überschuss
- Live-Tab: ↑/↓ Pfeil zeigt normale/invertierte Logik

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-04 15:12:15 +02:00
retr0andClaude Sonnet 4.6 33ada90df4 v1.8.12: Mindest-Laufzeit pro Überschuss-Gerät
- min_on_minutes pro Gerät: Gerät bleibt mindestens X Minuten an
  auch wenn Überschuss unter Ausschaltschwelle fällt (Boiler-Fix)
- Live-Tab zeigt verbleibende Mindestlaufzeit während Gerät läuft
- get_states() liefert on_minutes für Laufzeit-Tracking

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-04 12:02:37 +02:00
retr0andClaude Sonnet 4.6 0dbf0266a8 fix: _get_pv_surplus() via grid_power-Aggregat (SDM-630 fix, v1.8.11)
SDM-630 liefert grid_power (negativ=Einspeisung), wurde von active_power-
Logik nicht erfasst. Jetzt einheitlich über grid_power-Aggregat.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-04 11:42:08 +02:00
retr0 5942c18df6 fix: config.yaml version auf 1.8.10 2026-05-04 11:35:25 +02:00
retr0 58a33f966d debug: surplus_w in API + Live-Tab anzeigen 2026-05-04 11:31:06 +02:00
68 changed files with 847 additions and 13294 deletions
+11 -6
View File
@@ -1,8 +1,13 @@
*.log
*.txt
*.json
!haos-addon/**/*.json
shinelanx-modbus/
__pycache__/
*.pyc
!haos-addon/**/*.json
.env
*.json
*.log
*.pyc
*.txt
# versehentlich getrackte Alt-Dateien, gehoeren nicht ins Repo
growatt-diagnose-tool/
Modbus-Server-Kathrein.pdf
ShineLAN-X/releases/*.bin
shinelanx-modbus/
+51 -148
View File
@@ -1,56 +1,52 @@
# ShineBridge
Lokale Solar-Wechselrichter-Integration für Home Assistant — ohne Cloud, ohne Hersteller-App. Modbus TCP via ShineLAN-X, MQTT Discovery, persistente History, PV-Überschussladen.
Lokales Energiemanagement für Growatt/Goodwe-Wechselrichter in Home Assistant — ohne Cloud, ohne Hersteller-App. Modbus TCP / UDP, MQTT Discovery, persistente History, PV-Überschussladen, Energie-Dashboard.
> **Repository:** `https://gitea.bitfire.work/retr0/shinebridge`
>
> Firmware, Hardware-Diagnose und Flash-Tools für ShineLAN-X/ShineWifi-X liegen seit v1.9.0 in einem eigenen, privaten Repo (`growatt-diagnose-tool`) — dieses Repo ist reines Energiemanagement.
---
## Übersicht
| Komponente | Beschreibung | Status |
|---|---|---|
| **ShineBridge Add-on** | HAOS Add-on: Modbus TCP / UDP → MQTT Discovery | ✅ v1.5.3 |
| **ShineLAN-X Firmware** | NuttX auf STM32F103: Modbus RTU ↔ TCP | ✅ Produktiv |
| **ShineWifi-X ESPHome** | ESPHome-Configs für ESP8266-Stick | ✅ Getestet |
| **ShineDiag** | Portables Vor-Ort-Diagnose-Tool (Pi 3B) | ✅ Bereit |
---
## ShineBridge Add-on
### Architektur
## Architektur
```
Wechselrichter / Energiezähler
│ Modbus RTU (RS485 / USB)
[ShineLAN-X] ← STM32F103, NuttX, Modbus RTU ↔ TCP
[ShineLAN-X] ← STM32F103, NuttX, Modbus RTU ↔ TCP (Firmware: growatt-diagnose-tool)
│ Modbus TCP Port 502 (LAN)
│
│ UDP/8899 (Goodwe)
│ Modbus TCP Port 502 (Kathrein Wallbox)
│
[ShineBridge Add-on]
[ShineBridge Add-on] Port 8099 (HAOS Ingress)
├── Modbus TCP / Goodwe UDP / Kathrein Wallbox lesen
├── EMS-Controller: PV-Überschussladen + Zwangsladen
├── MQTT Discovery → Home Assistant Sensoren
├── Aggregat-Gerät „ShineBridge Gesamt"
├── Persistente History (SQLite, 7 Tage)
└── Web UI: Geräte verwalten, Live-Daten, Sparklines, EMS-Konfig
├── Energie-Dashboard (SVG-Flussdiagramm, EPEX Spot)
├── Finanzen-Tab (Abschlags-Tracker, Eigenversorgung)
└── Web UI: Live-Daten, Geräte, Einstellungen
```
### Features
## Features
- **Multi-Gerät** — beliebig viele Wechselrichter + Zähler + Wallbox, jeder als eigenes HA-Gerät
- **MQTT Discovery** — Sensoren erscheinen automatisch in HA
- **Aggregat-Gerät** — summiert alle Geräte für das HA Energie-Dashboard
- **Persistente History** — Messwerte überleben Add-on-Neustarts (SQLite, 7 Tage)
- **Energie-Dashboard** — HA-Style SVG-Flussdiagramm mit animierten Dots; EPEX-Spot-Chart
- **Finanzen-Tab** — kWh-Kosten, Eigenversorgung, Abschlags-Tracker (monatliche Rate + Grundpreis)
- **Flexibler Tarif** — Festpreis oder EPEX Spot (aWATTar DE/AT) mit Aufschlag
- **Sparklines** — Live-Graphen der letzten 5 Minuten pro Sensor
- **EMS-Controller** — PV-Überschussladen der Kathrein Wallbox, Zwangsladen-Fallback
- **EMS-Controller** — PV-Überschussladen der Kathrein Wallbox, Zwangsladen-Fallback mit konfigurierbarem Max-Ladestrom
- **Überschuss-Geräte** — Zigbee2MQTT-Geräte bei PV-Überschuss automatisch einschalten
- **Konfig-Export/Import** — JSON im Einstellungen-Tab
- **Port-Sicherheit** — `/api/*` nur über HAOS-Ingress erreichbar, kein Direktzugriff von außen
- **Kein Cloud-Zwang** — vollständig lokal
### Unterstützte Geräte
## Unterstützte Geräte
| Gerät | Typ | Sensoren | Protokoll |
|---|---|---|---|
@@ -61,13 +57,14 @@ Wechselrichter / Energiezähler
| Goodwe GW10KN-ET | Hybrid 3-phasig + Batterie | 39 | UDP/8899 (goodwe lib) |
| Kathrein Wallbox | EVSE + Meter + EMS | 18 | Modbus FC03, TCP/502 |
### EMS — PV-Überschussladen (Kathrein Wallbox)
## EMS — PV-Überschussladen (Kathrein Wallbox)
Der EMS-Controller regelt den Ladestrom der Kathrein Wallbox anhand des PV-Überschusses:
1. **PV-Laden** — Überschuss ≥ Mindestleistung (Standard: 1400 W = 6 A × 230 V): Ladestrom = verfügbare PV-Leistung / Phasen
2. **Warten** — Kein ausreichender Überschuss: Laden pausiert, Timer läuft
3. **Zwangsladen** — Nach Timeout ohne PV (Standard: 4 h): Laden mit Maximalstrom bis zur Zielzeit (Standard: 06:00 Uhr)
3. **Zwangsladen** — Nach Timeout ohne PV (Standard: 4 h): Laden mit konfiguriertem Max-Ladestrom bis zur Zielzeit (Standard: 06:00 Uhr), Strom wird bei jedem Poll erneut gesetzt (kein Einmal-Write)
4. **Unbekannte Ladezustände** (z. B. Fahrzeug-Störung) pausieren den Strom, ohne den Zwangslade-Timer zurückzusetzen
Konfigurierbar pro Gerät im Web UI:
@@ -77,48 +74,41 @@ Konfigurierbar pro Gerät im Web UI:
| Timeout ohne PV | Stunden bis Zwangsladen | 4 h |
| Vollladung bis | Zielzeit für Zwangsladen | 06:00 |
| Anzahl Phasen | 1 / 2 / 3-phasig | 3 |
| **Max. Ladestrom** | Obergrenze fürs Zwangsladen — auf die tatsächliche Wallbox-/Zuleitungs-Absicherung setzen | 32 A |
> **Hinweis:** Die EMS-Funktion muss in der Kathrein easyOperate-App aktiviert sein.
> **Wichtig:** Max. Ladestrom niemals höher als die real installierte Wallbox stellen (z. B. 16 A bei einer 11-kW-Wallbox) — sonst fordert das Zwangsladen mehr an, als die Hardware liefern kann, was bei manchen Fahrzeugen zu Ladefehlern führt.
>
> Die EMS-Funktion muss zusätzlich in der Kathrein easyOperate-App aktiviert sein.
### Netzleistung (grid_power)
## Netzleistung (grid_power)
Der Sensor `grid_power` (Goodwe) zeigt die Netzleistung intuitiv:
Der Sensor `grid_power` zeigt die Netzleistung intuitiv:
- **Positiv** = Netzbezug (Strom vom Netz)
- **Negativ** = Einspeisung (Strom ins Netz)
Bei reinen Growatt-Anlagen (ohne Goodwe) wird `power_to_grid` als Proxy verwendet (nur Einspeisung bekannt).
Bei reinen Growatt-Anlagen (ohne Goodwe) wird `power_to_grid` als Proxy verwendet.
### Installation
## Installation
**Schritt 1 — NuttX auf den ShineLAN-X flashen** *(einmalig, ST-Link nötig)*
```bash
# dapboot Bootloader
st-flash write ShineLAN-X/releases/dapboot.bin 0x08000000
# NuttX Firmware
st-flash write ShineLAN-X/releases/nuttx-mbusd-shinelanx-dfu.bin 0x08002000
```
SWD-Pinbelegung: `PA13=SWDIO PA14=SWCLK GND 3.3V`
**Schritt 2 — Add-on in Home Assistant installieren**
**Add-on in Home Assistant installieren**
1. **Einstellungen → Add-ons → Add-on-Store → ⋮ → Repositories**
2. URL: `https://gitea.bitfire.work/retr0/shinebridge`
3. „ShineBridge" → Installieren → Starten
4. MQTT-Zugangsdaten in der Add-on-Konfiguration eintragen
5. Web UI öffnen → Gerät hinzufügen
**Schritt 3 — Gerät konfigurieren**
**ShineLAN-X flashen**
Das Flashen des ShineLAN-X-Sticks (OTA oder ST-Link-Erstflash) läuft über das separate [growatt-diagnose-tool](https://gitea.bitfire.work/retr0/growatt-diagnose-tool) — dort auch die Firmware-Quelle und Doku.
**Gerät konfigurieren**
Im Web UI → „+ Gerät hinzufügen":
- Name, Modell, IP, Port, Modbus-Adresse, MQTT Topic-Präfix, Abfrageintervall
- Bei Kathrein Wallbox: EMS-Parameter konfigurieren
- Bei Kathrein Wallbox: EMS-Parameter konfigurieren (inkl. Max. Ladestrom!)
Sensoren erscheinen danach unter **Einstellungen → Geräte & Dienste → MQTT**.
### Aggregat-Sensoren (Energie-Dashboard)
## Aggregat-Sensoren (Energie-Dashboard)
Das Aggregat-Gerät „ShineBridge Gesamt" stellt folgende Sensoren bereit:
@@ -130,7 +120,7 @@ Das Aggregat-Gerät „ShineBridge Gesamt" stellt folgende Sensoren bereit:
| Batterie Ladezustand Ø | Durchschnitt aller Batterien (%) |
| Batterie Lade-/Entladeleistung | Summe (W) |
### History API
## History API
```
GET /api/history?inv_id=<id>&sensor_id=<id>&hours=<1-168>
@@ -138,113 +128,26 @@ GET /api/history?inv_id=<id>&sensor_id=<id>&hours=<1-168>
Gibt alle Messpunkte des gewählten Zeitfensters zurück (max. 7 Tage).
---
## ShineLAN-X Firmware
Der **Growatt ShineLAN-X** ist ein LAN-Monitoring-Stick (STM32F103RC + ENC28J60) der im USB-Port des Wechselrichters steckt. Die Custom-Firmware (NuttX + mbusd) macht ihn zum Modbus TCP Gateway.
> **Firmware von [mwalle (Martin Walle)](https://github.com/mwalle/shinelanx-modbus)**
> Das Binary `nuttx-mbusd-shinelanx-dfu.bin` ist das Werk von Martin Walle und wird hier unverändert weitergegeben.
> Lizenzen: Board-Support Apache 2.0 · mbusd BSD 3-Clause · Gateway-App BSD 2-Clause.
> Quellcode und Kompilieranleitung: [github.com/mwalle/shinelanx-modbus](https://github.com/mwalle/shinelanx-modbus)
### Flash-Layout
```
0x08000000 dapboot (7 KB) ← Bootloader für USB DFU OTA
0x08002000 NuttX (93 KB) ← Modbus RTU ↔ TCP, Port 502
```
### Hardware
| Komponente | Details |
|---|---|
| MCU | STM32F103RC — 256 kB Flash, 64 kB RAM |
| Ethernet | ENC28J60 (SPI2) |
| USB | PA11=D−, PA12=D+, PA8=Pullup |
| SWD | PA13=SWDIO, PA14=SWCLK |
| Debug UART | USART1 — PA9=TX, PA10=RX, 115200 Baud |
| LEDs | PC7 blau, PB1 rot, PB0 grün, PC5 blau RGB |
---
## ShineWifi-X (ESPHome)
Der **Growatt ShineWifi-X** ist ein WLAN-Stick (ESP8266) der im RS485-Port des Wechselrichters steckt. ESPHome liest die Register direkt aus.
```
Wechselrichter →RS485→ [ShineWifi-X ESP8266] →WLAN→ [Home Assistant]
```
**Erstinstallation** (USB-zu-Serial-Adapter, 3,3 V):
```bash
esphome run "ShineWifi-X/Growatt MIC 1500 TL-X/Growatt MIC 1500 TL-X.yaml"
```
| Modell | Ordner |
|---|---|
| Growatt MIC 1500 TL-X | `ShineWifi-X/Growatt MIC 1500 TL-X/` |
| Growatt MIC 2000 TL-X | `ShineWifi-X/Growatt MIC 2000 TL-X/` |
| Growatt SPH 5000 TL3-BH-UP | `ShineWifi-X/Growatt SPH 5000 TL3/` |
| Growatt MOD 6000 TL3-XH | `ShineWifi-X/Growatt MOD 6000 TL3-XH/` |
---
## ShineDiag — Vor-Ort-Diagnose
Portables Diagnose-Tool für Techniker. Raspberry Pi 3B als Gateway: LAN zum ShineLAN-X, WiFi-Hotspot für MacBook/Tablet.
```
[Wechselrichter] →USB→ [ShineLAN-X] →LAN→ [Pi 3B] →WiFi→ [MacBook/Tablet]
http://10.0.1.1
```
**Einmalig einrichten:**
```bash
git clone https://gitea.bitfire.work/retr0/shinebridge
cd shinebridge/tools/shinediag
sudo bash setup/install.sh
sudo reboot
```
**Vor Ort:**
1. LAN-Kabel: Pi ↔ ShineLAN-X
2. WiFi verbinden: **ShineDiag** | Passwort: `shinelanx`
3. Browser: `http://10.0.1.1` → Modell wählen → Auslesen → Export JSON
**Features:** Alle Sensoren, Rohdaten-Register-Dump (Adresse, Hex, Dezimal), JSON-Export.
---
## Repository-Struktur
```
shinebridge/
├── haos-addon/ ← HAOS Add-on
│ ├── config.yaml
│ ├── Dockerfile
│ └── src/
│ ├── main.py # Flask, Poll-Threads, REST API, Aggregation
│ ├── modbus_client.py # Modbus TCP, Float32-Dekodierung
│ ├── goodwe_client.py # Goodwe UDP/8899 via goodwe-Bibliothek
│ ├── wallbox_client.py # Kathrein Wallbox Modbus TCP
│ ├── ems_controller.py # PV-Überschussladen + Zwangsladen
│ ├── mqtt_publisher.py # MQTT Discovery + Aggregat
│ ├── inverters.py # Register-Maps aller Geräte
│ ├── history.py # SQLite Persistenz
│ └── web/index.html # Web UI
├── ShineLAN-X/
│ └── releases/
│ ├── dapboot.bin
│ └── nuttx-mbusd-shinelanx-dfu.bin
├── ShineWifi-X/ ← ESPHome Configs
├── tools/
│ └── shinediag/ ← Vor-Ort-Diagnose-Tool
└── repository.yaml
└── haos-addon/ ← HAOS Add-on (einziger Inhalt dieses Repos)
├── config.yaml
├── Dockerfile
└── src/
├── main.py # Flask, Poll-Threads, REST API
├── modbus_client.py # Modbus TCP, Float32-Dekodierung
├── goodwe_client.py # Goodwe UDP/8899 via goodwe-Bibliothek
├── wallbox_client.py # Kathrein Wallbox Modbus TCP
├── ems_controller.py # PV-Überschussladen + Zwangsladen
├── mqtt_publisher.py # MQTT Discovery + Aggregat
├── inverters.py # Register-Maps aller Geräte
├── history.py # SQLite Persistenz
└── web/index.html # Web UI (Energie, Finanzen, Live, Geräte, Einstellungen)
```
---
Firmware (ShineLAN-X, ShineWifi-X) und das Vor-Ort-Diagnose-Tool (ShineDiag) liegen im separaten, privaten Repo [`growatt-diagnose-tool`](https://gitea.bitfire.work/retr0/growatt-diagnose-tool).
## Lizenz
+53 -32
View File
@@ -1,11 +1,10 @@
# ShineBridge — Roadmap
## Offen / Bekannte Einschränkungen
## Bekannte Einschränkungen
- [ ] MQTT Dauerreconnect bei hohem Datenvolumen — rc=5 (Credentials / Broker-Last)
- [ ] Growatt-only: Netzbezug nicht direkt messbar — nur Einspeisung via `power_to_grid`
- [ ] Kathrein EVSE-Register 0x0060 (Charging-State) nur lesbar wenn Auto angeschlossen
- [ ] Growatt-only: Netzbezug nicht messbar (nur Einspeisung via `power_to_grid`)
- [ ] Port 8099 offen im LAN (`host_network: true`) — noch keine Authentifizierung
- [ ] NuttX OTA noch nicht auf echter Hardware getestet
---
@@ -14,27 +13,38 @@
- [x] Persistente History — SQLite `/data/history.db`, 7 Tage Retention (v1.3.0)
- [x] Konfig-Export/Import — JSON im Einstellungen-Tab (v1.3.0)
- [x] Goodwe GW10KN-ET — via goodwe-Bibliothek UDP/8899, 39 Sensoren (v1.4.0)
- [x] Kathrein Wallbox — Modbus TCP, 18 Sensoren Meter + EVSE + EMS (v1.5.0)
- [x] Kathrein Wallbox — Modbus TCP, 18 Sensoren + EMS-Controller (v1.5.0)
- [x] EMS-Controller — PV-Überschussladen + Zwangsladen-Fallback (v1.5.0)
- [x] EMS Web UI — Konfiguration direkt im Gerätedialog (v1.5.1)
- [x] grid_power Sensor — intuitive Anzeige positiv=Netzbezug (v1.5.2)
- [x] EMS Oszillations-Fix — has_pv prüft Gesamt-PV inkl. Ladeleistung (v1.5.3)
- [ ] Flash-Wizard — NuttX-Firmware via USB DFU direkt aus dem HA Web UI flashen
- [x] grid_power Sensor — positiv = Netzbezug, negativ = Einspeisung (v1.5.2)
- [x] Energie-Dashboard — HA-Style SVG-Flussdiagramm, animateMotion (v1.6.0)
- [x] Abrechnungsperiode + Strompreise — kWh + Kosten je Periode (v1.7.0)
- [x] Flexibler Tarif — EPEX Spot (aWATTar DE/AT) + Aufschlag (v1.8.16)
- [x] Finanzen-Tab — Eigenversorgung, Spot-Vergleich, Abschlags-Tracker (v1.8.14–16)
- [x] Überschuss-Geräte — Zigbee2MQTT-Geräte bei PV-Überschuss steuern (v1.8.8–13)
- [x] Setup-Wizard — geführte Ersteinrichtung (MQTT + erster Wechselrichter) (v1.8.18)
- [x] Flash-Wizard — ShineLAN-X direkt aus Web UI flashen, OTA + ST-Link-Anleitung (v1.8.18)
- [x] Port-Sicherheit — `/api/*` nur über HAOS-Ingress erreichbar (v1.8.18)
- [x] MQTT rc=5 — Reconnect-Loop verhindert, Fehlermeldung im UI (v1.8.18)
- [x] Mobile-Layout — horizontaler Overflow behoben, Tabs scrollen (v1.8.19)
- [ ] Hausverbrauch als berechneter Sensor: `PV + Netzbezug − Einspeisung − Bat_Laden + Bat_Entladen`
- [ ] Weitere Growatt-Modelle — MOD 10000, SPH 10000 etc.
- [ ] Port 8099 absichern — optionale Basic-Auth für Web UI
- [ ] Ladekosten-Berechnung: kWh je Session × Arbeitspreis
---
## Phase 3 — Hardware-Erweiterungen
### ShineLAN-X (STM32F103RC)
- [x] dapboot + NuttX produktiv (v1.0.0)
- [x] OTA HTTP-Server implementiert — Zwei-Phasen Flash (Stage 1: Staging, Stage 2: SRAM) (v1.8.18)
- [ ] OTA auf echter Hardware validieren
- [ ] Weitere Firmware-Varianten (modellspezifische Builds)
### Goodwe WiFi-Stick (WIFILAN_2.0 / HF-LPT230)
- [x] UDP/8899 Protokoll via goodwe-Bibliothek integriert (v1.4.0)
- [ ] JTAG-Analyse (TP15/TP16/TP17) — eigene Firmware wenn JTAG-eFuse nicht gesetzt
- [ ] RS485-Direktzugriff via 18-Pin-Stecker (USR-TCP232-304 getestet, kein COM-Port)
### ShineLAN-X
- [x] dapboot + NuttX produktiv (v1.0.0)
- [ ] OTA-Update via USB DFU direkt aus HA Web UI (Flash-Wizard)
- [ ] RS485-Direktzugriff via 18-Pin-Stecker
### ShineWifi-X als ShineBridge-Gateway
- [ ] Schlankes ESP8266-Projekt: RS485 → Modbus TCP Bridge
@@ -45,11 +55,10 @@
## Phase 4 — Energiebilanz & Dashboard
- [x] Aggregat-Gerät „ShineBridge Gesamt" (v1.2.0)
- [x] grid_power korrekt für Multi-Wechselrichter-Anlagen (v1.5.2)
- [x] EMS bezieht Wallbox-Ladeleistung in PV-Budget ein (v1.5.2)
- [ ] Hausverbrauch als berechneter Sensor: `PV + Netzbezug - Einspeisung - Bat_Ladung + Bat_Entladung`
- [x] EPEX Spot-Chart — 24h-Balkendiagramm im Energie-Dashboard (v1.8.16)
- [x] Eigenversorgungskarte — kWh + € gespart (v1.8.8)
- [ ] Virtuelle MQTT-Sensoren für HA Energie-Dashboard automatisch anlegen
- [ ] Ladekosten-Berechnung: kWh je Session × Arbeitspreis
- [ ] Prognose-gestütztes Laden — Spot-Preise × PV-Forecast für optimale Ladezeiten
---
@@ -57,26 +66,38 @@
- [x] WiFi-Hotspot „ShineDiag" (Pi 3B), http://10.0.1.1 (v2.0)
- [x] Alle Sensoren, Rohdaten-Register-Dump, JSON-Export (v2.0)
- [x] Pi-Setup-Script `install.sh` (v2.0)
- [ ] Goodwe-Unterstützung in ShineDiag (UDP/8899)
- [ ] Kathrein Wallbox in ShineDiag (EMS-Status, Ladezustand)
- [ ] Goodwe-Unterstützung (UDP/8899)
- [ ] Kathrein Wallbox (EMS-Status, Ladezustand)
---
## Erledigt
## Changelog
| Version | Datum | Inhalt |
|---|---|---|
| v1.0.0 | 2024 | Grundfunktion: ShineLAN-X + Growatt MIC, Modbus TCP + MQTT |
| v1.0.0 | 2024 | Grundfunktion: ShineLAN-X + Growatt MIC, Modbus TCP + MQTT Discovery |
| v1.1.3 | 2024 | Security: XSS-Fix, Flask-Binding, API-Validierung |
| v1.1.4 | 2024 | Fix: Flask zurück auf 0.0.0.0 (HA Ingress) |
| v1.1.5 | 2024 | Feature: Eastron SDM-630 + Float32 Decode |
| v1.2.0 | 2024 | Feature: Aggregat-Gerät + Energie-Dashboard Sensoren |
| v1.2.1 | 2024 | Fix: SDM-630 Gesamtwirkleistung aus Phasensumme |
| v1.1.5 | 2024 | Feature: Eastron SDM-630 + Float32-Dekodierung |
| v1.2.0 | 2024 | Feature: Aggregat-Gerät + Energie-Dashboard-Sensoren |
| v1.3.0 | 2024 | Feature: SQLite History, Konfig-Export/Import |
| v1.4.0 | 2025 | Feature: Goodwe GW10KN-ET via UDP/8899, 39 Sensoren |
| v1.5.0 | 2026-04-28 | Feature: Kathrein Wallbox + EMS-Controller |
| v1.5.1 | 2026-04-28 | Feature: EMS-Konfiguration im Web UI |
| v1.5.2 | 2026-04-28 | Fix: grid_power Anzeige, Aggregat, EMS Ladeleistung |
| v1.5.3 | 2026-04-28 | Fix: EMS Oszillation (has_pv = Gesamt-PV) |
| v1.6.0 | 2026-04-28 | Feature: Energie-Dashboard mit SVG-Flussdiagramm |
| v1.5.0 | 2026-04-28 | Feature: Kathrein Wallbox + EMS-Controller (PV-Überschussladen) |
| v1.5.3 | 2026-04-28 | Fix: EMS Oszillation, grid_power Anzeige |
| v1.6.0 | 2026-04-28 | Feature: Energie-Dashboard SVG-Flussdiagramm, animateMotion |
| v1.6.5 | 2026-04-28 | Redesign: Kreuz-Layout (Solar/Grid/Haus/Batterie/EV) |
| v1.7.0 | 2026-04-29 | Feature: Abrechnungsperiode, Strompreise, kWh-Kosten |
| v1.7.7 | 2026-04-30 | Fix: Goodwe pbattery1 Vorzeichen, Batterie-Flussrichtung |
| v1.8.8 | 2026-05-01 | Feature: Überschuss-Geräte (Zigbee2MQTT), Eigenversorgungskarte |
| v1.8.13 | 2026-05-02 | Feature: Labels, Invertiert-Modus, Mindest-Laufzeit, Z2M-Dropdown |
| v1.8.14 | 2026-05-03 | Feature: Abschlags-Tracker (monatliche Rate + Grundpreis) |
| v1.8.16 | 2026-05-04 | Feature: Finanzen-Tab, EPEX Spot-Chart, Festpreis vs. Spot-Vergleich |
| v1.8.17 | 2026-05-04 | Fix: Atomarer Config-Write + Backup-Fallback |
| v1.8.18 | 2026-05-05 | Feature: Flash-Wizard, Setup-Wizard, NuttX OTA, MQTT rc=5, Port-Sicherheit |
| v1.8.19 | 2026-05-05 | Fix: Mobile-Layout — Tabs scrollen, kein horizontaler Overflow |
| v1.8.20 | 2026-05-05 | Fix: Eigenversorgungskarte bei PV offline, Stromtarif-Einstellungen gehen nicht verloren |
| v1.8.21 | 2026-05-05 | Fix: Finanzen-Tab bleibt nicht bei "Lade..." hängen (fEur/fKwh Scope-Bug) |
| v1.8.22 | 2026-05-06 | Fix: Stromtarif-Einstellungen bleiben nach Neustart erhalten; Finanzen-Tab Layout |
| v1.8.23 | 2026-05-07 | Fix: Goodwe Netzbezug-Periode zeigt 0 kWh — integrierter grid_power-Zähler statt e_total_imp-Delta |
| v1.8.24 | 2026-05-07 | Fix: WR/Wallbox-Offline-Flapping — erst nach 3 aufeinanderfolgenden Lesefehlern offline schalten |
| v1.8.25 | 2026-05-07 | Fix: TCP-Timeout auf max. 40% des Poll-Intervalls begrenzt (Wallbox + Modbus) — verhindert Poll-Überlappung |
| v1.8.26 | 2026-05-07 | Fix: EMS-Countdown läuft ohne Auto — Kathrein 0x0060 IllegalAddress = kein Fahrzeug (default war fälschlich 1=Connected) |
-119
View File
@@ -1,119 +0,0 @@
# Growatt ShineLAN-X — Custom Firmware
Custom-Firmware für den **Growatt ShineLAN-X** LAN-Stick. Liest Modbus RTU vom Wechselrichter und publiziert alle Werte per **MQTT** direkt in Home Assistant — vollständig lokal, ohne Growatt-Cloud.
---
## Hardware
| Komponente | Details |
|---|---|
| MCU | STM32F103RBT6 (LQFP-64) |
| Ethernet | ENC28J60 (Bitbang SPI2 auf PB12–PB15) |
| Wechselrichter-Anbindung | USB-CDC (virtueller COM-Port, 115200 Baud) |
| Programmier-Interface | SWD via ST-Link |
### Pin-Belegung (vollständig verifiziert)
Quelle: [mwalle/shinelanx-modbus](https://github.com/mwalle/shinelanx-modbus) — identische Platine.
| Funktion | Pin | Hinweis |
|---|---|---|
| ETH CS | PB12 | SPI2 NSS |
| ETH SCK | PB13 | SPI2 SCK |
| ETH MISO | PB14 | SPI2 MISO |
| ETH MOSI | PB15 | SPI2 MOSI |
| ETH RST | PC8 | |
| ETH INT | PC6 | optional |
| USB D- | PA11 | Wechselrichter-Kommunikation |
| USB D+ | PA12 | Wechselrichter-Kommunikation |
| USB Pullup | PA8 | |
| Debug UART TX | PA9 | USART1, 115200 |
| Debug UART RX | PA10 | USART1, 115200 |
| LED Debug | PC7 | aktiv LOW |
| LED Rot | PB1 | aktiv LOW |
| LED Grün | PB0 | aktiv LOW |
| LED Blau | PC5 | aktiv LOW |
| Taster | PA3 | aktiv LOW |
### SWD-Verkabelung (Flashen)
| ShineLAN-X Testpunkt | ST-Link V2 |
|---|---|
| SWDIO | SWDIO |
| SWDCLK | SWCLK |
| GND | GND |
| 3.3V | 3.3V (nur wenn nicht extern versorgt) |
---
## Konfiguration
Alle Einstellungen in `firmware/include/config.h`:
```cpp
// DHCP (1) oder statische IP (0)
#define USE_DHCP 1
// MQTT-Broker
#define MQTT_BROKER "192.168.1.1"
#define MQTT_USER "mqtt"
#define MQTT_PASSWORD "..."
// Modbus Slave-Adresse (Standard: 1)
#define MODBUS_ADDR 1
```
---
## Flashen
```bash
cd firmware
pio run --target upload
```
---
## Wechselrichter-Kompatibilität
Die Sensor-Liste in `main.cpp` entspricht dem **Growatt SPH 5000 TL3-BH-UP** (dreiphasig, mit Batterie).
Beim Testen mit dem **MIC 1500 TL-X** (einphasig, ohne Batterie):
| Unterschied | MIC 1500 | SPH 5000 |
|---|---|---|
| AC Output Power | Register **40** | Register 35 |
| PV-Strings | nur PV1 | PV1 + PV2 |
| Phasen | nur L1 | L1 / L2 / L3 |
| Batterie-Register | nicht vorhanden | 1009–1058 |
Nicht vorhandene Register werden automatisch übersprungen (kein Absturz). Für Dauerbetrieb am MIC 1500 einfach die nicht passenden Sensoren in `main.cpp` auskommentieren und `ac_power_total` auf Register 40 ändern.
---
## MQTT Topics
Nach dem Start sendet die Firmware automatisch **MQTT Discovery** — Home Assistant erkennt das Gerät ohne manuelle Konfiguration.
| Topic | Inhalt |
|---|---|
| `homeassistant/sensor/growatt_shinelan_*/config` | Discovery (retained) |
| `growatt/shinelan/pv1_power` | PV1 Leistung in W |
| `growatt/shinelan/ac_power_total` | AC Ausgangsleistung in W |
| `growatt/shinelan/energy_today` | Tagesenergie in kWh |
| `growatt/shinelan/energy_total` | Gesamtenergie in kWh |
| `growatt/shinelan/bat_soc` | Batterie Ladestand in % |
| `growatt/shinelan/energy_import_total` | Netzbezug gesamt in kWh |
---
## LED-Status
| LED | Bedeutung |
|---|---|
| Alle 3 kurz an | Startup |
| Grün | MQTT verbunden |
| Rot | MQTT-Verbindungsfehler |
| Debug (PC7) | Ethernet Link UP |
| Blau blinkt | Modbus-Abfrage läuft |
-5
View File
@@ -1,5 +0,0 @@
.pio
.vscode/.browse.c_cpp.db*
.vscode/c_cpp_properties.json
.vscode/launch.json
.vscode/ipch
-10
View File
@@ -1,10 +0,0 @@
{
// See http://go.microsoft.com/fwlink/?LinkId=827846
// for the documentation about the extensions.json format
"recommendations": [
"platformio.platformio-ide"
],
"unwantedRecommendations": [
"ms-vscode.cpptools-extension-pack"
]
}
-20
View File
@@ -1,20 +0,0 @@
# Growatt ShineLAN-X Firmware
PlatformIO-Projekt fuer STM32F103RBT6. Liest Modbus RTU vom Wechselrichter via USB-CDC und publiziert via MQTT an Home Assistant.
## Wiki Knowledge Base
Path: /Users/retr0/Nextcloud/HomeLab/Vault
Beim Start einer neuen Session:
1. Zuerst `wiki/hot.md` lesen (aktueller Kontext)
2. Dann `wiki/entities/Growatt ShineLAN-X Firmware.md` fuer Projektdetails
3. Nur bei Bedarf weitere Wiki-Seiten
## Wichtige Konventionen
- Kommentare nur wenn das WARUM nicht offensichtlich ist
- Keine trailing summaries nach Tool-Calls
- config.h ist geheimnis-frei (DEFAULT_* Werte) -- committen OK
- Echte Credentials werden nur im EEPROM gespeichert, nie im Source
- Framework-Hacks in `~/.platformio/` werden bei `pio pkg update` zurueckgesetzt
-62
View File
@@ -1,62 +0,0 @@
#pragma once
// ============================================================
// Pin-Belegung — STM32F103RBT6, LQFP-64
// Quelle: https://github.com/mwalle/shinelanx-modbus
// ============================================================
// ENC28J60 — SPI2 (Hardware-SPI)
#define ETH_CS_PIN PB12
#define ETH_SCK_PIN PB13
#define ETH_MISO_PIN PB14
#define ETH_MOSI_PIN PB15
#define ETH_RST_PIN PC8
// LEDs (aktiv LOW)
#define LED_DEBUG PC7
#define LED_RED PB1
#define LED_GREEN PB0
#define LED_BLUE PC5
// User-Taster
#define BTN_USER PA3
// MAC-Adresse
#define MAC_ADDRESS 0xDE, 0xAD, 0xBE, 0xEF, 0xFE, 0xED
// ============================================================
// MODBUS
// ============================================================
#define MODBUS_BAUD 9600
#define MODBUS_ADDR 1
// ============================================================
// GERÄT
// ============================================================
#define MQTT_CLIENT "growatt-shinelan"
#define DEVICE_ID "growatt_shinelan"
#define DEVICE_NAME "Growatt ShineLAN-X"
#define DEVICE_MODEL "ShineLAN-X"
#define DEVICE_MFR "Growatt"
// ============================================================
// STANDARD-WERTE (erster Boot / Werkseinstellungen)
// Werden über die Web-UI überschrieben und in EEPROM gespeichert.
// ============================================================
#define DEFAULT_DHCP true
#define DEFAULT_STATIC_IP 192,168,1,99
#define DEFAULT_GW 192,168,1,1
#define DEFAULT_SUBNET 255,255,255,0
#define DEFAULT_DNS 192,168,1,1
#define DEFAULT_MQTT_BROKER "192.168.1.1"
#define DEFAULT_MQTT_PORT 1883
#define DEFAULT_MQTT_USER ""
#define DEFAULT_MQTT_PASS ""
#define DEFAULT_UPDATE_MS 10000UL
// ============================================================
// WECHSELRICHTER-MODELL
// Genau einen einkommentieren:
// ============================================================
#define INVERTER_MIC1500 // Growatt MIC 1500/2000 TL-X (einphasig)
// #define INVERTER_SPH5000 // Growatt SPH 5000 TL3 (dreiphasig, Hybrid)
@@ -1,50 +0,0 @@
#pragma once
#include <Arduino.h>
#include <EEPROM.h>
#include "config.h"
#define CFG_MAGIC 0xA55A1234
struct NetConfig {
uint32_t magic;
bool useDhcp;
uint8_t ip[4];
uint8_t gw[4];
uint8_t sn[4];
uint8_t dns[4];
char mqttBroker[40];
uint16_t mqttPort;
char mqttUser[32];
char mqttPass[64];
uint32_t updateMs;
};
inline void configDefaults(NetConfig& c) {
c.magic = CFG_MAGIC;
c.useDhcp = DEFAULT_DHCP;
uint8_t dip[] = {DEFAULT_STATIC_IP};
uint8_t dgw[] = {DEFAULT_GW};
uint8_t dsn[] = {DEFAULT_SUBNET};
uint8_t ddn[] = {DEFAULT_DNS};
memcpy(c.ip, dip, 4);
memcpy(c.gw, dgw, 4);
memcpy(c.sn, dsn, 4);
memcpy(c.dns, ddn, 4);
strncpy(c.mqttBroker, DEFAULT_MQTT_BROKER, sizeof(c.mqttBroker) - 1);
c.mqttBroker[sizeof(c.mqttBroker) - 1] = 0;
c.mqttPort = DEFAULT_MQTT_PORT;
strncpy(c.mqttUser, DEFAULT_MQTT_USER, sizeof(c.mqttUser) - 1);
c.mqttUser[sizeof(c.mqttUser) - 1] = 0;
strncpy(c.mqttPass, DEFAULT_MQTT_PASS, sizeof(c.mqttPass) - 1);
c.mqttPass[sizeof(c.mqttPass) - 1] = 0;
c.updateMs = DEFAULT_UPDATE_MS;
}
inline void configLoad(NetConfig& c) {
EEPROM.get(0, c);
if (c.magic != CFG_MAGIC) configDefaults(c);
}
inline void configSave(const NetConfig& c) {
EEPROM.put(0, c);
}
@@ -1,29 +0,0 @@
#pragma once
#include <Arduino.h>
#include "config.h"
// ============================================================
// Bitbang-SPI für ENC28J60 auf PC6/PC7/PC8/PC9
// ENC28J60 verwendet SPI Mode 0 (CPOL=0, CPHA=0)
// ============================================================
static inline void eth_spi_init() {
pinMode(ETH_SCK_PIN, OUTPUT); digitalWrite(ETH_SCK_PIN, LOW);
pinMode(ETH_MOSI_PIN, OUTPUT); digitalWrite(ETH_MOSI_PIN, LOW);
pinMode(ETH_MISO_PIN, INPUT);
pinMode(ETH_CS_PIN, OUTPUT); digitalWrite(ETH_CS_PIN, HIGH);
}
static inline void eth_cs_low() { digitalWrite(ETH_CS_PIN, LOW); }
static inline void eth_cs_high() { digitalWrite(ETH_CS_PIN, HIGH); }
static inline uint8_t eth_spi_transfer(uint8_t out) {
uint8_t in = 0;
for (int8_t i = 7; i >= 0; i--) {
digitalWrite(ETH_MOSI_PIN, (out >> i) & 1);
digitalWrite(ETH_SCK_PIN, HIGH);
in = (in << 1) | digitalRead(ETH_MISO_PIN);
digitalWrite(ETH_SCK_PIN, LOW);
}
return in;
}
@@ -1 +0,0 @@
{"type": "library", "name": "EthernetENC", "version": "2.0.5", "spec": {"owner": "jandrassy", "id": 11203, "name": "EthernetENC", "requirements": null, "uri": null}}
@@ -1,158 +0,0 @@
First an important note: There are more suitable libraries for ENC28J60 with ESP8266, RP2040, ESP32 and Mbed Arduino platforms:
* With ESP8266 and RP2040 platform you can use the EthernetCompat.h from the lwIP_enc28j60 bundled library.
* With ESP32 platform version 3 you can use the [EthernetESP32](https://github.com/Networking-for-Arduino/EthernetESP32) library which integrates with networking on the ESP32 platform.
* With Arduino Mbed Core boards you can use the [ENC28J60-EMAC](https://github.com/Networking-for-Arduino/ENC28J60-EMAC) library with the platforms Ethernet library
---
# EthernetENC
EthernetENC is the Ethernet library for ENC28J60. It is a modern version of [the UIPEthernet library](https://github.com/jandrassy/EthernetENC/wiki/UIPEthernet).
The modernization includes:
* Ethernet 2.0.0 Arduino library functions
* compatible include file names EthernetClient.h, EthernetServer.h and EthernetUdp.h
* support of many Arduino architectures by using the SPI library
* SPI transactions to share the SPI bus with devices with different communication settings
* SPI communication at 20 MHz if the MCU supports it, else on the maximum supported by the MCU
* client.flush() to send the packet immediately
* calls yield() in blocking functions
* has UDP backlog to receive more than one message at time
* Arduino 1.5 library format built with dot_a_linkage option for optimal build result
[The documentation of Arduino Ethernet library](https://www.arduino.cc/en/Reference/Ethernet) applies for classes and functions descriptions.
Limitations:
* UDP.beginMulticast is not supported, because the uIP stack doesn't support multicast
* UDP broadcasts receiving is turned off on ENC to lower the processing load on the library
This library doesn't have examples, because examples of the Arduino Ethernet library apply. You can find them in the Arduino IDE Examples menu Ethernet section. Only change `#include <Ethernet.h>` to `#include <EthernetENC.h>`. Some examples require [a little change](https://github.com/jandrassy/EthernetENC/wiki/Examples).
This library is based on the Norbert Truchsess's arduino-uip original source code repository and uses experience from the development of the multi-architecture support by Cassy. Applicable fixes and enhancements from development of EthernetENC were transfered to Cassy's UIPEthernet.
**You can find more information in project's [Wiki](https://github.com/jandrassy/EthernetENC/wiki).**
## Original notes by Norbert Truchsess
UIPEthernet is written as a wrapper around the mature uIP Stack by Adam Dunkels, which provides the low-level implementation for all supported protocols. To overcome the memory-constrains (a 'regular' uIP-application does all processing in RAM) the ENC28J60 internal memory is used for all stream buffers (in and out). Only 400-600 Bytes of Arduinos RAM are used (depending on the number of concurrently open connections). As of Flash-memory a ATmega368-based Arduino is the minimum requirenment.
uIP was written by Adam Dunkels of the Networked Embedded Systems group at the Swedish Institute of Computer Science.
This library was inspired by the SerialIP implementation by Adam Nielsen <malvineous@shikadi.net>, actually I took this code as a starting point, but in the latest versions there are very few lines left.
## Licenses
```
UIPEthernet.h
UIPEthernet.cpp
UIPServer.h
UIPServer.cpp
UIPClient.h
UIPClient.cpp
UIPUdp.h
UIPUdp.cpp
utility/mempool.h
utility/mempool.cpp
Copyright (c) 2013 Norbert Truchsess <norbert.truchsess@t-online.de>
All rights reserved.
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
--------------
utility/enc28j60.h
Author : Pascal Stang (c)2005
Modified by Norbert Truchsess
Copyright: GPL V2
--------------
utility/Enc28J60Network.h
utility/Enc28J60Network.cpp
Copyright (c) 2013 Norbert Truchsess <norbert.truchsess@t-online.de>
All rights reserved.
inspired and based on enc28j60.c file from the AVRlib library by Pascal Stang.
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
--------------
utility/uip.c
utility/uip_arp.h
utility/uip_arp.c
utility/uip_arch.h
utility/uip.h
utility/uipopt.h
Copyright (c) 2001-2003, Adam Dunkels <adam@sics.se>, <adam@dunkels.com>.
All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions
are met:
1. Redistributions of source code must retain the above copyright
notice, this list of conditions and the following disclaimer.
2. Redistributions in binary form must reproduce the above copyright
notice, this list of conditions and the following disclaimer in the
documentation and/or other materials provided with the distribution.
3. Neither the name of the Institute nor the names of its contributors
may be used to endorse or promote products derived from this software
without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE INSTITUTE AND CONTRIBUTORS ``AS IS'' AND
ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
ARE DISCLAIMED. IN NO EVENT SHALL THE INSTITUTE OR CONTRIBUTORS BE LIABLE
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
SUCH DAMAGE.
--------------
Dhcp.h
Dhcp.cpp
DHCP Library v0.3 - April 25, 2009
Author: Jordan Terrell - blog.jordanterrell.com
- as included in Arduinos stock Ethernet-library, no special licence mentioned here
--------------
Dns.h
Dns.cpp
(c) Copyright 2009-2010 MCQN Ltd.
Released under Apache License, version 2.0
```
@@ -1,11 +0,0 @@
name=EthernetENC
author=Norbert Truchsess, Juraj Andrassy
maintainer=Juraj Andrassy <juraj.andrassy@gmail.com>
sentence=Ethernet library for ENC28J60. Only include EthernetENC.h instead of Ethernet.h
paragraph=This is a modern version of the UIPEthernet library. EthernetENC library is compatible with all Arduino architectures with Arduino SPI library with transactions support.
url=https://github.com/Networking-for-Arduino/EthernetENC/wiki
architectures=*
version=2.0.5
category=Communication
includes=EthernetENC.h
dot_a_linkage=true
@@ -1,491 +0,0 @@
// DHCP Library v0.3 - April 25, 2009
// Author: Jordan Terrell - blog.jordanterrell.com
#include "Dhcp.h"
#include <Arduino.h>
#include "utility/util.h"
int DhcpClass::beginWithDHCP(uint8_t *mac, unsigned long timeout, unsigned long responseTimeout)
{
_dhcpLeaseTime=0;
_dhcpT1=0;
_dhcpT2=0;
_lastCheck=0;
_timeout = timeout;
_responseTimeout = responseTimeout;
// zero out _dhcpMacAddr
memset(_dhcpMacAddr, 0, 6);
reset_DHCP_lease();
memcpy((void*)_dhcpMacAddr, (void*)mac, 6);
_dhcp_state = STATE_DHCP_START;
return request_DHCP_lease();
}
void DhcpClass::reset_DHCP_lease(){
memset(_dhcpLocalIp, 0, 4);
memset(_dhcpSubnetMask, 0, 4);
memset(_dhcpGatewayIp, 0, 4);
memset(_dhcpDhcpServerIp, 0, 4);
memset(_dhcpDnsServerIp, 0, 4);
}
//return:0 on error, 1 if request is sent and response is received
int DhcpClass::request_DHCP_lease(){
uint8_t messageType = 0;
// Pick an initial transaction ID
_dhcpTransactionId = random(1UL, 2000UL);
_dhcpInitialTransactionId = _dhcpTransactionId;
_dhcpUdpSocket.stop();
if (_dhcpUdpSocket.begin(DHCP_CLIENT_PORT) == 0)
{
// Couldn't get a socket
return 0;
}
presend_DHCP();
int result = 0;
unsigned long startTime = millis();
while(_dhcp_state != STATE_DHCP_LEASED)
{
if(_dhcp_state == STATE_DHCP_START)
{
_dhcpTransactionId++;
send_DHCP_MESSAGE(DHCP_DISCOVER, ((millis() - startTime) / 1000));
_dhcp_state = STATE_DHCP_DISCOVER;
}
else if(_dhcp_state == STATE_DHCP_REREQUEST){
_dhcpTransactionId++;
send_DHCP_MESSAGE(DHCP_REQUEST, ((millis() - startTime)/1000));
_dhcp_state = STATE_DHCP_REQUEST;
}
else if(_dhcp_state == STATE_DHCP_DISCOVER)
{
uint32_t respId;
messageType = parseDHCPResponse(_responseTimeout, respId);
if(messageType == DHCP_OFFER)
{
// We'll use the transaction ID that the offer came with,
// rather than the one we were up to
_dhcpTransactionId = respId;
send_DHCP_MESSAGE(DHCP_REQUEST, ((millis() - startTime) / 1000));
_dhcp_state = STATE_DHCP_REQUEST;
}
}
else if(_dhcp_state == STATE_DHCP_REQUEST)
{
uint32_t respId;
messageType = parseDHCPResponse(_responseTimeout, respId);
if(messageType == DHCP_ACK)
{
_dhcp_state = STATE_DHCP_LEASED;
result = 1;
//use default lease time if we didn't get it
if(_dhcpLeaseTime == 0){
_dhcpLeaseTime = DEFAULT_LEASE;
}
//calculate T1 & T2 if we didn't get it
if(_dhcpT1 == 0){
//T1 should be 50% of _dhcpLeaseTime
_dhcpT1 = _dhcpLeaseTime >> 1;
}
if(_dhcpT2 == 0){
//T2 should be 87.5% (7/8ths) of _dhcpLeaseTime
_dhcpT2 = _dhcpT1 << 1;
}
_renewInSec = _dhcpT1;
_rebindInSec = _dhcpT2;
}
else if(messageType == DHCP_NAK)
_dhcp_state = STATE_DHCP_START;
}
if(messageType == 255)
{
messageType = 0;
_dhcp_state = STATE_DHCP_START;
}
if(result != 1 && ((millis() - startTime) > _timeout))
break;
}
// We're done with the socket now
_dhcpUdpSocket.stop();
_dhcpTransactionId++;
return result;
}
void DhcpClass::presend_DHCP()
{
}
void DhcpClass::send_DHCP_MESSAGE(uint8_t messageType, uint16_t secondsElapsed)
{
uint8_t buffer[32];
memset(buffer, 0, 32);
IPAddress dest_addr( 255, 255, 255, 255 ); // Broadcast address
if (-1 == _dhcpUdpSocket.beginPacket(dest_addr, DHCP_SERVER_PORT))
{
// FIXME Need to return errors
return;
}
buffer[0] = DHCP_BOOTREQUEST; // op
buffer[1] = DHCP_HTYPE10MB; // htype
buffer[2] = DHCP_HLENETHERNET; // hlen
buffer[3] = DHCP_HOPS; // hops
// xid
unsigned long xid = htonl(_dhcpTransactionId);
memcpy(buffer + 4, &(xid), 4);
// 8, 9 - seconds elapsed
buffer[8] = ((secondsElapsed & 0xff00) >> 8);
buffer[9] = (secondsElapsed & 0x00ff);
// flags - the only one flag DHCP uses is
// to request the server response as broadcast, not unicast
// unsigned short flags = htons(DHCP_FLAGSBROADCAST);
// memcpy(buffer + 10, &(flags), 2);
// ciaddr: already zeroed
// yiaddr: already zeroed
// siaddr: already zeroed
// giaddr: already zeroed
//put data in W5100 transmit buffer
_dhcpUdpSocket.write(buffer, 28);
memset(buffer, 0, 32); // clear local buffer
memcpy(buffer, _dhcpMacAddr, 6); // chaddr
//put data in W5100 transmit buffer
_dhcpUdpSocket.write(buffer, 16);
memset(buffer, 0, 32); // clear local buffer
// leave zeroed out for sname && file
// put in W5100 transmit buffer x 6 (192 bytes)
for(int i = 0; i < 6; i++) {
_dhcpUdpSocket.write(buffer, 32);
}
// OPT - Magic Cookie
buffer[0] = (uint8_t)((MAGIC_COOKIE >> 24)& 0xFF);
buffer[1] = (uint8_t)((MAGIC_COOKIE >> 16)& 0xFF);
buffer[2] = (uint8_t)((MAGIC_COOKIE >> 8)& 0xFF);
buffer[3] = (uint8_t)(MAGIC_COOKIE& 0xFF);
// OPT - message type
buffer[4] = dhcpMessageType;
buffer[5] = 0x01;
buffer[6] = messageType; //DHCP_REQUEST;
// OPT - client identifier
buffer[7] = dhcpClientIdentifier;
buffer[8] = 0x07;
buffer[9] = 0x01;
memcpy(buffer + 10, _dhcpMacAddr, 6);
// OPT - host name
buffer[16] = hostName;
if (_hostname == nullptr) {
buffer[17] = strlen(HOST_NAME) + 6; // length of hostname + last 3 bytes of mac address
strcpy((char*)&(buffer[18]), HOST_NAME);
printByte((char*)&(buffer[24]), _dhcpMacAddr[3]);
printByte((char*)&(buffer[26]), _dhcpMacAddr[4]);
printByte((char*)&(buffer[28]), _dhcpMacAddr[5]);
//put data in W5100 transmit buffer
_dhcpUdpSocket.write(buffer, 30);
} else {
uint8_t len = strlen(_hostname);
buffer[17] = len;
_dhcpUdpSocket.write(buffer, 18);
_dhcpUdpSocket.write(_hostname, len);
}
if(messageType == DHCP_REQUEST)
{
buffer[0] = dhcpRequestedIPaddr;
buffer[1] = 0x04;
buffer[2] = _dhcpLocalIp[0];
buffer[3] = _dhcpLocalIp[1];
buffer[4] = _dhcpLocalIp[2];
buffer[5] = _dhcpLocalIp[3];
buffer[6] = dhcpServerIdentifier;
buffer[7] = 0x04;
buffer[8] = _dhcpDhcpServerIp[0];
buffer[9] = _dhcpDhcpServerIp[1];
buffer[10] = _dhcpDhcpServerIp[2];
buffer[11] = _dhcpDhcpServerIp[3];
//put data in W5100 transmit buffer
_dhcpUdpSocket.write(buffer, 12);
}
buffer[0] = dhcpParamRequest;
buffer[1] = 0x06;
buffer[2] = subnetMask;
buffer[3] = routersOnSubnet;
buffer[4] = dns;
buffer[5] = domainName;
buffer[6] = dhcpT1value;
buffer[7] = dhcpT2value;
buffer[8] = endOption;
//put data in W5100 transmit buffer
_dhcpUdpSocket.write(buffer, 9);
_dhcpUdpSocket.endPacket();
}
uint8_t DhcpClass::parseDHCPResponse(unsigned long responseTimeout, uint32_t& transactionId)
{
uint8_t type = 0;
uint8_t opt_len = 0;
unsigned long startTime = millis();
while(_dhcpUdpSocket.parsePacket() <= 0)
{
if((millis() - startTime) > responseTimeout)
{
return 255;
}
delay(50);
}
// start reading in the packet
RIP_MSG_FIXED fixedMsg;
_dhcpUdpSocket.read((uint8_t*)&fixedMsg, sizeof(RIP_MSG_FIXED));
if(fixedMsg.op == DHCP_BOOTREPLY && _dhcpUdpSocket.remotePort() == DHCP_SERVER_PORT)
{
transactionId = ntohl(fixedMsg.xid);
if(memcmp(fixedMsg.chaddr, _dhcpMacAddr, 6) != 0 || (transactionId < _dhcpInitialTransactionId) || (transactionId > _dhcpTransactionId))
{
// Need to read the rest of the packet here regardless
_dhcpUdpSocket.discardReceived();
return 0;
}
memcpy(_dhcpLocalIp, fixedMsg.yiaddr, 4);
// Skip to the option part
// Doing this a byte at a time so we don't have to put a big buffer
// on the stack (as we don't have lots of memory lying around)
for (int i =0; i < (240 - (int)sizeof(RIP_MSG_FIXED)); i++)
{
_dhcpUdpSocket.read(); // we don't care about the returned byte
}
while (_dhcpUdpSocket.available() > 0)
{
switch (_dhcpUdpSocket.read())
{
case endOption :
break;
case padOption :
break;
case dhcpMessageType :
opt_len = _dhcpUdpSocket.read();
type = _dhcpUdpSocket.read();
break;
case subnetMask :
opt_len = _dhcpUdpSocket.read();
_dhcpUdpSocket.read(_dhcpSubnetMask, 4);
break;
case routersOnSubnet :
opt_len = _dhcpUdpSocket.read();
_dhcpUdpSocket.read(_dhcpGatewayIp, 4);
for (int i = 0; i < opt_len-4; i++)
{
_dhcpUdpSocket.read();
}
break;
case dns :
opt_len = _dhcpUdpSocket.read();
_dhcpUdpSocket.read(_dhcpDnsServerIp, 4);
for (int i = 0; i < opt_len-4; i++)
{
_dhcpUdpSocket.read();
}
break;
case dhcpServerIdentifier :
opt_len = _dhcpUdpSocket.read();
if( _dhcpDhcpServerIp[0] == 0 ||
IPAddress(_dhcpDhcpServerIp) == _dhcpUdpSocket.remoteIP() )
{
_dhcpUdpSocket.read(_dhcpDhcpServerIp, sizeof(_dhcpDhcpServerIp));
}
else
{
// Skip over the rest of this option
while (opt_len--)
{
_dhcpUdpSocket.read();
}
}
break;
case dhcpT1value :
opt_len = _dhcpUdpSocket.read();
_dhcpUdpSocket.read((uint8_t*)&_dhcpT1, sizeof(_dhcpT1));
_dhcpT1 = ntohl(_dhcpT1);
break;
case dhcpT2value :
opt_len = _dhcpUdpSocket.read();
_dhcpUdpSocket.read((uint8_t*)&_dhcpT2, sizeof(_dhcpT2));
_dhcpT2 = ntohl(_dhcpT2);
break;
case dhcpIPaddrLeaseTime :
opt_len = _dhcpUdpSocket.read();
_dhcpUdpSocket.read((uint8_t*)&_dhcpLeaseTime, sizeof(_dhcpLeaseTime));
_dhcpLeaseTime = ntohl(_dhcpLeaseTime);
_renewInSec = _dhcpLeaseTime;
break;
default :
opt_len = _dhcpUdpSocket.read();
// Skip over the rest of this option
while (opt_len--)
{
_dhcpUdpSocket.read();
}
break;
}
}
}
// Need to skip to end of the packet regardless here
_dhcpUdpSocket.discardReceived();
return type;
}
/*
returns:
0/DHCP_CHECK_NONE: nothing happened
1/DHCP_CHECK_RENEW_FAIL: renew failed
2/DHCP_CHECK_RENEW_OK: renew success
3/DHCP_CHECK_REBIND_FAIL: rebind fail
4/DHCP_CHECK_REBIND_OK: rebind success
*/
int DhcpClass::checkLease(){
//this uses a signed / unsigned trick to deal with millis overflow
unsigned long now = millis();
signed long snow = (long)now;
int rc=DHCP_CHECK_NONE;
if (_lastCheck != 0){
signed long factor;
//calc how many ms past the timeout we are
factor = snow - (long)_secTimeout;
//if on or passed the timeout, reduce the counters
if ( factor >= 0 ){
//next timeout should be now plus 1000 ms minus parts of second in factor
_secTimeout = snow + 1000 - factor % 1000;
//how many seconds late are we, minimum 1
factor = factor / 1000 +1;
//reduce the counters by that mouch
//if we can assume that the cycle time (factor) is fairly constant
//and if the remainder is less than cycle time * 2
//do it early instead of late
if(_renewInSec < factor*2 )
_renewInSec = 0;
else
_renewInSec -= factor;
if(_rebindInSec < factor*2 )
_rebindInSec = 0;
else
_rebindInSec -= factor;
}
//if we have a lease but should renew, do it
if (_dhcp_state == STATE_DHCP_LEASED && _renewInSec <=0){
_dhcp_state = STATE_DHCP_REREQUEST;
rc = 1 + request_DHCP_lease();
}
//if we have a lease or is renewing but should bind, do it
if( (_dhcp_state == STATE_DHCP_LEASED || _dhcp_state == STATE_DHCP_START) && _rebindInSec <=0){
//this should basically restart completely
_dhcp_state = STATE_DHCP_START;
reset_DHCP_lease();
rc = 3 + request_DHCP_lease();
}
}
else{
_secTimeout = snow + 1000;
}
_lastCheck = now;
return rc;
}
IPAddress DhcpClass::getLocalIp()
{
return IPAddress(_dhcpLocalIp);
}
IPAddress DhcpClass::getSubnetMask()
{
return IPAddress(_dhcpSubnetMask);
}
IPAddress DhcpClass::getGatewayIp()
{
return IPAddress(_dhcpGatewayIp);
}
IPAddress DhcpClass::getDhcpServerIp()
{
return IPAddress(_dhcpDhcpServerIp);
}
IPAddress DhcpClass::getDnsServerIp()
{
return IPAddress(_dhcpDnsServerIp);
}
void DhcpClass::setHostname(const char* hostname) {
_hostname = hostname;
}
void DhcpClass::printByte(char * buf, uint8_t n ) {
char *str = &buf[1];
buf[0]='0';
do {
unsigned long m = n;
n /= 16;
char c = m - 16 * n;
*str-- = c < 10 ? c + '0' : c + 'A' - 10;
} while(n);
}
@@ -1,189 +0,0 @@
// DHCP Library v0.3 - April 25, 2009
// Author: Jordan Terrell - blog.jordanterrell.com
#ifndef Dhcp_h
#define Dhcp_h
#include "EthernetUdp.h"
/* DHCP state machine. */
#define STATE_DHCP_START 0
#define STATE_DHCP_DISCOVER 1
#define STATE_DHCP_REQUEST 2
#define STATE_DHCP_LEASED 3
#define STATE_DHCP_REREQUEST 4
#define STATE_DHCP_RELEASE 5
#define DHCP_FLAGSBROADCAST 0x8000
/* UDP port numbers for DHCP */
#define DHCP_SERVER_PORT 67 /* from server to client */
#define DHCP_CLIENT_PORT 68 /* from client to server */
/* DHCP message OP code */
#define DHCP_BOOTREQUEST 1
#define DHCP_BOOTREPLY 2
/* DHCP message type */
#define DHCP_DISCOVER 1
#define DHCP_OFFER 2
#define DHCP_REQUEST 3
#define DHCP_DECLINE 4
#define DHCP_ACK 5
#define DHCP_NAK 6
#define DHCP_RELEASE 7
#define DHCP_INFORM 8
#define DHCP_HTYPE10MB 1
#define DHCP_HTYPE100MB 2
#define DHCP_HLENETHERNET 6
#define DHCP_HOPS 0
#define DHCP_SECS 0
#define MAGIC_COOKIE 0x63825363
#define MAX_DHCP_OPT 16
#define HOST_NAME "ENC28J"
#define DEFAULT_LEASE (900) //default lease time in seconds
#define DHCP_CHECK_NONE (0)
#define DHCP_CHECK_RENEW_FAIL (1)
#define DHCP_CHECK_RENEW_OK (2)
#define DHCP_CHECK_REBIND_FAIL (3)
#define DHCP_CHECK_REBIND_OK (4)
enum
{
padOption = 0,
subnetMask = 1,
timerOffset = 2,
routersOnSubnet = 3,
/* timeServer = 4,
nameServer = 5,*/
dns = 6,
/*logServer = 7,
cookieServer = 8,
lprServer = 9,
impressServer = 10,
resourceLocationServer = 11,*/
hostName = 12,
/*bootFileSize = 13,
meritDumpFile = 14,*/
domainName = 15,
/*swapServer = 16,
rootPath = 17,
extentionsPath = 18,
IPforwarding = 19,
nonLocalSourceRouting = 20,
policyFilter = 21,
maxDgramReasmSize = 22,
defaultIPTTL = 23,
pathMTUagingTimeout = 24,
pathMTUplateauTable = 25,
ifMTU = 26,
allSubnetsLocal = 27,
broadcastAddr = 28,
performMaskDiscovery = 29,
maskSupplier = 30,
performRouterDiscovery = 31,
routerSolicitationAddr = 32,
staticRoute = 33,
trailerEncapsulation = 34,
arpCacheTimeout = 35,
ethernetEncapsulation = 36,
tcpDefaultTTL = 37,
tcpKeepaliveInterval = 38,
tcpKeepaliveGarbage = 39,
nisDomainName = 40,
nisServers = 41,
ntpServers = 42,
vendorSpecificInfo = 43,
netBIOSnameServer = 44,
netBIOSdgramDistServer = 45,
netBIOSnodeType = 46,
netBIOSscope = 47,
xFontServer = 48,
xDisplayManager = 49,*/
dhcpRequestedIPaddr = 50,
dhcpIPaddrLeaseTime = 51,
/*dhcpOptionOverload = 52,*/
dhcpMessageType = 53,
dhcpServerIdentifier = 54,
dhcpParamRequest = 55,
/*dhcpMsg = 56,
dhcpMaxMsgSize = 57,*/
dhcpT1value = 58,
dhcpT2value = 59,
/*dhcpClassIdentifier = 60,*/
dhcpClientIdentifier = 61,
endOption = 255
};
typedef struct _RIP_MSG_FIXED
{
uint8_t op;
uint8_t htype;
uint8_t hlen;
uint8_t hops;
uint32_t xid;
uint16_t secs;
uint16_t flags;
uint8_t ciaddr[4];
uint8_t yiaddr[4];
uint8_t siaddr[4];
uint8_t giaddr[4];
uint8_t chaddr[6];
}RIP_MSG_FIXED;
class DhcpClass {
private:
uint32_t _dhcpInitialTransactionId;
uint32_t _dhcpTransactionId;
uint8_t _dhcpMacAddr[6];
const char* _hostname = nullptr;
#ifdef __arm__
uint8_t _dhcpLocalIp[4] __attribute__((aligned(4)));
uint8_t _dhcpSubnetMask[4] __attribute__((aligned(4)));
uint8_t _dhcpGatewayIp[4] __attribute__((aligned(4)));
uint8_t _dhcpDhcpServerIp[4] __attribute__((aligned(4)));
uint8_t _dhcpDnsServerIp[4] __attribute__((aligned(4)));
#else
uint8_t _dhcpLocalIp[4];
uint8_t _dhcpSubnetMask[4];
uint8_t _dhcpGatewayIp[4];
uint8_t _dhcpDhcpServerIp[4];
uint8_t _dhcpDnsServerIp[4];
#endif
uint32_t _dhcpLeaseTime;
uint32_t _dhcpT1, _dhcpT2;
signed long _renewInSec;
signed long _rebindInSec;
signed long _lastCheck;
unsigned long _timeout;
unsigned long _responseTimeout;
unsigned long _secTimeout;
uint8_t _dhcp_state;
EthernetUDP _dhcpUdpSocket;
int request_DHCP_lease();
void reset_DHCP_lease();
void presend_DHCP();
void send_DHCP_MESSAGE(uint8_t, uint16_t);
void printByte(char *, uint8_t);
uint8_t parseDHCPResponse(unsigned long responseTimeout, uint32_t& transactionId);
public:
IPAddress getLocalIp();
IPAddress getSubnetMask();
IPAddress getGatewayIp();
IPAddress getDhcpServerIp();
IPAddress getDnsServerIp();
int beginWithDHCP(uint8_t *, unsigned long timeout = 60000, unsigned long responseTimeout = 4000);
int checkLease();
void setHostname(const char* hostname);
};
#endif
@@ -1,416 +0,0 @@
// Arduino DNS client for Enc28J60-based Ethernet shield
// (c) Copyright 2009-2010 MCQN Ltd.
// Released under Apache License, version 2.0
#include "Dns.h"
#include <Arduino.h>
#define SOCKET_NONE 255
// Various flags and header field values for a DNS message
#define UDP_HEADER_SIZE 8
#define DNS_HEADER_SIZE 12
#define TTL_SIZE 4
#define QUERY_FLAG (0)
#define RESPONSE_FLAG (1<<15)
#define QUERY_RESPONSE_MASK (1<<15)
#define OPCODE_STANDARD_QUERY (0)
#define OPCODE_INVERSE_QUERY (1<<11)
#define OPCODE_STATUS_REQUEST (2<<11)
#define OPCODE_MASK (15<<11)
#define AUTHORITATIVE_FLAG (1<<10)
#define TRUNCATION_FLAG (1<<9)
#define RECURSION_DESIRED_FLAG (1<<8)
#define RECURSION_AVAILABLE_FLAG (1<<7)
#define RESP_NO_ERROR (0)
#define RESP_FORMAT_ERROR (1)
#define RESP_SERVER_FAILURE (2)
#define RESP_NAME_ERROR (3)
#define RESP_NOT_IMPLEMENTED (4)
#define RESP_REFUSED (5)
#define RESP_MASK (15)
#define TYPE_A (0x0001)
#define CLASS_IN (0x0001)
#define LABEL_COMPRESSION_MASK (0xC0)
// Port number that DNS servers listen on
#define DNS_PORT 53
// Possible return codes from ProcessResponse
#define SUCCESS 1
#define TIMED_OUT -1
#define INVALID_SERVER -2
#define TRUNCATED -3
#define INVALID_RESPONSE -4
void DNSClient::begin(const IPAddress& aDNSServer)
{
iDNSServer = aDNSServer;
iRequestId = 0;
}
int DNSClient::inet_aton(const char* aIPAddrString, IPAddress& aResult)
{
// See if we've been given a valid IP address
const char* p =aIPAddrString;
while (*p &&
( (*p == '.') || ((*p >= '0') && (*p <= '9')) ))
{
p++;
}
if (*p == '\0')
{
// It's looking promising, we haven't found any invalid characters
p = aIPAddrString;
int segment =0;
int segmentValue =0;
while (*p && (segment < 4))
{
if (*p == '.')
{
// We've reached the end of a segment
if (segmentValue > 255)
{
// You can't have IP address segments that don't fit in a byte
return 0;
}
else
{
aResult[segment] = (byte)segmentValue;
segment++;
segmentValue = 0;
}
}
else
{
// Next digit
segmentValue = (segmentValue*10)+(*p - '0');
}
p++;
}
// We've reached the end of address, but there'll still be the last
// segment to deal with
if ((segmentValue > 255) || (segment > 3))
{
// You can't have IP address segments that don't fit in a byte,
// or more than four segments
return 0;
}
else
{
aResult[segment] = (byte)segmentValue;
return 1;
}
}
else
{
return 0;
}
}
int DNSClient::getHostByName(const char* aHostname, IPAddress& aResult)
{
int ret =0;
// See if it's a numeric IP address
if (inet_aton(aHostname, aResult))
{
// It is, our work here is done
return 1;
}
// Check we've got a valid DNS server to use
if (iDNSServer == INADDR_NONE)
{
return INVALID_SERVER;
}
// Find a socket to use
if (iUdp.begin(1024+(millis() & 0xF)) == 1)
{
// Try up to three times
int retries = 0;
// while ((retries < 3) && (ret <= 0))
{
// Send DNS request
ret = iUdp.beginPacket(iDNSServer, DNS_PORT);
if (ret != 0)
{
// Now output the request data
ret = BuildRequest(aHostname);
if (ret != 0)
{
// And finally send the request
ret = iUdp.endPacket();
if (ret != 0)
{
// Now wait for a response
int wait_retries = 0;
ret = TIMED_OUT;
while ((wait_retries < 3) && (ret == TIMED_OUT))
{
ret = ProcessResponse(5000, aResult);
wait_retries++;
}
}
}
}
retries++;
}
// We're done with the socket now
iUdp.stop();
}
return ret;
}
uint16_t DNSClient::BuildRequest(const char* aName)
{
// Build header
// 1 1 1 1 1 1
// 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5
// +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
// | ID |
// +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
// |QR| Opcode |AA|TC|RD|RA| Z | RCODE |
// +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
// | QDCOUNT |
// +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
// | ANCOUNT |
// +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
// | NSCOUNT |
// +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
// | ARCOUNT |
// +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
// As we only support one request at a time at present, we can simplify
// some of this header
iRequestId = millis(); // generate a random ID
uint16_t twoByteBuffer;
// FIXME We should also check that there's enough space available to write to, rather
// FIXME than assume there's enough space (as the code does at present)
iUdp.write((uint8_t*)&iRequestId, sizeof(iRequestId));
twoByteBuffer = htons(QUERY_FLAG | OPCODE_STANDARD_QUERY | RECURSION_DESIRED_FLAG);
iUdp.write((uint8_t*)&twoByteBuffer, sizeof(twoByteBuffer));
twoByteBuffer = htons(1); // One question record
iUdp.write((uint8_t*)&twoByteBuffer, sizeof(twoByteBuffer));
twoByteBuffer = 0; // Zero answer records
iUdp.write((uint8_t*)&twoByteBuffer, sizeof(twoByteBuffer));
iUdp.write((uint8_t*)&twoByteBuffer, sizeof(twoByteBuffer));
// and zero additional records
iUdp.write((uint8_t*)&twoByteBuffer, sizeof(twoByteBuffer));
// Build question
const char* start =aName;
const char* end =start;
uint8_t len;
// Run through the name being requested
while (*end)
{
// Find out how long this section of the name is
end = start;
while (*end && (*end != '.') )
{
end++;
}
if (end-start > 0)
{
// Write out the size of this section
len = end-start;
iUdp.write(&len, sizeof(len));
// And then write out the section
iUdp.write((uint8_t*)start, end-start);
}
start = end+1;
}
// We've got to the end of the question name, so
// terminate it with a zero-length section
len = 0;
iUdp.write(&len, sizeof(len));
// Finally the type and class of question
twoByteBuffer = htons(TYPE_A);
iUdp.write((uint8_t*)&twoByteBuffer, sizeof(twoByteBuffer));
twoByteBuffer = htons(CLASS_IN); // Internet class of question
iUdp.write((uint8_t*)&twoByteBuffer, sizeof(twoByteBuffer));
// Success! Everything buffered okay
return 1;
}
uint16_t DNSClient::ProcessResponse(uint16_t aTimeout, IPAddress& aAddress)
{
uint32_t startTime = millis();
// Wait for a response packet
while(iUdp.parsePacket() <= 0)
{
if((millis() - startTime) > aTimeout)
return TIMED_OUT;
delay(50);
}
// We've had a reply!
// Read the UDP header
uint8_t header[DNS_HEADER_SIZE]; // Enough space to reuse for the DNS header
// Check that it's a response from the right server and the right port
if ( (iDNSServer != iUdp.remoteIP()) ||
(iUdp.remotePort() != DNS_PORT) )
{
// It's not from who we expected
return INVALID_SERVER;
}
// Read through the rest of the response
if (iUdp.available() < DNS_HEADER_SIZE)
{
return TRUNCATED;
}
iUdp.read(header, DNS_HEADER_SIZE);
uint16_t header_flags = htons(*((uint16_t*)&header[2]));
// Check that it's a response to this request
if ( ( iRequestId != (*((uint16_t*)&header[0])) ) ||
((header_flags & QUERY_RESPONSE_MASK) != (uint16_t)RESPONSE_FLAG) )
{
// Mark the entire packet as read
iUdp.discardReceived();
return INVALID_RESPONSE;
}
// Check for any errors in the response (or in our request)
// although we don't do anything to get round these
if ( (header_flags & TRUNCATION_FLAG) || (header_flags & RESP_MASK) )
{
// Mark the entire packet as read
iUdp.discardReceived();
return -5; //INVALID_RESPONSE;
}
// And make sure we've got (at least) one answer
uint16_t answerCount = htons(*((uint16_t*)&header[6]));
if (answerCount == 0 )
{
// Mark the entire packet as read
iUdp.discardReceived();
return -6; //INVALID_RESPONSE;
}
// Skip over any questions
for (uint16_t i =0; i < htons(*((uint16_t*)&header[4])); i++)
{
// Skip over the name
uint8_t len;
do
{
iUdp.read(&len, sizeof(len));
if (len > 0)
{
// Don't need to actually read the data out for the string, just
// advance ptr to beyond it
while(len--)
{
iUdp.read(); // we don't care about the returned byte
}
}
} while (len != 0);
// Now jump over the type and class
for (int i =0; i < 4; i++)
{
iUdp.read(); // we don't care about the returned byte
}
}
// Now we're up to the bit we're interested in, the answer
// There might be more than one answer (although we'll just use the first
// type A answer) and some authority and additional resource records but
// we're going to ignore all of them.
for (uint16_t i =0; i < answerCount; i++)
{
// Skip the name
uint8_t len;
do
{
iUdp.read(&len, sizeof(len));
if ((len & LABEL_COMPRESSION_MASK) == 0)
{
// It's just a normal label
if (len > 0)
{
// And it's got a length
// Don't need to actually read the data out for the string,
// just advance ptr to beyond it
while(len--)
{
iUdp.read(); // we don't care about the returned byte
}
}
}
else
{
// This is a pointer to a somewhere else in the message for the
// rest of the name. We don't care about the name, and RFC1035
// says that a name is either a sequence of labels ended with a
// 0 length octet or a pointer or a sequence of labels ending in
// a pointer. Either way, when we get here we're at the end of
// the name
// Skip over the pointer
iUdp.read(); // we don't care about the returned byte
// And set len so that we drop out of the name loop
len = 0;
}
} while (len != 0);
// Check the type and class
uint16_t answerType;
uint16_t answerClass;
iUdp.read((uint8_t*)&answerType, sizeof(answerType));
iUdp.read((uint8_t*)&answerClass, sizeof(answerClass));
// Ignore the Time-To-Live as we don't do any caching
for (int i =0; i < TTL_SIZE; i++)
{
iUdp.read(); // we don't care about the returned byte
}
// And read out the length of this answer
// Don't need header_flags anymore, so we can reuse it here
iUdp.read((uint8_t*)&header_flags, sizeof(header_flags));
if ( (htons(answerType) == TYPE_A) && (htons(answerClass) == CLASS_IN) )
{
if (htons(header_flags) != 4)
{
// It's a weird size
// Mark the entire packet as read
iUdp.discardReceived();
return -9;//INVALID_RESPONSE;
}
iUdp.read(aAddress.raw_address(), 4);
return SUCCESS;
}
else
{
// This isn't an answer type we're after, move onto the next one
for (uint16_t i =0; i < htons(header_flags); i++)
{
iUdp.read(); // we don't care about the returned byte
}
}
}
// Mark the entire packet as read
iUdp.discardReceived();
// If we get here then we haven't found an answer
return -10;//INVALID_RESPONSE;
}
@@ -1,41 +0,0 @@
// Arduino DNS client for Enc28J60-based Ethernet shield
// (c) Copyright 2009-2010 MCQN Ltd.
// Released under Apache License, version 2.0
#ifndef DNSClient_h
#define DNSClient_h
#include "EthernetUdp.h"
class DNSClient
{
public:
// ctor
void begin(const IPAddress& aDNSServer);
/** Convert a numeric IP address string into a four-byte IP address.
@param aIPAddrString IP address to convert
@param aResult IPAddress structure to store the returned IP address
@result 1 if aIPAddrString was successfully converted to an IP address,
else error code
*/
int inet_aton(const char *aIPAddrString, IPAddress& aResult);
/** Resolve the given hostname to an IP address.
@param aHostname Name to be resolved
@param aResult IPAddress structure to store the returned IP address
@result 1 if aIPAddrString was successfully converted to an IP address,
else error code
*/
int getHostByName(const char* aHostname, IPAddress& aResult);
protected:
uint16_t BuildRequest(const char* aName);
uint16_t ProcessResponse(uint16_t aTimeout, IPAddress& aAddress);
IPAddress iDNSServer;
uint16_t iRequestId;
EthernetUDP iUdp;
};
#endif
@@ -1,559 +0,0 @@
/*
UIPEthernet.cpp - Arduino implementation of a uIP wrapper class.
Copyright (c) 2013 Norbert Truchsess <norbert.truchsess@t-online.de>
All rights reserved.
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
#include <Arduino.h>
#include "Ethernet.h"
#include "Dns.h"
#include "utility/Enc28J60Network.h"
extern "C"
{
#include "utility/uip-conf.h"
#include "utility/uip.h"
#include "utility/uip_arp.h"
}
#define ETH_HDR ((struct uip_eth_hdr *)&uip_buf[0])
bool UIPEthernetClass::initialized = false;
memhandle UIPEthernetClass::in_packet(NOBLOCK);
memhandle UIPEthernetClass::uip_packet(NOBLOCK);
uint8_t UIPEthernetClass::uip_hdrlen(0);
uint8_t UIPEthernetClass::packetstate(0);
IPAddress UIPEthernetClass::_dnsServerAddress;
DhcpClass* UIPEthernetClass::_dhcp(NULL);
unsigned long UIPEthernetClass::periodic_timer;
unsigned long UIPEthernetClass::arp_timer;
// Because uIP isn't encapsulated within a class we have to use global
// variables, so we can only have one TCP/IP stack per program.
UIPEthernetClass::UIPEthernetClass()
{
}
void UIPEthernetClass::init(uint8_t csPin)
{
Enc28J60Network::setCsPin(csPin);
}
#if UIP_UDP
void
UIPEthernetClass::setHostname(const char* hostname)
{
if (_dhcp == NULL) {
_dhcp = new DhcpClass();
}
_dhcp->setHostname(hostname);
}
int
UIPEthernetClass::begin(const uint8_t* mac, unsigned long timeout, unsigned long responseTimeout)
{
if (_dhcp == NULL) {
_dhcp = new DhcpClass();
}
// Initialise the basic info
init(mac);
// Now try to get our config info from a DHCP server
int ret = _dhcp->beginWithDHCP((uint8_t*)mac, timeout, responseTimeout);
if(ret == 1)
{
// We've successfully found a DHCP server and got our configuration info, so set things
// accordingly
configure(_dhcp->getLocalIp(),_dhcp->getDnsServerIp(),_dhcp->getGatewayIp(),_dhcp->getSubnetMask());
}
return ret;
}
#endif
void
UIPEthernetClass::begin(const uint8_t* mac, IPAddress ip)
{
IPAddress dns = ip;
dns[3] = 1;
begin(mac, ip, dns);
}
void
UIPEthernetClass::begin(const uint8_t* mac, IPAddress ip, IPAddress dns)
{
IPAddress gateway = ip;
gateway[3] = 1;
begin(mac, ip, dns, gateway);
}
void
UIPEthernetClass::begin(const uint8_t* mac, IPAddress ip, IPAddress dns, IPAddress gateway)
{
IPAddress subnet(255, 255, 255, 0);
begin(mac, ip, dns, gateway, subnet);
}
void
UIPEthernetClass::begin(const uint8_t* mac, IPAddress ip, IPAddress dns, IPAddress gateway, IPAddress subnet)
{
init(mac);
configure(ip,dns,gateway,subnet);
}
void UIPEthernetClass::end()
{
//close all clients
for (int i = 0; i < UIP_CONNS; i++)
{
if (EthernetClient::all_data[i].state) {
EthernetClient client(&EthernetClient::all_data[i]);
client.stop();
}
}
// handle clients closings
uint32_t st = millis();
while (millis() - st < 3000)
{
tick();
}
initialized = false;
configure(INADDR_NONE, INADDR_NONE, INADDR_NONE, INADDR_NONE);
}
int UIPEthernetClass::maintain(){
if (!initialized)
return 0;
tick();
int rc = DHCP_CHECK_NONE;
#if UIP_UDP
if(_dhcp != NULL){
//we have a pointer to dhcp, use it
rc = _dhcp->checkLease();
switch ( rc ){
case DHCP_CHECK_NONE:
//nothing done
break;
case DHCP_CHECK_RENEW_OK:
case DHCP_CHECK_REBIND_OK:
//we might have got a new IP.
configure(_dhcp->getLocalIp(),_dhcp->getDnsServerIp(),_dhcp->getGatewayIp(),_dhcp->getSubnetMask());
break;
default:
//this is actually a error, it will retry though
break;
}
}
#endif
return rc;
}
EthernetLinkStatus UIPEthernetClass::linkStatus()
{
if (!Enc28J60Network::getrev())
return Unknown;
return Enc28J60Network::linkStatus() ? LinkON : LinkOFF;
}
EthernetHardwareStatus UIPEthernetClass::hardwareStatus()
{
if (!Enc28J60Network::getrev())
return EthernetNoHardware;
return EthernetENC28J60;
}
uint8_t* UIPEthernetClass::macAddress(uint8_t* mac)
{
memcpy(mac, uip_ethaddr.addr, 6);
return mac;
}
IPAddress UIPEthernetClass::localIP()
{
IPAddress ret;
uip_ipaddr_t a;
uip_gethostaddr(a);
return ip_addr_uip(a);
}
IPAddress UIPEthernetClass::subnetMask()
{
IPAddress ret;
uip_ipaddr_t a;
uip_getnetmask(a);
return ip_addr_uip(a);
}
IPAddress UIPEthernetClass::gatewayIP()
{
IPAddress ret;
uip_ipaddr_t a;
uip_getdraddr(a);
return ip_addr_uip(a);
}
IPAddress UIPEthernetClass::dnsServerIP()
{
return _dnsServerAddress;
}
IPAddress UIPEthernetClass::dnsIP(int n) {
return (n == 0) ? _dnsServerAddress : IPAddress();
}
int UIPEthernetClass::hostByName(const char* hostname, IPAddress& result)
{
// Look up the host first
int ret = 0;
#if UIP_UDP
DNSClient dns;
dns.begin(_dnsServerAddress);
ret = dns.getHostByName(hostname, result);
#endif
return ret;
}
void
UIPEthernetClass::tick()
{
if (!initialized)
return;
// run ARP table cleanup every 10 seconds as required by a comment in uip_arp.h
if (millis() - arp_timer > 10000) {
arp_timer = millis();
uip_arp_timer();
}
if (in_packet == NOBLOCK)
{
in_packet = Enc28J60Network::receivePacket();
#ifdef UIPETHERNET_DEBUG
if (in_packet != NOBLOCK)
{
Serial.println(F("--------------\npacket received"));
}
#endif
}
if (in_packet != NOBLOCK)
{
packetstate = UIPETHERNET_FREEPACKET;
uip_len = Enc28J60Network::blockSize(in_packet);
if (uip_len > 0)
{
Enc28J60Network::readPacket(in_packet,0,(uint8_t*)uip_buf,UIP_BUFSIZE);
if (ETH_HDR ->type == HTONS(UIP_ETHTYPE_IP))
{
uip_packet = in_packet; //required for upper_layer_checksum of in_packet!
#ifdef UIPETHERNET_DEBUG
Serial.print(F("readPacket type IP, uip_len: "));
Serial.print(uip_len);
Serial.print(F(", bits: "));
Serial.println(BUF->flags, BIN);
#endif
uip_arp_ipin();
uip_input();
if (uip_len > 0)
{
uip_arp_out();
network_send();
}
}
else if (ETH_HDR ->type == HTONS(UIP_ETHTYPE_ARP))
{
#ifdef UIPETHERNET_DEBUG
Serial.print(F("readPacket type ARP, uip_len: "));
Serial.println(uip_len);
#endif
uip_arp_arpin();
if (uip_len > 0)
{
network_send();
}
}
}
if (in_packet != NOBLOCK && (packetstate & UIPETHERNET_FREEPACKET))
{
#ifdef UIPETHERNET_DEBUG
Serial.println(F("freeing received packet"));
#endif
Enc28J60Network::freePacket();
in_packet = NOBLOCK;
}
}
unsigned long now = millis();
if ((long)( now - periodic_timer ) >= 0)
{
periodic_timer = now + UIP_PERIODIC_TIMER;
for (int i = 0; i < UIP_CONNS; i++)
{
uip_periodic(i);
// If the above function invocation resulted in data that
// should be sent out on the Enc28J60Network, the global variable
// uip_len is set to a value > 0.
if (uip_len > 0)
{
uip_arp_out();
network_send();
}
}
#if UIP_UDP
for (int i = 0; i < UIP_UDP_CONNS; i++)
{
uip_udp_periodic(i);
// If the above function invocation resulted in data that
// should be sent out on the Enc28J60Network, the global variable
// uip_len is set to a value > 0. */
if (uip_len > 0)
{
EthernetUDP::_send(&uip_udp_conns[i]);
}
}
#endif /* UIP_UDP */
}
}
boolean UIPEthernetClass::network_send()
{
if (packetstate & UIPETHERNET_SENDPACKET)
{
#ifdef UIPETHERNET_DEBUG
Serial.print(F("Enc28J60Network_send uip_packet: "));
Serial.print(uip_packet);
Serial.print(F(", hdrlen: "));
Serial.println(uip_hdrlen);
#endif
Enc28J60Network::writePacket(uip_packet, UIP_SENDBUFFER_OFFSET,uip_buf,uip_hdrlen);
packetstate &= ~ UIPETHERNET_SENDPACKET;
goto sendandfree;
}
uip_packet = Enc28J60Network::allocBlock(uip_len + UIP_SENDBUFFER_OFFSET + UIP_SENDBUFFER_PADDING);
if (uip_packet != NOBLOCK)
{
#ifdef UIPETHERNET_DEBUG
Serial.print(F("Enc28J60Network_send uip_buf (uip_len): "));
Serial.print(uip_len);
Serial.print(F(", packet: "));
Serial.print(uip_packet);
Serial.print(F(", bits: "));
Serial.println(BUF->flags, BIN);
#endif
Enc28J60Network::writePacket(uip_packet, UIP_SENDBUFFER_OFFSET,uip_buf,uip_len);
goto sendandfree;
}
return false;
sendandfree:
bool success = Enc28J60Network::sendPacket(uip_packet);
Enc28J60Network::freeBlock(uip_packet);
uip_packet = NOBLOCK;
return success;
}
void UIPEthernetClass::init(const uint8_t* mac) {
periodic_timer = millis() + UIP_PERIODIC_TIMER;
initialized = Enc28J60Network::init((uint8_t*)mac);
uip_seteth_addr(mac);
uip_init();
uip_arp_init();
}
void UIPEthernetClass::configure(IPAddress ip, IPAddress dns, IPAddress gateway, IPAddress subnet) {
uip_ipaddr_t ipaddr;
uip_ip_addr(ipaddr, ip);
uip_sethostaddr(ipaddr);
uip_ip_addr(ipaddr, gateway);
uip_setdraddr(ipaddr);
uip_ip_addr(ipaddr, subnet);
uip_setnetmask(ipaddr);
_dnsServerAddress = dns;
}
void
UIPEthernetClass::call_yield()
{
static bool in_yield;
static uint32_t last_call_millis;
if (!in_yield && millis() - last_call_millis > 0)
{
last_call_millis = millis();
in_yield = true;
yield();
in_yield = false;
}
}
/*---------------------------------------------------------------------------*/
uint16_t
UIPEthernetClass::chksum(uint16_t sum, const uint8_t *data, uint16_t len)
{
uint16_t t;
const uint8_t *dataptr;
const uint8_t *last_byte;
dataptr = data;
last_byte = data + len - 1;
while(dataptr < last_byte) { /* At least two more bytes */
t = (dataptr[0] << 8) + dataptr[1];
sum += t;
if(sum < t) {
sum++; /* carry */
}
dataptr += 2;
}
if(dataptr == last_byte) {
t = (dataptr[0] << 8) + 0;
sum += t;
if(sum < t) {
sum++; /* carry */
}
}
/* Return sum in host byte order. */
return sum;
}
/*---------------------------------------------------------------------------*/
uint16_t
UIPEthernetClass::ipchksum(void)
{
uint16_t sum;
sum = chksum(0, &uip_buf[UIP_LLH_LEN], UIP_IPH_LEN);
return (sum == 0) ? 0xffff : htons(sum);
}
/*---------------------------------------------------------------------------*/
uint16_t
#if UIP_UDP
UIPEthernetClass::upper_layer_chksum(uint8_t proto)
#else
uip_tcpchksum(void)
#endif
{
uint16_t upper_layer_len;
uint16_t sum;
#if UIP_CONF_IPV6
upper_layer_len = (((u16_t)(BUF->len[0]) << 8) + BUF->len[1]);
#else /* UIP_CONF_IPV6 */
upper_layer_len = (((u16_t)(BUF->len[0]) << 8) + BUF->len[1]) - UIP_IPH_LEN;
#endif /* UIP_CONF_IPV6 */
/* First sum pseudoheader. */
/* IP protocol and length fields. This addition cannot carry. */
#if UIP_UDP
sum = upper_layer_len + proto;
#else
sum = upper_layer_len + UIP_PROTO_TCP;
#endif
/* Sum IP source and destination addresses. */
sum = UIPEthernetClass::chksum(sum, (u8_t *)&BUF->srcipaddr[0], 2 * sizeof(uip_ipaddr_t));
uint8_t upper_layer_memlen;
#if UIP_UDP
switch(proto)
{
// case UIP_PROTO_ICMP:
// case UIP_PROTO_ICMP6:
// upper_layer_memlen = upper_layer_len;
// break;
case UIP_PROTO_UDP:
upper_layer_memlen = UIP_UDPH_LEN;
break;
default:
// case UIP_PROTO_TCP:
#endif
upper_layer_memlen = (BUF->tcpoffset >> 4) << 2;
#if UIP_UDP
break;
}
#endif
sum = UIPEthernetClass::chksum(sum, &uip_buf[UIP_IPH_LEN + UIP_LLH_LEN], upper_layer_memlen);
#ifdef UIPETHERNET_DEBUG_CHKSUM
Serial.print(F("chksum uip_buf["));
Serial.print(UIP_IPH_LEN + UIP_LLH_LEN);
Serial.print(F("-"));
Serial.print(UIP_IPH_LEN + UIP_LLH_LEN + upper_layer_memlen);
Serial.print(F("]: "));
Serial.println(htons(sum),HEX);
#endif
if (upper_layer_memlen < upper_layer_len)
{
sum = Enc28J60Network::chksum(
sum,
UIPEthernetClass::uip_packet,
(UIPEthernetClass::packetstate & UIPETHERNET_SENDPACKET ? UIP_IPH_LEN + UIP_LLH_LEN + UIP_SENDBUFFER_OFFSET : UIP_IPH_LEN + UIP_LLH_LEN) + upper_layer_memlen,
upper_layer_len - upper_layer_memlen
);
#ifdef UIPETHERNET_DEBUG_CHKSUM
Serial.print(F("chksum uip_packet("));
Serial.print(uip_packet);
Serial.print(F(")["));
Serial.print(UIP_IPH_LEN + UIP_LLH_LEN + upper_layer_memlen);
Serial.print(F("-"));
Serial.print(UIP_IPH_LEN + UIP_LLH_LEN + upper_layer_len);
Serial.print(F("]: "));
Serial.println(htons(sum),HEX);
#endif
}
return (sum == 0) ? 0xffff : htons(sum);
}
uint16_t
uip_ipchksum(void)
{
return UIPEthernetClass::ipchksum();
}
#if UIP_UDP
uint16_t
uip_tcpchksum(void)
{
uint16_t sum = UIPEthernetClass::upper_layer_chksum(UIP_PROTO_TCP);
return sum;
}
uint16_t
uip_udpchksum(void)
{
uint16_t sum = UIPEthernetClass::upper_layer_chksum(UIP_PROTO_UDP);
return sum;
}
#endif
UIPEthernetClass Ethernet;
extern "C" void serialPrint(int i);
void serialPrint(int i) {
Serial.println(i);
Serial.flush();
}
@@ -1,161 +0,0 @@
/*
UIPEthernet.h - Arduino implementation of a uIP wrapper class.
Copyright (c) 2013 Norbert Truchsess <norbert.truchsess@t-online.de>
All rights reserved.
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
#ifndef UIPETHERNET_H
#define UIPETHERNET_H
//#define UIPETHERNET_DEBUG
//#define UIPETHERNET_DEBUG_CHKSUM
//#define UIPETHERNET_DEBUG_UDP
//#define UIPETHERNET_DEBUG_CLIENT
#include <Arduino.h>
#include "Dhcp.h"
#include <IPAddress.h>
#include "utility/Enc28J60Network.h"
#include "EthernetClient.h"
#include "EthernetServer.h"
#include "EthernetUdp.h"
extern "C"
{
#include "utility/uip.h"
}
#define UIPETHERNET_FREEPACKET 1
#define UIPETHERNET_SENDPACKET 2
#define UIPETHERNET_BUFFERREAD 4
#define uip_ip_addr(addr, ip) do { \
((u16_t *)(addr))[0] = HTONS(((ip[0]) << 8) | (ip[1])); \
((u16_t *)(addr))[1] = HTONS(((ip[2]) << 8) | (ip[3])); \
} while(0)
#define ip_addr_uip(a) IPAddress(a[0] & 0xFF, a[0] >> 8 , a[1] & 0xFF, a[1] >> 8) //TODO this is not IPV6 capable
#define uip_seteth_addr(eaddr) do {uip_ethaddr.addr[0] = eaddr[0]; \
uip_ethaddr.addr[1] = eaddr[1];\
uip_ethaddr.addr[2] = eaddr[2];\
uip_ethaddr.addr[3] = eaddr[3];\
uip_ethaddr.addr[4] = eaddr[4];\
uip_ethaddr.addr[5] = eaddr[5];} while(0)
#define BUF ((struct uip_tcpip_hdr *)&uip_buf[UIP_LLH_LEN])
enum EthernetLinkStatus {
Unknown,
LinkON,
LinkOFF
};
enum EthernetHardwareStatus {
EthernetNoHardware,
EthernetW5100,
EthernetW5200,
EthernetW5500,
EthernetENC28J60 = 10
};
class UIPEthernetClass
{
public:
UIPEthernetClass();
void init(uint8_t csPin);
int begin(const uint8_t* mac, unsigned long timeout = 60000, unsigned long responseTimeout = 4000);
void begin(const uint8_t* mac, IPAddress ip);
void begin(const uint8_t* mac, IPAddress ip, IPAddress dns);
void begin(const uint8_t* mac, IPAddress ip, IPAddress dns, IPAddress gateway);
void begin(const uint8_t* mac, IPAddress ip, IPAddress dns, IPAddress gateway, IPAddress subnet);
void end();
// maintain() must be called at regular intervals to process the incoming serial
// data and issue IP events to the sketch. It does not return until all IP
// events have been processed. Renews dhcp-lease if required.
int maintain();
EthernetLinkStatus linkStatus();
EthernetHardwareStatus hardwareStatus();
uint8_t* macAddress(uint8_t* mac);
void MACAddress(uint8_t *mac_address) { macAddress(mac_address); }
IPAddress localIP();
IPAddress subnetMask();
IPAddress gatewayIP();
IPAddress dnsServerIP();
IPAddress dnsIP(int n = 0);
void setDnsServerIP(const IPAddress dns_server) { _dnsServerAddress = dns_server; }
void setDNS(IPAddress dns_server) { _dnsServerAddress = dns_server; }
void setHostname(const char* hostname); // only the pointer is stored!
int hostByName(const char* hostname, IPAddress& result);
private:
static bool initialized;
static memhandle in_packet;
static memhandle uip_packet;
static uint8_t uip_hdrlen;
static uint8_t packetstate;
static IPAddress _dnsServerAddress;
static DhcpClass* _dhcp;
static unsigned long periodic_timer;
static unsigned long arp_timer;
static void init(const uint8_t* mac);
static void configure(IPAddress ip, IPAddress dns, IPAddress gateway, IPAddress subnet);
static void tick();
static boolean network_send();
static void call_yield();
friend class EthernetServer;
friend class EthernetClient;
friend class EthernetUDP;
static uint16_t chksum(uint16_t sum, const uint8_t* data, uint16_t len);
static uint16_t ipchksum(void);
#if UIP_UDP
static uint16_t upper_layer_chksum(uint8_t proto);
#endif
friend uint16_t uip_ipchksum(void);
friend uint16_t uip_tcpchksum(void);
friend uint16_t uip_udpchksum(void);
friend void uipclient_appcall(void);
friend void uipudp_appcall(void);
#if UIP_CONF_IPV6
uint16_t uip_icmp6chksum(void);
#endif /* UIP_CONF_IPV6 */
};
extern UIPEthernetClass Ethernet;
#endif
@@ -1,663 +0,0 @@
/*
UIPClient.cpp - Arduino implementation of a uIP wrapper class.
Copyright (c) 2013 Norbert Truchsess <norbert.truchsess@t-online.de>
All rights reserved.
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
extern "C"
{
#include "utility/uip-conf.h"
#include "utility/uip.h"
#include "utility/uip_arp.h"
}
#include "Ethernet.h"
#include "EthernetClient.h"
#define UIP_TCP_PHYH_LEN UIP_LLH_LEN+UIP_IPTCPH_LEN
#define UIPClient EthernetClient // to not pollute source code history with the rename
uip_userdata_t UIPClient::all_data[UIP_CONNS];
UIPClient::UIPClient() :
data(NULL)
{
}
UIPClient::UIPClient(uip_userdata_t* conn_data) :
data(conn_data)
{
}
int
UIPClient::connect(IPAddress ip, uint16_t port)
{
stop();
uip_ipaddr_t ipaddr;
uip_ip_addr(ipaddr, ip);
struct uip_conn* conn = uip_connect(&ipaddr, htons(port));
if (conn)
{
#if UIP_CONNECT_TIMEOUT > 0
uint32_t timeout = millis() + connectTimeout;
#endif
while((conn->tcpstateflags & UIP_TS_MASK) != UIP_CLOSED)
{
UIPEthernetClass::tick();
if ((conn->tcpstateflags & UIP_TS_MASK) == UIP_ESTABLISHED)
{
data = (uip_userdata_t*) conn->appstate;
#ifdef UIPETHERNET_DEBUG_CLIENT
Serial.print(F("connected, state: "));
Serial.print(data->state);
Serial.print(F(", first packet in: "));
Serial.println(data->packets_in[0]);
#endif
return 1;
}
#if UIP_CONNECT_TIMEOUT > 0
if (((int32_t)(millis() - timeout)) > 0)
{
conn->tcpstateflags = UIP_CLOSED;
break;
}
#endif
}
}
return 0;
}
int
UIPClient::connect(const char *host, uint16_t port)
{
// Look up the host first
int ret = 0;
#if UIP_UDP
IPAddress remote_addr;
ret = Ethernet.hostByName(host, remote_addr);
if (ret == 1) {
return connect(remote_addr, port);
}
#endif
return ret;
}
void
UIPClient::stop()
{
if (data && data->state)
{
#ifdef UIPETHERNET_DEBUG_CLIENT
Serial.println(F("before stop(), with data"));
_dumpAllData();
#endif
if (data->state & UIP_CLIENT_REMOTECLOSED)
{
data->state = 0;
}
else
{
flush();
data->state |= UIP_CLIENT_CLOSE;
}
_flushBlocks(data->packets_in);
#ifdef UIPETHERNET_DEBUG_CLIENT
Serial.println(F("after stop()"));
_dumpAllData();
#endif
}
#ifdef UIPETHERNET_DEBUG_CLIENT
else
{
Serial.println(F("stop(), data: NULL"));
}
#endif
data = NULL;
UIPEthernetClass::tick();
}
uint8_t
UIPClient::connected()
{
return (data && (data->packets_in[0] != NOBLOCK || (data->state & UIP_CLIENT_CONNECTED))) ? 1 : 0;
}
bool
UIPClient::operator==(const UIPClient& rhs)
{
return data && rhs.data && (data == rhs.data);
}
UIPClient::operator bool()
{
UIPEthernetClass::tick();
return data && (!(data->state & UIP_CLIENT_REMOTECLOSED) || data->packets_in[0] != NOBLOCK);
}
size_t
UIPClient::write(uint8_t c)
{
return write(&c, 1);
}
size_t
UIPClient::write(const uint8_t *buf, size_t size)
{
uip_userdata_t* u = data;
int remain = size;
uint16_t written;
#if UIP_WRITE_TIMEOUT > 0
uint32_t timeout_start = millis();
#endif
repeat:
UIPEthernetClass::tick();
if (u && u->state && !(u->state & (UIP_CLIENT_CLOSE | UIP_CLIENT_REMOTECLOSED)))
{
uint8_t p = _currentBlock(u->packets_out);
if (u->packets_out[p] == NOBLOCK)
{
newpacket:
u->packets_out[p] = Enc28J60Network::allocBlock(UIP_SOCKET_DATALEN);
if (u->packets_out[p] == NOBLOCK)
{
#if UIP_WRITE_TIMEOUT > 0
if (millis() - timeout_start > UIP_WRITE_TIMEOUT)
{
setWriteError();
goto ready;
}
#endif
Ethernet.call_yield();
goto repeat;
}
u->out_pos = 0;
}
#ifdef UIPETHERNET_DEBUG_CLIENT
Serial.print(F("UIPClient.write: writePacket("));
Serial.print(u->packets_out[p]);
Serial.print(F(") pos: "));
Serial.print(u->out_pos);
Serial.print(F(", buf["));
Serial.print(size-remain);
Serial.print(F("-"));
Serial.print(remain);
Serial.print(F("]: '"));
Serial.write((uint8_t*)buf+size-remain,remain);
Serial.println(F("'"));
#endif
written = Enc28J60Network::writePacket(u->packets_out[p],u->out_pos,(uint8_t*)buf+size-remain,remain);
remain -= written;
u->out_pos+=written;
if (remain > 0)
{
if (p == 0) // block 0 just filled, start sending immediately
{
flush();
goto repeat;
}
if (p == UIP_SOCKET_NUMPACKETS-1)
{
#if UIP_WRITE_TIMEOUT > 0
if (millis() - timeout_start > UIP_WRITE_TIMEOUT)
{
setWriteError();
goto ready;
}
#endif
Ethernet.call_yield();
goto repeat;
}
p++;
goto newpacket;
}
ready:
return size-remain;
}
return 0;
}
int
UIPClient::availableForWrite()
{
const int MAX_AVAILABLE = UIP_SOCKET_DATALEN * UIP_SOCKET_NUMPACKETS;
UIPEthernetClass::tick();
if (data->packets_out[0] == NOBLOCK)
return MAX_AVAILABLE;
uint8_t p = _currentBlock(data->packets_out);
int used = UIP_SOCKET_DATALEN * p + data->out_pos;
return MAX_AVAILABLE - used;
}
void
UIPClient::flush()
{
UIPEthernetClass::tick();
if (data && data->packets_out[0] != NOBLOCK)
{
struct uip_conn* conn = &uip_conns[data->conn_index];
if (!uip_outstanding(conn))
{
uip_poll_conn(conn);
if (uip_len > 0)
{
uip_arp_out();
UIPEthernetClass::network_send();
}
}
}
}
int
UIPClient::available()
{
if (!(*this))
return 0;
int len = 0;
for (uint8_t i = 0; i < UIP_SOCKET_NUMPACKETS; i++)
{
len += Enc28J60Network::blockSize(data->packets_in[i]);
}
// if sketch checks for incoming data and there are unsent data, flush the transmit buffer
if (!len)
flush();
return len;
}
int
UIPClient::read(uint8_t *buf, size_t size)
{
if (*this)
{
uint16_t remain = size;
if (data->packets_in[0] == NOBLOCK)
{
flush(); // if sketch checks for incoming data and there are unsent data, flush the transmit buffer
return 0;
}
uint16_t read;
do
{
read = Enc28J60Network::readPacket(data->packets_in[0],0,buf+size-remain,remain);
if (read == Enc28J60Network::blockSize(data->packets_in[0]))
{
remain -= read;
_eatBlock(data->packets_in);
if (uip_stopped(&uip_conns[data->conn_index]) && !(data->state & (UIP_CLIENT_CLOSE | UIP_CLIENT_REMOTECLOSED)))
data->state |= UIP_CLIENT_RESTART;
if (data->packets_in[0] == NOBLOCK)
{
if (data->state & UIP_CLIENT_REMOTECLOSED)
{
data->state = 0;
data = NULL;
}
return size-remain;
}
}
else
{
Enc28J60Network::resizeBlock(data->packets_in[0],read);
break;
}
}
while(remain > 0);
return size;
}
return -1;
}
int
UIPClient::read()
{
uint8_t c;
if (read(&c,1) <= 0)
return -1;
return c;
}
int
UIPClient::peek()
{
if (*this)
{
if (data->packets_in[0] != NOBLOCK)
{
uint8_t c;
Enc28J60Network::readPacket(data->packets_in[0],0,&c,1);
return c;
}
}
return -1;
}
void
UIPClient::discardReceived()
{
if (*this)
{
_flushBlocks(data->packets_in);
}
}
IPAddress
UIPClient::remoteIP(void)
{
return data ? ip_addr_uip(uip_conns[data->conn_index].ripaddr) : IPAddress();
}
uint16_t
UIPClient::remotePort(void)
{
return data ? ntohs(uip_conns[data->conn_index].rport) : 0;
}
uint8_t
UIPClient::status()
{
return !data ? UIP_CLOSED : uip_conns[data->conn_index].tcpstateflags & UIP_TS_MASK;
}
void
uipclient_appcall(void)
{
uint16_t send_len = 0;
uip_userdata_t *u = (uip_userdata_t*)uip_conn->appstate;
if (!u && uip_connected())
{
#ifdef UIPETHERNET_DEBUG_CLIENT
Serial.println(F("UIPClient uip_connected"));
UIPClient::_dumpAllData();
#endif
u = (uip_userdata_t*) UIPClient::_allocateData();
if (u)
{
uip_conn->appstate = u;
#ifdef UIPETHERNET_DEBUG_CLIENT
Serial.print(F("UIPClient allocated state: "));
Serial.println(u->state,BIN);
#endif
}
#ifdef UIPETHERNET_DEBUG_CLIENT
else
Serial.println(F("UIPClient allocation failed"));
#endif
}
if (u)
{
if (uip_newdata())
{
#ifdef UIPETHERNET_DEBUG_CLIENT
Serial.print(F("UIPClient uip_newdata, uip_len:"));
Serial.println(uip_len);
#endif
if (uip_len && u->state && !(u->state & UIP_CLIENT_CLOSE))
{
for (uint8_t i=0; i < UIP_SOCKET_NUMPACKETS; i++)
{
if (u->packets_in[i] == NOBLOCK)
{
u->packets_in[i] = Enc28J60Network::allocBlock(uip_len);
if (u->packets_in[i] != NOBLOCK)
{
Enc28J60Network::copyPacket(u->packets_in[i],0,UIPEthernetClass::in_packet,((uint8_t*)uip_appdata)-uip_buf,uip_len);
if (i == UIP_SOCKET_NUMPACKETS-1)
uip_stop();
goto finish_newdata;
}
}
}
UIPEthernetClass::packetstate &= ~UIPETHERNET_FREEPACKET;
uip_stop();
}
}
finish_newdata:
if (u->state & UIP_CLIENT_RESTART)
{
u->state &= ~UIP_CLIENT_RESTART;
uip_restart();
}
// If the connection has been closed, save received but unread data.
if (uip_closed() || uip_timedout() || uip_aborted())
{
#ifdef UIPETHERNET_DEBUG_CLIENT
Serial.println(F("UIPClient uip_closed"));
UIPClient::_dumpAllData();
#endif
// drop outgoing packets not sent yet:
UIPClient::_flushBlocks(u->packets_out);
if (u->packets_in[0] != NOBLOCK)
{
((uip_userdata_closed_t *)u)->lport = uip_conn->lport;
u->state |= UIP_CLIENT_REMOTECLOSED;
}
else
u->state = 0;
// disassociate appdata.
#ifdef UIPETHERNET_DEBUG_CLIENT
Serial.println(F("after UIPClient uip_closed"));
UIPClient::_dumpAllData();
#endif
uip_conn->appstate = NULL;
goto finish;
}
if (uip_acked())
{
#ifdef UIPETHERNET_DEBUG_CLIENT
Serial.println(F("UIPClient uip_acked"));
#endif
UIPClient::_eatBlock(u->packets_out);
goto send;
}
if (uip_poll() || uip_rexmit())
{
#ifdef UIPETHERNET_DEBUG_CLIENT
//Serial.println(F("UIPClient uip_poll"));
#endif
send:
if (u->packets_out[0] != NOBLOCK)
{
if (u->packets_out[1] == NOBLOCK)
{
send_len = u->out_pos;
if (send_len > 0)
{
Enc28J60Network::resizeBlock(u->packets_out[0],0,send_len);
}
}
else
send_len = Enc28J60Network::blockSize(u->packets_out[0]);
if (send_len > 0)
{
UIPEthernetClass::uip_hdrlen = ((uint8_t*)uip_appdata)-uip_buf;
UIPEthernetClass::uip_packet = Enc28J60Network::allocBlock(UIPEthernetClass::uip_hdrlen+send_len + UIP_SENDBUFFER_OFFSET + UIP_SENDBUFFER_PADDING);
if (UIPEthernetClass::uip_packet != NOBLOCK)
{
Enc28J60Network::copyPacket(UIPEthernetClass::uip_packet,UIPEthernetClass::uip_hdrlen + UIP_SENDBUFFER_OFFSET,u->packets_out[0],0,send_len);
UIPEthernetClass::packetstate |= UIPETHERNET_SENDPACKET;
}
}
goto finish;
}
}
// don't close connection unless all outgoing packets are sent
if (u->state & UIP_CLIENT_CLOSE)
{
#ifdef UIPETHERNET_DEBUG_CLIENT
Serial.println(F("UIPClient state UIP_CLIENT_CLOSE"));
UIPClient::_dumpAllData();
#endif
if (u->packets_out[0] == NOBLOCK)
{
u->state = 0;
uip_conn->appstate = NULL;
uip_close();
#ifdef UIPETHERNET_DEBUG_CLIENT
Serial.println(F("no blocks out -> free userdata"));
UIPClient::_dumpAllData();
#endif
}
else
{
uip_stop();
#ifdef UIPETHERNET_DEBUG_CLIENT
Serial.println(F("blocks outstanding transfer -> uip_stop()"));
#endif
}
}
}
finish:
uip_send(uip_appdata,send_len);
uip_len = send_len;
}
uip_userdata_t *
UIPClient::_allocateData()
{
for ( uint8_t sock = 0; sock < UIP_CONNS; sock++ )
{
uip_userdata_t* data = &UIPClient::all_data[sock];
if (!data->state)
{
*data = uip_userdata_t();
data->conn_index = uip_conn - uip_conns; // pointer arithmetics
data->state = UIP_CLIENT_CONNECTED;
return data;
}
}
return NULL;
}
uint8_t
UIPClient::_currentBlock(memhandle* block)
{
for (uint8_t i = 1; i < UIP_SOCKET_NUMPACKETS; i++)
{
if (block[i] == NOBLOCK)
return i-1;
}
return UIP_SOCKET_NUMPACKETS-1;
}
void
UIPClient::_eatBlock(memhandle* block)
{
#ifdef UIPETHERNET_DEBUG_CLIENT
memhandle* start = block;
Serial.print(F("eatblock("));
Serial.print(*block);
Serial.print(F("): "));
for (uint8_t i = 0; i < UIP_SOCKET_NUMPACKETS; i++)
{
Serial.print(start[i]);
Serial.print(F(" "));
}
Serial.print(F("-> "));
#endif
Enc28J60Network::freeBlock(block[0]);
for (uint8_t i = 0; i < UIP_SOCKET_NUMPACKETS-1; i++)
{
block[i] = block[i+1];
}
block[UIP_SOCKET_NUMPACKETS-1] = NOBLOCK;
#ifdef UIPETHERNET_DEBUG_CLIENT
for (uint8_t i = 0; i < UIP_SOCKET_NUMPACKETS; i++)
{
Serial.print(start[i]);
Serial.print(F(" "));
}
Serial.println();
#endif
}
void
UIPClient::_flushBlocks(memhandle* block)
{
for (uint8_t i = 0; i < UIP_SOCKET_NUMPACKETS; i++)
{
Enc28J60Network::freeBlock(block[i]);
block[i] = NOBLOCK;
}
}
#ifdef UIPETHERNET_DEBUG_CLIENT
void
UIPClient::_dumpAllData() {
for (uint8_t i=0; i < UIP_CONNS; i++)
{
if (!all_data[i].state)
continue;
Serial.print(F("UIPClient::all_data["));
Serial.print(i);
Serial.print(F("], state:"));
Serial.println(all_data[i].state, BIN);
struct uip_conn& conn = uip_conns[all_data[i].conn_index];
Serial.println(ip_addr_uip(conn.ripaddr));
Serial.print(F("ix: "));
Serial.print(all_data[i].conn_index);
Serial.print(F(" tcp flags: 0x"));
Serial.print(conn.tcpstateflags, HEX);
Serial.print(F(" retransmission: timer "));
Serial.print(conn.timer);
Serial.print(F(" nrtx "));
Serial.println(conn.nrtx);
Serial.print(F("packets_in: "));
for (uint8_t j=0; j < UIP_SOCKET_NUMPACKETS; j++)
{
Serial.print(all_data[i].packets_in[j]);
Serial.print(F(" "));
}
Serial.println();
if (all_data[i].state & UIP_CLIENT_REMOTECLOSED)
{
Serial.print(F("state remote closed, local port: "));
Serial.println(htons(((uip_userdata_closed_t *)(&all_data[i]))->lport));
}
else
{
Serial.print(F("packets_out: "));
for (uint8_t j=0; j < UIP_SOCKET_NUMPACKETS; j++)
{
Serial.print(all_data[i].packets_out[j]);
Serial.print(F(" "));
}
Serial.println();
Serial.print(F("out_pos: "));
Serial.println(all_data[i].out_pos);
}
Serial.println();
}
for (uint8_t i=0; i < UIP_CONNS; i++)
{
struct uip_conn& conn = uip_conns[i];
Serial.print(i);
Serial.print(' ');
Serial.print(ip_addr_uip(conn.ripaddr));
Serial.print(':');
Serial.print(ntohs(conn.rport));
Serial.print(' ');
Serial.print(ntohs(conn.lport));
Serial.print(F(" tcp flags: 0x"));
Serial.print(conn.tcpstateflags, HEX);
Serial.print(F(" retransmission: timer "));
Serial.print(conn.timer);
Serial.print(F(" nrtx "));
Serial.println(conn.nrtx);
}
}
#endif
@@ -1,110 +0,0 @@
/*
UIPClient.h - Arduino implementation of a uIP wrapper class.
Copyright (c) 2013 Norbert Truchsess <norbert.truchsess@t-online.de>
All rights reserved.
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
#ifndef UIPCLIENT_H
#define UIPCLIENT_H
#include <Client.h>
#include "utility/mempool.h"
extern "C" {
#include "utility/uip.h"
}
#define UIP_SOCKET_DATALEN UIP_TCP_MSS
#define UIP_CLIENT_CONNECTED 0x01
#define UIP_CLIENT_CLOSE 0x02
#define UIP_CLIENT_REMOTECLOSED 0x04
#define UIP_CLIENT_RESTART 0x08
#define UIP_CLIENT_ACCEPTED 0x10
typedef struct {
uint8_t conn_index;
uint8_t state;
memhandle packets_in[UIP_SOCKET_NUMPACKETS];
uint16_t lport; /**< The local TCP port, in network byte order. */
} uip_userdata_closed_t;
typedef struct {
uint8_t conn_index;
uint8_t state;
memhandle packets_in[UIP_SOCKET_NUMPACKETS] = {NOBLOCK};
memhandle packets_out[UIP_SOCKET_NUMPACKETS] = {NOBLOCK};
memaddress out_pos;
} uip_userdata_t;
class EthernetClient : public Client {
public:
EthernetClient();
int connect(IPAddress ip, uint16_t port);
int connect(const char *host, uint16_t port);
int read(uint8_t *buf, size_t size);
void stop();
uint8_t connected();
operator bool();
virtual bool operator==(const EthernetClient&);
virtual bool operator!=(const EthernetClient& rhs) { return !this->operator==(rhs); };
size_t write(uint8_t);
size_t write(const uint8_t *buf, size_t size);
int availableForWrite();
void flush(); // flush sends the buffered data
int available();
int read();
int peek();
void discardReceived();
using Print::write;
IPAddress remoteIP();
uint16_t remotePort();
uint8_t status();
void setConnectionTimeout(uint16_t millis) {connectTimeout = millis;}
private:
EthernetClient(struct uip_conn *_conn);
EthernetClient(uip_userdata_t* conn_data);
uip_userdata_t* data;
uint16_t connectTimeout = 1000 * UIP_CONNECT_TIMEOUT;
static uip_userdata_t all_data[UIP_CONNS];
static uip_userdata_t* _allocateData();
static uint8_t _currentBlock(memhandle* blocks);
static void _eatBlock(memhandle* blocks);
static void _flushBlocks(memhandle* blocks);
#ifdef UIPETHERNET_DEBUG_CLIENT
static void _dumpAllData();
#endif
friend class UIPEthernetClass;
friend class EthernetServer;
friend void uipclient_appcall(void);
};
#endif
@@ -1 +0,0 @@
#include "Ethernet.h"
@@ -1,112 +0,0 @@
/*
UIPServer.cpp - Arduino implementation of a uIP wrapper class.
Copyright (c) 2013 Norbert Truchsess <norbert.truchsess@t-online.de>
All rights reserved.
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
#include "Ethernet.h"
#include "EthernetServer.h"
extern "C" {
#include "utility/uip-conf.h"
}
#define UIPServer EthernetServer // to not pollute source code history with the rename
#define UIPClient EthernetClient
UIPServer::UIPServer(uint16_t port) : _port(htons(port))
{
}
UIPClient UIPServer::available()
{
UIPEthernetClass::tick();
for ( uip_userdata_t* data = &UIPClient::all_data[0]; data < &UIPClient::all_data[UIP_CONNS]; data++ )
{
if (data->packets_in[0] != NOBLOCK
&& (((data->state & UIP_CLIENT_CONNECTED) && uip_conns[data->conn_index].lport ==_port)
|| ((data->state & UIP_CLIENT_REMOTECLOSED) && ((uip_userdata_closed_t *)data)->lport == _port)))
return UIPClient(data);
}
return UIPClient();
}
UIPClient UIPServer::accept()
{
UIPEthernetClass::tick();
for ( uip_userdata_t* data = &UIPClient::all_data[0]; data < &UIPClient::all_data[UIP_CONNS]; data++ )
{
if (!(data->state & UIP_CLIENT_ACCEPTED)
&& (((data->state & UIP_CLIENT_CONNECTED) && uip_conns[data->conn_index].lport ==_port)
|| ((data->state & UIP_CLIENT_REMOTECLOSED) && ((uip_userdata_closed_t *)data)->lport == _port))) {
data->state |= UIP_CLIENT_ACCEPTED;
return UIPClient(data);
}
}
return UIPClient();
}
void UIPServer::begin()
{
uip_listen(_port);
UIPEthernetClass::tick();
listening = true;
}
void UIPServer::begin(uint16_t port)
{
_port = port;
begin();
}
void UIPServer::end() {
uip_unlisten(_port);
listening = false;
for ( uip_userdata_t* data = &UIPClient::all_data[0]; data < &UIPClient::all_data[UIP_CONNS]; data++ )
{
if ((data->state & UIP_CLIENT_CONNECTED) && uip_conns[data->conn_index].lport ==_port)
{
UIPClient client(data);
client.stop();
}
}
}
UIPServer::operator bool() {
return listening;
}
size_t UIPServer::writeToAllClients(const uint8_t *buf, size_t size)
{
size_t ret = 0;
for ( uip_userdata_t* data = &UIPClient::all_data[0]; data < &UIPClient::all_data[UIP_CONNS]; data++ )
{
if ((data->state & UIP_CLIENT_CONNECTED) && uip_conns[data->conn_index].lport ==_port)
{
EthernetClient client(data);
ret = client.write(buf, size);
}
}
return ret;
}
size_t EthernetServerPrint::write(uint8_t c)
{
return write(&c,1);
}
size_t EthernetServerPrint::write(const uint8_t *buf, size_t size)
{
return EthernetServer::writeToAllClients(buf, size);
}
@@ -1,55 +0,0 @@
/*
UIPServer.h - Arduino implementation of a uIP wrapper class.
Copyright (c) 2013 Norbert Truchsess <norbert.truchsess@t-online.de>
All rights reserved.
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
#ifndef UIPSERVER_H
#define UIPSERVER_H
#include "EthernetClient.h"
class EthernetServer {
public:
EthernetServer(uint16_t port = 80);
EthernetClient available();
EthernetClient accept();
void begin();
void begin(uint16_t port);
void end();
operator bool();
protected:
size_t writeToAllClients(const uint8_t *buf, size_t size);
private:
uint16_t _port;
bool listening = false;
};
class EthernetServerPrint : public EthernetServer, public Print {
public:
EthernetServerPrint(uint16_t port = 80) : EthernetServer(port) {}
virtual size_t write(uint8_t);
virtual size_t write(const uint8_t *buf, size_t size);
using Print::write;
};
#endif
@@ -1,424 +0,0 @@
/*
UIPUdp.cpp - Arduino implementation of a uIP wrapper class.
Copyright (c) 2013 Norbert Truchsess <norbert.truchsess@t-online.de>
All rights reserved.
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
#include "Ethernet.h"
#include "EthernetUdp.h"
extern "C" {
#include "utility/uip-conf.h"
#include "utility/uip.h"
#include "utility/uip_arp.h"
}
#if UIP_UDP
#define UIP_ARPHDRSIZE 42
#define UDPBUF ((struct uip_udpip_hdr *)&uip_buf[UIP_LLH_LEN])
#define UIPUDP EthernetUDP // to not pollute source code history with the rename
// Constructor
UIPUDP::UIPUDP() :
_uip_udp_conn(NULL),
appdata()
{
}
// initialize, start listening on specified port. Returns 1 if successful, 0 if there are no sockets available to use
uint8_t
UIPUDP::begin(uint16_t port)
{
if (!_uip_udp_conn)
{
_uip_udp_conn = uip_udp_new(NULL, 0);
}
if (_uip_udp_conn)
{
uip_udp_bind(_uip_udp_conn,htons(port));
_uip_udp_conn->appstate = &appdata;
return 1;
}
return 0;
}
// Finish with the UDP socket
void
UIPUDP::stop()
{
if (_uip_udp_conn)
{
uip_udp_remove(_uip_udp_conn);
_uip_udp_conn->appstate = NULL;
_uip_udp_conn=NULL;
Enc28J60Network::freeBlock(appdata.packet_in);
_flushBlocks(appdata.packet_next);
Enc28J60Network::freeBlock(appdata.packet_out);
appdata = uip_udp_userdata_t();
}
}
// Sending UDP packets
// Start building up a packet to send to the remote host specific in ip and port
// Returns 1 if successful, 0 if there was a problem with the supplied IP address or port
int
UIPUDP::beginPacket(IPAddress ip, uint16_t port)
{
UIPEthernetClass::tick();
if ((ip[0] || ip[1] || ip[2] || ip[3]) && port)
{
uip_ipaddr_t ripaddr;
uip_ip_addr(&ripaddr, ip);
#ifdef UIPETHERNET_DEBUG_UDP
Serial.print(F("udp beginPacket, "));
#endif
if (_uip_udp_conn)
{
// [J.A]: for UDP server, setting ripaddr and rport causes in uIP filtering
// of incoming messages. it would be better to store the IP and port in fields
// and set them only in endPacket().
_uip_udp_conn->rport = htons(port);
uip_ipaddr_copy(_uip_udp_conn->ripaddr, &ripaddr);
}
else
{
_uip_udp_conn = uip_udp_new(&ripaddr,htons(port));
if (_uip_udp_conn)
{
#ifdef UIPETHERNET_DEBUG_UDP
Serial.print(F("new connection, "));
#endif
_uip_udp_conn->appstate = &appdata;
}
else
{
#ifdef UIPETHERNET_DEBUG_UDP
Serial.println(F("failed to allocate new connection"));
#endif
return 0;
}
}
#ifdef UIPETHERNET_DEBUG_UDP
Serial.print(F("rip: "));
Serial.print(ip);
Serial.print(F(", port: "));
Serial.println(port);
#endif
}
if (_uip_udp_conn)
{
if (appdata.packet_out == NOBLOCK)
{
appdata.packet_out = Enc28J60Network::allocBlock(UIP_UDP_MAXPACKETSIZE + UIP_SENDBUFFER_OFFSET + UIP_SENDBUFFER_PADDING);
appdata.out_pos = UIP_UDP_PHYH_LEN + UIP_SENDBUFFER_OFFSET;
if (appdata.packet_out != NOBLOCK)
return 1;
#ifdef UIPETHERNET_DEBUG_UDP
else
Serial.println(F("failed to allocate memory for packet"));
#endif
}
#ifdef UIPETHERNET_DEBUG_UDP
else
Serial.println(F("previous packet on that connection not sent yet"));
#endif
}
return 0;
}
// Start building up a packet to send to the remote host specific in host and port
// Returns 1 if successful, 0 if there was a problem resolving the hostname or port
int
UIPUDP::beginPacket(const char *host, uint16_t port)
{
// Look up the host first
int ret = 0;
IPAddress remote_addr;
ret = Ethernet.hostByName(host, remote_addr);
if (ret == 1) {
return beginPacket(remote_addr, port);
} else {
return ret;
}
}
// Finish off this packet and send it
// Returns 1 if the packet was sent successfully, 0 if there was an error
int
UIPUDP::endPacket()
{
if (_uip_udp_conn && appdata.packet_out != NOBLOCK)
{
appdata.send = true;
Enc28J60Network::resizeBlock(appdata.packet_out,0,appdata.out_pos + UIP_SENDBUFFER_PADDING);
uip_udp_periodic_conn(_uip_udp_conn);
if (uip_len > 0)
{
_send(_uip_udp_conn);
return 1;
}
}
return 0;
}
// Write a single byte into the packet
size_t
UIPUDP::write(uint8_t c)
{
return write(&c,1);
}
// Write size bytes from buffer into the packet
size_t
UIPUDP::write(const uint8_t *buffer, size_t size)
{
if (appdata.packet_out != NOBLOCK)
{
size_t ret = Enc28J60Network::writePacket(appdata.packet_out,appdata.out_pos,(uint8_t*)buffer,size);
appdata.out_pos += ret;
return ret;
}
return 0;
}
// Start processing the next available incoming packet
// Returns the size of the packet in bytes, or 0 if no packets are available
int
UIPUDP::parsePacket()
{
UIPEthernetClass::tick();
#ifdef UIPETHERNET_DEBUG_UDP
if (appdata.packet_in != NOBLOCK)
{
Serial.print(F("udp parsePacket freeing previous packet: "));
Serial.println(appdata.packet_in);
}
#endif
Enc28J60Network::freeBlock(appdata.packet_in);
appdata.packet_in = appdata.packet_next[0].packet;
uip_ipaddr_copy(appdata.remote_ip, appdata.packet_next[0].remote_ip);
appdata.remote_port = appdata.packet_next[0].remote_port;
_moveBlocks(appdata.packet_next);
#ifdef UIPETHERNET_DEBUG_UDP
if (appdata.packet_in != NOBLOCK)
{
Serial.print(F("udp parsePacket received packet: "));
Serial.print(appdata.packet_in);
}
#endif
int size = Enc28J60Network::blockSize(appdata.packet_in);
#ifdef UIPETHERNET_DEBUG_UDP
if (appdata.packet_in != NOBLOCK)
{
Serial.print(F(", size: "));
Serial.println(size);
}
#endif
return size;
}
// Number of bytes remaining in the current packet
int
UIPUDP::available()
{
UIPEthernetClass::tick();
return Enc28J60Network::blockSize(appdata.packet_in);
}
// Read a single byte from the current packet
int
UIPUDP::read()
{
unsigned char c;
if (read(&c,1) > 0)
{
return c;
}
return -1;
}
// Read up to len bytes from the current packet and place them into buffer
// Returns the number of bytes read, or 0 if none are available
int
UIPUDP::read(unsigned char* buffer, size_t len)
{
UIPEthernetClass::tick();
if (appdata.packet_in != NOBLOCK)
{
memaddress read = Enc28J60Network::readPacket(appdata.packet_in,0,buffer,len);
if (read == Enc28J60Network::blockSize(appdata.packet_in))
{
Enc28J60Network::freeBlock(appdata.packet_in);
appdata.packet_in = NOBLOCK;
}
else
Enc28J60Network::resizeBlock(appdata.packet_in,read);
return read;
}
return 0;
}
// Return the next byte from the current packet without moving on to the next byte
int
UIPUDP::peek()
{
UIPEthernetClass::tick();
if (appdata.packet_in != NOBLOCK)
{
unsigned char c;
if (Enc28J60Network::readPacket(appdata.packet_in,0,&c,1) == 1)
return c;
}
return -1;
}
// Finish reading the current packet
void
UIPUDP::discardReceived()
{
UIPEthernetClass::tick();
Enc28J60Network::freeBlock(appdata.packet_in);
appdata.packet_in = NOBLOCK;
}
// Return the IP address of the host who sent the current incoming packet
IPAddress
UIPUDP::remoteIP()
{
return ip_addr_uip(appdata.remote_ip);
}
// Return the port of the host who sent the current incoming packet
uint16_t
UIPUDP::remotePort()
{
return ntohs(appdata.remote_port);
}
// uIP callback function
void
uipudp_appcall(void) {
if (uip_udp_userdata_t *data = (uip_udp_userdata_t *)(uip_udp_conn->appstate))
{
if (uip_newdata())
{
uint8_t p = UIPUDP::_newBlock(data->packet_next);
if (data->packet_next[p].packet == NOBLOCK)
{
data->packet_next[p].remote_port = UDPBUF->srcport;
uip_ipaddr_copy( data->packet_next[p].remote_ip,UDPBUF->srcipaddr);
data->packet_next[p].packet = Enc28J60Network::allocBlock(ntohs(UDPBUF->udplen)-UIP_UDPH_LEN);
//if we are unable to allocate memory the packet is dropped. udp doesn't guarantee packet delivery
if (data->packet_next[p].packet != NOBLOCK)
{
//discard Linklevel and IP and udp-header and any trailing bytes:
Enc28J60Network::copyPacket(data->packet_next[p].packet,0,UIPEthernetClass::in_packet,UIP_UDP_PHYH_LEN,Enc28J60Network::blockSize(data->packet_next[p].packet));
#ifdef UIPETHERNET_DEBUG_UDP
Serial.print(F("udp, uip_newdata received packet: "));
Serial.print(data->packet_next[p].packet);
Serial.print(F(", size: "));
Serial.println(Enc28J60Network::blockSize(data->packet_next[p].packet));
#endif
}
}
}
if (uip_poll() && data->send)
{
//set uip_slen (uip private) by calling uip_udp_send
#ifdef UIPETHERNET_DEBUG_UDP
Serial.print(F("udp, uip_poll preparing packet to send: "));
Serial.print(data->packet_out);
Serial.print(F(", size: "));
Serial.println(Enc28J60Network::blockSize(data->packet_out));
#endif
UIPEthernetClass::uip_packet = data->packet_out;
UIPEthernetClass::packetstate |= UIPETHERNET_SENDPACKET;
UIPEthernetClass::uip_hdrlen = UIP_UDP_PHYH_LEN;
uip_udp_send(data->out_pos - (UIP_UDP_PHYH_LEN + UIP_SENDBUFFER_OFFSET));
}
}
}
void
UIPUDP::_send(struct uip_udp_conn *uip_udp_conn) {
uip_arp_out(); //add arp
if (uip_len == UIP_ARPHDRSIZE)
{
UIPEthernetClass::uip_packet = NOBLOCK;
UIPEthernetClass::packetstate &= ~UIPETHERNET_SENDPACKET;
#ifdef UIPETHERNET_DEBUG_UDP
Serial.println(F("udp, uip_poll results in ARP-packet"));
#endif
UIPEthernetClass::network_send();
}
else
//arp found ethaddr for ip (otherwise packet is replaced by arp-request)
{
uip_udp_userdata_t* data = (uip_udp_userdata_t *)(uip_udp_conn->appstate);
UIPEthernetClass::network_send();
data->send = false;
data->packet_out = NOBLOCK;
// [J.A] a listening UDP port in uIP filters received messages
// if rport and ripaddr are set. so we better clear them
uip_udp_conn->rport = 0;
uip_udp_conn->ripaddr[0] = 0;
uip_udp_conn->ripaddr[1] = 0;
#ifdef UIPETHERNET_DEBUG_UDP
Serial.print(F("udp, uip_packet to send: "));
Serial.println(UIPEthernetClass::uip_packet);
#endif
}
}
uint8_t
UIPUDP::_newBlock(uip_udp_msg_rec_t* block)
{
for (uint8_t i = 0; i < UIP_UDP_BACKLOG; i++)
{
if (block[i].packet == NOBLOCK)
return i;
}
return UIP_UDP_BACKLOG-1;
}
void
UIPUDP::_moveBlocks(uip_udp_msg_rec_t* block)
{
for (uint8_t i = 0; i < UIP_UDP_BACKLOG-1; i++)
{
block[i] = block[i+1];
}
block[UIP_UDP_BACKLOG-1].packet = NOBLOCK;
}
void
UIPUDP::_flushBlocks(uip_udp_msg_rec_t* block)
{
for (uint8_t i = 0; i < UIP_UDP_BACKLOG; i++)
{
Enc28J60Network::freeBlock(block[i].packet);
block[i].packet = NOBLOCK;
}
}
#endif
@@ -1,139 +0,0 @@
/*
UIPUdp.h - Arduino implementation of a uIP wrapper class.
Copyright (c) 2013 Norbert Truchsess <norbert.truchsess@t-online.de>
All rights reserved.
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
#ifndef UIPUDP_H
#define UIPUDP_H
#include <Arduino.h>
#include <Udp.h>
#include "utility/mempool.h"
extern "C" {
#include "utility/uip.h"
}
#define UIP_UDP_MAXDATALEN 1500
#define UIP_UDP_PHYH_LEN UIP_LLH_LEN+UIP_IPUDPH_LEN
#define UIP_UDP_MAXPACKETSIZE UIP_UDP_MAXDATALEN+UIP_UDP_PHYH_LEN
typedef struct {
memhandle packet = NOBLOCK;
uip_ipaddr_t remote_ip;
uint16_t remote_port = 0;
} uip_udp_msg_rec_t;
typedef struct {
memaddress out_pos;
uip_udp_msg_rec_t packet_next[UIP_UDP_BACKLOG];
memhandle packet_in = NOBLOCK;
memhandle packet_out = NOBLOCK;
boolean send;
uip_ipaddr_t remote_ip;
uint16_t remote_port = 0;
} uip_udp_userdata_t;
class EthernetUDP : public UDP
{
private:
struct uip_udp_conn *_uip_udp_conn;
uip_udp_userdata_t appdata;
public:
EthernetUDP(); // Constructor
uint8_t
begin(uint16_t);// initialize, start listening on specified port. Returns 1 if successful, 0 if there are no sockets available to use
void
stop(); // Finish with the UDP socket
// Sending UDP packets
// Start building up a packet to send to the remote host specific in ip and port
// Returns 1 if successful, 0 if there was a problem with the supplied IP address or port
int
beginPacket(IPAddress ip, uint16_t port);
// Start building up a packet to send to the remote host specific in host and port
// Returns 1 if successful, 0 if there was a problem resolving the hostname or port
int
beginPacket(const char *host, uint16_t port);
// Finish off this packet and send it
// Returns 1 if the packet was sent successfully, 0 if there was an error
int
endPacket();
// Write a single byte into the packet
size_t
write(uint8_t);
// Write size bytes from buffer into the packet
size_t
write(const uint8_t *buffer, size_t size);
// flush() should send the data, but here endPacket does it
void
flush() {}; // in Print it is empty, UDP makes it pure virtual
using Print::write;
// Start processing the next available incoming packet
// Returns the size of the packet in bytes, or 0 if no packets are available
int
parsePacket();
// Number of bytes remaining in the current packet
int
available();
// Read a single byte from the current packet
int
read();
// Read up to len bytes from the current packet and place them into buffer
// Returns the number of bytes read, or 0 if none are available
int
read(unsigned char* buffer, size_t len);
// Read up to len characters from the current packet and place them into buffer
// Returns the number of characters read, or 0 if none are available
int
read(char* buffer, size_t len)
{
return read((unsigned char*) buffer, len);
}
;
// Return the next byte from the current packet without moving on to the next byte
int
peek();
void
discardReceived(); // former flush
// Return the IP address of the host who sent the current incoming packet
IPAddress
remoteIP();
// Return the port of the host who sent the current incoming packet
uint16_t
remotePort();
private:
friend void uipudp_appcall(void);
friend class UIPEthernetClass;
static void _send(struct uip_udp_conn *uip_udp_conn);
static uint8_t _newBlock(uip_udp_msg_rec_t* blocks);
static void _moveBlocks(uip_udp_msg_rec_t* blocks);
static void _flushBlocks(uip_udp_msg_rec_t* blocks);
};
#endif
@@ -1,21 +0,0 @@
#ifndef UIP_TCP_STATES_H
#define UIP_TCP_STATES_H
#include <utility/uip.h>
// common constants for client.state() return values
enum uip_tcp_state {
CLOSED = UIP_CLOSED,
SYN_SENT = UIP_SYN_SENT,
SYN_RCVD = UIP_SYN_RCVD,
ESTABLISHED = UIP_ESTABLISHED,
FIN_WAIT_1 = UIP_FIN_WAIT_1,
FIN_WAIT_2 = UIP_FIN_WAIT_2,
CLOSE_WAIT = 10, // not used
CLOSING = UIP_CLOSING,
LAST_ACK = UIP_LAST_ACK,
TIME_WAIT = UIP_TIME_WAIT
};
#endif
@@ -1,391 +0,0 @@
/*
Enc28J60NetworkClass.cpp — Bitbang-SPI version for non-hardware-SPI pins
Based on UIPEthernet by Norbert Truchsess, GPL V2
Modification: Hardware SPI replaced with bitbang SPI.
Target: STM32F103RBT6 with ENC28J60 on PC6(SCK)/PC7(MISO)/PC8(CS)/PC9(MOSI)
*/
#include "Enc28J60Network.h"
#include <Arduino.h>
#include "config.h" // ETH_SCK_PIN / ETH_MOSI_PIN / ETH_MISO_PIN
// Note: <SPI.h> intentionally NOT included — this version uses bitbang SPI
extern "C" {
#include "enc28j60.h"
#include "uip.h"
}
// ============================================================
// Bitbang SPI — SPI Mode 0 (CPOL=0, CPHA=0)
// Pin definitions come from project include/config.h via build system
// ============================================================
#ifndef ETH_SCK_PIN
#error "ETH_SCK_PIN not defined — check include/config.h"
#endif
static bool _spi_ready = false;
static void bitbang_init() {
if (_spi_ready) return;
pinMode(ETH_SCK_PIN, OUTPUT); digitalWrite(ETH_SCK_PIN, LOW);
pinMode(ETH_MOSI_PIN, OUTPUT); digitalWrite(ETH_MOSI_PIN, LOW);
pinMode(ETH_MISO_PIN, INPUT);
_spi_ready = true;
}
static inline uint8_t bitbang_transfer(uint8_t out) {
uint8_t in = 0;
for (int8_t i = 7; i >= 0; i--) {
digitalWrite(ETH_MOSI_PIN, (out >> i) & 1);
digitalWrite(ETH_SCK_PIN, HIGH);
in = (in << 1) | digitalRead(ETH_MISO_PIN);
digitalWrite(ETH_SCK_PIN, LOW);
}
return in;
}
// ============================================================
// set CS to 0 = active
#define CSACTIVE digitalWrite(csPin, LOW)
// set CS to 1 = passive
#define CSPASSIVE digitalWrite(csPin, HIGH)
bool Enc28J60Network::spiInitialized = false;
uint8_t Enc28J60Network::csPin = SS;
uint16_t Enc28J60Network::nextPacketPtr;
uint8_t Enc28J60Network::bank = 0xff;
struct memblock Enc28J60Network::receivePkt;
void Enc28J60Network::initSPI()
{
if (spiInitialized)
return;
bitbang_init();
pinMode(csPin, OUTPUT);
CSPASSIVE;
spiInitialized = true;
}
bool Enc28J60Network::init(uint8_t* macaddr)
{
MemoryPool::init();
initSPI();
// perform system reset
writeOp(ENC28J60_SOFT_RESET, 0, ENC28J60_SOFT_RESET);
delay(50);
nextPacketPtr = RXSTART_INIT;
writeRegPair(ERXSTL, RXSTART_INIT);
writeRegPair(ERXRDPTL, RXSTART_INIT);
writeRegPair(ERXNDL, RXSTOP_INIT);
writeReg(ERXFCON, ERXFCON_UCEN|ERXFCON_CRCEN|ERXFCON_PMEN);
writeRegPair(EPMM0, 0x303f);
writeRegPair(EPMCSL, 0xf7f9);
writeRegPair(MACON1, MACON1_MARXEN|MACON1_TXPAUS|MACON1_RXPAUS);
writeOp(ENC28J60_BIT_FIELD_SET, MACON3, MACON3_PADCFG0|MACON3_TXCRCEN|MACON3_FRMLNEN);
writeRegPair(MAIPGL, 0x0C12);
writeReg(MABBIPG, 0x12);
writeRegPair(MAMXFLL, MAX_FRAMELEN);
writeReg(MAADR5, macaddr[0]);
writeReg(MAADR4, macaddr[1]);
writeReg(MAADR3, macaddr[2]);
writeReg(MAADR2, macaddr[3]);
writeReg(MAADR1, macaddr[4]);
writeReg(MAADR0, macaddr[5]);
phyWrite(PHCON2, PHCON2_HDLDIS);
setBank(ECON1);
writeOp(ENC28J60_BIT_FIELD_SET, EIE, EIE_INTIE|EIE_PKTIE);
writeOp(ENC28J60_BIT_FIELD_SET, ECON1, ECON1_RXEN);
phyWrite(PHLCON, 0x476);
return getrev();
}
memhandle Enc28J60Network::receivePacket()
{
uint8_t rxstat;
uint16_t len;
if (readReg(EPKTCNT) != 0)
{
uint16_t readPtr = nextPacketPtr+6 > RXSTOP_INIT ?
nextPacketPtr+6-((RXSTOP_INIT + 1)-RXSTART_INIT) : nextPacketPtr+6;
writeRegPair(ERDPTL, nextPacketPtr);
nextPacketPtr = readOp(ENC28J60_READ_BUF_MEM, 0);
nextPacketPtr |= readOp(ENC28J60_READ_BUF_MEM, 0) << 8;
len = readOp(ENC28J60_READ_BUF_MEM, 0);
len |= readOp(ENC28J60_READ_BUF_MEM, 0) << 8;
len -= 4;
rxstat = readOp(ENC28J60_READ_BUF_MEM, 0);
writeOp(ENC28J60_BIT_FIELD_SET, ECON2, ECON2_PKTDEC);
if ((rxstat & 0x80) != 0)
{
receivePkt.begin = readPtr;
receivePkt.size = len;
return UIP_RECEIVEBUFFERHANDLE;
}
setERXRDPT();
}
return (NOBLOCK);
}
void Enc28J60Network::setERXRDPT()
{
writeRegPair(ERXRDPTL, nextPacketPtr == RXSTART_INIT ? RXSTOP_INIT : nextPacketPtr-1);
}
memaddress Enc28J60Network::blockSize(memhandle handle)
{
return handle == NOBLOCK ? 0 :
handle == UIP_RECEIVEBUFFERHANDLE ? receivePkt.size : blocks[handle].size;
}
bool Enc28J60Network::sendPacket(memhandle handle)
{
memblock *packet = &blocks[handle];
uint16_t start = packet->begin;
uint16_t end = start + packet->size - 1 - UIP_SENDBUFFER_PADDING;
writeByte(start, 0);
writeRegPair(ETXSTL, start);
writeRegPair(ETXNDL, end);
bool success = false;
for (uint8_t retry = 0; retry < TX_COLLISION_RETRY_COUNT; retry++)
{
writeOp(ENC28J60_BIT_FIELD_SET, ECON1, ECON1_TXRST);
writeOp(ENC28J60_BIT_FIELD_CLR, ECON1, ECON1_TXRST);
writeOp(ENC28J60_BIT_FIELD_CLR, EIR, EIR_TXERIF | EIR_TXIF);
writeOp(ENC28J60_BIT_FIELD_SET, ECON1, ECON1_TXRTS);
uint8_t eir;
while (((eir = readReg(EIR)) & (EIR_TXIF | EIR_TXERIF)) == 0);
writeOp(ENC28J60_BIT_FIELD_CLR, ECON1, ECON1_TXRTS);
success = ((eir & EIR_TXERIF) == 0);
if (success) break;
uint8_t tsv4 = readByte(end + 4);
if (!(tsv4 & 0b00100000)) break;
}
return success;
}
uint16_t Enc28J60Network::setReadPtr(memhandle handle, memaddress position, uint16_t len)
{
memblock *packet = handle == UIP_RECEIVEBUFFERHANDLE ? &receivePkt : &blocks[handle];
memaddress start = handle == UIP_RECEIVEBUFFERHANDLE &&
packet->begin + position > RXSTOP_INIT ?
packet->begin + position-((RXSTOP_INIT + 1)-RXSTART_INIT) :
packet->begin + position;
writeRegPair(ERDPTL, start);
if (len > packet->size - position) len = packet->size - position;
return len;
}
uint16_t Enc28J60Network::readPacket(memhandle handle, memaddress position, uint8_t* buffer, uint16_t len)
{
len = setReadPtr(handle, position, len);
readBuffer(len, buffer);
return len;
}
uint16_t Enc28J60Network::writePacket(memhandle handle, memaddress position, uint8_t* buffer, uint16_t len)
{
memblock *packet = &blocks[handle];
uint16_t start = packet->begin + position;
if (len > packet->size - position) len = packet->size - position;
if (len == 0) return 0;
writeRegPair(EWRPTL, start);
writeBuffer(len, buffer);
return len;
}
uint8_t Enc28J60Network::readByte(uint16_t addr)
{
writeRegPair(ERDPTL, addr);
CSACTIVE;
bitbang_transfer(ENC28J60_READ_BUF_MEM);
uint8_t c = bitbang_transfer(0x00);
CSPASSIVE;
return c;
}
void Enc28J60Network::writeByte(uint16_t addr, uint8_t data)
{
writeRegPair(EWRPTL, addr);
CSACTIVE;
bitbang_transfer(ENC28J60_WRITE_BUF_MEM);
bitbang_transfer(data);
CSPASSIVE;
}
void Enc28J60Network::copyPacket(memhandle dest_pkt, memaddress dest_pos, memhandle src_pkt, memaddress src_pos, uint16_t len)
{
memblock *dest = &blocks[dest_pkt];
memblock *src = src_pkt == UIP_RECEIVEBUFFERHANDLE ? &receivePkt : &blocks[src_pkt];
memaddress start = src_pkt == UIP_RECEIVEBUFFERHANDLE && src->begin + src_pos > RXSTOP_INIT ?
src->begin + src_pos - ((RXSTOP_INIT + 1) - RXSTART_INIT) : src->begin + src_pos;
enc28J60_mempool_block_move_callback(dest->begin + dest_pos, start, len);
}
void enc28J60_mempool_block_move_callback(memaddress dest, memaddress src, memaddress len)
{
if (len == 1)
{
Enc28J60Network::writeByte(dest, Enc28J60Network::readByte(src));
}
else
{
len += src - 1;
Enc28J60Network::writeRegPair(EDMASTL, src);
Enc28J60Network::writeRegPair(EDMADSTL, dest);
if ((src <= RXSTOP_INIT) && (len > RXSTOP_INIT))
len -= ((RXSTOP_INIT + 1)-RXSTART_INIT);
Enc28J60Network::writeRegPair(EDMANDL, len);
Enc28J60Network::writeOp(ENC28J60_BIT_FIELD_CLR, ECON1, ECON1_CSUMEN);
Enc28J60Network::writeOp(ENC28J60_BIT_FIELD_SET, ECON1, ECON1_DMAST);
while (Enc28J60Network::readOp(ENC28J60_READ_CTRL_REG, ECON1) & ECON1_DMAST);
}
}
void Enc28J60Network::freePacket()
{
setERXRDPT();
}
uint8_t Enc28J60Network::readOp(uint8_t op, uint8_t address)
{
CSACTIVE;
bitbang_transfer(op | (address & ADDR_MASK));
if (address & 0x80) bitbang_transfer(0x00); // dummy read for MAC/MII
uint8_t c = bitbang_transfer(0x00);
CSPASSIVE;
return c;
}
void Enc28J60Network::writeOp(uint8_t op, uint8_t address, uint8_t data)
{
CSACTIVE;
bitbang_transfer(op | (address & ADDR_MASK));
bitbang_transfer(data);
CSPASSIVE;
}
void Enc28J60Network::readBuffer(uint16_t len, uint8_t* data)
{
CSACTIVE;
bitbang_transfer(ENC28J60_READ_BUF_MEM);
while (len--) *data++ = bitbang_transfer(0x00);
CSPASSIVE;
}
void Enc28J60Network::writeBuffer(uint16_t len, uint8_t* data)
{
CSACTIVE;
bitbang_transfer(ENC28J60_WRITE_BUF_MEM);
while (len--) bitbang_transfer(*data++);
CSPASSIVE;
}
void Enc28J60Network::setBank(uint8_t address)
{
if ((address & BANK_MASK) != bank)
{
writeOp(ENC28J60_BIT_FIELD_CLR, ECON1, (ECON1_BSEL1|ECON1_BSEL0));
writeOp(ENC28J60_BIT_FIELD_SET, ECON1, (address & BANK_MASK)>>5);
bank = (address & BANK_MASK);
}
}
uint8_t Enc28J60Network::readReg(uint8_t address)
{
setBank(address);
return readOp(ENC28J60_READ_CTRL_REG, address);
}
void Enc28J60Network::writeReg(uint8_t address, uint8_t data)
{
setBank(address);
writeOp(ENC28J60_WRITE_CTRL_REG, address, data);
}
void Enc28J60Network::writeRegPair(uint8_t address, uint16_t data)
{
setBank(address);
writeOp(ENC28J60_WRITE_CTRL_REG, address, (data & 0xFF));
writeOp(ENC28J60_WRITE_CTRL_REG, address+1, (data >> 8));
}
void Enc28J60Network::phyWrite(uint8_t address, uint16_t data)
{
writeReg(MIREGADR, address);
writeRegPair(MIWRL, data);
while (readReg(MISTAT) & MISTAT_BUSY) delayMicroseconds(15);
}
uint16_t Enc28J60Network::phyRead(uint8_t address)
{
writeReg(MIREGADR, address);
writeReg(MICMD, MICMD_MIIRD);
while (readReg(MISTAT) & MISTAT_BUSY) delayMicroseconds(15);
writeReg(MICMD, 0);
return (readReg(MIRDL) | readReg(MIRDH) << 8);
}
void Enc28J60Network::clkout(uint8_t clk)
{
writeReg(ECOCON, clk & 0x7);
}
uint8_t Enc28J60Network::getrev(void)
{
initSPI();
return readReg(EREVID);
}
uint16_t Enc28J60Network::chksum(uint16_t sum, memhandle handle, memaddress pos, uint16_t len)
{
uint16_t t;
len = setReadPtr(handle, pos, len) - 1;
CSACTIVE;
bitbang_transfer(ENC28J60_READ_BUF_MEM);
uint16_t i;
for (i = 0; i < len; i += 2)
{
t = bitbang_transfer(0x00) << 8;
t += bitbang_transfer(0x00);
sum += t;
if (sum < t) sum++;
}
if (i == len)
{
t = (bitbang_transfer(0x00) << 8) + 0;
sum += t;
if (sum < t) sum++;
}
CSPASSIVE;
return sum;
}
void Enc28J60Network::powerOff()
{
writeOp(ENC28J60_BIT_FIELD_CLR, ECON1, ECON1_RXEN);
delay(50);
writeOp(ENC28J60_BIT_FIELD_SET, ECON2, ECON2_VRPS);
delay(50);
writeOp(ENC28J60_BIT_FIELD_SET, ECON2, ECON2_PWRSV);
}
void Enc28J60Network::powerOn()
{
writeOp(ENC28J60_BIT_FIELD_CLR, ECON2, ECON2_PWRSV);
delay(50);
writeOp(ENC28J60_BIT_FIELD_SET, ECON1, ECON1_RXEN);
delay(50);
}
bool Enc28J60Network::linkStatus()
{
return (phyRead(PHSTAT2) & 0x0400) > 0;
}
@@ -1,94 +0,0 @@
/*
Enc28J60NetworkClass.h
UIPEthernet network driver for Microchip ENC28J60 Ethernet Interface.
Copyright (c) 2013 Norbert Truchsess <norbert.truchsess@t-online.de>
All rights reserved.
inspired by enc28j60.c file from the AVRlib library by Pascal Stang.
For AVRlib See http://www.procyonengineering.com/
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
#ifndef Enc28J60Network_H_
#define Enc28J60Network_H_
#include "mempool.h"
#define UIP_RECEIVEBUFFERHANDLE 0xff
#define UIP_SENDBUFFER_PADDING 7
#define UIP_SENDBUFFER_OFFSET 1
#define TX_COLLISION_RETRY_COUNT 3
//#define ENC28J60DEBUG
/*
* Empfangen von ip-header, arp etc...
* wenn tcp/udp -> tcp/udp-callback -> assign new packet to connection
*/
class Enc28J60Network : public MemoryPool
{
private:
static uint8_t csPin;
static bool spiInitialized;
static uint16_t nextPacketPtr;
static uint8_t bank;
static struct memblock receivePkt;
static uint8_t readOp(uint8_t op, uint8_t address);
static void writeOp(uint8_t op, uint8_t address, uint8_t data);
static uint16_t setReadPtr(memhandle handle, memaddress position, uint16_t len);
static void setERXRDPT();
static void readBuffer(uint16_t len, uint8_t* data);
static void writeBuffer(uint16_t len, uint8_t* data);
static uint8_t readByte(uint16_t addr);
static void writeByte(uint16_t addr, uint8_t data);
static void setBank(uint8_t address);
static uint8_t readReg(uint8_t address);
static void writeReg(uint8_t address, uint8_t data);
static void writeRegPair(uint8_t address, uint16_t data);
static void phyWrite(uint8_t address, uint16_t data);
static uint16_t phyRead(uint8_t address);
static void clkout(uint8_t clk);
friend void enc28J60_mempool_block_move_callback(memaddress,memaddress,memaddress);
public:
static uint8_t getrev(void);
static void powerOn();
static void powerOff();
static bool linkStatus();
static void setCsPin(uint8_t _csPin) {csPin = _csPin;}
static void initSPI();
static bool init(uint8_t* macaddr);
static memhandle receivePacket();
static void freePacket();
static memaddress blockSize(memhandle handle);
static bool sendPacket(memhandle handle);
static uint16_t readPacket(memhandle handle, memaddress position, uint8_t* buffer, uint16_t len);
static uint16_t writePacket(memhandle handle, memaddress position, uint8_t* buffer, uint16_t len);
static void copyPacket(memhandle dest, memaddress dest_pos, memhandle src, memaddress src_pos, uint16_t len);
static uint16_t chksum(uint16_t sum, memhandle handle, memaddress pos, uint16_t len);
};
#endif /* Enc28J60NetworkClass_H_ */
@@ -1,257 +0,0 @@
/*****************************************************************************
*
* Title : Microchip ENC28J60 Ethernet Interface Driver
* Author : Pascal Stang (c)2005
* Modified by Norbert Truchsess
* Copyright: GPL V2
*
*This driver provides initialization and transmit/receive
*functions for the Microchip ENC28J60 10Mb Ethernet Controller and PHY.
*This chip is novel in that it is a full MAC+PHY interface all in a 28-pin
*chip, using an SPI interface to the host processor.
*
*
*****************************************************************************/
#ifndef ENC28J60_H
#define ENC28J60_H
#include <inttypes.h>
// ENC28J60 Control Registers
// Control register definitions are a combination of address,
// bank number, and Ethernet/MAC/PHY indicator bits.
// - Register address (bits 0-4)
// - Bank number (bits 5-6)
// - MAC/PHY indicator (bit 7)
#define ADDR_MASK 0x1F
#define BANK_MASK 0x60
#define SPRD_MASK 0x80
// All-bank registers
#define EIE 0x1B
#define EIR 0x1C
#define ESTAT 0x1D
#define ECON2 0x1E
#define ECON1 0x1F
// Bank 0 registers
#define ERDPTL (0x00|0x00)
#define ERDPTH (0x01|0x00)
#define EWRPTL (0x02|0x00)
#define EWRPTH (0x03|0x00)
#define ETXSTL (0x04|0x00)
#define ETXSTH (0x05|0x00)
#define ETXNDL (0x06|0x00)
#define ETXNDH (0x07|0x00)
#define ERXSTL (0x08|0x00)
#define ERXSTH (0x09|0x00)
#define ERXNDL (0x0A|0x00)
#define ERXNDH (0x0B|0x00)
#define ERXRDPTL (0x0C|0x00)
#define ERXRDPTH (0x0D|0x00)
#define ERXWRPTL (0x0E|0x00)
#define ERXWRPTH (0x0F|0x00)
#define EDMASTL (0x10|0x00)
#define EDMASTH (0x11|0x00)
#define EDMANDL (0x12|0x00)
#define EDMANDH (0x13|0x00)
#define EDMADSTL (0x14|0x00)
#define EDMADSTH (0x15|0x00)
#define EDMACSL (0x16|0x00)
#define EDMACSH (0x17|0x00)
// Bank 1 registers
#define EHT0 (0x00|0x20)
#define EHT1 (0x01|0x20)
#define EHT2 (0x02|0x20)
#define EHT3 (0x03|0x20)
#define EHT4 (0x04|0x20)
#define EHT5 (0x05|0x20)
#define EHT6 (0x06|0x20)
#define EHT7 (0x07|0x20)
#define EPMM0 (0x08|0x20)
#define EPMM1 (0x09|0x20)
#define EPMM2 (0x0A|0x20)
#define EPMM3 (0x0B|0x20)
#define EPMM4 (0x0C|0x20)
#define EPMM5 (0x0D|0x20)
#define EPMM6 (0x0E|0x20)
#define EPMM7 (0x0F|0x20)
#define EPMCSL (0x10|0x20)
#define EPMCSH (0x11|0x20)
#define EPMOL (0x14|0x20)
#define EPMOH (0x15|0x20)
#define EWOLIE (0x16|0x20)
#define EWOLIR (0x17|0x20)
#define ERXFCON (0x18|0x20)
#define EPKTCNT (0x19|0x20)
// Bank 2 registers
#define MACON1 (0x00|0x40|0x80)
#define MACON2 (0x01|0x40|0x80)
#define MACON3 (0x02|0x40|0x80)
#define MACON4 (0x03|0x40|0x80)
#define MABBIPG (0x04|0x40|0x80)
#define MAIPGL (0x06|0x40|0x80)
#define MAIPGH (0x07|0x40|0x80)
#define MACLCON1 (0x08|0x40|0x80)
#define MACLCON2 (0x09|0x40|0x80)
#define MAMXFLL (0x0A|0x40|0x80)
#define MAMXFLH (0x0B|0x40|0x80)
#define MAPHSUP (0x0D|0x40|0x80)
#define MICON (0x11|0x40|0x80)
#define MICMD (0x12|0x40|0x80)
#define MIREGADR (0x14|0x40|0x80)
#define MIWRL (0x16|0x40|0x80)
#define MIWRH (0x17|0x40|0x80)
#define MIRDL (0x18|0x40|0x80)
#define MIRDH (0x19|0x40|0x80)
// Bank 3 registers
#define MAADR1 (0x00|0x60|0x80)
#define MAADR0 (0x01|0x60|0x80)
#define MAADR3 (0x02|0x60|0x80)
#define MAADR2 (0x03|0x60|0x80)
#define MAADR5 (0x04|0x60|0x80)
#define MAADR4 (0x05|0x60|0x80)
#define EBSTSD (0x06|0x60)
#define EBSTCON (0x07|0x60)
#define EBSTCSL (0x08|0x60)
#define EBSTCSH (0x09|0x60)
#define MISTAT (0x0A|0x60|0x80)
#define EREVID (0x12|0x60)
#define ECOCON (0x15|0x60)
#define EFLOCON (0x17|0x60)
#define EPAUSL (0x18|0x60)
#define EPAUSH (0x19|0x60)
// PHY registers
#define PHCON1 0x00
#define PHSTAT1 0x01
#define PHHID1 0x02
#define PHHID2 0x03
#define PHCON2 0x10
#define PHSTAT2 0x11
#define PHIE 0x12
#define PHIR 0x13
#define PHLCON 0x14
// ENC28J60 ERXFCON Register Bit Definitions
#define ERXFCON_UCEN 0x80
#define ERXFCON_ANDOR 0x40
#define ERXFCON_CRCEN 0x20
#define ERXFCON_PMEN 0x10
#define ERXFCON_MPEN 0x08
#define ERXFCON_HTEN 0x04
#define ERXFCON_MCEN 0x02
#define ERXFCON_BCEN 0x01
// ENC28J60 EIE Register Bit Definitions
#define EIE_INTIE 0x80
#define EIE_PKTIE 0x40
#define EIE_DMAIE 0x20
#define EIE_LINKIE 0x10
#define EIE_TXIE 0x08
#define EIE_WOLIE 0x04
#define EIE_TXERIE 0x02
#define EIE_RXERIE 0x01
// ENC28J60 EIR Register Bit Definitions
#define EIR_PKTIF 0x40
#define EIR_DMAIF 0x20
#define EIR_LINKIF 0x10
#define EIR_TXIF 0x08
#define EIR_WOLIF 0x04
#define EIR_TXERIF 0x02
#define EIR_RXERIF 0x01
// ENC28J60 ESTAT Register Bit Definitions
#define ESTAT_INT 0x80
#define ESTAT_LATECOL 0x10
#define ESTAT_RXBUSY 0x04
#define ESTAT_TXABRT 0x02
#define ESTAT_CLKRDY 0x01
// ENC28J60 ECON2 Register Bit Definitions
#define ECON2_AUTOINC 0x80
#define ECON2_PKTDEC 0x40
#define ECON2_PWRSV 0x20
#define ECON2_VRPS 0x08
// ENC28J60 ECON1 Register Bit Definitions
#define ECON1_TXRST 0x80
#define ECON1_RXRST 0x40
#define ECON1_DMAST 0x20
#define ECON1_CSUMEN 0x10
#define ECON1_TXRTS 0x08
#define ECON1_RXEN 0x04
#define ECON1_BSEL1 0x02
#define ECON1_BSEL0 0x01
// ENC28J60 MACON1 Register Bit Definitions
#define MACON1_LOOPBK 0x10
#define MACON1_TXPAUS 0x08
#define MACON1_RXPAUS 0x04
#define MACON1_PASSALL 0x02
#define MACON1_MARXEN 0x01
// ENC28J60 MACON2 Register Bit Definitions
#define MACON2_MARST 0x80
#define MACON2_RNDRST 0x40
#define MACON2_MARXRST 0x08
#define MACON2_RFUNRST 0x04
#define MACON2_MATXRST 0x02
#define MACON2_TFUNRST 0x01
// ENC28J60 MACON3 Register Bit Definitions
#define MACON3_PADCFG2 0x80
#define MACON3_PADCFG1 0x40
#define MACON3_PADCFG0 0x20
#define MACON3_TXCRCEN 0x10
#define MACON3_PHDRLEN 0x08
#define MACON3_HFRMLEN 0x04
#define MACON3_FRMLNEN 0x02
#define MACON3_FULDPX 0x01
// ENC28J60 MICMD Register Bit Definitions
#define MICMD_MIISCAN 0x02
#define MICMD_MIIRD 0x01
// ENC28J60 MISTAT Register Bit Definitions
#define MISTAT_NVALID 0x04
#define MISTAT_SCAN 0x02
#define MISTAT_BUSY 0x01
// ENC28J60 PHY PHCON1 Register Bit Definitions
#define PHCON1_PRST 0x8000
#define PHCON1_PLOOPBK 0x4000
#define PHCON1_PPWRSV 0x0800
#define PHCON1_PDPXMD 0x0100
// ENC28J60 PHY PHSTAT1 Register Bit Definitions
#define PHSTAT1_PFDPX 0x1000
#define PHSTAT1_PHDPX 0x0800
#define PHSTAT1_LLSTAT 0x0004
#define PHSTAT1_JBSTAT 0x0002
// ENC28J60 PHY PHCON2 Register Bit Definitions
#define PHCON2_FRCLINK 0x4000
#define PHCON2_TXDIS 0x2000
#define PHCON2_JABBER 0x0400
#define PHCON2_HDLDIS 0x0100
// ENC28J60 Packet Control Byte Bit Definitions
#define PKTCTRL_PHUGEEN 0x08
#define PKTCTRL_PPADEN 0x04
#define PKTCTRL_PCRCEN 0x02
#define PKTCTRL_POVERRIDE 0x01
// SPI operation codes
#define ENC28J60_READ_CTRL_REG 0x00
#define ENC28J60_READ_BUF_MEM 0x3A
#define ENC28J60_WRITE_CTRL_REG 0x40
#define ENC28J60_WRITE_BUF_MEM 0x7A
#define ENC28J60_BIT_FIELD_SET 0x80
#define ENC28J60_BIT_FIELD_CLR 0xA0
#define ENC28J60_SOFT_RESET 0xFF
// The RXSTART_INIT should be zero. See Rev. B4 Silicon Errata
// buffer boundaries applied to internal 8K ram
// the entire available packet buffer space is allocated
//
// start with recbuf at 0/
#define RXSTART_INIT 0x0
// receive buffer end. make sure this is an odd value ( See Rev. B1,B4,B5,B7 Silicon Errata 'Memory (Ethernet Buffer)')
#define RXSTOP_INIT (0x1FFF-0x1800)
// start TX buffer RXSTOP_INIT+1
#define TXSTART_INIT (RXSTOP_INIT+1)
// stp TX buffer at end of mem
#define TXSTOP_INIT 0x1FFF
//
// max frame length which the conroller will accept:
#define MAX_FRAMELEN 1500 // (note: maximum ethernet frame length would be 1518)
//#define MAX_FRAMELEN 600
#endif
@@ -1,168 +0,0 @@
/*
mempool.cpp - sleek implementation of a memory pool
Copyright (c) 2013 Norbert Truchsess <norbert.truchsess@t-online.de>
All rights reserved.
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
#include "mempool.h"
#include <string.h>
#define POOLOFFSET 1
struct memblock MemoryPool::blocks[MEMPOOL_NUM_MEMBLOCKS+1];
void
MemoryPool::init()
{
memset(&blocks[0], 0, sizeof(blocks));
blocks[POOLSTART].begin = MEMPOOL_STARTADDRESS;
blocks[POOLSTART].size = 0;
blocks[POOLSTART].nextblock = NOBLOCK;
}
memhandle
MemoryPool::allocBlock(memaddress size)
{
memblock* best = NULL;
memhandle cur = POOLSTART;
memblock* block = &blocks[POOLSTART];
memaddress bestsize = MEMPOOL_SIZE + 1;
do
{
memhandle next = block->nextblock;
memaddress freesize = ( next == NOBLOCK ? blocks[POOLSTART].begin + MEMPOOL_SIZE : blocks[next].begin) - block->begin - block->size;
if (freesize == size)
{
best = &blocks[cur];
goto found;
}
if (freesize > size && freesize < bestsize)
{
bestsize = freesize;
best = &blocks[cur];
}
if (next == NOBLOCK)
{
if (best)
goto found;
else
goto collect;
}
block = &blocks[next];
cur = next;
}
while (true);
collect:
{
cur = POOLSTART;
block = &blocks[POOLSTART];
memhandle next;
while ((next = block->nextblock) != NOBLOCK)
{
memaddress dest = block->begin + block->size;
memblock* nextblock = &blocks[next];
memaddress* src = &nextblock->begin;
if (dest != *src)
{
#ifdef MEMPOOL_MEMBLOCK_MV
MEMPOOL_MEMBLOCK_MV(dest,*src,nextblock->size);
#endif
*src = dest;
}
block = nextblock;
}
if (blocks[POOLSTART].begin + MEMPOOL_SIZE - block->begin - block->size >= size)
best = block;
else
goto notfound;
}
found:
{
block = &blocks[POOLOFFSET];
for (cur = POOLOFFSET; cur < MEMPOOL_NUM_MEMBLOCKS + POOLOFFSET; cur++)
{
if (block->size)
{
block++;
continue;
}
memaddress address = best->begin + best->size;
#ifdef MEMBLOCK_ALLOC
MEMBLOCK_ALLOC(address,size);
#endif
block->begin = address;
block->size = size;
block->nextblock = best->nextblock;
best->nextblock = cur;
return cur;
}
}
notfound: return NOBLOCK;
}
void
MemoryPool::freeBlock(memhandle handle)
{
if (handle == NOBLOCK)
return;
memblock *b = &blocks[POOLSTART];
do
{
memhandle next = b->nextblock;
if (next == handle)
{
memblock *f = &blocks[next];
#ifdef MEMBLOCK_FREE
MEMBLOCK_FREE(f->begin,f->size);
#endif
b->nextblock = f->nextblock;
f->size = 0;
f->nextblock = NOBLOCK;
return;
}
if (next == NOBLOCK)
return;
b = &blocks[next];
}
while (true);
}
void
MemoryPool::resizeBlock(memhandle handle, memaddress position)
{
memblock * block = &blocks[handle];
block->begin += position;
block->size -= position;
}
void
MemoryPool::resizeBlock(memhandle handle, memaddress position, memaddress size)
{
memblock * block = &blocks[handle];
block->begin += position;
block->size = size;
}
memaddress
MemoryPool::blockSize(memhandle handle)
{
return blocks[handle].size;
}
@@ -1,54 +0,0 @@
/*
mempool.h - sleek implementation of a memory pool
Copyright (c) 2013 Norbert Truchsess <norbert.truchsess@t-online.de>
All rights reserved.
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
#ifndef MEMPOOL_H
#define MEMPOOL_H
#include <inttypes.h>
#define POOLSTART 0
#define NOBLOCK 0
#include "mempool_conf.h"
struct memblock
{
memaddress begin;
memaddress size;
memhandle nextblock;
};
class MemoryPool
{
#ifdef MEMPOOLTEST_H
friend class MemoryPoolTest;
#endif
protected:
static struct memblock blocks[MEMPOOL_NUM_MEMBLOCKS+1];
public:
static void init();
static memhandle allocBlock(memaddress);
static void freeBlock(memhandle);
static void resizeBlock(memhandle handle, memaddress position);
static void resizeBlock(memhandle handle, memaddress position, memaddress size);
static memaddress blockSize(memhandle);
};
#endif
@@ -1,34 +0,0 @@
#ifndef MEMPOOLCONF_H
#define MEMPOOLCONF_H
#include "uipethernet-conf.h"
extern "C" {
#include "uipopt.h"
#include "enc28j60.h"
}
#include <inttypes.h>
typedef uint16_t memaddress;
typedef uint8_t memhandle;
#if UIP_SOCKET_NUMPACKETS and UIP_CONNS
#define NUM_TCP_MEMBLOCKS (UIP_SOCKET_NUMPACKETS*2)*UIP_CONNS
#else
#define NUM_TCP_MEMBLOCKS 0
#endif
#if UIP_UDP and UIP_UDP_CONNS
#define NUM_UDP_MEMBLOCKS ((2+UIP_UDP_BACKLOG)*UIP_UDP_CONNS)
#else
#define NUM_UDP_MEMBLOCKS 0
#endif
#define MEMPOOL_NUM_MEMBLOCKS (NUM_TCP_MEMBLOCKS+NUM_UDP_MEMBLOCKS)
#define MEMPOOL_STARTADDRESS TXSTART_INIT+1
#define MEMPOOL_SIZE TXSTOP_INIT-TXSTART_INIT
void enc28J60_mempool_block_move_callback(memaddress,memaddress,memaddress);
#define MEMPOOL_MEMBLOCK_MV(dest,src,size) enc28J60_mempool_block_move_callback(dest,src,size)
#endif
@@ -1,185 +0,0 @@
/**
* UIPEthernet Project-specific configuration options
* Copyright (c) 2013 Norbert Truchsess <norbert.truchsess@t-online.de>
* @{
*
* uIP has a number of configuration options that can be overridden
* for each project. These are kept in a project-specific uip-conf.h
* file and all configuration names have the prefix UIP_CONF.
*/
/*
* Copyright (c) 2006, Swedish Institute of Computer Science.
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions
* are met:
* 1. Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
* 2. Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
* 3. Neither the name of the Institute nor the names of its contributors
* may be used to endorse or promote products derived from this software
* without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE INSTITUTE AND CONTRIBUTORS ``AS IS'' AND
* ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
* ARE DISCLAIMED. IN NO EVENT SHALL THE INSTITUTE OR CONTRIBUTORS BE LIABLE
* FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
* DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
* OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
* HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
* LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
* OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
* SUCH DAMAGE.
*
* This file is part of the uIP TCP/IP stack
*
*/
#ifndef __UIP_CONF_H__
#define __UIP_CONF_H__
#include <inttypes.h>
#include "uipethernet-conf.h"
/**
* 8 bit datatype
*
* This typedef defines the 8-bit type used throughout uIP.
*
* \hideinitializer
*/
typedef uint8_t u8_t;
/**
* 16 bit datatype
*
* This typedef defines the 16-bit type used throughout uIP.
*
* \hideinitializer
*/
typedef uint16_t u16_t;
/**
* Statistics datatype
*
* This typedef defines the dataype used for keeping statistics in
* uIP.
*
* \hideinitializer
*/
typedef unsigned short uip_stats_t;
/**
* Maximum number of TCP connections.
* (see uipethernet-conf.h)
* \hideinitializer
*
* #define UIP_CONF_MAX_CONNECTIONS 4
*/
/**
* Maximum number of listening TCP ports.
*
* \hideinitializer
*/
#define UIP_CONF_MAX_LISTENPORTS 4
/**
* uIP buffer size.
*
* \hideinitializer
*/
#define UIP_CONF_BUFFER_SIZE 98
//#define UIP_CONF_BUFFER_SIZE 118
/**
* The TCP maximum segment size.
*
* This is should not be to set to more than
* UIP_BUFSIZE - UIP_LLH_LEN - UIP_TCPIP_HLEN.
*/
#define UIP_CONF_TCP_MSS 512
/**
* The size of the advertised receiver's window.
*
* Should be set low (i.e., to the size of the uip_buf buffer) is the
* application is slow to process incoming data, or high (32768 bytes)
* if the application processes data quickly.
*
* \hideinitializer
*/
#define UIP_CONF_RECEIVE_WINDOW 512
/**
* CPU byte order.
*
* \hideinitializer
*/
#define UIP_CONF_BYTE_ORDER __BYTE_ORDER__
/**
* Logging on or off
*
* \hideinitializer
*/
#define UIP_CONF_LOGGING 0
/**
* UDP support on or off
* (see uipethernet-conf.h)
* \hideinitializer
*
* #define UIP_CONF_UDP 1
*
* #define UIP_CONF_UDP_CONNS 4
*/
/**
* UDP checksums on or off
*
* \hideinitializer
*/
#define UIP_CONF_UDP_CHECKSUMS 1
/**
* UDP Broadcast (receive) on or off
* (see uipethernet-conf.h)
* \hideinitializer
*/
#define UIP_CONF_BROADCAST 1
/**
* uIP statistics on or off
*
* \hideinitializer
*/
#define UIP_CONF_STATISTICS 0
// SLIP
//#define UIP_CONF_LLH_LEN 0
typedef void* uip_tcp_appstate_t;
void uipclient_appcall(void);
#define UIP_APPCALL uipclient_appcall
typedef void* uip_udp_appstate_t;
void uipudp_appcall(void);
#define UIP_UDP_APPCALL uipudp_appcall
#define CC_REGISTER_ARG register
#define UIP_ARCH_CHKSUM 1
#endif /* __UIP_CONF_H__ */
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
@@ -1,138 +0,0 @@
/**
* \addtogroup uip
* {@
*/
/**
* \defgroup uiparch Architecture specific uIP functions
* @{
*
* The functions in the architecture specific module implement the IP
* check sum and 32-bit additions.
*
* The IP checksum calculation is the most computationally expensive
* operation in the TCP/IP stack and it therefore pays off to
* implement this in efficient assembler. The purpose of the uip-arch
* module is to let the checksum functions to be implemented in
* architecture specific assembler.
*
*/
/**
* \file
* Declarations of architecture specific functions.
* \author Adam Dunkels <adam@dunkels.com>
*/
/*
* Copyright (c) 2001, Adam Dunkels.
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions
* are met:
* 1. Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
* 2. Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
* 3. The name of the author may not be used to endorse or promote
* products derived from this software without specific prior
* written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS
* OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
* WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
* ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY
* DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
* DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE
* GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
* WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*
* This file is part of the uIP TCP/IP stack.
*
* $Id: uip_arch.h,v 1.2 2006/06/07 09:15:19 adam Exp $
*
*/
#ifndef __UIP_ARCH_H__
#define __UIP_ARCH_H__
#include "uip.h"
/**
* Carry out a 32-bit addition.
*
* Because not all architectures for which uIP is intended has native
* 32-bit arithmetic, uIP uses an external C function for doing the
* required 32-bit additions in the TCP protocol processing. This
* function should add the two arguments and place the result in the
* global variable uip_acc32.
*
* \note The 32-bit integer pointed to by the op32 parameter and the
* result in the uip_acc32 variable are in network byte order (big
* endian).
*
* \param op32 A pointer to a 4-byte array representing a 32-bit
* integer in network byte order (big endian).
*
* \param op16 A 16-bit integer in host byte order.
*/
void uip_add32(u8_t *op32, u16_t op16);
/**
* Calculate the Internet checksum over a buffer.
*
* The Internet checksum is the one's complement of the one's
* complement sum of all 16-bit words in the buffer.
*
* See RFC1071.
*
* \note This function is not called in the current version of uIP,
* but future versions might make use of it.
*
* \param buf A pointer to the buffer over which the checksum is to be
* computed.
*
* \param len The length of the buffer over which the checksum is to
* be computed.
*
* \return The Internet checksum of the buffer.
*/
u16_t uip_chksum(u16_t *buf, u16_t len);
/**
* Calculate the IP header checksum of the packet header in uip_buf.
*
* The IP header checksum is the Internet checksum of the 20 bytes of
* the IP header.
*
* \return The IP header checksum of the IP header in the uip_buf
* buffer.
*/
u16_t uip_ipchksum(void);
/**
* Calculate the TCP checksum of the packet in uip_buf and uip_appdata.
*
* The TCP checksum is the Internet checksum of data contents of the
* TCP segment, and a pseudo-header as defined in RFC793.
*
* \note The uip_appdata pointer that points to the packet data may
* point anywhere in memory, so it is not possible to simply calculate
* the Internet checksum of the contents of the uip_buf buffer.
*
* \return The TCP checksum of the TCP segment in uip_buf and pointed
* to by uip_appdata.
*/
u16_t uip_tcpchksum(void);
u16_t uip_udpchksum(void);
/** @} */
/** @} */
#endif /* __UIP_ARCH_H__ */
@@ -1,422 +0,0 @@
/**
* \addtogroup uip
* @{
*/
/**
* \defgroup uiparp uIP Address Resolution Protocol
* @{
*
* The Address Resolution Protocol ARP is used for mapping between IP
* addresses and link level addresses such as the Ethernet MAC
* addresses. ARP uses broadcast queries to ask for the link level
* address of a known IP address and the host which is configured with
* the IP address for which the query was meant, will respond with its
* link level address.
*
* \note This ARP implementation only supports Ethernet.
*/
/**
* \file
* Implementation of the ARP Address Resolution Protocol.
* \author Adam Dunkels <adam@dunkels.com>
*
*/
/*
* Copyright (c) 2001-2003, Adam Dunkels.
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions
* are met:
* 1. Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
* 2. Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
* 3. The name of the author may not be used to endorse or promote
* products derived from this software without specific prior
* written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS
* OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
* WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
* ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY
* DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
* DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE
* GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
* WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*
* This file is part of the uIP TCP/IP stack.
*
* $Id: uip_arp.c,v 1.8 2006/06/02 23:36:21 adam Exp $
*
*/
#include "uip_arp.h"
#include <string.h>
struct arp_hdr {
struct uip_eth_hdr ethhdr;
u16_t hwtype;
u16_t protocol;
u8_t hwlen;
u8_t protolen;
u16_t opcode;
struct uip_eth_addr shwaddr;
u16_t sipaddr[2];
struct uip_eth_addr dhwaddr;
u16_t dipaddr[2];
};
struct ethip_hdr {
struct uip_eth_hdr ethhdr;
/* IP header. */
u8_t vhl,
tos,
len[2],
ipid[2],
ipoffset[2],
ttl,
proto;
u16_t ipchksum;
u16_t srcipaddr[2],
destipaddr[2];
};
#define ARP_REQUEST 1
#define ARP_REPLY 2
#define ARP_HWTYPE_ETH 1
struct arp_entry {
u16_t ipaddr[2];
struct uip_eth_addr ethaddr;
u8_t time;
};
static const struct uip_eth_addr broadcast_ethaddr =
{{0xff,0xff,0xff,0xff,0xff,0xff}};
static const u16_t broadcast_ipaddr[2] = {0xffff,0xffff};
static struct arp_entry arp_table[UIP_ARPTAB_SIZE];
static u16_t ipaddr[2];
static u8_t i, c;
static u8_t arptime;
static u8_t tmpage;
#define BUF ((struct arp_hdr *)&uip_buf[0])
#define IPBUF ((struct ethip_hdr *)&uip_buf[0])
/*-----------------------------------------------------------------------------------*/
/**
* Initialize the ARP module.
*
*/
/*-----------------------------------------------------------------------------------*/
void
uip_arp_init(void)
{
for(i = 0; i < UIP_ARPTAB_SIZE; ++i) {
memset(arp_table[i].ipaddr, 0, 4);
}
}
/*-----------------------------------------------------------------------------------*/
/**
* Periodic ARP processing function.
*
* This function performs periodic timer processing in the ARP module
* and should be called at regular intervals. The recommended interval
* is 10 seconds between the calls.
*
*/
/*-----------------------------------------------------------------------------------*/
void
uip_arp_timer(void)
{
struct arp_entry *tabptr;
++arptime;
for(i = 0; i < UIP_ARPTAB_SIZE; ++i) {
tabptr = &arp_table[i];
if((tabptr->ipaddr[0] | tabptr->ipaddr[1]) != 0 &&
arptime - tabptr->time >= UIP_ARP_MAXAGE) {
memset(tabptr->ipaddr, 0, 4);
}
}
}
/*-----------------------------------------------------------------------------------*/
static void
uip_arp_update(u16_t *ipaddr, struct uip_eth_addr *ethaddr)
{
register struct arp_entry *tabptr;
/* Walk through the ARP mapping table and try to find an entry to
update. If none is found, the IP -> MAC address mapping is
inserted in the ARP table. */
for(i = 0; i < UIP_ARPTAB_SIZE; ++i) {
tabptr = &arp_table[i];
/* Only check those entries that are actually in use. */
if(tabptr->ipaddr[0] != 0 &&
tabptr->ipaddr[1] != 0) {
/* Check if the source IP address of the incoming packet matches
the IP address in this ARP table entry. */
if(memcmp(ipaddr, tabptr->ipaddr, 4) == 0) {
/* An old entry found, update this and return. */
memcpy(tabptr->ethaddr.addr, ethaddr->addr, 6);
tabptr->time = arptime;
return;
}
}
}
/* If we get here, no existing ARP table entry was found, so we
create one. */
/* First, we try to find an unused entry in the ARP table. */
for(i = 0; i < UIP_ARPTAB_SIZE; ++i) {
tabptr = &arp_table[i];
if(tabptr->ipaddr[0] == 0 &&
tabptr->ipaddr[1] == 0) {
break;
}
}
/* If no unused entry is found, we try to find the oldest entry and
throw it away. */
if(i == UIP_ARPTAB_SIZE) {
tmpage = 0;
c = 0;
for(i = 0; i < UIP_ARPTAB_SIZE; ++i) {
tabptr = &arp_table[i];
if(arptime - tabptr->time > tmpage) {
tmpage = arptime - tabptr->time;
c = i;
}
}
i = c;
tabptr = &arp_table[i];
}
/* Now, i is the ARP table entry which we will fill with the new
information. */
memcpy(tabptr->ipaddr, ipaddr, 4);
memcpy(tabptr->ethaddr.addr, ethaddr->addr, 6);
tabptr->time = arptime;
}
/*-----------------------------------------------------------------------------------*/
/**
* ARP processing for incoming IP packets
*
* This function should be called by the device driver when an IP
* packet has been received. The function will check if the address is
* in the ARP cache, and if so the ARP cache entry will be
* refreshed. If no ARP cache entry was found, a new one is created.
*
* This function expects an IP packet with a prepended Ethernet header
* in the uip_buf[] buffer, and the length of the packet in the global
* variable uip_len.
*/
/*-----------------------------------------------------------------------------------*/
//#if 0
void
uip_arp_ipin(void)
{
uip_len -= sizeof(struct uip_eth_hdr);
/* Only insert/update an entry if the source IP address of the
incoming IP packet comes from a host on the local network. */
if((IPBUF->srcipaddr[0] & uip_netmask[0]) !=
(uip_hostaddr[0] & uip_netmask[0])) {
return;
}
if((IPBUF->srcipaddr[1] & uip_netmask[1]) !=
(uip_hostaddr[1] & uip_netmask[1])) {
return;
}
uip_arp_update(IPBUF->srcipaddr, &(IPBUF->ethhdr.src));
return;
}
//#endif /* 0 */
/*-----------------------------------------------------------------------------------*/
/**
* ARP processing for incoming ARP packets.
*
* This function should be called by the device driver when an ARP
* packet has been received. The function will act differently
* depending on the ARP packet type: if it is a reply for a request
* that we previously sent out, the ARP cache will be filled in with
* the values from the ARP reply. If the incoming ARP packet is an ARP
* request for our IP address, an ARP reply packet is created and put
* into the uip_buf[] buffer.
*
* When the function returns, the value of the global variable uip_len
* indicates whether the device driver should send out a packet or
* not. If uip_len is zero, no packet should be sent. If uip_len is
* non-zero, it contains the length of the outbound packet that is
* present in the uip_buf[] buffer.
*
* This function expects an ARP packet with a prepended Ethernet
* header in the uip_buf[] buffer, and the length of the packet in the
* global variable uip_len.
*/
/*-----------------------------------------------------------------------------------*/
void
uip_arp_arpin(void)
{
if(uip_len < sizeof(struct arp_hdr)) {
uip_len = 0;
return;
}
uip_len = 0;
switch(BUF->opcode) {
case HTONS(ARP_REQUEST):
/* ARP request. If it asked for our address, we send out a
reply. */
if(uip_ipaddr_cmp(BUF->dipaddr, uip_hostaddr)) {
/* First, we register the one who made the request in our ARP
table, since it is likely that we will do more communication
with this host in the future. */
uip_arp_update(BUF->sipaddr, &BUF->shwaddr);
/* The reply opcode is 2. */
BUF->opcode = HTONS(2);
memcpy(BUF->dhwaddr.addr, BUF->shwaddr.addr, 6);
memcpy(BUF->shwaddr.addr, uip_ethaddr.addr, 6);
memcpy(BUF->ethhdr.src.addr, uip_ethaddr.addr, 6);
memcpy(BUF->ethhdr.dest.addr, BUF->dhwaddr.addr, 6);
BUF->dipaddr[0] = BUF->sipaddr[0];
BUF->dipaddr[1] = BUF->sipaddr[1];
BUF->sipaddr[0] = uip_hostaddr[0];
BUF->sipaddr[1] = uip_hostaddr[1];
BUF->ethhdr.type = HTONS(UIP_ETHTYPE_ARP);
uip_len = sizeof(struct arp_hdr);
}
break;
case HTONS(ARP_REPLY):
/* ARP reply. We insert or update the ARP table if it was meant
for us. */
if(uip_ipaddr_cmp(BUF->dipaddr, uip_hostaddr)) {
uip_arp_update(BUF->sipaddr, &BUF->shwaddr);
}
break;
}
return;
}
/*-----------------------------------------------------------------------------------*/
/**
* Prepend Ethernet header to an outbound IP packet and see if we need
* to send out an ARP request.
*
* This function should be called before sending out an IP packet. The
* function checks the destination IP address of the IP packet to see
* what Ethernet MAC address that should be used as a destination MAC
* address on the Ethernet.
*
* If the destination IP address is in the local network (determined
* by logical ANDing of netmask and our IP address), the function
* checks the ARP cache to see if an entry for the destination IP
* address is found. If so, an Ethernet header is prepended and the
* function returns. If no ARP cache entry is found for the
* destination IP address, the packet in the uip_buf[] is replaced by
* an ARP request packet for the IP address. The IP packet is dropped
* and it is assumed that they higher level protocols (e.g., TCP)
* eventually will retransmit the dropped packet.
*
* If the destination IP address is not on the local network, the IP
* address of the default router is used instead.
*
* When the function returns, a packet is present in the uip_buf[]
* buffer, and the length of the packet is in the global variable
* uip_len.
*/
/*-----------------------------------------------------------------------------------*/
void
uip_arp_out(void)
{
struct arp_entry *tabptr;
/* Find the destination IP address in the ARP table and construct
the Ethernet header. If the destination IP addres isn't on the
local network, we use the default router's IP address instead.
If not ARP table entry is found, we overwrite the original IP
packet with an ARP request for the IP address. */
/* First check if destination is a local broadcast. */
if(uip_ipaddr_cmp(IPBUF->destipaddr, broadcast_ipaddr)) {
memcpy(IPBUF->ethhdr.dest.addr, broadcast_ethaddr.addr, 6);
} else {
/* Check if the destination address is on the local network. */
if(!uip_ipaddr_maskcmp(IPBUF->destipaddr, uip_hostaddr, uip_netmask)) {
/* Destination address was not on the local network, so we need to
use the default router's IP address instead of the destination
address when determining the MAC address. */
uip_ipaddr_copy(ipaddr, uip_draddr);
} else {
/* Else, we use the destination IP address. */
uip_ipaddr_copy(ipaddr, IPBUF->destipaddr);
}
for(i = 0; i < UIP_ARPTAB_SIZE; ++i) {
tabptr = &arp_table[i];
if(uip_ipaddr_cmp(ipaddr, tabptr->ipaddr)) {
break;
}
}
if(i == UIP_ARPTAB_SIZE) {
/* The destination address was not in our ARP table, so we
overwrite the IP packet with an ARP request. */
memset(BUF->ethhdr.dest.addr, 0xff, 6);
memset(BUF->dhwaddr.addr, 0x00, 6);
memcpy(BUF->ethhdr.src.addr, uip_ethaddr.addr, 6);
memcpy(BUF->shwaddr.addr, uip_ethaddr.addr, 6);
uip_ipaddr_copy(BUF->dipaddr, ipaddr);
uip_ipaddr_copy(BUF->sipaddr, uip_hostaddr);
BUF->opcode = HTONS(ARP_REQUEST); /* ARP request. */
BUF->hwtype = HTONS(ARP_HWTYPE_ETH);
BUF->protocol = HTONS(UIP_ETHTYPE_IP);
BUF->hwlen = 6;
BUF->protolen = 4;
BUF->ethhdr.type = HTONS(UIP_ETHTYPE_ARP);
uip_appdata = &uip_buf[UIP_TCPIP_HLEN + UIP_LLH_LEN];
uip_len = sizeof(struct arp_hdr);
return;
}
/* Build an ethernet header. */
memcpy(IPBUF->ethhdr.dest.addr, tabptr->ethaddr.addr, 6);
}
memcpy(IPBUF->ethhdr.src.addr, uip_ethaddr.addr, 6);
IPBUF->ethhdr.type = HTONS(UIP_ETHTYPE_IP);
uip_len += sizeof(struct uip_eth_hdr);
}
/*-----------------------------------------------------------------------------------*/
/** @} */
/** @} */
@@ -1,144 +0,0 @@
/**
* \addtogroup uip
* @{
*/
/**
* \addtogroup uiparp
* @{
*/
/**
* \file
* Macros and definitions for the ARP module.
* \author Adam Dunkels <adam@dunkels.com>
*/
/*
* Copyright (c) 2001-2003, Adam Dunkels.
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions
* are met:
* 1. Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
* 2. Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
* 3. The name of the author may not be used to endorse or promote
* products derived from this software without specific prior
* written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS
* OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
* WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
* ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY
* DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
* DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE
* GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
* WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*
* This file is part of the uIP TCP/IP stack.
*
* $Id: uip_arp.h,v 1.5 2006/06/11 21:46:39 adam Exp $
*
*/
#ifndef __UIP_ARP_H__
#define __UIP_ARP_H__
#include "uip.h"
extern struct uip_eth_addr uip_ethaddr;
/**
* The Ethernet header.
*/
struct uip_eth_hdr {
struct uip_eth_addr dest;
struct uip_eth_addr src;
u16_t type;
};
#define UIP_ETHTYPE_ARP 0x0806
#define UIP_ETHTYPE_IP 0x0800
#define UIP_ETHTYPE_IP6 0x86dd
/* The uip_arp_init() function must be called before any of the other
ARP functions. */
void uip_arp_init(void);
/* The uip_arp_ipin() function should be called whenever an IP packet
arrives from the Ethernet. This function refreshes the ARP table or
inserts a new mapping if none exists. The function assumes that an
IP packet with an Ethernet header is present in the uip_buf buffer
and that the length of the packet is in the uip_len variable. */
void uip_arp_ipin(void);
//#define uip_arp_ipin()
/* The uip_arp_arpin() should be called when an ARP packet is received
by the Ethernet driver. This function also assumes that the
Ethernet frame is present in the uip_buf buffer. When the
uip_arp_arpin() function returns, the contents of the uip_buf
buffer should be sent out on the Ethernet if the uip_len variable
is > 0. */
void uip_arp_arpin(void);
/* The uip_arp_out() function should be called when an IP packet
should be sent out on the Ethernet. This function creates an
Ethernet header before the IP header in the uip_buf buffer. The
Ethernet header will have the correct Ethernet MAC destination
address filled in if an ARP table entry for the destination IP
address (or the IP address of the default router) is present. If no
such table entry is found, the IP packet is overwritten with an ARP
request and we rely on TCP to retransmit the packet that was
overwritten. In any case, the uip_len variable holds the length of
the Ethernet frame that should be transmitted. */
void uip_arp_out(void);
/* The uip_arp_timer() function should be called every ten seconds. It
is responsible for flushing old entries in the ARP table. */
void uip_arp_timer(void);
/** @} */
/**
* \addtogroup uipconffunc
* @{
*/
/**
* Specifiy the Ethernet MAC address.
*
* The ARP code needs to know the MAC address of the Ethernet card in
* order to be able to respond to ARP queries and to generate working
* Ethernet headers.
*
* \note This macro only specifies the Ethernet MAC address to the ARP
* code. It cannot be used to change the MAC address of the Ethernet
* card.
*
* \param eaddr A pointer to a struct uip_eth_addr containing the
* Ethernet MAC address of the Ethernet card.
*
* \hideinitializer
*/
#define uip_setethaddr(eaddr) do {uip_ethaddr.addr[0] = eaddr.addr[0]; \
uip_ethaddr.addr[1] = eaddr.addr[1];\
uip_ethaddr.addr[2] = eaddr.addr[2];\
uip_ethaddr.addr[3] = eaddr.addr[3];\
uip_ethaddr.addr[4] = eaddr.addr[4];\
uip_ethaddr.addr[5] = eaddr.addr[5];} while(0)
/** @} */
/** @} */
#endif /* __UIP_ARP_H__ */
@@ -1,48 +0,0 @@
#ifndef UIPETHERNET_CONF_H
#define UIPETHERNET_CONF_H
// https://github.com/jandrassy/EthernetENC/wiki/Settings
/* for TCP */
#ifndef UIP_SOCKET_NUMPACKETS
#define UIP_SOCKET_NUMPACKETS 3
#endif
#ifndef UIP_CONF_MAX_CONNECTIONS
#define UIP_CONF_MAX_CONNECTIONS 4
#endif
/* for UDP
* set UIP_CONF_UDP to 0 to disable UDP (saves aprox. 4kB flash) */
#ifndef UIP_CONF_UDP
#define UIP_CONF_UDP 1
#endif
#ifndef UIP_CONF_UDP_CONNS
#define UIP_CONF_UDP_CONNS 2
#endif
/**
* size of received UDP messages backlog. it must be at least 1
*/
#ifndef UIP_UDP_BACKLOG
#define UIP_UDP_BACKLOG 2
#endif
/* timeout in ms for attempts to get a free memory block to write
* before returning number of bytes sent so far
* set to 0 to block until connection is closed by timeout */
#ifndef UIP_WRITE_TIMEOUT
#define UIP_WRITE_TIMEOUT 2000
#endif
/* timeout after which UIPClient::connect gives up. The timeout is specified in seconds.
* if set to a number <= 0 connect will timeout when uIP does (which might be longer than you expect...) */
#ifndef UIP_CONNECT_TIMEOUT
#define UIP_CONNECT_TIMEOUT 5
#endif
/* periodic timer for uip (in ms) */
#ifndef UIP_PERIODIC_TIMER
#define UIP_PERIODIC_TIMER 100
#endif
#endif
@@ -1,555 +0,0 @@
/**
* \defgroup uipopt Configuration options for uIP
* @{
*
* uIP is configured using the per-project configuration file
* uipopt.h. This file contains all compile-time options for uIP and
* should be tweaked to match each specific project. The uIP
* distribution contains a documented example "uipopt.h" that can be
* copied and modified for each project.
*
* \note Most of the configuration options in the uipopt.h should not
* be changed, but rather the per-project uip-conf.h file.
*/
/**
* \file
* Configuration options for uIP.
* \author Adam Dunkels <adam@dunkels.com>
*
* This file is used for tweaking various configuration options for
* uIP. You should make a copy of this file into one of your project's
* directories instead of editing this example "uipopt.h" file that
* comes with the uIP distribution.
*/
/*
* Copyright (c) 2001-2003, Adam Dunkels.
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions
* are met:
* 1. Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
* 2. Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
* 3. The name of the author may not be used to endorse or promote
* products derived from this software without specific prior
* written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS
* OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
* WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
* ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY
* DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
* DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE
* GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
* WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*
* This file is part of the uIP TCP/IP stack.
*
* $Id: uipopt.h,v 1.4 2006/06/12 08:00:31 adam Exp $
*
*/
#ifndef __UIPOPT_H__
#define __UIPOPT_H__
#ifndef UIP_LITTLE_ENDIAN
#if defined(LITTLE_ENDIAN)
#define UIP_LITTLE_ENDIAN LITTLE_ENDIAN
#elif defined(__ORDER_LITTLE_ENDIAN__)
#define UIP_LITTLE_ENDIAN __ORDER_LITTLE_ENDIAN__
#else
#define UIP_LITTLE_ENDIAN 1234
#endif
#endif /* UIP_LITTLE_ENDIAN */
#ifndef UIP_BIG_ENDIAN
#if defined(BIG_ENDIAN)
#define UIP_BIG_ENDIAN BIG_ENDIAN
#elif defined(__ORDER_BIG_ENDIAN__)
#define UIP_BIG_ENDIAN __ORDER_BIG_ENDIAN__
#else
#define UIP_BIG_ENDIAN 4321
#endif
#endif /* UIP_BIG_ENDIAN */
#include "uip-conf.h"
/*------------------------------------------------------------------------------*/
/**
* \name Static configuration options
* @{
*
* These configuration options can be used for setting the IP address
* settings statically, but only if UIP_FIXEDADDR is set to 1. The
* configuration options for a specific node includes IP address,
* netmask and default router as well as the Ethernet address. The
* netmask, default router and Ethernet address are appliciable only
* if uIP should be run over Ethernet.
*
* All of these should be changed to suit your project.
*/
/**
* Determines if uIP should use a fixed IP address or not.
*
* If uIP should use a fixed IP address, the settings are set in the
* uipopt.h file. If not, the macros uip_sethostaddr(),
* uip_setdraddr() and uip_setnetmask() should be used instead.
*
* \hideinitializer
*/
#define UIP_FIXEDADDR 0
/**
* Ping IP address asignment.
*
* uIP uses a "ping" packets for setting its own IP address if this
* option is set. If so, uIP will start with an empty IP address and
* the destination IP address of the first incoming "ping" (ICMP echo)
* packet will be used for setting the hosts IP address.
*
* \note This works only if UIP_FIXEDADDR is 0.
*
* \hideinitializer
*/
#ifdef UIP_CONF_PINGADDRCONF
#define UIP_PINGADDRCONF UIP_CONF_PINGADDRCONF
#else /* UIP_CONF_PINGADDRCONF */
#define UIP_PINGADDRCONF 0
#endif /* UIP_CONF_PINGADDRCONF */
/**
* Specifies if the uIP ARP module should be compiled with a fixed
* Ethernet MAC address or not.
*
* If this configuration option is 0, the macro uip_setethaddr() can
* be used to specify the Ethernet address at run-time.
*
* \hideinitializer
*/
#define UIP_FIXEDETHADDR 0
/** @} */
/*------------------------------------------------------------------------------*/
/**
* \name IP configuration options
* @{
*
*/
/**
* The IP TTL (time to live) of IP packets sent by uIP.
*
* This should normally not be changed.
*/
#define UIP_TTL 64
/**
* Turn on support for IP packet reassembly.
*
* uIP supports reassembly of fragmented IP packets. This features
* requires an additonal amount of RAM to hold the reassembly buffer
* and the reassembly code size is approximately 700 bytes. The
* reassembly buffer is of the same size as the uip_buf buffer
* (configured by UIP_BUFSIZE).
*
* \note IP packet reassembly is not heavily tested.
*
* \hideinitializer
*/
#define UIP_REASSEMBLY 0
/**
* The maximum time an IP fragment should wait in the reassembly
* buffer before it is dropped.
*
*/
#define UIP_REASS_MAXAGE 40
/** @} */
/*------------------------------------------------------------------------------*/
/**
* \name UDP configuration options
* @{
*/
/**
* Toggles wether UDP support should be compiled in or not.
*
* \hideinitializer
*/
#ifdef UIP_CONF_UDP
#define UIP_UDP UIP_CONF_UDP
#else /* UIP_CONF_UDP */
#define UIP_UDP 0
#endif /* UIP_CONF_UDP */
/**
* Toggles if UDP checksums should be used or not.
*
* \note Support for UDP checksums is currently not included in uIP,
* so this option has no function.
*
* \hideinitializer
*/
#ifdef UIP_CONF_UDP_CHECKSUMS
#define UIP_UDP_CHECKSUMS UIP_CONF_UDP_CHECKSUMS
#else
#define UIP_UDP_CHECKSUMS 0
#endif
/**
* The maximum amount of concurrent UDP connections.
*
* \hideinitializer
*/
#ifdef UIP_CONF_UDP_CONNS
#define UIP_UDP_CONNS UIP_CONF_UDP_CONNS
#else /* UIP_CONF_UDP_CONNS */
#define UIP_UDP_CONNS 10
#endif /* UIP_CONF_UDP_CONNS */
/**
* The name of the function that should be called when UDP datagrams arrive.
*
* \hideinitializer
*/
/** @} */
/*------------------------------------------------------------------------------*/
/**
* \name TCP configuration options
* @{
*/
/**
* Determines if support for opening connections from uIP should be
* compiled in.
*
* If the applications that are running on top of uIP for this project
* do not need to open outgoing TCP connections, this configration
* option can be turned off to reduce the code size of uIP.
*
* \hideinitializer
*/
#define UIP_ACTIVE_OPEN 1
/**
* The maximum number of simultaneously open TCP connections.
*
* Since the TCP connections are statically allocated, turning this
* configuration knob down results in less RAM used. Each TCP
* connection requires approximatly 30 bytes of memory.
*
* \hideinitializer
*/
#ifndef UIP_CONF_MAX_CONNECTIONS
#define UIP_CONNS 10
#else /* UIP_CONF_MAX_CONNECTIONS */
#define UIP_CONNS UIP_CONF_MAX_CONNECTIONS
#endif /* UIP_CONF_MAX_CONNECTIONS */
/**
* The maximum number of simultaneously listening TCP ports.
*
* Each listening TCP port requires 2 bytes of memory.
*
* \hideinitializer
*/
#ifndef UIP_CONF_MAX_LISTENPORTS
#define UIP_LISTENPORTS 20
#else /* UIP_CONF_MAX_LISTENPORTS */
#define UIP_LISTENPORTS UIP_CONF_MAX_LISTENPORTS
#endif /* UIP_CONF_MAX_LISTENPORTS */
/**
* Determines if support for TCP urgent data notification should be
* compiled in.
*
* Urgent data (out-of-band data) is a rarely used TCP feature that
* very seldom would be required.
*
* \hideinitializer
*/
#define UIP_URGDATA 0
/**
* The initial retransmission timeout counted in timer pulses.
*
* This should not be changed.
*/
#define UIP_RTO 3
/**
* The maximum number of times a segment should be retransmitted
* before the connection should be aborted.
*
* This should not be changed.
*/
#define UIP_MAXRTX 8
/**
* The maximum number of times a SYN segment should be retransmitted
* before a connection request should be deemed to have been
* unsuccessful.
*
* This should not need to be changed.
*/
#define UIP_MAXSYNRTX 5
/**
* The TCP maximum segment size.
*
* This is should not be to set to more than
* UIP_BUFSIZE - UIP_LLH_LEN - UIP_TCPIP_HLEN.
*/
#ifndef UIP_CONF_TCP_MSS
#define UIP_TCP_MSS (UIP_BUFSIZE - UIP_LLH_LEN - UIP_TCPIP_HLEN)
#else
#define UIP_TCP_MSS UIP_CONF_TCP_MSS
#endif
/**
* The size of the advertised receiver's window.
*
* Should be set low (i.e., to the size of the uip_buf buffer) is the
* application is slow to process incoming data, or high (32768 bytes)
* if the application processes data quickly.
*
* \hideinitializer
*/
#ifndef UIP_CONF_RECEIVE_WINDOW
#define UIP_RECEIVE_WINDOW UIP_TCP_MSS
#else
#define UIP_RECEIVE_WINDOW UIP_CONF_RECEIVE_WINDOW
#endif
/**
* How long a connection should stay in the TIME_WAIT state.
*
* This configiration option has no real implication, and it should be
* left untouched.
*/
#define UIP_TIME_WAIT_TIMEOUT 120
/** @} */
/*------------------------------------------------------------------------------*/
/**
* \name ARP configuration options
* @{
*/
/**
* The size of the ARP table.
*
* This option should be set to a larger value if this uIP node will
* have many connections from the local network.
*
* \hideinitializer
*/
#ifdef UIP_CONF_ARPTAB_SIZE
#define UIP_ARPTAB_SIZE UIP_CONF_ARPTAB_SIZE
#else
#define UIP_ARPTAB_SIZE 8
#endif
/**
* The maxium age of ARP table entries measured in 10ths of seconds.
*
* An UIP_ARP_MAXAGE of 120 corresponds to 20 minutes (BSD
* default).
*/
#define UIP_ARP_MAXAGE 120
/** @} */
/*------------------------------------------------------------------------------*/
/**
* \name General configuration options
* @{
*/
/**
* The size of the uIP packet buffer.
*
* The uIP packet buffer should not be smaller than 60 bytes, and does
* not need to be larger than 1500 bytes. Lower size results in lower
* TCP throughput, larger size results in higher TCP throughput.
*
* \hideinitializer
*/
#ifndef UIP_CONF_BUFFER_SIZE
#define UIP_BUFSIZE 400
#else /* UIP_CONF_BUFFER_SIZE */
#define UIP_BUFSIZE UIP_CONF_BUFFER_SIZE
#endif /* UIP_CONF_BUFFER_SIZE */
/**
* Determines if statistics support should be compiled in.
*
* The statistics is useful for debugging and to show the user.
*
* \hideinitializer
*/
#ifndef UIP_CONF_STATISTICS
#define UIP_STATISTICS 0
#else /* UIP_CONF_STATISTICS */
#define UIP_STATISTICS UIP_CONF_STATISTICS
#endif /* UIP_CONF_STATISTICS */
/**
* Determines if logging of certain events should be compiled in.
*
* This is useful mostly for debugging. The function uip_log()
* must be implemented to suit the architecture of the project, if
* logging is turned on.
*
* \hideinitializer
*/
#ifndef UIP_CONF_LOGGING
#define UIP_LOGGING 0
#else /* UIP_CONF_LOGGING */
#define UIP_LOGGING UIP_CONF_LOGGING
#endif /* UIP_CONF_LOGGING */
/**
* Broadcast support.
*
* This flag configures IP broadcast support. This is useful only
* together with UDP.
*
* \hideinitializer
*
*/
#if UIP_UDP && UIP_CONF_BROADCAST
#define UIP_BROADCAST UIP_CONF_BROADCAST
#else /* UIP_CONF_BROADCAST */
#define UIP_BROADCAST 0
#endif /* UIP_CONF_BROADCAST */
/**
* Print out a uIP log message.
*
* This function must be implemented by the module that uses uIP, and
* is called by uIP whenever a log message is generated.
*/
void uip_log(char *msg);
/**
* The link level header length.
*
* This is the offset into the uip_buf where the IP header can be
* found. For Ethernet, this should be set to 14. For SLIP, this
* should be set to 0.
*
* \hideinitializer
*/
#ifdef UIP_CONF_LLH_LEN
#define UIP_LLH_LEN UIP_CONF_LLH_LEN
#else /* UIP_CONF_LLH_LEN */
#define UIP_LLH_LEN 14
#endif /* UIP_CONF_LLH_LEN */
/** @} */
/*------------------------------------------------------------------------------*/
/**
* \name CPU architecture configuration
* @{
*
* The CPU architecture configuration is where the endianess of the
* CPU on which uIP is to be run is specified. Most CPUs today are
* little endian, and the most notable exception are the Motorolas
* which are big endian. The BYTE_ORDER macro should be changed to
* reflect the CPU architecture on which uIP is to be run.
*/
/**
* The byte order of the CPU architecture on which uIP is to be run.
*
* This option can be either BIG_ENDIAN (Motorola byte order) or
* LITTLE_ENDIAN (Intel byte order).
*
* \hideinitializer
*/
#ifdef UIP_CONF_BYTE_ORDER
#define UIP_BYTE_ORDER UIP_CONF_BYTE_ORDER
#else /* UIP_CONF_BYTE_ORDER */
#define UIP_BYTE_ORDER UIP_LITTLE_ENDIAN
#endif /* UIP_CONF_BYTE_ORDER */
/** @} */
/*------------------------------------------------------------------------------*/
/**
* \name Appication specific configurations
* @{
*
* An uIP application is implemented using a single application
* function that is called by uIP whenever a TCP/IP event occurs. The
* name of this function must be registered with uIP at compile time
* using the UIP_APPCALL definition.
*
* uIP applications can store the application state within the
* uip_conn structure by specifying the type of the application
* structure by typedef:ing the type uip_tcp_appstate_t and uip_udp_appstate_t.
*
* The file containing the definitions must be included in the
* uipopt.h file.
*
* The following example illustrates how this can look.
\code
void httpd_appcall(void);
#define UIP_APPCALL httpd_appcall
struct httpd_state {
u8_t state;
u16_t count;
char *dataptr;
char *script;
};
typedef struct httpd_state uip_tcp_appstate_t
\endcode
*/
/**
* \var #define UIP_APPCALL
*
* The name of the application function that uIP should call in
* response to TCP/IP events.
*
*/
/**
* \var typedef uip_tcp_appstate_t
*
* The type of the application state that is to be stored in the
* uip_conn structure. This usually is typedef:ed to a struct holding
* application state information.
*/
/**
* \var typedef uip_udp_appstate_t
*
* The type of the application state that is to be stored in the
* uip_conn structure. This usually is typedef:ed to a struct holding
* application state information.
*/
/** @} */
/** @} */
#endif /* __UIPOPT_H__ */
@@ -1,10 +0,0 @@
#ifndef UTIL_H
#define UTIL_H
#define htonl(x) ( ((x)<<24 & 0xFF000000UL) | \
((x)<< 8 & 0x00FF0000UL) | \
((x)>> 8 & 0x0000FF00UL) | \
((x)>>24 & 0x000000FFUL) )
#define ntohl(x) htonl(x)
#endif
-22
View File
@@ -1,22 +0,0 @@
[env:growatt-shinelan]
platform = ststm32
board = genericSTM32F103RB
framework = arduino
upload_protocol = stlink
debug_tool = stlink
monitor_speed = 115200
lib_deps =
4-20ma/ModbusMaster@^2.0.1
knolleary/PubSubClient@^2.8
# EthernetENC ist lokal in lib/EthernetENC/ (Bitbang-SPI Modifikation)
build_flags =
-DPIO_FRAMEWORK_ARDUINO_NOERRNO
-DUSBCON
-DUSBD_USE_CDC
-DHAL_PCD_MODULE_ENABLED
-DUSB_CDC_TRANSMIT_TIMEOUT=3000
-DCUSTOM_USBD_PID=0x5740
'-DUSB_PRODUCT_STRING="STM32 Virtual COM Port"'
-I include
-712
View File
@@ -1,712 +0,0 @@
#include <Arduino.h>
#include <SPI.h>
#include <EthernetENC.h>
#include <PubSubClient.h>
#include <ModbusMaster.h>
#include <EEPROM.h>
#include "config.h"
#include "config_manager.h"
extern "C" void SystemClock_Config(void) {
RCC_OscInitTypeDef osc = {};
RCC_ClkInitTypeDef clk = {};
RCC_PeriphCLKInitTypeDef periphClk = {};
osc.OscillatorType = RCC_OSCILLATORTYPE_HSE;
osc.HSEState = RCC_HSE_ON;
osc.PLL.PLLState = RCC_PLL_ON;
osc.PLL.PLLSource = RCC_PLLSOURCE_HSE;
osc.PLL.PLLMUL = RCC_PLL_MUL9;
if (HAL_RCC_OscConfig(&osc) == HAL_OK) {
clk.ClockType = RCC_CLOCKTYPE_HCLK | RCC_CLOCKTYPE_SYSCLK
| RCC_CLOCKTYPE_PCLK1 | RCC_CLOCKTYPE_PCLK2;
clk.SYSCLKSource = RCC_SYSCLKSOURCE_PLLCLK;
clk.AHBCLKDivider = RCC_SYSCLK_DIV1;
clk.APB1CLKDivider = RCC_HCLK_DIV2;
clk.APB2CLKDivider = RCC_HCLK_DIV1;
HAL_RCC_ClockConfig(&clk, FLASH_LATENCY_2);
periphClk.PeriphClockSelection = RCC_PERIPHCLK_USB;
periphClk.UsbClockSelection = RCC_USBCLKSOURCE_PLL_DIV1_5;
HAL_RCCEx_PeriphCLKConfig(&periphClk);
} else {
osc.OscillatorType = RCC_OSCILLATORTYPE_HSI;
osc.HSEState = RCC_HSE_OFF;
osc.HSIState = RCC_HSI_ON;
osc.HSICalibrationValue = RCC_HSICALIBRATION_DEFAULT;
osc.PLL.PLLSource = RCC_PLLSOURCE_HSI_DIV2;
osc.PLL.PLLMUL = RCC_PLL_MUL12;
HAL_RCC_OscConfig(&osc);
clk.ClockType = RCC_CLOCKTYPE_HCLK | RCC_CLOCKTYPE_SYSCLK
| RCC_CLOCKTYPE_PCLK1 | RCC_CLOCKTYPE_PCLK2;
clk.SYSCLKSource = RCC_SYSCLKSOURCE_PLLCLK;
clk.AHBCLKDivider = RCC_SYSCLK_DIV1;
clk.APB1CLKDivider = RCC_HCLK_DIV2;
clk.APB2CLKDivider = RCC_HCLK_DIV1;
HAL_RCC_ClockConfig(&clk, FLASH_LATENCY_1);
periphClk.PeriphClockSelection = RCC_PERIPHCLK_USB;
periphClk.UsbClockSelection = RCC_USBCLKSOURCE_PLL;
HAL_RCCEx_PeriphCLKConfig(&periphClk);
}
}
// Hält PA8 HIGH (USB getrennt) bis setup() es manuell verbindet
extern "C" void USBD_reenumerate(void) {
__HAL_RCC_GPIOA_CLK_ENABLE();
GPIO_InitTypeDef gpio = {};
gpio.Pin = GPIO_PIN_8;
gpio.Mode = GPIO_MODE_OUTPUT_PP;
gpio.Pull = GPIO_NOPULL;
gpio.Speed = GPIO_SPEED_FREQ_HIGH;
HAL_GPIO_Init(GPIOA, &gpio);
HAL_GPIO_WritePin(GPIOA, GPIO_PIN_8, GPIO_PIN_SET);
}
HardwareSerial DebugSerial(PA10, PA9);
// USB-Diagnose: was der Wechselrichter per Control-Transfer schickt
extern __IO bool dtrState;
extern __IO bool rtsState;
struct { uint32_t baud; uint8_t format; uint8_t parity; uint8_t bits; }
g_lineCoding = {115200, 0, 0, 8};
volatile bool g_lineCodingUpdated = false;
// Wird vom USB-Stack aufgerufen wenn Wechselrichter SET_LINE_CODING schickt
// usbd_cdc_if.c kann nicht geändert werden → wir lesen linecoding-Struct direkt
extern "C" {
typedef struct { uint32_t bitrate; uint8_t format; uint8_t paritytype; uint8_t datatype; }
USBD_CDC_LineCodingTypeDef;
extern USBD_CDC_LineCodingTypeDef linecoding;
}
// CDC SERIAL_STATE Notification — NuttX sendet dies beim Öffnen von /dev/ttyACM0
// signalisiert dem Wechselrichter DCD+DSR=1 → startet Modbus-Server im Inverter
#include "usbd_core.h"
extern USBD_HandleTypeDef hUSBD_Device_CDC;
#define CDC_SERIAL_STATE_EP 0x82U
void sendSerialStateNotification() {
static uint8_t notif[10] = {
0xA1, 0x20, 0x00, 0x00, 0x00, 0x00, 0x02, 0x00, 0x03, 0x00
};
USBD_LL_Transmit(&hUSBD_Device_CDC, CDC_SERIAL_STATE_EP, notif, sizeof(notif));
}
// ModbusMaster schreibt Byte-für-Byte. Würden wir sofort senden, käme Byte 1 als
// eigenes 1-Byte USB-Paket an — der Wechselrichter behandelt jedes Short Packet
// als Frame-Ende und sieht keinen gültigen Modbus-Frame.
// Lösung: Bytes puffern, dann in flush() als ein einziges USB-Paket senden.
class ModbusStream : public Stream {
public:
uint8_t txBuf[16]; uint8_t txLen = 0;
uint8_t rxBuf[32]; uint8_t rxLen = 0;
uint32_t rxTotal = 0;
void reset() { txLen = 0; rxLen = 0; }
int available() override { return SerialUSB.available(); }
int read() override {
int b = SerialUSB.read();
if (b >= 0) { rxTotal++; if (rxLen < sizeof(rxBuf)) rxBuf[rxLen++] = (uint8_t)b; }
return b;
}
int peek() override { return SerialUSB.peek(); }
size_t write(uint8_t b) override {
if (txLen < sizeof(txBuf)) txBuf[txLen++] = b;
return 1;
}
size_t write(const uint8_t* buf, size_t size) override {
for (size_t i = 0; i < size && txLen < sizeof(txBuf); i++) txBuf[txLen++] = buf[i];
return size;
}
void flush() override {
if (txLen > 0) SerialUSB.write(txBuf, txLen);
// txLen nicht zurücksetzen — Debug-Code liest txBuf danach noch
}
using Print::write;
} modbusSerial;
// ============================================================
// Sensor-Definitionen
// ============================================================
struct Sensor {
const char* id;
const char* name;
uint16_t address;
bool isDword;
float scale;
const char* unit;
const char* deviceClass;
const char* stateClass;
const char* icon;
};
#if defined(INVERTER_MIC1500)
const Sensor SENSORS[] = {
{"pv_voltage", "PV Voltage", 3, false, 0.1f, "V", "voltage", "measurement", "mdi:solar-panel"},
{"pv_current", "PV Current", 4, false, 0.1f, "A", "current", "measurement", "mdi:solar-panel"},
{"pv_power", "PV Power", 5, true, 0.1f, "W", "power", "measurement", "mdi:solar-panel"},
{"grid_frequency", "Grid Frequency", 37, false, 0.01f, "Hz", "frequency", "measurement", "mdi:sine-wave"},
{"grid_voltage", "Grid Voltage", 38, false, 0.1f, "V", "voltage", "measurement", "mdi:flash"},
{"grid_current", "Grid Current", 39, false, 0.1f, "A", "current", "measurement", "mdi:flash"},
{"ac_power", "AC Output Power", 40, true, 0.1f, "W", "power", "measurement", "mdi:flash"},
{"energy_today", "Energy Today", 53, true, 0.1f, "kWh", "energy", "total_increasing", "mdi:solar-power"},
{"energy_total", "Energy Total", 55, true, 0.1f, "kWh", "energy", "total_increasing", "mdi:solar-power"},
{"inverter_temp", "Inverter Temperature", 93, false, 0.1f, "°C", "temperature", "measurement", "mdi:thermometer"},
};
#elif defined(INVERTER_SPH5000)
const Sensor SENSORS[] = {
{"pv1_voltage", "PV1 Voltage", 3, false, 0.1f, "V", "voltage", "measurement", "mdi:solar-panel"},
{"pv1_current", "PV1 Current", 4, false, 0.1f, "A", "current", "measurement", "mdi:solar-panel"},
{"pv1_power", "PV1 Power", 5, true, 0.1f, "W", "power", "measurement", "mdi:solar-panel"},
{"pv2_voltage", "PV2 Voltage", 7, false, 0.1f, "V", "voltage", "measurement", "mdi:solar-panel"},
{"pv2_current", "PV2 Current", 8, false, 0.1f, "A", "current", "measurement", "mdi:solar-panel"},
{"pv2_power", "PV2 Power", 9, true, 0.1f, "W", "power", "measurement", "mdi:solar-panel"},
{"ac_power_total", "AC Output Power Total", 35, true, 0.1f, "W", "power", "measurement", "mdi:flash"},
{"grid_frequency", "Grid Frequency", 37, false, 0.01f, "Hz", "frequency", "measurement", "mdi:sine-wave"},
{"grid_voltage_l1", "Grid Voltage L1", 38, false, 0.1f, "V", "voltage", "measurement", "mdi:flash"},
{"grid_current_l1", "Grid Current L1", 39, false, 0.1f, "A", "current", "measurement", "mdi:flash"},
{"grid_voltage_l2", "Grid Voltage L2", 42, false, 0.1f, "V", "voltage", "measurement", "mdi:flash"},
{"grid_current_l2", "Grid Current L2", 43, false, 0.1f, "A", "current", "measurement", "mdi:flash"},
{"grid_voltage_l3", "Grid Voltage L3", 46, false, 0.1f, "V", "voltage", "measurement", "mdi:flash"},
{"grid_current_l3", "Grid Current L3", 47, false, 0.1f, "A", "current", "measurement", "mdi:flash"},
{"energy_today", "Energy Today", 53, true, 0.1f, "kWh", "energy", "total_increasing", "mdi:solar-power"},
{"energy_total", "Energy Total", 55, true, 0.1f, "kWh", "energy", "total_increasing", "mdi:solar-power"},
{"inverter_temp", "Inverter Temperature", 93, false, 0.1f, "°C", "temperature", "measurement", "mdi:thermometer"},
{"bat_discharge_power", "Battery Discharge Power", 1009, true, 0.1f, "W", "power", "measurement", "mdi:battery-minus"},
{"bat_charge_power", "Battery Charge Power", 1011, true, 0.1f, "W", "power", "measurement", "mdi:battery-plus"},
{"bat_voltage", "Battery Voltage", 1013, false, 0.1f, "V", "voltage", "measurement", "mdi:battery"},
{"bat_soc", "Battery State of Charge", 1014, false, 1.0f, "%", "battery", "measurement", "mdi:battery"},
{"bat_temperature", "Battery Temperature", 1040, false, 0.1f, "°C", "temperature", "measurement", "mdi:thermometer"},
{"power_to_grid", "Power To Grid", 1021, true, 0.1f, "W", "power", "measurement", "mdi:transmission-tower-export"},
{"power_to_user", "Power To User", 1029, true, 0.1f, "W", "power", "measurement", "mdi:transmission-tower-import"},
{"energy_import_total", "Energy Import Total", 1046, true, 0.1f, "kWh", "energy", "total_increasing", "mdi:transmission-tower-import"},
{"energy_export_total", "Energy Export Total", 1050, true, 0.1f, "kWh", "energy", "total_increasing", "mdi:transmission-tower-export"},
{"bat_discharge_total", "Battery Discharge Total", 1054, true, 0.1f, "kWh", "energy", "total_increasing", "mdi:battery-minus"},
{"bat_charge_total", "Battery Charge Total", 1058, true, 0.1f, "kWh", "energy", "total_increasing", "mdi:battery-plus"},
};
#else
#error "Kein Inverter-Modell definiert! INVERTER_MIC1500 oder INVERTER_SPH5000 in config.h setzen."
#endif
const uint8_t SENSOR_COUNT = sizeof(SENSORS) / sizeof(SENSORS[0]);
// ============================================================
// Globale Objekte
// ============================================================
NetConfig cfg;
byte mac[] = {MAC_ADDRESS};
EthernetClient ethClient;
PubSubClient mqtt(ethClient);
ModbusMaster modbus;
EthernetServer webServer(80);
uint32_t g_okTotal = 0;
uint32_t g_failTotal = 0;
uint8_t g_lastErr = 0;
uint32_t g_rawTotal = 0;
// ============================================================
// Web-Server
// ============================================================
static bool webReadLine(EthernetClient& c, char* buf, size_t maxLen) {
size_t i = 0;
uint32_t t = millis();
while (millis() - t < 1000) {
while (c.available()) {
char ch = c.read();
if (ch == '\n') { buf[i] = 0; return true; }
if (ch != '\r' && i < maxLen - 1) buf[i++] = ch;
t = millis();
}
}
buf[i] = 0;
return false;
}
static void ipToStr(const uint8_t ip[4], char* buf) {
snprintf(buf, 16, "%u.%u.%u.%u", ip[0], ip[1], ip[2], ip[3]);
}
static void urlDecode(char* s) {
char* out = s;
while (*s) {
if (*s == '+') {
*out++ = ' '; s++;
} else if (*s == '%' && s[1] && s[2]) {
char h[3] = {s[1], s[2], 0};
*out++ = (char)strtol(h, nullptr, 16);
s += 3;
} else {
*out++ = *s++;
}
}
*out = 0;
}
static void parseIP(const char* s, uint8_t out[4]) {
unsigned a = 0, b = 0, c = 0, d = 0;
if (sscanf(s, "%u.%u.%u.%u", &a, &b, &c, &d) == 4) {
out[0] = a; out[1] = b; out[2] = c; out[3] = d;
}
}
static void parseFormBody(char* body) {
cfg.useDhcp = false; // Checkbox fehlt im POST wenn nicht gesetzt
char* p = body;
while (p && *p) {
char* eq = strchr(p, '=');
char* amp = strchr(p, '&');
if (!eq) break;
*eq = 0;
if (amp) *amp = 0;
char* key = p;
char* val = eq + 1;
urlDecode(key);
urlDecode(val);
if (!strcmp(key, "dhcp")) cfg.useDhcp = true;
else if (!strcmp(key, "ip")) parseIP(val, cfg.ip);
else if (!strcmp(key, "gw")) parseIP(val, cfg.gw);
else if (!strcmp(key, "sn")) parseIP(val, cfg.sn);
else if (!strcmp(key, "dns")) parseIP(val, cfg.dns);
else if (!strcmp(key, "broker")) strncpy(cfg.mqttBroker, val, sizeof(cfg.mqttBroker) - 1);
else if (!strcmp(key, "port")) cfg.mqttPort = (uint16_t)atoi(val);
else if (!strcmp(key, "user")) strncpy(cfg.mqttUser, val, sizeof(cfg.mqttUser) - 1);
else if (!strcmp(key, "pass")) strncpy(cfg.mqttPass, val, sizeof(cfg.mqttPass) - 1);
else if (!strcmp(key, "interval")) cfg.updateMs = (uint32_t)atoi(val) * 1000UL;
p = amp ? amp + 1 : nullptr;
}
}
static void sendPage(EthernetClient& cl) {
char buf[16];
cl.print(
"HTTP/1.1 200 OK\r\nContent-Type: text/html\r\nConnection: close\r\n\r\n"
"<!DOCTYPE html><html lang='de'><head>"
"<meta charset='utf-8'>"
"<meta name='viewport' content='width=device-width,initial-scale=1'>"
"<title>ShineLAN-X</title><style>"
"*{box-sizing:border-box}"
"body{font-family:Arial,sans-serif;background:#1a1a2e;color:#eee;margin:0}"
"header{background:#16213e;padding:12px 18px;display:flex;align-items:center;"
"border-bottom:2px solid #e94560}"
"header h1{margin:0;font-size:1.1em;color:#e94560;flex:1}"
".sub{font-size:.78em;color:#64748b;margin-top:2px}"
".badge{padding:3px 10px;border-radius:10px;font-size:.78em;font-weight:bold}"
".on{background:#14532d;color:#4ade80}"
".off{background:#450a0a;color:#f87171}"
"main{max-width:520px;margin:14px auto;padding:0 12px}"
".stats{display:grid;grid-template-columns:repeat(3,1fr);gap:8px;margin-bottom:12px}"
".sbox{background:#16213e;border-radius:6px;padding:9px;text-align:center}"
".sval{font-size:1.4em;font-weight:bold;color:#e94560}"
".slbl{font-size:.7em;color:#64748b;margin-top:2px}"
".card{background:#16213e;border-radius:7px;padding:14px;margin-bottom:12px}"
".card h2{margin:0 0 12px;font-size:.72em;text-transform:uppercase;"
"letter-spacing:.08em;color:#64748b;"
"border-bottom:1px solid #0f3460;padding-bottom:7px}"
".row{display:flex;align-items:center;gap:8px;margin-bottom:9px}"
".row label{min-width:115px;font-size:.84em;color:#94a3b8}"
".row input[type=text],.row input[type=number],.row input[type=password]"
"{flex:1;background:#0f3460;border:1px solid #1e4a80;color:#eee;"
"padding:6px 9px;border-radius:4px;font-size:.84em;min-width:0}"
".row input[type=checkbox]{width:16px;height:16px;accent-color:#e94560;cursor:pointer}"
".btn{width:100%;padding:10px;border:none;border-radius:6px;"
"font-size:.88em;cursor:pointer;margin-top:5px;font-weight:bold}"
".save{background:#e94560;color:#fff}"
".save:hover{background:#c73652}"
".rst{background:#334155;color:#eee}"
".rst:hover{background:#475569}"
"</style></head><body>"
);
// Header
cl.print("<header><div><h1>&#9728; ShineLAN-X</h1>"
"<div class='sub'>Growatt Modbus &#x2192; MQTT Bridge</div></div>"
"<div style='text-align:right'><span class='badge ");
cl.print(mqtt.connected() ? "on'>Verbunden" : "off'>Getrennt");
cl.print("</span><div class='sub'>IP: ");
cl.print(Ethernet.localIP());
cl.print("</div></div></header><main>");
// Statistik
cl.print("<div class='stats'>"
"<div class='sbox'><div class='sval'>");
cl.print(g_okTotal);
cl.print("</div><div class='slbl'>OK</div></div>"
"<div class='sbox'><div class='sval'>");
cl.print(g_failTotal);
cl.print("</div><div class='slbl'>Fehler</div></div>"
"<div class='sbox'><div class='sval'>0x");
snprintf(buf, sizeof(buf), "%02X", g_lastErr);
cl.print(buf);
cl.print("</div><div class='slbl'>Letzter Err</div></div></div>");
// Formular — Netzwerk
cl.print("<form method='post' action='/'>"
"<div class='card'><h2>Netzwerk</h2>"
"<div class='row'><label>DHCP</label>"
"<input type='checkbox' id='dc' name='dhcp'");
if (cfg.useDhcp) cl.print(" checked");
cl.print(" onchange='tog()'></div><div id='st'>");
ipToStr(cfg.ip, buf);
cl.print("<div class='row'><label>IP-Adresse</label>"
"<input type='text' name='ip' value='"); cl.print(buf); cl.print("'></div>");
ipToStr(cfg.gw, buf);
cl.print("<div class='row'><label>Gateway</label>"
"<input type='text' name='gw' value='"); cl.print(buf); cl.print("'></div>");
ipToStr(cfg.sn, buf);
cl.print("<div class='row'><label>Subnet</label>"
"<input type='text' name='sn' value='"); cl.print(buf); cl.print("'></div>");
ipToStr(cfg.dns, buf);
cl.print("<div class='row'><label>DNS</label>"
"<input type='text' name='dns' value='"); cl.print(buf); cl.print("'></div>");
cl.print("</div></div>");
// Formular — MQTT
cl.print("<div class='card'><h2>MQTT</h2>"
"<div class='row'><label>Broker</label>"
"<input type='text' name='broker' value='");
cl.print(cfg.mqttBroker);
cl.print("'></div>"
"<div class='row'><label>Port</label>"
"<input type='number' name='port' value='");
cl.print(cfg.mqttPort);
cl.print("' min='1' max='65535'></div>"
"<div class='row'><label>Benutzer</label>"
"<input type='text' name='user' value='");
cl.print(cfg.mqttUser);
cl.print("' autocomplete='off'></div>"
"<div class='row'><label>Passwort</label>"
"<input type='password' name='pass' value='");
cl.print(cfg.mqttPass);
cl.print("' autocomplete='off'></div>"
"<div class='row'><label>Intervall (s)</label>"
"<input type='number' name='interval' value='");
cl.print(cfg.updateMs / 1000UL);
cl.print("' min='1' max='3600'></div></div>");
// Buttons + Script
cl.print(
"<button class='btn save' type='submit'>"
"&#128190; Speichern &amp; Neustart</button>"
"<button class='btn rst' type='button' "
"onclick='if(confirm(\"Wirklich auf Werkseinstellungen zur\\u00FCcksetzen?\"))"
"location=\"/reset\"'>&#8635; Werkseinstellungen</button>"
"</form></main>"
"<script>"
"function tog(){"
"document.getElementById('st').style.display="
"document.getElementById('dc').checked?'none':'block';}"
"window.onload=tog;"
"</script></body></html>"
);
}
static void handleWebRequest() {
EthernetClient client = webServer.available();
if (!client) return;
char line[80];
if (!webReadLine(client, line, sizeof(line))) { client.stop(); return; }
bool isPost = (strncmp(line, "POST", 4) == 0);
bool isReset = (strstr(line, "/reset") != nullptr);
// Header lesen, Content-Length suchen (case-insensitive via tolower)
int contentLen = 0;
char hdr[80];
while (webReadLine(client, hdr, sizeof(hdr)) && hdr[0] != '\0') {
for (char* p = hdr; *p; p++) *p = (char)tolower((unsigned char)*p);
if (strncmp(hdr, "content-length:", 15) == 0)
contentLen = atoi(hdr + 15);
}
if (isReset) {
configDefaults(cfg);
configSave(cfg);
client.print("HTTP/1.1 303 See Other\r\nLocation: /\r\nConnection: close\r\n\r\n");
client.flush(); client.stop();
delay(300);
NVIC_SystemReset();
return;
}
if (isPost && contentLen > 0 && contentLen < 500) {
char body[500] = {};
int n = 0;
uint32_t t = millis();
while (n < contentLen && millis() - t < 3000) {
if (client.available()) { body[n++] = client.read(); t = millis(); }
}
body[n] = 0;
parseFormBody(body);
cfg.magic = CFG_MAGIC;
configSave(cfg);
client.print("HTTP/1.1 303 See Other\r\nLocation: /\r\nConnection: close\r\n\r\n");
client.flush(); client.stop();
delay(300);
NVIC_SystemReset();
return;
}
sendPage(client);
client.flush();
client.stop();
}
// ============================================================
// LED
// ============================================================
void ledSet(uint8_t pin, bool on) { digitalWrite(pin, on ? LOW : HIGH); }
// ============================================================
// MQTT
// ============================================================
void publishDiscovery() {
char topic[128];
char payload[640];
for (uint8_t i = 0; i < SENSOR_COUNT; i++) {
const Sensor& s = SENSORS[i];
snprintf(topic, sizeof(topic),
"homeassistant/sensor/%s_%s/config", DEVICE_ID, s.id);
snprintf(payload, sizeof(payload),
"{"
"\"name\":\"%s\","
"\"unique_id\":\"%s_%s\","
"\"state_topic\":\"growatt/shinelan/%s\","
"\"unit_of_measurement\":\"%s\","
"\"device_class\":\"%s\","
"\"state_class\":\"%s\","
"\"icon\":\"%s\","
"\"device\":{"
"\"identifiers\":[\"%s\"],"
"\"name\":\"%s\","
"\"model\":\"%s\","
"\"manufacturer\":\"%s\""
"}}",
s.name,
DEVICE_ID, s.id,
s.id,
s.unit, s.deviceClass, s.stateClass, s.icon,
DEVICE_ID, DEVICE_NAME, DEVICE_MODEL, DEVICE_MFR);
mqtt.publish(topic, payload, true);
}
}
bool mqttReconnect() {
DebugSerial.print("MQTT connecting... ");
ledSet(LED_RED, true);
bool ok = (strlen(cfg.mqttUser) > 0)
? mqtt.connect(MQTT_CLIENT, cfg.mqttUser, cfg.mqttPass)
: mqtt.connect(MQTT_CLIENT);
if (ok) {
DebugSerial.println("OK");
ledSet(LED_RED, false);
ledSet(LED_GREEN, true);
publishDiscovery();
} else {
DebugSerial.print("FAIL rc=");
DebugSerial.println(mqtt.state());
}
return ok;
}
// ============================================================
// Setup
// ============================================================
void setup() {
DebugSerial.begin(115200);
delay(10);
DebugSerial.println("\r\n=== Growatt ShineLAN-X ===");
DebugSerial.println("Build: " __DATE__ " " __TIME__);
configLoad(cfg);
pinMode(LED_DEBUG, OUTPUT); ledSet(LED_DEBUG, false);
pinMode(LED_RED, OUTPUT); ledSet(LED_RED, false);
pinMode(LED_GREEN, OUTPUT); ledSet(LED_GREEN, false);
pinMode(LED_BLUE, OUTPUT); ledSet(LED_BLUE, false);
ledSet(LED_RED, true); ledSet(LED_GREEN, true); ledSet(LED_BLUE, true);
delay(300);
ledSet(LED_RED, false); ledSet(LED_GREEN, false); ledSet(LED_BLUE, false);
pinMode(ETH_RST_PIN, OUTPUT);
digitalWrite(ETH_RST_PIN, LOW); delay(20);
digitalWrite(ETH_RST_PIN, HIGH); delay(200);
Ethernet.init(ETH_CS_PIN);
if (cfg.useDhcp) {
DebugSerial.println("ETH: DHCP...");
if (Ethernet.begin(mac) == 0) {
// DHCP fehlgeschlagen: Fallback auf gespeicherte statische IP
DebugSerial.println("ETH: DHCP failed, static fallback");
IPAddress ip (cfg.ip[0], cfg.ip[1], cfg.ip[2], cfg.ip[3]);
IPAddress gw (cfg.gw[0], cfg.gw[1], cfg.gw[2], cfg.gw[3]);
IPAddress sn (cfg.sn[0], cfg.sn[1], cfg.sn[2], cfg.sn[3]);
IPAddress dns(cfg.dns[0], cfg.dns[1], cfg.dns[2], cfg.dns[3]);
Ethernet.begin(mac, ip, dns, gw, sn);
}
} else {
IPAddress ip (cfg.ip[0], cfg.ip[1], cfg.ip[2], cfg.ip[3]);
IPAddress gw (cfg.gw[0], cfg.gw[1], cfg.gw[2], cfg.gw[3]);
IPAddress sn (cfg.sn[0], cfg.sn[1], cfg.sn[2], cfg.sn[3]);
IPAddress dns(cfg.dns[0], cfg.dns[1], cfg.dns[2], cfg.dns[3]);
Ethernet.begin(mac, ip, dns, gw, sn);
}
DebugSerial.print("ETH: IP=");
DebugSerial.println(Ethernet.localIP());
if (Ethernet.linkStatus() == LinkON) ledSet(LED_DEBUG, true);
webServer.begin();
DebugSerial.print("Web: http://");
DebugSerial.println(Ethernet.localIP());
mqtt.setServer(cfg.mqttBroker, cfg.mqttPort);
mqtt.setBufferSize(768);
SerialUSB.begin(MODBUS_BAUD);
delay(10000);
HAL_GPIO_WritePin(GPIOA, GPIO_PIN_8, GPIO_PIN_RESET);
// Warte bis Wechselrichter USB-CDC konfiguriert hat
delay(3000);
// SERIAL_STATE(DCD=1,DSR=1) senden — NuttX tut dies beim Öffnen von ttyACM0
// signalisiert dem Wechselrichter dass das Device bereit ist
sendSerialStateNotification();
delay(500);
modbus.begin(MODBUS_ADDR, modbusSerial);
DebugSerial.println("Setup done.");
}
// ============================================================
// Loop
// ============================================================
unsigned long lastUpdate = 0;
static bool g_usbInfoPublished = false;
void loop() {
handleWebRequest();
if (!mqtt.connected()) {
if (!mqttReconnect()) {
delay(5000);
return;
}
}
mqtt.loop();
handleWebRequest();
// usbinfo: initial publish + DTR-Änderungen
{
static bool lastDtr = false;
if (!g_usbInfoPublished || (bool)dtrState != lastDtr) {
g_usbInfoPublished = true;
lastDtr = (bool)dtrState;
char info[80];
snprintf(info, sizeof(info), "baud=%lu fmt=%u par=%u bits=%u dtr=%d rts=%d",
(unsigned long)linecoding.bitrate, linecoding.format,
linecoding.paritytype, linecoding.datatype,
(int)dtrState, (int)rtsState);
mqtt.publish("growatt/shinelan/usbinfo", info, true);
}
}
// SERIAL_STATE(DCD=1,DSR=1) alle 5s senden bis DTR vom Wechselrichter gesetzt wird
{
static uint32_t lastNotif = 0;
if (!dtrState && millis() - lastNotif > 5000) {
lastNotif = millis();
sendSerialStateNotification();
}
}
// Raw-RX-Drain: jedes Byte das außerhalb von Modbus-Fenstern ankommt fangen
// Beantwortet: sendet der Inverter JEMALS irgendwas (spontan oder verspätet)?
{
static uint8_t rawBuf[32] = {};
static uint8_t rawLen = 0;
static uint32_t rawPublish = 0;
while (SerialUSB.available()) {
uint8_t b = (uint8_t)SerialUSB.read();
g_rawTotal++;
if (rawLen < sizeof(rawBuf)) rawBuf[rawLen++] = b;
}
if (rawLen > 0 && millis() - rawPublish > 1000) {
rawPublish = millis();
char msg[80];
size_t p = snprintf(msg, sizeof(msg), "raw=%lu: ", g_rawTotal);
for (uint8_t i = 0; i < rawLen && p < 74; i++)
p += snprintf(msg + p, sizeof(msg) - p, "%02X", rawBuf[i]);
mqtt.publish("growatt/shinelan/rawbytes", msg);
rawLen = 0;
}
}
if (millis() - lastUpdate < cfg.updateMs) return;
lastUpdate = millis();
ledSet(LED_BLUE, true);
uint32_t okCnt = 0, failCnt = 0;
uint8_t lastErr = 0;
uint16_t lastErrReg = 0;
for (uint8_t i = 0; i < SENSOR_COUNT; i++) {
const Sensor& s = SENSORS[i];
modbusSerial.reset();
uint8_t res = modbus.readInputRegisters(s.address, s.isDword ? 2 : 1);
char dbg[80];
size_t pos = snprintf(dbg, sizeof(dbg), "ok=%lu fail=%lu err=0x%02X@%u avS=%d dtr=%d raw=%lu rx=%u tx=",
g_okTotal + okCnt, g_failTotal + failCnt,
lastErr, lastErrReg,
SerialUSB.availableForWrite(),
(int)dtrState,
g_rawTotal,
modbusSerial.rxLen);
for (uint8_t j = 0; j < modbusSerial.txLen && pos < 74; j++)
pos += snprintf(dbg + pos, sizeof(dbg) - pos, "%02X", modbusSerial.txBuf[j]);
pos += snprintf(dbg + pos, sizeof(dbg) - pos, " rx=");
for (uint8_t j = 0; j < modbusSerial.rxLen && pos < 78; j++)
pos += snprintf(dbg + pos, sizeof(dbg) - pos, "%02X", modbusSerial.rxBuf[j]);
mqtt.publish("growatt/shinelan/debug", dbg);
delay(10); // mbusd -R10: 10ms Pause zwischen Requests
if (res != modbus.ku8MBSuccess) {
failCnt++;
lastErr = res;
lastErrReg = s.address;
continue;
}
okCnt++;
float val;
if (s.isDword) {
uint32_t raw = ((uint32_t)modbus.getResponseBuffer(0) << 16)
| modbus.getResponseBuffer(1);
val = raw * s.scale;
} else {
val = modbus.getResponseBuffer(0) * s.scale;
}
char topic[64], payload[24];
snprintf(topic, sizeof(topic), "growatt/shinelan/%s", s.id);
snprintf(payload, sizeof(payload), "%.1f", (double)val);
mqtt.publish(topic, payload);
}
g_okTotal += okCnt;
g_failTotal += failCnt;
g_lastErr = lastErr;
ledSet(LED_BLUE, false);
}
View File
Binary file not shown.
@@ -1,212 +0,0 @@
esphome:
name: growatt-mic-1500
friendly_name: Growatt MIC 1500 TL-X
esp8266:
board: esp07s
logger:
api:
encryption:
key: "HIER_API_KEY_EINTRAGEN" # 32-Byte Base64-Key, wird in der ESPHome-UI automatisch generiert
ota:
- platform: esphome
password: "HIER_OTA_PASSWORT_EINTRAGEN" # Beliebiges Passwort für Over-the-Air Updates
wifi:
ssid: "HIER_WLAN_NAME_EINTRAGEN" # Name deines WLANs (SSID)
password: "HIER_WLAN_PASSWORT_EINTRAGEN" # Passwort deines WLANs
ap:
ssid: "Growatt MIC 1500 TL-X Fallback Hotspot"
password: "HIER_HOTSPOT_PASSWORT_EINTRAGEN" # Passwort für den Notfall-Hotspot (min. 8 Zeichen)
captive_portal:
time:
- platform: homeassistant
id: homeassistant_time
output:
- id: light_bl # Blaue LED
platform: gpio
pin: 16
- id: light_gr # Grüne LED
platform: gpio
pin: 0
- id: light_rd # Rote LED
platform: gpio
pin: 2
uart:
id: mod_bus
tx_pin: 1
rx_pin: 3
baud_rate: 115200
modbus:
id: modbus1
uart_id: mod_bus
modbus_controller:
- id: growatt
address: 1
modbus_id: modbus1
setup_priority: -10
on_online:
then:
- output.turn_on: light_gr
on_offline:
then:
- output.turn_off: light_gr
script:
- id: blink_blue
mode: single
then:
- output.turn_on: light_bl
- delay: 200ms
- output.turn_off: light_bl
sensor:
# --- PV Eingang ---
- platform: modbus_controller
name: "PV Voltage"
address: 3
register_type: read
unit_of_measurement: V
device_class: voltage
state_class: measurement
icon: mdi:solar-panel
value_type: U_WORD
accuracy_decimals: 1
filters:
- multiply: 0.1
on_value:
then:
- script.execute: blink_blue
- platform: modbus_controller
name: "PV Current"
address: 4
register_type: read
unit_of_measurement: A
device_class: current
state_class: measurement
icon: mdi:solar-panel
value_type: U_WORD
accuracy_decimals: 1
filters:
- multiply: 0.1
- platform: modbus_controller
name: "PV Power"
address: 5
register_type: read
unit_of_measurement: W
device_class: power
state_class: measurement
icon: mdi:solar-panel
value_type: U_DWORD
accuracy_decimals: 1
filters:
- multiply: 0.1
# --- AC Ausgang / Netz ---
- platform: modbus_controller
name: "Grid Frequency"
address: 37
register_type: read
unit_of_measurement: Hz
device_class: frequency
state_class: measurement
icon: mdi:sine-wave
value_type: U_WORD
accuracy_decimals: 2
filters:
- multiply: 0.01
- platform: modbus_controller
name: "Grid Voltage"
address: 38
register_type: read
unit_of_measurement: V
device_class: voltage
state_class: measurement
icon: mdi:flash
value_type: U_WORD
accuracy_decimals: 1
filters:
- multiply: 0.1
- platform: modbus_controller
name: "Grid Current"
address: 39
register_type: read
unit_of_measurement: A
device_class: current
state_class: measurement
icon: mdi:flash
value_type: U_WORD
accuracy_decimals: 1
filters:
- multiply: 0.1
- platform: modbus_controller
name: "AC Output Power"
address: 40
register_type: read
unit_of_measurement: W
device_class: power
state_class: measurement
icon: mdi:flash
value_type: U_DWORD
accuracy_decimals: 1
filters:
- multiply: 0.1
# --- Energie (PV Erzeugung) ---
- platform: modbus_controller
name: "Energy Today"
address: 53
register_type: read
unit_of_measurement: kWh
device_class: energy
state_class: total_increasing
icon: mdi:solar-power
value_type: U_DWORD
accuracy_decimals: 1
filters:
- multiply: 0.1
- platform: modbus_controller
name: "Energy Total"
address: 55
register_type: read
unit_of_measurement: kWh
device_class: energy
state_class: total_increasing
icon: mdi:solar-power
value_type: U_DWORD
accuracy_decimals: 1
filters:
- multiply: 0.1
# --- Temperatur ---
- platform: modbus_controller
name: "Inverter Temperature"
address: 93
register_type: read
unit_of_measurement: "°C"
device_class: temperature
state_class: measurement
icon: mdi:thermometer
value_type: U_WORD
accuracy_decimals: 1
filters:
- multiply: 0.1
@@ -1,212 +0,0 @@
esphome:
name: growatt-mic-2000
friendly_name: Growatt MIC 2000 TL-X
esp8266:
board: esp07s
logger:
api:
encryption:
key: "HIER_API_KEY_EINTRAGEN"
ota:
- platform: esphome
password: "HIER_OTA_PASSWORT_EINTRAGEN"
wifi:
ssid: "HIER_WLAN_NAME_EINTRAGEN"
password: "HIER_WLAN_PASSWORT_EINTRAGEN"
ap:
ssid: "Growatt MIC 2000 TL-X Fallback Hotspot"
password: "HIER_HOTSPOT_PASSWORT_EINTRAGEN"
captive_portal:
time:
- platform: homeassistant
id: homeassistant_time
output:
- id: light_bl
platform: gpio
pin: 16
- id: light_gr
platform: gpio
pin: 0
- id: light_rd
platform: gpio
pin: 2
uart:
id: mod_bus
tx_pin: 1
rx_pin: 3
baud_rate: 115200
modbus:
id: modbus1
uart_id: mod_bus
modbus_controller:
- id: growatt
address: 1
modbus_id: modbus1
setup_priority: -10
on_online:
then:
- output.turn_on: light_gr
on_offline:
then:
- output.turn_off: light_gr
script:
- id: blink_blue
mode: single
then:
- output.turn_on: light_bl
- delay: 200ms
- output.turn_off: light_bl
sensor:
# --- PV Eingang ---
- platform: modbus_controller
name: "PV Voltage"
address: 3
register_type: read
unit_of_measurement: V
device_class: voltage
state_class: measurement
icon: mdi:solar-panel
value_type: U_WORD
accuracy_decimals: 1
filters:
- multiply: 0.1
on_value:
then:
- script.execute: blink_blue
- platform: modbus_controller
name: "PV Current"
address: 4
register_type: read
unit_of_measurement: A
device_class: current
state_class: measurement
icon: mdi:solar-panel
value_type: U_WORD
accuracy_decimals: 1
filters:
- multiply: 0.1
- platform: modbus_controller
name: "PV Power"
address: 5
register_type: read
unit_of_measurement: W
device_class: power
state_class: measurement
icon: mdi:solar-panel
value_type: U_DWORD
accuracy_decimals: 1
filters:
- multiply: 0.1
# --- AC Ausgang / Netz ---
- platform: modbus_controller
name: "Grid Frequency"
address: 37
register_type: read
unit_of_measurement: Hz
device_class: frequency
state_class: measurement
icon: mdi:sine-wave
value_type: U_WORD
accuracy_decimals: 2
filters:
- multiply: 0.01
- platform: modbus_controller
name: "Grid Voltage"
address: 38
register_type: read
unit_of_measurement: V
device_class: voltage
state_class: measurement
icon: mdi:flash
value_type: U_WORD
accuracy_decimals: 1
filters:
- multiply: 0.1
- platform: modbus_controller
name: "Grid Current"
address: 39
register_type: read
unit_of_measurement: A
device_class: current
state_class: measurement
icon: mdi:flash
value_type: U_WORD
accuracy_decimals: 1
filters:
- multiply: 0.1
- platform: modbus_controller
name: "AC Output Power"
address: 40
register_type: read
unit_of_measurement: W
device_class: power
state_class: measurement
icon: mdi:flash
value_type: U_DWORD
accuracy_decimals: 1
filters:
- multiply: 0.1
# --- Energie (PV Erzeugung) ---
- platform: modbus_controller
name: "Energy Today"
address: 53
register_type: read
unit_of_measurement: kWh
device_class: energy
state_class: total_increasing
icon: mdi:solar-power
value_type: U_DWORD
accuracy_decimals: 1
filters:
- multiply: 0.1
- platform: modbus_controller
name: "Energy Total"
address: 55
register_type: read
unit_of_measurement: kWh
device_class: energy
state_class: total_increasing
icon: mdi:solar-power
value_type: U_DWORD
accuracy_decimals: 1
filters:
- multiply: 0.1
# --- Temperatur ---
- platform: modbus_controller
name: "Inverter Temperature"
address: 93
register_type: read
unit_of_measurement: "°C"
device_class: temperature
state_class: measurement
icon: mdi:thermometer
value_type: U_WORD
accuracy_decimals: 1
filters:
- multiply: 0.1
@@ -1,50 +0,0 @@
# Growatt MIC 2000 TL-X — ESPHome Konfiguration
Modbus-RTU Integration des Growatt MIC 2000 TL-X (einphasiger String-Wechselrichter) über einen Growatt SuniStick mit ESPHome-Firmware.
Gleiche Registerstruktur wie der MIC 1500 TL-X — die MIC TL-X Familie verwendet ein einheitliches Protokoll.
---
## Modbus-Register Übersicht
Alle Register sind **Input Register** (Modbus Funktionscode 04, `register_type: "read"`).
Modbus-Adresse des Geräts: `0x01`
### PV Eingang
| Name | Adresse | Typ | Multiplikator | Einheit |
|------|---------|-----|---------------|---------|
| PV Voltage | 3 | U_WORD | 0.1 | V |
| PV Current | 4 | U_WORD | 0.1 | A |
| PV Power | 5 | U_DWORD | 0.1 | W |
### AC Ausgang / Netz
| Name | Adresse | Typ | Multiplikator | Einheit |
|------|---------|-----|---------------|---------|
| Grid Frequency | 37 | U_WORD | **0.01** | Hz |
| Grid Voltage | 38 | U_WORD | 0.1 | V |
| Grid Current | 39 | U_WORD | 0.1 | A |
| AC Output Power | 40 | U_DWORD | 0.1 | W |
### Energie (PV Erzeugung)
| Name | Adresse | Typ | Multiplikator | Einheit |
|------|---------|-----|---------------|---------|
| Energy Today | 53 | U_DWORD | 0.1 | kWh |
| Energy Total | 55 | U_DWORD | 0.1 | kWh |
### Temperatur
| Name | Adresse | Typ | Multiplikator | Einheit |
|------|---------|-----|---------------|---------|
| Inverter Temperature | 93 | U_WORD | 0.1 | °C |
---
## Hinweise
- **Einphasig, kein Hybrid, kein Batterie-Interface** — keine Register >100 vorhanden.
- Register identisch mit MIC 1500 TL-X. Andere MIC TL-X Modelle (1000W, 1500W, 2000W) verwenden dieselbe Registerstruktur.
- Passwörter und API-Keys sind durch Platzhalter ersetzt und müssen vor dem Einsatz angepasst werden.
@@ -1,331 +0,0 @@
esphome:
name: growatt-mod-6000
friendly_name: Growatt MOD 6000 TL3-XH
esp8266:
board: esp07s
logger:
api:
encryption:
key: "HIER_API_KEY_EINTRAGEN"
ota:
- platform: esphome
password: "HIER_OTA_PASSWORT_EINTRAGEN"
wifi:
ssid: "HIER_WLAN_NAME_EINTRAGEN"
password: "HIER_WLAN_PASSWORT_EINTRAGEN"
ap:
ssid: "Growatt MOD 6000 Fallback Hotspot"
password: "HIER_HOTSPOT_PASSWORT_EINTRAGEN"
captive_portal:
time:
- platform: homeassistant
id: homeassistant_time
output:
- id: light_bl
platform: gpio
pin: 16
- id: light_gr
platform: gpio
pin: 0
- id: light_rd
platform: gpio
pin: 2
uart:
id: mod_bus
tx_pin: 1
rx_pin: 3
baud_rate: 115200
modbus:
id: modbus1
uart_id: mod_bus
modbus_controller:
- id: growatt
address: 1
modbus_id: modbus1
setup_priority: -10
on_online:
then:
- output.turn_on: light_gr
on_offline:
then:
- output.turn_off: light_gr
script:
- id: blink_blue
mode: single
then:
- output.turn_on: light_bl
- delay: 200ms
- output.turn_off: light_bl
sensor:
# --- PV Eingang ---
- platform: modbus_controller
name: "PV1 Voltage"
address: 3003
register_type: read
unit_of_measurement: V
device_class: voltage
state_class: measurement
icon: mdi:solar-panel
value_type: U_WORD
accuracy_decimals: 1
filters:
- multiply: 0.1
on_value:
then:
- script.execute: blink_blue
- platform: modbus_controller
name: "PV1 Current"
address: 3004
register_type: read
unit_of_measurement: A
device_class: current
state_class: measurement
icon: mdi:solar-panel
value_type: U_WORD
accuracy_decimals: 1
filters:
- multiply: 0.1
- platform: modbus_controller
name: "PV1 Power"
address: 3005
register_type: read
unit_of_measurement: W
device_class: power
state_class: measurement
icon: mdi:solar-panel
value_type: U_DWORD
accuracy_decimals: 1
filters:
- multiply: 0.1
- platform: modbus_controller
name: "PV2 Voltage"
address: 3007
register_type: read
unit_of_measurement: V
device_class: voltage
state_class: measurement
icon: mdi:solar-panel
value_type: U_WORD
accuracy_decimals: 1
filters:
- multiply: 0.1
- platform: modbus_controller
name: "PV2 Current"
address: 3008
register_type: read
unit_of_measurement: A
device_class: current
state_class: measurement
icon: mdi:solar-panel
value_type: U_WORD
accuracy_decimals: 1
filters:
- multiply: 0.1
- platform: modbus_controller
name: "PV2 Power"
address: 3009
register_type: read
unit_of_measurement: W
device_class: power
state_class: measurement
icon: mdi:solar-panel
value_type: U_DWORD
accuracy_decimals: 1
filters:
- multiply: 0.1
# --- AC Ausgang / Netz (dreiphasig) ---
- platform: modbus_controller
name: "AC Output Power Total"
address: 3023
register_type: read
unit_of_measurement: W
device_class: power
state_class: measurement
icon: mdi:flash
value_type: U_DWORD
accuracy_decimals: 1
filters:
- multiply: 0.1
- platform: modbus_controller
name: "Grid Frequency"
address: 3025
register_type: read
unit_of_measurement: Hz
device_class: frequency
state_class: measurement
icon: mdi:sine-wave
value_type: U_WORD
accuracy_decimals: 2
filters:
- multiply: 0.01
- platform: modbus_controller
name: "Grid Voltage L1"
address: 3026
register_type: read
unit_of_measurement: V
device_class: voltage
state_class: measurement
icon: mdi:flash
value_type: U_WORD
accuracy_decimals: 1
filters:
- multiply: 0.1
- platform: modbus_controller
name: "Grid Current L1"
address: 3027
register_type: read
unit_of_measurement: A
device_class: current
state_class: measurement
icon: mdi:flash
value_type: U_WORD
accuracy_decimals: 1
filters:
- multiply: 0.1
- platform: modbus_controller
name: "Grid Voltage L2"
address: 3030
register_type: read
unit_of_measurement: V
device_class: voltage
state_class: measurement
icon: mdi:flash
value_type: U_WORD
accuracy_decimals: 1
filters:
- multiply: 0.1
- platform: modbus_controller
name: "Grid Current L2"
address: 3031
register_type: read
unit_of_measurement: A
device_class: current
state_class: measurement
icon: mdi:flash
value_type: U_WORD
accuracy_decimals: 1
filters:
- multiply: 0.1
- platform: modbus_controller
name: "Grid Voltage L3"
address: 3034
register_type: read
unit_of_measurement: V
device_class: voltage
state_class: measurement
icon: mdi:flash
value_type: U_WORD
accuracy_decimals: 1
filters:
- multiply: 0.1
- platform: modbus_controller
name: "Grid Current L3"
address: 3035
register_type: read
unit_of_measurement: A
device_class: current
state_class: measurement
icon: mdi:flash
value_type: U_WORD
accuracy_decimals: 1
filters:
- multiply: 0.1
# --- Energie (PV Erzeugung) ---
- platform: modbus_controller
name: "Energy Today"
address: 3049
register_type: read
unit_of_measurement: kWh
device_class: energy
state_class: total_increasing
icon: mdi:solar-power
value_type: U_DWORD
accuracy_decimals: 1
filters:
- multiply: 0.1
- platform: modbus_controller
name: "Energy Total"
address: 3051
register_type: read
unit_of_measurement: kWh
device_class: energy
state_class: total_increasing
icon: mdi:solar-power
value_type: U_DWORD
accuracy_decimals: 1
filters:
- multiply: 0.1
# --- Temperatur ---
- platform: modbus_controller
name: "Inverter Temperature"
address: 3093
register_type: read
unit_of_measurement: "°C"
device_class: temperature
state_class: measurement
icon: mdi:thermometer
value_type: U_WORD
accuracy_decimals: 1
filters:
- multiply: 0.1
# --- Smartmeter (optional, nur wenn Smartmeter angeschlossen) ---
- platform: modbus_controller
name: "Power To User"
address: 3041
register_type: read
unit_of_measurement: W
device_class: power
state_class: measurement
icon: mdi:transmission-tower-import
value_type: U_DWORD
accuracy_decimals: 1
filters:
- multiply: 0.1
- platform: modbus_controller
name: "Power To Grid"
address: 3043
register_type: read
unit_of_measurement: W
device_class: power
state_class: measurement
icon: mdi:transmission-tower-export
value_type: U_DWORD
accuracy_decimals: 1
filters:
- multiply: 0.1
@@ -1,67 +0,0 @@
# Growatt MOD 6000 TL3-XH — ESPHome Konfiguration
Modbus-RTU Integration des Growatt MOD 6000 TL3-XH (dreiphasiger String-Wechselrichter, kein Hybrid) über einen Growatt SuniStick mit ESPHome-Firmware.
---
## Modbus-Register Übersicht
Alle Register sind **Input Register** (Modbus Funktionscode 04, `register_type: "read"`).
Modbus-Adresse des Geräts: `0x01`
> **Wichtig:** Die MOD TL3-XH Serie verwendet 3000er-Register — nicht die kleinen Register der MIC/MIN-Serie.
### PV Eingang
| Name | Adresse | Typ | Multiplikator | Einheit |
|------|---------|-----|---------------|---------|
| PV1 Voltage | 3003 | U_WORD | 0.1 | V |
| PV1 Current | 3004 | U_WORD | 0.1 | A |
| PV1 Power | 3005 | U_DWORD | 0.1 | W |
| PV2 Voltage | 3007 | U_WORD | 0.1 | V |
| PV2 Current | 3008 | U_WORD | 0.1 | A |
| PV2 Power | 3009 | U_DWORD | 0.1 | W |
### AC Ausgang / Netz (dreiphasig)
| Name | Adresse | Typ | Multiplikator | Einheit |
|------|---------|-----|---------------|---------|
| AC Output Power Total | 3023 | U_DWORD | 0.1 | W |
| Grid Frequency | 3025 | U_WORD | **0.01** | Hz |
| Grid Voltage L1 | 3026 | U_WORD | 0.1 | V |
| Grid Current L1 | 3027 | U_WORD | 0.1 | A |
| Grid Voltage L2 | 3030 | U_WORD | 0.1 | V |
| Grid Current L2 | 3031 | U_WORD | 0.1 | A |
| Grid Voltage L3 | 3034 | U_WORD | 0.1 | V |
| Grid Current L3 | 3035 | U_WORD | 0.1 | A |
### Energie (PV Erzeugung)
| Name | Adresse | Typ | Multiplikator | Einheit |
|------|---------|-----|---------------|---------|
| Energy Today | 3049 | U_DWORD | 0.1 | kWh |
| Energy Total | 3051 | U_DWORD | 0.1 | kWh |
### Temperatur
| Name | Adresse | Typ | Multiplikator | Einheit |
|------|---------|-----|---------------|---------|
| Inverter Temperature | 3093 | U_WORD | 0.1 | °C |
### Smartmeter (optional)
Nur relevant wenn ein Smartmeter am Wechselrichter angeschlossen ist.
| Name | Adresse | Typ | Multiplikator | Einheit |
|------|---------|-----|---------------|---------|
| Power To User (Bezug) | 3041 | U_DWORD | 0.1 | W |
| Power To Grid (Einspeisung) | 3043 | U_DWORD | 0.1 | W |
---
## Hinweise
- **Dreiphasig, kein Hybrid, kein Batterie-Interface.**
- Registerstruktur verifiziert gegen Growatt MOD TL3-XH Modbus-Protokoll (gleiche Familie wie MOD 7000 TL3-XH).
- Ohne angeschlossenen Smartmeter liefern Register 3041/3043 Nullwerte — diese Sensoren können dann entfernt werden.
- Passwörter und API-Keys sind durch Platzhalter ersetzt und müssen vor dem Einsatz angepasst werden.
@@ -1,452 +0,0 @@
esphome:
name: growatt-sph5000
friendly_name: Growatt SPH 5000 TL3-BH-UP
esp8266:
board: esp07s
logger:
api:
encryption:
key: "HIER_API_KEY_EINTRAGEN" # 32-Byte Base64-Key, wird in der ESPHome-UI automatisch generiert
ota:
- platform: esphome
password: "HIER_OTA_PASSWORT_EINTRAGEN" # Beliebiges Passwort für Over-the-Air Updates
wifi:
ssid: "HIER_WLAN_NAME_EINTRAGEN" # Name deines WLANs (SSID)
password: "HIER_WLAN_PASSWORT_EINTRAGEN" # Passwort deines WLANs
ap:
ssid: "Growatt-SPH5000 Fallback Hotspot"
password: "HIER_HOTSPOT_PASSWORT_EINTRAGEN" # Passwort für den Notfall-Hotspot (min. 8 Zeichen)
captive_portal:
time:
- platform: homeassistant
id: homeassistant_time
output:
- id: light_bl
platform: gpio
pin: 16
- id: light_gr
platform: gpio
pin: 0
- id: light_rd
platform: gpio
pin: 2
uart:
id: mod_bus
tx_pin: 1
rx_pin: 3
baud_rate: 115200
modbus:
id: modbus1
uart_id: mod_bus
modbus_controller:
- id: growatt
address: 1
modbus_id: modbus1
setup_priority: -10
on_online:
then:
- output.turn_on: light_gr
on_offline:
then:
- output.turn_off: light_gr
script:
- id: blink_blue
mode: single
then:
- output.turn_on: light_bl
- delay: 200ms
- output.turn_off: light_bl
sensor:
# --- PV Eingang ---
- platform: modbus_controller
name: "PV1 Voltage"
address: 3
register_type: read
unit_of_measurement: V
device_class: voltage
state_class: measurement
icon: mdi:solar-panel
value_type: U_WORD
accuracy_decimals: 1
filters:
- multiply: 0.1
on_value:
then:
- script.execute: blink_blue
- platform: modbus_controller
name: "PV1 Current"
address: 4
register_type: read
unit_of_measurement: A
device_class: current
state_class: measurement
icon: mdi:solar-panel
value_type: U_WORD
accuracy_decimals: 1
filters:
- multiply: 0.1
- platform: modbus_controller
name: "PV1 Power"
address: 5
register_type: read
unit_of_measurement: W
device_class: power
state_class: measurement
icon: mdi:solar-panel
value_type: U_DWORD
accuracy_decimals: 1
filters:
- multiply: 0.1
- platform: modbus_controller
name: "PV2 Voltage"
address: 7
register_type: read
unit_of_measurement: V
device_class: voltage
state_class: measurement
icon: mdi:solar-panel
value_type: U_WORD
accuracy_decimals: 1
filters:
- multiply: 0.1
- platform: modbus_controller
name: "PV2 Current"
address: 8
register_type: read
unit_of_measurement: A
device_class: current
state_class: measurement
icon: mdi:solar-panel
value_type: U_WORD
accuracy_decimals: 1
filters:
- multiply: 0.1
- platform: modbus_controller
name: "PV2 Power"
address: 9
register_type: read
unit_of_measurement: W
device_class: power
state_class: measurement
icon: mdi:solar-panel
value_type: U_DWORD
accuracy_decimals: 1
filters:
- multiply: 0.1
# --- AC Ausgang / Netz ---
- platform: modbus_controller
name: "AC Output Power Total"
address: 35
register_type: read
unit_of_measurement: W
device_class: power
state_class: measurement
icon: mdi:flash
value_type: U_DWORD
accuracy_decimals: 1
filters:
- multiply: 0.1
- platform: modbus_controller
name: "Grid Frequency"
address: 37
register_type: read
unit_of_measurement: Hz
device_class: frequency
state_class: measurement
icon: mdi:sine-wave
value_type: U_WORD
accuracy_decimals: 2
filters:
- multiply: 0.01
- platform: modbus_controller
name: "Grid Voltage L1"
address: 38
register_type: read
unit_of_measurement: V
device_class: voltage
state_class: measurement
icon: mdi:flash
value_type: U_WORD
accuracy_decimals: 1
filters:
- multiply: 0.1
- platform: modbus_controller
name: "Grid Current L1"
address: 39
register_type: read
unit_of_measurement: A
device_class: current
state_class: measurement
icon: mdi:flash
value_type: U_WORD
accuracy_decimals: 1
filters:
- multiply: 0.1
- platform: modbus_controller
name: "Grid Voltage L2"
address: 42
register_type: read
unit_of_measurement: V
device_class: voltage
state_class: measurement
icon: mdi:flash
value_type: U_WORD
accuracy_decimals: 1
filters:
- multiply: 0.1
- platform: modbus_controller
name: "Grid Current L2"
address: 43
register_type: read
unit_of_measurement: A
device_class: current
state_class: measurement
icon: mdi:flash
value_type: U_WORD
accuracy_decimals: 1
filters:
- multiply: 0.1
- platform: modbus_controller
name: "Grid Voltage L3"
address: 46
register_type: read
unit_of_measurement: V
device_class: voltage
state_class: measurement
icon: mdi:flash
value_type: U_WORD
accuracy_decimals: 1
filters:
- multiply: 0.1
- platform: modbus_controller
name: "Grid Current L3"
address: 47
register_type: read
unit_of_measurement: A
device_class: current
state_class: measurement
icon: mdi:flash
value_type: U_WORD
accuracy_decimals: 1
filters:
- multiply: 0.1
# --- Energie (PV Erzeugung) ---
- platform: modbus_controller
name: "Energy Today"
address: 53
register_type: read
unit_of_measurement: kWh
device_class: energy
state_class: total_increasing
icon: mdi:solar-power
value_type: U_DWORD
accuracy_decimals: 1
filters:
- multiply: 0.1
- platform: modbus_controller
name: "Energy Total"
address: 55
register_type: read
unit_of_measurement: kWh
device_class: energy
state_class: total_increasing
icon: mdi:solar-power
value_type: U_DWORD
accuracy_decimals: 1
filters:
- multiply: 0.1
# --- Temperatur ---
- platform: modbus_controller
name: "Inverter Temperature"
address: 93
register_type: read
unit_of_measurement: "°C"
device_class: temperature
state_class: measurement
icon: mdi:thermometer
value_type: U_WORD
accuracy_decimals: 1
filters:
- multiply: 0.1
# --- Batterie ---
- platform: modbus_controller
name: "Battery Discharge Power"
address: 1009
register_type: read
unit_of_measurement: W
device_class: power
state_class: measurement
icon: mdi:battery-minus
value_type: U_DWORD
accuracy_decimals: 1
filters:
- multiply: 0.1
- platform: modbus_controller
name: "Battery Charge Power"
address: 1011
register_type: read
unit_of_measurement: W
device_class: power
state_class: measurement
icon: mdi:battery-plus
value_type: U_DWORD
accuracy_decimals: 1
filters:
- multiply: 0.1
- platform: modbus_controller
name: "Battery Voltage"
address: 1013
register_type: read
unit_of_measurement: V
device_class: voltage
state_class: measurement
icon: mdi:battery
value_type: U_WORD
accuracy_decimals: 1
filters:
- multiply: 0.1
- platform: modbus_controller
name: "Battery State of Charge"
address: 1014
register_type: read
unit_of_measurement: "%"
device_class: battery
state_class: measurement
icon: mdi:battery
value_type: U_WORD
accuracy_decimals: 0
- platform: modbus_controller
name: "Battery Temperature"
address: 1040
register_type: read
unit_of_measurement: "°C"
device_class: temperature
state_class: measurement
icon: mdi:thermometer
value_type: U_WORD
accuracy_decimals: 1
filters:
- multiply: 0.1
# --- Batterie Energie (kWh Zähler für Energie-Dashboard) ---
- platform: modbus_controller
name: "Battery Discharge Total"
address: 1054
register_type: read
unit_of_measurement: kWh
device_class: energy
state_class: total_increasing
icon: mdi:battery-minus
value_type: U_DWORD
accuracy_decimals: 1
filters:
- multiply: 0.1
- platform: modbus_controller
name: "Battery Charge Total"
address: 1058
register_type: read
unit_of_measurement: kWh
device_class: energy
state_class: total_increasing
icon: mdi:battery-plus
value_type: U_DWORD
accuracy_decimals: 1
filters:
- multiply: 0.1
# --- Smartmeter Momentanleistung ---
- platform: modbus_controller
name: "Power To Grid"
address: 1021
register_type: read
unit_of_measurement: W
device_class: power
state_class: measurement
icon: mdi:transmission-tower-export
value_type: U_DWORD
accuracy_decimals: 1
filters:
- multiply: 0.1
- platform: modbus_controller
name: "Power To User"
address: 1029
register_type: read
unit_of_measurement: W
device_class: power
state_class: measurement
icon: mdi:transmission-tower-import
value_type: U_DWORD
accuracy_decimals: 1
filters:
- multiply: 0.1
# --- Netzenergie Import / Export (kWh Zähler für Energie-Dashboard) ---
- platform: modbus_controller
name: "Energy Import Total"
address: 1046
register_type: read
unit_of_measurement: kWh
device_class: energy
state_class: total_increasing
icon: mdi:transmission-tower-import
value_type: U_DWORD
accuracy_decimals: 1
filters:
- multiply: 0.1
- platform: modbus_controller
name: "Energy Export Total"
address: 1050
register_type: read
unit_of_measurement: kWh
device_class: energy
state_class: total_increasing
icon: mdi:transmission-tower-export
value_type: U_DWORD
accuracy_decimals: 1
filters:
- multiply: 0.1
@@ -1,92 +0,0 @@
# Growatt SPH 5000 TL3-BH-UP — ESPHome Konfiguration
Modbus-RTU Integration des Growatt SPH 5000 TL3-BH-UP (3-phasiger Hybrid-Wechselrichter mit Batterie) über einen Growatt SuniStick mit ESPHome-Firmware.
---
## Modbus-Register Übersicht
Alle Register sind **Input Register** (Modbus Funktionscode 04, `register_type: "read"`).
Modbus-Adresse des Geräts: `0x01`
### PV Eingang
| Name | Adresse | Typ | Multiplikator | Einheit |
|------|---------|-----|---------------|---------|
| PV1 Voltage | 3 | U_WORD | 0.1 | V |
| PV1 Current | 4 | U_WORD | 0.1 | A |
| PV1 Power | 5 | U_DWORD | 0.1 | W |
| PV2 Voltage | 7 | U_WORD | 0.1 | V |
| PV2 Current | 8 | U_WORD | 0.1 | A |
| PV2 Power | 9 | U_DWORD | 0.1 | W |
### AC Ausgang / Netz
| Name | Adresse | Typ | Multiplikator | Einheit |
|------|---------|-----|---------------|---------|
| AC Output Power Total | 35 | U_DWORD | 0.1 | W |
| Grid Frequency | 37 | U_WORD | **0.01** | Hz |
| Grid Voltage L1 | 38 | U_WORD | 0.1 | V |
| Grid Current L1 | 39 | U_WORD | 0.1 | A |
| Grid Voltage L2 | 42 | U_WORD | 0.1 | V |
| Grid Current L2 | 43 | U_WORD | 0.1 | A |
| Grid Voltage L3 | 46 | U_WORD | 0.1 | V |
| Grid Current L3 | 47 | U_WORD | 0.1 | A |
> **Wichtig:** Grid Frequency hat Multiplikator `0.01` — der Rohwert ist z.B. `5000` = 50,00 Hz.
### Energie (PV Erzeugung)
| Name | Adresse | Typ | Multiplikator | Einheit |
|------|---------|-----|---------------|---------|
| Energy Today | 53 | U_DWORD | 0.1 | kWh |
| Energy Total | 55 | U_DWORD | 0.1 | kWh |
### Temperatur
| Name | Adresse | Typ | Multiplikator | Einheit |
|------|---------|-----|---------------|---------|
| Inverter Temperature | 93 | U_WORD | 0.1 | °C |
### Batterie — Momentanleistung
| Name | Adresse | Typ | Multiplikator | Einheit |
|------|---------|-----|---------------|---------|
| Battery Discharge Power | 1009 | U_DWORD | 0.1 | W |
| Battery Charge Power | 1011 | U_DWORD | 0.1 | W |
| Battery Voltage | 1013 | U_WORD | 0.1 | V |
| Battery State of Charge | 1014 | U_WORD | 1 | % |
| Battery Temperature | 1040 | U_WORD | 0.1 | °C |
### Batterie — Energie (für Energie-Dashboard)
| Name | Adresse | Typ | Multiplikator | Einheit |
|------|---------|-----|---------------|---------|
| Battery Discharge Total | 1054 | U_DWORD | 0.1 | kWh |
| Battery Charge Total | 1058 | U_DWORD | 0.1 | kWh |
> **Wichtig:** 1009/1011 = Momentanleistung in **Watt**. 1054/1058 = kumulierte Energie in **kWh** für das HA Energie-Dashboard.
### Smartmeter — Momentanleistung
| Name | Adresse | Typ | Multiplikator | Einheit |
|------|---------|-----|---------------|---------|
| Power To Grid | 1021 | U_DWORD | 0.1 | W |
| Power To User | 1029 | U_DWORD | 0.1 | W |
> **Hinweis:** Beide Werte sind immer positiv (unsigned). "To Grid" = Einspeisung, "To User" = Bezug vom Netz.
### Netzenergie Import / Export (für Energie-Dashboard)
| Name | Adresse | Typ | Multiplikator | Einheit |
|------|---------|-----|---------------|---------|
| Energy Import Total | 1046 | U_DWORD | 0.1 | kWh |
| Energy Export Total | 1050 | U_DWORD | 0.1 | kWh |
---
## Hinweise
- **U_DWORD** = 32-Bit-Wert über 2 aufeinanderfolgende Register. ESPHome liest diese automatisch korrekt, wenn `value_type: U_DWORD` gesetzt ist.
- Passwörter und API-Keys sind durch `#` ersetzt und müssen vor dem Einsatz angepasst werden.
- Diese Config wurde für den SPH 5000 TL3-BH-UP verifiziert. Andere SPH-Modelle (SPH 3000, SPH 6000 usw.) verwenden dieselbe Registerstruktur.
+1
View File
@@ -0,0 +1 @@
firmware/
+2 -2
View File
@@ -1,7 +1,7 @@
name: ShineBridge
version: "1.8.9"
version: "1.9.4"
slug: shinebridge
description: Growatt Wechselrichter lokal in Home Assistant — Modbus TCP via ShineLAN-X, MQTT Discovery, Web UI
description: Energiemanagement für Growatt/Goodwe-Wechselrichter — PV-Überschussladen, Wallbox-EMS, Tarifvergleich, Zigbee2MQTT-Überschussgeräte
url: https://gitea.bitfire.work/retr0/shinebridge
arch:
- aarch64
+26 -6
View File
@@ -23,6 +23,11 @@ TARGET_HOUR = 6 # Zielstunde: Vollladung bis 06:00
POLL_INTERVAL_S = 30 # Sekunden zwischen EMS-Updates
PHASES = 3 # Anzahl der Phasen
VOLTAGE_V = 230 # Netzspannung
MAX_CHARGE_MA = 32000 # Absolutes Wallbox-Limit (Hardware-Grenze WallboxReader).
# ACHTUNG: die tatsächlich installierte Wallbox/Zuleitung kann
# niedriger dimensioniert sein (z.B. 11kW-Wallbox = 16A) — dann
# über max_charge_ma im Konstruktor auf den echten Wert begrenzen,
# sonst fordert das Zwangsladen mehr an als das Gerät liefern kann.
# Charging-State Werte (Kathrein Modbus)
STATE_IDLE = 0
@@ -39,11 +44,13 @@ class EmsController:
pv_timeout_h: float = PV_TIMEOUT_H,
target_hour: int = TARGET_HOUR,
phases: int = PHASES,
max_charge_ma: int = MAX_CHARGE_MA,
):
self.min_pv_power = min_pv_power
self.pv_timeout_h = pv_timeout_h
self.target_hour = target_hour
self.phases = phases
self.max_charge_ma = max_charge_ma
self._pv_ok_since: Optional[float] = None # Zeitpunkt letzter ausreichender PV
self._no_pv_since: Optional[float] = None # Zeitpunkt seit kein PV
@@ -66,7 +73,7 @@ class EmsController:
total_w = surplus_w + wallbox_w
current_a = total_w / (VOLTAGE_V * self.phases)
ma = int(current_a * 1000)
return max(6000, min(32000, ma))
return max(6000, min(self.max_charge_ma, ma))
def update(self, wallbox_reader, pv_surplus_w: float, charging_state: int,
wallbox_power_w: float = 0.0) -> str:
@@ -84,8 +91,8 @@ class EmsController:
if wallbox_reader.enable_ems():
self._ems_enabled = True
log.info("[EMS] EMS aktiviert")
else:
# Auto weg → alles zurücksetzen
elif charging_state == STATE_IDLE:
# Wirklich kein Fahrzeug angesteckt → alles zurücksetzen, Timer neu starten
if self._ems_enabled:
wallbox_reader.set_current(0)
self._ems_enabled = False
@@ -93,6 +100,16 @@ class EmsController:
self._no_pv_since = None
log.info("[EMS] Auto abgesteckt — EMS zurückgesetzt")
return "kein Fahrzeug"
else:
# Unbekannter/nicht abgedeckter Ladezustand (z.B. Fehler/Störung am Fahrzeug).
# NICHT wie "abgesteckt" behandeln — sonst verliert der Zwangslade-Timer seinen
# Fortschritt und die Deadline (Zielzeit) wird nie mehr erreicht, obwohl das Auto
# die ganze Zeit angesteckt war. Strom sicherheitshalber pausieren, aber Zustand/
# Timer erhalten, damit nach Rückkehr in einen bekannten Zustand nahtlos weitergeht.
wallbox_reader.set_current(0)
log.warning("[EMS] Unbekannter Ladezustand %d (evtl. Störung am Fahrzeug) — "
"Strom pausiert, Zwangslade-Timer bleibt erhalten", charging_state)
return f"unbekannter Zustand ({charging_state})"
if charging_state == STATE_COMPLETED:
wallbox_reader.set_current(0)
@@ -116,13 +133,16 @@ class EmsController:
no_pv_h = (now - self._no_pv_since) / 3600.0
if no_pv_h >= self.pv_timeout_h:
# Zwangsladen bis Zielzeit
# Zwangsladen bis Zielzeit — Strom bei JEDEM Update erneut setzen (nicht nur beim
# ersten Eintritt), sonst bleibt ein einzelner fehlgeschlagener/verworfener Write
# (Modbus-Hänger, Fahrzeug-Störung) unbemerkt für den Rest der Nacht stehen.
target = self._next_target()
mins_left = (target - datetime.now()).total_seconds() / 60
if not self._forced_charging:
wallbox_reader.set_current(32000, self.phases)
self._forced_charging = True
log.info("[EMS] Zwangsladen gestartet — %dmin bis %02d:00", int(mins_left), self.target_hour)
log.info("[EMS] Zwangsladen gestartet — %dmin bis %02d:00, max %.0fA",
int(mins_left), self.target_hour, self.max_charge_ma / 1000)
wallbox_reader.set_current(self.max_charge_ma, self.phases)
return f"Zwangsladen ({no_pv_h:.1f}h kein PV, {int(mins_left)}min bis {self.target_hour:02d}:00)"
# Warten auf PV
+78 -16
View File
@@ -7,7 +7,7 @@ from typing import Dict, List, Optional, Tuple
log = logging.getLogger(__name__)
DB_PATH = "/data/history.db"
RETENTION_DAYS = 7
RETENTION_DAYS = 14
_lock = threading.Lock()
_conn: sqlite3.Connection | None = None
@@ -47,11 +47,31 @@ def init_db():
PRIMARY KEY (agg_id, period_type, period_key)
)
""")
# Tariftage: täglicher Verbrauch + Preisvergleich
c.execute("""
CREATE TABLE IF NOT EXISTS tariff_days (
date TEXT PRIMARY KEY,
kwh REAL NOT NULL,
spot_ct REAL,
fixed_ct REAL NOT NULL,
markup_ct REAL NOT NULL DEFAULT 0
)
""")
c.commit()
cleanup_old()
_start_cleanup_scheduler()
log.info("History DB initialisiert: %s", DB_PATH)
def _start_cleanup_scheduler():
def _loop():
while True:
time.sleep(86400)
cleanup_old()
t = threading.Thread(target=_loop, daemon=True, name="history-cleanup")
t.start()
def period_key(period_type: str, billing_day: int = 1, billing_month: int = 1) -> str:
import datetime
today = datetime.date.today()
@@ -107,21 +127,18 @@ def write_batch(inv_id: str, ts: float, values: Dict[str, float]):
def load_recent(inv_id: str, limit: int = 300) -> Dict[str, List[Tuple[float, float]]]:
"""Letzte `limit` Messpunkte pro Sensor — zum Befüllen der In-Memory-Deque beim Start."""
with _lock:
rows = _get_conn().execute("""
SELECT sensor_id, ts, value
FROM (
SELECT sensor_id, ts, value,
ROW_NUMBER() OVER (PARTITION BY sensor_id ORDER BY ts DESC) AS rn
FROM measurements
WHERE inv_id = ?
)
WHERE rn <= ?
ORDER BY ts ASC
""", (inv_id, limit)).fetchall()
result: Dict[str, List[Tuple[float, float]]] = {}
for sensor_id, ts, value in rows:
result.setdefault(sensor_id, []).append((ts, value))
c = _get_conn()
sensors = [r[0] for r in c.execute(
"SELECT DISTINCT sensor_id FROM measurements WHERE inv_id = ?", (inv_id,)
).fetchall()]
result: Dict[str, List[Tuple[float, float]]] = {}
for sid in sensors:
rows = c.execute(
"SELECT ts, value FROM measurements "
"WHERE inv_id = ? AND sensor_id = ? ORDER BY ts DESC LIMIT ?",
(inv_id, sid, limit),
).fetchall()
result[sid] = [(ts, val) for ts, val in reversed(rows)]
return result
@@ -137,6 +154,51 @@ def query(inv_id: str, sensor_id: str,
return rows
def save_daily_tariff_snapshot(date_iso: str, prev_kwh: float, cur_kwh: float,
spot_ct: Optional[float], fixed_ct: float, markup_ct: float):
"""Speichert den Tagesverbrauch + Preisdaten für einen abgeschlossenen Tag."""
delta = max(0.0, cur_kwh - prev_kwh)
with _lock:
c = _get_conn()
c.execute("""
INSERT OR IGNORE INTO tariff_days(date, kwh, spot_ct, fixed_ct, markup_ct)
VALUES(?, ?, ?, ?, ?)
""", (date_iso, round(delta, 4), spot_ct, fixed_ct, markup_ct))
c.commit()
def get_tariff_days(from_date: str, to_date: str) -> List[Tuple]:
"""Gibt alle Tariftage im Bereich [from_date, to_date] zurück."""
with _lock:
return _get_conn().execute("""
SELECT date, kwh, spot_ct, fixed_ct, markup_ct
FROM tariff_days
WHERE date >= ? AND date <= ?
ORDER BY date ASC
""", (from_date, to_date)).fetchall()
def get_daily_kwh_start(date_iso: str) -> Optional[float]:
"""Gibt den gespeicherten kWh-Tagesstartwert zurück (aus period_starts)."""
with _lock:
row = _get_conn().execute("""
SELECT value FROM period_starts
WHERE agg_id='tariff' AND period_type='daily' AND period_key=?
""", (date_iso,)).fetchone()
return row[0] if row else None
def save_daily_kwh_start(date_iso: str, kwh: float):
"""Speichert den kWh-Stand als Tagesstartwert (einmalig, INSERT OR IGNORE)."""
with _lock:
c = _get_conn()
c.execute("""
INSERT OR IGNORE INTO period_starts(agg_id, period_type, period_key, value)
VALUES('tariff', 'daily', ?, ?)
""", (date_iso, kwh))
c.commit()
def cleanup_old(days: int = RETENTION_DAYS):
cutoff = time.time() - days * 86400
with _lock:
+3 -2
View File
@@ -247,8 +247,9 @@ INVERTERS = {
name="Kathrein Wallbox",
manufacturer="Kathrein",
sensors=_kathrein_sensors(),
# Meter-Block + EVSE-Status + EMS-Setpoint
read_ranges=[(0x0030, 48), (0x0060, 10), (0x00A2, 1)],
# Meter-Block 0x0030-0x005F + charging_state 0x0060 (solo, 0x0061-0x0064 existieren nicht)
# + EVSE-Status 0x0065-0x006A + EMS-Setpoint 0x00A2
read_ranges=[(0x0030, 48), (0x0060, 1), (0x0065, 6), (0x00A2, 1)],
protocol="kathrein",
goodwe_family="",
),
+230 -56
View File
@@ -30,6 +30,21 @@ WEB_DIR = os.path.join(os.path.dirname(__file__), "web")
app = Flask(__name__, static_folder=WEB_DIR)
@app.before_request
def _check_ingress():
# Static files and the root page are always served (no API data inside)
if request.path == "/" or not request.path.startswith("/api/"):
return None
# Requests routed through HAOS ingress proxy carry this header
if request.headers.get("X-Ingress-Path"):
return None
# Allow loopback for local debugging / health checks
if request.remote_addr in ("127.0.0.1", "::1"):
return None
return jsonify({"error": "Zugriff nur über die HAOS-Oberfläche erlaubt"}), 403
# ── Aggregation ───────────────────────────────────────────────
# Welche Sensor-IDs fließen in welchen Aggregat-Bucket (Summe, außer AGG_AVG)
@@ -39,8 +54,8 @@ AGG_SENSOR_IDS: Dict[str, List[str]] = {
"total_energy_today": ["energy_today", "e_day"],
"total_energy_total": ["energy_total", "e_total"],
"grid_power": ["grid_power"],
"grid_import_kwh": ["import_kwh", "e_total_imp"],
"grid_export_kwh": ["export_kwh", "e_total_exp"],
"grid_import_kwh": ["import_kwh", "_int_import", "e_total_imp"],
"grid_export_kwh": ["export_kwh", "_int_export", "e_total_exp"],
"bat_charge_power": ["bat_charge_power"],
"bat_discharge_power": ["bat_discharge_power"],
"bat_charge_total": ["bat_charge_total", "e_bat_charge_total"],
@@ -77,6 +92,7 @@ class State:
surplus_devices_cfg: List[Dict[str, Any]] = []
z2m_base: str = "zigbee2mqtt"
z2m_devices: List[Dict[str, Any]] = []
last_tariff_snapshot_date: str = ""
_publisher: Optional[MqttPublisher] = None
_surplus_ctrl: Optional[SurplusDeviceController] = None
@@ -101,30 +117,42 @@ def _defaults() -> Dict[str, Any]:
"spot_country": "de",
"spot_markup": 0.0,
"spot_chart": True,
"billing_tracker_enabled": False,
"monthly_rate_eur": 0.0,
"grundpreis_eur_per_month": 0.0,
"inverters": [],
"surplus_devices": [],
"z2m_base": "zigbee2mqtt",
}
def _load_json_safe(path: str) -> Optional[Dict]:
"""Lädt eine JSON-Datei; gibt None zurück wenn fehlend oder korrupt."""
try:
with open(path) as f:
return json.load(f)
except Exception as e:
log.warning("JSON-Ladefehler %s: %s", path, e)
return None
def load_config() -> Dict[str, Any]:
cfg = _defaults()
# MQTT-Grundeinstellungen aus HAOS-Options (überschreibbar durch config.json)
if os.path.exists(HA_OPTIONS_PATH):
try:
with open(HA_OPTIONS_PATH) as f:
ha = json.load(f)
for k in ("mqtt_broker", "mqtt_port", "mqtt_user", "mqtt_pass"):
if k in ha:
cfg[k] = ha[k]
except Exception as e:
log.warning("HA options Fehler: %s", e)
if os.path.exists(CONFIG_PATH):
try:
with open(CONFIG_PATH) as f:
cfg.update(json.load(f))
except Exception as e:
log.warning("Config-Datei Fehler: %s", e)
ha = _load_json_safe(HA_OPTIONS_PATH) or {}
for k in ("mqtt_broker", "mqtt_port", "mqtt_user", "mqtt_pass"):
if k in ha:
cfg[k] = ha[k]
# Eigene persistente Config — Hauptdatei, dann Backup als Fallback
loaded = _load_json_safe(CONFIG_PATH)
if loaded is None and os.path.exists(CONFIG_PATH + ".bak"):
log.warning("config.json korrupt — lade Backup")
loaded = _load_json_safe(CONFIG_PATH + ".bak")
if loaded:
cfg.update(loaded)
return cfg
def save_config():
data = {
"mqtt_broker": State.mqtt_cfg.get("mqtt_broker", ""),
@@ -139,12 +167,24 @@ def save_config():
"spot_country": State.mqtt_cfg.get("spot_country", "de"),
"spot_markup": State.mqtt_cfg.get("spot_markup", 0.0),
"spot_chart": State.mqtt_cfg.get("spot_chart", True),
"billing_tracker_enabled": State.mqtt_cfg.get("billing_tracker_enabled", False),
"monthly_rate_eur": State.mqtt_cfg.get("monthly_rate_eur", 0.0),
"grundpreis_eur_per_month": State.mqtt_cfg.get("grundpreis_eur_per_month", 0.0),
"inverters": State.inverters_cfg,
"surplus_devices": State.surplus_devices_cfg,
"z2m_base": State.z2m_base,
}
with open(CONFIG_PATH, "w") as f:
# Backup der letzten guten Config anlegen
if os.path.exists(CONFIG_PATH):
try:
os.replace(CONFIG_PATH, CONFIG_PATH + ".bak")
except OSError:
pass
# Atomarer Write: erst .tmp schreiben, dann umbenennen
tmp = CONFIG_PATH + ".tmp"
with open(tmp, "w") as f:
json.dump(data, f, indent=2)
os.replace(tmp, CONFIG_PATH)
# ── Aggregation ───────────────────────────────────────────────
@@ -180,29 +220,44 @@ def _compute_aggregates(allow_stale: bool = False) -> Dict[str, float]:
# ── EMS Hilfsfunktionen ───────────────────────────────────────
def _get_pv_surplus() -> float:
"""PV-Überschuss in Watt aus laufenden Geräten ermitteln.
Goodwe: active_power negativ = Einspeisung (Überschuss).
Growatt: power_to_grid positiv = Einspeisung.
"""
surplus = 0.0
with State.lock:
for inv_cfg in State.inverters_cfg:
d = State.inv_data.get(inv_cfg["id"], {})
if not d.get("modbus_ok") or not d.get("values"):
continue
v = d["values"]
# Goodwe ET: active_power > 0 = Einspeisung, < 0 = Netzbezug
# house_consumption = ppv + pbattery1 - active_power (Bibliotheks-Formel)
if "active_power" in v:
surplus += max(0.0, v["active_power"])
# Growatt
if "power_to_grid" in v:
surplus += max(0.0, v["power_to_grid"])
return surplus
"""PV-Überschuss in Watt. Nutzt grid_power-Aggregat (negativ = Einspeisung)."""
agg = _compute_aggregates(allow_stale=True)
return max(0.0, -agg.get("grid_power", 0.0))
# ── Poll Loop ─────────────────────────────────────────────────
def _run_daily_tariff_snapshot():
import datetime
today = datetime.date.today()
yesterday = today - datetime.timedelta(days=1)
today_iso = today.isoformat()
yesterday_iso = yesterday.isoformat()
agg = _compute_aggregates(allow_stale=True)
cur_kwh = agg.get("grid_import_kwh")
if cur_kwh is None:
return
history.save_daily_kwh_start(today_iso, cur_kwh)
prev_kwh = history.get_daily_kwh_start(yesterday_iso)
if prev_kwh is None:
return
fixed_ct = float(State.mqtt_cfg.get("price_import", 0.30)) * 100
markup_ct = float(State.mqtt_cfg.get("spot_markup", 0.0))
country = str(State.mqtt_cfg.get("spot_country", "de"))
y_start = datetime.datetime.combine(yesterday, datetime.time.min).timestamp()
y_end = datetime.datetime.combine(today, datetime.time.min).timestamp()
spot_ct = _get_avg_spot_price(y_start, y_end, country)
history.save_daily_tariff_snapshot(yesterday_iso, prev_kwh, cur_kwh, spot_ct, fixed_ct, markup_ct)
log.info("Tariftag %s gespeichert: %.3f kWh, spot=%.2f ct, fixed=%.2f ct",
yesterday_iso, max(0, cur_kwh - prev_kwh), spot_ct or 0, fixed_ct)
def _poll_loop(inv_cfg: Dict[str, Any], stop: threading.Event):
inv_id = inv_cfg["id"]
model_id = inv_cfg.get("inverter_model", "MIC_1500_TL_X")
@@ -224,17 +279,18 @@ def _poll_loop(inv_cfg: Dict[str, Any], stop: threading.Event):
log.info("[%s] Poll-Loop: %s @ %s (Goodwe UDP/8899) alle %ds",
inv_id, inverter.name, host, interval)
elif inverter.protocol == "kathrein":
reader = WallboxReader(host=host, port=port)
reader = WallboxReader(host=host, port=port, timeout=min(5.0, interval * 0.4))
ems = EmsController(
min_pv_power=inv_cfg.get("ems_min_pv", 1400),
pv_timeout_h=inv_cfg.get("ems_timeout", 4.0),
target_hour=inv_cfg.get("ems_target_hour", 6),
phases=inv_cfg.get("ems_phases", 3),
max_charge_ma=inv_cfg.get("ems_max_current_ma", 32000),
)
log.info("[%s] Poll-Loop: %s @ %s:%s (Kathrein EMS) alle %ds",
inv_id, inverter.name, host, port, interval)
else:
reader = ModbusReader(host=host, port=port, slave=slave)
reader = ModbusReader(host=host, port=port, slave=slave, timeout=min(5.0, interval * 0.4))
log.info("[%s] Poll-Loop: %s @ %s:%s alle %ds",
inv_id, inverter.name, host, port, interval)
@@ -254,6 +310,13 @@ def _poll_loop(inv_cfg: Dict[str, Any], stop: threading.Event):
q.append(pt)
log.info("[%s] %d Sensoren aus DB geladen", inv_id, len(hist_data))
# Integrierte Grid-Zähler — starten bei 0, wachsen mit jedem Poll, persistieren in DB
inv_int_import = hist_data["_int_import"][-1][1] if hist_data.get("_int_import") else 0.0
inv_int_export = hist_data["_int_export"][-1][1] if hist_data.get("_int_export") else 0.0
last_grid_ts = time.time()
_fail_count = 0 # aufeinanderfolgende Lesefehler; erst ab >= 3 → offline
_OFFLINE_THRESHOLD = 3
while not stop.is_set():
t0 = time.time()
values = reader.read(inverter)
@@ -266,12 +329,24 @@ def _poll_loop(inv_cfg: Dict[str, Any], stop: threading.Event):
if values and "grid_power" not in values and "import_kwh" in values and "total_power" in values:
values["grid_power"] = values["total_power"]
# grid_power integrieren → interne kWh-Zähler (Periodenberechnung für Goodwe / kein SDM-630)
if values is not None and "grid_power" in values:
dt_s = t0 - last_grid_ts
if 0 < dt_s < 300:
gp = values["grid_power"]
if gp > 0:
inv_int_import = round(inv_int_import + gp * dt_s / 3_600_000, 6)
elif gp < 0:
inv_int_export = round(inv_int_export + (-gp) * dt_s / 3_600_000, 6)
values["_int_import"] = inv_int_import
values["_int_export"] = inv_int_export
last_grid_ts = t0
# EMS: PV-Überschuss aus anderen Geräten holen und Ladestrom regeln
if ems is not None and values is not None and inv_cfg.get("ems_enabled", True):
pv_surplus = _get_pv_surplus()
# 0x0060 manchmal nicht lesbar → 1 (EV Connected) annehmen,
# damit EMS aktiviert; Wallbox ignoriert Befehle wenn kein Auto da
charging_state = int(values.get("charging_state", 1))
# charging_state aus 0x0060 (solo-Read); fehlt bei kein Fahrzeug → 0 (Idle)
charging_state = int(values.get("charging_state", 0))
wallbox_power = values.get("total_power", 0.0)
ems_status = ems.update(reader, pv_surplus, charging_state, wallbox_power)
values["ems_status_code"] = float(charging_state)
@@ -280,6 +355,7 @@ def _poll_loop(inv_cfg: Dict[str, Any], stop: threading.Event):
with State.lock:
d = State.inv_data.setdefault(inv_id, {"poll_count": 0})
if values is not None:
_fail_count = 0
d["values"] = values
d["last_update"] = time.time()
d["modbus_ok"] = True
@@ -291,12 +367,15 @@ def _poll_loop(inv_cfg: Dict[str, Any], stop: threading.Event):
q.append((now, val))
history.write_batch(inv_id, now, values)
if _publisher:
_publisher.publish_data(values, prefix)
pub_values = {k: v for k, v in values.items() if not k.startswith("_")}
_publisher.publish_data(pub_values, prefix)
_publisher.publish_status("online", prefix)
else:
d["modbus_ok"] = False
if _publisher:
_publisher.publish_status("offline", prefix)
_fail_count += 1
if _fail_count >= _OFFLINE_THRESHOLD:
d["modbus_ok"] = False
if _publisher:
_publisher.publish_status("offline", prefix)
# Aggregate nach jedem erfolgreichen Poll neu berechnen und publizieren
if values is not None and _publisher:
@@ -304,6 +383,14 @@ def _poll_loop(inv_cfg: Dict[str, Any], stop: threading.Event):
if agg:
_publisher.publish_aggregates(agg)
# Täglicher Tarif-Snapshot (einmal pro Tag, beim ersten Poll nach Mitternacht)
if values is not None:
import datetime
today_iso = datetime.date.today().isoformat()
if State.last_tariff_snapshot_date != today_iso:
State.last_tariff_snapshot_date = today_iso
threading.Thread(target=_run_daily_tariff_snapshot, daemon=True).start()
stop.wait(max(0.0, interval - (time.time() - t0)))
reader.close()
@@ -404,6 +491,7 @@ def api_get_config():
cfg = {**State.mqtt_cfg, "inverters": State.inverters_cfg}
cfg.pop("mqtt_pass", None)
cfg["mqtt_connected"] = _publisher.connected if _publisher else False
cfg["mqtt_error"] = _publisher.last_error if _publisher else None
return jsonify(cfg)
@app.post("/api/config")
@@ -415,17 +503,25 @@ def api_save_config():
State.mqtt_cfg[k] = data[k]
if data.get("mqtt_pass"):
State.mqtt_cfg["mqtt_pass"] = data["mqtt_pass"]
for k in ("price_import", "price_export", "spot_markup"):
if k in data:
State.mqtt_cfg[k] = float(data[k])
for k in ("price_import", "price_export", "spot_markup", "monthly_rate_eur", "grundpreis_eur_per_month"):
if k in data and data[k] is not None:
try:
State.mqtt_cfg[k] = float(data[k])
except (ValueError, TypeError):
pass
for k in ("billing_day", "billing_month"):
if k in data:
State.mqtt_cfg[k] = int(data[k])
if k in data and data[k] is not None:
try:
State.mqtt_cfg[k] = int(data[k])
except (ValueError, TypeError):
pass
for k in ("tariff_type", "spot_country"):
if k in data:
if k in data and data[k] is not None:
State.mqtt_cfg[k] = str(data[k])
if "spot_chart" in data:
State.mqtt_cfg["spot_chart"] = bool(data["spot_chart"])
if "billing_tracker_enabled" in data:
State.mqtt_cfg["billing_tracker_enabled"] = bool(data["billing_tracker_enabled"])
save_config()
threading.Thread(target=_restart_all, daemon=True).start()
return jsonify({"ok": True})
@@ -499,8 +595,8 @@ def api_period_energy():
pv_total = entry.get("total_energy_total")
grid_exp = entry.get("grid_export_kwh")
bat_dch = entry.get("bat_discharge_total")
if pv_total is not None and grid_exp is not None:
savings = round(max(0.0, pv_total - grid_exp), 2)
if pv_total is not None:
savings = round(max(0.0, pv_total - (grid_exp or 0.0)), 2)
entry["savings_kwh"] = savings
entry["savings_eur"] = round(savings * eff_price, 2)
elif bat_dch is not None:
@@ -509,8 +605,78 @@ def api_period_energy():
result[period_type] = entry
if State.mqtt_cfg.get("billing_tracker_enabled", False):
monthly_rate = float(State.mqtt_cfg.get("monthly_rate_eur", 0.0))
grundpreis = float(State.mqtt_cfg.get("grundpreis_eur_per_month", 0.0))
yr_key = history.period_key("yearly", billing_day, billing_month)
yr_start = datetime.date.fromisoformat(yr_key)
today = datetime.date.today()
# Anzahl geleisteter Abschlagszahlungen (ganze Monate seit Periodenstart)
months_diff = (today.year - yr_start.year) * 12 + (today.month - yr_start.month)
if today.day >= yr_start.day:
months_diff += 1
payments_made = max(0, months_diff)
total_paid = round(payments_made * monthly_rate, 2)
grundpreis_total= round(payments_made * grundpreis, 2)
energy_cost = result.get("yearly", {}).get("import_cost", 0.0)
total_cost = round(energy_cost + grundpreis_total, 2)
nachzahlung = round(total_cost - total_paid, 2)
result["billing_tracker"] = {
"monthly_rate_eur": monthly_rate,
"grundpreis_eur_per_month": grundpreis,
"payments_made": payments_made,
"total_paid_eur": total_paid,
"grundpreis_total_eur": grundpreis_total,
"energy_cost_eur": energy_cost,
"total_cost_eur": total_cost,
"nachzahlung_eur": nachzahlung,
}
return jsonify(result)
@app.get("/api/finance")
def api_finance():
import datetime
billing_day = int(State.mqtt_cfg.get("billing_day", 1))
billing_month = int(State.mqtt_cfg.get("billing_month", 1))
yr_key = history.period_key("yearly", billing_day, billing_month)
yr_start = datetime.date.fromisoformat(yr_key)
today = datetime.date.today()
rows = history.get_tariff_days(yr_key, today.isoformat())
days = []
fixed_total = 0.0
spot_total = 0.0
spot_available = 0
for date_iso, kwh, spot_ct, fixed_ct, markup_ct in rows:
fixed_day = round(kwh * fixed_ct / 100, 4)
spot_day = round(kwh * ((spot_ct + markup_ct) / 100), 4) if spot_ct is not None else None
fixed_total += fixed_day
if spot_day is not None:
spot_total += spot_day
spot_available += 1
days.append({
"date": date_iso,
"kwh": round(kwh, 3),
"fixed_eur": fixed_day,
"spot_eur": spot_day,
"spot_ct": spot_ct,
"fixed_ct": fixed_ct,
})
result = {
"period_start": yr_key,
"days": days,
"fixed_total_eur": round(fixed_total, 2),
"spot_total_eur": round(spot_total, 2) if spot_available else None,
"spot_days": spot_available,
"total_days": len(days),
"savings_eur": round(fixed_total - spot_total, 2) if spot_available else None,
}
return jsonify(result)
@app.get("/api/z2m-devices")
def api_z2m_devices():
with State.lock:
@@ -522,7 +688,8 @@ def api_get_surplus_devices():
devices = State.surplus_devices_cfg
z2m_base = State.z2m_base
states = _surplus_ctrl.get_states() if _surplus_ctrl else {}
return jsonify({"devices": devices, "z2m_base": z2m_base, "states": states})
surplus_w = _get_pv_surplus()
return jsonify({"devices": devices, "z2m_base": z2m_base, "states": states, "surplus_w": surplus_w})
@app.post("/api/surplus-devices")
def api_save_surplus_devices():
@@ -732,6 +899,9 @@ def api_spot_price():
log.warning("Spot-Price API Fehler: %s", e)
return jsonify({"ok": False, "data": _spot_cache.get("data", [])})
# Flash/OTA-Endpunkte ausgelagert nach growatt-diagnose-tool (v1.9.0) —
# ShineBridge fokussiert sich auf Energiemanagement, kein Firmware-Flashing mehr.
@app.get("/")
def index():
return send_from_directory(WEB_DIR, "index.html")
@@ -746,8 +916,12 @@ if __name__ == "__main__":
history.init_db()
cfg = load_config()
with State.lock:
State.mqtt_cfg = {k: cfg[k] for k in
("mqtt_broker", "mqtt_port", "mqtt_user", "mqtt_pass")}
State.mqtt_cfg = {k: cfg[k] for k in (
"mqtt_broker", "mqtt_port", "mqtt_user", "mqtt_pass",
"price_import", "price_export", "billing_day", "billing_month",
"tariff_type", "spot_country", "spot_markup", "spot_chart",
"billing_tracker_enabled", "monthly_rate_eur", "grundpreis_eur_per_month",
) if k in cfg}
State.inverters_cfg = cfg.get("inverters", [])
State.surplus_devices_cfg = cfg.get("surplus_devices", [])
State.z2m_base = cfg.get("z2m_base", "zigbee2mqtt")
+28 -2
View File
@@ -12,12 +12,23 @@ AGG_DEVICE_ID = "shinebridge_aggregate"
AGG_TOPIC = "shinebridge/aggregate"
_RC_MESSAGES = {
1: "Falsche Protokollversion",
2: "Client-ID abgelehnt",
3: "Broker nicht verfügbar",
4: "Falscher Benutzername oder Passwort",
5: "Nicht autorisiert — Credentials prüfen",
}
class MqttPublisher:
def __init__(self, broker: str, port: int, user: str, password: str,
agg_meta: Optional[Dict] = None):
self._broker = broker
self._port = port
self._connected = False
self._last_error: Optional[str] = None
self._auth_failed = False
self._registered: List[Tuple] = []
self._agg_meta: Dict = agg_meta or {}
@@ -32,6 +43,8 @@ class MqttPublisher:
def _on_connect(self, client, userdata, flags, rc):
if rc == 0:
self._connected = True
self._last_error = None
self._auth_failed = False
log.info("MQTT verbunden: %s:%d", self._broker, self._port)
for entry in self._registered:
self._publish_discovery(*entry)
@@ -40,11 +53,17 @@ class MqttPublisher:
for topic, _ in self._subscriptions:
client.subscribe(topic)
else:
log.error("MQTT Verbindungsfehler rc=%d", rc)
msg = _RC_MESSAGES.get(rc, f"Unbekannter Fehler")
self._last_error = f"rc={rc}: {msg}"
log.error("MQTT Verbindungsfehler %s", self._last_error)
if rc == 5:
self._auth_failed = True
client.loop_stop()
def _on_disconnect(self, client, userdata, rc):
self._connected = False
log.warning("MQTT getrennt rc=%d", rc)
if rc != 0:
log.warning("MQTT getrennt rc=%d", rc)
def _on_message(self, client, userdata, msg):
for topic, callback in self._subscriptions:
@@ -60,6 +79,9 @@ class MqttPublisher:
self._client.subscribe(topic)
def connect(self):
if self._auth_failed:
log.warning("MQTT connect übersprungen — Authentifizierung fehlgeschlagen")
return
try:
self._client.connect_async(self._broker, self._port, keepalive=60)
self._client.loop_start()
@@ -74,6 +96,10 @@ class MqttPublisher:
def connected(self) -> bool:
return self._connected
@property
def last_error(self) -> Optional[str]:
return self._last_error
# ── Gerät-Discovery ──────────────────────────────────────
def register_inverter(self, inverter: Inverter, device_id: str,
+32 -7
View File
@@ -1,6 +1,7 @@
import json
import logging
from typing import Any, Dict, List
import time
from typing import Any, Dict, List, Optional
log = logging.getLogger(__name__)
@@ -13,6 +14,7 @@ class SurplusDeviceController:
self._base = z2m_base
self._devices: List[Dict[str, Any]] = []
self._states: Dict[str, bool] = {}
self._on_since: Dict[str, float] = {} # z2m_name → Einschaltzeitpunkt
def set_config(self, devices: List[Dict[str, Any]], z2m_base: str = "zigbee2mqtt"):
enabled_now = {d["z2m_name"] for d in devices if d.get("enabled", True)}
@@ -24,6 +26,7 @@ class SurplusDeviceController:
self._base = z2m_base
def update(self, surplus_w: float):
now = time.time()
for dev in self._devices:
name = dev["z2m_name"]
if not dev.get("enabled", True):
@@ -32,14 +35,34 @@ class SurplusDeviceController:
continue
threshold = float(dev.get("threshold_w", 500))
hysteresis = float(dev.get("hysteresis_w", 150))
min_on_s = float(dev.get("min_on_minutes", 0)) * 60
inverted = bool(dev.get("inverted", False))
currently_on = self._states.get(name, False)
if not currently_on and surplus_w >= threshold:
self._send(name, True)
elif currently_on and surplus_w < (threshold - hysteresis):
self._send(name, False)
def get_states(self) -> Dict[str, bool]:
return dict(self._states)
# Invertiert: EIN bei Netzbezug (kein Überschuss), AUS bei Überschuss
should_turn_on = not currently_on and (surplus_w >= threshold if not inverted else surplus_w < (threshold - hysteresis))
should_turn_off = currently_on and (surplus_w < (threshold - hysteresis) if not inverted else surplus_w >= threshold)
if should_turn_on:
self._send(name, True)
self._on_since[name] = now
elif should_turn_off:
on_since = self._on_since.get(name, 0.0)
if now - on_since >= min_on_s:
self._send(name, False)
else:
remaining = int((min_on_s - (now - on_since)) / 60)
log.info("[SurplusDevices] %s bleibt an — Mindestlaufzeit: noch %dmin", name, remaining)
def get_states(self) -> Dict[str, Any]:
now = time.time()
result = {}
for name, on in self._states.items():
entry: Dict[str, Any] = {"on": on}
if on and name in self._on_since:
entry["on_minutes"] = int((now - self._on_since[name]) / 60)
result[name] = entry
return result
def _send(self, z2m_name: str, on: bool):
topic = f"{self._base}/{z2m_name}/set"
@@ -47,6 +70,8 @@ class SurplusDeviceController:
try:
self._pub.publish_raw(topic, payload)
self._states[z2m_name] = on
if not on:
self._on_since.pop(z2m_name, None)
log.info("[SurplusDevices] %s → %s", z2m_name, "ON" if on else "OFF")
except Exception as e:
log.error("[SurplusDevices] MQTT Fehler: %s", e)
+332 -41
View File
@@ -13,32 +13,39 @@
--radius: 10px;
}
* { box-sizing: border-box; margin: 0; padding: 0; }
html, body { max-width: 100vw; overflow-x: hidden; }
body { background: var(--bg); color: var(--text);
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
font-size: 14px; min-height: 100vh; }
header { display: flex; align-items: center; gap: 12px;
padding: 16px 20px; background: var(--surface);
header { display: flex; align-items: center; gap: 10px;
padding: 12px 16px; background: var(--surface);
border-bottom: 1px solid var(--border);
position: sticky; top: 0; z-index: 100; }
header h1 { font-size: 16px; font-weight: 600; }
header .subtitle { font-size: 12px; color: var(--text-dim); }
.status-pill { margin-left: auto; display: flex; gap: 8px; align-items: center; }
position: sticky; top: 0; z-index: 100;
min-width: 0; }
header h1 { font-size: 16px; font-weight: 600; white-space: nowrap; }
header .subtitle { font-size: 12px; color: var(--text-dim);
white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.status-pill { margin-left: auto; display: flex; gap: 6px; align-items: center; flex-shrink: 0; }
.pill { display: flex; align-items: center; gap: 5px; padding: 4px 10px;
border-radius: 20px; font-size: 12px; font-weight: 600;
background: var(--surface2); border: 1px solid var(--border); }
background: var(--surface2); border: 1px solid var(--border); white-space: nowrap; }
.pill.ok { color: var(--green); border-color: var(--green); }
.pill.err { color: var(--red); border-color: var(--red); }
.dot { width: 7px; height: 7px; border-radius: 50%; background: currentColor;
animation: pulse 2s infinite; }
@keyframes pulse { 0%,100%{opacity:1} 50%{opacity:.4} }
main { padding: 20px; max-width: 1100px; margin: 0 auto; }
.tabs { display: flex; gap: 4px; margin-bottom: 20px;
border-bottom: 1px solid var(--border); }
.tab { padding: 10px 16px; cursor: pointer; color: var(--text-dim);
main { padding: 16px; max-width: 1100px; margin: 0 auto; }
.tabs { display: flex; gap: 2px; margin-bottom: 16px;
border-bottom: 1px solid var(--border);
overflow-x: auto; -webkit-overflow-scrolling: touch;
scrollbar-width: none; }
.tabs::-webkit-scrollbar { display: none; }
.tab { padding: 10px 14px; cursor: pointer; color: var(--text-dim);
font-weight: 500; border-bottom: 2px solid transparent;
margin-bottom: -1px; transition: color .15s, border-color .15s; }
margin-bottom: -1px; transition: color .15s, border-color .15s;
white-space: nowrap; flex-shrink: 0; }
.tab.active { color: var(--accent); border-color: var(--accent); }
.tab:hover { color: var(--text); }
.panel { display: none; }
@@ -137,11 +144,11 @@
.info-chip span { color: var(--text); font-weight: 600; }
/* Energy Dashboard */
.energy-wrap { max-width: 580px; margin: 0 auto; }
.energy-svg-wrap { background: var(--surface); border: 1px solid var(--border); border-radius: var(--radius); margin-bottom: 16px; overflow: hidden; }
.energy-kwh { display: grid; grid-template-columns: repeat(auto-fit, minmax(95px, 1fr)); gap: 10px; }
.kwh-card { background: var(--surface); border: 1px solid var(--border); border-radius: var(--radius); padding: 14px 8px; text-align: center; }
.kwh-card .kv { font-size: 18px; font-weight: 700; line-height: 1.2; font-variant-numeric: tabular-nums; }
.energy-wrap { max-width: 580px; margin: 0 auto; width: 100%; }
.energy-svg-wrap { background: var(--surface); border: 1px solid var(--border); border-radius: var(--radius); margin-bottom: 16px; overflow: hidden; width: 100%; }
.energy-kwh { display: grid; grid-template-columns: repeat(auto-fit, minmax(88px, 1fr)); gap: 8px; }
.kwh-card { background: var(--surface); border: 1px solid var(--border); border-radius: var(--radius); padding: 12px 6px; text-align: center; min-width: 0; }
.kwh-card .kv { font-size: 17px; font-weight: 700; line-height: 1.2; font-variant-numeric: tabular-nums; }
.kwh-card .kl { font-size: 10px; color: var(--text-dim); text-transform: uppercase; letter-spacing: .05em; margin-top: 4px; }
.flow-dot { /* dots via animateMotion — no CSS animation needed */ }
@@ -154,6 +161,15 @@
.toast.show { transform: translateY(0); opacity: 1; }
.toast.ok { border-color: var(--green); color: var(--green); }
.toast.err { border-color: var(--red); color: var(--red); }
@media (max-width: 520px) {
main { padding: 10px; }
.settings-section { max-width: 100%; }
.sensor-grid { grid-template-columns: repeat(auto-fill, minmax(140px, 1fr)); }
.inv-list { grid-template-columns: 1fr; }
.kwh-card .kv { font-size: 15px; }
header svg { display: none; }
}
</style>
</head>
<body>
@@ -175,6 +191,7 @@
<main>
<div class="tabs">
<div class="tab active" onclick="switchTab('energy')">Energie</div>
<div class="tab" onclick="switchTab('finance')">Finanzen</div>
<div class="tab" onclick="switchTab('live')">Live-Daten</div>
<div class="tab" onclick="switchTab('inverters')">Geräte</div>
<div class="tab" onclick="switchTab('settings')">Einstellungen</div>
@@ -185,6 +202,11 @@
<div id="energy-content"><div class="no-data">Warte auf erste Messung…</div></div>
</div>
<!-- Finance Panel -->
<div class="panel" id="panel-finance">
<div id="finance-content"><div class="no-data">Lade…</div></div>
</div>
<!-- Live Panel -->
<div class="panel" id="panel-live">
<div id="live-content"><div class="no-data">Warte auf erste Messung...</div></div>
@@ -199,6 +221,7 @@
<div class="panel" id="panel-settings">
<div class="settings-section">
<h3>MQTT Broker</h3>
<div id="mqtt-error-banner" style="display:none;background:var(--red,#c0392b);color:#fff;border-radius:8px;padding:10px 14px;margin-bottom:12px;font-size:.9rem;line-height:1.4"></div>
<div class="field"><label>Broker</label>
<input type="text" id="cfg-mqtt-broker" placeholder="core-mosquitto"></div>
<div class="field"><label>Port</label>
@@ -254,6 +277,23 @@
</div>
<div style="font-size:11px;color:var(--text-dim);margin-top:4px">z.B. 1 . 4 = 01. April</div>
</div>
<div class="field" style="border-top:1px solid var(--border);padding-top:12px;margin-top:4px">
<div style="display:flex;align-items:center;gap:8px;margin-bottom:10px">
<input type="checkbox" id="cfg-billing-tracker" style="width:auto;margin:0" onchange="updateBillingTrackerUI()">
<label for="cfg-billing-tracker" style="margin:0;cursor:pointer;font-size:13px;font-weight:600">Abschlags-Tracker aktivieren</label>
</div>
<div id="billing-tracker-fields" style="display:none">
<div style="display:grid;grid-template-columns:1fr 1fr;gap:8px">
<div><label style="font-size:11px;color:var(--text-dim)">Monatliche Rate (€)</label>
<input type="number" id="cfg-monthly-rate" step="0.01" min="0" placeholder="120.00">
</div>
<div><label style="font-size:11px;color:var(--text-dim)">Grundpreis pro Monat (€)</label>
<input type="number" id="cfg-grundpreis" step="0.01" min="0" placeholder="12.50">
</div>
</div>
<div style="font-size:11px;color:var(--text-dim);margin-top:6px">Zeigt Nachzahlung / Guthaben für das laufende Abrechnungsjahr.</div>
</div>
</div>
<button class="btn btn-primary" onclick="savePrices()">Speichern</button>
</div>
@@ -282,6 +322,8 @@
<div id="import-result" style="margin-top:.5rem;font-size:.85rem"></div>
</div>
</div>
<!-- Flash/OTA-Panel ausgelagert nach growatt-diagnose-tool (v1.9.0) -->
</main>
<!-- Inverter Edit Modal -->
@@ -326,6 +368,9 @@
<option value="2">2-phasig</option>
<option value="3" selected>3-phasig</option>
</select></div>
<div class="field"><label>Max. Ladestrom (A)</label>
<input type="number" id="ems-max-current" value="32" min="6" max="32" step="1">
<div style="font-size:11px;color:var(--muted);margin-top:3px">Obergrenze für Zwangsladen — auf die tatsächliche Wallbox-/Zuleitungs-Absicherung setzen (z.B. 16A bei einer 11kW-Wallbox), nicht höher als die Hardware zulässt</div></div>
</div>
<div class="modal-actions">
@@ -413,11 +458,12 @@ function showToast(msg, type) {
}
function switchTab(name) {
["energy","live","inverters","settings"].forEach((t, i) => {
["energy","finance","live","inverters","settings"].forEach((t, i) => {
document.querySelectorAll(".tab")[i].classList.toggle("active", t === name);
document.querySelectorAll(".panel")[i].classList.toggle("active", t === name);
});
if (name === "energy" || name === "live") startRefresh(); else stopRefresh();
if (name === "finance") loadFinance();
}
// ── Energy Dashboard ──────────────────────────────────────────
@@ -472,6 +518,15 @@ function renderSpotChart(spotData) {
</div>`;
}
function fEur(v) {
if (v == null) return null;
return v >= 100 ? v.toFixed(0) + ' €' : v.toFixed(2) + ' €';
}
function fKwh(v) {
if (v == null) return null;
return (v >= 100 ? v.toFixed(0) : v.toFixed(1)) + ' kWh';
}
function renderEnergy(inverters, aggregates, period, spotData) {
const el = document.getElementById("energy-content");
if (!aggregates || !Object.keys(aggregates).length) {
@@ -607,15 +662,6 @@ function renderEnergy(inverters, aggregates, period, spotData) {
const pi = period.price_import || 0.30;
const pe = period.price_export || 0.08;
function fEur(v) {
if (v == null) return null;
return v >= 100 ? v.toFixed(0) + ' €' : v.toFixed(2) + ' €';
}
function fKwh(v) {
if (v == null) return null;
return (v >= 100 ? v.toFixed(0) : v.toFixed(1)) + ' kWh';
}
function periodCard(label, kwh, cost, col, sub) {
if (kwh == null) return '';
return `<div class="kwh-card" style="border-top:3px solid ${col}">
@@ -648,12 +694,200 @@ function renderEnergy(inverters, aggregates, period, spotData) {
const spotHtml = (period.spot_chart !== false) ? renderSpotChart(spotData) : '';
const bt = period.billing_tracker;
const trackerHtml = bt ? (() => {
const nz = bt.nachzahlung_eur;
const isNachzahlung = nz > 0;
const nzColor = isNachzahlung ? '#e05c5c' : '#4caf82';
const nzLabel = isNachzahlung ? 'Voraussichtliche Nachzahlung' : 'Voraussichtliches Guthaben';
const nzSign = isNachzahlung ? '+' : '−';
return `<div style="margin-top:16px;background:var(--surface);border:1px solid var(--border);border-radius:var(--radius);padding:14px 16px">
<div style="font-size:10px;font-weight:700;letter-spacing:.08em;color:var(--text-dim);text-transform:uppercase;margin-bottom:12px">Abschlags-Übersicht · ${bt.payments_made} Zahlung${bt.payments_made !== 1 ? 'en' : ''}</div>
<div style="display:grid;grid-template-columns:repeat(3,1fr);gap:8px;margin-bottom:12px">
<div class="kwh-card">
<div class="kv">${fEur(bt.total_paid_eur)}</div>
<div class="kl">Bereits bezahlt<br><span style="opacity:.6">${bt.payments_made} × ${fEur(bt.monthly_rate_eur)}</span></div>
</div>
<div class="kwh-card">
<div class="kv">${fEur(bt.grundpreis_total_eur)}</div>
<div class="kl">Grundpreis anteilig<br><span style="opacity:.6">${bt.payments_made} × ${fEur(bt.grundpreis_eur_per_month)}</span></div>
</div>
<div class="kwh-card">
<div class="kv">${fEur(bt.energy_cost_eur)}</div>
<div class="kl">Energiekosten<br><span style="opacity:.6">Netzbezug</span></div>
</div>
</div>
<div style="display:flex;align-items:center;justify-content:space-between;background:var(--bg);border-radius:8px;padding:12px 16px">
<div>
<div style="font-size:12px;color:var(--text-dim)">${nzLabel}</div>
<div style="font-size:11px;color:var(--text-dim);margin-top:2px">Gesamtkosten ${fEur(bt.total_cost_eur)} − Bezahlt ${fEur(bt.total_paid_eur)}</div>
</div>
<div style="font-size:24px;font-weight:700;color:${nzColor};font-variant-numeric:tabular-nums">${nzSign}${fEur(Math.abs(nz))}</div>
</div>
</div>`;
})() : '';
el.innerHTML = `<div class="energy-wrap">
<div class="energy-svg-wrap">${svg}${spotHtml}</div>
${cards ? `<div style="margin-top:16px">${cards}</div>` : ''}
${trackerHtml}
</div>`;
}
// ── Finance Tab ───────────────────────────────────────────────
async function loadFinance() {
const el = document.getElementById("finance-content");
let data;
try { data = await fetchJSON(api("api/finance")); }
catch(e) { el.innerHTML = '<div class="no-data">Fehler beim Laden</div>'; return; }
try {
const { days, fixed_total_eur, spot_total_eur, savings_eur, period_start, spot_days, total_days } = data;
if (!days || days.length === 0) {
el.innerHTML = `<div style="text-align:center;padding:40px;color:var(--text-dim)">
<div style="font-size:32px;margin-bottom:12px">📊</div>
<div style="font-weight:600;margin-bottom:6px">Noch keine Daten</div>
<div style="font-size:13px">Der Tracker sammelt ab morgen täglich Daten.<br>Abrechnungsjahr ab ${period_start}.</div>
</div>`;
return;
}
const C = { fixed: '#58a6ff', spot: '#f0883e', grid: '#30363d', green: '#4caf82', red: '#e05c5c' };
// ── Empfehlung ───────────────────────────────────────────────
let empfehlung = '';
if (spot_total_eur !== null && spot_days >= 7) {
const lohnt = savings_eur > 0;
const col = lohnt ? C.green : C.red;
const icon = lohnt ? '✓' : '✗';
empfehlung = `<div style="display:flex;align-items:center;justify-content:space-between;background:var(--surface);border:1px solid ${col};border-radius:var(--radius);padding:18px 22px;margin-bottom:24px">
<div>
<div style="font-weight:700;font-size:15px;color:${col}">${icon} Flexibler Tarif würde sich ${lohnt ? 'lohnen' : 'nicht lohnen'}</div>
<div style="font-size:12px;color:var(--text-dim);margin-top:5px">Basierend auf ${spot_days} Tagen im Abrechnungsjahr</div>
</div>
<div style="font-size:26px;font-weight:700;color:${col};margin-left:20px;white-space:nowrap">${lohnt ? '−' : '+'}${fEur(Math.abs(savings_eur))}</div>
</div>`;
}
// ── Summary-Karten ───────────────────────────────────────────
const spotCard = spot_total_eur !== null
? `<div class="kwh-card" style="border-top:3px solid ${C.spot};padding:18px 10px">
<div class="kv" style="color:${C.spot};font-size:22px">${fEur(spot_total_eur)}</div>
<div class="kl" style="font-size:11px;margin-top:6px">Spot-Tarif (hypothetisch)<br><span style="opacity:.6">${spot_days} Tage mit Preisdaten</span></div>
</div>`
: `<div class="kwh-card" style="padding:18px 10px"><div class="kv" style="color:var(--text-dim);font-size:22px">–</div><div class="kl" style="font-size:11px;margin-top:6px">Spot-Tarif<br><span style="opacity:.6">Noch keine Daten</span></div></div>`;
const savCard = savings_eur !== null
? `<div class="kwh-card" style="border-top:3px solid ${savings_eur > 0 ? C.green : C.red};padding:18px 10px">
<div class="kv" style="color:${savings_eur > 0 ? C.green : C.red};font-size:22px">${savings_eur > 0 ? '−' : '+'}${fEur(Math.abs(savings_eur))}</div>
<div class="kl" style="font-size:11px;margin-top:6px">${savings_eur > 0 ? 'Ersparnis mit Spot' : 'Mehrkosten mit Spot'}<br><span style="opacity:.6">gegenüber Festpreis</span></div>
</div>`
: '';
const cards = `
<div style="font-size:11px;font-weight:600;text-transform:uppercase;letter-spacing:.08em;color:var(--text-dim);margin-bottom:10px">Kostenübersicht · Abrechnungsjahr</div>
<div class="energy-kwh" style="margin-bottom:28px;gap:12px">
<div class="kwh-card" style="border-top:3px solid ${C.fixed};padding:18px 10px">
<div class="kv" style="color:${C.fixed};font-size:22px">${fEur(fixed_total_eur)}</div>
<div class="kl" style="font-size:11px;margin-top:6px">Festpreis-Kosten<br><span style="opacity:.6">${total_days} Tage</span></div>
</div>
${spotCard}${savCard}
</div>`;
// ── SVG-Balkendiagramm ────────────────────────────────────────
// Bei langen Abrechnungsjahren nicht jeden einzelnen Tag zeigen — statt 112
// Tagesbalken lieber Wochenbalken, und davon nur die letzten 14 (reicht für
// einen Trend, bleibt aber lesbar statt endlos zu scrollen).
const rawN = days.length;
const bucket = rawN > 180 ? 14 : rawN > 21 ? 7 : 1;
const allPeriods = bucket === 1 ? days.map(d => ({
fixed_eur: d.fixed_eur, spot_eur: d.spot_eur, label: d.date.slice(5),
})) : (() => {
const out = [];
for (let i = 0; i < days.length; i += bucket) {
const chunk = days.slice(i, i + bucket);
const fixed = chunk.reduce((s, d) => s + (d.fixed_eur || 0), 0);
const spotVals = chunk.map(d => d.spot_eur).filter(v => v != null);
const spot = spotVals.length ? spotVals.reduce((a, b) => a + b, 0) : null;
out.push({ fixed_eur: fixed, spot_eur: spot, label: chunk[0].date.slice(5) });
}
return out;
})();
const MAX_PERIODS = 14;
const periods = allPeriods.slice(-MAX_PERIODS);
// Echtes responsives SVG: viewBox in einer "Mobile-first"-Breite, skaliert per
// width:100% frei auf den Container (Handy wie PC) — kein Scrollen mehr nötig,
// da wir uns auf max. 14 Balken begrenzen.
const n = periods.length;
const W = 380, H = 220, PL = 40, PR = 8, PT = 14, PB = 46;
const cW = W - PL - PR;
const cH = H - PT - PB;
const slot = cW / n;
const barW = Math.max(3, slot * 0.36);
const barGap = Math.max(1.5, slot * 0.08);
const rBar = Math.min(2.5, barW / 2);
const allVals = periods.flatMap(d => [d.fixed_eur, d.spot_eur].filter(v => v != null));
const maxVal = Math.max(...allVals, 0.01);
const yTicks = 4;
let gridLines = '', yLabels = '';
for (let i = 0; i <= yTicks; i++) {
const v = maxVal * i / yTicks;
const y = PT + cH - (cH * i / yTicks);
gridLines += `<line x1="${PL}" y1="${y.toFixed(1)}" x2="${W-PR}" y2="${y.toFixed(1)}" stroke="${C.grid}" stroke-width="1" stroke-dasharray="2,3"/>`;
yLabels += `<text x="${PL-6}" y="${(y+3).toFixed(1)}" text-anchor="end" font-size="9" fill="#8b949e">${fEur(v)}</text>`;
}
// Labels schräg statt horizontal — braucht bei 14 Balken pro Label deutlich
// weniger Breite, dadurch bleiben trotz Platzmangel alle 14 lesbar.
let bars = '', xLabels = '';
periods.forEach((d, i) => {
const cx = PL + i * slot + slot / 2;
const hFixed = Math.max(0, cH * d.fixed_eur / maxVal);
bars += `<rect x="${(cx - barGap/2 - barW).toFixed(1)}" y="${(PT + cH - hFixed).toFixed(1)}" width="${barW.toFixed(1)}" height="${Math.max(hFixed,1).toFixed(1)}" rx="${rBar}" fill="url(#gradFixed)"/>`;
if (d.spot_eur != null) {
const hSpot = Math.max(0, cH * d.spot_eur / maxVal);
bars += `<rect x="${(cx + barGap/2).toFixed(1)}" y="${(PT + cH - hSpot).toFixed(1)}" width="${barW.toFixed(1)}" height="${Math.max(hSpot,1).toFixed(1)}" rx="${rBar}" fill="url(#gradSpot)"/>`;
}
const ly = H - PB + 12;
xLabels += `<text x="${cx.toFixed(1)}" y="${ly}" text-anchor="end" font-size="8.5" fill="#8b949e" transform="rotate(-40 ${cx.toFixed(1)} ${ly})">${d.label}</text>`;
});
const defs = `<defs>
<linearGradient id="gradFixed" x1="0" y1="0" x2="0" y2="1">
<stop offset="0%" stop-color="${C.fixed}" stop-opacity="0.95"/>
<stop offset="100%" stop-color="${C.fixed}" stop-opacity="0.55"/>
</linearGradient>
<linearGradient id="gradSpot" x1="0" y1="0" x2="0" y2="1">
<stop offset="0%" stop-color="${C.spot}" stop-opacity="0.95"/>
<stop offset="100%" stop-color="${C.spot}" stop-opacity="0.55"/>
</linearGradient>
</defs>`;
const bucketLabel = bucket === 1 ? 'Tag' : bucket === 7 ? 'Woche' : '2 Wochen';
const rangeNote = allPeriods.length > MAX_PERIODS ? ` · letzte ${n}` : '';
const legend = `<div style="display:flex;gap:18px;margin-top:10px;font-size:11px;color:var(--text-dim)">
<span style="display:inline-flex;align-items:center;gap:6px"><span style="width:10px;height:10px;border-radius:3px;background:${C.fixed};display:inline-block"></span>Festpreis</span>
<span style="display:inline-flex;align-items:center;gap:6px"><span style="width:10px;height:10px;border-radius:3px;background:${C.spot};display:inline-block"></span>Spot (hypothetisch)</span>
</div>`;
const chartSection = `
<div style="border-top:1px solid var(--border);padding-top:24px;margin-top:4px">
<div style="font-size:11px;font-weight:600;text-transform:uppercase;letter-spacing:.08em;color:var(--text-dim);margin-bottom:14px">Kosten je ${bucketLabel}${rangeNote}</div>
<svg viewBox="0 0 ${W} ${H}" style="width:100%;max-width:640px;height:auto;display:block">
${defs}${gridLines}${yLabels}${bars}${xLabels}
</svg>
${legend}
</div>`;
el.innerHTML = `<div style="padding:4px 0">${empfehlung}${cards}${chartSection}</div>`;
} catch(e) { el.innerHTML = '<div class="no-data">Fehler beim Laden</div>'; console.error('loadFinance:', e); }
}
// ── Live Data ─────────────────────────────────────────────────
async function refreshData() {
@@ -806,6 +1040,7 @@ async function openModal(invId) {
document.getElementById("ems-timeout").value = inv.ems_timeout ?? 4;
document.getElementById("ems-target-hour").value = inv.ems_target_hour ?? 6;
document.getElementById("ems-phases").value = inv.ems_phases ?? 3;
document.getElementById("ems-max-current").value = inv.ems_max_current_ma ? inv.ems_max_current_ma / 1000 : 32;
document.getElementById("ems-enabled").checked = inv.ems_enabled ?? true;
} else {
document.getElementById("modal-title").textContent = "Gerät hinzufügen";
@@ -820,6 +1055,7 @@ async function openModal(invId) {
document.getElementById("ems-timeout").value = "4";
document.getElementById("ems-target-hour").value = "6";
document.getElementById("ems-phases").value = "3";
document.getElementById("ems-max-current").value = "32";
}
toggleEmsSection(modelSel.value);
modal.classList.add("open");
@@ -854,6 +1090,7 @@ async function saveInverter() {
inv.ems_timeout = parseFloat(document.getElementById("ems-timeout").value);
inv.ems_target_hour = parseInt(document.getElementById("ems-target-hour").value);
inv.ems_phases = parseInt(document.getElementById("ems-phases").value);
inv.ems_max_current_ma = Math.round(parseFloat(document.getElementById("ems-max-current").value) * 1000);
inv.ems_enabled = document.getElementById("ems-enabled").checked;
}
const idx = invertersList.findIndex(i => i.id === id);
@@ -894,10 +1131,22 @@ function updateTariffUI() {
document.getElementById("tariff-spot-fields").style.display = isSpot ? "" : "none";
}
function updateBillingTrackerUI() {
const enabled = document.getElementById("cfg-billing-tracker").checked;
document.getElementById("billing-tracker-fields").style.display = enabled ? "" : "none";
}
async function loadSettings() {
const cfg = await fetchJSON(api("api/config"));
globalConfig = cfg;
loadSurplusDevices();
const errBanner = document.getElementById("mqtt-error-banner");
if (cfg.mqtt_error) {
errBanner.textContent = "⚠ MQTT Fehler: " + cfg.mqtt_error;
errBanner.style.display = "";
} else {
errBanner.style.display = "none";
}
document.getElementById("cfg-mqtt-broker").value = cfg.mqtt_broker || "";
document.getElementById("cfg-mqtt-port").value = cfg.mqtt_port || 1883;
document.getElementById("cfg-mqtt-user").value = cfg.mqtt_user || "";
@@ -910,6 +1159,11 @@ async function loadSettings() {
document.getElementById("cfg-spot-chart").checked = cfg.spot_chart ?? true;
document.getElementById((cfg.tariff_type ?? "fixed") === "spot" ? "tariff-spot" : "tariff-fixed").checked = true;
updateTariffUI();
const trackerEnabled = cfg.billing_tracker_enabled ?? false;
document.getElementById("cfg-billing-tracker").checked = trackerEnabled;
document.getElementById("billing-tracker-fields").style.display = trackerEnabled ? "" : "none";
document.getElementById("cfg-monthly-rate").value = cfg.monthly_rate_eur ?? 0;
document.getElementById("cfg-grundpreis").value = cfg.grundpreis_eur_per_month ?? 0;
}
async function savePrices() {
@@ -921,8 +1175,11 @@ async function savePrices() {
spot_markup: parseFloat(document.getElementById("cfg-spot-markup").value || 0),
spot_country: document.getElementById("cfg-spot-country").value,
spot_chart: document.getElementById("cfg-spot-chart").checked,
billing_day: parseInt(document.getElementById("cfg-billing-day").value),
billing_month: parseInt(document.getElementById("cfg-billing-month").value),
billing_day: parseInt(document.getElementById("cfg-billing-day").value) || 1,
billing_month: parseInt(document.getElementById("cfg-billing-month").value) || 1,
billing_tracker_enabled: document.getElementById("cfg-billing-tracker").checked,
monthly_rate_eur: parseFloat(document.getElementById("cfg-monthly-rate").value || 0),
grundpreis_eur_per_month: parseFloat(document.getElementById("cfg-grundpreis").value || 0),
};
try {
await fetchJSON(api("api/config"), {
@@ -988,18 +1245,31 @@ async function importConfig(input) {
let surplusDevices = [];
function field(label, input) {
return `<div style="display:flex;flex-direction:column;gap:3px">
<span style="font-size:10px;color:var(--text-dim);text-transform:uppercase;letter-spacing:.05em;white-space:nowrap">${label}</span>
${input}
</div>`;
}
function renderSurplusDeviceRow(dev) {
const id = dev.id || ('sd_' + Math.random().toString(36).slice(2));
return `<div class="surplus-device-row" data-id="${esc(id)}">
<div style="display:flex;gap:8px;flex-wrap:wrap;align-items:center">
<input type="text" placeholder="Name" style="flex:1;min-width:110px" value="${esc(dev.name||'')}" data-field="name">
<input type="text" placeholder="Z2M Friendly Name" list="z2m-device-list" style="flex:1;min-width:140px" value="${esc(dev.z2m_name||'')}" data-field="z2m_name">
<input type="number" placeholder="Ab (W)" title="Schwellwert" style="width:76px" value="${dev.threshold_w||500}" min="0" step="50" data-field="threshold_w">
<input type="number" placeholder="Hysterese (W)" title="Hysterese" style="width:90px" value="${dev.hysteresis_w||150}" min="0" step="50" data-field="hysteresis_w">
<label style="cursor:pointer;display:flex;align-items:center;gap:4px;font-size:12px;white-space:nowrap">
<input type="checkbox" ${dev.enabled!==false?'checked':''} data-field="enabled" style="width:auto;margin:0"> Aktiv
</label>
<button class="btn btn-secondary" style="padding:3px 8px;font-size:12px;flex-shrink:0" onclick="removeSurplusDevice(this)">✕</button>
<div style="display:flex;gap:8px;flex-wrap:wrap;align-items:flex-end">
${field('Name', `<input type="text" placeholder="Heizstab Keller" style="width:130px" value="${esc(dev.name||'')}" data-field="name">`)}
${field('Z2M Friendly Name', `<input type="text" placeholder="heizstab-keller" list="z2m-device-list" style="width:160px" value="${esc(dev.z2m_name||'')}" data-field="z2m_name">`)}
${field('Schwellwert (W)', `<input type="number" style="width:90px" value="${dev.threshold_w||500}" min="0" step="50" data-field="threshold_w">`)}
${field('Hysterese (W)', `<input type="number" style="width:90px" value="${dev.hysteresis_w||150}" min="0" step="50" data-field="hysteresis_w">`)}
${field('Min. Laufzeit (min)', `<input type="number" style="width:90px" value="${dev.min_on_minutes||0}" min="0" step="5" data-field="min_on_minutes">`)}
<div style="display:flex;flex-direction:column;gap:6px;padding-bottom:2px">
<label style="cursor:pointer;display:flex;align-items:center;gap:4px;font-size:12px;white-space:nowrap">
<input type="checkbox" ${dev.enabled!==false?'checked':''} data-field="enabled" style="width:auto;margin:0"> Aktiv
</label>
<label style="cursor:pointer;display:flex;align-items:center;gap:4px;font-size:12px;white-space:nowrap" title="AUS bei Überschuss, EIN bei Netzbezug">
<input type="checkbox" ${dev.inverted?'checked':''} data-field="inverted" style="width:auto;margin:0"> Invertiert
</label>
</div>
<button class="btn btn-secondary" style="padding:3px 8px;font-size:12px;flex-shrink:0;align-self:flex-end" onclick="removeSurplusDevice(this)">✕</button>
</div>
</div>`;
}
@@ -1041,7 +1311,9 @@ function _collectSurplusDevices() {
name: row.querySelector('[data-field=name]').value.trim(),
z2m_name: row.querySelector('[data-field=z2m_name]').value.trim(),
threshold_w: parseFloat(row.querySelector('[data-field=threshold_w]').value) || 0,
hysteresis_w: parseFloat(row.querySelector('[data-field=hysteresis_w]').value) || 0,
hysteresis_w: parseFloat(row.querySelector('[data-field=hysteresis_w]').value) || 0,
min_on_minutes: parseFloat(row.querySelector('[data-field=min_on_minutes]').value) || 0,
inverted: row.querySelector('[data-field=inverted]').checked,
enabled: row.querySelector('[data-field=enabled]').checked,
}));
}
@@ -1069,25 +1341,44 @@ function renderSurplusStatus(surplusData) {
const rows = surplusData.devices
.filter(d => d.enabled !== false)
.map(d => {
const on = states[d.z2m_name] === true;
const st = states[d.z2m_name] || {};
const on = st.on === true;
const onMin = st.on_minutes ?? null;
const minOn = d.min_on_minutes || 0;
const dot = `<span style="width:8px;height:8px;border-radius:50%;display:inline-block;flex-shrink:0;background:${on?'var(--green)':'var(--border)'}"></span>`;
let info = '';
if (on && minOn > 0 && onMin !== null) {
const remaining = Math.max(0, minOn - onMin);
info = remaining > 0
? `<span style="font-size:11px;color:var(--text-dim)">(${remaining}min Mindestlaufzeit)</span>`
: `<span style="font-size:11px;color:var(--text-dim)">(${onMin}min)</span>`;
}
return `<div style="display:flex;align-items:center;gap:6px;font-size:12px;padding:4px 0">
${dot}
<span>${esc(d.name||d.z2m_name)}</span>
<span style="color:var(--text-dim);font-size:11px">ab ${d.threshold_w}W</span>
<span style="color:var(--text-dim);font-size:11px">${d.inverted ? '↓' : '↑'} ${d.threshold_w}W</span>
${info}
<span style="margin-left:auto;font-weight:600;color:${on?'var(--green)':'var(--text-dim)'}">${on?'EIN':'AUS'}</span>
</div>`;
}).join('');
if (!rows) return '';
const surplusW = surplusData.surplus_w ?? null;
const surplusInfo = surplusW !== null
? `<span style="font-size:11px;color:var(--text-dim);margin-left:auto">${surplusW >= 0 ? '+' : ''}${Math.round(surplusW)} W Überschuss</span>`
: '';
return `<div class="inv-section">
<div class="inv-header">
<div class="inv-title">Überschuss-Geräte</div>
<div class="inv-badge ok">Z2M</div>
${surplusInfo}
</div>
<div style="padding:0 2px">${rows}</div>
</div>`;
}
// Flash-Wizard + Setup-Wizard ausgelagert nach growatt-diagnose-tool (v1.9.0) —
// Geräte werden jetzt direkt über "+ Gerät hinzufügen" im Geräte-Tab angelegt.
// ── Init ──────────────────────────────────────────────────────
(async () => {
-278
View File
@@ -1,278 +0,0 @@
#!/usr/bin/env python3
"""ShineDiag — Vor-Ort-Diagnose für Growatt-Wechselrichter via ShineLAN-X."""
import json
import logging
import os
import sqlite3
import sys
import threading
import time
from typing import Dict, List, Optional
from flask import Flask, jsonify, request, send_from_directory
sys.path.insert(0, os.path.join(os.path.dirname(__file__), "../../haos-addon/src"))
from inverters import INVERTERS
from modbus_client import ModbusReader
logging.basicConfig(level=logging.INFO, format="%(asctime)s %(levelname)s: %(message)s")
log = logging.getLogger(__name__)
WEB_DIR = os.path.join(os.path.dirname(__file__), "web")
DB_PATH = os.environ.get("DB_PATH", "/var/lib/shinediag/history.db")
POLL_SEC = int(os.environ.get("POLL_SEC", "30"))
app = Flask(__name__, static_folder=WEB_DIR)
# ── SQLite ────────────────────────────────────────────────────
_db_lock = threading.Lock()
_db_conn: Optional[sqlite3.Connection] = None
def _db() -> sqlite3.Connection:
global _db_conn
if _db_conn is None:
os.makedirs(os.path.dirname(DB_PATH), exist_ok=True)
_db_conn = sqlite3.connect(DB_PATH, check_same_thread=False)
_db_conn.execute("PRAGMA journal_mode=WAL")
_db_conn.execute("PRAGMA synchronous=NORMAL")
_db_conn.execute("""
CREATE TABLE IF NOT EXISTS measurements (
inv_key TEXT NOT NULL,
sensor_id TEXT NOT NULL,
ts REAL NOT NULL,
value REAL NOT NULL
)""")
_db_conn.execute("""
CREATE INDEX IF NOT EXISTS idx_inv_sensor_ts
ON measurements(inv_key, sensor_id, ts)""")
_db_conn.commit()
return _db_conn
def db_write(inv_key: str, ts: float, values: Dict[str, float]):
rows = [(inv_key, sid, ts, v) for sid, v in values.items()]
with _db_lock:
_db().executemany(
"INSERT INTO measurements(inv_key,sensor_id,ts,value) VALUES(?,?,?,?)", rows)
_db().commit()
def db_query(inv_key: str, sensor_ids: List[str],
from_ts: float, to_ts: float) -> Dict[str, List]:
result = {sid: [] for sid in sensor_ids}
placeholders = ",".join("?" * len(sensor_ids))
with _db_lock:
rows = _db().execute(f"""
SELECT sensor_id, ts, value FROM measurements
WHERE inv_key=? AND sensor_id IN ({placeholders})
AND ts BETWEEN ? AND ?
ORDER BY ts ASC
""", [inv_key] + sensor_ids + [from_ts, to_ts]).fetchall()
for sid, ts, val in rows:
if sid in result:
result[sid].append({"ts": ts, "v": val})
return result
def db_cleanup(days: int = 7):
cutoff = time.time() - days * 86400
with _db_lock:
n = _db().execute("DELETE FROM measurements WHERE ts<?", (cutoff,)).rowcount
_db().commit()
if n:
log.info("DB: %d alte Einträge gelöscht", n)
# ── Poll-State ────────────────────────────────────────────────
_state_lock = threading.Lock()
_poll_thread: Optional[threading.Thread] = None
_poll_stop = threading.Event()
_conn_cfg: Dict = {} # aktuelle Verbindungs-Config
_last_values: Dict = {} # letzter erfolgreicher Poll
_last_ts: float = 0.0
_last_ok: bool = False
_inv_key: str = ""
def _poll_worker():
global _last_values, _last_ts, _last_ok
cfg = _conn_cfg.copy()
inverter = INVERTERS[cfg["model"]]
reader = ModbusReader(cfg["ip"], int(cfg["port"]), int(cfg["slave"]), timeout=5.0)
log.info("Poll-Thread gestartet: %s @ %s alle %ds", inverter.name, cfg["ip"], POLL_SEC)
while not _poll_stop.is_set():
t0 = time.time()
values = reader.read(inverter)
with _state_lock:
if values:
_last_values = values
_last_ts = t0
_last_ok = True
db_write(_inv_key, t0, values)
else:
_last_ok = False
_poll_stop.wait(max(0.0, POLL_SEC - (time.time() - t0)))
reader.close()
log.info("Poll-Thread beendet")
def _start_poll(cfg: dict):
global _poll_thread, _conn_cfg, _inv_key, _last_values, _last_ts, _last_ok
_stop_poll()
with _state_lock:
_conn_cfg = cfg
_inv_key = f"{cfg['ip']}:{cfg['port']}:{cfg['slave']}:{cfg['model']}"
_last_values = {}
_last_ts = 0.0
_last_ok = False
_poll_stop.clear()
_poll_thread = threading.Thread(target=_poll_worker, daemon=True, name="poll")
_poll_thread.start()
def _stop_poll():
global _poll_thread
_poll_stop.set()
if _poll_thread and _poll_thread.is_alive():
_poll_thread.join(timeout=10)
_poll_thread = None
# ── Flask ─────────────────────────────────────────────────────
@app.get("/")
def index():
return send_from_directory(WEB_DIR, "index.html")
@app.get("/api/models")
def api_models():
return jsonify({
k: {"id": v.id, "name": v.name, "manufacturer": v.manufacturer,
"sensor_count": len(v.sensors)}
for k, v in INVERTERS.items()
})
@app.post("/api/connect")
def api_connect():
body = request.get_json(force=True) or {}
for field in ("ip", "model"):
if not body.get(field):
return jsonify({"error": f"'{field}' fehlt"}), 400
if body["model"] not in INVERTERS:
return jsonify({"error": "Unbekanntes Modell"}), 400
cfg = {
"ip": body["ip"],
"port": int(body.get("port", 502)),
"slave": int(body.get("slave", 1)),
"model": body["model"],
}
_start_poll(cfg)
db_cleanup()
return jsonify({"ok": True})
@app.post("/api/disconnect")
def api_disconnect():
_stop_poll()
return jsonify({"ok": True})
@app.get("/api/status")
def api_status():
with _state_lock:
connected = _poll_thread is not None and _poll_thread.is_alive()
cfg = _conn_cfg.copy()
values = _last_values.copy()
ts = _last_ts
ok = _last_ok
inverter = INVERTERS.get(cfg.get("model", ""), None)
sensors = []
if inverter:
sensors = [{"id": s.id, "name": s.name, "unit": s.unit,
"device_class": s.device_class, "icon": s.icon}
for s in inverter.sensors]
return jsonify({
"connected": connected,
"modbus_ok": ok,
"last_ts": ts,
"cfg": cfg,
"values": values,
"sensors": sensors,
"inv_name": inverter.name if inverter else "",
})
@app.get("/api/chart-data")
def api_chart_data():
sensors = request.args.get("sensors", "")
hours = min(float(request.args.get("hours", "24")), 24 * 7)
if not sensors:
return jsonify({"error": "sensors required"}), 400
sensor_ids = [s.strip() for s in sensors.split(",") if s.strip()]
to_ts = time.time()
from_ts = to_ts - hours * 3600
with _state_lock:
key = _inv_key
if not key:
return jsonify({sid: [] for sid in sensor_ids})
return jsonify(db_query(key, sensor_ids, from_ts, to_ts))
@app.post("/api/scan")
def api_scan():
"""Einmal-Scan ohne Polling — für schnelle Diagnose."""
body = request.get_json(force=True) or {}
model_id = body.get("model", "MIC_1500_TL_X")
inverter = INVERTERS.get(model_id)
if not inverter:
return jsonify({"error": f"Unbekanntes Modell: {model_id}"}), 400
reader = ModbusReader(body.get("ip", "10.0.0.100"),
int(body.get("port", 502)),
int(body.get("slave", 1)), timeout=5.0)
values = reader.read(inverter)
reader.close()
if values is None:
return jsonify({"ok": False, "error": "Keine Verbindung"}), 502
return jsonify({"ok": True, "model": inverter.name,
"sensors": [{"id": s.id, "name": s.name, "value": values.get(s.id),
"unit": s.unit} for s in inverter.sensors]})
@app.post("/api/raw")
def api_raw():
body = request.get_json(force=True) or {}
start = max(0, int(body.get("start", 0)))
count = min(125, max(1, int(body.get("count", 100))))
from pymodbus.client import ModbusTcpClient
client = ModbusTcpClient(body.get("ip", "10.0.0.100"),
port=int(body.get("port", 502)), timeout=5.0)
if not client.connect():
return jsonify({"ok": False, "error": "Verbindung fehlgeschlagen"}), 502
try:
r = client.read_input_registers(start, count, slave=int(body.get("slave", 1)))
if r.isError():
return jsonify({"ok": False, "error": str(r)}), 502
return jsonify({"ok": True, "registers": [
{"addr": start + i, "hex": f"0x{v:04X}",
"dec": v, "signed": v if v < 0x8000 else v - 0x10000}
for i, v in enumerate(r.registers)
]})
finally:
client.close()
if __name__ == "__main__":
_db()
port = int(os.environ.get("PORT", "80"))
log.info("ShineDiag auf Port %d", port)
app.run(host="0.0.0.0", port=port, threaded=True)
-8
View File
@@ -1,8 +0,0 @@
# /etc/dhcpcd.conf — eth0 feste IP für ShineDiag
# Diese Datei ans Ende von /etc/dhcpcd.conf anhängen
interface eth0
static ip_address=10.0.0.1/24
static routers=
static domain_name_servers=
noipv6
-9
View File
@@ -1,9 +0,0 @@
# /etc/dnsmasq.conf — DHCP für ShineLAN-X auf eth0
interface=eth0
dhcp-range=10.0.0.100,10.0.0.200,12h
# ShineLAN-X bekommt immer 10.0.0.100 (MAC anpassen!)
# dhcp-host=AA:BB:CC:DD:EE:FF,shinelanx,10.0.0.100
bind-interfaces
-16
View File
@@ -1,16 +0,0 @@
# /etc/hostapd/hostapd.conf — WiFi-Hotspot "ShineDiag"
interface=wlan0
driver=nl80211
ssid=ShineDiag
hw_mode=g
channel=6
ieee80211n=1
wmm_enabled=1
auth_algs=1
wpa=2
wpa_passphrase=shinelanx
wpa_key_mgmt=WPA-PSK
wpa_pairwise=CCMP
rsn_pairwise=CCMP
-69
View File
@@ -1,69 +0,0 @@
#!/bin/bash
# ShineDiag Pi 3B Setup — einmalig als root ausführen
set -e
echo "=== ShineDiag Installation ==="
# Pakete
apt-get update -q
apt-get install -y python3-pip hostapd dnsmasq
# Python-Abhängigkeiten
pip3 install flask pymodbus --break-system-packages
# Netzwerk: eth0 statische IP
cat setup/dhcpcd.conf >> /etc/dhcpcd.conf
# DHCP-Server für eth0
cp setup/dnsmasq.conf /etc/dnsmasq.conf
systemctl enable dnsmasq
# WiFi-Hotspot
cp setup/hostapd.conf /etc/hostapd/hostapd.conf
echo 'DAEMON_CONF="/etc/hostapd/hostapd.conf"' >> /etc/default/hostapd
systemctl unmask hostapd
systemctl enable hostapd
# Wlan0 IP
cat >> /etc/dhcpcd.conf << 'EOF'
interface wlan0
static ip_address=10.0.1.1/24
nohook wpa_supplicant
EOF
# DHCP auch für wlan0 (MacBook-Verbindung)
cat >> /etc/dnsmasq.conf << 'EOF'
interface=wlan0
dhcp-range=10.0.1.100,10.0.1.200,1h
EOF
# systemd-Service für ShineDiag
INSTALL_DIR="$(pwd)"
cat > /etc/systemd/system/shinediag.service << EOF
[Unit]
Description=ShineDiag Diagnose-Tool
After=network.target
[Service]
ExecStart=/usr/bin/python3 ${INSTALL_DIR}/diagnose.py
WorkingDirectory=${INSTALL_DIR}
Environment=PORT=80
Environment=SHINELANX_IP=10.0.0.100
Restart=always
User=root
[Install]
WantedBy=multi-user.target
EOF
systemctl daemon-reload
systemctl enable shinediag
echo ""
echo "=== Fertig! Bitte neu starten: sudo reboot ==="
echo ""
echo "Nach dem Neustart:"
echo " WiFi: 'ShineDiag' | Passwort: shinelanx"
echo " Browser: http://10.0.1.1"
-601
View File
@@ -1,601 +0,0 @@
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1">
<title>ShineDiag</title>
<style>
*{box-sizing:border-box;margin:0;padding:0}
body{font-family:system-ui,sans-serif;background:#0f172a;color:#e2e8f0;min-height:100vh}
/* Header */
header{background:#1e293b;padding:12px 16px;border-bottom:1px solid #334155;
display:flex;align-items:center;justify-content:space-between;gap:8px;position:sticky;top:0;z-index:10}
.header-left{display:flex;align-items:center;gap:10px}
header h1{font-size:1.1rem;font-weight:700;color:#f8fafc}
header p{font-size:.7rem;color:#94a3b8}
.status-dot{width:10px;height:10px;border-radius:50%;background:#334155;flex-shrink:0}
.status-dot.ok{background:#22c55e;box-shadow:0 0 6px #22c55e88}
.status-dot.err{background:#ef4444}
/* Tabs */
.tabs{display:flex;background:#1e293b;border-bottom:1px solid #334155;overflow-x:auto;scrollbar-width:none}
.tabs::-webkit-scrollbar{display:none}
.tab{flex:1;min-width:80px;padding:10px 4px;border:none;background:none;color:#64748b;
font-size:.8rem;cursor:pointer;border-bottom:2px solid transparent;white-space:nowrap}
.tab.active{color:#60a5fa;border-bottom-color:#3b82f6}
/* Main */
main{padding:12px;max-width:640px;margin:0 auto}
.card{background:#1e293b;border:1px solid #334155;border-radius:12px;padding:14px;margin-bottom:12px}
.card-title{font-size:.75rem;font-weight:600;color:#64748b;text-transform:uppercase;
letter-spacing:.05em;margin-bottom:10px}
/* Connect form */
label{display:block;font-size:.78rem;color:#94a3b8;margin-bottom:3px;margin-top:8px}
input,select{width:100%;padding:9px 11px;border-radius:8px;border:1px solid #334155;
background:#0f172a;color:#f1f5f9;font-size:.9rem}
input:focus,select:focus{outline:2px solid #3b82f6;border-color:transparent}
.row2{display:grid;grid-template-columns:1fr 1fr;gap:8px}
button{width:100%;padding:11px;border-radius:8px;border:none;cursor:pointer;
font-size:.9rem;font-weight:600;margin-top:10px;transition:opacity .15s}
button:disabled{opacity:.4;cursor:not-allowed}
.btn-primary{background:#3b82f6;color:#fff}.btn-primary:hover:not(:disabled){background:#2563eb}
.btn-danger{background:#dc2626;color:#fff}.btn-danger:hover:not(:disabled){background:#b91c1c}
.btn-secondary{background:#475569;color:#e2e8f0}.btn-secondary:hover:not(:disabled){background:#334155}
.btn-success{background:#059669;color:#fff}.btn-success:hover:not(:disabled){background:#047857}
/* Metric cards grid */
.metrics{display:grid;grid-template-columns:repeat(auto-fill,minmax(140px,1fr));gap:8px;margin-bottom:4px}
.metric{background:#0f172a;border:1px solid #334155;border-radius:10px;padding:12px 10px}
.metric-label{font-size:.7rem;color:#64748b;text-transform:uppercase;letter-spacing:.04em;margin-bottom:4px}
.metric-value{font-size:1.5rem;font-weight:700;color:#f1f5f9;line-height:1}
.metric-unit{font-size:.75rem;color:#94a3b8;margin-top:2px}
.metric.highlight .metric-value{color:#60a5fa}
.metric.warn .metric-value{color:#f59e0b}
.metric.ok .metric-value{color:#4ade80}
/* Chart */
.chart-wrap{position:relative;margin-top:8px}
canvas.chart{width:100%;border-radius:8px;display:block}
.chart-legend{display:flex;flex-wrap:wrap;gap:10px;margin-top:6px;padding:0 4px}
.legend-item{display:flex;align-items:center;gap:5px;font-size:.75rem;color:#94a3b8}
.legend-dot{width:10px;height:10px;border-radius:50%}
/* Range selector */
.range-btns{display:flex;gap:4px;margin-bottom:10px}
.range-btn{flex:1;padding:7px 4px;border-radius:6px;border:1px solid #334155;
background:transparent;color:#94a3b8;font-size:.75rem;cursor:pointer}
.range-btn.active{background:#3b82f6;color:#fff;border-color:#3b82f6}
/* Sensor table */
.sensor-table{width:100%;border-collapse:collapse}
.sensor-table th{text-align:left;font-size:.72rem;color:#64748b;padding:5px 6px;border-bottom:1px solid #334155}
.sensor-table td{padding:7px 6px;font-size:.83rem;border-bottom:1px solid #1e293b}
.sensor-table tr:last-child td{border-bottom:none}
.sensor-table .v{font-weight:600;color:#f1f5f9;text-align:right}
.sensor-table .u{color:#64748b;font-size:.72rem;text-align:right}
/* Raw table */
.raw-table{width:100%;border-collapse:collapse;font-family:monospace;font-size:.78rem}
.raw-table th{text-align:left;color:#64748b;padding:4px 6px;border-bottom:1px solid #334155}
.raw-table td{padding:4px 6px;border-bottom:1px solid #1e293b;color:#cbd5e1}
.raw-table td:first-child{color:#60a5fa}
.raw-controls{display:grid;grid-template-columns:1fr 1fr;gap:8px;margin-bottom:8px}
/* Status bar */
.update-bar{font-size:.72rem;color:#475569;text-align:right;margin-bottom:4px}
/* Alert */
.alert{padding:10px 12px;border-radius:8px;font-size:.83rem;margin-bottom:10px}
.alert-info{background:#0c1a2e;color:#60a5fa;border:1px solid #1e3a5f}
.alert-ok{background:#052e16;color:#4ade80;border:1px solid #166534}
.alert-err{background:#1c0a09;color:#f87171;border:1px solid #7f1d1d}
.spinner{display:inline-block;width:14px;height:14px;border:2px solid #fff4;
border-top-color:#fff;border-radius:50%;animation:spin .7s linear infinite}
@keyframes spin{to{transform:rotate(360deg)}}
.hidden{display:none!important}
</style>
</head>
<body>
<header>
<div class="header-left">
<svg width="28" height="28" viewBox="0 0 24 24" fill="none" stroke="#f59e0b" stroke-width="2">
<circle cx="12" cy="12" r="5"/>
<line x1="12" y1="1" x2="12" y2="3"/><line x1="12" y1="21" x2="12" y2="23"/>
<line x1="4.22" y1="4.22" x2="5.64" y2="5.64"/><line x1="18.36" y1="18.36" x2="19.78" y2="19.78"/>
<line x1="1" y1="12" x2="3" y2="12"/><line x1="21" y1="12" x2="23" y2="12"/>
<line x1="4.22" y1="19.78" x2="5.64" y2="18.36"/><line x1="18.36" y1="5.64" x2="19.78" y2="4.22"/>
</svg>
<div><h1>ShineDiag</h1><p id="hdrSub">Nicht verbunden</p></div>
</div>
<div id="statusDot" class="status-dot"></div>
</header>
<div class="tabs">
<button class="tab active" onclick="showTab('dashboard')">Dashboard</button>
<button class="tab" onclick="showTab('charts')">Diagramme</button>
<button class="tab" onclick="showTab('sensors')">Sensoren</button>
<button class="tab" onclick="showTab('raw')">Rohdaten</button>
</div>
<main>
<!-- ── Dashboard ── -->
<div id="tabDashboard">
<!-- Connect card (shown when disconnected) -->
<div class="card" id="connectCard">
<div class="card-title">Verbindung</div>
<label>ShineLAN-X IP</label>
<input id="cfgIp" value="10.0.0.100">
<div class="row2">
<div><label>Port</label><input id="cfgPort" type="number" value="502"></div>
<div><label>Modbus-Adresse</label><input id="cfgSlave" type="number" value="1"></div>
</div>
<label>Modell</label>
<select id="cfgModel"></select>
<button class="btn-primary" id="btnConnect" onclick="connect()">Verbinden &amp; Aufzeichnen</button>
</div>
<!-- Live data (shown when connected) -->
<div id="liveCard" class="hidden">
<div class="update-bar" id="updateBar">—</div>
<div class="card">
<div class="card-title">Spannung</div>
<div class="metrics" id="voltageMetrics"></div>
</div>
<div class="card">
<div class="card-title">Frequenz &amp; Leistung</div>
<div class="metrics" id="powerMetrics"></div>
</div>
<div class="card" id="extraCard" style="display:none">
<div class="card-title">Weitere</div>
<div class="metrics" id="extraMetrics"></div>
</div>
<button class="btn-danger" onclick="disconnect()">Verbindung trennen</button>
</div>
</div>
<!-- ── Charts ── -->
<div id="tabCharts" class="hidden">
<div class="card" id="noDataCard">
<div class="alert alert-info">Zuerst im Dashboard verbinden und einige Messpunkte sammeln.</div>
</div>
<div id="chartSection" class="hidden">
<div class="range-btns">
<button class="range-btn active" onclick="setRange(1,this)">1 Std</button>
<button class="range-btn" onclick="setRange(6,this)">6 Std</button>
<button class="range-btn" onclick="setRange(24,this)">24 Std</button>
<button class="range-btn" onclick="setRange(168,this)">7 Tage</button>
</div>
<div class="card">
<div class="card-title">Spannung (V)</div>
<div class="chart-wrap"><canvas id="chartVoltage" class="chart" height="160"></canvas></div>
<div class="chart-legend" id="legendVoltage"></div>
</div>
<div class="card">
<div class="card-title">Frequenz (Hz)</div>
<div class="chart-wrap"><canvas id="chartFreq" class="chart" height="120"></canvas></div>
</div>
<div class="card">
<div class="card-title">Leistung (W)</div>
<div class="chart-wrap"><canvas id="chartPower" class="chart" height="140"></canvas></div>
<div class="chart-legend" id="legendPower"></div>
</div>
</div>
</div>
<!-- ── Sensors ── -->
<div id="tabSensors" class="hidden">
<div class="card">
<table class="sensor-table">
<thead><tr><th>Sensor</th><th style="text-align:right">Wert</th><th style="text-align:right">Einheit</th></tr></thead>
<tbody id="sensorBody"><tr><td colspan="3" style="color:#475569;padding:12px">Noch nicht verbunden.</td></tr></tbody>
</table>
<button class="btn-success" id="btnExport" onclick="exportJson()" style="margin-top:10px">Export JSON</button>
</div>
</div>
<!-- ── Raw ── -->
<div id="tabRaw" class="hidden">
<div class="card">
<div class="raw-controls">
<div><label>Start</label><input id="rawStart" type="number" value="0"></div>
<div><label>Anzahl (max 125)</label><input id="rawCount" type="number" value="100"></div>
</div>
<button class="btn-secondary" onclick="rawScan()">Register lesen</button>
<div id="rawAlert" style="margin-top:8px"></div>
<div style="overflow-x:auto;margin-top:8px">
<table class="raw-table">
<thead><tr><th>Addr</th><th>Hex</th><th>Dez</th><th>Signed</th></tr></thead>
<tbody id="rawBody"></tbody>
</table>
</div>
</div>
</div>
</main>
<script>
// ── State ────────────────────────────────────────────────────
let currentStatus = null;
let chartHours = 1;
let refreshTimer = null;
const COLORS = ['#60a5fa','#fbbf24','#4ade80','#f87171','#a78bfa'];
// Sensor-IDs je Messtyp
const VOLTAGE_IDS = ['voltage_l1','voltage_l2','voltage_l3','ac_voltage','bat_voltage'];
const FREQ_IDS = ['frequency'];
const POWER_IDS = ['total_power','ac_power','ac_power_total','pv_power','bat_charge_power','bat_discharge_power'];
const EXTRA_IDS = ['temperature','bat_soc','bat_current'];
const LABELS = {
voltage_l1:'Spannung L1', voltage_l2:'Spannung L2', voltage_l3:'Spannung L3',
ac_voltage:'Spannung AC', bat_voltage:'Batterie',
frequency:'Frequenz',
total_power:'Netzleistung', ac_power:'AC Leistung', ac_power_total:'AC Leistung',
pv_power:'PV Leistung', bat_charge_power:'Bat. Laden', bat_discharge_power:'Bat. Entladen',
temperature:'Temperatur', bat_soc:'Ladezustand', bat_current:'Bat. Strom',
};
// ── Tabs ─────────────────────────────────────────────────────
function showTab(name) {
['dashboard','charts','sensors','raw'].forEach(t => {
document.getElementById('tab'+cap(t)).classList.toggle('hidden', t !== name);
});
document.querySelectorAll('.tab').forEach((btn, i) => {
btn.classList.toggle('active', ['dashboard','charts','sensors','raw'][i] === name);
});
if (name === 'charts') loadCharts();
}
const cap = s => s[0].toUpperCase() + s.slice(1);
// ── Models ───────────────────────────────────────────────────
async function loadModels() {
const res = await fetch('/api/models');
const data = await res.json();
const sel = document.getElementById('cfgModel');
sel.innerHTML = '';
for (const [id, m] of Object.entries(data)) {
const o = document.createElement('option');
o.value = id;
o.textContent = `${m.name}`;
sel.appendChild(o);
}
// restore from localStorage
const saved = localStorage.getItem('shinediag_model');
if (saved) sel.value = saved;
}
// ── Connect ──────────────────────────────────────────────────
async function connect() {
const btn = document.getElementById('btnConnect');
btn.disabled = true;
btn.innerHTML = '<span class="spinner"></span>';
const cfg = {
ip: document.getElementById('cfgIp').value,
port: parseInt(document.getElementById('cfgPort').value),
slave: parseInt(document.getElementById('cfgSlave').value),
model: document.getElementById('cfgModel').value,
};
localStorage.setItem('shinediag_model', cfg.model);
const res = await fetch('/api/connect', {method:'POST',
headers:{'Content-Type':'application/json'}, body: JSON.stringify(cfg)});
const d = await res.json();
btn.disabled = false;
btn.textContent = 'Verbinden & Aufzeichnen';
if (d.ok) startRefresh();
}
async function disconnect() {
await fetch('/api/disconnect', {method:'POST'});
stopRefresh();
showDisconnected();
}
// ── Refresh ──────────────────────────────────────────────────
function startRefresh() {
stopRefresh();
pollStatus();
refreshTimer = setInterval(pollStatus, 30000);
}
function stopRefresh() {
if (refreshTimer) { clearInterval(refreshTimer); refreshTimer = null; }
}
async function pollStatus() {
try {
const res = await fetch('/api/status');
currentStatus = await res.json();
renderDashboard(currentStatus);
} catch(e) {
console.error(e);
}
}
// ── Dashboard ────────────────────────────────────────────────
function renderDashboard(s) {
const dot = document.getElementById('statusDot');
const hdr = document.getElementById('hdrSub');
if (!s.connected) { showDisconnected(); return; }
document.getElementById('connectCard').classList.add('hidden');
document.getElementById('liveCard').classList.remove('hidden');
dot.className = 'status-dot ' + (s.modbus_ok ? 'ok' : 'err');
hdr.textContent = s.inv_name || s.cfg.model;
if (s.last_ts) {
const ago = Math.round(Date.now()/1000 - s.last_ts);
document.getElementById('updateBar').textContent =
`Letztes Update: ${ago < 60 ? ago+'s' : Math.round(ago/60)+'min'} · alle 30s`;
}
const v = s.values;
// Spannung
const voltIds = VOLTAGE_IDS.filter(id => v[id] !== undefined);
renderMetrics('voltageMetrics', voltIds.map(id => ({
label: LABELS[id], value: v[id]?.toFixed(1), unit: 'V', cls: 'highlight'
})));
// Frequenz + Leistung
const pm = [];
if (v.frequency !== undefined)
pm.push({label:'Frequenz', value: v.frequency?.toFixed(2), unit:'Hz', cls:'ok'});
const pwrId = POWER_IDS.find(id => v[id] !== undefined);
if (pwrId) {
const pval = v[pwrId];
pm.push({label: LABELS[pwrId], value: pval?.toFixed(0), unit:'W',
cls: pval < 0 ? 'ok' : 'warn'});
}
renderMetrics('powerMetrics', pm);
// Extras (Temperatur, SoC, …)
const extras = EXTRA_IDS.filter(id => v[id] !== undefined)
.map(id => ({label: LABELS[id], value: v[id]?.toFixed(1),
unit: id==='bat_soc'?'%':id==='temperature'?'°C':'A'}));
document.getElementById('extraCard').style.display = extras.length ? '' : 'none';
if (extras.length) renderMetrics('extraMetrics', extras);
// Sensor-Tabelle aktualisieren
if (s.sensors.length) {
const tbody = document.getElementById('sensorBody');
tbody.innerHTML = s.sensors.map(sen => {
const val = v[sen.id] !== undefined ? v[sen.id] : '—';
return `<tr><td>${esc(sen.name)}</td>
<td class="v">${val}</td>
<td class="u">${esc(sen.unit||'')}</td></tr>`;
}).join('');
}
}
function renderMetrics(containerId, items) {
document.getElementById(containerId).innerHTML = items.map(m =>
`<div class="metric ${m.cls||''}">
<div class="metric-label">${esc(m.label)}</div>
<div class="metric-value">${esc(m.value??'—')}</div>
<div class="metric-unit">${esc(m.unit||'')}</div>
</div>`
).join('');
}
function showDisconnected() {
document.getElementById('connectCard').classList.remove('hidden');
document.getElementById('liveCard').classList.add('hidden');
document.getElementById('statusDot').className = 'status-dot';
document.getElementById('hdrSub').textContent = 'Nicht verbunden';
}
// ── Charts ───────────────────────────────────────────────────
function setRange(h, btn) {
chartHours = h;
document.querySelectorAll('.range-btn').forEach(b => b.classList.remove('active'));
btn.classList.add('active');
loadCharts();
}
async function loadCharts() {
if (!currentStatus?.connected) {
document.getElementById('noDataCard').classList.remove('hidden');
document.getElementById('chartSection').classList.add('hidden');
return;
}
document.getElementById('noDataCard').classList.add('hidden');
document.getElementById('chartSection').classList.remove('hidden');
const v = currentStatus.values || {};
const voltIds = VOLTAGE_IDS.filter(id => v[id] !== undefined);
const pwrIds = POWER_IDS.filter(id => v[id] !== undefined);
const sensors = [...voltIds, 'frequency', ...pwrIds].join(',');
const res = await fetch(`/api/chart-data?sensors=${sensors}&hours=${chartHours}`);
const data = await res.json();
drawLineChart('chartVoltage', voltIds.map((id,i) => ({
label: LABELS[id], color: COLORS[i], points: data[id]||[]
})), 'legendVoltage');
drawLineChart('chartFreq', [{
label:'Frequenz', color: COLORS[2], points: data.frequency||[]
}], null, {yMin:49, yMax:51});
drawLineChart('chartPower', pwrIds.map((id,i) => ({
label: LABELS[id], color: COLORS[i], points: data[id]||[]
})), 'legendPower');
}
function drawLineChart(canvasId, series, legendId, opts={}) {
const canvas = document.getElementById(canvasId);
const dpr = window.devicePixelRatio || 1;
const W = canvas.parentElement.clientWidth;
const H = canvas.height || 140;
canvas.width = W * dpr;
canvas.height = H * dpr;
canvas.style.width = W + 'px';
canvas.style.height = H + 'px';
const ctx = canvas.getContext('2d');
ctx.scale(dpr, dpr);
const PAD = {top:10, right:10, bottom:28, left:42};
const iW = W - PAD.left - PAD.right;
const iH = H - PAD.top - PAD.bottom;
ctx.clearRect(0, 0, W, H);
ctx.fillStyle = '#0f172a';
ctx.fillRect(0, 0, W, H);
const allPts = series.flatMap(s => s.points);
if (allPts.length < 2) {
ctx.fillStyle = '#475569';
ctx.font = '13px system-ui';
ctx.textAlign = 'center';
ctx.fillText('Noch keine Daten', W/2, H/2);
return;
}
const tsMin = Math.min(...allPts.map(p => p.ts));
const tsMax = Math.max(...allPts.map(p => p.ts));
let vMin = opts.yMin ?? Math.min(...allPts.map(p => p.v));
let vMax = opts.yMax ?? Math.max(...allPts.map(p => p.v));
if (vMin === vMax) { vMin -= 1; vMax += 1; }
const vPad = (vMax - vMin) * 0.08;
vMin -= vPad; vMax += vPad;
const tx = ts => PAD.left + (ts - tsMin) / (tsMax - tsMin) * iW;
const ty = v => PAD.top + (1 - (v - vMin) / (vMax - vMin)) * iH;
// Grid
ctx.strokeStyle = '#1e293b'; ctx.lineWidth = 1;
for (let i = 0; i <= 4; i++) {
const y = PAD.top + iH * i / 4;
ctx.beginPath(); ctx.moveTo(PAD.left, y); ctx.lineTo(PAD.left + iW, y); ctx.stroke();
const val = vMax - (vMax - vMin) * i / 4;
ctx.fillStyle = '#475569'; ctx.font = '10px system-ui'; ctx.textAlign = 'right';
ctx.fillText(val.toFixed(val > 100 ? 0 : 1), PAD.left - 4, y + 4);
}
// X-Achse Zeitstempel
const fmtTs = ts => {
const d = new Date(ts * 1000);
if (chartHours <= 6) return d.toLocaleTimeString('de',{hour:'2-digit',minute:'2-digit'});
return d.toLocaleDateString('de',{weekday:'short',hour:'2-digit',minute:'2-digit'});
};
ctx.fillStyle = '#475569'; ctx.font = '10px system-ui'; ctx.textAlign = 'center';
[0, 0.25, 0.5, 0.75, 1].forEach(f => {
const ts = tsMin + (tsMax - tsMin) * f;
ctx.fillText(fmtTs(ts), PAD.left + iW * f, H - 6);
});
// Series
series.forEach(s => {
if (!s.points.length) return;
ctx.beginPath();
ctx.strokeStyle = s.color;
ctx.lineWidth = 2;
ctx.lineJoin = 'round';
s.points.forEach((p, i) => {
const x = tx(p.ts), y = ty(p.v);
i === 0 ? ctx.moveTo(x, y) : ctx.lineTo(x, y);
});
ctx.stroke();
});
// Legend
if (legendId) {
document.getElementById(legendId).innerHTML = series.map(s =>
`<div class="legend-item">
<div class="legend-dot" style="background:${s.color}"></div>
<span>${esc(s.label)}</span>
</div>`
).join('');
}
}
// ── Raw ──────────────────────────────────────────────────────
async function rawScan() {
const alert = document.getElementById('rawAlert');
const s = currentStatus;
const cfg = s?.cfg || {ip:'10.0.0.100', port:502, slave:1};
alert.innerHTML = '<div class="alert alert-info">Lese Register…</div>';
try {
const res = await fetch('/api/raw', {method:'POST',
headers:{'Content-Type':'application/json'},
body: JSON.stringify({
ip: cfg.ip, port: cfg.port, slave: cfg.slave,
start: parseInt(document.getElementById('rawStart').value),
count: parseInt(document.getElementById('rawCount').value),
})
});
const d = await res.json();
if (!d.ok) { alert.innerHTML = `<div class="alert alert-err">${esc(d.error)}</div>`; return; }
document.getElementById('rawBody').innerHTML = d.registers.map(r =>
`<tr><td>${r.addr}</td><td>${r.hex}</td><td>${r.dec}</td><td>${r.signed}</td></tr>`
).join('');
alert.innerHTML = `<div class="alert alert-ok">&#x2713; ${d.registers.length} Register</div>`;
} catch(e) {
alert.innerHTML = `<div class="alert alert-err">${esc(e.message)}</div>`;
}
}
// ── Export ───────────────────────────────────────────────────
function exportJson() {
if (!currentStatus) return;
const blob = new Blob([JSON.stringify(currentStatus, null, 2)], {type:'application/json'});
const a = document.createElement('a');
a.href = URL.createObjectURL(blob);
a.download = `shinediag_${new Date().toISOString().slice(0,19).replace(/:/g,'-')}.json`;
a.click();
}
// ── Helpers ──────────────────────────────────────────────────
function esc(s) {
return String(s??'').replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;');
}
// ── Init ─────────────────────────────────────────────────────
async function init() {
await loadModels();
const savedIp = localStorage.getItem('shinediag_ip');
if (savedIp) document.getElementById('cfgIp').value = savedIp;
// Reconnect if server already polling
const res = await fetch('/api/status');
const s = await res.json();
if (s.connected) {
currentStatus = s;
renderDashboard(s);
startRefresh();
}
}
init();
</script>
</body>
</html>