Arduino IDE and ISO C++ declaration forbids

I have used an ESP8266 with this sketch for about 2 years.

https://github.com/apdlv72/VitoWifi

The ESP has to be replaced, unfortunately, because it is defective.

I use it to control my ventilation system via an Opentherm Gateway (Vitovent 300 / Brink Renovent)

The sketch can no longer be compiled. In the meantime, there is an ISO C ++ declaration check that leads to the termination.
In the past, the design still worked. Unfortunately I don't know the Arduino version number anymore. I currently have 1.8.13

here is part of the code and the error message ... can someone please help me? That would be great.

Code:

    OTMessage(int32_t dword) {
      parity = (0x80000000 & dword) >> 31;
      type   = (0x70000000 & dword) >> 28;
      spare  = (0x0f000000 & dword) >> 24;
      dataid = (0x00ff0000 & dword) >> 16;
      hi     = (0x0000ff00 & dword) >> 8;
      lo     = (0x000000ff & dword);
    }

    String toString() {
      return String("OTMessage[")
             + msgTypeToStr(type)
             + ",id:" + dataid + ",hi:" + hi + ",lo:" + lo
             + "," + dataIdToStr(dataid)
             + "]";
    }
};

typedef struct {
  // Use signed 16 bits to allow -1 to indicate that
  // value was not yet assigned.
  int16_t hi;
  int16_t lo;
} Value;

// Global status
struct {
  struct {
    long serial; // time of last messe via serial line
#ifdef WITH_DEBUG
    struct {
      long fed;		// via URL /feedmsg?msg=...&feed=1
      long debug; 	// via URL /feedmsg?msg=... (parsed only, not fed)
    } wifi;
#endif
  }        lastMsg;         // time (millis()) when last message was received

  int16_t  ventSetpoint; 	 // as set by controller
  int16_t  ventOverride; 	 // as overridden by OTGW (sent to slave instead of ventSetpoint)
  int16_t  ventAcknowledged; // by the slave, sent to OTGW
  int16_t  ventReported;     // from OTGW to controller, pretending level is as requested (ventSetpoint)
  int16_t  ventRelative; 	 // as reported by ventilation
  int16_t  tempSupply;  	 // milli Celsius (inlet)
  int16_t  tempExhaust;  	 // milli Celsius (inlet)
  int16_t  tsps[64];  	 	 // transparent slave parameters

  struct {
    Value master;
    Value slave;
  }        version;			// values for DI_*_PROD_VERS

  Value    status;
  Value    faultFlagsCode;
  Value    configMemberId;
  Value    masterConfig;
  Value    remoteParamFlags;

  struct {
    long serial;	// total number of messages received on serial line

    struct {
      long fed; 	// via /feedmsg?feed=1...
      long debug; 	// (parsed only, not fed)
    }    wifi;

    // number of invalid messages received
    struct {
      long length;		// invalid length
      long format;		// invalid line (pasing failed)
      long source;		// invalid source (not T,B,R,A)
    } invalid;

    // expected messages of various types
    struct {
      long T; long B; long R; long A;
      long otgw; // otgw message response e.g. "GW: 1" replied on "GW=1"
    } expected;

    // unexpected messages of various types
    struct {
      long T; long B; long R; long A; long zero;
    } unexpected;

  } messages; // number of messages received

#ifdef WITH_DALLAS_TEMP_SENSORS
  int sensorsFound;
  long lastMeasure;
  int16_t extraTemps[MAX_NUM_SENSORS];
#endif

} state;


// Credentials used to 1. set up ESP8266 as an access point and 2. define WiFI network to conenct to.
typedef struct {
  uint8_t used;
  char    ssid[32 + 1];
  char    psk[32 + 1];
} Credentials;


// I'm from cologne ;)
#define MAGIC 4711+3

// Everything that goes to EEPROM
struct {
  // Detect if first time setup of EEPROM was done.
  uint16_t    magic;
  boolean 	configured;
  // Start ESP in access point mode?
  Credentials accessPoint;
  // Network to connect to
  Credentials homeNetwork;
  // Count incremented on every reboot to enablee monitoring of device crashes.
  uint32_t reboots;
} * EE = 0;


// User committed first time setup (e.g. AP, network) and it was saved permanently.
boolean configured = false;

// A random token used during setup to authenticate committer.
char configToken[10 + 1];

// Time at which an attempt to connect to a network will be considered expired during setup procedure.
long connectAttemptExpires = -1;

// Remember whether temperatures have been already requested for the first time.
boolean temperaturesRequested = false;

Credentials accessPoint;
Credentials homeNetwork;

// After setup, a reboot is necessary. However, cannot do this while handling client request,
// because this will abort the connection. Instead of this, finish sending the reponse to the
// client and schedule a reboot after millis() will exceed this value.
long rebootScheduledAfter = -1;

// A string to hold incoming serial data from OTGW.
String inputString = "";
// wWhether the string is complete.
boolean stringComplete = false;

ESP8266WebServer server(80);


/***************************************************************

   Heart beat LED

*/

boolean led_lit = false;

void ledToggle() {
  led_lit = !led_lit;
  digitalWrite(LED_HEART, led_lit ? LOW : HIGH);
}

void ledFlash(int ms) {
  ledToggle();
  delay(ms);
  ledToggle();
}

/***************************************************************

   Dallas temperature sensors support

*/

#ifdef WITH_DALLAS_TEMP_SENSORS

// Data wire is plugged into port 2 on the Arduino
#define ONE_WIRE_BUS 2 // GPIO2
#define TEMPERATURE_PRECISION 9


// Setup a oneWire instance to communicate with any OneWire devices (not just Maxim/Dallas temperature ICs)
OneWire oneWire(ONE_WIRE_BUS);

// Pass our oneWire reference to Dallas Temperature.
DallasTemperature sensors(&oneWire);

#endif // WITH_DALLAS_TEMP_SENSORS

/***************************************************************

   Error logging

*/

#ifdef WITH_ERROR_LOG

#define MAX_ERR_LEN		32
#define MAX_ERR_COUNT	10

typedef struct {
  long time; // millis
  char text[MAX_ERR_LEN] ;
} LogEntry;


//LogEntry errorLog[MAX_ERR_COUNT];
//uint8_t  errorLogIndex = 0;

// round robin buffer with last N errors
void addError(const char * text) {
  /*
  	LogEntry * e = &errorLog[errorLogIndex];
  	e->time = millis();
  	strncpy(e->text, text, sizeof(e->text));
  	e->text[sizeof(e->text)-1] = 0;
  	errorLogIndex = (errorLogIndex+1)%MAX_ERR_COUNT;
  */
}
#else

void addError(const char * text) {}

#endif // WITH_ERROR_LOG

/***************************************************************

   Debug support

*/

#define DBG_OFF 0
#define DBG_ON  1

#ifdef WITH_DEBUG
int DEBUG_LEVEL = DBG_ON;
void dbg(String s)         {
  if (DBG_OFF < DEBUG_LEVEL) Serial.print(s);
}
void dbgln(String s)       {
  if (DBG_OFF < DEBUG_LEVEL) Serial.println(s);
}
void dbg(const char * s)   {
  if (DBG_OFF < DEBUG_LEVEL) Serial.print(s);
}
void dbgln(const char * s) {
  if (DBG_OFF < DEBUG_LEVEL) Serial.println(s);
}
void dbg(int i)            {
  if (DBG_OFF < DEBUG_LEVEL) Serial.print(i);
}
void dbgln(int i)          {
  if (DBG_OFF < DEBUG_LEVEL) Serial.println(i);
}
#else
int DEBUG_LEVEL = DBG_OFF;
#define dbg(WHATEVER) {}
#define dbgln(WHATEVER) {}
#endif


/***************************************************************

   OpenTherm message parsing

*/

int onUnexpectedMessage(char sender, OTMessages& m) {
  //dbg("ZZ=onUnexpectedMessage:"); dbg(sender); dbg(": "); dbgln(m.toString());
  switch (sender) {
    case 'T': state.messages.unexpected.T++; return RC_UNEXP_T;
    case 'B': state.messages.unexpected.B++; return RC_UNEXP_B;
    case 'R': state.messages.unexpected.R++; return RC_UNEXP_R;
    case 'A': state.messages.unexpected.A++; return RC_UNEXP_A;
  }
  return RC_UNEXP_O;
}

int toTemp(int8_t hi, int8_t lo) {
  // TODO: Need proof if temperatue conversion is correct for t<0
  // But wow? Even at winter time, temperature will never be <0 ... because of my setup.
  // Maybe someone else can help here?
  int sign = hi < 0 ?  -1 :  1;
  int abs  = hi < 0 ? -hi : hi;
  abs = 100 * abs + (100 * lo / 255);
  return sign * abs;
}

int onMasterMessage(OTMessage& m) {

  state.messages.expected.T++; {
    switch (m.type) {
      case OTMessage::MT_READ_DATA:
      case OTMessage::MT_WRITE_DATA:

        switch (m.dataid) {
          case OTMessage::DI_CONTROL_SETPOINT:
            state.ventSetpoint = m.lo;
            break;
          case OTMessage::DI_MASTER_PROD_VERS:
            state.version.master.hi = m.hi;
            state.version.master.lo = m.lo;
            return RC_OK;
          case OTMessage::DI_SLAVE_PROD_VERS:
            state.version.slave.hi = m.hi;
            state.version.slave.lo = m.lo;
            return RC_OK;
        }

        state.messages.expected.T++;
        return RC_OK; // ACKs from slave will be more usefull than these
    }
  }
  state.messages.expected.T--; // revoke former increment

  return onUnexpectedMessage('T', m);
}

int onSlaveMessage(OTMessage& m) {

  state.messages.expected.B++; {

    if (OTMessage::MT_UNKN_DATAID == m.type) {
      switch (m.dataid) {
        // Vitovent 300 never replies to these, never did, never will?
        // Seems like controller was designed to speak also to devices
        // that DO support this.
        case OTMessage::DI_MASTER_PROD_VERS:
          // should not be assigne here (always 0)
          //state.version.master.hi = m.hi;
          //state.version.master.lo = m.lo;
          return RC_OK; // its ok if that fails
        case OTMessage::DI_SLAVE_PROD_VERS:
          // should not be assigne here (always 0)
          //state.version.slave.hi = m.hi;
          //state.version.slave.lo = m.lo;
          return RC_OK; // its ok if that fails
      }
    }

    switch (m.dataid) {

      case OTMessage::DI_CONTROL_SETPOINT:
        state.ventAcknowledged = m.lo;
        return RC_OK;
      case OTMessage::DI_REL_VENTILATION:
        state.ventRelative = m.lo;
        return RC_OK;
      case OTMessage::DI_SUPPLY_INLET_TEMP:
        state.tempSupply = toTemp(m.hi, m.lo);
        return RC_OK;
      case OTMessage::DI_EXHAUST_INLET_TEMP:
        state.tempExhaust = toTemp(m.hi, m.lo);
        return RC_OK;
      case OTMessage::DI_TSP_SETTING:
        // Check filter:
        // "Die Filternachricht steckt aber in dem Telegramm mit den hex codes Bxx5917yy."
        // Sent from Boiler to Thermostat:
        // 0000 0000 0101 1001 00010111 00000000
        // PTTT SSSS DDDD DDDD HHHHHHHH LLLLLLLL # data ID: 0x59 = 89 = DI_TSP_SETTING
        // parameter no. 0x17 = 23
        if (0 <= m.hi && m.hi < 64) {
          state.tsps[m.hi] = m.lo;
        }
        return RC_OK;
      case OTMessage::DI_STATUS:
        state.status.hi = m.hi;
        state.status.lo = m.lo;
        return RC_OK;
      case OTMessage::DI_CONFIG_MEMBERID:
        state.configMemberId.hi = m.hi;
        state.configMemberId.lo = m.lo;
        return RC_OK;
      case OTMessage::DI_MASTER_CONFIG:
        state.masterConfig.hi = m.hi;
        state.masterConfig.lo = m.lo;
        return RC_OK;
      case OTMessage::DI_REMOTE_PARAM_FLAGS:
        state.remoteParamFlags.hi = m.hi;
        state.remoteParamFlags.lo = m.lo;
        return RC_OK;
      case OTMessage::DI_FAULT_FLAGS_CODE:
        state.faultFlagsCode.hi = m.hi;
        state.faultFlagsCode.lo = m.lo;
        return RC_OK;
    }
  }
  state.messages.expected.B--; // revoke former increment

  return onUnexpectedMessage('B', m); // B: boiler
}

int onRequestMessage(OTMessage& m) {

  state.messages.expected.R++; {
    switch (m.dataid) {
      // OTGW pretending towards the slave that controller wants to set this ventilation level,
      // e.g. R90470003	WRITE-DATA	CONTROL SETPOINT V/H: 3
      case OTMessage::DI_CONTROL_SETPOINT:
        // Hmm.. sometimes it does, sometime is does not.
        // Therefore saving this vakue now in overrideVentSetPoint()
        //state.ventOverride = m.lo;
        return RC_OK;
    }
  } state.messages.expected.R--; // revoke former increment

  return onUnexpectedMessage('R', m);
}

int onAnswerMessage(OTMessage& m) {

  state.messages.expected.A++; {
    switch (m.dataid) {
      case OTMessage::DI_CONTROL_SETPOINT:
        state.ventReported = m.lo;
        // OTGW answering to controller, pretending the current ventilation
        // level is the one that was requested by the controller before.
        return 0;
    }
  } state.messages.expected.A--; // revoke former increment

  return onUnexpectedMessage('A', m);
}

int onInvalidMessageSource(char sender, OTMessage& m) {
  onUnexpectedMessage('?', m);
  return RC_INV_SOURCE;
}

Error:

ESP_VitoWifi:223:28: error: ISO C++ forbids declaration of 'OTMessage' with no type [-fpermissive]
ESP_VitoWifi:509:21: error: 'OTMessage' was not declared in this scope
ESP_VitoWifi:509:32: error: 'm' was not declared in this scope
ESP_VitoWifi:539:20: error: 'OTMessage' was not declared in this scope
ESP_VitoWifi:539:31: error: 'm' was not declared in this scope
ESP_VitoWifi:613:22: error: 'OTMessage' was not declared in this scope
ESP_VitoWifi:613:33: error: 'm' was not declared in this scope
ESP_VitoWifi:630:21: error: 'OTMessage' was not declared in this scope
ESP_VitoWifi:630:32: error: 'm' was not declared in this scope
ESP_VitoWifi:645:41: error: 'OTMessage' has not been declared
ESP_VitoWifi:509:21: error: redefinition of 'int onMasterMessage'
ESP_VitoWifi:509:5: error: 'int onMasterMessage' previously defined here
ESP_VitoWifi:509:21: error: 'OTMessage' was not declared in this scope
ESP_VitoWifi:509:32: error: 'm' was not declared in this scope

do you need

void
    OTMessage(int32_t dword) {
      parity = (0x80000000 & dword) >> 31;
1 Like

Maybe it´s possible to ignore this error, but how?

Maybe an "-fpermissive flag" - but how to insert/use this in arduino ide?

The code is working for more than 2 years without problems.

at first, i thought you didn't post the beginning of the code. did you delete the beginning of the code by mistake?

it's not possible to "ignore" this error. all functions need a return type, "void" if nothing is returned.

conclusion = Don't post snippets (Snippets R Us!)

:slight_smile:

i don't think it's a snippet (376 lines)

it's a long one :slight_smile:

no setup(), no loop()...

seems OTMessage is a class and that might be the constructor (hence no return type) but it ought to be within the class definition or missing a OTMessage::

==> we are missing critical context

Agreed. We need to see the definition of the OTMessage class / struct / datatype.

sorry here´s the complete code.


const char DEFAULT_START_SSID[] = "OPEN";
// Initial password used to connect to it and set up networking.
// The device will normally show up as SSID "http://192.168.4.1"
// Connect to this network using the intial password and navigate your
// browser to the above URL.
#define DEFAULT_PSK  "12345678"
#define HTTP_LISTEN_PORT 80
/****************************************/
#include <ESP8266WiFi.h>
#include <ESP8266WebServer.h>
#include <EEPROM.h>
#include <errno.h>
#include "eEEPROM.h"
const String buildNo   = __DATE__ " " __TIME__;
#define LED_HEART 	0 // heartbeat 
#define LED_ONBOARD 2 // blue onboard LED / temp. sensors
#define LEVEL_NOOVER -1 // do not overrride (monitor mode)
#define LEVEL_OFF   0 // standby, ventilation off
#define LEVEL_LOW   1 // "reduced" mode (27% for mine ... may vary)
#define LEVEL_NORM  2 // "normal"  mode (55% for mine) 
#define LEVEL_HIGH  3 // "party"   mode (100%)
// result codes of onOTMessage
#define RC_OK         0	// message handled
#define RC_INV_LENGTH 1 // invalid message length (not Sxxxxxxxx)
#define RC_INV_FORMAT 2 // xxxxxxxx was not a parsable hex value
#define RC_INV_SOURCE 3 // S is not a known source (T,B.R,A)
#define RC_UNEXP_T    4 // unexpected message from T(hermostat)/controller/master
#define RC_UNEXP_B    5 // unexpected message from B(oiler)/ventilation/slave
#define RC_UNEXP_R    6 // unexpected request (sent by OTG in favor of master)
#define RC_UNEXP_A    7 // unexpected answer  (sent by OTG in favor of slave)
#define RC_UNEXP_O    8 // unexpected message from unknown source (should never occur) 

#define CT_TEXT_PLAIN  "text/plain"
#define CT_TEXT_HTML   "text/html"
#define CT_APPL_JSON   "application/json"

// Context type sent along with content stored in PROGMEM: 
const char PGM_CT_TEXT_HTML[] PROGMEM = "text/html";
const char PGM_CT_APPL_JSON[] PROGMEM = "application/json";

const char HTML_INDEX[] PROGMEM =
#include "index.html.h"
		 // The following line makes buildNo accessible for javascript:
		 "<script>var buildNo = '" __DATE__ " " __TIME__ "';</script>";
;
#ifdef WITH_WEB_SETUP
const char HTML_SETUP[] PROGMEM =
#include "setup.html.h"
;
#endif

const char MANIFEST_JSON[] PROGMEM =
#include "manifest.json.h"
;

const char FAVICON_ICO[] PROGMEM =
#include "favicon.ico.h"
;

const char FAN_PNG[] PROGMEM =
#include "fan.png.h"
;

// Conveniently zero-terminates a C-string.
#define TZERO(STR) { (STR)[sizeof((STR))-1] = 0; }

class OTMessages {

  public:
	
	// OpenTherm message types
	const static uint8_t MT_READ_DATA    = 0;
	const static uint8_t MT_WRITE_DATA   = 1;
	const static uint8_t MT_INVALID_DATA = 2;
	const static uint8_t MT_RESERVED     = 3;
	const static uint8_t MT_READ_ACK     = 4;
	const static uint8_t MT_WRITE_ACK    = 5;
	const static uint8_t MT_DATA_INVALID = 6;
	const static uint8_t MT_UNKN_DATAID  = 7;
	
	// OpenTherm data IDs (common and vendor specific)
	// see https://forum.fhem.de/index.php?topic=29762.115;wap2
	const static uint8_t DI_MASTER_CONFIG		=   2;
	const static uint8_t DI_REMOTE_PARAM_FLAGS 	=   6;
	const static uint8_t DI_STATUS 				=  70;
	const static uint8_t DI_CONTROL_SETPOINT 	=  71;
	// According to http://otgw.tclcode.com/matrix.cgi#thermostats only:
	const static uint8_t DI_FAULT_FLAGS_CODE	=  72;
	const static uint8_t DI_CONFIG_MEMBERID 	=  74;
	const static uint8_t DI_REL_VENTILATION 	=  77;
	const static uint8_t DI_SUPPLY_INLET_TEMP 	=  80;
	const static uint8_t DI_EXHAUST_INLET_TEMP 	=  82;
	const static uint8_t DI_TSP_SETTING 		=  89; 
	const static uint8_t DI_MASTER_PROD_VERS 	= 126;
	const static uint8_t DI_SLAVE_PROD_VERS 	= 127;
      
    
    static String msgTypeToStr(uint8_t t) {
      switch(t) {
        case MT_READ_DATA:    return "READ_DATA";
        case MT_WRITE_DATA:   return "WRITE_DATA";
        case MT_INVALID_DATA: return "INVALID_DATA";
        case MT_RESERVED:     return "RESERVED";
        case MT_READ_ACK:     return "READ_ACK";
        case MT_WRITE_ACK:    return "WRITE_ACK";
        case MT_DATA_INVALID: return "DATA_INVALID";
        case MT_UNKN_DATAID:  return "UNKN_DATAID";
      }
    }

    static String dataIdToStr(uint8_t id) {
      switch(id) {
		case DI_MASTER_CONFIG:		return "Master configuration";
		case DI_REMOTE_PARAM_FLAGS:	return "Remote parameter flags";
		case DI_STATUS:				return "Status V/H";
		case DI_FAULT_FLAGS_CODE:	return "Fault flags/code V/H";
		case DI_CONTROL_SETPOINT:	return "Control setpoint V/H";
		case DI_CONFIG_MEMBERID:	return "Configuration/memberid V/H";
		case DI_REL_VENTILATION:	return "Relative ventilation";
		case DI_SUPPLY_INLET_TEMP:	return "Supply inlet temperature";
		case DI_EXHAUST_INLET_TEMP:	return "Exhaust inlet temperature";    
		// TODO: Find out meaning of various "transparent slave parameters"
		// These are requested by the master in a round robin fashion from slave in the range 0-63,
		// however not some indexes are skipped. 
		case DI_TSP_SETTING:		return "TSP setting V/H";
		case DI_MASTER_PROD_VERS:	return "Master product version";
		case DI_SLAVE_PROD_VERS:	return "Slave product version";
		
		#ifdef WITH_ALL_DATAIDS
		case  3: return "SLAVE CONFIG/MEMBERID",READ,FLAG,00000000,U8,0,255,0,Yes
		case  4: return "COMMAND";
		case  5: return "FAULT FLAGS/CODE";
		case  7: return "COOLING CONTROL";
		case  8: return "TsetCH2";
		case  9: return "REMOTE ROOM SETPOINT";
		case 10: return "TSP NUMBER";
		case 11: return "TSP ENTRY";
		case 11: return "TSP ENTRY";
		case 12: return "FAULT BUFFER SIZE";
		case 13: return "FAULT BUFFER ENTRY";
		case 14: return "CAPACITY SETTING";
		case 15: return "MAX CAPACITY / MIN-MOD-LEVEL";
		case 16: return "ROOM SETPOINT";
		case 17: return "RELATIVE MODULATION LEVEL";
		case 18: return "CH WATER PRESSURE";
		case 19: return "DHW FLOW RATE";
		case 20: return "DAY - TIME";
		case 20: return "DAY - TIME";
		case 21: return "DATE";
		case 21: return "DATE";
		case 22: return "YEAR";
		case 22: return "YEAR";
		case 23: return "SECOND ROOM SETPOINT";
		case 24: return "ROOM TEMPERATURE";
		case 25: return "BOILER WATER TEMP.";
		case 26: return "DHW TEMPERATURE";
		case 27: return "OUTSIDE TEMPERATURE";
		case 28: return "RETURN WATER TEMPERATURE";
		case 29: return "SOLAR STORAGE TEMPERATURE";
		case 30: return "SOLAR COLLECTOR TEMPERATURE";
		case 31: return "SECOND BOILER WATER TEMP.";
		case 32: return "SECOND DHW TEMPERATURE";
		case 32: return "EXHAUST TEMPERATURE";
		case 48: return "DHW SETPOINT BOUNDS";
		case 49: return "MAX CH SETPOINT BOUNDS";
		case 50: return "OTC HC-RATIO BOUNDS";
		case 56: return "DHW SETPOINT";
		case 56: return "DHW SETPOINT";
		case 57: return "MAX CH WATER SETPOINT";
		case 57: return "MAX CH WATER SETPOINT";
		case 58: return "OTC HEATCURVE RATIO";
		case 58: return "OTC HEATCURVE RATIO";
		case // New ID for ventilation/heat-recovery applications
		case 73: return "DIAGNOSTIC CODE V/H";
		case 75: return "OPENTHERM VERSION V/H";
		case 76: return "VERSION & TYPE V/H";
		case 78: return "RELATIVE HUMIDITY";
		case 78: return "RELATIVE HUMIDITY";
		case 79: return "CO2 LEVEL";
		case 79: return "CO2 LEVEL";
		case 81: return "SUPPLY OUTLET TEMPERATURE";
		case 83: return "EXHAUST OUTLET TEMPERATURE";
		case 84: return "ACTUAL EXHAUST FAN SPEED";
		case 85: return "ACTUAL INLET FAN SPEED";
		case 86: return "REMOTE PARAMETER SETTINGS V/H";
		case 87: return "NOMINAL VENTIALTION VALUE";
		case 87: return "NOMINAL VENTIALTION VALUE";
		case 88: return "TSP NUMBER V/H";
		case 89: return "TSP ENTRY V/H";
		case 90: return "FAULT BUFFER SIZE V/H";
		case 91: return "FAULT BUFFER ENTRY V/H";
		case 115: return "OEM DIAGNOSTIC CODE";
		case 116: return "BURNER STARTS";
		case 116: return "BURNER STARTS";
		case 117: return "CH PUMP STATRS";
		case 117: return "CH PUMP STATRS";
		case 118: return "DHW PUMP/VALVE STARTS";
		case 118: return "DHW PUMP/VALVE STARTS";
		case 119: return "DHW BURNER STARTS";
		case 119: return "DHW BURNER STARTS";
		case 120: return "BURNER OPERATION HOURS";
		case 120: return "BURNER OPERATION HOURS";
		case 121: return "CH PUMP OPERATION HOURS";
		case 121: return "CH PUMP OPERATION HOURS";
		case 122: return "DHW PUMP/VALVE OPERATION HOURS";
		case 122: return "DHW PUMP/VALVE OPERATION HOURS";
		case 123: return "DHW BURNER HOURS";
		case 123: return "DHW BURNER HOURS";
		case 124: return "OPENTHERM VERSION MASTER";
		case 125: return "OPENTHERM VERSION SLAVE";
		#endif      
     }

     return String("?");
   }
   
    uint32_t 
		parity : 1, 
		type   : 3, 
		spare  : 8, 
		dataid : 8, 
		hi     : 8, 
		lo     : 8;
    
    // Construcor: takes raw 32 bit message and splits the double word into its components.
   OTMessage(int32_t dword) {
     parity = (0x80000000 & dword)>>31;
     type   = (0x70000000 & dword)>>28;
     spare  = (0x0f000000 & dword)>>24;
     dataid = (0x00ff0000 & dword)>>16;
     hi     = (0x0000ff00 & dword)>>8;
     lo     = (0x000000ff & dword);
    }

    String toString() {
      return String("OTMessage[")    
    		  + msgTypeToStr(type)  
			  + ",id:" + dataid + ",hi:" + hi + ",lo:" + lo  
			  + "," + dataIdToStr(dataid)
			  + "]";
    }  
};

typedef struct {
   // Use signed 16 bits to allow -1 to indicate that
   // value was not yet assigned.
   int16_t hi;  
   int16_t lo;
} Value;

// Global status 
struct {
  struct {
	  long serial; // time of last messe via serial line
      #ifdef WITH_DEBUG
	  struct { 
		  long fed;		// via URL /feedmsg?msg=...&feed=1 
		  long debug; 	// via URL /feedmsg?msg=... (parsed only, not fed)
	  } wifi;
      #endif
  }        lastMsg;         // time (millis()) when last message was received
  
  int16_t  ventSetpoint; 	 // as set by controller
  int16_t  ventOverride; 	 // as overridden by OTGW (sent to slave instead of ventSetpoint)
  int16_t  ventAcknowledged; // by the slave, sent to OTGW 
  int16_t  ventReported;     // from OTGW to controller, pretending level is as requested (ventSetpoint)
  int16_t  ventRelative; 	 // as reported by ventilation
  int16_t  tempSupply;  	 // milli Celsius (inlet)
  int16_t  tempExhaust;  	 // milli Celsius (inlet)
  int16_t  tsps[64];  	 	 // transparent slave parameters
  
  struct { 
	  Value master; 
	  Value slave; 
  }        version;			// values for DI_*_PROD_VERS
  
  Value    status; 		 
  Value    faultFlagsCode;	
  Value    configMemberId; 
  Value    masterConfig;
  Value    remoteParamFlags;
    
  struct {
	  long serial;	// total number of messages received on serial line
	  
	  struct { 
		  long fed; 	// via /feedmsg?feed=1...
		  long debug; 	// (parsed only, not fed)
	  }    wifi;
	  
	  // number of invalid messages received 
	  struct { 
		  long length;		// invalid length
		  long format;		// invalid line (pasing failed)
		  long source;		// invalid source (not T,B,R,A)
	  } invalid;
	  
	  // expected messages of various types
	  struct { 
		  long T; long B; long R; long A; 
	  	  long otgw; // otgw message response e.g. "GW: 1" replied on "GW=1" 
	  } expected;
	  
	  // unexpected messages of various types
	  struct { 
		  long T; long B; long R; long A; long zero; 
	  } unexpected;
	  
  } messages; // number of messages received
  
  #ifdef WITH_DALLAS_TEMP_SENSORS
  int sensorsFound;
  long lastMeasure;
  int16_t extraTemps[MAX_NUM_SENSORS];
  #endif
  
} state;


// Credentials used to 1. set up ESP8266 as an access point and 2. define WiFI network to conenct to.
typedef struct {
	uint8_t used;
	char    ssid[32+1];
	char    psk[32+1];	
} Credentials;


// I'm from cologne ;)
#define MAGIC 4711+3

// Everything that goes to EEPROM
struct {	
	// Detect if first time setup of EEPROM was done. 
	uint16_t    magic;
	boolean 	configured;
	// Start ESP in access point mode?
	Credentials accessPoint;
	// Network to connect to
	Credentials homeNetwork;
	// Count incremented on every reboot to enablee monitoring of device crashes.
	uint32_t reboots;
} * EE = 0;


// User committed first time setup (e.g. AP, network) and it was saved permanently.
boolean configured = false;

// A random token used during setup to authenticate committer.
char configToken[10+1];

// Time at which an attempt to connect to a network will be considered expired during setup procedure.
long connectAttemptExpires = -1;

// Remember whether temperatures have been already requested for the first time. 
boolean temperaturesRequested = false;

Credentials accessPoint;
Credentials homeNetwork;

// After setup, a reboot is necessary. However, cannot do this while handling client request,
// because this will abort the connection. Instead of this, finish sending the reponse to the
// client and schedule a reboot after millis() will exceed this value.
long rebootScheduledAfter = -1;

// A string to hold incoming serial data from OTGW.
String inputString = "";         
// wWhether the string is complete.
boolean stringComplete = false;  

ESP8266WebServer server(HTTP_LISTEN_PORT);


/***************************************************************
 * 
 * Heart beat LED 
 * 
 */

boolean led_lit=false;

void ledToggle() {
	led_lit = !led_lit;
	digitalWrite(LED_HEART, led_lit ? LOW : HIGH);
}

void ledFlash(int ms) {
	ledToggle();
	delay(ms);
	ledToggle();
}

/***************************************************************
 * 
 * Dallas temperature sensors support
 * 
 */

#ifdef WITH_DALLAS_TEMP_SENSORS

// Data wire is plugged into port 2 on the Arduino
#define ONE_WIRE_BUS 2 // GPIO2
#define TEMPERATURE_PRECISION 9


// Setup a oneWire instance to communicate with any OneWire devices (not just Maxim/Dallas temperature ICs)
OneWire oneWire(ONE_WIRE_BUS);

// Pass our oneWire reference to Dallas Temperature. 
DallasTemperature sensors(&oneWire);

#endif // WITH_DALLAS_TEMP_SENSORS

/***************************************************************
 * 
 * Error logging
 * 
 */

#ifdef WITH_ERROR_LOG

#define MAX_ERR_LEN		32
#define MAX_ERR_COUNT	10

typedef struct {
	long time; // millis
	char text[MAX_ERR_LEN] ;
} LogEntry;


//LogEntry errorLog[MAX_ERR_COUNT];
//uint8_t  errorLogIndex = 0;

// round robin buffer with last N errors
void addError(const char * text) {
/*	
	LogEntry * e = &errorLog[errorLogIndex];
	e->time = millis();
	strncpy(e->text, text, sizeof(e->text));
	e->text[sizeof(e->text)-1] = 0;
	errorLogIndex = (errorLogIndex+1)%MAX_ERR_COUNT;
	*/
} 
#else

void addError(const char * text) {}

#endif // WITH_ERROR_LOG

/***************************************************************
 * 
 * Debug support
 * 
 */

#define DBG_OFF 0 
#define DBG_ON  1
		
#ifdef WITH_DEBUG
	int DEBUG_LEVEL = DBG_ON;
	void dbg(String s)         { if (DBG_OFF<DEBUG_LEVEL) Serial.print(s);   } 
	void dbgln(String s)       { if (DBG_OFF<DEBUG_LEVEL) Serial.println(s); }
	void dbg(const char * s)   { if (DBG_OFF<DEBUG_LEVEL) Serial.print(s);   } 
	void dbgln(const char * s) { if (DBG_OFF<DEBUG_LEVEL) Serial.println(s); }
	void dbg(int i)            { if (DBG_OFF<DEBUG_LEVEL) Serial.print(i);   } 
	void dbgln(int i)          { if (DBG_OFF<DEBUG_LEVEL) Serial.println(i); }
#else
	int DEBUG_LEVEL = DBG_OFF;
	#define dbg(WHATEVER) {}
	#define dbgln(WHATEVER) {}
#endif

	
/***************************************************************
 * 
 * OpenTherm message parsing
 * 
 */

int onUnexpectedMessage(char sender, OTMessages& m) {
  //dbg("ZZ=onUnexpectedMessage:"); dbg(sender); dbg(": "); dbgln(m.toString());
  switch (sender) {
	  case 'T': state.messages.unexpected.T++; return RC_UNEXP_T;
	  case 'B': state.messages.unexpected.B++; return RC_UNEXP_B;
	  case 'R': state.messages.unexpected.R++; return RC_UNEXP_R;
	  case 'A': state.messages.unexpected.A++; return RC_UNEXP_A;
  }
  return RC_UNEXP_O;
}

int toTemp(int8_t hi, int8_t lo) {
	// TODO: Need proof if temperatue conversion is correct for t<0 
	// But wow? Even at winter time, temperature will never be <0 ... because of my setup.
	// Maybe someone else can help here?
	int sign = hi<0 ?  -1 :  1;
	int abs  = hi<0 ? -hi : hi; 
    abs = 100*abs + (100*lo/255);
    return sign*abs;
}

int onMasterMessage(OTMessage& m) {
	
	state.messages.expected.T++; {
		switch (m.type) {
		case OTMessage::MT_READ_DATA:
		case OTMessage::MT_WRITE_DATA:
			
			switch (m.dataid) {
			case OTMessage::DI_CONTROL_SETPOINT:   
				state.ventSetpoint = m.lo;
				break;
			case OTMessage::DI_MASTER_PROD_VERS:
				state.version.master.hi = m.hi;
				state.version.master.lo = m.lo;
				return RC_OK; 
			case OTMessage::DI_SLAVE_PROD_VERS:
				state.version.slave.hi = m.hi;
				state.version.slave.lo = m.lo;
				return RC_OK;
			}
			
			state.messages.expected.T++;
			return RC_OK; // ACKs from slave will be more usefull than these
		}
	}
	state.messages.expected.T--; // revoke former increment
	
	return onUnexpectedMessage('T', m);
}

int onSlaveMessage(OTMessage& m) {

	state.messages.expected.B++; {
	
		if (OTMessage::MT_UNKN_DATAID==m.type) {
			switch (m.dataid) {
			// Vitovent 300 never replies to these, never did, never will?
			// Seems like controller was designed to speak also to devices
			// that DO support this.
			case OTMessage::DI_MASTER_PROD_VERS:
				// should not be assigne here (always 0)
				//state.version.master.hi = m.hi;
				//state.version.master.lo = m.lo;
				return RC_OK; // its ok if that fails 
			case OTMessage::DI_SLAVE_PROD_VERS:
				// should not be assigne here (always 0)
				//state.version.slave.hi = m.hi;
				//state.version.slave.lo = m.lo;
				return RC_OK; // its ok if that fails
			}
		}
	
		switch (m.dataid) {
	
		case OTMessage::DI_CONTROL_SETPOINT:   
			state.ventAcknowledged = m.lo;
			return RC_OK;
		case OTMessage::DI_REL_VENTILATION:    
			state.ventRelative = m.lo;
			return RC_OK;
		case OTMessage::DI_SUPPLY_INLET_TEMP:
			state.tempSupply = toTemp(m.hi, m.lo);
			return RC_OK;
		case OTMessage::DI_EXHAUST_INLET_TEMP: 
			state.tempExhaust = toTemp(m.hi, m.lo);
			return RC_OK;
		case OTMessage::DI_TSP_SETTING:        
			// Check filter:
			// "Die Filternachricht steckt aber in dem Telegramm mit den hex codes Bxx5917yy."
			// Sent from Boiler to Thermostat:	   
			// 0000 0000 0101 1001 00010111 00000000
			// PTTT SSSS DDDD DDDD HHHHHHHH LLLLLLLL # data ID: 0x59 = 89 = DI_TSP_SETTING
			// parameter no. 0x17 = 23 
			if (0<=m.hi && m.hi<64) {
				state.tsps[m.hi] = m.lo;
			}
			return RC_OK;
		case OTMessage::DI_STATUS:
			state.status.hi = m.hi;
			state.status.lo = m.lo;
			return RC_OK;
		case OTMessage::DI_CONFIG_MEMBERID:
			state.configMemberId.hi = m.hi;
			state.configMemberId.lo = m.lo;
			return RC_OK;
		case OTMessage::DI_MASTER_CONFIG:
			state.masterConfig.hi = m.hi;
			state.masterConfig.lo = m.lo;
			return RC_OK;
		case OTMessage::DI_REMOTE_PARAM_FLAGS:
			state.remoteParamFlags.hi = m.hi;
			state.remoteParamFlags.lo = m.lo;
			return RC_OK;
		case OTMessage::DI_FAULT_FLAGS_CODE:
			state.faultFlagsCode.hi = m.hi;
			state.faultFlagsCode.lo = m.lo;
			return RC_OK;
		}
	}	 
	state.messages.expected.B--; // revoke former increment
	
	return onUnexpectedMessage('B', m); // B: boiler
}

int onRequestMessage(OTMessage& m) {
	
	state.messages.expected.R++; {
		switch (m.dataid) {
		// OTGW pretending towards the slave that controller wants to set this ventilation level,
		// e.g. R90470003	WRITE-DATA	CONTROL SETPOINT V/H: 3
		case OTMessage::DI_CONTROL_SETPOINT:
			// Hmm.. sometimes it does, sometime is does not.
			// Therefore saving this vakue now in overrideVentSetPoint() 
			//state.ventOverride = m.lo;
			return RC_OK;
		}
	} state.messages.expected.R--; // revoke former increment
	
	return onUnexpectedMessage('R', m);
}

int onAnswerMessage(OTMessage& m) {
	
	state.messages.expected.A++; {
		switch (m.dataid) {
			case OTMessage::DI_CONTROL_SETPOINT:
				state.ventReported=m.lo;
				// OTGW answering to controller, pretending the current ventilation
				// level is the one that was requested by the controller before.
				return 0;
		}
	} state.messages.expected.A--; // revoke former increment
	
	return onUnexpectedMessage('A', m);
}

int onInvalidMessageSource(char sender, OTMessage& m) {
  onUnexpectedMessage('?', m);
  return RC_INV_SOURCE;
}

int onInvalidLine(const char * line) {
  state.messages.invalid.format++;
  addError(line);
  return RC_INV_FORMAT;
}

int onInvalidLength(const char * line) {
  state.messages.invalid.length++;
  addError(line);
  return RC_INV_LENGTH;
}

String onOTGWMessage(String line, boolean feed) {
	
  const char * cstr   = line.c_str();
  char         sender = cstr[0];
  const char * hexstr = cstr+1;

  // Indicate there is some activity:
  ledFlash(5);
  
  // ACK of OTGW to command 'VS=x' or 'GW=1' messages sent by us to control fan speed level.
  if (line.startsWith("VS: ") || line.startsWith("GW: ")) {
	  if (feed) {
		  state.messages.expected.otgw++;
	  }
	  return String("ACK:'")+line + "'\n"; // adding white space causes flush??
  }
  
  // This message sent by OTGW if there is no controller connected, probably
  // to fulfill OpneTherm specs (at least 1 msg/sec sent)
  if (line.startsWith("R00000000")) {
	  state.messages.unexpected.zero++;
	  return String("NOCONN:'")+line+"\n";
  }
  
  String rc;
  int len = strlen(hexstr);
  if (8!=len) { // 1: 'B'/'T'/'A'/'R' followed by 8 hex chars
	  rc = String("INVLEN[") + len + "]:'" + cstr + "':";
	  
	  if (feed) {
		  // add real resut code if we should handle this message
		  rc += onInvalidLength(cstr);
	  }
	  else {
		  // otherwise bail out after parsing, no rc applicable
		  rc += "none";		  
	  }
	  // and newline, to ease outout to client in tailf
	  rc += "\n"; 
	  return rc;
  }
  
  //dbg("hexstr: "); dbg(hexstr); dbgln("'");
  char hs[5], ls[5];
  strncpy(hs, hexstr,   4); TZERO(hs);
  strncpy(ls, hexstr+4, 4); TZERO(ls);
  
  uint32_t hi = strtol(hs, NULL, 16);
  uint32_t lo = strtol(ls, NULL, 16);  
  uint32_t number = (hi<<16)|lo;
  if (0==number && EINVAL==errno) {
	  rc = "INVLINE:";
	  rc += cstr;
	  rc += ':';
	  if (feed) {
		  rc += onInvalidLine(cstr);
	  }
	  else {
		  rc += "none";
	  }
	  rc += "\n";
	  return rc;
  }

  OTMessage msg(number);
  rc = String(sender)+":";
  rc += msg.toString();
  rc += ':';
  
  if (feed) {	  
	  switch(sender) {
		case 'T': // from master: thermostat, controller
		  rc += onMasterMessage(msg);
		  break;
		case 'B': // from slave: boiler, ventilation
		  rc += onSlaveMessage(msg);
		  break;
		case 'A': // response (answer) replied by OTGW to the master 
		  rc += onAnswerMessage(msg);
		  break;
		case 'R': // request send by OTGW to the slave
		  rc += onRequestMessage(msg);
		  break;
		default:      
		  state.messages.invalid.source++;
		  rc += onInvalidMessageSource(sender, msg);
	  }
  }
  else {
	  rc += "none";
  }
  	  
  rc += "\n";
  return rc; 
}


int overrideVentSetPoint(int level) {
  level = level<-1 ? -1 : level>LEVEL_HIGH ? LEVEL_HIGH : level;
  if (level<0) {
	  for (int i=0; i<3; i++) {
		  // stop overriding, just monitor
		  Serial.print("GW=0\r\n"); 
		  delay(100);
	  } 
  }
  else {
	  for (int i=0; i<3; i++) {
		  Serial.print("GW=1\r\n");
		  delay(10);
		  Serial.print("VS="); Serial.print(level); Serial.print("\r\n");
		  delay(20);
	  }
  }
  dbg("ZZ=overrideVentSetPoint, old:"); dbg(state.ventOverride); dbg(" new:"); dbgln(level);
  int old = state.ventOverride; 
  state.ventOverride = level;
  return old;
}


/***************************************************************
 * 
 * EEPROM configuration and networking setup
 * 
 */

boolean readConfiguration() {
    //dbgln("ZZ=readConfiguration");
	boolean success = false;
	uint16_t magic;

	ESP.wdtDisable() ;
	EEPROM.begin(sizeof(*EE));
	eEE_READ(EE->magic, magic);
	
	//dbg("ZZ=magic="); dbgln(magic); 
	if (MAGIC==magic) {
		
		eEE_READ(EE->configured, configured);
		//dbg("ZZ=configured="); dbgln(configured); 
		
		eEE_READ(EE->accessPoint.used, accessPoint.used);
		if (accessPoint.used) {
			eEE_READ(EE->accessPoint, accessPoint);
		}
		eEE_READ(EE->homeNetwork.used, homeNetwork.used);
		if (homeNetwork.used) {
			eEE_READ(EE->homeNetwork, homeNetwork);
		}
		
		//dbgln("readConfiguration: success=true");
		success = true;
	}
	EEPROM.end();
	ESP.wdtEnable(5000);
	
	return success;
}

void firstTimeSetup() {
    dbgln("ZZ=firstTimeSetup");
	
	ESP.wdtDisable() ;
	EEPROM.begin(sizeof(*EE));
	
    dbgln("ZZ=writing credentials");
	Credentials creds = { 0, "", "" }; 
	memset(&creds, 0, sizeof(creds));
	
	eEE_WRITE(creds, EE->accessPoint.used);	
	eEE_WRITE(creds, EE->homeNetwork);
    dbgln("ZZ=credentials written");

    dbgln("ZZ=writing config flag");
	eEE_WRITE(configured, EE->configured);
    dbgln("ZZ=config flag written");
	
	// write magic to state that eeprom content is valid now
	uint16_t magic = MAGIC;
	eEE_WRITE(magic, EE->magic);

	EEPROM.commit();
	EEPROM.end();
	ESP.wdtEnable(5000); 
    dbgln("ZZ=firstTimeSetup done");
}

uint32_t reboots;

void updateReboots() {
    //dbgln("ZZ=updateReboots");

	ESP.wdtDisable() ;
	EEPROM.begin(sizeof(*EE));

	eEE_READ(EE->reboots, reboots);
	reboots++;
	//dbg("ZZ=reboots="); dbgln(reboots); 
	eEE_WRITE(reboots, EE->reboots);
	
	EEPROM.commit();
	EEPROM.end();
	ESP.wdtEnable(5000) ;
    //dbgln("ZZ=updateReboots done");
}

String toStr(IPAddress a) {
  return String("") + a[0] + '.' + a[1] + '.' + a[2] + '.' + a[3];
}

String getAPIP() {
  return toStr(WiFi.softAPIP());
}
  
void setupNetwork() {
	
	//dbgln("ZZ=setupNetwork");
	// if configured, try to connect to network first
	if (homeNetwork.used) {
		
	    int retries = 40; //*250ms=10s
	    
	    dbg("ZZ=connecting to "); dbgln(homeNetwork.ssid);
	    WiFi.mode(accessPoint.used ? WIFI_AP_STA : WIFI_STA);
		WiFi.begin(homeNetwork.ssid,  homeNetwork.psk);

	    while (WiFi.status() != WL_CONNECTED && 0<retries--) {
	      digitalWrite(LED_ONBOARD, retries % 2);
	      delay(250);
	      //dbgln("ZZ=.");
	    }
	}
	else {
		//dbgln("ZZ=no network set up");
	}
	
	boolean connected = WiFi.status()==WL_CONNECTED;	
	//dbg("setupNetwork:"); dbg(connected ? " " : " not "); dbgln("connected");
	
	// if that fails (or requested through configuration), start access point (as well)
	if (accessPoint.used || !connected) {
		
		if (!configured) {
			//dbgln("not configured => WIFI_AP_STA");
			// will need to connect to network during setup
		    WiFi.mode(WIFI_AP_STA);
		}
		else if (homeNetwork.used) {
			// was configured to connect to a network		
			//dbgln("homeNetwork used => WIFI_AP_STA");
		    WiFi.mode(WIFI_AP_STA);			
		}
		else {
			// was configured to stay access point only, no network		
			//dbgln("configure, no home network => WIFI_AP");
			WiFi.mode(WIFI_AP);
		}
	    
	    String sid = accessPoint.ssid;
	    String psk = accessPoint.psk;
	    //dbg("ZZ=accessPoint.ssid: "); dbgln(sid);
	    //dbg("ZZ=accessPoint.psk:  "); dbgln(psk);
	    if (!accessPoint.used) {
			#ifdef DEFAULT_START_SSID
	    	sid = DEFAULT_START_SSID;
			#else
	    	sid = String("http://") + getAPIP();
			#endif	    	
	    	psk = DEFAULT_PSK;
	    }
	    
	    // Prefix debug messages with (invalid) command prefix "ZZ=" in case we are really conneted to a OTGW.
	    // This way it will just print a "invalid command" message and we do not risk to confuse/crash it.
	    dbg("ZZ=ssid: "); dbgln(sid);
	    dbg("ZZ=psk:  "); dbgln(psk);
		WiFi.softAP(sid.c_str(), psk.c_str());
	}
}

/***************************************************************
 * 
 * Web server support  
 * 
 */

// Send headers with HTTP response that disable browser caching
void avoidCaching() {
	server.sendHeader(String("Pragma"),  String("no-cache"));
	server.sendHeader(String("Expires"), String("0"));
	server.sendHeader(String("Cache-Control"), String("no-cache, no-store, must-revalidate"));
}

// Send 320 redirect HTTP response
void sendRedirect(String path) {
	String body = String("<htm><body>Proceed to <a href=\"")+path+"\">"+path+"</a></body></html>";
	server.sendHeader("Location", path);		
	server.send(302, CT_TEXT_HTML, body);	
}

void handleIndex() {
	server.send_P(200, PGM_CT_TEXT_HTML, HTML_INDEX);	
}

// handle requests to /: if not yet configured, redirect to /setup, otherwise show default page 
void handleRoot() {
	#ifdef WITH_WEB_SETUP
	if (!configured) {
		sendRedirect("/setup");
		return;
	}
	#endif
	handleIndex();
}

void handleNotFound() {
	server.send(404, CT_TEXT_HTML, String("Not found: ") + server.uri());
	//handleIndex();
}

/***************************************************************
 * 
 * Web server support (first time network setup)  
 * 
 */

#ifdef WITH_WEB_SETUP
void handleSetup() {
	server.send_P(200, PGM_CT_TEXT_HTML, HTML_SETUP);		
}
#endif

String getLocalAddr() {
	IPAddress local = WiFi.localIP();
	String addr = String(local[0])+"."+local[1]+"."+local[2]+"."+local[3];	
	return addr;
}

void createConfigToken() {
	int i;
	for (i=0; i<sizeof(configToken)-1; i++) {
		configToken[i]=random('A', 'Z' + 1);
	}
	configToken[i]=0;
}

void handleAjax() {
	String action = server.arg("a");
	
	if (action=="set") { // set network name and password, start to connect
		String ssid = server.arg("ssid");
		String psk  = server.arg("psk");
		if (ssid.length()<1 || psk.length()<8) {
			server.send(200, CT_APPL_JSON, "{\"ok\":0,\"msg\":\"Name or password to short\"}\n");
			return;
		}
		else {		
			strncpy(homeNetwork.ssid, ssid.c_str(), sizeof(homeNetwork.ssid)); TZERO(homeNetwork.ssid); 
			strncpy(homeNetwork.psk,  psk.c_str(),  sizeof(homeNetwork.psk )); TZERO(homeNetwork.psk ); 
			homeNetwork.used = true;
			
			WiFi.mode(WIFI_AP_STA);
			WiFi.begin(homeNetwork.ssid, homeNetwork.psk);
			long timeout=20; // secs
			connectAttemptExpires = millis()+timeout*1000L;
			createConfigToken();
			dbg("ZZ=created configToken "); dbgln(configToken);
			server.send(200, CT_APPL_JSON, 
				String("{\"ok\":1,\"msg\":\"Ok\",\"token\":\"")+configToken+"\",\"timeout\":\""+timeout+"\"}\n");
			return;
		}
	}
	else if (action=="status") { // sent when setup.html is polling for device coming up after reboot
		String saddr = getLocalAddr();
		String caddr = toStr(server.client().remoteIP());
		server.send(200, CT_APPL_JSON, 
			String("{\"ok\":1,\"nwssid\":\"")+homeNetwork.ssid+"\",\"saddr\":\""+saddr+"\",\"caddr\":\""+caddr+"\"}\n");
		return;	
	}
	
	String token = server.arg("t");
	if (token=="MaGiC") {
		// okay (for debug purposes only)
		dbg("ZZ=debug token: "); dbgln(token);
	}
	else if (token!=configToken) {
		dbg("ZZ=token exp: "); dbgln(configToken);
		dbg("ZZ=token got: "); dbgln(token);
		server.send(200, CT_APPL_JSON, "{\"ok\":0,\"msg\":\"Token mismatch\"}\n");
		return;
	}
	
	if (action=="wait") { // setup.html is checking if device connected
		long now = millis();
		if (WiFi.status() == WL_CONNECTED) {
			String addr = getLocalAddr();
			if (80!=HTTP_LISTEN_PORT) { 
				addr+=":"; 
				addr+=HTTP_LISTEN_PORT; 
			}
			server.send(200, CT_APPL_JSON, String("{\"ok\":1,\"status\":0,\"addr\":\"")+addr+"\"}\n");
			return;
		}
		else if (connectAttemptExpires<0 || now>connectAttemptExpires) {
			server.send(200, CT_APPL_JSON, "{\"ok\":1,\"status\":2,\"msg\":\"Timeout\"}\n");
			return;
		}
		else {
			int left = (connectAttemptExpires-now)/1000;
			server.send(200, CT_APPL_JSON, String("{\"ok\":1,\"status\":1,\"msg\":\"Connecting\",\"left\":")+left+"}\n");
			return;
		}
	}
	else if (action=="save") { // setup.html connected via network (not AP), ready to commit settings 
		String ap = server.arg("ap");
		if (ap=="1") { // keep ap?
			String apssid = server.arg("apssid");
			String appsk  = server.arg("appsk");
			strncpy(accessPoint.ssid, apssid.c_str(), sizeof(accessPoint.ssid)); TZERO(accessPoint.ssid); 
			strncpy(accessPoint.psk,  appsk.c_str(),  sizeof(accessPoint.psk )); TZERO(accessPoint.psk ); 
			accessPoint.used = true;
		}
		else if (ap=="0") {
			accessPoint.used = false;			
		}
		else {
			server.send(200, CT_APPL_JSON, String("{\"ok\":0,\"msg\":\"parameter 'ap' missing\"}"));
			return;
		}
		
		homeNetwork.used = true;
		ESP.wdtDisable() ;
		EEPROM.begin(sizeof(*EE));
		dbg("ZZ=saving accessPoint: "); dbg(accessPoint.used); dbg(" "); dbg(accessPoint.ssid); dbg(" "); dbgln(accessPoint.psk);
		eEE_WRITE(accessPoint, EE->accessPoint);
		dbg("ZZ=saving homeNetwork: "); dbg(homeNetwork.used); dbg(" "); dbg(homeNetwork.ssid); dbg(" "); dbgln(homeNetwork.psk);
		eEE_WRITE(homeNetwork, EE->homeNetwork);
		boolean configured = true;
		eEE_WRITE(configured, EE->configured);
		EEPROM.commit();
		EEPROM.end();
		ESP.wdtEnable(5000) ;

		
		String body = "{\"ok\":1}\n\n";
		server.send(200, CT_APPL_JSON, body);
		delay(200);		
		server.client().stop();
		delay(200);		
		dbgln("ZZ=reboot scheduled");
		rebootScheduledAfter = millis()+1*1000;
		return;
	}
	server.send(404, CT_APPL_JSON, String("{\"ok\":0,\"msg\":\"unknown resource\"}"));
}


/***************************************************************
 * 
 * Web server support (normal operation)  
 * 
 */
void sendBinary(PGM_P src, size_t contentLength, const char *contentType) {
	  // For some reason, this blocks for about one second and the client
	  // receives no content. Not implemented for binary data ?????
	  WiFiClient client = server.client();
	  String head
	    = String("HTTP/1.0 200 OK\r\n") +
	             "Content-Type: "   + contentType + "\r\n"
	             "Content-Length: " + contentLength + "\r\n" 
	             "Connection: close\r\n"
	             "\r\n";
	  // TODO: Send in chunks. Avoid to allocate buffer for whole content.
	  // This is probably the reason why fan.png crashes.
	  char body[contentLength];
	  memcpy_P(body, src, contentLength);
	  
	  client.write(head.c_str(), head.length());
	  client.write(body, contentLength);
	  client.flush();
	  delay(2);
	  client.stop();	
}

void handleFavicon() {
	sendBinary(FAVICON_ICO, sizeof(FAVICON_ICO), "image/x-icon");
/*	
  // For some reason, this blocks for about one second and the client
  // receives no content. Not implemented for binary data ?????
  WiFiClient client = server.client();
  int size = sizeof(FAVICON_ICO);
  String head
    = String("HTTP/1.0 200 OK\r\n"
             "Content-Type: image/x-icon\r\n"
             "Content-Length: ") + size + "\r\n" 
             "Connection: close\r\n"
             "\r\n";
  
  char buffer[size];
  memcpy_P(buffer, (char*)FAVICON_ICO, size);
  
  client.write(head.c_str(), head.length());
  client.write(buffer, size);
  client.flush();
  delay(2);
  client.stop();
*/  
}

// TODO: makes ESP crash:
/*
void handleHomeIcon() {
	sendBinary(FAN_PNG, sizeof(FAN_PNG), "image/png");
}
*/
void handleManifest() {
	server.send_P(200, PGM_CT_APPL_JSON, MANIFEST_JSON);		
}

String upTime() {
	long secs = millis()/1000;
	long mins = secs / 60; secs %= 60;
	long hrs  = mins / 60; mins %= 60;
	long days = hrs  / 24; hrs  %= 24; 	
	String t = String(days) + "d," + hrs + "h," + mins + "m," + secs + "s";
	return t;
}

void handleApiStatus() {
	  String refresh = server.arg("refresh");
	  String filterCheck = String(state.tsps[23], 16);
	  String json = String() +
		"{\n"
		"  \"ok\":1,\n"
		"  \"build\":\"" + buildNo + "\",\n"
		"  \"reboots\":\"" + reboots + "\",\n"
	    "  \"now\":" + millis() + ",\n"
		"  \"uptime\":\"" + upTime()+ "\",\n"
	    "  \"lastMsg\":{" + 
				"\"serial\":" + state.lastMsg.serial + ""
				#ifdef WITH_DEBUG
				",\"wifi\":{\"fed\":" + state.lastMsg.wifi.fed  + ",\"debug\":" + state.lastMsg.wifi.debug + "}" 
				#endif
		   "},\n"	
	    "  \"dbg\":{\"level\":" + DEBUG_LEVEL + "},\n"
		"  \"vent\":{\"set\":" + state.ventSetpoint + ",\"override\":" + state.ventOverride + ",\"ackn\":" + state.ventAcknowledged + ",\"reported\":" + state.ventReported + ",\"relative\":" + state.ventRelative + "},\n"
		"  \"temp\":{\"supply\":"   + state.tempSupply   + ",\"exhaust\":" + state.tempExhaust + "},\n"
		"  \"status\":["            + state.status.hi           + "," + state.status.lo           + "],\n"
		"  \"faultFlagsCode\":["    + state.faultFlagsCode.hi   + "," + state.faultFlagsCode.lo   + "],\n"
		"  \"configMemberId\":["    + state.configMemberId.hi   + "," + state.configMemberId.lo   + "],\n"
		"  \"masterConfig\":["      + state.masterConfig.hi     + "," + state.masterConfig.lo     + "],\n"
		"  \"remoteParamFlags\":["  + state.remoteParamFlags.hi + "," + state.remoteParamFlags.lo + "],\n"
		"  \"filterCheck\":" + filterCheck + ",\n"
		"  \"messages\":{\n" + 
		"    \"total\":"   + (state.messages.serial+state.messages.wifi.fed+state.messages.wifi.debug) + ",\n" 
		"    \"serial\":"  + state.messages.serial + ",\n"
		#ifdef WITH_DEBUG
		"    \"wifi\":{\"fed\":" + state.messages.wifi.fed + ",\"debug\":" + state.messages.wifi.debug + "},\n"
		#endif
		"    \"invalid\":{\"len\":"  + state.messages.invalid.length + ",\"format\":" + state.messages.invalid.format + ",\"src\":" + state.messages.invalid.source + "},\n" 
		"    \"expected\":{"  + 
				"\"T\":"  + state.messages.expected.T + "," 
				"\"B\":"  + state.messages.expected.B + "," 
				"\"R\":"  + state.messages.expected.R + "," 
				"\"A\":"  + state.messages.expected.A + "},\n"
		"    \"unexpected\":{"  + 
				"\"T\":" + state.messages.unexpected.T + ","
				"\"B\":" + state.messages.unexpected.B + ","
				"\"R\":" + state.messages.unexpected.R + ","
				"\"A\":" + state.messages.unexpected.A + ","
				"\"zero\":" + state.messages.unexpected.zero + "}\n"
		"  },\n"
        "  \"freeheap\":"  + ESP.getFreeHeap() + ",\n"				
        "  \"debug\":"    + DEBUG_LEVEL + ",\n";

	  #ifdef WITH_DALLAS_TEMP_SENSORS
	  json +=					
		String(
		"  \"sensorsFound\":")+state.sensorsFound+",\n"
		"  \"lastMeasure\":"+(millis()-state.lastMeasure)+",\n"
	    "  \"sensors\":[";
	  for (int i=0; i<sizeof(state.extraTemps)/sizeof(state.extraTemps[0]); i++) {
		  if (i>0) json += ",";
		  json += state.extraTemps[i];
	  }	  
	  json += "],\n";
	  #endif
				  
	  json +=
	    "  \"tsps\":[";	  
	  // print "transparent slave parameters" as json array
	  for (int i=0; i<sizeof(state.tsps)/sizeof(state.tsps[0]); i++) {
	    if (i>0    ) json += ",";
		if (0==i%8) json+="\n    ";
	    json += state.tsps[i];
	  }
	  json += "]\n";
	  
	  json += "}\n";
	  
	  avoidCaching();
	  if (refresh!="") server.sendHeader("Refresh",  refresh);
	  server.send(200, CT_APPL_JSON, json); 				
}

boolean timeWasSet = false;

String getDate() {

	if (timeWasSet) {
		// TODO: Set time later via API call and use a millis() based RTC or add NTP support (time server)
		// Use time to fallback to basic level control e.g. if no command received via WiFi for more than 
		// a couple of hours/minutes.
	}
	return String(millis());	
}

void handleApiLevel() {
	int l = state.ventOverride;
	String json = String("{\"ok\":1,\"now\":") + getDate() + ",\"level\":"+l+"}\n";
	avoidCaching();
	server.send(200, CT_APPL_JSON, json); 						
}

#ifdef WITH_DEBUG
void handleDbgSet() {
	String level = server.arg("level");
	int old = DEBUG_LEVEL; 
	DEBUG_LEVEL = atoi(level.c_str());
	String body = String("debug level set from ") + old + " to " + DEBUG_LEVEL; 
	avoidCaching();
	server.send(200, CT_TEXT_PLAIN, body);
}

void handleDbgMsg() {
	String msg  = server.arg("msg");
	String feed = server.arg("feed");
	String rc = "no message";
	if (msg!="") {
		if (feed=="1") {
			state.lastMsg.wifi.fed = millis();
			state.messages.wifi.fed++;
			rc = onOTGWMessage(msg, true);
		}
		else {
			state.lastMsg.wifi.debug = millis();
			state.messages.wifi.debug++;
			rc = onOTGWMessage(msg, false);			
		}
	}
	avoidCaching();
	server.send(200, CT_TEXT_PLAIN, rc);
}

// Use this like: "curl http://192.168.4.1/tailf".
// It will however not receive a valid HTTP response but dump things to stdout.
void handleDbgTailF() {
	
	String cmd = server.arg("cmd");
	
	WiFiClient client = server.client();
	// Send at least a very basic HTTP header. 
	client.write("HTTP/1.0 200 OK\r\nContent-Type: text/plain\r\n\r\n");
	
	if (cmd=="") {
		client.print("No cmd sent\n");
	}
	else {		
		Serial.print(cmd); Serial.print("\r\n");
		Serial.print(cmd); Serial.print("\n");
		client.print(String("Cmd sent: '") + cmd + "'");
	}

	inputString = "";
	uint32_t maxWait = 60*1000; // 20s
	do {
		//client.print("waiting for serial data\n");
		// Wait for data on serial line to become available
		while (client.connected() && !Serial.available() && maxWait--){
			delay(1);
		}

		if (Serial.available()) {
			//client.print("Serial.available\n");
			char inChar = (char)Serial.read();
			
			//client.print("Serial.available: 0x");
			//client.print(String((int)inChar, 16)); client.print(" ");
			//client.print(String((int)inChar, 10)); client.print("\n");
			
			switch (inChar) {
				case '\r':
					// ignore CR, assume next is newline
					break;
				case '\n':
					stringComplete = true;
					break;
				default:
					inputString += inChar;	    				
			}
		}	

		if (stringComplete) {
			//client.print("stringComplete: '");
			//client.print(inputString);
			//client.print("'\n");

			state.lastMsg.serial = millis();
			state.messages.serial++;
			client.print(inputString); client.print("\n");
			String rc = onOTGWMessage(inputString, true);			
			client.print(rc);
			
			stringComplete = false;
			inputString = "";
			
			ledToggle();
		}
		
	} while (client.connected());
	//dbgln("client disconnected");

	client.stop();
	delay(10);
}
#endif // WITH_DEBUG


#ifdef WITH_PIN_TEST
void handlePinTest() {
	String str = server.arg("p");
	if (str=="") {
		server.send(404, CT_TEXT_PLAIN, "No pin specified\n");
		return;
	}

	int pin = atoi(str.c_str());
	server.send(200, CT_TEXT_PLAIN, String("Toggle pin ") + pin + "\n");
	server.client().stop();
	
	dbg("toggle start pin"); dbgln(pin);
	pinMode(pin, OUTPUT);	
	for (int i=0; i<10; i++) {
		dbg("digitalWrite("); dbg(pin); dbg(","); dbgln(i%2);   
		digitalWrite(pin, i%2);
		delay(200);
	}
	dbg("toggle stop pin"); dbgln(pin);
}
#endif // WITH_PIN_TEST


void handleApiSet() {
	String s = server.arg("level");
	s.trim();
	s.toLowerCase();

	int l = -1;
	if      (s=="high")   l=LEVEL_HIGH;
	// -------
	else if (s=="norm")   l=LEVEL_NORM;  
	else if (s=="normal") l=LEVEL_NORM;  
	// -------
	else if (s=="low")    l=LEVEL_LOW;  
	// -------
	else if (s=="off")    l=LEVEL_OFF;  
	else { 
	  l = atoi(s.c_str());
	  if (0==l && EINVAL==errno) {
		  l=-1; // inval
	  }
	  else if (l<LEVEL_OFF || l>LEVEL_HIGH) {
		  l=-1; // inval		  
	  }
	}
		
	String json;
	if (-1==l) {
		json = String("{\"ok\":0,\"now\":")+getDate()+",,\"msg\":\"invalid value\",\"value\":\""+s+"\"}\n";
	}
	else {
		int old = overrideVentSetPoint(l);
		json = String("{\"ok\":1,\"now\":")+getDate()+",\"level\":"+l+",\"value\":\""+s+"\",\"old\":" + old + "}\n";
	}
	avoidCaching();
	server.send(200, CT_APPL_JSON, json); 					
}


/*
 From Arduino docs:
 SerialEvent occurs whenever a new data comes in the
 hardware serial RX.  This routine is run between each
 time loop() runs, so using delay inside loop can delay
 response.  Multiple bytes of data may be available.
 
 For some reason, this does not work on ESP (never called).
 Therefor calling it at the beginning of the main loop()
 */
void serialEvent() {
  if (!Serial.available()) return;
  
  while (Serial.available()) {
	
    // get the new byte:
    char c = (char)Serial.read();
    //dbg("read: "); dbgln(String(inChar));
    
    // if the incoming character is a newline, set a flag
    // so the main loop can do something about it:
	switch (c) {
		// ignore CR, assume next is a newline
		case '\r': break;
		case '\n': stringComplete = true; break;
		default:   inputString += c;	    				
	}
  }
}


#ifdef WITH_DALLAS_TEMP_SENSORS
// forward declarations
void setupSensors();
void handleSensors();
#endif

#ifdef WITH_USAGE_HELP
void handleHelp() {
	String body = 
		"/index\n"
		"/setup\n"
		"/api/status?refresh=<int>\n"
		"/api/set?level=[0-3]\n"
		"/api/level\n"
			
		#ifdef WITH_DEBUG
		"/dbg/tailf?cmd=<str>\n"
		"/dbg/set?level=[0-1]\n"
		"/dbg/msg?msg=X00000000&feed=[0-1]\n"
		#endif // WITH_DEBUG
			
		#ifdef WITH_PIN_TEST
		"/pintest?pin=X\n"
		#endif // WITH_PIN_TEST
			
		"/help\n";			
	server.send(200, CT_TEXT_PLAIN, body); 						
}
#endif


void setup() {
	
  Serial.begin(9600); // as used by OTGW
  overrideVentSetPoint(LEVEL_NOOVER);  
  
  inputString.reserve(200);
  
  if (!readConfiguration()) {
	  // when EEPROM was not staring with magic,need to set up first:
	  firstTimeSetup();
	  // read again, this time it should succeed:
	  readConfiguration(); 
  }
  else {
	  // increment reboot counter in EEPROM
	  updateReboots();
  }
	  

  // init global state
  state.ventSetpoint = -1;
  // set by overrideVentSetPoint above
  //state.ventOverride = -1; 
  state.ventAcknowledged = -1;
  state.ventReported = -1;
  state.ventRelative = -1;
  state.tempSupply   = -30000; // -300C 
  state.tempExhaust  = -30000;
  state.status.hi           = state.status.lo           = -1; 		 
  state.faultFlagsCode.hi   = state.faultFlagsCode.lo   = -1;	
  state.configMemberId.hi   = state.configMemberId.lo   = -1; 
  state.masterConfig.hi     = state.masterConfig.lo     = -1;
  state.remoteParamFlags.hi = state.remoteParamFlags.lo = -1;
  for (int i=0; i<sizeof(state.tsps)/sizeof(state.tsps[0]); i++) {
	  state.tsps[i] = -1;
  }
  
  #ifdef WITH_DALLAS_TEMP_SENSORS
  setupSensors();    
  #endif
    
  setupNetwork();

  server.on("/",              handleRoot);
  server.on("/index",         handleIndex);
  server.on("/index.html",    handleIndex);
  server.on("/manifest.json", handleManifest);
  server.on("/favicon.ico",   handleFavicon);
  // TODO: makes ESP crash
  //server.on("/fan.png",       handleHomeIcon);
  server.on("/api/status",    handleApiStatus);
  server.on("/api/set",       handleApiSet);
  server.on("/api/level",     handleApiLevel);
  #ifdef WITH_WEB_SETUP
  server.on("/setup",    	  handleSetup);
  #endif
  server.on("/ajax",          handleAjax);

  #ifdef WITH_DEBUG
  server.on("/dbg/tailf",     handleDbgTailF);
  server.on("/dbg/set",       handleDbgSet);
  server.on("/dbg/msg",       handleDbgMsg);
  #endif
  
  #ifdef WITH_PIN_TEST
  server.on("/dbg/pin",       handlePinTest);
  #endif

  server.on("/help",          handleHelp);
  server.onNotFound(          handleNotFound);  
  server.begin();
}


void handleHeartbeat() {
	
	int now = millis();
	// Start blinking after things stable.
	// Seems like fiddeling with GPIO0 too early resets the device sporadically.
	if (now<5000) return;
	
	boolean light = false;
	int level     = state.ventOverride; // 0=off,...,3=high
	if (now>30000 && WiFi.status() != WL_CONNECTED) {
		light = (now/100)%2;
	}
	else if (level<0) {
		light = (now/2000)%2;
	}
	else {
		int sequence  = now%2500;           // 0,...,2500-1
		int prefix    = 500*(level+1);      // 500, 1000, 1500, 2000
		if (sequence < prefix) {
			sequence %= 500; 				// runs 1x,2x,3x or 4x through 0,..,500-1
			light = sequence<250;
		}
	}
	
	if (light != led_lit) {
		pinMode(LED_HEART, OUTPUT);
		digitalWrite(LED_HEART, light ? LOW : HIGH);
		led_lit = light;
	}
}

long loopIterations = 0;

void loop() {
	serialEvent();	
	server.handleClient();
	
	#ifdef WITH_DALLAS_TEMP_SENSORS
	if (!temperaturesRequested || 0==loopIterations%(1000*1000))  {
		handleSensors();
	}
	#endif

	if (stringComplete) {
		state.lastMsg.serial = millis();
		state.messages.serial++;
		onOTGWMessage(inputString, true);
		inputString = "";
		stringComplete = false;
	}
	
	handleHeartbeat();
	
	if (rebootScheduledAfter>-1 && rebootScheduledAfter>millis()) {
		rebootScheduledAfter = -1;
		dbgln("*********** REBOOT **********");
		delay(100);
		ESP.reset();
		delay(10000);
	}
	
	loopIterations++;
}


#ifdef WITH_DALLAS_TEMP_SENSORS
String toString(DeviceAddress& a) {
	String rtv;
	for (int i=0; i<sizeof(a)/sizeof(a[0]); i++) {
		if (i) rtv+=":";
		if (a[i]<0x10) rtv+="0";
		rtv += String(a[i], HEX);
	}
	return rtv;
}

/*
// function to print a device's resolution
void printResolution(DeviceAddress deviceAddress)
{
  Serial.print("Resolution: ");
  Serial.print(sensors.getResolution(deviceAddress));
  Serial.println();    
}
*/

// function to print the temperature for a device
void printTemperature(DeviceAddress deviceAddress)
{
  float tempC = sensors.getTempC(deviceAddress);
  dbg("Temp C: ");
  dbg(tempC);
}


void handleSensors() {
	//dbgln("ZZ=handleSensors");
	if (!temperaturesRequested) {
		sensors.requestTemperatures();
		temperaturesRequested=true;
	}
	
	state.lastMeasure = millis();	
	//dbgln("ZZ=handleSensors: reading ts");
    for (int i=0; i<state.sensorsFound; i++) {
    	//dbg("ZZ=sensors.getTempCByIndex("); dbg(i); dbgln(")");
    	float t = sensors.getTempCByIndex(i);
    	//dbg("ZZ=t="); dbgln((int)(100*t));
    	state.extraTemps[i] = 100*t;
	}
    sensors.requestTemperatures();
    temperaturesRequested=true;
	//dbgln("ZZ=handleSensors done");
}

void setupSensors() {
	//dbgln("ZZ=setupSensors");

	// Start up the library
	sensors.begin();
	sensors.setResolution(11);

	// locate devices on the bus
	//dbg("ZZ=locating devices...");
	state.sensorsFound = sensors.getDeviceCount();
	//dbg("ZZ=found "); dbg(state.sensorsFound); dbgln(" sensors.");
	
	// report parasite power requirements
	//dbg("parasite power: "); 
	//if (sensors.isParasitePowerMode()) dbgln("ON"); else dbgln("OFF");
}
#endif
1 Like

Is it OTMessage or OTMessages?

yeah, seems a pretty liberal use of the singular or plural:

      case OTMessage::MT_READ_DATA:
      case OTMessage::MT_WRITE_DATA:
int onSlaveMessage(OTMessage& m) {

this can't be the constructor, it would require the 's'

    // Construcor: takes raw 32 bit message and splits the double word into its components.
    OTMessage(int32_t dword) {

It's likely not the code that was ever compiled...

I think you are right. It seems that my sketch is not the used version in past. Some things are different... maybe the correct version went into the grave with my 15 year old Thinkpad :rofl:

I just corrected the class "OTMessages"... to "OTMessage". and some "onSlaveMessages" to "onSlaveMessage" also.

I think it will be done soon ... I only have this error: ESP8266 2.0.0 is installed.


const char DEFAULT_START_SSID[] = "OPEN";
// Initial password used to connect to it and set up networking.
// The device will normally show up as SSID "http://192.168.4.1"
// Connect to this network using the intial password and navigate your
// browser to the above URL.
#define DEFAULT_PSK  "12345678"
#define HTTP_LISTEN_PORT 80
//#define WITH_USAGE_HELP
/****************************************/
#include <ESP8266WiFi.h>
#include <ESP8266WebServer.h>
#include <EEPROM.h>
#include <errno.h>
#include "eEEPROM.h"
const String buildNo   = __DATE__ " " __TIME__;
#define LED_HEART 	0 // heartbeat 
#define LED_ONBOARD 2 // blue onboard LED / temp. sensors
#define LEVEL_NOOVER -1 // do not overrride (monitor mode)
#define LEVEL_OFF   0 // standby, ventilation off
#define LEVEL_LOW   1 // "reduced" mode (27% for mine ... may vary)
#define LEVEL_NORM  2 // "normal"  mode (55% for mine) 
#define LEVEL_HIGH  3 // "party"   mode (100%)
// result codes of onOTMessage
#define RC_OK         0	// message handled
#define RC_INV_LENGTH 1 // invalid message length (not Sxxxxxxxx)
#define RC_INV_FORMAT 2 // xxxxxxxx was not a parsable hex value
#define RC_INV_SOURCE 3 // S is not a known source (T,B.R,A)
#define RC_UNEXP_T    4 // unexpected message from T(hermostat)/controller/master
#define RC_UNEXP_B    5 // unexpected message from B(oiler)/ventilation/slave
#define RC_UNEXP_R    6 // unexpected request (sent by OTG in favor of master)
#define RC_UNEXP_A    7 // unexpected answer  (sent by OTG in favor of slave)
#define RC_UNEXP_O    8 // unexpected message from unknown source (should never occur) 

#define CT_TEXT_PLAIN  "text/plain"
#define CT_TEXT_HTML   "text/html"
#define CT_APPL_JSON   "application/json"

// Context type sent along with content stored in PROGMEM: 
const char PGM_CT_TEXT_HTML[] PROGMEM = "text/html";
const char PGM_CT_APPL_JSON[] PROGMEM = "application/json";

const char HTML_INDEX[] PROGMEM =
#include "index.html.h"
		 // The following line makes buildNo accessible for javascript:
		 "<script>var buildNo = '" __DATE__ " " __TIME__ "';</script>";
;
#ifdef WITH_WEB_SETUP
const char HTML_SETUP[] PROGMEM =
#include "setup.html.h"
;
#endif

const char MANIFEST_JSON[] PROGMEM =
#include "manifest.json.h"
;

const char FAVICON_ICO[] PROGMEM =
#include "favicon.ico.h"
;

const char FAN_PNG[] PROGMEM =
#include "fan.png.h"
;

// Conveniently zero-terminates a C-string.
#define TZERO(STR) { (STR)[sizeof((STR))-1] = 0; }

class OTMessage {

  public:
	
	// OpenTherm message types
	const static uint8_t MT_READ_DATA    = 0;
	const static uint8_t MT_WRITE_DATA   = 1;
	const static uint8_t MT_INVALID_DATA = 2;
	const static uint8_t MT_RESERVED     = 3;
	const static uint8_t MT_READ_ACK     = 4;
	const static uint8_t MT_WRITE_ACK    = 5;
	const static uint8_t MT_DATA_INVALID = 6;
	const static uint8_t MT_UNKN_DATAID  = 7;
	
	// OpenTherm data IDs (common and vendor specific)
	// see https://forum.fhem.de/index.php?topic=29762.115;wap2
	const static uint8_t DI_MASTER_CONFIG		=   2;
	const static uint8_t DI_REMOTE_PARAM_FLAGS 	=   6;
	const static uint8_t DI_STATUS 				=  70;
	const static uint8_t DI_CONTROL_SETPOINT 	=  71;
	// According to http://otgw.tclcode.com/matrix.cgi#thermostats only:
	const static uint8_t DI_FAULT_FLAGS_CODE	=  72;
	const static uint8_t DI_CONFIG_MEMBERID 	=  74;
	const static uint8_t DI_REL_VENTILATION 	=  77;
	const static uint8_t DI_SUPPLY_INLET_TEMP 	=  80;
	const static uint8_t DI_EXHAUST_INLET_TEMP 	=  82;
	const static uint8_t DI_TSP_SETTING 		=  89; 
	const static uint8_t DI_MASTER_PROD_VERS 	= 126;
	const static uint8_t DI_SLAVE_PROD_VERS 	= 127;
      
    
    static String msgTypeToStr(uint8_t t) {
      switch(t) {
        case MT_READ_DATA:    return "READ_DATA";
        case MT_WRITE_DATA:   return "WRITE_DATA";
        case MT_INVALID_DATA: return "INVALID_DATA";
        case MT_RESERVED:     return "RESERVED";
        case MT_READ_ACK:     return "READ_ACK";
        case MT_WRITE_ACK:    return "WRITE_ACK";
        case MT_DATA_INVALID: return "DATA_INVALID";
        case MT_UNKN_DATAID:  return "UNKN_DATAID";
      }
    }

    static String dataIdToStr(uint8_t id) {
      switch(id) {
		case DI_MASTER_CONFIG:		return "Master configuration";
		case DI_REMOTE_PARAM_FLAGS:	return "Remote parameter flags";
		case DI_STATUS:				return "Status V/H";
		case DI_FAULT_FLAGS_CODE:	return "Fault flags/code V/H";
		case DI_CONTROL_SETPOINT:	return "Control setpoint V/H";
		case DI_CONFIG_MEMBERID:	return "Configuration/memberid V/H";
		case DI_REL_VENTILATION:	return "Relative ventilation";
		case DI_SUPPLY_INLET_TEMP:	return "Supply inlet temperature";
		case DI_EXHAUST_INLET_TEMP:	return "Exhaust inlet temperature";    
		// TODO: Find out meaning of various "transparent slave parameters"
		// These are requested by the master in a round robin fashion from slave in the range 0-63,
		// however not some indexes are skipped. 
		case DI_TSP_SETTING:		return "TSP setting V/H";
		case DI_MASTER_PROD_VERS:	return "Master product version";
		case DI_SLAVE_PROD_VERS:	return "Slave product version";
		
		#ifdef WITH_ALL_DATAIDS
		case  3: return "SLAVE CONFIG/MEMBERID",READ,FLAG,00000000,U8,0,255,0,Yes
		case  4: return "COMMAND";
		case  5: return "FAULT FLAGS/CODE";
		case  7: return "COOLING CONTROL";
		case  8: return "TsetCH2";
		case  9: return "REMOTE ROOM SETPOINT";
		case 10: return "TSP NUMBER";
		case 11: return "TSP ENTRY";
		case 11: return "TSP ENTRY";
		case 12: return "FAULT BUFFER SIZE";
		case 13: return "FAULT BUFFER ENTRY";
		case 14: return "CAPACITY SETTING";
		case 15: return "MAX CAPACITY / MIN-MOD-LEVEL";
		case 16: return "ROOM SETPOINT";
		case 17: return "RELATIVE MODULATION LEVEL";
		case 18: return "CH WATER PRESSURE";
		case 19: return "DHW FLOW RATE";
		case 20: return "DAY - TIME";
		case 20: return "DAY - TIME";
		case 21: return "DATE";
		case 21: return "DATE";
		case 22: return "YEAR";
		case 22: return "YEAR";
		case 23: return "SECOND ROOM SETPOINT";
		case 24: return "ROOM TEMPERATURE";
		case 25: return "BOILER WATER TEMP.";
		case 26: return "DHW TEMPERATURE";
		case 27: return "OUTSIDE TEMPERATURE";
		case 28: return "RETURN WATER TEMPERATURE";
		case 29: return "SOLAR STORAGE TEMPERATURE";
		case 30: return "SOLAR COLLECTOR TEMPERATURE";
		case 31: return "SECOND BOILER WATER TEMP.";
		case 32: return "SECOND DHW TEMPERATURE";
		case 32: return "EXHAUST TEMPERATURE";
		case 48: return "DHW SETPOINT BOUNDS";
		case 49: return "MAX CH SETPOINT BOUNDS";
		case 50: return "OTC HC-RATIO BOUNDS";
		case 56: return "DHW SETPOINT";
		case 56: return "DHW SETPOINT";
		case 57: return "MAX CH WATER SETPOINT";
		case 57: return "MAX CH WATER SETPOINT";
		case 58: return "OTC HEATCURVE RATIO";
		case 58: return "OTC HEATCURVE RATIO";
		case // New ID for ventilation/heat-recovery applications
		case 73: return "DIAGNOSTIC CODE V/H";
		case 75: return "OPENTHERM VERSION V/H";
		case 76: return "VERSION & TYPE V/H";
		case 78: return "RELATIVE HUMIDITY";
		case 78: return "RELATIVE HUMIDITY";
		case 79: return "CO2 LEVEL";
		case 79: return "CO2 LEVEL";
		case 81: return "SUPPLY OUTLET TEMPERATURE";
		case 83: return "EXHAUST OUTLET TEMPERATURE";
		case 84: return "ACTUAL EXHAUST FAN SPEED";
		case 85: return "ACTUAL INLET FAN SPEED";
		case 86: return "REMOTE PARAMETER SETTINGS V/H";
		case 87: return "NOMINAL VENTIALTION VALUE";
		case 87: return "NOMINAL VENTIALTION VALUE";
		case 88: return "TSP NUMBER V/H";
		case 89: return "TSP ENTRY V/H";
		case 90: return "FAULT BUFFER SIZE V/H";
		case 91: return "FAULT BUFFER ENTRY V/H";
		case 115: return "OEM DIAGNOSTIC CODE";
		case 116: return "BURNER STARTS";
		case 116: return "BURNER STARTS";
		case 117: return "CH PUMP STATRS";
		case 117: return "CH PUMP STATRS";
		case 118: return "DHW PUMP/VALVE STARTS";
		case 118: return "DHW PUMP/VALVE STARTS";
		case 119: return "DHW BURNER STARTS";
		case 119: return "DHW BURNER STARTS";
		case 120: return "BURNER OPERATION HOURS";
		case 120: return "BURNER OPERATION HOURS";
		case 121: return "CH PUMP OPERATION HOURS";
		case 121: return "CH PUMP OPERATION HOURS";
		case 122: return "DHW PUMP/VALVE OPERATION HOURS";
		case 122: return "DHW PUMP/VALVE OPERATION HOURS";
		case 123: return "DHW BURNER HOURS";
		case 123: return "DHW BURNER HOURS";
		case 124: return "OPENTHERM VERSION MASTER";
		case 125: return "OPENTHERM VERSION SLAVE";
		#endif      
     }

     return String("?");
   }
   
    uint32_t 
		parity : 1, 
		type   : 3, 
		spare  : 8, 
		dataid : 8, 
		hi     : 8, 
		lo     : 8;
    
    // Construcor: takes raw 32 bit message and splits the double word into its components.
   OTMessage(int32_t dword) {
     parity = (0x80000000 & dword)>>31;
     type   = (0x70000000 & dword)>>28;
     spare  = (0x0f000000 & dword)>>24;
     dataid = (0x00ff0000 & dword)>>16;
     hi     = (0x0000ff00 & dword)>>8;
     lo     = (0x000000ff & dword);
    }

    String toString() {
      return String("OTMessage[")    
    		  + msgTypeToStr(type)  
			  + ",id:" + dataid + ",hi:" + hi + ",lo:" + lo  
			  + "," + dataIdToStr(dataid)
			  + "]";
    }  
};

typedef struct {
   // Use signed 16 bits to allow -1 to indicate that
   // value was not yet assigned.
   int16_t hi;  
   int16_t lo;
} Value;

// Global status 
struct {
  struct {
	  long serial; // time of last messe via serial line
      #ifdef WITH_DEBUG
	  struct { 
		  long fed;		// via URL /feedmsg?msg=...&feed=1 
		  long debug; 	// via URL /feedmsg?msg=... (parsed only, not fed)
	  } wifi;
      #endif
  }        lastMsg;         // time (millis()) when last message was received
  
  int16_t  ventSetpoint; 	 // as set by controller
  int16_t  ventOverride; 	 // as overridden by OTGW (sent to slave instead of ventSetpoint)
  int16_t  ventAcknowledged; // by the slave, sent to OTGW 
  int16_t  ventReported;     // from OTGW to controller, pretending level is as requested (ventSetpoint)
  int16_t  ventRelative; 	 // as reported by ventilation
  int16_t  tempSupply;  	 // milli Celsius (inlet)
  int16_t  tempExhaust;  	 // milli Celsius (inlet)
  int16_t  tsps[64];  	 	 // transparent slave parameters
  
  struct { 
	  Value master; 
	  Value slave; 
  }        version;			// values for DI_*_PROD_VERS
  
  Value    status; 		 
  Value    faultFlagsCode;	
  Value    configMemberId; 
  Value    masterConfig;
  Value    remoteParamFlags;
    
  struct {
	  long serial;	// total number of messages received on serial line
	  
	  struct { 
		  long fed; 	// via /feedmsg?feed=1...
		  long debug; 	// (parsed only, not fed)
	  }    wifi;
	  
	  // number of invalid messages received 
	  struct { 
		  long length;		// invalid length
		  long format;		// invalid line (pasing failed)
		  long source;		// invalid source (not T,B,R,A)
	  } invalid;
	  
	  // expected messages of various types
	  struct { 
		  long T; long B; long R; long A; 
	  	  long otgw; // otgw message response e.g. "GW: 1" replied on "GW=1" 
	  } expected;
	  
	  // unexpected messages of various types
	  struct { 
		  long T; long B; long R; long A; long zero; 
	  } unexpected;
	  
  } messages; // number of messages received
  
  #ifdef WITH_DALLAS_TEMP_SENSORS
  int sensorsFound;
  long lastMeasure;
  int16_t extraTemps[MAX_NUM_SENSORS];
  #endif
  
} state;


// Credentials used to 1. set up ESP8266 as an access point and 2. define WiFI network to conenct to.
typedef struct {
	uint8_t used;
	char    ssid[32+1];
	char    psk[32+1];	
} Credentials;


// I'm from cologne ;)
#define MAGIC 4711+3

// Everything that goes to EEPROM
struct {	
	// Detect if first time setup of EEPROM was done. 
	uint16_t    magic;
	boolean 	configured;
	// Start ESP in access point mode?
	Credentials accessPoint;
	// Network to connect to
	Credentials homeNetwork;
	// Count incremented on every reboot to enablee monitoring of device crashes.
	uint32_t reboots;
} * EE = 0;


// User committed first time setup (e.g. AP, network) and it was saved permanently.
boolean configured = false;

// A random token used during setup to authenticate committer.
char configToken[10+1];

// Time at which an attempt to connect to a network will be considered expired during setup procedure.
long connectAttemptExpires = -1;

// Remember whether temperatures have been already requested for the first time. 
boolean temperaturesRequested = false;

Credentials accessPoint;
Credentials homeNetwork;

// After setup, a reboot is necessary. However, cannot do this while handling client request,
// because this will abort the connection. Instead of this, finish sending the reponse to the
// client and schedule a reboot after millis() will exceed this value.
long rebootScheduledAfter = -1;

// A string to hold incoming serial data from OTGW.
String inputString = "";         
// wWhether the string is complete.
boolean stringComplete = false;  

ESP8266WebServer server(HTTP_LISTEN_PORT);


/***************************************************************
 * 
 * Heart beat LED 
 * 
 */

boolean led_lit=false;

void ledToggle() {
	led_lit = !led_lit;
	digitalWrite(LED_HEART, led_lit ? LOW : HIGH);
}

void ledFlash(int ms) {
	ledToggle();
	delay(ms);
	ledToggle();
}

/***************************************************************
 * 
 * Dallas temperature sensors support
 * 
 */

#ifdef WITH_DALLAS_TEMP_SENSORS

// Data wire is plugged into port 2 on the Arduino
#define ONE_WIRE_BUS 2 // GPIO2
#define TEMPERATURE_PRECISION 9


// Setup a oneWire instance to communicate with any OneWire devices (not just Maxim/Dallas temperature ICs)
OneWire oneWire(ONE_WIRE_BUS);

// Pass our oneWire reference to Dallas Temperature. 
DallasTemperature sensors(&oneWire);

#endif // WITH_DALLAS_TEMP_SENSORS

/***************************************************************
 * 
 * Error logging
 * 
 */

#ifdef WITH_ERROR_LOG

#define MAX_ERR_LEN		32
#define MAX_ERR_COUNT	10

typedef struct {
	long time; // millis
	char text[MAX_ERR_LEN] ;
} LogEntry;


//LogEntry errorLog[MAX_ERR_COUNT];
//uint8_t  errorLogIndex = 0;

// round robin buffer with last N errors
void addError(const char * text) {
/*	
	LogEntry * e = &errorLog[errorLogIndex];
	e->time = millis();
	strncpy(e->text, text, sizeof(e->text));
	e->text[sizeof(e->text)-1] = 0;
	errorLogIndex = (errorLogIndex+1)%MAX_ERR_COUNT;
	*/
} 
#else

void addError(const char * text) {}

#endif // WITH_ERROR_LOG

/***************************************************************
 * 
 * Debug support
 * 
 */

#define DBG_OFF 0 
#define DBG_ON  1
		
#ifdef WITH_DEBUG
	int DEBUG_LEVEL = DBG_ON;
	void dbg(String s)         { if (DBG_OFF<DEBUG_LEVEL) Serial.print(s);   } 
	void dbgln(String s)       { if (DBG_OFF<DEBUG_LEVEL) Serial.println(s); }
	void dbg(const char * s)   { if (DBG_OFF<DEBUG_LEVEL) Serial.print(s);   } 
	void dbgln(const char * s) { if (DBG_OFF<DEBUG_LEVEL) Serial.println(s); }
	void dbg(int i)            { if (DBG_OFF<DEBUG_LEVEL) Serial.print(i);   } 
	void dbgln(int i)          { if (DBG_OFF<DEBUG_LEVEL) Serial.println(i); }
#else
	int DEBUG_LEVEL = DBG_OFF;
	#define dbg(WHATEVER) {}
	#define dbgln(WHATEVER) {}
#endif

	
/***************************************************************
 * 
 * OpenTherm message parsing
 * 
 */

int onUnexpectedMessage(char sender, OTMessage& m) {
  //dbg("ZZ=onUnexpectedMessage:"); dbg(sender); dbg(": "); dbgln(m.toString());
  switch (sender) {
	  case 'T': state.messages.unexpected.T++; return RC_UNEXP_T;
	  case 'B': state.messages.unexpected.B++; return RC_UNEXP_B;
	  case 'R': state.messages.unexpected.R++; return RC_UNEXP_R;
	  case 'A': state.messages.unexpected.A++; return RC_UNEXP_A;
  }
  return RC_UNEXP_O;
}

int toTemp(int8_t hi, int8_t lo) {
	// TODO: Need proof if temperatue conversion is correct for t<0 
	// But wow? Even at winter time, temperature will never be <0 ... because of my setup.
	// Maybe someone else can help here?
	int sign = hi<0 ?  -1 :  1;
	int abs  = hi<0 ? -hi : hi; 
    abs = 100*abs + (100*lo/255);
    return sign*abs;
}

int onMasterMessage(OTMessage& m) {
	
	state.messages.expected.T++; {
		switch (m.type) {
		case OTMessage::MT_READ_DATA:
		case OTMessage::MT_WRITE_DATA:
			
			switch (m.dataid) {
			case OTMessage::DI_CONTROL_SETPOINT:   
				state.ventSetpoint = m.lo;
				break;
			case OTMessage::DI_MASTER_PROD_VERS:
				state.version.master.hi = m.hi;
				state.version.master.lo = m.lo;
				return RC_OK; 
			case OTMessage::DI_SLAVE_PROD_VERS:
				state.version.slave.hi = m.hi;
				state.version.slave.lo = m.lo;
				return RC_OK;
			}
			
			state.messages.expected.T++;
			return RC_OK; // ACKs from slave will be more usefull than these
		}
	}
	state.messages.expected.T--; // revoke former increment
	
	return onUnexpectedMessage('T', m);
}

int onSlaveMessage(OTMessage& m) {

	state.messages.expected.B++; {
	
		if (OTMessage::MT_UNKN_DATAID==m.type) {
			switch (m.dataid) {
			// Vitovent 300 never replies to these, never did, never will?
			// Seems like controller was designed to speak also to devices
			// that DO support this.
			case OTMessage::DI_MASTER_PROD_VERS:
				// should not be assigne here (always 0)
				//state.version.master.hi = m.hi;
				//state.version.master.lo = m.lo;
				return RC_OK; // its ok if that fails 
			case OTMessage::DI_SLAVE_PROD_VERS:
				// should not be assigne here (always 0)
				//state.version.slave.hi = m.hi;
				//state.version.slave.lo = m.lo;
				return RC_OK; // its ok if that fails
			}
		}
	
		switch (m.dataid) {
	
		case OTMessage::DI_CONTROL_SETPOINT:   
			state.ventAcknowledged = m.lo;
			return RC_OK;
		case OTMessage::DI_REL_VENTILATION:    
			state.ventRelative = m.lo;
			return RC_OK;
		case OTMessage::DI_SUPPLY_INLET_TEMP:
			state.tempSupply = toTemp(m.hi, m.lo);
			return RC_OK;
		case OTMessage::DI_EXHAUST_INLET_TEMP: 
			state.tempExhaust = toTemp(m.hi, m.lo);
			return RC_OK;
		case OTMessage::DI_TSP_SETTING:        
			// Check filter:
			// "Die Filternachricht steckt aber in dem Telegramm mit den hex codes Bxx5917yy."
			// Sent from Boiler to Thermostat:	   
			// 0000 0000 0101 1001 00010111 00000000
			// PTTT SSSS DDDD DDDD HHHHHHHH LLLLLLLL # data ID: 0x59 = 89 = DI_TSP_SETTING
			// parameter no. 0x17 = 23 
			if (0<=m.hi && m.hi<64) {
				state.tsps[m.hi] = m.lo;
			}
			return RC_OK;
		case OTMessage::DI_STATUS:
			state.status.hi = m.hi;
			state.status.lo = m.lo;
			return RC_OK;
		case OTMessage::DI_CONFIG_MEMBERID:
			state.configMemberId.hi = m.hi;
			state.configMemberId.lo = m.lo;
			return RC_OK;
		case OTMessage::DI_MASTER_CONFIG:
			state.masterConfig.hi = m.hi;
			state.masterConfig.lo = m.lo;
			return RC_OK;
		case OTMessage::DI_REMOTE_PARAM_FLAGS:
			state.remoteParamFlags.hi = m.hi;
			state.remoteParamFlags.lo = m.lo;
			return RC_OK;
		case OTMessage::DI_FAULT_FLAGS_CODE:
			state.faultFlagsCode.hi = m.hi;
			state.faultFlagsCode.lo = m.lo;
			return RC_OK;
		}
	}	 
	state.messages.expected.B--; // revoke former increment
	
	return onUnexpectedMessage('B', m); // B: boiler
}

int onRequestMessage(OTMessage& m) {
	
	state.messages.expected.R++; {
		switch (m.dataid) {
		// OTGW pretending towards the slave that controller wants to set this ventilation level,
		// e.g. R90470003	WRITE-DATA	CONTROL SETPOINT V/H: 3
		case OTMessage::DI_CONTROL_SETPOINT:
			// Hmm.. sometimes it does, sometime is does not.
			// Therefore saving this vakue now in overrideVentSetPoint() 
			//state.ventOverride = m.lo;
			return RC_OK;
		}
	} state.messages.expected.R--; // revoke former increment
	
	return onUnexpectedMessage('R', m);
}

int onAnswerMessage(OTMessage& m) {
	
	state.messages.expected.A++; {
		switch (m.dataid) {
			case OTMessage::DI_CONTROL_SETPOINT:
				state.ventReported=m.lo;
				// OTGW answering to controller, pretending the current ventilation
				// level is the one that was requested by the controller before.
				return 0;
		}
	} state.messages.expected.A--; // revoke former increment
	
	return onUnexpectedMessage('A', m);
}

int onInvalidMessageSource(char sender, OTMessage& m) {
  onUnexpectedMessage('?', m);
  return RC_INV_SOURCE;
}

int onInvalidLine(const char * line) {
  state.messages.invalid.format++;
  addError(line);
  return RC_INV_FORMAT;
}

int onInvalidLength(const char * line) {
  state.messages.invalid.length++;
  addError(line);
  return RC_INV_LENGTH;
}

String onOTGWMessage(String line, boolean feed) {
	
  const char * cstr   = line.c_str();
  char         sender = cstr[0];
  const char * hexstr = cstr+1;

  // Indicate there is some activity:
  ledFlash(5);
  
  // ACK of OTGW to command 'VS=x' or 'GW=1' messages sent by us to control fan speed level.
  if (line.startsWith("VS: ") || line.startsWith("GW: ")) {
	  if (feed) {
		  state.messages.expected.otgw++;
	  }
	  return String("ACK:'")+line + "'\n"; // adding white space causes flush??
  }
  
  // This message sent by OTGW if there is no controller connected, probably
  // to fulfill OpneTherm specs (at least 1 msg/sec sent)
  if (line.startsWith("R00000000")) {
	  state.messages.unexpected.zero++;
	  return String("NOCONN:'")+line+"\n";
  }
  
  String rc;
  int len = strlen(hexstr);
  if (8!=len) { // 1: 'B'/'T'/'A'/'R' followed by 8 hex chars
	  rc = String("INVLEN[") + len + "]:'" + cstr + "':";
	  
	  if (feed) {
		  // add real resut code if we should handle this message
		  rc += onInvalidLength(cstr);
	  }
	  else {
		  // otherwise bail out after parsing, no rc applicable
		  rc += "none";		  
	  }
	  // and newline, to ease outout to client in tailf
	  rc += "\n"; 
	  return rc;
  }
  
  //dbg("hexstr: "); dbg(hexstr); dbgln("'");
  char hs[5], ls[5];
  strncpy(hs, hexstr,   4); TZERO(hs);
  strncpy(ls, hexstr+4, 4); TZERO(ls);
  
  uint32_t hi = strtol(hs, NULL, 16);
  uint32_t lo = strtol(ls, NULL, 16);  
  uint32_t number = (hi<<16)|lo;
  if (0==number && EINVAL==errno) {
	  rc = "INVLINE:";
	  rc += cstr;
	  rc += ':';
	  if (feed) {
		  rc += onInvalidLine(cstr);
	  }
	  else {
		  rc += "none";
	  }
	  rc += "\n";
	  return rc;
  }

  OTMessage msg(number);
  rc = String(sender)+":";
  rc += msg.toString();
  rc += ':';
  
  if (feed) {	  
	  switch(sender) {
		case 'T': // from master: thermostat, controller
		  rc += onMasterMessage(msg);
		  break;
		case 'B': // from slave: boiler, ventilation
		  rc += onSlaveMessage(msg);
		  break;
		case 'A': // response (answer) replied by OTGW to the master 
		  rc += onAnswerMessage(msg);
		  break;
		case 'R': // request send by OTGW to the slave
		  rc += onRequestMessage(msg);
		  break;
		default:      
		  state.messages.invalid.source++;
		  rc += onInvalidMessageSource(sender, msg);
	  }
  }
  else {
	  rc += "none";
  }
  	  
  rc += "\n";
  return rc; 
}


int overrideVentSetPoint(int level) {
  level = level<-1 ? -1 : level>LEVEL_HIGH ? LEVEL_HIGH : level;
  if (level<0) {
	  for (int i=0; i<3; i++) {
		  // stop overriding, just monitor
		  Serial.print("GW=0\r\n"); 
		  delay(100);
	  } 
  }
  else {
	  for (int i=0; i<3; i++) {
		  Serial.print("GW=1\r\n");
		  delay(10);
		  Serial.print("VS="); Serial.print(level); Serial.print("\r\n");
		  delay(20);
	  }
  }
  dbg("ZZ=overrideVentSetPoint, old:"); dbg(state.ventOverride); dbg(" new:"); dbgln(level);
  int old = state.ventOverride; 
  state.ventOverride = level;
  return old;
}


/***************************************************************
 * 
 * EEPROM configuration and networking setup
 * 
 */

boolean readConfiguration() {
    //dbgln("ZZ=readConfiguration");
	boolean success = false;
	uint16_t magic;

	ESP.wdtDisable() ;
	EEPROM.begin(sizeof(*EE));
	eEE_READ(EE->magic, magic);
	
	//dbg("ZZ=magic="); dbgln(magic); 
	if (MAGIC==magic) {
		
		eEE_READ(EE->configured, configured);
		//dbg("ZZ=configured="); dbgln(configured); 
		
		eEE_READ(EE->accessPoint.used, accessPoint.used);
		if (accessPoint.used) {
			eEE_READ(EE->accessPoint, accessPoint);
		}
		eEE_READ(EE->homeNetwork.used, homeNetwork.used);
		if (homeNetwork.used) {
			eEE_READ(EE->homeNetwork, homeNetwork);
		}
		
		//dbgln("readConfiguration: success=true");
		success = true;
	}
	EEPROM.end();
	ESP.wdtEnable(5000);
	
	return success;
}

void firstTimeSetup() {
    dbgln("ZZ=firstTimeSetup");
	
	ESP.wdtDisable() ;
	EEPROM.begin(sizeof(*EE));
	
    dbgln("ZZ=writing credentials");
	Credentials creds = { 0, "", "" }; 
	memset(&creds, 0, sizeof(creds));
	
	eEE_WRITE(creds, EE->accessPoint.used);	
	eEE_WRITE(creds, EE->homeNetwork);
    dbgln("ZZ=credentials written");

    dbgln("ZZ=writing config flag");
	eEE_WRITE(configured, EE->configured);
    dbgln("ZZ=config flag written");
	
	// write magic to state that eeprom content is valid now
	uint16_t magic = MAGIC;
	eEE_WRITE(magic, EE->magic);

	EEPROM.commit();
	EEPROM.end();
	ESP.wdtEnable(5000); 
    dbgln("ZZ=firstTimeSetup done");
}

uint32_t reboots;

void updateReboots() {
    //dbgln("ZZ=updateReboots");

	ESP.wdtDisable() ;
	EEPROM.begin(sizeof(*EE));

	eEE_READ(EE->reboots, reboots);
	reboots++;
	//dbg("ZZ=reboots="); dbgln(reboots); 
	eEE_WRITE(reboots, EE->reboots);
	
	EEPROM.commit();
	EEPROM.end();
	ESP.wdtEnable(5000) ;
    //dbgln("ZZ=updateReboots done");
}

String toStr(IPAddress a) {
  return String("") + a[0] + '.' + a[1] + '.' + a[2] + '.' + a[3];
}

String getAPIP() {
  return toStr(WiFi.softAPIP());
}
  
void setupNetwork() {
	
	//dbgln("ZZ=setupNetwork");
	// if configured, try to connect to network first
	if (homeNetwork.used) {
		
	    int retries = 40; //*250ms=10s
	    
	    dbg("ZZ=connecting to "); dbgln(homeNetwork.ssid);
	    WiFi.mode(accessPoint.used ? WIFI_AP_STA : WIFI_STA);
		WiFi.begin(homeNetwork.ssid,  homeNetwork.psk);

	    while (WiFi.status() != WL_CONNECTED && 0<retries--) {
	      digitalWrite(LED_ONBOARD, retries % 2);
	      delay(250);
	      //dbgln("ZZ=.");
	    }
	}
	else {
		//dbgln("ZZ=no network set up");
	}
	
	boolean connected = WiFi.status()==WL_CONNECTED;	
	//dbg("setupNetwork:"); dbg(connected ? " " : " not "); dbgln("connected");
	
	// if that fails (or requested through configuration), start access point (as well)
	if (accessPoint.used || !connected) {
		
		if (!configured) {
			//dbgln("not configured => WIFI_AP_STA");
			// will need to connect to network during setup
		    WiFi.mode(WIFI_AP_STA);
		}
		else if (homeNetwork.used) {
			// was configured to connect to a network		
			//dbgln("homeNetwork used => WIFI_AP_STA");
		    WiFi.mode(WIFI_AP_STA);			
		}
		else {
			// was configured to stay access point only, no network		
			//dbgln("configure, no home network => WIFI_AP");
			WiFi.mode(WIFI_AP);
		}
	    
	    String sid = accessPoint.ssid;
	    String psk = accessPoint.psk;
	    //dbg("ZZ=accessPoint.ssid: "); dbgln(sid);
	    //dbg("ZZ=accessPoint.psk:  "); dbgln(psk);
	    if (!accessPoint.used) {
			#ifdef DEFAULT_START_SSID
	    	sid = DEFAULT_START_SSID;
			#else
	    	sid = String("http://") + getAPIP();
			#endif	    	
	    	psk = DEFAULT_PSK;
	    }
	    
	    // Prefix debug messages with (invalid) command prefix "ZZ=" in case we are really conneted to a OTGW.
	    // This way it will just print a "invalid command" message and we do not risk to confuse/crash it.
	    dbg("ZZ=ssid: "); dbgln(sid);
	    dbg("ZZ=psk:  "); dbgln(psk);
		WiFi.softAP(sid.c_str(), psk.c_str());
	}
}

/***************************************************************
 * 
 * Web server support  
 * 
 */

// Send headers with HTTP response that disable browser caching
void avoidCaching() {
	server.sendHeader(String("Pragma"),  String("no-cache"));
	server.sendHeader(String("Expires"), String("0"));
	server.sendHeader(String("Cache-Control"), String("no-cache, no-store, must-revalidate"));
}

// Send 320 redirect HTTP response
void sendRedirect(String path) {
	String body = String("<htm><body>Proceed to <a href=\"")+path+"\">"+path+"</a></body></html>";
	server.sendHeader("Location", path);		
	server.send(302, CT_TEXT_HTML, body);	
}

void handleIndex() {
	server.send_P(200, PGM_CT_TEXT_HTML, HTML_INDEX);	
}

// handle requests to /: if not yet configured, redirect to /setup, otherwise show default page 
void handleRoot() {
	#ifdef WITH_WEB_SETUP
	if (!configured) {
		sendRedirect("/setup");
		return;
	}
	#endif
	handleIndex();
}

void handleNotFound() {
	server.send(404, CT_TEXT_HTML, String("Not found: ") + server.uri());
	//handleIndex();
}

/***************************************************************
 * 
 * Web server support (first time network setup)  
 * 
 */

#ifdef WITH_WEB_SETUP
void handleSetup() {
	server.send_P(200, PGM_CT_TEXT_HTML, HTML_SETUP);		
}
#endif

String getLocalAddr() {
	IPAddress local = WiFi.localIP();
	String addr = String(local[0])+"."+local[1]+"."+local[2]+"."+local[3];	
	return addr;
}

void createConfigToken() {
	int i;
	for (i=0; i<sizeof(configToken)-1; i++) {
		configToken[i]=random('A', 'Z' + 1);
	}
	configToken[i]=0;
}

void handleAjax() {
	String action = server.arg("a");
	
	if (action=="set") { // set network name and password, start to connect
		String ssid = server.arg("ssid");
		String psk  = server.arg("psk");
		if (ssid.length()<1 || psk.length()<8) {
			server.send(200, CT_APPL_JSON, "{\"ok\":0,\"msg\":\"Name or password to short\"}\n");
			return;
		}
		else {		
			strncpy(homeNetwork.ssid, ssid.c_str(), sizeof(homeNetwork.ssid)); TZERO(homeNetwork.ssid); 
			strncpy(homeNetwork.psk,  psk.c_str(),  sizeof(homeNetwork.psk )); TZERO(homeNetwork.psk ); 
			homeNetwork.used = true;
			
			WiFi.mode(WIFI_AP_STA);
			WiFi.begin(homeNetwork.ssid, homeNetwork.psk);
			long timeout=20; // secs
			connectAttemptExpires = millis()+timeout*1000L;
			createConfigToken();
			dbg("ZZ=created configToken "); dbgln(configToken);
			server.send(200, CT_APPL_JSON, 
				String("{\"ok\":1,\"msg\":\"Ok\",\"token\":\"")+configToken+"\",\"timeout\":\""+timeout+"\"}\n");
			return;
		}
	}
	else if (action=="status") { // sent when setup.html is polling for device coming up after reboot
		String saddr = getLocalAddr();
		String caddr = toStr(server.client().remoteIP());
		server.send(200, CT_APPL_JSON, 
			String("{\"ok\":1,\"nwssid\":\"")+homeNetwork.ssid+"\",\"saddr\":\""+saddr+"\",\"caddr\":\""+caddr+"\"}\n");
		return;	
	}
	
	String token = server.arg("t");
	if (token=="MaGiC") {
		// okay (for debug purposes only)
		dbg("ZZ=debug token: "); dbgln(token);
	}
	else if (token!=configToken) {
		dbg("ZZ=token exp: "); dbgln(configToken);
		dbg("ZZ=token got: "); dbgln(token);
		server.send(200, CT_APPL_JSON, "{\"ok\":0,\"msg\":\"Token mismatch\"}\n");
		return;
	}
	
	if (action=="wait") { // setup.html is checking if device connected
		long now = millis();
		if (WiFi.status() == WL_CONNECTED) {
			String addr = getLocalAddr();
			if (80!=HTTP_LISTEN_PORT) { 
				addr+=":"; 
				addr+=HTTP_LISTEN_PORT; 
			}
			server.send(200, CT_APPL_JSON, String("{\"ok\":1,\"status\":0,\"addr\":\"")+addr+"\"}\n");
			return;
		}
		else if (connectAttemptExpires<0 || now>connectAttemptExpires) {
			server.send(200, CT_APPL_JSON, "{\"ok\":1,\"status\":2,\"msg\":\"Timeout\"}\n");
			return;
		}
		else {
			int left = (connectAttemptExpires-now)/1000;
			server.send(200, CT_APPL_JSON, String("{\"ok\":1,\"status\":1,\"msg\":\"Connecting\",\"left\":")+left+"}\n");
			return;
		}
	}
	else if (action=="save") { // setup.html connected via network (not AP), ready to commit settings 
		String ap = server.arg("ap");
		if (ap=="1") { // keep ap?
			String apssid = server.arg("apssid");
			String appsk  = server.arg("appsk");
			strncpy(accessPoint.ssid, apssid.c_str(), sizeof(accessPoint.ssid)); TZERO(accessPoint.ssid); 
			strncpy(accessPoint.psk,  appsk.c_str(),  sizeof(accessPoint.psk )); TZERO(accessPoint.psk ); 
			accessPoint.used = true;
		}
		else if (ap=="0") {
			accessPoint.used = false;			
		}
		else {
			server.send(200, CT_APPL_JSON, String("{\"ok\":0,\"msg\":\"parameter 'ap' missing\"}"));
			return;
		}
		
		homeNetwork.used = true;
		ESP.wdtDisable() ;
		EEPROM.begin(sizeof(*EE));
		dbg("ZZ=saving accessPoint: "); dbg(accessPoint.used); dbg(" "); dbg(accessPoint.ssid); dbg(" "); dbgln(accessPoint.psk);
		eEE_WRITE(accessPoint, EE->accessPoint);
		dbg("ZZ=saving homeNetwork: "); dbg(homeNetwork.used); dbg(" "); dbg(homeNetwork.ssid); dbg(" "); dbgln(homeNetwork.psk);
		eEE_WRITE(homeNetwork, EE->homeNetwork);
		boolean configured = true;
		eEE_WRITE(configured, EE->configured);
		EEPROM.commit();
		EEPROM.end();
		ESP.wdtEnable(5000) ;

		
		String body = "{\"ok\":1}\n\n";
		server.send(200, CT_APPL_JSON, body);
		delay(200);		
		server.client().stop();
		delay(200);		
		dbgln("ZZ=reboot scheduled");
		rebootScheduledAfter = millis()+1*1000;
		return;
	}
	server.send(404, CT_APPL_JSON, String("{\"ok\":0,\"msg\":\"unknown resource\"}"));
}


/***************************************************************
 * 
 * Web server support (normal operation)  
 * 
 */
void sendBinary(PGM_P src, size_t contentLength, const char *contentType) {
	  // For some reason, this blocks for about one second and the client
	  // receives no content. Not implemented for binary data ?????
	  WiFiClient client = server.client();
	  String head
	    = String("HTTP/1.0 200 OK\r\n") +
	             "Content-Type: "   + contentType + "\r\n"
	             "Content-Length: " + contentLength + "\r\n" 
	             "Connection: close\r\n"
	             "\r\n";
	  // TODO: Send in chunks. Avoid to allocate buffer for whole content.
	  // This is probably the reason why fan.png crashes.
	  char body[contentLength];
	  memcpy_P(body, src, contentLength);
	  
	  client.write(head.c_str(), head.length());
	  client.write(body, contentLength);
	  client.flush();
	  delay(2);
	  client.stop();	
}

void handleFavicon() {
	sendBinary(FAVICON_ICO, sizeof(FAVICON_ICO), "image/x-icon");
/*	
  // For some reason, this blocks for about one second and the client
  // receives no content. Not implemented for binary data ?????
  WiFiClient client = server.client();
  int size = sizeof(FAVICON_ICO);
  String head
    = String("HTTP/1.0 200 OK\r\n"
             "Content-Type: image/x-icon\r\n"
             "Content-Length: ") + size + "\r\n" 
             "Connection: close\r\n"
             "\r\n";
  
  char buffer[size];
  memcpy_P(buffer, (char*)FAVICON_ICO, size);
  
  client.write(head.c_str(), head.length());
  client.write(buffer, size);
  client.flush();
  delay(2);
  client.stop();
*/  
}

// TODO: makes ESP crash:
/*
void handleHomeIcon() {
	sendBinary(FAN_PNG, sizeof(FAN_PNG), "image/png");
}
*/
void handleManifest() {
	server.send_P(200, PGM_CT_APPL_JSON, MANIFEST_JSON);		
}

String upTime() {
	long secs = millis()/1000;
	long mins = secs / 60; secs %= 60;
	long hrs  = mins / 60; mins %= 60;
	long days = hrs  / 24; hrs  %= 24; 	
	String t = String(days) + "d," + hrs + "h," + mins + "m," + secs + "s";
	return t;
}

void handleApiStatus() {
	  String refresh = server.arg("refresh");
	  String filterCheck = String(state.tsps[23], 16);
	  String json = String() +
		"{\n"
		"  \"ok\":1,\n"
		"  \"build\":\"" + buildNo + "\",\n"
		"  \"reboots\":\"" + reboots + "\",\n"
	    "  \"now\":" + millis() + ",\n"
		"  \"uptime\":\"" + upTime()+ "\",\n"
	    "  \"lastMsg\":{" + 
				"\"serial\":" + state.lastMsg.serial + ""
				#ifdef WITH_DEBUG
				",\"wifi\":{\"fed\":" + state.lastMsg.wifi.fed  + ",\"debug\":" + state.lastMsg.wifi.debug + "}" 
				#endif
		   "},\n"	
	    "  \"dbg\":{\"level\":" + DEBUG_LEVEL + "},\n"
		"  \"vent\":{\"set\":" + state.ventSetpoint + ",\"override\":" + state.ventOverride + ",\"ackn\":" + state.ventAcknowledged + ",\"reported\":" + state.ventReported + ",\"relative\":" + state.ventRelative + "},\n"
		"  \"temp\":{\"supply\":"   + state.tempSupply   + ",\"exhaust\":" + state.tempExhaust + "},\n"
		"  \"status\":["            + state.status.hi           + "," + state.status.lo           + "],\n"
		"  \"faultFlagsCode\":["    + state.faultFlagsCode.hi   + "," + state.faultFlagsCode.lo   + "],\n"
		"  \"configMemberId\":["    + state.configMemberId.hi   + "," + state.configMemberId.lo   + "],\n"
		"  \"masterConfig\":["      + state.masterConfig.hi     + "," + state.masterConfig.lo     + "],\n"
		"  \"remoteParamFlags\":["  + state.remoteParamFlags.hi + "," + state.remoteParamFlags.lo + "],\n"
		"  \"filterCheck\":" + filterCheck + ",\n"
		"  \"messages\":{\n" + 
		"    \"total\":"   + (state.messages.serial+state.messages.wifi.fed+state.messages.wifi.debug) + ",\n" 
		"    \"serial\":"  + state.messages.serial + ",\n"
		#ifdef WITH_DEBUG
		"    \"wifi\":{\"fed\":" + state.messages.wifi.fed + ",\"debug\":" + state.messages.wifi.debug + "},\n"
		#endif
		"    \"invalid\":{\"len\":"  + state.messages.invalid.length + ",\"format\":" + state.messages.invalid.format + ",\"src\":" + state.messages.invalid.source + "},\n" 
		"    \"expected\":{"  + 
				"\"T\":"  + state.messages.expected.T + "," 
				"\"B\":"  + state.messages.expected.B + "," 
				"\"R\":"  + state.messages.expected.R + "," 
				"\"A\":"  + state.messages.expected.A + "},\n"
		"    \"unexpected\":{"  + 
				"\"T\":" + state.messages.unexpected.T + ","
				"\"B\":" + state.messages.unexpected.B + ","
				"\"R\":" + state.messages.unexpected.R + ","
				"\"A\":" + state.messages.unexpected.A + ","
				"\"zero\":" + state.messages.unexpected.zero + "}\n"
		"  },\n"
        "  \"freeheap\":"  + ESP.getFreeHeap() + ",\n"				
        "  \"debug\":"    + DEBUG_LEVEL + ",\n";

	  #ifdef WITH_DALLAS_TEMP_SENSORS
	  json +=					
		String(
		"  \"sensorsFound\":")+state.sensorsFound+",\n"
		"  \"lastMeasure\":"+(millis()-state.lastMeasure)+",\n"
	    "  \"sensors\":[";
	  for (int i=0; i<sizeof(state.extraTemps)/sizeof(state.extraTemps[0]); i++) {
		  if (i>0) json += ",";
		  json += state.extraTemps[i];
	  }	  
	  json += "],\n";
	  #endif
				  
	  json +=
	    "  \"tsps\":[";	  
	  // print "transparent slave parameters" as json array
	  for (int i=0; i<sizeof(state.tsps)/sizeof(state.tsps[0]); i++) {
	    if (i>0    ) json += ",";
		if (0==i%8) json+="\n    ";
	    json += state.tsps[i];
	  }
	  json += "]\n";
	  
	  json += "}\n";
	  
	  avoidCaching();
	  if (refresh!="") server.sendHeader("Refresh",  refresh);
	  server.send(200, CT_APPL_JSON, json); 				
}

boolean timeWasSet = false;

String getDate() {

	if (timeWasSet) {
		// TODO: Set time later via API call and use a millis() based RTC or add NTP support (time server)
		// Use time to fallback to basic level control e.g. if no command received via WiFi for more than 
		// a couple of hours/minutes.
	}
	return String(millis());	
}

void handleApiLevel() {
	int l = state.ventOverride;
	String json = String("{\"ok\":1,\"now\":") + getDate() + ",\"level\":"+l+"}\n";
	avoidCaching();
	server.send(200, CT_APPL_JSON, json); 						
}

#ifdef WITH_DEBUG
void handleDbgSet() {
	String level = server.arg("level");
	int old = DEBUG_LEVEL; 
	DEBUG_LEVEL = atoi(level.c_str());
	String body = String("debug level set from ") + old + " to " + DEBUG_LEVEL; 
	avoidCaching();
	server.send(200, CT_TEXT_PLAIN, body);
}

void handleDbgMsg() {
	String msg  = server.arg("msg");
	String feed = server.arg("feed");
	String rc = "no message";
	if (msg!="") {
		if (feed=="1") {
			state.lastMsg.wifi.fed = millis();
			state.messages.wifi.fed++;
			rc = onOTGWMessage(msg, true);
		}
		else {
			state.lastMsg.wifi.debug = millis();
			state.messages.wifi.debug++;
			rc = onOTGWMessage(msg, false);			
		}
	}
	avoidCaching();
	server.send(200, CT_TEXT_PLAIN, rc);
}

// Use this like: "curl http://192.168.4.1/tailf".
// It will however not receive a valid HTTP response but dump things to stdout.
void handleDbgTailF() {
	
	String cmd = server.arg("cmd");
	
	WiFiClient client = server.client();
	// Send at least a very basic HTTP header. 
	client.write("HTTP/1.0 200 OK\r\nContent-Type: text/plain\r\n\r\n");
	
	if (cmd=="") {
		client.print("No cmd sent\n");
	}
	else {		
		Serial.print(cmd); Serial.print("\r\n");
		Serial.print(cmd); Serial.print("\n");
		client.print(String("Cmd sent: '") + cmd + "'");
	}

	inputString = "";
	uint32_t maxWait = 60*1000; // 20s
	do {
		//client.print("waiting for serial data\n");
		// Wait for data on serial line to become available
		while (client.connected() && !Serial.available() && maxWait--){
			delay(1);
		}

		if (Serial.available()) {
			//client.print("Serial.available\n");
			char inChar = (char)Serial.read();
			
			//client.print("Serial.available: 0x");
			//client.print(String((int)inChar, 16)); client.print(" ");
			//client.print(String((int)inChar, 10)); client.print("\n");
			
			switch (inChar) {
				case '\r':
					// ignore CR, assume next is newline
					break;
				case '\n':
					stringComplete = true;
					break;
				default:
					inputString += inChar;	    				
			}
		}	

		if (stringComplete) {
			//client.print("stringComplete: '");
			//client.print(inputString);
			//client.print("'\n");

			state.lastMsg.serial = millis();
			state.messages.serial++;
			client.print(inputString); client.print("\n");
			String rc = onOTGWMessage(inputString, true);			
			client.print(rc);
			
			stringComplete = false;
			inputString = "";
			
			ledToggle();
		}
		
	} while (client.connected());
	//dbgln("client disconnected");

	client.stop();
	delay(10);
}
#endif // WITH_DEBUG


#ifdef WITH_PIN_TEST
void handlePinTest() {
	String str = server.arg("p");
	if (str=="") {
		server.send(404, CT_TEXT_PLAIN, "No pin specified\n");
		return;
	}

	int pin = atoi(str.c_str());
	server.send(200, CT_TEXT_PLAIN, String("Toggle pin ") + pin + "\n");
	server.client().stop();
	
	dbg("toggle start pin"); dbgln(pin);
	pinMode(pin, OUTPUT);	
	for (int i=0; i<10; i++) {
		dbg("digitalWrite("); dbg(pin); dbg(","); dbgln(i%2);   
		digitalWrite(pin, i%2);
		delay(200);
	}
	dbg("toggle stop pin"); dbgln(pin);
}
#endif // WITH_PIN_TEST


void handleApiSet() {
	String s = server.arg("level");
	s.trim();
	s.toLowerCase();

	int l = -1;
	if      (s=="high")   l=LEVEL_HIGH;
	// -------
	else if (s=="norm")   l=LEVEL_NORM;  
	else if (s=="normal") l=LEVEL_NORM;  
	// -------
	else if (s=="low")    l=LEVEL_LOW;  
	// -------
	else if (s=="off")    l=LEVEL_OFF;  
	else { 
	  l = atoi(s.c_str());
	  if (0==l && EINVAL==errno) {
		  l=-1; // inval
	  }
	  else if (l<LEVEL_OFF || l>LEVEL_HIGH) {
		  l=-1; // inval		  
	  }
	}
		
	String json;
	if (-1==l) {
		json = String("{\"ok\":0,\"now\":")+getDate()+",,\"msg\":\"invalid value\",\"value\":\""+s+"\"}\n";
	}
	else {
		int old = overrideVentSetPoint(l);
		json = String("{\"ok\":1,\"now\":")+getDate()+",\"level\":"+l+",\"value\":\""+s+"\",\"old\":" + old + "}\n";
	}
	avoidCaching();
	server.send(200, CT_APPL_JSON, json); 					
}


/*
 From Arduino docs:
 SerialEvent occurs whenever a new data comes in the
 hardware serial RX.  This routine is run between each
 time loop() runs, so using delay inside loop can delay
 response.  Multiple bytes of data may be available.
 
 For some reason, this does not work on ESP (never called).
 Therefor calling it at the beginning of the main loop()
 */
void serialEvent() {
  if (!Serial.available()) return;
  
  while (Serial.available()) {
	
    // get the new byte:
    char c = (char)Serial.read();
    //dbg("read: "); dbgln(String(inChar));
    
    // if the incoming character is a newline, set a flag
    // so the main loop can do something about it:
	switch (c) {
		// ignore CR, assume next is a newline
		case '\r': break;
		case '\n': stringComplete = true; break;
		default:   inputString += c;	    				
	}
  }
}


#ifdef WITH_DALLAS_TEMP_SENSORS
// forward declarations
void setupSensors();
void handleSensors();
#endif

#ifdef WITH_USAGE_HELP
void handleHelp() {
	String body = 
		"/index\n"
		"/setup\n"
		"/api/status?refresh=<int>\n"
		"/api/set?level=[0-3]\n"
		"/api/level\n"
			
		#ifdef WITH_DEBUG
		"/dbg/tailf?cmd=<str>\n"
		"/dbg/set?level=[0-1]\n"
		"/dbg/msg?msg=X00000000&feed=[0-1]\n"
		#endif // WITH_DEBUG
			
		#ifdef WITH_PIN_TEST
		"/pintest?pin=X\n"
		#endif // WITH_PIN_TEST
			
		"/help\n";			
	server.send(200, CT_TEXT_PLAIN, body); 						
}
#endif


void setup() {
	
  Serial.begin(9600); // as used by OTGW
  overrideVentSetPoint(LEVEL_NOOVER);  
  
  inputString.reserve(200);
  
  if (!readConfiguration()) {
	  // when EEPROM was not staring with magic,need to set up first:
	  firstTimeSetup();
	  // read again, this time it should succeed:
	  readConfiguration(); 
  }
  else {
	  // increment reboot counter in EEPROM
	  updateReboots();
  }
	  

  // init global state
  state.ventSetpoint = -1;
  // set by overrideVentSetPoint above
  //state.ventOverride = -1; 
  state.ventAcknowledged = -1;
  state.ventReported = -1;
  state.ventRelative = -1;
  state.tempSupply   = -30000; // -300C 
  state.tempExhaust  = -30000;
  state.status.hi           = state.status.lo           = -1; 		 
  state.faultFlagsCode.hi   = state.faultFlagsCode.lo   = -1;	
  state.configMemberId.hi   = state.configMemberId.lo   = -1; 
  state.masterConfig.hi     = state.masterConfig.lo     = -1;
  state.remoteParamFlags.hi = state.remoteParamFlags.lo = -1;
  for (int i=0; i<sizeof(state.tsps)/sizeof(state.tsps[0]); i++) {
	  state.tsps[i] = -1;
  }
  
  #ifdef WITH_DALLAS_TEMP_SENSORS
  setupSensors();    
  #endif
    
  setupNetwork();

  server.on("/",              handleRoot);
  server.on("/index",         handleIndex);
  server.on("/index.html",    handleIndex);
  server.on("/manifest.json", handleManifest);
  server.on("/favicon.ico",   handleFavicon);
  // TODO: makes ESP crash
  //server.on("/fan.png",       handleHomeIcon);
  server.on("/api/status",    handleApiStatus);
  server.on("/api/set",       handleApiSet);
  server.on("/api/level",     handleApiLevel);
  #ifdef WITH_WEB_SETUP
  server.on("/setup",    	  handleSetup);
  #endif
  server.on("/ajax",          handleAjax);

  #ifdef WITH_DEBUG
  server.on("/dbg/tailf",     handleDbgTailF);
  server.on("/dbg/set",       handleDbgSet);
  server.on("/dbg/msg",       handleDbgMsg);
  #endif
  
  #ifdef WITH_PIN_TEST
  server.on("/dbg/pin",       handlePinTest);
  #endif
}


void handleHeartbeat() {
	
	int now = millis();
	// Start blinking after things stable.
	// Seems like fiddeling with GPIO0 too early resets the device sporadically.
	if (now<5000) return;
	
	boolean light = false;
	int level     = state.ventOverride; // 0=off,...,3=high
	if (now>30000 && WiFi.status() != WL_CONNECTED) {
		light = (now/100)%2;
	}
	else if (level<0) {
		light = (now/2000)%2;
	}
	else {
		int sequence  = now%2500;           // 0,...,2500-1
		int prefix    = 500*(level+1);      // 500, 1000, 1500, 2000
		if (sequence < prefix) {
			sequence %= 500; 				// runs 1x,2x,3x or 4x through 0,..,500-1
			light = sequence<250;
		}
	}
	
	if (light != led_lit) {
		pinMode(LED_HEART, OUTPUT);
		digitalWrite(LED_HEART, light ? LOW : HIGH);
		led_lit = light;
	}
}

long loopIterations = 0;

void loop() {
	serialEvent();	
	server.handleClient();
	
	#ifdef WITH_DALLAS_TEMP_SENSORS
	if (!temperaturesRequested || 0==loopIterations%(1000*1000))  {
		handleSensors();
	}
	#endif

	if (stringComplete) {
		state.lastMsg.serial = millis();
		state.messages.serial++;
		onOTGWMessage(inputString, true);
		inputString = "";
		stringComplete = false;
	}
	
	handleHeartbeat();
	
	if (rebootScheduledAfter>-1 && rebootScheduledAfter>millis()) {
		rebootScheduledAfter = -1;
		dbgln("*********** REBOOT **********");
		delay(100);
		ESP.reset();
		delay(10000);
	}
	
	loopIterations++;
}


#ifdef WITH_DALLAS_TEMP_SENSORS
String toString(DeviceAddress& a) {
	String rtv;
	for (int i=0; i<sizeof(a)/sizeof(a[0]); i++) {
		if (i) rtv+=":";
		if (a[i]<0x10) rtv+="0";
		rtv += String(a[i], HEX);
	}
	return rtv;
}

/*
// function to print a device's resolution
void printResolution(DeviceAddress deviceAddress)
{
  Serial.print("Resolution: ");
  Serial.print(sensors.getResolution(deviceAddress));
  Serial.println();    
}
*/

// function to print the temperature for a device
void printTemperature(DeviceAddress deviceAddress)
{
  float tempC = sensors.getTempC(deviceAddress);
  dbg("Temp C: ");
  dbg(tempC);
}


void handleSensors() {
	//dbgln("ZZ=handleSensors");
	if (!temperaturesRequested) {
		sensors.requestTemperatures();
		temperaturesRequested=true;
	}
	
	state.lastMeasure = millis();	
	//dbgln("ZZ=handleSensors: reading ts");
    for (int i=0; i<state.sensorsFound; i++) {
    	//dbg("ZZ=sensors.getTempCByIndex("); dbg(i); dbgln(")");
    	float t = sensors.getTempCByIndex(i);
    	//dbg("ZZ=t="); dbgln((int)(100*t));
    	state.extraTemps[i] = 100*t;
	}
    sensors.requestTemperatures();
    temperaturesRequested=true;
	//dbgln("ZZ=handleSensors done");
}

void setupSensors() {
	//dbgln("ZZ=setupSensors");

	// Start up the library
	sensors.begin();
	sensors.setResolution(11);

	// locate devices on the bus
	//dbg("ZZ=locating devices...");
	state.sensorsFound = sensors.getDeviceCount();
	//dbg("ZZ=found "); dbg(state.sensorsFound); dbgln(" sensors.");
	
	// report parasite power requirements
	//dbg("parasite power: "); 
	//if (sensors.isParasitePowerMode()) dbgln("ON"); else dbgln("OFF");
}
#endif

error message:

In file included from C:\Users\michael\AppData\Local\Arduino15\packages\esp8266\hardware\esp8266\2.0.0\libraries\ESP8266WiFi\src\ESP8266WiFi.cpp:30:0:
C:\Users\michael\AppData\Local\Arduino15\packages\esp8266\hardware\esp8266\2.0.0/tools/sdk/include/user_interface.h:451:40: error: 'freedom_outside_cb_t' was not declared in this scope
 int wifi_register_send_pkt_freedom_cb (freedom_outside_cb_t cb);
                                        ^
C:\Users\michael\AppData\Local\Arduino15\packages\esp8266\hardware\esp8266\2.0.0/tools/sdk/include/user_interface.h:456:62: error: 'int wifi_register_send_pkt_freedom_cb(freedom_outside_cb_t)' redeclared as different kind of symbol
 int wifi_register_send_pkt_freedom_cb(freedom_outside_cb_t cb);
                                                              ^
C:\Users\michael\AppData\Local\Arduino15\packages\esp8266\hardware\esp8266\2.0.0/tools/sdk/include/user_interface.h:451:5: error: previous declaration of 'int wifi_register_send_pkt_freedom_cb'
 int wifi_register_send_pkt_freedom_cb (freedom_outside_cb_t cb);
     ^
C:\Users\michael\AppData\Local\Arduino15\packages\esp8266\hardware\esp8266\2.0.0/tools/sdk/include/user_interface.h:458:60: error: declaration of C function 'int wifi_send_pkt_freedom(uint8*, int, bool)' conflicts with
 int wifi_send_pkt_freedom(uint8 *buf, int len, bool sys_seq);
                                                            ^
C:\Users\michael\AppData\Local\Arduino15\packages\esp8266\hardware\esp8266\2.0.0/tools/sdk/include/user_interface.h:453:5: error: previous declaration 'int wifi_send_pkt_freedom(uint8**, int, bool)' here
 int wifi_send_pkt_freedom (uint8 ** buf, int len, bool sys_seq);
     ^
exit status 1
Fehler beim Kompilieren für das Board NodeMCU 0.9 (ESP-12 Module).

without the full project it's hard to see what's wrong
you might have conflicting libraries as well.

One Question. I found the right .bin compilation.
I uploaded this .bin and everything works.
But, is it possible to extract this Binary File, to get the working files for editing with arduino IDE.

No you can’t really regenerate the original C++ files and libraries from the binary. It has been compiled and optimized - it’s a one way trip.

thanks a lot for you help.
i think for future i will move to USR TCP232 Communication. Thats optional possible on my Opentherm Gateway (from nodo shop) . And networking cables are more stable than wifi :slight_smile:

This topic was automatically closed 120 days after the last reply. New replies are no longer allowed.