Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1c787349e3 | ||
|
|
1b78857b3d | ||
|
|
6f30fe6b1c | ||
|
|
9d954d6bd6 | ||
|
|
5b6bed16db | ||
|
|
e6422892f3 | ||
|
|
608f6bfd42 | ||
|
|
722301b1d6 | ||
|
|
e9168ee72b | ||
|
|
e342fff539 | ||
|
|
ee404065ce | ||
|
|
eb797ed8b7 | ||
|
|
f5ed7c5df6 | ||
|
|
b36b0194f7 | ||
|
|
04a8fb3125 | ||
|
|
d9f94d3f28 | ||
|
|
c4047fc804 | ||
|
|
5564a50c3c | ||
|
|
dc2df891fb | ||
|
|
a361c30f1b | ||
|
|
a9f33c8e9e | ||
|
|
512b743b16 | ||
|
|
cb5f23d486 | ||
|
|
7904d498b5 | ||
|
|
5a00c7b5d3 | ||
|
|
b64bcde203 | ||
|
|
b3f67df967 | ||
|
|
e72ad2db19 | ||
|
|
3b3d4055f6 | ||
|
|
15c0ede72e | ||
|
|
bbfb11fb9c | ||
|
|
2456f356b4 | ||
|
|
dfb42e6902 | ||
|
|
fec49ec4fb | ||
|
|
5ab8ee75fb |
+11
-6
@@ -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/
|
||||
|
||||
@@ -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
@@ -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) |
|
||||
|
||||
@@ -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 |
|
||||
@@ -1,5 +0,0 @@
|
||||
.pio
|
||||
.vscode/.browse.c_cpp.db*
|
||||
.vscode/c_cpp_properties.json
|
||||
.vscode/launch.json
|
||||
.vscode/ipch
|
||||
-10
@@ -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"
|
||||
]
|
||||
}
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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>☀ ShineLAN-X</h1>"
|
||||
"<div class='sub'>Growatt Modbus → 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'>"
|
||||
"💾 Speichern & Neustart</button>"
|
||||
"<button class='btn rst' type='button' "
|
||||
"onclick='if(confirm(\"Wirklich auf Werkseinstellungen zur\\u00FCcksetzen?\"))"
|
||||
"location=\"/reset\"'>↻ 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);
|
||||
}
|
||||
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.
|
||||
@@ -0,0 +1 @@
|
||||
firmware/
|
||||
@@ -1,7 +1,7 @@
|
||||
name: ShineBridge
|
||||
version: "1.8.12"
|
||||
version: "1.10.2"
|
||||
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
|
||||
@@ -13,6 +13,7 @@ host_network: true
|
||||
ingress: true
|
||||
ingress_port: 8099
|
||||
panel_icon: mdi:solar-power
|
||||
homeassistant_api: true
|
||||
options:
|
||||
mqtt_broker: "core-mosquitto"
|
||||
mqtt_port: 1883
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -0,0 +1,76 @@
|
||||
import json
|
||||
import logging
|
||||
import os
|
||||
import urllib.error
|
||||
import urllib.request
|
||||
from typing import Dict, Optional
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
# Suffix am ESPHome-Entity-Namen -> ShineBridge-interner Sensor-Id.
|
||||
# Passt zur esphome-growatt-Komponente (siehe growatt-diagnose-tool/ShineWifi-X) —
|
||||
# Entity-Id ergibt sich als sensor.<prefix>_<suffix>, z.B. sensor.growatt_wifi_x_1_dcpower.
|
||||
ENTITY_SUFFIX_MAP = {
|
||||
"pv_voltage": "dcvoltage",
|
||||
"pv_current": "dcinputcurrent",
|
||||
"pv_power": "dcpower",
|
||||
"grid_frequency": "acfrequency",
|
||||
"grid_voltage": "acvoltage",
|
||||
"grid_current": "acoutputcurrent",
|
||||
"ac_power": "acpower",
|
||||
"energy_today": "energytoday",
|
||||
"energy_total": "energytotal",
|
||||
"inverter_temp": "temperature",
|
||||
}
|
||||
|
||||
|
||||
class HAEntityReader:
|
||||
"""Liest Wechselrichter-Werte aus bereits bestehenden Home-Assistant-Entities
|
||||
(z.B. ein per ESPHome direkt in HA eingebundener ShineWifi-X-Stick) statt sie
|
||||
selbst per Modbus/UDP zu pollen.
|
||||
|
||||
Braucht 'homeassistant_api: true' in config.yaml — HAOS injiziert dann
|
||||
SUPERVISOR_TOKEN und die interne API ist unter http://supervisor/core/api
|
||||
erreichbar (kein manuell erstelltes Long-Lived Token nötig)."""
|
||||
|
||||
BASE_URL = "http://supervisor/core/api"
|
||||
|
||||
def __init__(self, entity_prefix: str):
|
||||
self.entity_prefix = entity_prefix
|
||||
self.token = os.environ.get("SUPERVISOR_TOKEN", "")
|
||||
if not self.token:
|
||||
related = [k for k in os.environ if "TOKEN" in k.upper() or "SUPERVISOR" in k.upper() or "HASSIO" in k.upper()]
|
||||
log.warning("[HA-Entity] SUPERVISOR_TOKEN nicht gesetzt. Verwandte Env-Vars gefunden: %s", related or "keine")
|
||||
|
||||
def read(self, inverter=None) -> Optional[Dict[str, float]]:
|
||||
if not self.entity_prefix:
|
||||
log.error("[HA-Entity] Kein Entity-Prefix konfiguriert")
|
||||
return None
|
||||
if not self.token:
|
||||
log.error("[HA-Entity %s] Kein SUPERVISOR_TOKEN — 'homeassistant_api: true' in config.yaml gesetzt und Add-on neu installiert?", self.entity_prefix)
|
||||
return None
|
||||
|
||||
result: Dict[str, float] = {}
|
||||
any_ok = False
|
||||
for sensor_id, suffix in ENTITY_SUFFIX_MAP.items():
|
||||
val = self._get_state(f"sensor.{self.entity_prefix}_{suffix}")
|
||||
if val is not None:
|
||||
result[sensor_id] = val
|
||||
any_ok = True
|
||||
return result if any_ok else None
|
||||
|
||||
def _get_state(self, entity_id: str) -> Optional[float]:
|
||||
req = urllib.request.Request(
|
||||
f"{self.BASE_URL}/states/{entity_id}",
|
||||
headers={"Authorization": f"Bearer {self.token}"},
|
||||
)
|
||||
try:
|
||||
with urllib.request.urlopen(req, timeout=5) as r:
|
||||
data = json.loads(r.read())
|
||||
return round(float(data["state"]), 3)
|
||||
except (urllib.error.HTTPError, urllib.error.URLError, ValueError, KeyError) as e:
|
||||
log.debug("[HA-Entity] %s nicht lesbar: %s", entity_id, e)
|
||||
return None
|
||||
|
||||
def close(self):
|
||||
pass
|
||||
+77
-15
@@ -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()
|
||||
|
||||
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 sensor_id, ts, value in rows:
|
||||
result.setdefault(sensor_id, []).append((ts, value))
|
||||
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:
|
||||
|
||||
@@ -197,6 +197,23 @@ def _kathrein_sensors() -> List[Sensor]:
|
||||
]
|
||||
|
||||
|
||||
def _wifi_x_sensors() -> List[Sensor]:
|
||||
# reg/count/scale/data_type ungenutzt bei protocol="ha_entity" — Werte kommen
|
||||
# fertig geparst von HAEntityReader, hier nur Metadaten fuer MQTT-Discovery.
|
||||
return [
|
||||
Sensor("pv_voltage", "PV Spannung", 0, 1, 1.0, "V", "voltage", "measurement", "mdi:solar-panel"),
|
||||
Sensor("pv_current", "PV Strom", 0, 1, 1.0, "A", "current", "measurement", "mdi:solar-panel"),
|
||||
Sensor("pv_power", "PV Leistung", 0, 1, 1.0, "W", "power", "measurement", "mdi:solar-panel"),
|
||||
Sensor("grid_frequency", "Netz-Frequenz", 0, 1, 1.0, "Hz", "frequency", "measurement", "mdi:sine-wave"),
|
||||
Sensor("grid_voltage", "Netz-Spannung", 0, 1, 1.0, "V", "voltage", "measurement", "mdi:flash"),
|
||||
Sensor("grid_current", "Netz-Strom", 0, 1, 1.0, "A", "current", "measurement", "mdi:flash"),
|
||||
Sensor("ac_power", "AC Ausgangsleistung", 0, 1, 1.0, "W", "power", "measurement", "mdi:flash"),
|
||||
Sensor("energy_today", "Energie Heute", 0, 1, 1.0, "kWh", "energy", "total_increasing", "mdi:solar-power"),
|
||||
Sensor("energy_total", "Energie Gesamt", 0, 1, 1.0, "kWh", "energy", "total_increasing", "mdi:solar-power"),
|
||||
Sensor("inverter_temp", "Wechselrichter Temp.", 0, 1, 1.0, "°C", "temperature", "measurement", "mdi:thermometer"),
|
||||
]
|
||||
|
||||
|
||||
INVERTERS = {
|
||||
"MIC_1500_TL_X": Inverter(
|
||||
id="MIC_1500_TL_X",
|
||||
@@ -247,9 +264,19 @@ 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="",
|
||||
),
|
||||
"GROWATT_WIFI_X": Inverter(
|
||||
id="GROWATT_WIFI_X",
|
||||
name="Growatt ShineWifi-X (ESPHome)",
|
||||
manufacturer="Growatt",
|
||||
sensors=_wifi_x_sensors(),
|
||||
read_ranges=[],
|
||||
protocol="ha_entity",
|
||||
goodwe_family="",
|
||||
),
|
||||
}
|
||||
|
||||
+233
-32
@@ -14,6 +14,7 @@ import history
|
||||
from modbus_client import ModbusReader
|
||||
from goodwe_client import GoodweReader
|
||||
from wallbox_client import WallboxReader
|
||||
from ha_entity_client import HAEntityReader
|
||||
from ems_controller import EmsController
|
||||
from mqtt_publisher import MqttPublisher
|
||||
from surplus_devices import SurplusDeviceController
|
||||
@@ -30,6 +31,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 +55,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 +93,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 +118,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)
|
||||
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]
|
||||
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)
|
||||
# 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 +168,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 ───────────────────────────────────────────────
|
||||
|
||||
@@ -187,6 +228,37 @@ def _get_pv_surplus() -> float:
|
||||
|
||||
# ── 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")
|
||||
@@ -201,24 +273,29 @@ def _poll_loop(inv_cfg: Dict[str, Any], stop: threading.Event):
|
||||
log.error("[%s] Ungültige Konfiguration: %s", inv_id, e)
|
||||
return
|
||||
|
||||
host = inv_cfg["modbus_ip"]
|
||||
host = inv_cfg.get("modbus_ip", "")
|
||||
ems: Optional[EmsController] = None
|
||||
if inverter.protocol == "goodwe_udp":
|
||||
reader = GoodweReader(host=host, family=inverter.goodwe_family)
|
||||
log.info("[%s] Poll-Loop: %s @ %s (Goodwe UDP/8899) alle %ds",
|
||||
inv_id, inverter.name, host, interval)
|
||||
elif inverter.protocol == "ha_entity":
|
||||
reader = HAEntityReader(entity_prefix=inv_cfg.get("ha_entity_prefix", ""))
|
||||
log.info("[%s] Poll-Loop: %s (HA-Entity Prefix: %s) alle %ds",
|
||||
inv_id, inverter.name, inv_cfg.get("ha_entity_prefix", ""), 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)
|
||||
|
||||
@@ -238,6 +315,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)
|
||||
@@ -250,12 +334,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)
|
||||
@@ -264,6 +360,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
|
||||
@@ -275,9 +372,12 @@ 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:
|
||||
_fail_count += 1
|
||||
if _fail_count >= _OFFLINE_THRESHOLD:
|
||||
d["modbus_ok"] = False
|
||||
if _publisher:
|
||||
_publisher.publish_status("offline", prefix)
|
||||
@@ -288,6 +388,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()
|
||||
@@ -388,6 +496,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")
|
||||
@@ -399,17 +508,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:
|
||||
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:
|
||||
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})
|
||||
@@ -483,8 +600,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:
|
||||
@@ -493,8 +610,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:
|
||||
@@ -650,10 +837,17 @@ def api_import_config():
|
||||
if not isinstance(inverters, list):
|
||||
return jsonify({"error": "inverters ungültig"}), 400
|
||||
for inv in inverters:
|
||||
if not inv.get("modbus_ip"):
|
||||
model_id = inv.get("inverter_model")
|
||||
if model_id not in INVERTERS:
|
||||
return jsonify({"error": f"Unbekanntes Modell: {model_id}"}), 400
|
||||
protocol = INVERTERS[model_id].protocol
|
||||
# modbus_ip wird als generisches Host-Feld fuer alle IP-basierten Protokolle
|
||||
# wiederverwendet (modbus, goodwe_udp, kathrein) — nur ha_entity braucht keins.
|
||||
if protocol == "ha_entity":
|
||||
if not inv.get("ha_entity_prefix"):
|
||||
return jsonify({"error": f"ha_entity_prefix fehlt in Gerät {inv.get('name', '?')}"}), 400
|
||||
elif not inv.get("modbus_ip"):
|
||||
return jsonify({"error": f"modbus_ip fehlt in Gerät {inv.get('name', '?')}"}), 400
|
||||
if inv.get("inverter_model") not in INVERTERS:
|
||||
return jsonify({"error": f"Unbekanntes Modell: {inv.get('inverter_model')}"}), 400
|
||||
|
||||
with State.lock:
|
||||
if mqtt := data.get("mqtt"):
|
||||
@@ -717,6 +911,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")
|
||||
@@ -731,8 +928,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")
|
||||
|
||||
@@ -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,10 +53,16 @@ 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
|
||||
if rc != 0:
|
||||
log.warning("MQTT getrennt rc=%d", rc)
|
||||
|
||||
def _on_message(self, client, userdata, msg):
|
||||
@@ -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,
|
||||
|
||||
@@ -36,12 +36,17 @@ class SurplusDeviceController:
|
||||
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:
|
||||
# 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 currently_on and surplus_w < (threshold - hysteresis):
|
||||
elif should_turn_off:
|
||||
on_since = self._on_since.get(name, 0.0)
|
||||
if now - on_since >= min_on_s:
|
||||
self._send(name, False)
|
||||
|
||||
+365
-41
@@ -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 -->
|
||||
@@ -293,12 +335,19 @@
|
||||
<input type="text" id="modal-name" placeholder="z.B. Dach Süd"></div>
|
||||
<div class="field"><label>Gerätetyp</label>
|
||||
<select id="modal-model"></select></div>
|
||||
<div id="modbus-fields">
|
||||
<div class="field"><label>IP-Adresse</label>
|
||||
<input type="text" id="modal-ip" placeholder="10.10.20.190"></div>
|
||||
<div class="field"><label>Modbus Port</label>
|
||||
<input type="number" id="modal-port" value="502"></div>
|
||||
<div class="field"><label>Modbus Slave-Adresse</label>
|
||||
<input type="number" id="modal-addr" value="1" min="1" max="247"></div>
|
||||
</div>
|
||||
<div id="ha-entity-fields" style="display:none">
|
||||
<div class="field"><label>HA Entity-Prefix</label>
|
||||
<input type="text" id="modal-ha-prefix" placeholder="growatt_wifi_x_1">
|
||||
<div style="font-size:11px;color:var(--muted);margin-top:3px">Ohne "sensor."-Präfix — z.B. bei <code>sensor.growatt_wifi_x_1_dcpower</code> ist der Wert <code>growatt_wifi_x_1</code>. Braucht ein bereits per ESPHome in HA eingebundenes Gerät, keinen direkten Netzwerkzugriff.</div></div>
|
||||
</div>
|
||||
<div class="field"><label>MQTT Topic-Präfix</label>
|
||||
<input type="text" id="modal-prefix" placeholder="growatt/wechselrichter1"></div>
|
||||
<div class="field"><label>Abfrageintervall (Sekunden)</label>
|
||||
@@ -326,6 +375,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 +465,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 +525,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 +669,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 +701,234 @@ 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 ───────────────────────────────────────────────
|
||||
|
||||
// Erzeugt plausible, aber frei erfundene Tagesdaten fürs Beispiel-Diagramm im
|
||||
// Leerzustand (Erstinstallation). Deterministisch (kein echter Zufall), damit
|
||||
// die Vorschau bei jedem Laden gleich aussieht statt bei jedem Reload zu springen.
|
||||
function exampleFinanceDays(n) {
|
||||
const out = [];
|
||||
const today = new Date();
|
||||
for (let i = n - 1; i >= 0; i--) {
|
||||
const d = new Date(today); d.setDate(d.getDate() - i);
|
||||
const wave = 4.2 + 2.6 * Math.sin(i * 0.6) + (i % 3 === 0 ? 1.4 : 0);
|
||||
const fixed = Math.max(1.5, wave);
|
||||
const spot = Math.max(0.8, fixed * (0.68 + 0.1 * Math.cos(i * 0.9)));
|
||||
out.push({ date: d.toISOString().slice(0, 10), fixed_eur: +fixed.toFixed(2), spot_eur: +spot.toFixed(2) });
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
// Baut das Kosten-Balkendiagramm (echte oder Beispiel-Daten). Aggregiert ab
|
||||
// 14 Wochen (98 Tagen) angesammelter Historie automatisch von Tages- auf
|
||||
// Wochenbalken; zeigt davon je nach verfügbarer Breite die letzten 10–28
|
||||
// Perioden (rollierendes Fenster — ältere Balken wandern rechts raus, sobald
|
||||
// neue dazukommen).
|
||||
function buildCostChart(days, el, isExample) {
|
||||
const C = isExample
|
||||
? { fixed: '#5b6472', spot: '#6b6155', grid: '#262b33' }
|
||||
: { fixed: '#58a6ff', spot: '#f0883e', grid: '#30363d' };
|
||||
|
||||
const rawN = days.length;
|
||||
const bucket = rawN >= 98 ? 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;
|
||||
})();
|
||||
|
||||
// Container-Breite messen und die SVG in echten Pixeln in genau dieser Breite
|
||||
// zeichnen (keine viewBox-Skalierung) — Höhe bleibt dabei fix, unabhängig von
|
||||
// der Breite. Auf breiten Screens werden mehr Perioden gezeigt statt dieselben
|
||||
// künstlich zu verbreitern.
|
||||
const H = 240, PL = 40, PR = 8, PT = 14, PB = 46;
|
||||
const measuredW = el.clientWidth || 380;
|
||||
const W = Math.max(300, measuredW);
|
||||
const cW = W - PL - PR;
|
||||
const IDEAL_SLOT = 24; // px/Periode, entspricht dem Slot, der auf Handy-Breite gut aussah
|
||||
const maxPeriods = Math.min(28, Math.max(10, Math.round(cW / IDEAL_SLOT)));
|
||||
const periods = allPeriods.slice(-maxPeriods);
|
||||
|
||||
const n = periods.length;
|
||||
const cH = H - PT - PB;
|
||||
const slot = cW / n;
|
||||
const barW = Math.max(3, Math.min(26, 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>`;
|
||||
}
|
||||
|
||||
const gradId = isExample ? 'Ex' : '';
|
||||
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${gradId})"/>`;
|
||||
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${gradId})"/>`;
|
||||
}
|
||||
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${gradId}" 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${gradId}" 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' : 'Woche';
|
||||
const rangeNote = allPeriods.length > n ? ` · 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>`;
|
||||
|
||||
return `
|
||||
<div style="position:relative;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 width="${W}" height="${H}" style="display:block">
|
||||
${defs}${gridLines}${yLabels}${bars}${xLabels}
|
||||
</svg>
|
||||
${legend}
|
||||
${isExample ? `<div style="position:absolute;top:20px;right:0;font-size:10px;font-weight:700;letter-spacing:.08em;color:var(--text-dim);background:var(--surface2);border:1px solid var(--border);border-radius:20px;padding:3px 10px">BEISPIEL</div>` : ''}
|
||||
</div>`;
|
||||
}
|
||||
|
||||
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) {
|
||||
const exampleChart = buildCostChart(exampleFinanceDays(14), el, true);
|
||||
el.innerHTML = `<div style="padding:4px 0">
|
||||
<div style="text-align:center;padding:28px 20px;color:var(--text-dim);border:1px dashed var(--border);border-radius:var(--radius);margin-bottom:24px">
|
||||
<div style="font-size:28px;margin-bottom:10px">📊</div>
|
||||
<div style="font-weight:600;margin-bottom:6px;color:var(--text)">Noch keine echten Daten</div>
|
||||
<div style="font-size:13px">Der Tracker sammelt ab morgen täglich Daten.<br>Abrechnungsjahr ab ${period_start}. Unten siehst du, wie die Ansicht dann aussieht.</div>
|
||||
</div>
|
||||
${exampleChart}
|
||||
</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>`;
|
||||
|
||||
const chartSection = buildCostChart(days, el, false);
|
||||
|
||||
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() {
|
||||
@@ -768,7 +1043,9 @@ function renderInverterList() {
|
||||
</div>
|
||||
</div>
|
||||
<div class="inv-card-meta">
|
||||
📡 ${esc(inv.modbus_ip)}:${inv.modbus_port || 502} · Slave ${inv.modbus_address || 1}<br>
|
||||
${inv.inverter_model === "GROWATT_WIFI_X"
|
||||
? `🏠 HA-Entity: ${esc(inv.ha_entity_prefix || "–")}`
|
||||
: `📡 ${esc(inv.modbus_ip)}:${inv.modbus_port || 502} · Slave ${inv.modbus_address || 1}`}<br>
|
||||
📨 ${esc(inv.mqtt_topic_prefix)}<br>
|
||||
⏱ alle ${inv.update_interval || 30}s
|
||||
</div>
|
||||
@@ -781,16 +1058,19 @@ function renderInverterList() {
|
||||
el.innerHTML = cards + `<div class="add-btn" onclick="openModal()">+ Gerät hinzufügen</div>`;
|
||||
}
|
||||
|
||||
function toggleEmsSection(model) {
|
||||
function onModelChange(model) {
|
||||
document.getElementById("ems-section").style.display =
|
||||
model === "KATHREIN_WALLBOX" ? "" : "none";
|
||||
const isHaEntity = model === "GROWATT_WIFI_X";
|
||||
document.getElementById("modbus-fields").style.display = isHaEntity ? "none" : "";
|
||||
document.getElementById("ha-entity-fields").style.display = isHaEntity ? "" : "none";
|
||||
}
|
||||
|
||||
async function openModal(invId) {
|
||||
await loadModels();
|
||||
const modal = document.getElementById("modal-backdrop");
|
||||
const modelSel = document.getElementById("modal-model");
|
||||
modelSel.onchange = () => toggleEmsSection(modelSel.value);
|
||||
modelSel.onchange = () => onModelChange(modelSel.value);
|
||||
if (invId) {
|
||||
const inv = invertersList.find(i => i.id === invId) || {};
|
||||
document.getElementById("modal-title").textContent = "Gerät bearbeiten";
|
||||
@@ -800,12 +1080,14 @@ async function openModal(invId) {
|
||||
document.getElementById("modal-ip").value = inv.modbus_ip || "";
|
||||
document.getElementById("modal-port").value = inv.modbus_port || 502;
|
||||
document.getElementById("modal-addr").value = inv.modbus_address || 1;
|
||||
document.getElementById("modal-ha-prefix").value = inv.ha_entity_prefix || "";
|
||||
document.getElementById("modal-prefix").value = inv.mqtt_topic_prefix || "";
|
||||
document.getElementById("modal-interval").value = inv.update_interval || 30;
|
||||
document.getElementById("ems-min-pv").value = inv.ems_min_pv ?? 1400;
|
||||
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";
|
||||
@@ -814,14 +1096,16 @@ async function openModal(invId) {
|
||||
document.getElementById("modal-ip").value = "";
|
||||
document.getElementById("modal-port").value = "502";
|
||||
document.getElementById("modal-addr").value = "1";
|
||||
document.getElementById("modal-ha-prefix").value = "";
|
||||
document.getElementById("modal-prefix").value = "growatt/wechselrichter" + (invertersList.length + 1);
|
||||
document.getElementById("modal-interval").value = "30";
|
||||
document.getElementById("ems-min-pv").value = "1400";
|
||||
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);
|
||||
onModelChange(modelSel.value);
|
||||
modal.classList.add("open");
|
||||
}
|
||||
|
||||
@@ -849,11 +1133,15 @@ async function saveInverter() {
|
||||
mqtt_topic_prefix: document.getElementById("modal-prefix").value.trim(),
|
||||
update_interval: parseInt(document.getElementById("modal-interval").value),
|
||||
};
|
||||
if (model === "GROWATT_WIFI_X") {
|
||||
inv.ha_entity_prefix = document.getElementById("modal-ha-prefix").value.trim();
|
||||
}
|
||||
if (model === "KATHREIN_WALLBOX") {
|
||||
inv.ems_min_pv = parseInt(document.getElementById("ems-min-pv").value);
|
||||
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 +1182,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 +1210,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 +1226,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,19 +1296,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">
|
||||
<input type="number" placeholder="Min. Laufzeit (min)" title="Mindest-Laufzeit in Minuten" style="width:110px" value="${dev.min_on_minutes||0}" min="0" step="5" data-field="min_on_minutes">
|
||||
<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>
|
||||
<button class="btn btn-secondary" style="padding:3px 8px;font-size:12px;flex-shrink:0" onclick="removeSurplusDevice(this)">✕</button>
|
||||
<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>`;
|
||||
}
|
||||
@@ -1044,6 +1364,7 @@ function _collectSurplusDevices() {
|
||||
threshold_w: parseFloat(row.querySelector('[data-field=threshold_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,
|
||||
}));
|
||||
}
|
||||
@@ -1086,7 +1407,7 @@ function renderSurplusStatus(surplusData) {
|
||||
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>`;
|
||||
@@ -1106,6 +1427,9 @@ function renderSurplusStatus(surplusData) {
|
||||
</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 () => {
|
||||
|
||||
@@ -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)
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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"
|
||||
@@ -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 & 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 & 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">✓ ${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,'&').replace(/</g,'<').replace(/>/g,'>');
|
||||
}
|
||||
|
||||
// ── 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>
|
||||
Reference in New Issue
Block a user