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:
commit
c11fe9ab04
189 files changed
+29806
No files matched your search
@@ -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;
|
||||
}
|
||||
Reference in new issue
Block a user