// STACJA POGODOWA - ESP32-C6 (VELXIO)
// Wzorowane na sprawdzonej, dzialajacej wersji z Wokwi:
// - globalny HTTPClient http; (nie tworzony na nowo w kazdym wywolaniu)
// - odczyt strumieniowy zamiast http.getString() (mniej alokacji na stercie)
// - wysylka HTTP na osobnym zadaniu (sendTask)
// - heap_caps_check_integrity_all() do wczesnego wykrywania korupcji sterty
// - WiFi.begin(ssid, pass, wifiChannel) NIE POMINIETE
//
// UWAGA: ESP32-C6 ma JEDEN rdzen - sendTask dzieli rdzen 0 z loop()
// przez preemptywny scheduler. Cale I2C w jednym zadaniu = jeden punkt
// dostepu do magistrali, brak przeplatania transakcji miedzy kontekstami.
//
// Kazdy czujnik probkowany niezaleznie; brak jednego NIE blokuje reszty.
// NAN = "brak wiarygodnej probki" -> w URL poleci "nan" -> floatval() w
// PHP zamieni na 0.0 (bezpieczne dla json_encode).
//
// Podzial rol miedzy czujnikami (bez duplikacji funkcji):
// AS5600 (0x36, I2C) - kat obrotu choragiewki WZGLEDEM OBUDOWY stacji
// BMM150 (0x13, I2C) - azymut magnetyczny SAMEJ STACJI (kalibracja/Polnoc)
// -> suma AS5600 + BMM150 = prawdziwy kierunek wiatru
// BME280 (0x76, I2C) - temperatura, wilgotnosc, cisnienie (JEDYNE zrodlo)
// TSL2561 (0x29, I2C) - naslonecznienie (Lux)
// Water Level (0x77+0x78, I2C, 2x ATtiny1616) - poziom deszczowki w zbiorniku
// GUVA-S12D (analog, pin 4) - indeks UV
// Anemometr (open-collector, pin 5) - predkosc wiatru (impulsy)
// D7S (0x55, I2C) - wibracje/sejsmika: intensity (SI) + PGA
#include <WiFi.h>
#include <Wire.h>
#include <math.h> // fabs()
#include <HTTPClient.h>
#include <Adafruit_Sensor.h>
#include <Adafruit_BME280.h>
#include <Adafruit_TSL2561_U.h>
#include "bmm150.h"
#include "bmm150_defs.h"
const char* ssid = "Wokwi-GUEST";
const char* pass = "";
const int wifiChannel = 6; // wymuszony kanal - pomija skanowanie sieci
HTTPClient http;
const char* serverURL = "http://kshyhoo.eu/weather/update.php";
// Prosty "sekret" chroniacy endpoint przed przypadkowym albo zlosliwym nadpisaniem danych przez kogos innego, kto zgadnie adres URL. Musi byc identyczny z wartoscia sprawdzana w update.php
const char* apiToken = "kshyhoo";
const unsigned long sendInterval = 5000; // co ile ms wysylac dane
// GLOBALNE ZMIENNE STANU (aktualizowane WYLACZNIE w sendTask)
float angleDeg = NAN; // kat choragiewki z AS5600 (0-360)
float stationHeadingDeg = NAN; // azymut stacji z BMM150 (0-360)
float windDeg = NAN; // prawdziwy kierunek wiatru (0-360)
float bmeTempC = NAN;
float bmePressureHpa = NAN;
float bmeHumidityPct = NAN;
float lightLux = NAN;
float uvIndex = NAN;
float waterLvl = NAN; // poziom wody [cm]
float d7sSi = NAN; // intensywnosc wibracji [SI]
float d7sPga = NAN; // przyspieszenie chwilowe PGA [m/s^2]
float windSpeedMps = 0; // predkosc wiatru z impulsow (liczona w loop)
#define SDA_PIN 23
#define SCL_PIN 22
// KIERUNEK WIATRU (AS5600)
#define WIND_I2C_ADDR 0x36
#define WIND_REG_RAW_ANGLE_HI 0x0C
uint16_t readAs5600RawAngle() {
Wire.beginTransmission(WIND_I2C_ADDR);
Wire.write(WIND_REG_RAW_ANGLE_HI);
if (Wire.endTransmission(false) != 0) {
Serial.println("AS5600: blad I2C przy ustawianiu wskaznika rejestru!");
return 0xFFFF;
}
Wire.requestFrom((int)WIND_I2C_ADDR, 2);
if (Wire.available() < 2) {
Serial.println("AS5600: za malo bajtow w odpowiedzi I2C!");
return 0xFFFF;
}
uint8_t hi = Wire.read();
uint8_t lo = Wire.read();
return ((uint16_t)(hi & 0x0F) << 8) | lo;
}
const char* angleToCompass(float degrees) {
static const char* names[] = { "N", "NE", "E", "SE", "S", "SW", "W", "NW" };
int sector = (int)((degrees + 22.5) / 45.0) % 8;
if (sector < 0) sector += 8;
return names[sector];
}
void updateWindDirection() {
uint16_t raw = readAs5600RawAngle();
if (raw == 0xFFFF) {
return; // blad I2C - zostaje poprzednia wartosc
}
angleDeg = raw * 360.0 / 4096.0;
}
// BMM150 - azymut SAMEJ STACJI wzgledem prawdziwej Polnocy
BMM150 bmm = BMM150();
bool bmmOk = false;
float readStationHeadingDeg() {
bmm.read_mag_data();
float x = bmm.raw_mag_data.raw_datax;
float y = bmm.raw_mag_data.raw_datay;
float heading = atan2(x, y);
if (heading < 0) heading += 2 * PI;
if (heading > 2 * PI) heading -= 2 * PI;
return heading * 180.0 / PI;
}
// Zwraca prawdziwy kierunek wiatru (0-360, 0=Polnoc)
// Zalozenie co do znaku sumy (a nie roznicy) trzeba zweryfikowac fizycznie na gotowym montazu
float windDirectionDeg(float vaneDeg, float stationHeadingDeg) {
float windDeg = vaneDeg + stationHeadingDeg;
while (windDeg >= 360.0f) windDeg -= 360.0f;
while (windDeg < 0.0f) windDeg += 360.0f;
return windDeg;
}
// PREDKOSC WIATRU (ANEMOMETR - A3144)
// Wyjscie OUT anemometru to open-collector (jak A3144) - INPUT_PULLUP zastepuje zewnetrzny rezystor podciagajacy
#define WIND_SPEED_PIN 5
// Musi byc zgodne z PULSES_PER_REV i RPM_PER_MPS w anemometer.chip.c - to ta sama kalibracja fizycznego wirnika, wiec obie strony (symulacja i firmware) musza sie zgadzac, inaczej przeliczenie na m/s bedzie zle
#define PULSES_PER_REV 4
#define RPM_PER_MPS 20.0
volatile uint32_t windPulseCount = 0;
unsigned long lastWindCalcMillis = 0;
const unsigned long windCalcIntervalMs = 1000; // okno pomiaru czestotliwosci impulsow
void IRAM_ATTR onWindPulse() {
windPulseCount++;
}
// UWAGA: funkcja NIE dotyka I2C (tylko licznik impulsow i millis), wiec jest bezpieczna do wywolywania z loop().
void updateWindSpeed() {
unsigned long now = millis();
unsigned long dt = now - lastWindCalcMillis;
if (dt < windCalcIntervalMs) {
return; // za wczesnie - czekamy na koniec okna pomiarowego
}
noInterrupts();
uint32_t pulses = windPulseCount;
windPulseCount = 0;
interrupts();
//Serial.printf("Anemometr: impulsy=%u, dt=%lu ms\n", pulses, dt);
float freqHz = pulses / (dt / 1000.0f);
float rpm = freqHz * 60.0f / PULSES_PER_REV;
windSpeedMps = rpm / RPM_PER_MPS;
lastWindCalcMillis = now;
}
// TEMPERATURA / CISNIENIE / WILGOTNOSC (BME280)
// Adres 0x76 - zgodny z naszym bme280.chip.c przy pinie SDO zwartym do masy (INPUT_PULLDOWN w chipie = 0x76 domyslnie, bez zewnetrznego podciagniecia SDO do VCC). Jesli podlaczysz SDO do VCC, zmienic na 0x77!
#define BME280_I2C_ADDR 0x76
Adafruit_BME280 bme;
bool bmeOk = false;
// Zwraca false, jesli odczyt sie nie powiodl (np. czujnik nieobecny) - w takim wypadku poprzednie wartosci zostaja, zeby nie wysylac smieci
bool updateBme() {
if (!bmeOk) {
return false;
}
float t = bme.readTemperature();
float p = bme.readPressure() / 100.0F; // Pa -> hPa
float h = bme.readHumidity();
if (isnan(t) || isnan(p) || isnan(h)) {
Serial.println("BME280: odczyt zwrocil NaN - pomijam probke");
return false;
}
bmeTempC = t;
bmePressureHpa = p;
bmeHumidityPct = h;
return true;
}
// TSL2561 - naslonecznienie (Lux)
// event.light == 65536 to sentinel przesycenia wg oficjalnej dokumentacji Adafruit - dlatego sprawdzamy zwracany bool z getEvent(), NIE event.light
bool updateLight() {
const uint8_t ADDR = 0x29;
// CONTROL: upewnij sie, ze czujnik jest wlaczony (idempotentne)
Wire.beginTransmission(ADDR);
Wire.write(0x80 | 0x00);
Wire.write(0x03);
if (Wire.endTransmission() != 0) return false;
auto readChannel = [&](uint8_t reg) -> uint16_t {
Wire.beginTransmission(ADDR);
Wire.write(0x80 | reg);
if (Wire.endTransmission() != 0) return 0;
if (Wire.requestFrom(ADDR, (uint8_t)2) != 2) return 0;
uint16_t lo = Wire.read();
uint16_t hi = Wire.read();
return (hi << 8) | lo;
};
uint16_t ch0 = readChannel(0x0C);
uint16_t ch1 = readChannel(0x0E);
float ratio = ch0 ? (float)ch1 / ch0 : 0;
lightLux = ratio <= 0.5f ? 0.0304f * ch0 - 0.062f * ch0 * pow(ratio, 1.4f) : 0;
return true;
}
// GUVA-S12D - indeks UV (czujnik analogowy, pin 4)
// SIG -> 12-bit ADC, 3.3 V: 0..4095
// Wzor wg oficjalnej dokumentacji Seeed:
// illumination [mW/m2] = 307 * Vsig[V]
// UV Index = illumination / 200
const int pinUv = 4;
const float darkOffset = 0.05; // V with no UV
const float voltsPerIndex = 0.1;
float readUvIndex() {
int raw = analogRead(pinUv);
float voltage = raw * (3.3f / 4095.0f);
float idx = (voltage - darkOffset) / voltsPerIndex;
if (idx < 0) idx = 0;
Serial.printf("UV raw=%d voltage=%.3f index=%.2f\n", raw, voltage, idx);
return idx;
}
// Water Level Sensor - 2x ATtiny1616, adresy 0x77 (8 sekcji) i 0x78 (12 sekcji)
// Algorytm 1:1 z oficjalnego przykladu Seeed
#define ATTINY1_HIGH_ADDR 0x78
#define ATTINY2_LOW_ADDR 0x77
#define WATER_THRESHOLD 100
bool readWaterLevel(float& level) {
int sections = 0;
if (Wire.requestFrom((int)ATTINY2_LOW_ADDR, 8) != 8) {
level = NAN;
return false;
}
while (Wire.available())
if (Wire.read() > WATER_THRESHOLD) sections++;
if (Wire.requestFrom((int)ATTINY1_HIGH_ADDR, 12) != 12) {
level = NAN;
return false;
}
while (Wire.available())
if (Wire.read() > WATER_THRESHOLD) sections++;
level = sections * 0.5f; // 20 sekcji * 0.5 cm = 10 cm
return true;
}
// D7S - czujnik wibracji/sejsmiczny (Grove, I2C 0x55)
// Rejestry i protokol 1:1 z oficjalnego przykladu Velxio - custom chip w tym
// symulatorze implementuje WLASNY, uproszczony protokol rejestrowy (nie pelne
// zachowanie prawdziwego krzemu Omron), dlatego mowimy z nim surowym I2C,
// bez zadnej biblioteki (RAK12027-D7S czekalby w nieskonczonosc na cos,
// czego ten chip nigdy nie wystawi).
#define D7S_I2C_ADDR 0x55
bool d7sRead8(uint16_t reg, uint8_t& value) {
Wire.beginTransmission(D7S_I2C_ADDR);
Wire.write(reg >> 8);
Wire.write(reg & 0xFF);
if (Wire.endTransmission(false) != 0) return false;
if (Wire.requestFrom((int)D7S_I2C_ADDR, 1) != 1) return false;
value = Wire.read();
return true;
}
bool d7sRead16(uint16_t reg, uint16_t& value) {
Wire.beginTransmission(D7S_I2C_ADDR);
Wire.write(reg >> 8);
Wire.write(reg & 0xFF);
if (Wire.endTransmission(false) != 0) return false;
if (Wire.requestFrom((int)D7S_I2C_ADDR, 2) != 2) return false;
uint16_t hi = Wire.read();
uint16_t lo = Wire.read();
value = (hi << 8) | lo;
return true;
}
bool updateD7S() {
uint8_t state, events;
if (!d7sRead8(0x1000, state) || !d7sRead8(0x1002, events)) {
d7sSi = NAN;
d7sPga = NAN;
return false;
}
if (state == 1) {
uint16_t si, pga;
if (!d7sRead16(0x2000, si) || !d7sRead16(0x2002, pga)) {
d7sSi = NAN;
d7sPga = NAN;
return false;
}
d7sSi = si / 1000.0f;
d7sPga = pga / 1000.0f;
} else {
d7sSi = NAN;
d7sPga = NAN;
}
if (events & 0x01)
Serial.println("D7S: SHUTOFF - odetnij gaz");
if (events & 0x02)
Serial.println("D7S: COLLAPSE - budynek niebezpieczny");
if (events) {
Wire.beginTransmission(D7S_I2C_ADDR);
Wire.write(0x10);
Wire.write(0x05);
Wire.write(0x04);
Wire.endTransmission();
}
return true;
}
// Wysylka danych do serwera (update.php) - sprawdzona wersja: globalny http, odczyt strumieniowy, pojedyncze & miedzy parametrami
void sendWeatherData() {
if (WiFi.status() != WL_CONNECTED) {
Serial.println("Brak polaczenia WiFi - dane nie zostaly wyslane");
return;
}
char url[448];
char siStr[8], pgaStr[8];
snprintf(siStr, sizeof(siStr), "%.2f", isnan(d7sSi) ? 0.0f : d7sSi);
snprintf(pgaStr, sizeof(pgaStr), "%.2f", isnan(d7sPga) ? 0.0f : d7sPga);
const char* direction = isnan(windDeg) ? "" : angleToCompass(windDeg);
snprintf(url, sizeof(url), "%s?angleDeg=%.1f&windSpeed=%.2f&tempC=%.1f&pressure=%.1f&humidity=%.1f&uv=%.2f&water=%.1f&lux=%.2f&si=%s&pga=%s&direction=%s&token=%s", serverURL, angleDeg, windSpeedMps, bmeTempC, bmePressureHpa, bmeHumidityPct, uvIndex, waterLvl, lightLux, siStr, pgaStr, direction, apiToken);
Serial.printf("Wysylanie, dlugosc URL: %u\n", (unsigned)strlen(url));
Serial.println(url);
if (!http.begin(url)) {
Serial.println("Blad: nie mozna uruchomic HTTP");
return;
}
http.setTimeout(5000);
int httpCode = http.GET();
Serial.printf("httpCode = %d\n", httpCode);
if (httpCode > 0) {
Serial.printf("Serwer odpowiedział: HTTP %d\n", httpCode);
WiFiClient* stream = http.getStreamPtr();
uint32_t discarded = 0;
uint8_t buf[64];
while (stream->available()) {
int n = stream->read(buf, sizeof(buf));
if (n <= 0) break;
discarded += n;
}
Serial.printf("Odebrano i odrzucono %u B odpowiedzi\n", discarded);
} else {
Serial.print("Błąd HTTP: ");
Serial.println(http.errorToString(httpCode));
}
http.end();
Serial.println("http.end() zakończone");
heap_caps_check_integrity_all(true);
}
// Zamienia kod wl_status_t z WiFi.status() na czytelny tekst po polsku - bez tego trudno zdiagnozowac, DLACZEGO polaczenie nie wychodzi
const char* wifiStatusToText(wl_status_t status) {
switch (status) {
case WL_IDLE_STATUS: return "IDLE (jeszcze nie zaczeto laczyc)";
case WL_NO_SSID_AVAIL: return "BRAK SSID (punkt dostepu niewidoczny)";
case WL_SCAN_COMPLETED: return "SKANOWANIE ZAKONCZONE";
case WL_CONNECTED: return "POLACZONO";
case WL_CONNECT_FAILED: return "BLAD POLACZENIA (zle haslo/uwierzytelnienie)";
case WL_CONNECTION_LOST: return "UTRACONO POLACZENIE";
case WL_DISCONNECTED: return "ROZLACZONO";
default: return "NIEZNANY";
}
}
// Uchwyt zadania wysylajacego
TaskHandle_t sendTaskHandle = NULL;
// Zadanie wysylajace - WYLACZNIE ono dotyka magistrali I2C
// (AS5600, BME280, TSL2561, BMM150, Water Level, D7S) oraz HTTPClient.
// Aktualizuje globalne zmienne stanu; loop() je tylko czyta do logu.
void sendTask(void* param) {
(void)param;
for (;;) {
if (WiFi.status() == WL_CONNECTED) {
// AS5600 - kat choragiewki
updateWindDirection();
// BMM150 - azymut SAMEJ STACJI wzgledem prawdziwej Polnocy
if (!isnan(angleDeg)) {
if (bmmOk) {
stationHeadingDeg = readStationHeadingDeg();
Serial.println("BMM150 OK");
}
windDeg = windDirectionDeg(angleDeg, isnan(stationHeadingDeg) ? 0 : stationHeadingDeg);
} else {
windDeg = NAN;
}
// BME280 - TEMPERATURA / CISNIENIE / WILGOTNOSC
if (updateBme()) {
Serial.println("BME280 OK");
}
// TSL2561 - naslonecznienie (Lux)
if (updateLight()) {
Serial.println("TSL2561 OK");
}
// GUVA-S12D - indeks UV
uvIndex = readUvIndex();
// Water Level Sensor
if (readWaterLevel(waterLvl)) {
Serial.println("RAIN OK");
} else {
Serial.println("RAIN: brak odczytu");
}
// D7S - wibracje/sejsmika
if (updateD7S()) {
Serial.println("D7S OK");
Serial.printf("DEBUG sendTask: si=%.2f pga=%.2f (adres: %p)\n", d7sSi, d7sPga, (void*)&d7sSi);
} else {
Serial.println("D7S: brak odczytu");
}
// Wysylka (czyta globalne zmienne)
Serial.printf("[] heap=%u\n", ESP.getFreeHeap());
Serial.printf("[] angle=%.1f station=%.1f wind=%.1f speed=%.2f\n", angleDeg, stationHeadingDeg, windDeg, windSpeedMps);
Serial.printf("[] T=%.1f P=%.1f H=%.1f lux=%.1f\n", bmeTempC, bmePressureHpa, bmeHumidityPct, lightLux);
Serial.printf("[] uv=%.2f water=%.1f si=%.2f pga=%.2f\n", uvIndex, waterLvl);
Serial.printf("[] si=%.2f pga=%.2f\n", d7sSi, d7sPga);
sendWeatherData();
}
vTaskDelay(pdMS_TO_TICKS(sendInterval));
}
}
void setup() {
Serial.begin(115200);
delay(200);
heap_caps_check_integrity_all(true);
Wire.begin(SDA_PIN, SCL_PIN);
// 100 kHz dla CALEJ magistrali - D7S (0x55) wymaga max 100 kHz;
// pozostale czujniki tez poprawnie pracuja na tej predkosci
Wire.setClock(100000);
Serial.println("Skanowanie I2C...");
for (uint8_t addr = 1; addr < 127; addr++) {
Wire.beginTransmission(addr);
if (Wire.endTransmission() == 0) {
Serial.printf(" znaleziono urzadzenie pod 0x%02X\n", addr);
}
}
// D7S
uint8_t d7sTest;
if (d7sRead8(0x1000, d7sTest)) {
Serial.println("D7S OK");
} else {
Serial.println("D7S NIE ZNALEZIONY (0x55) - si/pga beda = 0");
}
// BME280
bmeOk = bme.begin(BME280_I2C_ADDR, &Wire);
if (bmeOk) {
Serial.println("BME280 OK");
} else {
Serial.printf("BME280 NIE ZNALEZIONY pod 0x%02X!\n", BME280_I2C_ADDR);
}
// BMM150
if (bmm.initialize() == BMM150_E_ID_NOT_CONFORM) {
Serial.println("BMM150: Chip ID nie odpowiada - kompas NIEDOSTEPNY!");
bmmOk = false;
} else {
Serial.println("BMM150 OK");
bmmOk = true;
}
// TSL2561
Wire.beginTransmission(0x29);
Wire.write(0x80);
Wire.write(0x03);
if (Wire.endTransmission() == 0) {
Serial.println("TSL2561 OK");
} else {
Serial.println("TSL2561 NIE ZNALEZIONY!");
}
delay(20);
// anemometr
pinMode(WIND_SPEED_PIN, INPUT_PULLUP);
attachInterrupt(digitalPinToInterrupt(WIND_SPEED_PIN), onWindPulse, FALLING);
lastWindCalcMillis = millis();
pinMode(pinUv, INPUT);
Serial.println("Laczenie z WiFi...");
WiFi.mode(WIFI_STA);
delay(100);
WiFi.begin(ssid, pass, wifiChannel);
unsigned long wifiStart = millis();
while (WiFi.status() != WL_CONNECTED && millis() - wifiStart < 15000) {
delay(500);
Serial.printf(" status=%d (%s)\n", WiFi.status(), wifiStatusToText(WiFi.status()));
}
Serial.println();
if (WiFi.status() == WL_CONNECTED) {
Serial.println("WiFi polaczone");
Serial.print("Adres IP: ");
Serial.println(WiFi.localIP());
} else {
Serial.print("Blad: nie udalo sie polaczyc z WiFi, ostatni status: ");
Serial.println(wifiStatusToText(WiFi.status()));
}
// Uruchom zadanie wysylajace (dzieli rdzen z loop - C6 ma 1 rdzen)
xTaskCreate(sendTask, "sendTask", 16384, NULL, 1, &sendTaskHandle);
}
void loop() {
updateWindSpeed();
delay(50);
}