Close

R4_Slave

A project log for Ender_Clock_Duino

Arduino Clock Rabbit Hole

rhea-raeRhea Rae 02/21/2026 at 16:030 Comments

// =====Code for the Arduino R4_Slave====

#include <Arduino.h>
#include <Wire.h>

#include <WiFiS3.h>
#include <WiFiSSLClient.h>
#include <ArduinoHttpClient.h>

// ================= USER SETTINGS =================
static const char* WIFI_SSID = "wifi ssid";
static const char* WIFI_PASS = "wifipassword";

static const float LATITUDE  = 39.1518f;
static const float LONGITUDE = -77.9822f;

static const uint8_t I2C_ADDR = 0x12;

static const uint32_t WEATHER_PERIOD_OK_MS   = 10UL * 60UL * 1000UL; // 10 min
static const uint32_t WEATHER_PERIOD_FAIL_MS = 60UL * 1000UL;        // 60 sec base fail retry
static const uint32_t FAIL_BACKOFF_MAX_MS    = 10UL * 60UL * 1000UL; // cap at 10 min

static const char* WEATHER_HOST = "api.open-meteo.com";
static const int   WEATHER_PORT = 443;

static const uint16_t WIFI_CONNECT_WINDOW_MS = 3500;
static const uint16_t WIFI_IP_WINDOW_MS      = 1200;
static const uint16_t HTTP_TIMEOUT_MS        = 9000;

static const uint32_t STALE_AFTER_MS         = 40UL * 60UL * 1000UL; // 40 min (matches R3)

// ================= STATE =================
static int16_t  g_tempF10   = 0;     // temp * 10 (F)
static uint16_t g_wmoCode   = 0;     // weathercode
static uint32_t g_lastOkMs  = 0;     // millis at last successful fetch

static bool g_wifiOk        = false; // connected + real IP
static bool g_haveWeather   = false; // ever fetched successfully since boot

static uint32_t g_lastAttemptMs = 0;
static uint32_t g_failBackoffMs = WEATHER_PERIOD_FAIL_MS;

// Cached I2C packet (8 bytes)
static volatile uint8_t g_pkt[8] = {0};

// ================= LED =================
static const uint32_t LED_FAST_MS = 120;
static const uint32_t LED_SLOW_MS = 700;
static uint32_t led_last_ms = 0;
static bool led_state = false;

static void led_update() {
  bool stale = false;
  if (g_haveWeather && g_lastOkMs != 0) stale = ((millis() - g_lastOkMs) > STALE_AFTER_MS);

  if (g_haveWeather && !stale) {
    digitalWrite(LED_BUILTIN, HIGH);
    return;
  }

  uint32_t now = millis();
  if (!g_wifiOk) {
    if (now - led_last_ms >= LED_FAST_MS) {
      led_last_ms = now;
      led_state = !led_state;
      digitalWrite(LED_BUILTIN, led_state ? HIGH : LOW);
    }
    return;
  }

  if (now - led_last_ms >= LED_SLOW_MS) {
    led_last_ms = now;
    led_state = !led_state;
    digitalWrite(LED_BUILTIN, led_state ? HIGH : LOW);
  }
}

// ================= UTILS =================
static uint8_t crc_xor(const uint8_t* p, size_t n) {
  uint8_t c = 0;
  for (size_t i = 0; i < n; i++) c ^= p[i];
  return c;
}

static bool ip_is_nonzero(IPAddress ip) {
  return !(ip[0] == 0 && ip[1] == 0 && ip[2] == 0 && ip[3] == 0);
}

static void wifi_ensure_connected() {
  if (WiFi.status() == WL_CONNECTED && ip_is_nonzero(WiFi.localIP())) {
    g_wifiOk = true;
    return;
  }

  g_wifiOk = false;

  Serial.println("WiFi: connecting...");
  WiFi.begin(WIFI_SSID, WIFI_PASS);

  uint32_t start = millis();
  while (millis() - start < WIFI_CONNECT_WINDOW_MS) {
    if (WiFi.status() == WL_CONNECTED) {
      uint32_t ipStart = millis();
      while (millis() - ipStart < WIFI_IP_WINDOW_MS) {
        IPAddress ip = WiFi.localIP();
        if (ip_is_nonzero(ip)) {
          g_wifiOk = true;
          Serial.print("WiFi: connected, IP=");
          Serial.println(ip);
          return;
        }
        delay(25);
      }
      Serial.print("WiFi: connected but no IP, IP=");
      Serial.println(WiFi.localIP());
      g_wifiOk = false;
      return;
    }
    delay(50);
  }

  Serial.println("WiFi: connect timeout");
  g_wifiOk = false;
}

// Parse helpers
static bool parse_float_after_key(const String &s, int startAt, const char* key, float &out) {
  int idx = s.indexOf(key, startAt);
  if (idx < 0) return false;
  idx += (int)strlen(key);
  while (idx < (int)s.length() && s[idx] == ' ') idx++;
  int end = idx;
  while (end < (int)s.length()) {
    char c = s[end];
    if ((c >= '0' && c <= '9') || c == '-' || c == '.') end++;
    else break;
  }
  if (end <= idx) return false;
  out = s.substring(idx, end).toFloat();
  return true;
}

static bool parse_int_after_key(const String &s, int startAt, const char* key, int &out) {
  int idx = s.indexOf(key, startAt);
  if (idx < 0) return false;
  idx += (int)strlen(key);
  while (idx < (int)s.length() && s[idx] == ' ') idx++;
  int end = idx;
  while (end < (int)s.length()) {
    char c = s[end];
    if (c >= '0' && c <= '9') end++;
    else break;
  }
  if (end <= idx) return false;
  out = s.substring(idx, end).toInt();
  return true;
}

static bool fetch_weather_once(int16_t &tempF10_out, uint16_t &code_out) {
  if (!g_wifiOk) return false;

  // DNS sanity check (only costs a tiny bit)
  IPAddress hostIp;
  int dnsOk = WiFi.hostByName(WEATHER_HOST, hostIp);
  Serial.print("DNS ");
  Serial.print(WEATHER_HOST);
  Serial.print(" ok=");
  Serial.print(dnsOk);
  Serial.print(" ip=");
  Serial.println(hostIp);
  if (dnsOk == 0 || !ip_is_nonzero(hostIp)) return false;

  WiFiSSLClient ssl;
  HttpClient client(ssl, WEATHER_HOST, WEATHER_PORT);
  client.setHttpResponseTimeout(HTTP_TIMEOUT_MS);

  char path[240];
  snprintf(
    path, sizeof(path),
    "/v1/forecast?latitude=%.4f&longitude=%.4f¤t_weather=true&temperature_unit=fahrenheit&timezone=auto",
    (double)LATITUDE, (double)LONGITUDE
  );

  Serial.print("HTTP GET ");
  Serial.println(path);

  client.get(path);

  int status = client.responseStatusCode();
  Serial.print("HTTP status: ");
  Serial.println(status);

  String body = client.responseBody();
  Serial.print("Body len: ");
  Serial.println(body.length());
  Serial.print("Body head: ");
  Serial.println(body.substring(0, 140));

  if (status != 200) return false;
  if (body.length() < 50) return false;

  int cw = body.indexOf("\"current_weather\"");
  if (cw < 0) {
    Serial.println("No current_weather object");
    return false;
  }

  float tF = NAN;
  int weathercode = -1;

  bool okT = parse_float_after_key(body, cw, "\"temperature\":", tF);
  bool okC = parse_int_after_key(body, cw, "\"weathercode\":", weathercode);

  if (!okT || !okC || isnan(tF) || weathercode < 0) return false;

  tempF10_out = (int16_t)lroundf(tF * 10.0f);
  if (weathercode > 65535) weathercode = 65535;
  code_out = (uint16_t)weathercode;

  return true;
}

static void rebuild_packet_cache() {
  uint32_t lastOk = g_lastOkMs;

  uint32_t ageMs = (lastOk == 0) ? 0xFFFFFFFFUL : (millis() - lastOk);
  uint32_t ageSec32 = (ageMs == 0xFFFFFFFFUL) ? 65535UL : (ageMs / 1000UL);
  if (ageSec32 > 65535UL) ageSec32 = 65535UL;
  uint16_t ageSec = (uint16_t)ageSec32;

  bool stale = false;
  if (g_haveWeather && lastOk != 0) stale = ((millis() - lastOk) > STALE_AFTER_MS);

  uint8_t pkt[8];

  int16_t  t = g_tempF10;
  uint16_t c = g_wmoCode;

  pkt[0] = (uint8_t)(t & 0xFF);
  pkt[1] = (uint8_t)((t >> 8) & 0xFF);

  pkt[2] = (uint8_t)(c & 0xFF);
  pkt[3] = (uint8_t)((c >> 8) & 0xFF);

  pkt[4] = (uint8_t)(ageSec & 0xFF);
  pkt[5] = (uint8_t)((ageSec >> 8) & 0xFF);

  // bit0 = wifiOk, bit1 = haveWeather, bit2 = stale
  uint8_t flags = 0;
  if (g_wifiOk)      flags |= (1 << 0);
  if (g_haveWeather) flags |= (1 << 1);
  if (stale)         flags |= (1 << 2);
  pkt[6] = flags;

  pkt[7] = crc_xor(pkt, 7);

  noInterrupts();
  for (uint8_t i = 0; i < 8; i++) g_pkt[i] = pkt[i];
  interrupts();
}

static void weather_update_if_due() {
  uint32_t now = millis();

  uint32_t period = g_haveWeather ? WEATHER_PERIOD_OK_MS : g_failBackoffMs;
  if (g_lastAttemptMs != 0 && (now - g_lastAttemptMs) < period) return;
  g_lastAttemptMs = now;

  wifi_ensure_connected();

  if (!g_wifiOk) {
    Serial.println("Weather: skip (no wifi)");
    // backoff grows when failing before first success
    if (!g_haveWeather) {
      g_failBackoffMs = min(g_failBackoffMs * 2UL, FAIL_BACKOFF_MAX_MS);
      Serial.print("Backoff now ");
      Serial.print(g_failBackoffMs / 1000UL);
      Serial.println("s");
    }
    rebuild_packet_cache();
    return;
  }

  int16_t newTempF10 = 0;
  uint16_t newCode = 0;

  bool ok = fetch_weather_once(newTempF10, newCode);
  if (ok) {
    g_tempF10  = newTempF10;
    g_wmoCode  = newCode;
    g_lastOkMs = now;
    g_haveWeather = true;
    g_failBackoffMs = WEATHER_PERIOD_FAIL_MS; // reset backoff
    Serial.println("Weather: OK");
  } else {
    Serial.println("Weather: FAIL (keeping last good if any)");
    if (!g_haveWeather) {
      g_failBackoffMs = min(g_failBackoffMs * 2UL, FAIL_BACKOFF_MAX_MS);
      Serial.print("Backoff now ");
      Serial.print(g_failBackoffMs / 1000UL);
      Serial.println("s");
    }
  }

  rebuild_packet_cache();
}

// I2C request: send cached 8-byte packet (FAST)
static void onI2CRequest() {
  Wire.write((const uint8_t*)g_pkt, 8);
}

void setup() {
  pinMode(LED_BUILTIN, OUTPUT);
  digitalWrite(LED_BUILTIN, LOW);

  Serial.begin(115200);
  delay(300);
  Serial.println("R4 Weather Slave starting...");

  Wire.begin(I2C_ADDR);
  Wire.onRequest(onI2CRequest);

  g_tempF10 = 0;
  g_wmoCode = 0;
  g_lastOkMs = 0;
  g_wifiOk = false;
  g_haveWeather = false;
  g_lastAttemptMs = 0;
  g_failBackoffMs = WEATHER_PERIOD_FAIL_MS;

  rebuild_packet_cache();
  weather_update_if_due();
}

void loop() {
  weather_update_if_due();
  led_update();
  delay(10);
}

Discussions