
//                                meridian orrery
//                                                     by illusionmanager 2026

// Uncomment this if you want the ESP32 to start its own WiFi access point
//#define HAS_WEBSERVER

#include <Wire.h>
#include <RTClib.h>
#include <math.h>
#include <stdio.h>
#include <string.h>

#ifdef HAS_WEBSERVER
#include <WiFi.h>
#include <WebServer.h>
#endif

// --- hardware + motion constants ---

// MICRO_STEPS is fixed at 8
#define MICRO_STEPS 8

// STEPS_PER_DEGREE is a measured value for the geared stepper motor.
// Pick the one that you have.
// The motor labeled 1:236 turned out to be about 1:236.14.
//#define STEPS_PER_DEGREE 13.11944 // motor ratio labeled 1:236 = 236.14
#define STEPS_PER_DEGREE 33.52681 // motor ratio labeled 1:603 = 603.48

// PULSE_TIME determines the speed at which the motor runs.
// A lower value makes it run faster, but below some value,
// the motor will not run at all, or not reliably.
#define PULSE_TIME 58

// Interchange these two if the orrery does not run clockwise during reset.
// This depends on how you wired the motor.
#define CLOCKWISE HIGH
#define COUNTERCLOCKWISE LOW

// Use ZODIAC_OFFSET from 0 to 359 if Neptune is not exactly at the
// Aries/Pisces boundary after reset.
#define ZODIAC_OFFSET 2
#define DIR_PIN GPIO_NUM_1
#define STEP_PIN GPIO_NUM_2
#define ENABLE_PIN GPIO_NUM_3
#define TOUCH_PIN GPIO_NUM_4
#define SDA_PIN GPIO_NUM_8
#define SCL_PIN GPIO_NUM_9
#define MAGNET_PIN GPIO_NUM_10

#define EXTRA_HOME 40

#ifdef HAS_WEBSERVER
WebServer server(80);

#define AP_NAME "Meridian Orrery"
#define WEB_USERNAME ""
#define WEB_PASSWORD ""

static IPAddress ap_ip(192, 168, 4, 4);
static IPAddress ap_gateway(192, 168, 4, 4);
static IPAddress ap_subnet(255, 255, 255, 0);
#endif

// These values depend on printing accuracy. You should not need to change them.
// But if you want to calibrate them, connect the device to the computer,
// send the 'r' command, and then enter 2490 followed by a few 1's until Neptune
// moves a tiny bit. Add those numbers together and subtract 1. That is the value
// for Neptune. Next enter -2130 followed by a few -1's until Uranus starts to move.
// Continue until you have done all the planets.
// You could go in smaller steps, for example 0.5 degree.
static const double engage[8] = {
  0.0,    // Mercury
  356.0,  // Venus
  710.5,  // Earth
  1066.0, // Mars
  1423.0, // Jupiter
  1780.5, // Saturn
  2139.0, // Uranus
  2497.0  // Neptune
};


RTC_DS3231 rtc;

struct Datum {
  int year;
  int month;
  int day;
  int hour;
  int minute;
};

static const char *month_names[] = {
  "Jan", "Feb", "Mar", "Apr",
  "May", "Jun", "Jul", "Aug",
  "Sep", "Oct", "Nov", "Dec"
};

static uint32_t last_rotate = 0;

#ifdef HAS_WEBSERVER

static bool web_busy = false;
static bool web_pending = false;
static Datum web_target;
static Datum web_display_date;
static char web_status[96] = "Ready";
static bool web_status_pending = false;
static uint32_t web_first_unpolled_status_time = 0;

void web_status_set(const char *fmt, ...) {
  va_list args;
  va_start(args, fmt);

  vsnprintf(web_status, sizeof(web_status), fmt, args);

  va_end(args);

  if (!web_status_pending) {
    web_first_unpolled_status_time = millis();
    web_status_pending = true;
  }
}
static void web_poll_after_status() {
  if (!web_status_pending) {
    return;
  }

  if ((uint32_t)(millis() - web_first_unpolled_status_time) < 1000) {
    return;
  }

  server.handleClient();

  web_status_pending = false;
}

#define WEB_STATUS(...) web_status_set(__VA_ARGS__)

#else

#define WEB_STATUS(...)

#endif

static int prev_day = 0;



struct Poly4 {
  double a0, a1, a2, a3;
};

struct MeeusElemDate {
  struct Poly4 L;
  struct Poly4 a_au;
  struct Poly4 e;
  struct Poly4 i;
  struct Poly4 omega;
  struct Poly4 pi;
};

static const char *planet_names[8] = {
  "Mercury", "Venus", "Earth", "Mars", "Jupiter", "Saturn", "Uranus", "Neptune"
};

static float start_offset[8];

static const struct MeeusElemDate meeus_elem[8] = {
  {
    { 252.250906, 149474.0722491, 0.00030350, 0.000000018 },
    { 0.387098310, 0.0, 0.0, 0.0 },
    { 0.20563175, 0.000020407, -0.0000000283, -0.00000000018 },
    { 7.004986, 0.0018215, -0.00001810, 0.000000056 },
    { 48.330893, 1.1861883, 0.00017542, 0.000000215 },
    { 77.456119, 1.5564776, 0.00029544, 0.000000009 }
  },
  {
    { 181.979801, 58519.2130302, 0.00031014, 0.000000015 },
    { 0.723329820, 0.0, 0.0, 0.0 },
    { 0.00677192, -0.000047765, 0.0000000981, 0.00000000046 },
    { 3.394662, 0.0010037, -0.00000088, -0.000000007 },
    { 76.679920, 0.9011206, 0.00040618, -0.000000093 },
    { 131.563703, 1.4022288, -0.00107618, -0.000005678 }
  },
  {
    { 100.466457, 36000.7698278, 0.00030322, 0.000000020 },
    { 1.000001018, 0.0, 0.0, 0.0 },
    { 0.01670863, -0.000042037, -0.0000001267, 0.00000000014 },
    { 0.0, 0.0, 0.0, 0.0 },
    { 0.0, 0.0, 0.0, 0.0 },
    { 102.937348, 1.7195366, 0.00045688, -0.000000018 }
  },
  {
    { 355.433000, 19141.6964471, 0.00031052, 0.000000016 },
    { 1.523679342, 0.0, 0.0, 0.0 },
    { 0.09340065, 0.000090484, -0.0000000806, -0.00000000025 },
    { 1.849726, -0.0006011, 0.00001276, -0.000000007 },
    { 49.558093, 0.7720959, 0.00001557, 0.000002267 },
    { 336.060234, 1.8410449, 0.00013477, 0.000000536 }
  },
  {
    { 34.351519, 3036.3027748, 0.00022330, 0.000000037 },
    { 5.202603209, 0.0000001913, 0.0, 0.0 },
    { 0.04849793, 0.000163225, -0.0000004714, -0.00000000201 },
    { 1.303267, -0.0054965, 0.00000466, -0.000000002 },
    { 100.464407, 1.0209774, 0.00040315, 0.000000404 },
    { 14.331207, 1.6126352, 0.00103042, -0.000004464 }
  },
  {
    { 50.077444, 1223.5110686, 0.00051908, -0.000000030 },
    { 9.554909192, -0.0000021390, 0.000000004, 0.0 },
    { 0.05554814, -0.000346641, -0.0000006436, 0.00000000340 },
    { 2.488879, -0.0037362, -0.00001519, 0.000000087 },
    { 113.665503, 0.8770880, -0.00012176, -0.000002249 },
    { 93.057237, 1.9637613, 0.00083753, 0.000004928 }
  },
  {
    { 314.055005, 429.8640561, 0.00030390, 0.000000026 },
    { 19.218446062, -0.0000000372, 0.00000000098, 0.0 },
    { 0.04638122, -0.000027293, 0.0000000789, 0.00000000024 },
    { 0.773197, 0.0007744, 0.00003749, -0.000000092 },
    { 74.005957, 0.5211278, 0.00133947, 0.000018484 },
    { 173.005291, 1.4863790, 0.00021406, 0.000000434 }
  },
  {
    { 304.348665, 219.8833092, 0.00030882, 0.000000018 },
    { 30.110386869, -0.0000001663, 0.00000000069, 0.0 },
    { 0.00945575, 0.000006033, 0.0, -0.00000000005 },
    { 1.769953, -0.0093082, -0.00000708, 0.000000027 },
    { 131.784057, 1.1022039, 0.00025952, -0.000000637 },
    { 48.120276, 1.4262957, 0.00038434, 0.000000020 }
  }
};

volatile bool magnet_seen = false;

char cmd_buf[64];
int cmd_pos = 0;

static double deg2rad(double x) {
  return x * (PI / 180.0);
}

static double rad2deg(double x) {
  return x * (180.0 / PI);
}

static double wrap360(double x) {
  while (x < 0.0) x += 360.0;
  while (x >= 360.0) x -= 360.0;
  return x;
}

static double poly4_eval(const struct Poly4 &p, double T) {
  return ((p.a3 * T + p.a2) * T + p.a1) * T + p.a0;
}

static double solve_kepler_E(double M, double e) {
  double E = M + e * sin(M) * (1.0 + e * cos(M));

  for (int i = 0; i < 10; i++) {
    double f = E - e * sin(E) - M;
    double fp = 1.0 - e * cos(E);
    double dE = f / fp;
    E -= dE;

    if (fabs(dE) < 1e-12) break;
  }

  return E;
}

static double heliocentric_longitude(double N, double inc, double w, double a_au, double e, double M) {
  N = deg2rad(wrap360(N));
  inc = deg2rad(inc);
  w = deg2rad(wrap360(w));
  M = deg2rad(wrap360(M));

  double Ec = solve_kepler_E(M, e);

  double xv = a_au * (cos(Ec) - e);
  double yv = a_au * (sqrt(1.0 - e * e) * sin(Ec));

  double v = atan2(yv, xv);
  double r = sqrt(xv * xv + yv * yv);

  double xh = r * (cos(N) * cos(v + w) - sin(N) * sin(v + w) * cos(inc));
  double yh = r * (sin(N) * cos(v + w) + cos(N) * sin(v + w) * cos(inc));

  return wrap360(rad2deg(atan2(yh, xh)));
}

static double julian_day(const Datum &d) {
  int y = d.year;
  int m = d.month;
  int day = d.day;

  if (m <= 2) {
    y -= 1;
    m += 12;
  }

  int A = y / 100;
  int B = 2 - A + (A / 4);

  double day_fraction =
      (d.hour +
      d.minute / 60.0) / 24.0;

  double jd =
      floor(365.25 * (y + 4716))
    + floor(30.6001 * (m + 1))
    + day + day_fraction + B - 1524.5;

  return jd;
}

static double planet_helio_lon(double jd_ut, int p) {
  double T = (jd_ut - 2451545.0) / 36525.0;
  const struct MeeusElemDate &E = meeus_elem[p];

  double L = wrap360(poly4_eval(E.L, T));
  double a = poly4_eval(E.a_au, T);
  double ecc = poly4_eval(E.e, T);
  double inc = poly4_eval(E.i, T);
  double Om = wrap360(poly4_eval(E.omega, T));
  double piL = wrap360(poly4_eval(E.pi, T));

  double M = wrap360(L - piL);
  double w = wrap360(piL - Om);

  return wrap360(heliocentric_longitude(Om, inc, w, a, ecc, M));
}

static double moon_phase_angle(double jd) {
  double T = (jd - 2451545.0) / 36525.0;

  double L0 = 218.3164477 + 481267.88123421 * T;
  double M = 357.5291092 + 35999.0502909 * T;
  double Mp = 134.9633964 + 477198.8675055 * T;
  double D = 297.8501921 + 445267.1114034 * T;
  double F = 93.2720950 + 483202.0175233 * T;

  double Mr = deg2rad(M);
  double Mpr = deg2rad(Mp);
  double Dr = deg2rad(D);
  double Fr = deg2rad(F);

  double Ls =
      280.46646 + 36000.76983 * T
    + 1.914602 * sin(Mr)
    + 0.019993 * sin(2.0 * Mr);

  double Lm =
      L0
    + 6.288774 * sin(Mpr)
    + 1.274027 * sin(2.0 * Dr - Mpr)
    + 0.658314 * sin(2.0 * Dr)
    + 0.213618 * sin(2.0 * Mpr)
    - 0.185116 * sin(Mr)
    - 0.114332 * sin(2.0 * Fr);

  return wrap360(Lm - Ls);
}

void print_date_time();

static void print_angles() {
  DateTime now = rtc.now();

  Datum d;
  d.year = now.year();
  d.month = now.month();
  d.day = now.day();
  d.hour = now.hour();
  d.minute = now.minute();

  double jd = julian_day(d);

  Serial.printf("Angles for %d-%s-%d %02d:%02d:%02d\n",
    now.day(), month_names[now.month() - 1], now.year(),
    now.hour(), now.minute(), now.second());

  for (int p = 0; p < 8; p++) {
    double lon = planet_helio_lon(jd, p);
    Serial.printf("%-8s  %8.3f deg\n", planet_names[p], lon);
  }

  Serial.printf("%-8s  %8.3f deg\n", "Moon", moon_phase_angle(jd));
}

void enable_motor() {
  digitalWrite(ENABLE_PIN, LOW);
}

void disable_motor() {
  digitalWrite(ENABLE_PIN, HIGH);
}

void rotate(float deg) {
  if (deg < 0) {
    digitalWrite(DIR_PIN, CLOCKWISE);
  } else {
    digitalWrite(DIR_PIN, COUNTERCLOCKWISE);
  }
#ifdef HAS_WEBSERVER
  server.handleClient();
#endif
  long steps_to_do = lround(fabs(deg) * STEPS_PER_DEGREE * MICRO_STEPS);

  const float decel_degrees = 30.0;
  const float decel_factor = 3.0;

  const long decel_steps = decel_degrees * STEPS_PER_DEGREE * MICRO_STEPS;
  long step_counter = 0;
  while (steps_to_do > 0) {
    int this_pulse_time = PULSE_TIME;

    if (steps_to_do < decel_steps && decel_steps > 1) {
      float t = 1.0f - (float)(steps_to_do - 1) / (float)(decel_steps - 1);
      float u = t * t * (3.0f - 2.0f * t);
      float min_speed_factor = 1.0f / decel_factor;
      float speed_factor = 1.0f - (1.0f - min_speed_factor) * u;

      this_pulse_time = (int)lroundf((float)PULSE_TIME / speed_factor);
    }
#ifdef HAS_WEBSERVER
    if ((step_counter % 200) == 0) {
      web_poll_after_status();
    }
    step_counter++;
#endif
    digitalWrite(STEP_PIN, HIGH);
    delayMicroseconds(this_pulse_time);
    digitalWrite(STEP_PIN, LOW);
    delayMicroseconds(this_pulse_time);

    steps_to_do--;

  }
  last_rotate = millis();
#ifdef HAS_WEBSERVER
      server.handleClient();
#endif
}

void IRAM_ATTR magnet_isr() {
  magnet_seen = true;
}

void go_home(float extra_after_home) {
  Serial.println("going home");
  WEB_STATUS("Going home");

  enable_motor();

  if (digitalRead(MAGNET_PIN)) {
    Serial.println("magnet active, clearing it first");
    WEB_STATUS("Clearing magnet");
    digitalWrite(DIR_PIN, CLOCKWISE);

    long quiet_steps = lround(20.0 * STEPS_PER_DEGREE * MICRO_STEPS);
    long steps_since_seen = 0;

    while (steps_since_seen < quiet_steps) {
      digitalWrite(STEP_PIN, HIGH);
      delayMicroseconds(PULSE_TIME);
      digitalWrite(STEP_PIN, LOW);
      delayMicroseconds(PULSE_TIME);

      if (digitalRead(MAGNET_PIN)) {
        steps_since_seen = 0;
      } else {
        steps_since_seen++;
      }

    }
  }

  magnet_seen = false;
  attachInterrupt(digitalPinToInterrupt(MAGNET_PIN), magnet_isr, RISING);

  digitalWrite(DIR_PIN, CLOCKWISE);

  for (long i = 0; i < (long)(STEPS_PER_DEGREE * MICRO_STEPS * 360.0 * 6.0); i++) {
    digitalWrite(STEP_PIN, HIGH);
    delayMicroseconds(PULSE_TIME);
    digitalWrite(STEP_PIN, LOW);
    delayMicroseconds(PULSE_TIME);
#ifdef HAS_WEBSERVER
    if ((i % 200) == 0) {
      web_poll_after_status();
    }
#endif
    if (magnet_seen) {
      Serial.println("zero detected");
      WEB_STATUS("Zero detected");
      break;
    }
  }

  detachInterrupt(digitalPinToInterrupt(MAGNET_PIN));

  rotate(-(360.0 * 3.0) - EXTRA_HOME - extra_after_home);

  disable_motor();

  Serial.println("home done");
  WEB_STATUS("Home done");
}


void compute_start_offsets() {
  float tab_loss[8];

  for (int i = 0; i < 8; i++) {
    float turns = roundf((float)engage[i] / 360.0f);
    tab_loss[i] = turns * 360.0f - (float)engage[i];
  }

  start_offset[7] = 0.0f;
  start_offset[6] = tab_loss[6] - tab_loss[7];
  start_offset[5] = 0.0f;
  start_offset[4] = tab_loss[4] - tab_loss[5];
  start_offset[3] = 0.0f;
  start_offset[2] = tab_loss[2] - tab_loss[3];
  start_offset[1] = 0.0f;
  start_offset[0] = tab_loss[0] - tab_loss[1];
}


static void place_targets(const float target[8]) {
  enable_motor();

  float prev_target = -ZODIAC_OFFSET;
  int dir = 1;

  for (int i = 7; i >= 0; i--) {
    delay(250);

    float current_angle = wrap360(prev_target + start_offset[i]);
    float extra = dir * wrap360(dir * (target[i] - current_angle));
    float move = dir * engage[i] + extra;

    WEB_STATUS("Rotate %s to %.1f deg", planet_names[i], target[i]);

    Serial.printf("Rotating to %.1f deg for %s\n", target[i], planet_names[i]);

    rotate(move);

    prev_target = target[i];
    dir = -dir;
  }

  disable_motor();
}

static void do_solar_system_for(const Datum &when) {
  float target[8];
#ifdef HAS_WEBSERVER
  web_busy = true;
#endif

  double jd = julian_day(when);

  for (int p = 0; p < 8; p++) {
    target[p] = (float)planet_helio_lon(jd, p);
  }

  float moon_phase = (float)moon_phase_angle(jd);
  float moon_correction = wrap360(moon_phase + 11.0f * target[2] - ZODIAC_OFFSET) / 11.0f;

  go_home(moon_correction);

  Serial.printf("Moon phase %.1f\n", moon_phase);

  place_targets(target);

  Serial.println("Solar System done.");

#ifdef HAS_WEBSERVER
  WEB_STATUS("Ready");
  web_busy = false;
#endif
}

static void do_solar_system() {
  DateTime now = rtc.now();

  Datum d;
  d.year = now.year();
  d.month = now.month();
  d.day = now.day();
  d.hour = now.hour();
  d.minute = now.minute();

  print_date_time();
  do_solar_system_for(d);
  print_date_time();
}

// --- webserver ---

#ifdef HAS_WEBSERVER
const unsigned char icon_png[] = {
  0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A, 0x00, 0x00, 0x00, 0x0D, 0x49, 0x48, 0x44, 0x52,
  0x00, 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, 0xC0, 0x08, 0x06, 0x00, 0x00, 0x00, 0x52, 0xDC, 0x6C,
  0x07, 0x00, 0x00, 0x0D, 0x63, 0x49, 0x44, 0x41, 0x54, 0x78, 0xDA, 0xED, 0x5D, 0x3B, 0x6E, 0x1C,
  0x39, 0x14, 0x7C, 0x24, 0xF6, 0x12, 0x3A, 0xC0, 0x24, 0x0A, 0xE4, 0x54, 0xA1, 0x00, 0x41, 0xD0,
  0x29, 0x14, 0x29, 0x10, 0xA0, 0xCC, 0xC0, 0x06, 0x3A, 0x82, 0x03, 0x01, 0xCE, 0x04, 0x38, 0x98,
  0x48, 0xA7, 0x30, 0x06, 0x02, 0x14, 0x3A, 0xB5, 0x83, 0x49, 0x74, 0x00, 0x1D, 0x83, 0x1B, 0xEC,
  0xF4, 0xA0, 0xDD, 0x43, 0xB2, 0xF9, 0xE7, 0x23, 0x59, 0x0F, 0x58, 0xC0, 0xD0, 0x1A, 0xD6, 0x34,
  0x59, 0x55, 0xAF, 0xEA, 0xB1, 0xA7, 0x5B, 0x28, 0xA5, 0x08, 0x15, 0x5D, 0x8A, 0x88, 0x68, 0xBF,
  0xDB, 0x12, 0x11, 0xD1, 0xE7, 0xC7, 0xFB, 0xF1, 0x7F, 0xFC, 0xF9, 0xF1, 0x2B, 0xEA, 0x1F, 0xBE,
  0x78, 0xB8, 0x3C, 0xFE, 0xF9, 0x6C, 0x73, 0x45, 0x44, 0x44, 0xE7, 0x37, 0xF7, 0xD3, 0x8F, 0x04,
  0x96, 0x3E, 0xAE, 0x04, 0x08, 0xE0, 0x0F, 0xF6, 0xFD, 0x6E, 0x7B, 0x04, 0x79, 0x2C, 0xC0, 0x63,
  0x6B, 0x22, 0xC8, 0xD9, 0xE6, 0x6A, 0x22, 0x06, 0x48, 0x01, 0x02, 0xF4, 0x09, 0x76, 0x90, 0x02,
  0x04, 0xC8, 0x5E, 0xFB, 0xDD, 0x56, 0x11, 0x11, 0xFD, 0xFC, 0xF7, 0x5B, 0x57, 0xD7, 0x75, 0xFB,
  0xFC, 0x34, 0xD9, 0x27, 0x90, 0x01, 0x04, 0x38, 0x05, 0xFD, 0xE7, 0xC7, 0x7B, 0x32, 0x85, 0x9F,
  0x2B, 0xF0, 0xB2, 0x66, 0xFE, 0xDD, 0xF4, 0x59, 0x4E, 0x7E, 0x96, 0xBA, 0x03, 0x5D, 0x3C, 0x5C,
  0x4E, 0xDD, 0x41, 0x80, 0x00, 0x83, 0x12, 0x20, 0x05, 0xE8, 0x27, 0x20, 0x15, 0x0E, 0xA6, 0x27,
  0x81, 0x3B, 0xC5, 0x35, 0x8C, 0x4A, 0x86, 0xD1, 0x08, 0xA0, 0xF6, 0xBB, 0x6D, 0x90, 0xBD, 0x69,
  0xC0, 0x57, 0x47, 0xE7, 0x95, 0xDB, 0xE7, 0xA7, 0xE1, 0x32, 0xC3, 0x10, 0x04, 0x08, 0x55, 0xFB,
  0x99, 0x3A, 0xB6, 0x08, 0x8A, 0x23, 0x21, 0x22, 0xAE, 0x5B, 0x80, 0x00, 0x8D, 0x03, 0xDF, 0x47,
  0xED, 0x1B, 0x07, 0x7C, 0x16, 0x42, 0x1C, 0xBA, 0x82, 0x00, 0x01, 0x3A, 0x06, 0xFE, 0x80, 0xAD,
  0xDF, 0xDB, 0x0A, 0xF6, 0x4A, 0x84, 0xAE, 0x08, 0xE0, 0x03, 0xFC, 0x8B, 0x87, 0x4B, 0xBA, 0x7E,
  0x7C, 0x1D, 0xCA, 0xEF, 0x9A, 0xC8, 0xF0, 0xF6, 0x72, 0xE7, 0xDC, 0x15, 0x7A, 0x23, 0x42, 0x17,
  0x04, 0xF0, 0x01, 0xFE, 0x88, 0x41, 0x2F, 0x47, 0x57, 0xE8, 0x85, 0x08, 0xAD, 0x13, 0xC0, 0x49,
  0xBD, 0x30, 0xF7, 0xCE, 0x33, 0x34, 0xE8, 0xA1, 0x8B, 0xB6, 0x4A, 0x00, 0x67, 0xE0, 0xC3, 0xE6,
  0x60, 0x9D, 0xBB, 0x22, 0x80, 0x8B, 0xDD, 0x01, 0xF0, 0xEB, 0x10, 0xA1, 0x45, 0x5B, 0xD4, 0x12,
  0x01, 0x56, 0x37, 0x01, 0x56, 0xA7, 0xBE, 0x35, 0x0A, 0x10, 0x1F, 0xB5, 0xBC, 0x8D, 0x7C, 0x71,
  0xBA, 0x2E, 0x86, 0x27, 0x80, 0x8B, 0xEA, 0xF7, 0x3E, 0xAF, 0x6E, 0x6D, 0x3F, 0xBE, 0xFE, 0xFE,
  0xB0, 0x81, 0x37, 0x64, 0xF2, 0x94, 0x85, 0x0C, 0xEC, 0x09, 0xF0, 0xF6, 0x72, 0xA7, 0x6C, 0x0B,
  0x05, 0xE0, 0xF3, 0x23, 0xC2, 0xA1, 0x0B, 0x88, 0x58, 0xE0, 0x97, 0xD8, 0x6B, 0xCE, 0x04, 0x50,
  0xDF, 0xBF, 0x6C, 0xE0, 0xF3, 0x1B, 0xB3, 0xA6, 0x26, 0xF0, 0xFB, 0x1E, 0x4E, 0x46, 0x74, 0x97,
  0xF6, 0x09, 0xB0, 0xB6, 0x58, 0x50, 0x7D, 0xBE, 0xF9, 0x20, 0x37, 0xF8, 0x53, 0x63, 0x80, 0x1D,
  0x01, 0x6C, 0x96, 0x07, 0xAA, 0xDF, 0x67, 0x5E, 0xA8, 0xD9, 0x09, 0x38, 0x11, 0xC0, 0x6A, 0x79,
  0xA0, 0xFA, 0x00, 0x7F, 0x0E, 0x12, 0x70, 0x21, 0x80, 0x15, 0xFC, 0x29, 0x3D, 0x1F, 0x8A, 0xC7,
  0x9E, 0xA6, 0x2A, 0x4B, 0xE0, 0x76, 0x2A, 0xC9, 0x41, 0x25, 0x4C, 0x0B, 0x75, 0xF1, 0x70, 0x49,
  0x5F, 0x7F, 0x7F, 0x08, 0x80, 0xBF, 0xBD, 0x7A, 0x7B, 0xB9, 0x2B, 0xF2, 0x7B, 0xFE, 0xFC, 0xF8,
  0x75, 0xFC, 0x1E, 0x77, 0x73, 0x04, 0xB0, 0xB5, 0xC8, 0xDB, 0xE7, 0xA7, 0x28, 0x66, 0xA3, 0xEA,
  0xAA, 0x7F, 0xC9, 0x27, 0x68, 0xCC, 0x9F, 0xC3, 0xD4, 0x0C, 0x01, 0xD6, 0xC0, 0x0F, 0xBF, 0xDF,
  0xB4, 0xF7, 0x2F, 0xFA, 0xFB, 0x0E, 0x64, 0x0B, 0xEA, 0x02, 0x55, 0x32, 0x80, 0x0D, 0xFC, 0xF0,
  0xFB, 0xED, 0xD7, 0xF7, 0x2F, 0x9B, 0xE2, 0xA0, 0x0A, 0x15, 0x4D, 0xC9, 0x05, 0xFC, 0xF0, 0xFB,
  0xA8, 0x1A, 0x36, 0x48, 0x72, 0x01, 0x3F, 0xFC, 0x7E, 0x3F, 0xFE, 0xBF, 0xC6, 0x2F, 0x0D, 0xCD,
  0x1C, 0x12, 0xE0, 0x47, 0xB5, 0xEC, 0xFF, 0x63, 0x4B, 0x02, 0xFC, 0xA8, 0x91, 0x4B, 0x02, 0xFC,
  0x28, 0x10, 0x00, 0xE0, 0x47, 0x25, 0xAA, 0xB5, 0x67, 0x9F, 0x8E, 0x44, 0x00, 0x80, 0x1F, 0x55,
  0xAC, 0xE6, 0x2F, 0x12, 0xE1, 0x40, 0x00, 0xED, 0xED, 0x0D, 0x00, 0xFF, 0x10, 0x55, 0x65, 0x7F,
  0x75, 0x4F, 0xE2, 0xAE, 0x46, 0x00, 0xD3, 0xBD, 0x3D, 0x00, 0xFF, 0x18, 0x35, 0xBD, 0x8B, 0xA0,
  0x05, 0xEB, 0x95, 0x9C, 0x00, 0x6F, 0x2F, 0x77, 0xDA, 0x39, 0xF0, 0xE1, 0x84, 0x17, 0x85, 0x1C,
  0xC0, 0xAA, 0xF3, 0x24, 0x25, 0xC0, 0x7E, 0xB7, 0xD5, 0xDE, 0x04, 0x75, 0x50, 0x04, 0xA8, 0xFF,
  0x40, 0x36, 0x28, 0xD4, 0x93, 0x97, 0xEE, 0x38, 0x29, 0xEF, 0x05, 0xD2, 0xFA, 0x7E, 0xDC, 0xD8,
  0x36, 0x6C, 0x15, 0xF9, 0x3E, 0xC0, 0xC1, 0x5D, 0xD4, 0xFF, 0x3E, 0x80, 0x29, 0xF4, 0x02, 0xFC,
  0xE3, 0x76, 0x81, 0x12, 0x59, 0x20, 0xD6, 0x5A, 0x27, 0x21, 0x80, 0xC9, 0xF7, 0x23, 0xF4, 0x0E,
  0x9F, 0x05, 0xB2, 0x5A, 0xA1, 0x14, 0xD6, 0x3A, 0x9A, 0x00, 0x26, 0xDF, 0x8F, 0xD0, 0x8B, 0x9A,
  0x44, 0x30, 0x07, 0x09, 0x52, 0x59, 0xEB, 0x58, 0x02, 0x68, 0x0F, 0xBB, 0x10, 0x7A, 0x51, 0x4B,
  0x12, 0xA4, 0xB4, 0x43, 0x29, 0x73, 0xE5, 0x3F, 0x91, 0xD6, 0x07, 0xBE, 0x1F, 0xE5, 0x6C, 0x87,
  0xE8, 0x99, 0xA2, 0x9E, 0x12, 0x91, 0xE3, 0xB1, 0x38, 0xC1, 0x53, 0x20, 0xD3, 0x7D, 0x3E, 0xF8,
  0x46, 0x17, 0x2A, 0x14, 0x3B, 0x11, 0xC0, 0x57, 0xA1, 0x98, 0x0B, 0x25, 0x00, 0x46, 0x9E, 0xA8,
  0x14, 0xF5, 0xD7, 0xAB, 0x5D, 0xE7, 0xE5, 0xFA, 0x84, 0xE8, 0xE9, 0x41, 0x6A, 0xA1, 0xA3, 0xD0,
  0x20, 0x02, 0xE8, 0x9E, 0xDE, 0x86, 0xFB, 0x7C, 0x50, 0xA5, 0xC9, 0x33, 0x17, 0xE1, 0x50, 0xFC,
  0x85, 0x10, 0x40, 0xAB, 0xFE, 0xB0, 0x3E, 0xA8, 0x92, 0xA5, 0xFB, 0xE2, 0x7D, 0x08, 0x06, 0xBD,
  0xA7, 0x40, 0xBA, 0xE0, 0x8B, 0xA9, 0x0F, 0xAA, 0x74, 0xE9, 0xA6, 0x4A, 0x21, 0x0F, 0xE3, 0xF2,
  0xEA, 0x00, 0x96, 0xE0, 0x9B, 0x13, 0xFC, 0x2A, 0xD7, 0xF7, 0x4C, 0xF1, 0xB6, 0xC8, 0x32, 0xEB,
  0x9C, 0x6B, 0xAD, 0x75, 0x5D, 0xC0, 0x37, 0x87, 0x7A, 0x11, 0x20, 0xC5, 0x2F, 0x0C, 0x99, 0x18,
  0xC4, 0x3C, 0xF9, 0xCB, 0x56, 0x78, 0x9D, 0x52, 0x99, 0x75, 0xCE, 0xB5, 0xD6, 0x29, 0x04, 0x59,
  0xFA, 0xFC, 0xB2, 0xE5, 0xCF, 0x5A, 0x9F, 0xF9, 0x1F, 0x36, 0x5C, 0x01, 0xFE, 0x94, 0x15, 0xFC,
  0x19, 0xBB, 0x8A, 0xF6, 0x94, 0xD9, 0xE7, 0x59, 0xA1, 0xCE, 0x04, 0xD0, 0x31, 0xED, 0x30, 0x9B,
  0x6D, 0x5D, 0xF9, 0xA0, 0xFE, 0x0D, 0xAF, 0x81, 0x0E, 0x83, 0x3E, 0x67, 0x0C, 0xD2, 0x71, 0x81,
  0xB4, 0xEA, 0x5F, 0xC2, 0x3F, 0x9F, 0xDF, 0xDC, 0xD3, 0xD9, 0xE6, 0xEA, 0xF8, 0x1F, 0xBA, 0x40,
  0x3B, 0xEA, 0x9F, 0x73, 0xDF, 0xE6, 0x36, 0x3E, 0xA6, 0x0B, 0x38, 0x65, 0x80, 0x54, 0x23, 0xA7,
  0x94, 0x9B, 0x36, 0x57, 0xAE, 0xD8, 0x0D, 0x1C, 0x39, 0x0B, 0xA4, 0xF0, 0xFE, 0x8B, 0x43, 0xAB,
  0x1A, 0xB8, 0x30, 0x8D, 0xE6, 0x57, 0x3F, 0x87, 0x74, 0x59, 0xA0, 0x5A, 0xEA, 0x6F, 0x23, 0x2E,
  0x11, 0x89, 0xF3, 0x9B, 0x7B, 0x71, 0x7E, 0x73, 0x2F, 0xAE, 0x1F, 0x5F, 0xA3, 0x54, 0xA6, 0x45,
  0xFF, 0xCB, 0x41, 0xFD, 0xCF, 0x36, 0x57, 0x74, 0xFD, 0xF8, 0x4A, 0xD3, 0x3E, 0x4C, 0xFB, 0x52,
  0x03, 0x0F, 0xA1, 0x5D, 0x60, 0xB5, 0x03, 0x30, 0x54, 0xFF, 0x2C, 0x6A, 0x36, 0x62, 0x17, 0xE8,
  0x6C, 0xBD, 0x82, 0xBA, 0x80, 0x6C, 0x50, 0xFD, 0xAD, 0x53, 0x81, 0xD0, 0x4E, 0x30, 0x60, 0x17,
  0x88, 0x52, 0x7F, 0x86, 0x0F, 0xC0, 0x0A, 0xEA, 0x02, 0xD2, 0x17, 0x14, 0x99, 0x03, 0x4D, 0x92,
  0xD0, 0x1C, 0xA3, 0x88, 0x03, 0xA9, 0x7F, 0xAC, 0xE7, 0x67, 0x27, 0x82, 0x3A, 0x6C, 0xAE, 0x91,
  0xDC, 0x46, 0x00, 0xED, 0x37, 0xBD, 0x1A, 0xB0, 0x09, 0x38, 0xD8, 0x1A, 0xB4, 0xE3, 0xE9, 0xB0,
  0xB9, 0xF6, 0xF6, 0x18, 0xE9, 0xA3, 0x10, 0x35, 0x1E, 0x78, 0x94, 0x4A, 0x09, 0x3C, 0x40, 0xD1,
  0x7D, 0x17, 0x88, 0xED, 0x74, 0x9C, 0x9F, 0xFF, 0xA9, 0xC3, 0xA8, 0xAD, 0xDB, 0x19, 0x09, 0xA0,
  0x3B, 0x4C, 0x18, 0x21, 0x24, 0x8E, 0x70, 0x30, 0xD6, 0x73, 0xDE, 0xD1, 0x61, 0xD4, 0x76, 0x30,
  0x26, 0x3D, 0xC3, 0xEF, 0x28, 0xE0, 0x50, 0x1D, 0x13, 0x5C, 0xA5, 0xEC, 0x9A, 0x1C, 0xCB, 0x27,
  0x0C, 0x4B, 0x57, 0x85, 0xE8, 0xE1, 0xB6, 0x07, 0x74, 0x81, 0x36, 0x07, 0x1B, 0xBE, 0xA5, 0xC3,
  0xAA, 0xA9, 0xEB, 0x69, 0x09, 0x60, 0x78, 0xDF, 0xD2, 0x30, 0xE1, 0xB2, 0xE3, 0x2E, 0xA0, 0x5A,
  0xF3, 0xF4, 0x81, 0x25, 0x1C, 0x31, 0x7D, 0x4A, 0x00, 0x5D, 0xAB, 0x68, 0x25, 0xFC, 0xAE, 0x05,
  0x63, 0x1F, 0xA5, 0xEB, 0xB1, 0x0B, 0xE8, 0xAE, 0x89, 0xEB, 0x48, 0x33, 0x53, 0x18, 0x56, 0xAB,
  0x04, 0xD0, 0xB5, 0x8A, 0x5E, 0x14, 0xC2, 0xE7, 0x3A, 0x3A, 0xEC, 0x02, 0xAA, 0xE7, 0xBD, 0x75,
  0xB9, 0x2E, 0xDD, 0xF5, 0xCB, 0xB5, 0x56, 0xC1, 0xF9, 0xE4, 0x37, 0x45, 0x6B, 0x1C, 0xA5, 0x0B,
  0x8C, 0xA4, 0xFE, 0xD3, 0x5E, 0x2F, 0xC3, 0xB0, 0xCE, 0x06, 0xC9, 0xB5, 0x16, 0xD1, 0x5B, 0x40,
  0xF2, 0xB9, 0x9E, 0x8E, 0xC6, 0x85, 0x6A, 0xC4, 0x1B, 0xFE, 0x74, 0x7B, 0xBD, 0xC4, 0xF8, 0xEA,
  0xDD, 0xA0, 0xBD, 0xB5, 0x48, 0xDF, 0xEB, 0xE9, 0xE1, 0xF6, 0x08, 0x53, 0x27, 0xEB, 0xFD, 0x5C,
  0xC7, 0x65, 0xAF, 0xFF, 0x22, 0x80, 0xE1, 0xC0, 0xA0, 0xB7, 0x45, 0x12, 0x83, 0x75, 0x01, 0x35,
  0xC2, 0xE8, 0xD3, 0xD5, 0xF2, 0x2E, 0x31, 0x3E, 0x27, 0xC0, 0x30, 0x87, 0x5F, 0x01, 0x5D, 0xAD,
  0xD9, 0x2E, 0x60, 0x51, 0xFF, 0x21, 0x6C, 0x90, 0x01, 0xC3, 0xEA, 0x84, 0x00, 0x96, 0x90, 0xD4,
  0xA5, 0x32, 0x0C, 0x32, 0x12, 0xB5, 0xA9, 0xFF, 0x10, 0xE7, 0x3A, 0x86, 0x1C, 0x70, 0xDA, 0x01,
  0x7A, 0x1A, 0x91, 0xB9, 0xD8, 0x96, 0x11, 0x46, 0xA2, 0xA3, 0xAB, 0xBF, 0xE9, 0x5A, 0xE7, 0xF8,
  0x90, 0x2B, 0xAD, 0xA3, 0x67, 0x95, 0xE8, 0x7D, 0x24, 0x3A, 0xBC, 0xFA, 0x4F, 0xFB, 0x6C, 0xB3,
  0xF2, 0x47, 0x02, 0x2C, 0x67, 0xA4, 0x23, 0x84, 0xA4, 0x80, 0x30, 0xDC, 0x4C, 0x17, 0xC8, 0xA9,
  0xFE, 0xAD, 0x89, 0xC1, 0x72, 0x9F, 0xE7, 0x58, 0x97, 0xAD, 0x87, 0x3C, 0xD7, 0xC0, 0xAA, 0xDB,
  0xF8, 0x80, 0x91, 0x68, 0xD3, 0x36, 0x70, 0x40, 0xF5, 0x5F, 0xC5, 0x89, 0x34, 0x6D, 0xEC, 0x20,
  0x3E, 0x51, 0xF4, 0xD8, 0x05, 0x46, 0xFA, 0x6A, 0x67, 0x68, 0x0E, 0x98, 0x30, 0x2F, 0x2D, 0xA1,
  0x51, 0x8C, 0xBA, 0x38, 0xAD, 0x77, 0x01, 0xD3, 0x10, 0x60, 0xE0, 0xE7, 0xA0, 0x0A, 0xD3, 0x1A,
  0x49, 0x4B, 0x00, 0x1E, 0x66, 0x71, 0x7A, 0x3A, 0x18, 0x1B, 0xE1, 0x0B, 0x2F, 0x21, 0x65, 0xC2,
  0xB4, 0xEC, 0x2D, 0x00, 0x87, 0x28, 0xF4, 0x08, 0xB7, 0x47, 0x8C, 0x34, 0xFA, 0xF4, 0x09, 0xC2,
  0x92, 0xF0, 0x74, 0x64, 0x6F, 0xBB, 0xC7, 0xB8, 0x0B, 0x60, 0xF4, 0xE9, 0xB9, 0x5E, 0x72, 0x20,
  0xB5, 0x10, 0x3E, 0x0A, 0xD1, 0x62, 0x17, 0xC0, 0xC1, 0x97, 0x7F, 0x07, 0x94, 0x78, 0x3C, 0x78,
  0x37, 0x20, 0x81, 0xFA, 0x07, 0x08, 0x86, 0x4C, 0x61, 0x09, 0x06, 0x0E, 0xC3, 0x8A, 0xD3, 0x66,
  0x42, 0xFD, 0xFD, 0x1D, 0x80, 0x5C, 0xAA, 0x46, 0x8F, 0x13, 0x20, 0x57, 0x60, 0x37, 0x3C, 0x12,
  0x85, 0xFA, 0x3B, 0xD4, 0x12, 0xDB, 0x9F, 0x1F, 0xEF, 0xFE, 0x6F, 0x89, 0x44, 0x17, 0xE0, 0xD7,
  0x05, 0xA0, 0xFE, 0xE1, 0x25, 0x63, 0xC3, 0x20, 0xB7, 0x2A, 0x3D, 0xA1, 0x61, 0xD0, 0x05, 0x6C,
  0x5F, 0x77, 0x74, 0x56, 0xFF, 0x11, 0xC8, 0xA2, 0xC3, 0xB6, 0x34, 0x3D, 0x2F, 0x65, 0xE0, 0x30,
  0x1C, 0x32, 0x12, 0xAD, 0xD6, 0x05, 0x4C, 0x04, 0x1C, 0xFD, 0xE0, 0xCB, 0xA5, 0xFE, 0xFC, 0xF8,
  0xD5, 0x9D, 0x05, 0x52, 0xB9, 0x94, 0x82, 0x69, 0x17, 0x50, 0xB8, 0xED, 0x21, 0xB1, 0x05, 0x42,
  0xF9, 0xDB, 0x81, 0x5A, 0x5D, 0x00, 0xEA, 0x0F, 0x02, 0xB0, 0x08, 0xC3, 0xB5, 0xBA, 0x80, 0x45,
  0xFD, 0xB1, 0x83, 0xA1, 0x04, 0xC0, 0xE2, 0x45, 0x75, 0x81, 0x92, 0xEA, 0x6F, 0xBB, 0xE9, 0x0D,
  0xF6, 0xC7, 0x71, 0x4F, 0xBB, 0xEA, 0x00, 0x89, 0x55, 0x58, 0x70, 0xBE, 0x3D, 0x02, 0xEA, 0x0F,
  0x0B, 0x94, 0x4D, 0xCD, 0x53, 0x83, 0x12, 0xEA, 0x0F, 0x02, 0xB4, 0x4A, 0x9C, 0x10, 0x30, 0x29,
  0x10, 0x1D, 0x04, 0xE8, 0xA6, 0x18, 0x86, 0x61, 0xDC, 0xF6, 0x00, 0x02, 0xB0, 0x0F, 0xC3, 0xD9,
  0xBA, 0x00, 0xEE, 0xDE, 0x05, 0x01, 0x4A, 0x17, 0xA7, 0x91, 0x28, 0x0E, 0xBE, 0x40, 0x80, 0x71,
  0xBB, 0x00, 0x0E, 0xBE, 0x0A, 0x10, 0xA0, 0xE5, 0x16, 0x9B, 0xE2, 0xA6, 0x30, 0xC6, 0x5D, 0x40,
  0x31, 0x1B, 0x7D, 0x76, 0xF1, 0xA8, 0x48, 0x74, 0x80, 0xBC, 0x64, 0x54, 0x39, 0x37, 0x0F, 0xE1,
  0x17, 0x16, 0xA8, 0x98, 0x22, 0x85, 0xA8, 0x6C, 0xC2, 0x2E, 0xA0, 0x70, 0xF0, 0x05, 0x02, 0x04,
  0x55, 0x42, 0x7F, 0xEC, 0x6D, 0x83, 0x52, 0xCD, 0xCB, 0xA1, 0xFE, 0x19, 0x09, 0x30, 0xD8, 0x43,
  0xB0, 0x6A, 0x74, 0x81, 0xD8, 0xCE, 0x04, 0xF5, 0xCF, 0x54, 0x17, 0x0F, 0x97, 0x6E, 0xAF, 0x49,
  0x45, 0xD5, 0xEB, 0x02, 0x50, 0xFF, 0xE4, 0xB9, 0x0C, 0x21, 0xB8, 0xA5, 0x2E, 0x00, 0x41, 0xCA,
  0x6C, 0x81, 0x6C, 0xCF, 0x4E, 0x6F, 0xA9, 0x0A, 0x8E, 0x6F, 0x45, 0xC1, 0x6B, 0x52, 0x16, 0x22,
  0x42, 0xFD, 0x3D, 0x4B, 0xF7, 0x08, 0x50, 0x53, 0x07, 0xC0, 0xE3, 0x12, 0x13, 0x06, 0xEB, 0xD0,
  0x91, 0xA8, 0x49, 0xFD, 0x71, 0xF0, 0x15, 0x96, 0xA5, 0xB4, 0x1D, 0x00, 0x41, 0xAA, 0x98, 0x0D,
  0x4A, 0xA9, 0xFE, 0xD8, 0x84, 0x44, 0xFB, 0x28, 0x2B, 0xDB, 0x89, 0xD1, 0xC2, 0x70, 0x74, 0x67,
  0x45, 0xF8, 0x4D, 0x6B, 0x91, 0x25, 0x16, 0x93, 0x65, 0x17, 0xC0, 0xE8, 0xB3, 0x90, 0x90, 0x49,
  0x22, 0xFD, 0x23, 0xE3, 0x50, 0xF5, 0xBA, 0x40, 0x0B, 0xA3, 0xCF, 0xD6, 0x5C, 0x82, 0xE9, 0x11,
  0xA0, 0xD2, 0x25, 0x2D, 0xA3, 0x8A, 0x02, 0x07, 0xEA, 0x9F, 0xA1, 0x4C, 0x98, 0x96, 0x96, 0xA9,
  0x82, 0x6A, 0x99, 0xE1, 0x85, 0x6C, 0x90, 0x08, 0xFC, 0x9C, 0xAA, 0x65, 0xF5, 0x6F, 0xB0, 0x94,
  0x61, 0x3D, 0xFF, 0x27, 0x80, 0xED, 0x2D, 0x7A, 0x23, 0x7A, 0xF5, 0x80, 0x50, 0x9A, 0xAA, 0x0B,
  0x28, 0xD8, 0xCF, 0x32, 0x5D, 0x77, 0xC2, 0xC5, 0x64, 0x81, 0x44, 0x6F, 0x0C, 0xE7, 0x4C, 0x30,
  0x13, 0xC8, 0x6D, 0xA2, 0x83, 0x83, 0xAF, 0xF4, 0x19, 0xEE, 0xAF, 0x0C, 0x80, 0x20, 0x5C, 0x2E,
  0x0C, 0x1F, 0xC0, 0xAE, 0x5C, 0xD5, 0x1F, 0x07, 0x5F, 0x79, 0x02, 0xB0, 0x31, 0x04, 0xCF, 0x42,
  0x03, 0x4E, 0x84, 0x0B, 0x75, 0x81, 0x15, 0xF5, 0xC7, 0x22, 0x47, 0xB8, 0x03, 0xDB, 0x50, 0x47,
  0xDA, 0x54, 0x06, 0x07, 0x62, 0x79, 0xBB, 0xC0, 0x4C, 0x60, 0xD6, 0xD4, 0x1F, 0xF6, 0x27, 0xA1,
  0xFF, 0x9F, 0xEF, 0x95, 0xB4, 0xA9, 0x4C, 0x2B, 0x36, 0xA8, 0x55, 0xA2, 0x4E, 0x9F, 0x1B, 0xEA,
  0x5F, 0xCE, 0xFE, 0x2C, 0xD7, 0x54, 0x2E, 0x43, 0x81, 0xC6, 0x06, 0xA1, 0xDC, 0x6C, 0x50, 0xF0,
  0x48, 0x14, 0xEA, 0x9F, 0xAF, 0x0C, 0x18, 0x16, 0xDA, 0x0C, 0x70, 0xFB, 0xFC, 0x64, 0x6B, 0xD3,
  0xA8, 0x95, 0x0A, 0xB1, 0x41, 0x6F, 0x2F, 0x77, 0x50, 0xFF, 0x8C, 0xFE, 0x7F, 0xF9, 0x83, 0x25,
  0xC6, 0x57, 0xBF, 0x10, 0x83, 0x1C, 0x90, 0x37, 0x0C, 0xAF, 0x90, 0x09, 0xEA, 0x9F, 0xD9, 0x1A,
  0xCB, 0xB5, 0x36, 0xDE, 0x42, 0x0E, 0x60, 0xF4, 0x19, 0x45, 0xAA, 0x91, 0x25, 0xD4, 0x3F, 0x9B,
  0xFF, 0x17, 0xD6, 0x0E, 0xB0, 0x3C, 0x0F, 0xC0, 0x38, 0xB4, 0x1A, 0x70, 0xA1, 0xFE, 0x91, 0xF6,
  0x67, 0xE9, 0xFF, 0x75, 0x0F, 0x80, 0x70, 0x7A, 0x4D, 0x2A, 0x73, 0x1B, 0xC4, 0x8D, 0x9C, 0xD1,
  0x5D, 0x00, 0x07, 0x5F, 0x79, 0xEC, 0x8F, 0xF6, 0x35, 0xA9, 0x6B, 0x2D, 0x82, 0x88, 0xE8, 0xE7,
  0xBF, 0xDF, 0xB0, 0xA2, 0x65, 0xBB, 0x08, 0xD4, 0x3F, 0xB2, 0x74, 0x98, 0xD5, 0xAD, 0xAB, 0x36,
  0x04, 0x1B, 0x9E, 0x15, 0xA4, 0x5A, 0x61, 0x7A, 0xCB, 0x00, 0x6E, 0x45, 0xFD, 0x99, 0x67, 0x43,
  0xE5, 0x88, 0x69, 0x3D, 0x01, 0x74, 0x9B, 0x60, 0x1B, 0xD7, 0xA1, 0xD2, 0x01, 0x19, 0xE1, 0x37,
  0xBE, 0x74, 0x58, 0x35, 0xED, 0x87, 0x74, 0x55, 0x30, 0x1C, 0x8A, 0xE5, 0x07, 0x72, 0x83, 0xA3,
  0x4F, 0x96, 0xAE, 0x40, 0x87, 0x55, 0x53, 0x57, 0x36, 0x9E, 0x03, 0xE8, 0x0E, 0xC5, 0x4A, 0xBE,
  0x05, 0xB1, 0x83, 0x56, 0xEC, 0x1D, 0x86, 0x6B, 0xAA, 0x7F, 0x2F, 0xE7, 0x3D, 0x3A, 0x8C, 0x1A,
  0x0E, 0x78, 0xED, 0x04, 0xD0, 0x6D, 0x06, 0xC3, 0x30, 0xAC, 0x38, 0x6F, 0xAC, 0x0F, 0xA0, 0x5B,
  0x3C, 0xF8, 0xE2, 0x48, 0x1A, 0x43, 0xF8, 0xF5, 0x27, 0x00, 0x11, 0x09, 0x5D, 0x70, 0xE0, 0xD4,
  0x05, 0x1A, 0x50, 0x2D, 0x91, 0x83, 0x2C, 0x28, 0x77, 0xF5, 0x3F, 0x60, 0x58, 0x84, 0x10, 0x40,
  0x1B, 0x1C, 0x18, 0x59, 0x0E, 0xA7, 0xAF, 0x0F, 0xE6, 0x7E, 0x69, 0x5D, 0xC2, 0x30, 0x2C, 0xB8,
  0xAF, 0x25, 0xB7, 0xB5, 0x75, 0xC1, 0xE6, 0xDA, 0xFA, 0xCB, 0x15, 0x55, 0x32, 0x85, 0xE1, 0xDA,
  0x17, 0xAD, 0x7C, 0xA6, 0x52, 0x87, 0xBF, 0x5B, 0xE5, 0x33, 0xBB, 0x28, 0x7B, 0xED, 0xD1, 0x67,
  0x4C, 0x27, 0x65, 0xD4, 0x85, 0x95, 0x4F, 0xF8, 0x75, 0x22, 0x80, 0x29, 0x40, 0xD4, 0x1C, 0x89,
  0xEE, 0x77, 0x5B, 0x15, 0xF2, 0xFB, 0xDF, 0x5E, 0xEE, 0xA6, 0x16, 0x59, 0x9A, 0x08, 0xAB, 0x61,
  0xB8, 0xE6, 0xC1, 0xD7, 0x7E, 0xB7, 0x8D, 0xFA, 0x22, 0xFE, 0xE7, 0xC7, 0x7B, 0xAD, 0x75, 0x5D,
  0xC5, 0xA4, 0x2D, 0xFC, 0x1E, 0x37, 0x47, 0xA9, 0xF5, 0xCF, 0xFD, 0xFD, 0xCB, 0xE6, 0xE4, 0x2F,
  0x7D, 0xFD, 0xFD, 0x51, 0xB2, 0x6D, 0xAB, 0xFD, 0x6E, 0x9B, 0xCC, 0x7E, 0x4D, 0x80, 0x3C, 0xA8,
  0x73, 0x89, 0x6B, 0x30, 0x92, 0xF6, 0x6C, 0x73, 0x55, 0x83, 0x00, 0x49, 0xD7, 0x73, 0x71, 0x2D,
  0x35, 0x2C, 0x9D, 0xFA, 0xFE, 0x65, 0x43, 0x1A, 0x8C, 0x8A, 0x24, 0x04, 0xD8, 0xEF, 0xB6, 0x6A,
  0x99, 0xAE, 0x2F, 0x1E, 0x2E, 0xE9, 0xFA, 0xF1, 0x55, 0xE4, 0x50, 0xA4, 0xD2, 0x99, 0x63, 0xA9,
  0xD0, 0x39, 0x88, 0x61, 0x52, 0xDA, 0xEB, 0xC7, 0xD7, 0x9C, 0x60, 0x51, 0x4B, 0x8B, 0x52, 0x2A,
  0xC3, 0x19, 0xD6, 0x34, 0xCB, 0xB5, 0xBE, 0xBD, 0xDC, 0x9D, 0xD8, 0x9F, 0xDB, 0xE7, 0x27, 0x27,
  0x61, 0x71, 0x22, 0x40, 0xC1, 0x2E, 0xA0, 0x38, 0x9C, 0x38, 0x67, 0x52, 0xE5, 0x93, 0x6B, 0xCB,
  0xAC, 0xFE, 0x8A, 0xE3, 0xE9, 0x7D, 0x06, 0xC2, 0x07, 0xAB, 0xBF, 0x53, 0x06, 0xE0, 0x9A, 0x05,
  0x1A, 0x2C, 0x11, 0x12, 0x90, 0x51, 0x79, 0xBC, 0xBF, 0x37, 0x01, 0x4C, 0x13, 0x21, 0x8E, 0xA7,
  0xC3, 0x5C, 0x6B, 0x6E, 0x0B, 0xF0, 0x8D, 0xAF, 0x34, 0xB6, 0x32, 0x64, 0xF2, 0x13, 0x64, 0x81,
  0x4C, 0x59, 0xC0, 0xA7, 0xDD, 0x84, 0x64, 0x80, 0x1A, 0x55, 0x30, 0x1C, 0x67, 0x0F, 0xBA, 0xDC,
  0x2A, 0xA5, 0xE5, 0xD3, 0xD9, 0x72, 0x57, 0xEF, 0x1F, 0x44, 0x80, 0xD8, 0xC0, 0x81, 0x42, 0xA5,
  0x14, 0xCA, 0x14, 0x83, 0x19, 0x6F, 0x02, 0x58, 0x42, 0x07, 0x5A, 0x3A, 0xAA, 0x58, 0x77, 0x4B,
  0x85, 0xC1, 0x90, 0xD7, 0xA4, 0x6A, 0xEF, 0x11, 0x42, 0x20, 0x46, 0xD5, 0x0C, 0xBE, 0x6B, 0xF7,
  0xFC, 0xA4, 0x24, 0xC0, 0x34, 0xCA, 0x42, 0x20, 0x46, 0xB1, 0x09, 0xBE, 0x3A, 0x4C, 0x66, 0x23,
  0x00, 0x11, 0x09, 0xDD, 0xA8, 0xE9, 0xE0, 0xC9, 0x40, 0x02, 0x54, 0x36, 0xEB, 0xA3, 0x1B, 0xC2,
  0x1C, 0xB0, 0x28, 0x4A, 0x12, 0x80, 0xCE, 0x6F, 0xEE, 0x61, 0x85, 0x50, 0x2C, 0xAC, 0x4F, 0xCC,
  0x00, 0x46, 0xC6, 0x7C, 0x20, 0x58, 0x21, 0x54, 0xAB, 0xD6, 0x27, 0x09, 0x01, 0x60, 0x85, 0x50,
  0xAD, 0x5A, 0x9F, 0x54, 0x04, 0x30, 0x5A, 0x21, 0xDD, 0x98, 0x0A, 0x85, 0x0A, 0x29, 0x1D, 0x96,
  0x62, 0xAD, 0x4F, 0x32, 0x02, 0x1C, 0xDA, 0x90, 0x30, 0x78, 0x36, 0x74, 0x01, 0x54, 0xAC, 0xEF,
  0x57, 0x3E, 0x98, 0xAB, 0x42, 0x00, 0xA2, 0xE3, 0x21, 0x04, 0xF2, 0x00, 0x2A, 0xBB, 0xEF, 0xD7,
  0x61, 0xAD, 0x3A, 0x01, 0x6C, 0x79, 0x00, 0x24, 0x40, 0x85, 0x80, 0x3F, 0x97, 0xEF, 0xFF, 0x0B,
  0xB4, 0x01, 0xB7, 0x42, 0xAC, 0xB6, 0x2C, 0x0B, 0x6B, 0x71, 0xAB, 0x04, 0xCA, 0x29, 0xF4, 0x9A,
  0x7C, 0x7F, 0xEA, 0x2F, 0x61, 0x25, 0x27, 0xC0, 0x21, 0xB4, 0x28, 0x83, 0x4D, 0x02, 0x01, 0x50,
  0xAC, 0xF0, 0x23, 0x73, 0x5C, 0x80, 0xC9, 0xA3, 0x21, 0x14, 0xA3, 0x42, 0x43, 0x6F, 0x4A, 0xDF,
  0x9F, 0x9D, 0x00, 0x44, 0x24, 0x4C, 0xA1, 0x18, 0x24, 0x40, 0x71, 0xB2, 0xCF, 0x32, 0xE3, 0xF5,
  0x68, 0x43, 0x31, 0x48, 0x80, 0xF2, 0x01, 0x7F, 0xEA, 0xD0, 0x5B, 0x92, 0x00, 0x74, 0x7E, 0x73,
  0x0F, 0x12, 0xA0, 0xA2, 0xC0, 0x9F, 0xFB, 0x8B, 0x56, 0x32, 0xF7, 0xC5, 0x81, 0x04, 0x28, 0xAE,
  0xE0, 0x2F, 0x42, 0x00, 0x90, 0x00, 0xC5, 0x15, 0xFC, 0xC5, 0x08, 0x00, 0x12, 0xA0, 0x38, 0x82,
  0xBF, 0x28, 0x01, 0x40, 0x02, 0x14, 0x37, 0xF0, 0x13, 0x65, 0x3A, 0x08, 0x5B, 0x2B, 0xD3, 0x31,
  0x37, 0x11, 0x4E, 0x8C, 0x3B, 0x2F, 0x65, 0xBA, 0x4B, 0xB8, 0xD6, 0x93, 0x45, 0x64, 0x8D, 0x55,
  0x30, 0x75, 0x02, 0xA2, 0xFF, 0x6F, 0x7D, 0xC5, 0xBD, 0x43, 0xFD, 0xD5, 0x7E, 0xB7, 0x65, 0x07,
  0xFE, 0x6A, 0x04, 0x58, 0x23, 0x01, 0x6E, 0xA0, 0xEB, 0x0F, 0xFC, 0xA6, 0x8E, 0x5F, 0xFB, 0x99,
  0x52, 0x55, 0x2C, 0x90, 0xEB, 0xE2, 0xE4, 0x7A, 0x02, 0x35, 0xAA, 0xBE, 0xDF, 0xE7, 0x00, 0x7E,
  0x16, 0x04, 0x58, 0xF3, 0x86, 0xC8, 0x05, 0xFD, 0xF9, 0x7D, 0x4E, 0x7B, 0x2A, 0x99, 0x2C, 0x96,
  0xB0, 0xDD, 0xEC, 0x84, 0x5C, 0xD0, 0x8F, 0xDF, 0xE7, 0x26, 0x68, 0x5C, 0x3A, 0x80, 0x53, 0xCB,
  0x3C, 0x58, 0x22, 0x74, 0x03, 0xC6, 0xAA, 0xFF, 0xF6, 0x72, 0x47, 0x2B, 0xFB, 0xC7, 0x6A, 0xEF,
  0xD8, 0x11, 0x60, 0x2D, 0x17, 0x70, 0xF1, 0x8E, 0xA8, 0x3E, 0xF6, 0x8C, 0x25, 0x01, 0x5C, 0x3C,
  0x24, 0xBA, 0x41, 0x1B, 0xAA, 0xCF, 0x3D, 0xC3, 0x71, 0x26, 0xC0, 0xAA, 0x25, 0x42, 0x37, 0xE0,
  0xAD, 0xFA, 0x2D, 0x4C, 0xF1, 0xD8, 0x13, 0xC0, 0x65, 0xA1, 0x41, 0x04, 0xEC, 0x47, 0xD7, 0x04,
  0x70, 0x6D, 0xB5, 0x17, 0x0F, 0x97, 0xB5, 0x5E, 0x3B, 0x3A, 0x0C, 0xF0, 0x3F, 0x3F, 0xDE, 0x57,
  0xF7, 0xA0, 0x25, 0x6B, 0xDA, 0x12, 0x01, 0x9C, 0xD5, 0x07, 0xF9, 0xA0, 0xBC, 0xF8, 0xB4, 0xDA,
  0x85, 0x9B, 0x23, 0x80, 0xCF, 0x86, 0x80, 0x08, 0x58, 0xE7, 0x5E, 0x09, 0xE0, 0xBD, 0x41, 0xB0,
  0x46, 0x69, 0xAD, 0x4E, 0x2F, 0x02, 0xD3, 0x3A, 0x01, 0x9C, 0x6D, 0xD1, 0xA2, 0x4D, 0xA3, 0x2B,
  0x68, 0xC4, 0x64, 0xBF, 0xDB, 0x92, 0xE7, 0x3A, 0x36, 0xBF, 0x86, 0x5D, 0x10, 0x20, 0x84, 0x08,
  0xB0, 0x47, 0x7E, 0x5D, 0xB4, 0x37, 0xE0, 0x77, 0x49, 0x80, 0x10, 0x22, 0x0C, 0xDA, 0x15, 0xBC,
  0xD4, 0xBE, 0x47, 0xE0, 0x77, 0x4D, 0x80, 0x50, 0x22, 0xCC, 0xB2, 0x42, 0x8F, 0x64, 0x50, 0xFB,
  0xDD, 0x96, 0x5C, 0xBC, 0xFD, 0x08, 0xC0, 0x1F, 0x82, 0x00, 0xBE, 0xA1, 0xAE, 0x33, 0x42, 0x04,
  0x01, 0x7E, 0xB4, 0xA1, 0xC1, 0x10, 0x04, 0x88, 0x69, 0xFD, 0x73, 0x50, 0x10, 0x11, 0x67, 0x42,
  0x1C, 0x01, 0x4F, 0x44, 0xDE, 0xA0, 0x1F, 0x75, 0x40, 0x30, 0x1A, 0x01, 0xA2, 0xBB, 0x82, 0x4E,
  0x29, 0x89, 0x68, 0x02, 0x4E, 0x09, 0xF0, 0xA8, 0xC3, 0xE7, 0x27, 0x22, 0xA2, 0x54, 0xD7, 0x30,
  0xEA, 0x88, 0x78, 0x58, 0x02, 0xA4, 0x26, 0x83, 0xA9, 0x5B, 0x2C, 0x6B, 0x46, 0x14, 0xD3, 0x67,
  0x39, 0xF9, 0x59, 0x8C, 0xAA, 0x03, 0xF4, 0x20, 0x80, 0x37, 0x19, 0x88, 0x28, 0xC8, 0x26, 0x71,
  0xAE, 0xE9, 0x01, 0x04, 0x00, 0x3D, 0x08, 0x50, 0xD4, 0x57, 0xD7, 0xA8, 0x06, 0xF2, 0x0A, 0x08,
  0x00, 0x52, 0x00, 0xEC, 0x20, 0x40, 0x47, 0xA4, 0x58, 0x06, 0xD3, 0xA9, 0x62, 0x09, 0x32, 0x7F,
  0x07, 0x73, 0x85, 0xC0, 0xDD, 0x7D, 0xFD, 0x07, 0x6E, 0xB2, 0xE3, 0x39, 0x86, 0x85, 0x44, 0x23,
  0x00, 0x00, 0x00, 0x00, 0x49, 0x45, 0x4E, 0x44, 0xAE, 0x42, 0x60, 0x82,
};

const unsigned int icon_png_len = 3484;

const uint8_t bitmaps[] ={
  0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 
	0x00, 0x00, 0x09, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x7d, 0xf0, 
	0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x39, 0xc0, 0x1e, 0x00, 0x00, 0x00, 0x00, 
	0x00, 0x00, 0x00, 0x00, 0x00, 0x27, 0x9f, 0x01, 0xf0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 
	0x0f, 0x3f, 0xc1, 0x8c, 0x0c, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x43, 0xc8, 0xc8, 0x30, 0x72, 
	0xde, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x24, 0x20, 0x10, 0x0f, 0xdb, 0x03, 0x80, 0x00, 0x00, 
	0x00, 0x00, 0x01, 0x08, 0x00, 0x10, 0x00, 0xd0, 0xf9, 0x80, 0x00, 0x00, 0x00, 0x00, 0x01, 0x30, 
	0x00, 0x10, 0x00, 0x26, 0x05, 0xc0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0x00, 0x10, 0x00, 0x30, 
	0x37, 0x20, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xf8, 0x33, 0xe0, 0x1c, 0x00, 0xe0, 0x00, 0x00, 
	0x00, 0x00, 0x00, 0x03, 0xc6, 0xec, 0x30, 0x13, 0xcd, 0xe0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x06, 
	0x33, 0x8b, 0xf0, 0x18, 0x17, 0x20, 0x00, 0x00, 0x00, 0x00, 0x00, 0x03, 0xbf, 0x7f, 0x80, 0x37, 
	0xe0, 0x60, 0x00, 0x00, 0x00, 0x00, 0x00, 0x03, 0x9f, 0xdd, 0x00, 0x24, 0x0b, 0x20, 0x00, 0x00, 
	0x00, 0x00, 0x00, 0x00, 0x1b, 0x0b, 0x00, 0x23, 0x78, 0x60, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 
	0x8d, 0x0e, 0x00, 0x18, 0x07, 0xc0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xf0, 0x82, 0x00, 0x1b, 
	0xa4, 0xe0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x20, 0xf8, 0x00, 0x08, 0x2f, 0xf0, 0x00, 0x00, 
	0x00, 0x00, 0x00, 0x00, 0xc0, 0x00, 0x00, 0x0f, 0xec, 0xff, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 
	0x80, 0x00, 0x00, 0x08, 0x3e, 0x7c, 0xc0, 0x00, 0x00, 0x00, 0x00, 0x03, 0x00, 0x00, 0x00, 0x0f, 
	0xe0, 0x7c, 0x20, 0x00, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x07, 0xd9, 0x8e, 0x30, 0x00, 
	0x00, 0x00, 0x00, 0x08, 0x00, 0x00, 0x00, 0x03, 0x4f, 0x0e, 0x10, 0x00, 0x00, 0x00, 0x00, 0x08, 
	0x00, 0x00, 0x00, 0x00, 0x6e, 0x0d, 0x08, 0x00, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x00, 
	0xfe, 0x05, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xf2, 0x04, 0x08, 0x00, 
	0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, 0x00, 0xec, 0x08, 0x0c, 0x00, 0x00, 0x00, 0x00, 0x40, 
	0x00, 0x00, 0x00, 0x01, 0xbc, 0x08, 0x08, 0x00, 0x00, 0x00, 0x00, 0x40, 0x00, 0x00, 0x00, 0x01, 
	0xc8, 0x18, 0x08, 0x00, 0x00, 0x00, 0x00, 0x80, 0x00, 0x00, 0x00, 0x01, 0x98, 0x30, 0x18, 0x00, 
	0x00, 0x00, 0x00, 0x80, 0x00, 0x00, 0x00, 0x01, 0xf0, 0xe0, 0x10, 0x00, 0x00, 0x00, 0x01, 0x00, 
	0x00, 0x00, 0x00, 0x01, 0x20, 0xe4, 0x20, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x01, 
	0x60, 0xf9, 0x20, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x40, 0x01, 0x80, 0x3b, 0xc0, 0x00, 
	0x00, 0x00, 0x02, 0x00, 0x24, 0x00, 0x40, 0x01, 0x00, 0x3f, 0x80, 0x00, 0x00, 0x00, 0x02, 0x00, 
	0x3f, 0x00, 0xc8, 0x03, 0x00, 0x0e, 0x00, 0x00, 0x00, 0x00, 0x02, 0x00, 0x80, 0x80, 0xfe, 0x00, 
	0x00, 0x08, 0x00, 0x00, 0x00, 0x00, 0x02, 0x03, 0xff, 0x80, 0x03, 0x03, 0x00, 0x00, 0x00, 0x00, 
	0x00, 0x00, 0x01, 0x9b, 0x70, 0x00, 0x01, 0x81, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x60, 
	0x60, 0x00, 0x00, 0x82, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xe0, 0x00, 0x00, 0x82, 
	0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xc0, 0x00, 0x00, 0xc2, 0x00, 0x00, 0x00, 0x00, 
	0x00, 0x00, 0x00, 0x01, 0xc0, 0x00, 0x00, 0x42, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 
	0x80, 0x00, 0x01, 0xc2, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x03, 0x00, 0x00, 0x01, 0x42, 
	0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0x80, 0x00, 0x06, 0x42, 0x00, 0x00, 0x00, 0x00, 
	0x00, 0x00, 0x00, 0x02, 0x80, 0x00, 0x04, 0x42, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x05, 
	0x00, 0x00, 0x18, 0xc6, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x05, 0x00, 0x00, 0x70, 0x82, 
	0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x07, 0x00, 0x00, 0xc0, 0x82, 0x00, 0x00, 0x00, 0x00, 
	0x00, 0x00, 0x00, 0x06, 0x00, 0x03, 0x00, 0x06, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0a, 
	0x00, 0x0a, 0x00, 0x04, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0a, 0x00, 0x18, 0x00, 0x04, 
	0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x1a, 0x00, 0x60, 0x00, 0x06, 0x00, 0x00, 0x00, 0x00, 
	0x00, 0x00, 0x00, 0x12, 0x03, 0x80, 0x00, 0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x12, 
	0x1c, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 0x00, 0x00, 0x30, 0x00, 0x10, 0xf0, 0x00, 0x00, 0x04, 
	0x00, 0x00, 0x00, 0x00, 0x00, 0x20, 0x00, 0x37, 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 0x00, 
	0x00, 0xc0, 0x03, 0xf0, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0x03, 0x00, 
	0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0x03, 0x10, 0x00, 0x00, 0x00, 0x04, 
	0x00, 0x00, 0x00, 0x00, 0x00, 0xe0, 0x00, 0xf0, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x00, 
	0x00, 0x30, 0x00, 0x27, 0x80, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 0x00, 0x00, 0x30, 0x00, 0x7f, 
	0xcc, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 0x00, 0x00, 0x40, 0x00, 0xff, 0xff, 0xc0, 0x00, 0x06, 
	0x00, 0x00, 0x00, 0x00, 0x00, 0xc0, 0x01, 0xff, 0x00, 0x78, 0x00, 0x02, 0x00, 0x00, 0x00, 0x00, 
	0x00, 0x80, 0x01, 0xfb, 0x00, 0x0e, 0x00, 0x02, 0x00, 0x00, 0x00, 0x00, 0x01, 0x80, 0x01, 0x5b, 
	0x00, 0x02, 0x00, 0x06, 0x00, 0x00, 0x00, 0x00, 0x00, 0xc0, 0x03, 0x7b, 0x00, 0x03, 0x00, 0x02, 
	0x00, 0x00, 0x00, 0x00, 0x00, 0x60, 0x02, 0x8a, 0x00, 0x01, 0x00, 0x02, 0x00, 0x00, 0x00, 0x00, 
	0x00, 0x20, 0x02, 0xba, 0x00, 0x01, 0x00, 0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x20, 0x02, 0xb2, 
	0x00, 0x01, 0x00, 0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x60, 0x00, 0x8e, 0x00, 0x01, 0x00, 0x02, 
	0x00, 0x00, 0x00, 0x00, 0x00, 0x40, 0x00, 0x40, 0x00, 0x01, 0x80, 0x02, 0x00, 0x00, 0x00, 0x00, 
	0x00, 0xc0, 0x00, 0x00, 0x00, 0x01, 0x00, 0x03, 0x00, 0x00, 0x00, 0x00, 0x01, 0x80, 0x00, 0x00, 
	0x00, 0x01, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x01, 
	0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0x00, 0x00, 0x00, 0x01, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 
	0x00, 0x80, 0x00, 0x00, 0x00, 0x03, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0xc0, 0x00, 0x00, 
	0x00, 0x01, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x40, 0x00, 0x00, 0x00, 0x02, 0x00, 0x01, 
	0x80, 0x00, 0x00, 0x00, 0x00, 0x20, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x80, 0x00, 0x00, 0x00, 
	0x00, 0x20, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x80, 0x00, 0x00, 0x00, 0x00, 0x60, 0x01, 0x80, 
	0x00, 0x0e, 0x00, 0x00, 0xc0, 0x00, 0x00, 0x00, 0x00, 0x40, 0x19, 0x80, 0x00, 0x0c, 0x00, 0x00, 
	0xc0, 0x00, 0x00, 0x00, 0x01, 0x80, 0x1d, 0x80, 0x00, 0x18, 0x00, 0x00, 0xc0, 0x00, 0x00, 0x00, 
	0x01, 0x80, 0x36, 0x80, 0x00, 0x18, 0x00, 0x0f, 0xa0, 0x00, 0x00, 0x00, 0x01, 0x00, 0x26, 0xc0, 
	0x00, 0x3e, 0x44, 0xfe, 0x30, 0x00, 0x00, 0x00, 0x01, 0x00, 0x26, 0x40, 0x00, 0x23, 0xff, 0x80, 
	0x18, 0x00, 0x00, 0x00, 0x00, 0x80, 0x64, 0x40, 0x00, 0x60, 0x00, 0x00, 0x0c, 0x00, 0x00, 0x00, 
	0x00, 0xc0, 0x44, 0x40, 0x00, 0xc0, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 0x00, 0x68, 0x44, 0x40, 
	0x00, 0xc0, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x0f, 0xf8, 0x40, 0x40, 0x00, 0x80, 0x00, 0x00, 
	0x03, 0x00, 0x00, 0x00, 0x0d, 0x88, 0xc0, 0x40, 0x01, 0x80, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 
	0x3b, 0xe8, 0x40, 0x40, 0x01, 0x00, 0x00, 0x00, 0x01, 0x80, 0x00, 0x00, 0x68, 0x08, 0x40, 0xc0, 
	0x03, 0x00, 0x00, 0x00, 0x0c, 0x80, 0x00, 0x00, 0x7c, 0x18, 0xc0, 0x40, 0x03, 0x00, 0x00, 0x00, 
	0x08, 0xc0, 0x00, 0x00, 0x5c, 0x18, 0x45, 0xc0, 0x02, 0x00, 0x00, 0x00, 0x18, 0x40, 0x00, 0x00, 
	0x64, 0x10, 0x7f, 0xc4, 0x82, 0x00, 0x00, 0x00, 0x3a, 0x60, 0x00, 0x00, 0x1f, 0xf9, 0xff, 0xff, 
	0x86, 0x00, 0x00, 0x00, 0x6b, 0x80, 0x00, 0x00, 0x06, 0x1f, 0xf8, 0x01, 0x84, 0x00, 0x00, 0x00, 
	0xce, 0x00, 0x00, 0x00, 0x06, 0x78, 0x43, 0xff, 0xfd, 0x90, 0x00, 0x00, 0xde, 0x00, 0x00, 0x00, 
	0x02, 0x67, 0xfc, 0x08, 0x16, 0xff, 0xa0, 0x01, 0x9a, 0x00, 0x00, 0x00, 0x00, 0x7c, 0x00, 0x00, 
	0x00, 0x01, 0xff, 0xfb, 0x9a, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0x4f, 
	0xff, 0xec, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0x14, 0x0f, 0xe0, 0x00, 
	0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x7f, 0x00, 0x00, 0x00, 0x00, 0x00, 
	0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xf0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 
	0x00, 0x00, 0x00, 0x18, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00
  };

static void web_display_current_date() {
  DateTime now = rtc.now();

  web_display_date.year = now.year();
  web_display_date.month = now.month();
  web_display_date.day = now.day();
  web_display_date.hour = now.hour();
  web_display_date.minute = now.minute();
}

static void handle_icon() {
  server.send_P(200, "image/png", (const char*)icon_png, icon_png_len);
}

static bool web_auth_ok() {
  if (strlen(WEB_USERNAME) == 0 || strlen(WEB_PASSWORD) == 0) {
    return true;
  }

  if (server.authenticate(WEB_USERNAME, WEB_PASSWORD)) {
    return true;
  }

  server.requestAuthentication();
  return false;
}

static void send_status_json() {
  char answer[180];
  if (!web_auth_ok()) {
    return;
  }
  snprintf(answer, sizeof(answer),
    "{\"busy\":%s,\"pending\":%s,\"status\":\"%s\"}",
    web_busy ? "true" : "false",
    web_pending ? "true" : "false",
    web_status
  );

  server.send(200, "application/json", answer);
}

static void send_page() {
  if (!web_auth_ok()) {
    return;
  }

  DateTime now = rtc.now();
  Datum shown_date = web_display_date;

  if (web_pending || web_busy) {
    shown_date = web_target;
  }

  char current_date[16];
  char html_date[16];

  snprintf(current_date, sizeof(current_date),
    "%d-%s-%d", now.day(), month_names[now.month() - 1], now.year());

  snprintf(html_date, sizeof(html_date),
    "%04d-%02d-%02d", shown_date.year, shown_date.month, shown_date.day);

  static char bitmap_js[4500];
  int bmp_pos = 0;

  for (unsigned int i = 0; i < sizeof(bitmaps); i++) {
    bmp_pos += snprintf(bitmap_js + bmp_pos,
      sizeof(bitmap_js) - bmp_pos,
      "%u%s",
      bitmaps[i],
      (i + 1 < sizeof(bitmaps)) ? "," : "");
  }

  static char page[8000];

  snprintf(page, sizeof(page),
    "<!DOCTYPE html>"
    "<html>"
    "<head>"
    "<meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">"
    "<title>Meridian Orrery</title>"
    "<style>"
    "*{box-sizing:border-box;}"
    "@-moz-document url-prefix(){body{zoom:0.7;}}"
    "body{font-family:Helvetica,Arial,sans-serif;text-align:center;background:#d7edf3;color:#203238;font-size:22px;margin:0;padding:30px;}"
    ".card{max-width:520px;margin:auto;background:white;border-radius:18px;padding:25px;box-shadow:0 4px 20px rgba(0,0,0,0.12);}"
    "h1{font-size:34px;margin:10px 0 5px 0;}"
    "input{font-size:22px;padding:10px;border-radius:10px;border:1px solid #89a;width:240px;text-align:center;}"
    "button,.button{display:inline-block;margin:12px;padding:14px 22px;border-radius:10px;border:0;background:#7aa8b7;color:white;font-size:22px;text-decoration:none;}"    "button:disabled{opacity:0.5;}"
    ".secondary{background:#b8978d;}"
    ".small{font-size:14px;color:#50656b;}"
    "#logo{position:fixed;right:12px;bottom:12px;opacity:0.55;image-rendering:pixelated;}"
    "</style>"
    "<link rel=\"manifest\" href=\"/manifest.json\">"
    "<link rel=\"icon\" type=\"image/png\" href=\"/icon.png\">"
    "<link rel=\"apple-touch-icon\" href=\"/icon.png\">"
    "<meta name=\"theme-color\" content=\"#203238\">"
    "</head>"
    "<body>"
    "<div class=\"card\">"
    "<div style=\"display:flex;align-items:center;justify-content:center;gap:12px;\">"
    "<img src=\"/icon.png\" width=\"42\" height=\"42\">"
    "<h1 style=\"margin:0;\">Meridian Orrery</h1>"
    "</div>"
    "<p class=\"small\">by illusionmanager</p>"
    "<p><b>%s</b></p>"
    "<p><b id=\"status\">%s</b></p>"
    "<form action=\"/set\" method=\"get\">"
    "<p>Temporarily set date:</p>"
    "<input id=\"date_input\" type=\"date\" name=\"date\" value=\"%s\">"
    "<br>"
    "<button id=\"set_button\" type=\"submit\">Move orrery</button>"
    "</form>"
    "<a id=\"reset_button\" class=\"button secondary\" href=\"/reset\">Reset to current date</a>"
    "<p class=\"small\">The next daily update will always restore the current date.</p>"
    "</div>"
    "<canvas id=\"logo\" width=\"93\" height=\"112\"></canvas>"
    "<script>"
    "let last_ok=Date.now();"
    "async function updateStatus(){"
    "try{"
    "const r=await fetch('/status');"
    "const s=await r.json();"
    "last_ok=Date.now();"
    "document.getElementById('status').textContent=s.status;"
    "const disabled=s.busy||s.pending;"
    "document.getElementById('date_input').disabled=disabled;"
    "document.getElementById('set_button').disabled=disabled;"
    "const reset=document.getElementById('reset_button');"
    "reset.style.pointerEvents=disabled?'none':'auto';"
    "reset.style.opacity=disabled?'0.5':'1';"
    "}catch(e){"
    "if(Date.now()-last_ok>20000){"
    "document.getElementById('status').textContent='Connection lost';"
    "}"
    "}"
    "}"
    "const bmp=[%s];"
    "const c=document.getElementById('logo');"
    "const ctx=c.getContext('2d');"
    "ctx.fillStyle='#203238';"
    "for(let y=0;y<112;y++){"
    "for(let x=0;x<93;x++){"
    "const b=bmp[y*12+(x>>3)];"
    "if(b&(1<<(7-(x&7))))ctx.fillRect(x,y,1,1);"
    "}"
    "}"
    "setInterval(updateStatus,1000);"
    "updateStatus();"
    "</script>"
    "</body>"
    "</html>",
    current_date,
    web_status,
    html_date,
    bitmap_js
  );

  server.send(200, "text/html", page);

}

static void handle_root() {
  send_page();
}

static void handle_set_date() {
  if (!web_auth_ok()) {
    return;
  }
  if (web_busy || web_pending) {
    server.send(409, "text/plain", "Orrery is busy. Try again when it is ready.");
    return;
  }

  if (!server.hasArg("date")) {
    server.send(400, "text/plain", "No date received.");
    return;
  }

  char date_string[16];
  server.arg("date").toCharArray(date_string, sizeof(date_string));

  int year, month, day;

  if (sscanf(date_string, "%4d-%2d-%2d", &year, &month, &day) != 3) {
    
    server.send(400, "text/plain", "Bad date format.");
    return;
  }
  if (year <1500 || year > 2500 || month < 1 || month > 12 || day < 1 || day > 31) {
    server.send(400, "text/plain", "Date out of range.");
    return;
  }

  DateTime now = rtc.now();

  web_target.year = year;
  web_target.month = month;
  web_target.day = day;
  web_target.hour = now.hour();
  web_target.minute = now.minute();

  web_display_date = web_target;

  web_pending = true;

  server.sendHeader("Location", "/");
  server.send(303);
}


static void handle_manifest() {
  server.send(
    200,
    "application/json",
    "{"
    "\"name\":\"Meridian Orrery\","
    "\"short_name\":\"Orrery\","
    "\"display\":\"standalone\","
    "\"background_color\":\"#d7edf3\","
    "\"theme_color\":\"#203238\","
    "\"start_url\":\"/\","
    "\"icons\":[{"
      "\"src\":\"/icon.png\","
      "\"sizes\":\"192x192\","
      "\"type\":\"image/png\""
    "}]"
    "}"
  );
}
static void handle_reset() {
  if (!web_auth_ok()) {
    return;
  }
  if (web_busy || web_pending) {
    server.send(409, "text/plain", "Orrery is busy. Try again when it is ready.");
    return;
  }

  DateTime now = rtc.now();

  web_target.year = now.year();
  web_target.month = now.month();
  web_target.day = now.day();
  web_target.hour = now.hour();
  web_target.minute = now.minute();

  web_display_current_date();
  
  web_pending = true;

  server.sendHeader("Location", "/");
  server.send(303);
}

static void start_web_server() {
  WiFi.mode(WIFI_AP);
  WiFi.setSleep(false);
  WiFi.softAPConfig(ap_ip, ap_gateway, ap_subnet);
  WiFi.softAP(AP_NAME);

  Serial.print("Access point started: ");
  Serial.println(AP_NAME);
  Serial.print("Open: http://");
  Serial.println(WiFi.softAPIP());

  server.on("/", handle_root);
  server.on("/set", handle_set_date);
  server.on("/reset", handle_reset);
  server.on("/status", send_status_json);
  server.on("/icon.png", handle_icon);
  server.on("/manifest.json", handle_manifest);
  server.onNotFound(handle_root);
  server.begin();
}

static void handle_web_pending() {
  if (!web_pending || web_busy) {
    return;
  }

  web_pending = false;

  Datum target = web_target;

  do_solar_system_for(target);

}

#endif


void print_help() {
  Serial.println("Commands:");
  Serial.println("  h or ?                  help");
  Serial.println("  d                       print date and time");
  Serial.println("  dDD-MM-YYYY             set date");
  Serial.println("  dHH:MM                  set time");
  Serial.println("  dDD-MM-YYYY HH:MM       set date and time");
  Serial.println("  <number>                rotate motor in degrees for example 5 or -30");
  Serial.println("  r                       reset / go home");
  Serial.println("  p                       print planet angles");
  Serial.println("  s                       set all planets and moon");
}

void print_date_time() {
  DateTime now = rtc.now();

  Serial.printf("%d-%s-%d %02d:%02d:%02d\n",
    now.day(), month_names[now.month() - 1], now.year(),
    now.hour(), now.minute(), now.second());
}

void handle_date_command(const char *s) {
  int day, month, year, hour, minute;
  DateTime now = rtc.now();

  if (strcmp(s, "d") == 0) {
    print_date_time();
    return;
  }

  if (sscanf(s, "d%2d-%2d-%4d %2d:%2d", &day, &month, &year, &hour, &minute) == 5) {
    rtc.adjust(DateTime(year, month, day, hour, minute, 0));
    Serial.println("Date and time set");
    print_date_time();
    return;
  }

  if (sscanf(s, "d%2d-%2d-%4d", &day, &month, &year) == 3) {
    rtc.adjust(DateTime(year, month, day, now.hour(), now.minute(), now.second()));
    Serial.println("Date set");
    print_date_time();
    return;
  }

  if (sscanf(s, "d%2d:%2d", &hour, &minute) == 2) {
    rtc.adjust(DateTime(now.year(), now.month(), now.day(), hour, minute, 0));
    Serial.println("Time set");
    print_date_time();
    return;
  }

  Serial.println("Bad date/time format. Use h or ?");
}

void handle_rotate_command(const char *s) {
  float deg;
  char extra;

  if (sscanf(s, "%f %c", &deg, &extra) == 1) {
    enable_motor();
    rotate(deg);
    disable_motor();

    Serial.printf("Rotated %.1f degrees\n", deg);
    Serial.flush();
  } else {
    Serial.println("Bad rotation value");
  }
}

void handle_command(const char *s) {
  if (s[0] == 0) return;

  if (strcmp(s, "h") == 0 || strcmp(s, "?") == 0) {
    print_help();
    return;
  }

  if (strcmp(s, "r") == 0) {
    go_home(0);
    return;
  }

  if (strcmp(s, "p") == 0) {
    print_angles();
    return;
  }

  if (strcmp(s, "s") == 0) {
    do_solar_system();
    return;
  }

  if (s[0] == 'd') {
    handle_date_command(s);
    return;
  }

  if ((s[0] >= '0' && s[0] <= '9') || s[0] == '-' || s[0] == '+') {
    handle_rotate_command(s);
    return;
  }

  Serial.println("Unknown command. Use h or ?");
}

void setup() {
  Serial.begin(115200);
  delay(1500);

  Serial.println("Meridian Orrery, by illusionmanager (2026)");
  pinMode(SDA_PIN, INPUT_PULLUP);
  pinMode(SCL_PIN, INPUT_PULLUP);
  delay(100);
  Wire.begin(SDA_PIN, SCL_PIN);

  pinMode(STEP_PIN, OUTPUT);
  pinMode(DIR_PIN, OUTPUT);
  pinMode(ENABLE_PIN, OUTPUT);
  pinMode(TOUCH_PIN, INPUT_PULLDOWN);
  pinMode(MAGNET_PIN, INPUT_PULLDOWN);

  disable_motor();
  delay(1500);

  bool rtc_found = false;

  for (int i = 0; i < 10; i++) {
    if (rtc.begin()) {
      rtc_found = true;
      break;
    }

    Serial.println("RTC not found, retrying...");
    delay(1000);
  }

  if (!rtc_found) {
    Serial.println("Couldn't find RTC");
    Serial.flush();

    while (1) {
      delay(10);
    }
  }

  if (rtc.lostPower()) {
    Serial.println("RTC lost power, setting compile time");
    rtc.adjust(DateTime(F(__DATE__), F(__TIME__)));
  }

  compute_start_offsets();
  DateTime now = rtc.now();
#ifdef HAS_WEBSERVER
  web_display_current_date();
  start_web_server();
#endif

  Serial.println("Press h for help");

  prev_day = now.day();

  do_solar_system();

  delay(1000);
  print_date_time();
}

void loop() {
#ifdef HAS_WEBSERVER
  server.handleClient();
  handle_web_pending();
  web_poll_after_status();
#endif

  static uint32_t touch_start_time = 0;
  static bool touch_active = false;

  DateTime now = rtc.now();

  if (now.hour() == 12 && now.day() != prev_day) {
    do_solar_system();
#ifdef HAS_WEBSERVER
    web_display_current_date();
#endif
    prev_day = now.day();
  }

  if (Serial.available()) {
    char c = Serial.read();

    if (c == '\r' || c == '\n') {
      if (cmd_pos > 0) {
        cmd_buf[cmd_pos] = 0;
        handle_command(cmd_buf);
        cmd_pos = 0;
      }
    } else {
      if (cmd_pos < (int)sizeof(cmd_buf) - 1) {
        cmd_buf[cmd_pos++] = c;
      }
    }
  }

  bool touch_allowed =
      (last_rotate == 0) ||
      ((uint32_t)(millis() - last_rotate) >= 1000);

  bool pin_is_touched = false;

  if (touch_allowed) {
    pin_is_touched = (digitalRead(TOUCH_PIN) == HIGH);
  } else {
    touch_active = false;
  }

  if (pin_is_touched && !touch_active) {
    touch_start_time = millis();
    touch_active = true;
  }

  if (!pin_is_touched && touch_active) {
    uint32_t touch_duration = millis() - touch_start_time;

    if (touch_duration < 500) {
      do_solar_system();
      prev_day = now.day();
      delay(1000);
    }

    touch_active = false;
  }
}