Vogelalbum 1.0.0

Eine Kamera am Vogelhaus, ein Rechner der erkennt wer da war — und eine
Sammlung, die sich von allein fuellt.

Zwei neuronale Netze: eines findet den Vogel im Bild, eines bestimmt die
Art. 148 heimische Arten, auf Deutschland eingeschraenkt und neu normiert —
ohne diese Einschraenkung wird aus einer Kohlmeise eine nordamerikanische
Schwesterart. Daraus entsteht ein Sammelalbum: nummerierte Seiten,
gefundene Arten in Farbe, fehlende als Schatten, dazu Erstfang, Punkte,
Stufen und 15 Abzeichen.

Laeuft vollstaendig auf eigener Hardware. Kein Cloud-Dienst, kein Konto,
keine Telemetrie; die Bilder verlassen das eigene Netz nicht.

Zwei getrennt deploybare Teile: die Sammlung als Django-Anwendung, die
Erkennung als eigener Dienst. Der Erkennungs-Dienst fragt selbst an und
braucht deshalb weder eine erreichbare Adresse noch einen offenen Port —
er laeuft hinter einem gewoehnlichen Router.

Dabei: Firmware fuer Raspberry Pi und ESP32-S3, Anbindung an Home
Assistant ueber MQTT, Oberflaeche auf Deutsch und Englisch, Einstellungen
je Kamera aus dem Browser, und ein Weg fuer Fotos von unterwegs.

AGPL-3.0.
This commit is contained in:
christian committed 2026-10-14 18:55:23 +00:00
commit cc9e0786c2
189 files changed
+29792

No files matched your search

+7
View File
@@ -0,0 +1,7 @@
idf_component_register(
SRCS "vogelkamera.c" "netz.c"
INCLUDE_DIRS ""
# app_update liefert esp_ota_ops.h -- ohne das kein Update ueber WLAN.
REQUIRES esp_wifi esp_http_server esp_netif esp_event nvs_flash
esp_timer esp_driver_tsens lwip app_update
)
+124
View File
@@ -0,0 +1,124 @@
menu "Vogelkamera"
# ⚠ Hier standen bis zum 23.09.2026 WLAN-Name und Passwort. Sie sind
# bewusst entfernt: Zugangsdaten, die beim Bauen eingebacken werden,
# zwingen jeden Nutzer dazu, die Firmware selbst zu uebersetzen. Fuer
# ein veroeffentlichtes Repo ist das untauglich.
#
# Stattdessen: Das Geraet macht beim ersten Start ein eigenes WLAN auf
# und fragt im Browser danach. Siehe netz.c, uebernommen aus Hey Fred.
config VK_PORT
int "HTTP-Port"
default 8080
help
Gleicher Port wie am Pi. Der Detector erwartet
http://<kamera>:8080/stream.
choice VK_BOARD
prompt "Pinbelegung der Kamera"
default VK_BOARD_S3_WROOM
help
Die Pins haengen am Board, nicht am Sensor. Stimmt die Belegung
nicht, scheitert esp_camera_init mit 0x105 (ESP_ERR_NOT_FOUND) —
dann ist es die falsche Wahl hier, nicht die Kamera.
config VK_BOARD_S3_WROOM
bool "ESP32-S3-WROOM CAM (Freenove, Heemol und Klone)"
config VK_BOARD_XIAO_S3
bool "Seeed XIAO ESP32S3 Sense"
config VK_BOARD_AI_THINKER
bool "AI-Thinker ESP32-CAM (klassisch, ESP32 statt S3)"
endchoice
config VK_FRAMESIZE
int "Bildgroesse"
default 13
range 0 21
help
⚠ Die Zahlen sind Positionen in framesize_t aus dem
Kameratreiber -- und die haben sich schon einmal verschoben.
Am 23.09.2026 stand hier "9 = XGA 1024x768"; tatsaechlich war
9 laengst HVGA 480x320. Die Kamera lief daraufhin monatelang
auf einem Sechstel der Flaeche, ohne dass etwas gemeldet haette.
Nach einem Treiber-Update deshalb gegen sensor.h pruefen:
managed_components/espressif__esp32-camera/driver/include/sensor.h
Stand des Treibers vom 23.09.2026:
11 = SVGA 800x600
12 = XGA 1024x768
13 = HD 1280x720 (Vorgabe, wie der Pi am Futterhaus)
14 = SXGA 1280x1024
15 = UXGA 1600x1200
Mehr ist nicht besser: ueber WLAN ist die Bildrate
bandbreitenbegrenzt, nicht rechenbegrenzt. Aber zu klein
findet keine Voegel -- unter 5-8 % Bildflaeche erkennt das
Modell sie nicht mehr.
config VK_JPEG_QUALITAET
int "JPEG-Qualitaet (10 = gut, 63 = grob)"
default 12
range 4 63
help
Kleinere Zahl heisst besseres Bild und groessere Dateien. Bei
knapper Funkstrecke bringt eine hoehere Zahl mehr Bilder pro
Sekunde — und mehr Bilder finden mehr Voegel als schoenere.
config VK_HFLIP
bool "Bild horizontal spiegeln"
default n
config VK_VFLIP
bool "Bild vertikal spiegeln (Kamera haengt ueber Kopf)"
default n
config VK_MAX_FPS
int "Bilder je Sekunde hoechstens senden (0 = ohne Bremse)"
range 0 30
default 5
help
Ohne Bremse sendet die Kamera so schnell die Funkstrecke
hergibt -- gemessen 16 Bilder/s. Das heizt den Chip und
saettigt bei schwachem Empfang die Strecke.
Gebraucht wird es nicht: Ein Vogelbesuch dauert Sekunden bis
Minuten, und der Detector bestimmt nur jedes sechste Bild.
Fuenf sind reichlich.
Gemessen am 25.09.2026 an der Traenke, in der Sonne:
17 B/s -> Plateau 78,8 °C, Empfang -69 dBm
5 B/s -> Plateau 76,7 °C, Empfang -57 dBm
Zwei Grad kuehler, und zwoelf dB mehr Reserve auf der
Funkstrecke. Letzteres war nicht beabsichtigt und ist der
groessere Gewinn.
config VK_SAETTIGUNG
int "Farbsaettigung (-2 bis 2)"
range -2 2
default 2
help
Der Sensor liefert von Haus aus ein flaues Bild. Neben der
Pi-Kamera am Futterhaus faellt das sofort auf. Farbe ist hier
nicht nur Geschmack: Die Arterkennung arbeitet damit.
config VK_KONTRAST
int "Kontrast (-2 bis 2)"
range -2 2
default 1
help
Nicht ans Maximum. Zu viel Kontrast frisst die Zeichnung im
dunklen Gefieder — und genau die braucht der Erkenner.
config VK_HELLIGKEIT
int "Helligkeit (-2 bis 2)"
range -2 2
default 0
help
Nur anfassen, wenn das Bild durchweg zu dunkel oder zu hell
ist. Gegen Gegenlicht hilft eher das Ausrichten.
endmenu
+4
View File
@@ -0,0 +1,4 @@
dependencies:
espressif/esp32-camera: "^2.0.11"
idf:
version: ">=5.1"
+453
View File
@@ -0,0 +1,453 @@
#include <string.h>
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "freertos/event_groups.h"
#include "esp_log.h"
#include "esp_wifi.h"
#include "esp_event.h"
#include "esp_netif.h"
#include "nvs_flash.h"
#include "nvs.h"
#include "esp_random.h"
#include "esp_sntp.h"
#include <time.h>
#include <sys/time.h>
#include "netz.h"
static const char *TAG = "netz";
// Der Name des Einrichtungsnetzes steht erst fest, wenn die MAC bekannt
// ist. Einmal gebaut, danach nur noch gelesen.
static char setup_ssid[33] = {0};
const char *netz_setup_ssid(void)
{
if (setup_ssid[0] == '\0') {
uint8_t mac[6] = {0};
esp_wifi_get_mac(WIFI_IF_AP, mac);
snprintf(setup_ssid, sizeof(setup_ssid), "%s-%02X%02X",
NETZ_SETUP_PRAEFIX, mac[4], mac[5]);
}
return setup_ssid;
}
#define NVS_BEREICH "vogelkamera"
#define NVS_SSID "ssid"
#define NVS_PASS "pass"
#define NVS_OTA "ota"
// Wuerfelt beim ersten Aufruf ein Geheimnis und merkt es sich dauerhaft.
// esp_fill_random speist sich aus dem Hardware-Zufall des Chips -- nicht
// aus rand(), das auf jedem Geraet dieselbe Folge liefern wuerde.
const char *netz_ota_token(void)
{
static char token[49] = {0};
if (token[0] != '\0') return token;
nvs_handle_t h;
size_t laenge = sizeof(token);
if (nvs_open(NVS_BEREICH, NVS_READONLY, &h) == ESP_OK) {
esp_err_t r = nvs_get_str(h, NVS_OTA, token, &laenge);
nvs_close(h);
if (r == ESP_OK && token[0] != '\0') return token;
}
uint8_t roh[24];
esp_fill_random(roh, sizeof(roh));
for (size_t i = 0; i < sizeof(roh); i++) {
snprintf(token + i * 2, 3, "%02x", roh[i]);
}
if (nvs_open(NVS_BEREICH, NVS_READWRITE, &h) == ESP_OK) {
nvs_set_str(h, NVS_OTA, token);
nvs_commit(h);
nvs_close(h);
ESP_LOGI(TAG, "Update-Geheimnis erzeugt und gespeichert");
}
return token;
}
// Wie lange Fred beim Start um die Verbindung kämpft, bevor er die
// Einrichtung öffnet.
//
// Der erste Entwurf gab nach acht Anläufen auf — rund zwanzig Sekunden. Das
// war falsch gedacht: Ein Router, der nach einem Stromausfall langsamer
// hochkommt als Fred, hätte gereicht, damit ein Kindergerät im Regal
// plötzlich einen Hotspot aufmacht und niemand weiß warum. Drei Minuten
// überstehen jeden Neustart eines Zugangspunkts, und wer wirklich ein
// falsches Passwort eingetippt hat, wartet sie einmalig ab.
#define START_GEDULD_MS (3 * 60 * 1000)
// ⚠ Beim ALLERERSTEN Start, wenn noch nie Zugangsdaten da waren, gilt die
// lange Geduld nicht -- da wartet ein Mensch davor. Drei Minuten
// Schwarzsehen, bevor ueberhaupt ein Netz auftaucht, kosten Nerven und
// erzeugen den Eindruck, das Geraet sei kaputt. Am 23.09.2026 genau so
// erlebt.
#define ERSTSTART_GEDULD_MS (30 * 1000)
// Im laufenden Betrieb wird gar nicht mehr aufgegeben: Ein Aussetzer ist
// kein Grund, die gespeicherten Zugangsdaten in Frage zu stellen.
static EventGroupHandle_t s_ereignisse;
#define VERBUNDEN BIT0
static char s_ip[16];
static esp_netif_t *s_sta_netif;
static esp_netif_t *s_ap_netif;
static int s_trennungen;
static int s_letzter_grund;
static bool s_verbunden;
// Solange true, wird bei jedem Abriss sofort neu verbunden — auch endlos.
static bool s_dranbleiben;
// ------------------------------------------------------------------- NVS
const char *netz_gespeicherte_ssid(void)
{
static char ssid[NETZ_SSID_MAX + 1];
ssid[0] = '\0';
nvs_handle_t h;
if (nvs_open(NVS_BEREICH, NVS_READONLY, &h) != ESP_OK) return ssid;
size_t laenge = sizeof(ssid);
if (nvs_get_str(h, NVS_SSID, ssid, &laenge) != ESP_OK) ssid[0] = '\0';
nvs_close(h);
return ssid;
}
bool netz_zugang_vorhanden(void)
{
nvs_handle_t h;
if (nvs_open(NVS_BEREICH, NVS_READONLY, &h) != ESP_OK) return false;
size_t laenge = 0;
esp_err_t err = nvs_get_str(h, NVS_SSID, NULL, &laenge);
nvs_close(h);
return err == ESP_OK && laenge > 1;
}
static bool zugang_lesen(char *ssid, char *passwort)
{
nvs_handle_t h;
if (nvs_open(NVS_BEREICH, NVS_READONLY, &h) != ESP_OK) return false;
size_t l_ssid = NETZ_SSID_MAX + 1;
size_t l_pass = NETZ_PASS_MAX + 1;
esp_err_t a = nvs_get_str(h, NVS_SSID, ssid, &l_ssid);
esp_err_t b = nvs_get_str(h, NVS_PASS, passwort, &l_pass);
nvs_close(h);
// Ein offenes Netz hat kein Passwort — das ist kein Fehler.
if (b != ESP_OK) passwort[0] = '\0';
return a == ESP_OK;
}
bool netz_zugang_speichern(const char *ssid, const char *passwort,
esp_err_t *fehler)
{
// ⚠ Hier stand ESP_ERROR_CHECK auf jedem Aufruf. Das beendet bei einem
// Fehler das ganze Geraet -- mitten in der Bearbeitung einer
// Web-Anfrage. Ergebnis: Der Browser wartet ewig auf eine Antwort,
// die nie kommt, nichts ist gespeichert, und der Nutzer sieht nur,
// dass "es nicht geht". Genau so stand jemand am 23.09.2026
// einen Abend lang davor.
//
// Ein Fehler beim Speichern gehoert gemeldet, nicht verschwiegen.
esp_err_t r;
nvs_handle_t h;
r = nvs_open(NVS_BEREICH, NVS_READWRITE, &h);
if (r != ESP_OK) { if (fehler) *fehler = r; return false; }
r = nvs_set_str(h, NVS_SSID, ssid);
if (r == ESP_OK) r = nvs_set_str(h, NVS_PASS, passwort ? passwort : "");
if (r == ESP_OK) r = nvs_commit(h);
nvs_close(h);
if (r != ESP_OK) {
ESP_LOGE(TAG, "Speichern fehlgeschlagen: %s", esp_err_to_name(r));
if (fehler) *fehler = r;
return false;
}
// Zurueckgelesen statt geglaubt: Erst wenn es wirklich drinsteht,
// duerfen wir neu starten.
if (!netz_zugang_vorhanden()) {
ESP_LOGE(TAG, "Nach dem Speichern nichts gefunden");
if (fehler) *fehler = ESP_ERR_NVS_NOT_FOUND;
return false;
}
ESP_LOGI(TAG, "Zugangsdaten für '%s' gespeichert", ssid);
if (fehler) *fehler = ESP_OK;
return true;
}
void netz_zugang_loeschen(void)
{
nvs_handle_t h;
if (nvs_open(NVS_BEREICH, NVS_READWRITE, &h) != ESP_OK) return;
nvs_erase_key(h, NVS_SSID);
nvs_erase_key(h, NVS_PASS);
nvs_commit(h);
nvs_close(h);
ESP_LOGW(TAG, "Zugangsdaten gelöscht");
}
// ---------------------------------------------------------------- Ereignisse
static void ereignis(void *arg, esp_event_base_t basis, int32_t id, void *daten)
{
if (basis == WIFI_EVENT && id == WIFI_EVENT_STA_START) {
esp_wifi_connect();
} else if (basis == WIFI_EVENT && id == WIFI_EVENT_STA_DISCONNECTED) {
wifi_event_sta_disconnected_t *e = (wifi_event_sta_disconnected_t *)daten;
s_ip[0] = '\0';
s_verbunden = false;
s_trennungen++;
s_letzter_grund = e->reason;
// Der Grund ist die halbe Miete: 15 und 204 heißen falsches
// Passwort, 201 „Netz nicht gefunden", 2/3/4 zeigen auf
// Verschlüsselung und Management-Frames. Ohne diese Zahl rät man.
ESP_LOGW(TAG, "getrennt (Grund %d), Abriss Nr. %d", e->reason, s_trennungen);
// Im Ereignis nicht schlafen — der Handler blockiert sonst die
// gesamte Ereignisschleife. Ein Rückfall kommt ohnehin gleich wieder.
if (s_dranbleiben) esp_wifi_connect();
} else if (basis == IP_EVENT && id == IP_EVENT_STA_GOT_IP) {
ip_event_got_ip_t *e = (ip_event_got_ip_t *)daten;
snprintf(s_ip, sizeof(s_ip), IPSTR, IP2STR(&e->ip_info.ip));
ESP_LOGI(TAG, "verbunden, IP %s", s_ip);
s_verbunden = true;
xEventGroupSetBits(s_ereignisse, VERBUNDEN);
} else if (basis == WIFI_EVENT && id == WIFI_EVENT_AP_STACONNECTED) {
ESP_LOGI(TAG, "jemand hat sich mit %s verbunden", netz_setup_ssid());
}
}
// ------------------------------------------------------------------- Start
void netz_init(void)
{
s_ereignisse = xEventGroupCreate();
s_ip[0] = '\0';
ESP_ERROR_CHECK(esp_netif_init());
ESP_ERROR_CHECK(esp_event_loop_create_default());
// Beide Schnittstellen anlegen, aber nur eine wird eingeschaltet.
s_sta_netif = esp_netif_create_default_wifi_sta();
s_ap_netif = esp_netif_create_default_wifi_ap();
wifi_init_config_t cfg = WIFI_INIT_CONFIG_DEFAULT();
ESP_ERROR_CHECK(esp_wifi_init(&cfg));
ESP_ERROR_CHECK(esp_event_handler_instance_register(
WIFI_EVENT, ESP_EVENT_ANY_ID, &ereignis, NULL, NULL));
ESP_ERROR_CHECK(esp_event_handler_instance_register(
IP_EVENT, IP_EVENT_STA_GOT_IP, &ereignis, NULL, NULL));
// Die Zugangsdaten liegen bei uns im NVS, nicht in der WLAN-Ablage des
// Treibers. Sonst hätten wir zwei Wahrheiten, die auseinanderlaufen.
ESP_ERROR_CHECK(esp_wifi_set_storage(WIFI_STORAGE_RAM));
}
bool netz_verbinden(void)
{
char ssid[NETZ_SSID_MAX + 1] = {0};
char passwort[NETZ_PASS_MAX + 1] = {0};
if (!zugang_lesen(ssid, passwort)) {
ESP_LOGW(TAG, "keine Zugangsdaten im NVS");
return false;
}
wifi_config_t wcfg = {0};
strlcpy((char *)wcfg.sta.ssid, ssid, sizeof(wcfg.sta.ssid));
strlcpy((char *)wcfg.sta.password, passwort, sizeof(wcfg.sta.password));
// Geschützte Management-Frames. Durch die Null-Initialisierung stünde
// `capable` sonst auf false — und ein Netz, das PMF verlangt, weist
// Fred dann ab, obwohl das Passwort stimmt. Genau das ist bei
// IoT-Netzen von UniFi und FritzBox die Voreinstellung.
wcfg.sta.pmf_cfg.capable = true;
wcfg.sta.pmf_cfg.required = false;
// Erlaubt auch WPA3-Netze (und WPA2/WPA3 gemischt). Ohne das scheitert
// die Anmeldung an neueren Zugangspunkten ohne erkennbaren Grund.
wcfg.sta.sae_pwe_h2e = WPA3_SAE_PWE_BOTH;
// Kein Mindestniveau erzwingen: Was der Zugangspunkt anbietet, nehmen wir.
wcfg.sta.threshold.authmode = WIFI_AUTH_OPEN;
s_dranbleiben = true;
ESP_ERROR_CHECK(esp_wifi_set_mode(WIFI_MODE_STA));
ESP_ERROR_CHECK(esp_wifi_set_config(WIFI_IF_STA, &wcfg));
ESP_ERROR_CHECK(esp_wifi_start());
ESP_LOGI(TAG, "verbinde mit '%s' (bis zu %d s Geduld)", ssid, START_GEDULD_MS / 1000);
EventBits_t bits = xEventGroupWaitBits(
s_ereignisse, VERBUNDEN, pdFALSE, pdFALSE, pdMS_TO_TICKS(START_GEDULD_MS));
if (bits & VERBUNDEN) return true;
// Erst jetzt aufgeben — und das Nachfassen einstellen, damit der
// Einrichtungsmodus nicht gegen einen weiterlaufenden Verbindungsversuch
// arbeitet.
s_dranbleiben = false;
ESP_LOGE(TAG, "'%s' in %d s nicht erreicht (zuletzt Grund %d, %d Abrisse)"
" — Einrichtung wird geöffnet",
ssid, START_GEDULD_MS / 1000, s_letzter_grund, s_trennungen);
esp_wifi_stop();
return false;
}
void netz_setup_ap_start(void)
{
const char *name = netz_setup_ssid();
wifi_config_t wcfg = {
.ap = {
.channel = 1,
.max_connection = 4,
// Offen mit Absicht: Wer hier steht, soll ohne Zettel und ohne
// App hineinkommen. Zu holen gibt es nichts — das Formular
// nimmt nur entgegen, es gibt nichts preis.
.authmode = WIFI_AUTH_OPEN,
},
};
// APSTA statt reinem AP: Nur so lässt sich im Einrichtungsmodus nach
// Netzen suchen, und genau das braucht das Formular für seine Liste.
strlcpy((char *)wcfg.ap.ssid, name, sizeof(wcfg.ap.ssid));
wcfg.ap.ssid_len = strlen(name);
ESP_ERROR_CHECK(esp_wifi_set_mode(WIFI_MODE_APSTA));
ESP_ERROR_CHECK(esp_wifi_set_config(WIFI_IF_AP, &wcfg));
ESP_ERROR_CHECK(esp_wifi_start());
ESP_LOGI(TAG, "Einrichtung offen: '%s' → http://192.168.4.1/", name);
}
#define FUNDE_VORRAT 20
static netz_fund_t s_funde[FUNDE_VORRAT];
static int s_funde_anzahl = -1; // -1 = noch nie gesucht
void netz_funde_auffrischen(void)
{
s_funde_anzahl = netz_suchen(s_funde, FUNDE_VORRAT);
ESP_LOGI(TAG, "%d Netze gefunden (einmalig, vor dem ersten Besucher)",
s_funde_anzahl);
}
int netz_funde_holen(netz_fund_t *ziel, int max)
{
if (s_funde_anzahl < 0) netz_funde_auffrischen();
int n = s_funde_anzahl < max ? s_funde_anzahl : max;
for (int i = 0; i < n; i++) ziel[i] = s_funde[i];
return n;
}
int netz_suchen(netz_fund_t *ziel, int max)
{
wifi_scan_config_t cfg = { .show_hidden = false };
if (esp_wifi_scan_start(&cfg, true) != ESP_OK) {
ESP_LOGW(TAG, "Suche fehlgeschlagen");
return 0;
}
uint16_t anzahl = 0;
esp_wifi_scan_get_ap_num(&anzahl);
if (anzahl == 0) return 0;
wifi_ap_record_t *roh = calloc(anzahl, sizeof(wifi_ap_record_t));
if (!roh) return 0;
esp_wifi_scan_get_ap_records(&anzahl, roh);
// Die Liste kommt nach Feldstärke sortiert. Deshalb genügt es, jede
// SSID beim ersten Auftreten zu übernehmen — Mesh-Netze tauchen sonst
// fünfmal auf und das Formular wird unlesbar.
int n = 0;
for (int i = 0; i < anzahl && n < max; i++) {
const char *ssid = (const char *)roh[i].ssid;
if (ssid[0] == '\0') continue;
bool schon_da = false;
for (int k = 0; k < n; k++) {
if (strcmp(ziel[k].ssid, ssid) == 0) { schon_da = true; break; }
}
if (schon_da) continue;
strlcpy(ziel[n].ssid, ssid, sizeof(ziel[n].ssid));
ziel[n].rssi = roh[i].rssi;
ziel[n].verschluesselt = roh[i].authmode != WIFI_AUTH_OPEN;
n++;
}
free(roh);
ESP_LOGI(TAG, "%d Netze gefunden", n);
return n;
}
void zeit_holen(void)
{
if (esp_sntp_enabled()) return;
esp_sntp_setoperatingmode(ESP_SNTP_OPMODE_POLL);
esp_sntp_setservername(0, "pool.ntp.org");
esp_sntp_init();
// Mitteleuropa mit Sommerzeit. Die Regel steht in der Zeichenkette,
// deshalb stimmt die Umstellung auch ohne Netz — sobald die Uhr
// einmal gestellt ist.
setenv("TZ", "CET-1CEST,M3.5.0/2,M10.5.0/3", 1);
tzset();
ESP_LOGI(TAG, "Uhrzeit wird geholt");
}
bool zeit_steht(void)
{
time_t jetzt = time(NULL);
struct tm z;
localtime_r(&jetzt, &z);
// Vor der ersten Antwort steht die Uhr auf 1970.
return z.tm_year > 120;
}
int netz_feldstaerke(void)
{
// Wie gut Fred den Zugangspunkt hoert, in dBm. Ueber -60 ist gut,
// unter -75 wird es unzuverlaessig — und Unzuverlaessigkeit ist
// genau das, was eine stockende Tonuebertragung erklaeren wuerde.
wifi_ap_record_t ap;
if (esp_wifi_sta_get_ap_info(&ap) != ESP_OK) return 0;
return ap.rssi;
}
const char *netz_ip(void)
{
return s_ip;
}
int netz_trennungen(void)
{
return s_trennungen;
}
int netz_letzter_grund(void)
{
return s_letzter_grund;
}
bool netz_verbunden(void)
{
return s_verbunden;
}
+116
View File
@@ -0,0 +1,116 @@
// WLAN: Zugangsdaten im NVS, Verbindung als Station, Einrichtung als
// Zugangspunkt. Wer die Daten hat, verbindet sich; wer keine hat, macht
// sein eigenes Netz auf.
//
// Uebernommen aus Hey Fred (firmware/main/netz.c) am 23.09.2026. Dort ist
// es erprobt; hier geaendert wurden der NVS-Bereich und der Name des
// Einrichtungsnetzes.
#pragma once
#include <stdbool.h>
#include <stdint.h>
#include "esp_err.h"
#define NETZ_SSID_MAX 32
#define NETZ_PASS_MAX 64
// Name des Zugangspunkts zur Einrichtung. ⚠ Anders als bei Fred NICHT
// fest: Die letzten beiden Bytes der MAC haengen hinten dran, also etwa
// "Vogelkamera-A4F2". Zwei Kameras im selben Garten -- Futterhaus und
// Traenke -- wuerden sich sonst denselben Namen teilen, und man wuesste
// beim Einrichten nicht, welche man gerade vor sich hat.
#define NETZ_SETUP_PRAEFIX "Vogelkamera"
// Der tatsaechliche Name, erst zur Laufzeit bekannt.
const char *netz_setup_ssid(void);
// Das Geheimnis fuer Updates ueber WLAN.
//
// ⚠ Bewusst NICHT einkompiliert (so macht es Hey Fred in secrets.h). Fuer
// ein Geraet, das als Bausatz weitergegeben wird, waere das derselbe
// Fehler wie eingebackene WLAN-Daten: Jeder muesste die Firmware selbst
// uebersetzen, und alle Geraete haetten dasselbe Geheimnis.
//
// Stattdessen: Beim ersten Start wuerfelt sich jedes Geraet sein eigenes
// und legt es im NVS ab. Zu sehen bekommt man es im Einrichtungsnetz --
// wer dort steht, steht ohnehin vor dem Geraet.
const char *netz_ota_token(void);
typedef struct {
char ssid[NETZ_SSID_MAX + 1];
int8_t rssi;
bool verschluesselt;
} netz_fund_t;
// Einmal beim Start aufrufen, vor allem anderen.
void netz_init(void);
// Liegen Zugangsdaten im NVS?
bool netz_zugang_vorhanden(void);
// Der gespeicherte Netzname, oder "" wenn keiner da ist. Fuer die Anzeige
// auf der Einrichtungsseite: Erst damit sieht man, ob das Speichern
// ueberhaupt ankommt -- ohne Kabel, ohne Raten.
const char *netz_gespeicherte_ssid(void);
// Gibt false zurueck, wenn das Schreiben scheitert -- der Grund steht dann
// in *fehler. ⚠ Frueher brach das Geraet bei einem Fehler einfach ab
// (ESP_ERROR_CHECK): Es startete mitten im Absenden neu, der Browser blieb
// haengen, nichts war gespeichert, und niemand erfuhr warum.
bool netz_zugang_speichern(const char *ssid, const char *passwort,
esp_err_t *fehler);
void netz_zugang_loeschen(void);
// Verbindet mit den gespeicherten Daten. Blockiert bis zum Erfolg oder bis
// die Versuche aufgebraucht sind. Gibt false zurück, wenn es nicht klappt —
// dann gehört Fred in den Einrichtungsmodus.
bool netz_verbinden(void);
// Öffnet den Zugangspunkt für die Einrichtung. Danach ist Fred unter
// 192.168.4.1 erreichbar.
void netz_setup_ap_start(void);
// Sucht Netze in der Umgebung. Schreibt bis `max` Funde nach `ziel` und gibt
// die Anzahl zurück. Doppelte SSIDs werden übersprungen, der stärkste
// Fund gewinnt.
int netz_suchen(netz_fund_t *ziel, int max);
// Sucht EINMAL und merkt sich das Ergebnis.
//
// ⚠ Der Grund ist unangenehm und kostet sonst Stunden: Eine WLAN-Suche
// wechselt den Funkkanal. Laeuft gleichzeitig der eigene Zugangspunkt,
// fliegen die verbundenen Geraete dabei raus. Sucht man also, waehrend
// jemand das Formular betrachtet, verliert genau dieser Jemand die
// Verbindung -- und sein Absenden kommt nie an. Am 23.09.2026 genau so
// passiert, an Handy wie an Mac.
//
// Deshalb: einmal suchen, bevor der erste Besucher da ist, und danach
// nur noch die Liste ausliefern.
int netz_funde_holen(netz_fund_t *ziel, int max);
void netz_funde_auffrischen(void);
// IP als Zeichenkette, leer wenn nicht verbunden.
const char *netz_ip(void);
// Wie oft die Verbindung seit dem Start abgerissen ist, und warum zuletzt.
// Der Grund ist der Code des WLAN-Treibers: 15 und 204 stehen für ein
// falsches Passwort, 201 für „Netz nicht gefunden", 2 bis 4 zeigen auf
// Verschlüsselung und Management-Frames.
int netz_trennungen(void);
int netz_letzter_grund(void);
// Läuft die Verbindung gerade?
bool netz_verbunden(void);
// Holt die Uhrzeit per SNTP und stellt sie auf deutsche Zeit. Blockiert
// nicht — das Ergebnis trudelt im Hintergrund ein.
void zeit_holen(void);
// Steht die Uhr schon auf einer plausiblen Zeit?
bool zeit_steht(void);
// Empfangsstaerke zum Zugangspunkt in dBm (negativ; naeher an null ist
// besser). 0 heisst: nicht ermittelbar.
int netz_feldstaerke(void);
File diff suppressed because it is too large. Load diff