This is an old revision of the document!


Beware, this is highly unreliable… it's just good enough for me ;)

/*
 * WebSocketClient.ino
 *
 *  Created on: 24.05.2015
 *
 */
 
//#include <Arduino.h>
#include <M5Stack.h>
#include <ArduinoJson.h>
#include <WiFi.h>
#include <WiFiMulti.h>
#include <WiFiClientSecure.h>
 
#include <HttpClient.h>
 
#include <WebSocketsClient.h>
 
 
WiFiMulti WiFiMulti;
WebSocketsClient webSocket;
 
HttpClient htclient;
 
bool localTesting = true;
bool genRandomVals = false;
 
const char* OVMSUSER = "admin";
const char* OVMSPASS = "OVMSP455w0rd"; // Change this to your password
 
const int LCDBrightnessSteps[] = {60, 120, 250};
int LCDBrightnessStep = 0;
 
const int heightBarGraphSOC = 72;
const int widthBarGraphSOC = 300;
const int leftBarGraphSOC  = (320-widthBarGraphSOC-4)/2;
const int topBarGraphSOC   = 0;
const int SOCtextSize  = 7;
const int SOCtextWidth = M5.Lcd.textWidth("99%")*SOCtextSize;
 
const int heightBarGraphPWR = 32;
const int widthBarGraphPWR = 220;
const int PWRtextSize  = 3;
const int PWRtextWidth = M5.Lcd.textWidth("100kW")*PWRtextSize;
const int leftBarGraphPWR  = (320-widthBarGraphPWR-4);
const int topBarGraphPWR   = 80;
 
const int topTextLineRange = topBarGraphPWR + heightBarGraphPWR + 16;
const int topTextLineConsumption = topTextLineRange + 32;
const int topTextLineTemps = topTextLineConsumption + 32;
 
const float MAXINPOWER  = 100;
const float MAXOUTPOWER = 200;
const float MAXRANGEPWR = 300;
 
float vbsoc         = 90.5;
float vbrangeideal  = 402 ;
float vbrangefull   = 450 ;
float vetemp        = 13  ;
float vbtemp        = 15  ;
float xknbinlettemp = 14  ;
float vbpower       = 1   ;
float vbconsumption = 160 ;
 
const uint16_t DARKERCYAN = 0x0124;
 
const size_t jsonCapacity = JSON_OBJECT_SIZE(1) + JSON_OBJECT_SIZE(38) + 910;
DynamicJsonDocument jsonDoc(jsonCapacity);
DeserializationError payloadDecodeError;
uint8_t M5triggerUpdate = 0;
 
uint8_t dummyCounter = 0;
 
unsigned long prevTimer     = millis();
unsigned long displayTimer  = millis();
unsigned long spentTimer       = 0;
bool          slowRefreshTrigger = false;
unsigned long slowRefreshTimer = 0;
unsigned long slowRefreshMax   = 5000;
bool          mildRefreshTrigger = false;
unsigned long mildRefreshTimer = 0;
unsigned long mildRefreshMax   = 2000;
bool          fastRefreshTrigger = false;
unsigned long fastRefreshTimer = 0;
unsigned long fastRefreshMax   = 100;
 
#define USE_SERIAL Serial
 
void hexdump(const void *mem, uint32_t len, uint8_t cols = 16) {
	const uint8_t* src = (const uint8_t*) mem;
	USE_SERIAL.printf("\n[HEXDUMP] Address: 0x%08X len: 0x%X (%d)", (ptrdiff_t)src, len, len);
	for(uint32_t i = 0; i < len; i++) {
		if(i % cols == 0) {
			USE_SERIAL.printf("\n[0x%08X] 0x%08X: ", (ptrdiff_t)src, i);
		}
		USE_SERIAL.printf("%02X ", *src);
		src++;
	}
	USE_SERIAL.printf("\n");
}
 
void draw_vbsoc(float vbsoc) {
  M5.Lcd.setTextSize(SOCtextSize);
  uint16_t SOCcolor = GREEN;
  uint16_t SOCtextColor = 0x0720; // Slightly Darker Green
  uint16_t BorderColor = WHITE;
  uint16_t BGcolor = 0x2970; //BLUE;
  if(vbsoc<25){ SOCcolor = YELLOW; SOCtextColor = YELLOW; BorderColor = YELLOW; }
  if(vbsoc<10){ SOCcolor = RED; SOCtextColor = RED; BorderColor = RED; BGcolor = MAROON; }
  M5.Lcd.fillRoundRect(leftBarGraphSOC, topBarGraphSOC, widthBarGraphSOC+6, heightBarGraphSOC+4 , 6, BGcolor);
  //M5.Lcd.fillRect(leftBarGraphSOC, topBarGraphSOC, widthBarGraphSOC+4, heightBarGraphSOC+4 , BGcolor);
  M5.Lcd.drawRoundRect(leftBarGraphSOC, topBarGraphSOC, widthBarGraphSOC+4, heightBarGraphSOC+4 , 6, BorderColor);
  M5.Lcd.fillRoundRect(leftBarGraphSOC+2, topBarGraphSOC+2, widthBarGraphSOC*(vbsoc/100), heightBarGraphSOC , 4, SOCcolor);
  if(vbsoc>=50){
    M5.Lcd.setTextColor(BLACK);
    M5.Lcd.setCursor(leftBarGraphSOC+6+3*vbsoc-SOCtextWidth-6, topBarGraphSOC+14);
    M5.Lcd.print(vbsoc,0); M5.Lcd.print('%');
  }else{
    M5.Lcd.setTextColor(SOCtextColor);
    M5.Lcd.setCursor(leftBarGraphSOC+6+3*vbsoc+8, topBarGraphSOC+12);
    M5.Lcd.print(vbsoc,0); M5.Lcd.print('%');
  }
  //M5.Lcd.printf("%*s", 5, vbsoc);
  M5triggerUpdate++;
}
 
void draw_vbpower(float vbpower) {
  M5.Lcd.setTextSize(PWRtextSize);
  uint16_t PWRcolor;
 
  uint16_t BorderColor = LIGHTGREY; //WHITE;
  uint16_t BGcolor = 0x528A;
 
  // Draw empty full bar
  M5.Lcd.fillRoundRect(leftBarGraphPWR, topBarGraphPWR, widthBarGraphPWR+6, heightBarGraphPWR+4 , 6, BGcolor);
  //M5.Lcd.fillRect(leftBarGraphSOC, topBarGraphSOC, widthBarGraphSOC+4, heightBarGraphSOC+4 , BGcolor);
  M5.Lcd.drawRoundRect(leftBarGraphPWR, topBarGraphPWR, widthBarGraphPWR+4, heightBarGraphPWR+4 , 6, BorderColor);
  float barOffset;
  float barWidth;
 
  if(vbpower>0){
    PWRcolor = YELLOW; if(vbpower>25) PWRcolor = RED;
    barWidth = (widthBarGraphPWR*vbpower)/MAXRANGEPWR;
    // (220*140)/220
    barOffset = (widthBarGraphPWR*(float)MAXINPOWER)/MAXRANGEPWR;
  } else {
    PWRcolor = GREEN;
    barWidth = (widthBarGraphPWR*-vbpower)/MAXRANGEPWR;
    barOffset = (widthBarGraphPWR*((float)MAXINPOWER+vbpower))/MAXRANGEPWR;
  }
  // USE_SERIAL.print(barOffset); USE_SERIAL.print(" "); USE_SERIAL.println(barWidth);
 
  // Draw Power Bar
  //M5.Lcd.fillRoundRect(leftBarGraphPWR+2+barOffset, topBarGraphPWR+2, barWidth, heightBarGraphPWR , 4, PWRcolor);
  M5.Lcd.fillRect(leftBarGraphPWR+2+barOffset, topBarGraphPWR+2, barWidth, heightBarGraphPWR, PWRcolor);
 
  // Clean text
  M5.Lcd.fillRoundRect(4, topBarGraphPWR, PWRtextWidth, heightBarGraphPWR+4 , 6, BLACK);
 
  // Place 0 line
  M5.Lcd.drawLine(
    leftBarGraphPWR+2+(widthBarGraphPWR*(float)MAXINPOWER)/MAXRANGEPWR, 
    topBarGraphPWR, 
    leftBarGraphPWR+2+(widthBarGraphPWR*(float)MAXINPOWER)/MAXRANGEPWR, 
    topBarGraphPWR+heightBarGraphPWR+2, 
    BorderColor);
 
  M5.Lcd.setCursor(4, topBarGraphPWR+8);
  if(vbpower>0){
    M5.Lcd.setTextColor(RED);
  }else{
    M5.Lcd.setTextColor(GREEN);
  }
  if(vbpower > 0   && vbpower < 10  ) M5.Lcd.print(" ");
  if(vbpower > -10 && vbpower < 100 ) M5.Lcd.print(" ");
 
  M5.Lcd.print(vbpower,0); M5.Lcd.print("kW");
  //M5.Lcd.drawFloat(vbpower, 0, 4, topBarGraphPWR+8);
  //M5.Lcd.drawNumber(vbpower, 4, topBarGraphPWR+8);
  //M5.Lcd.print('kW');
  //M5.Lcd.printf("%*s", 5, vbpower);
  M5triggerUpdate++;
}
 
void draw_vbrange() {
  M5.Lcd.setTextSize(3);
  M5.lcd.fillRect(0, topTextLineRange, 320,32, DARKERCYAN);
  M5.Lcd.setCursor(4, topTextLineRange+4);
  M5.Lcd.setTextColor(LIGHTGREY);
  M5.Lcd.print("range: "); M5.Lcd.print(vbrangeideal,0); M5.Lcd.print("/");
  M5.Lcd.print(vbrangefull,0); M5.Lcd.print(" km");
  M5triggerUpdate++;
}
 
void draw_vbconsumption() {
  uint16_t textColor = GREEN;
  if(vbconsumption>150) textColor = YELLOW;
  if(vbconsumption>200) textColor = RED;
  M5.Lcd.setTextSize(3);
  M5.lcd.fillRect(0, topTextLineConsumption, 320,32, DARKERCYAN);
  M5.Lcd.setCursor(4, topTextLineConsumption+4);
  M5.Lcd.setTextColor(LIGHTGREY);
  M5.Lcd.print("cons: ");
  M5.Lcd.setTextColor(textColor);
  M5.Lcd.print(vbconsumption,0);
  M5.Lcd.setTextColor(LIGHTGREY);
  M5.Lcd.print(" Wh/km");
  M5triggerUpdate++;
}
 
void draw_temps() {
  M5.Lcd.setTextSize(2);
  M5.lcd.fillRect(0, topTextLineTemps,320 , 24, DARKERCYAN);
  M5.Lcd.setCursor(4, topTextLineTemps+4);
  M5.Lcd.setTextColor(LIGHTGREY); M5.Lcd.print("temps ext:");
  M5.Lcd.print(vetemp,0);
  M5.Lcd.setTextColor(LIGHTGREY); M5.Lcd.print(" in:");
  M5.Lcd.print(xknbinlettemp,0);
  M5.Lcd.setTextColor(LIGHTGREY); M5.Lcd.print(" bt:");
  M5.Lcd.print(vbtemp,0);
  M5triggerUpdate++;
}
 
void tickerBox(uint16_t tickerColor) {
  M5.lcd.fillRect(0,240-6 ,4 ,4 , tickerColor);
  M5triggerUpdate++;
}
 
void changeLCDBrightness() {
  LCDBrightnessStep++;
  if(LCDBrightnessStep==3) LCDBrightnessStep=0;
  M5.Lcd.setBrightness(LCDBrightnessSteps[LCDBrightnessStep]);
  if(localTesting) USE_SERIAL.printf("[LCD] Setting Brightness to : %u (step %u)\n", LCDBrightnessSteps[LCDBrightnessStep], LCDBrightnessStep);
}
 
void sendButtonA() {
  sendEvent("event+raise+M5.button.A");
}
 
void sendButtonB() {
  sendEvent("event+raise+M5.button.B");
}
 
void sendEvent(char* event){
  M5.Lcd.fillRect(0, 220, 320, 32, BLUE);
  M5.Lcd.setCursor(6, 222);
  M5.Lcd.setTextColor(YELLOW);
  M5.Lcd.setTextSize(2);
  M5.Lcd.print(event);
  M5.update();
  M5triggerUpdate = 0;
 
  WiFiClient client;
  if (!client.connect("192.168.4.1", 80 )) {
    Serial.println("connection failed");
    return;
  }
  String url = "/api/execute?apikey=";
    url += OVMSPASS;
    url += "&command=";
    url += event;
  USE_SERIAL.print("Requesting URL: ");
  USE_SERIAL.println(url);
 
  client.print(String("GET ") + url + " HTTP/1.1\r\n" +
    "Host: 192.168.4.1\r\n" +
    "Connection: close\r\n\r\n");
    unsigned long timeout = millis();
    while (client.available() == 0) {
      if (millis() - timeout > 5000) {
          USE_SERIAL.println(">>> Client Timeout !");
          client.stop();
          return;
      }
  }
  while(client.available()) {
    String line = client.readStringUntil('\r');
    USE_SERIAL.print(line);
  }
 
  Serial.println();
  Serial.println("closing connection");
 
  M5.Lcd.fillRect(0, 220, 320, 32, BLACK);
  M5.update();
  M5triggerUpdate = 0;
}
 
void webSocketEvent(WStype_t type, uint8_t * payload, size_t length) {
 
  // Timer control
  prevTimer=displayTimer;
  displayTimer=millis();
  spentTimer=displayTimer-prevTimer;
  slowRefreshTimer+=spentTimer;
  mildRefreshTimer+=spentTimer;
  fastRefreshTimer+=spentTimer;
  slowRefreshTrigger = (slowRefreshTimer>=slowRefreshMax) ? true : false;
  mildRefreshTrigger = (mildRefreshTimer>=mildRefreshMax) ? true : false;
  fastRefreshTrigger = (fastRefreshTimer>=fastRefreshMax) ? true : false;
 
	switch(type) {
		case WStype_DISCONNECTED:
			USE_SERIAL.printf("[WSc] Disconnected!\n");
      tickerBox(RED); M5.update();
			break;
		case WStype_CONNECTED:
      tickerBox(YELLOW); M5.update();
			if(localTesting) USE_SERIAL.printf("[WSc] Connected to url: %s\n", payload);
			// send message to server when Connected
			//webSocket.sendTXT("M5 Stack Connected");
      sendEvent("event+raise+M5.websocket.connected");
			break;
		case WStype_TEXT:
			//if(localTesting) USE_SERIAL.printf("[WSc] get text: %s\n", payload);
 
      payloadDecodeError = deserializeJson(jsonDoc, payload);
      // Test if parsing succeeds.
      if (payloadDecodeError) {
        USE_SERIAL.printf("deserializeJson() failed: \n");
        USE_SERIAL.println(payloadDecodeError.c_str());
      }else{
        //USE_SERIAL.printf("parseObject() succeeded\n");
        JsonObject metrics = jsonDoc["metrics"];
        if (!metrics.isNull()) {
 
          if(localTesting && genRandomVals){
            metrics["v.b.soc"] = random(0,200)/2;
            metrics["v.b.power"] = random(-MAXINPOWER*10,MAXOUTPOWER*10+1)/10;
            metrics["v.b.range.ideal"] = random(360*10,400*10)/10;
            metrics["v.b.range.ideal"] = random(360*10,400*10)/10;
            metrics["v.e.temp"] = random(12*10,17*10)/10;
            metrics["v.b.temp"] = random(18*10,21*10)/10;
            metrics["xkn.b.inlet.temp"] = random(14*10,19*10)/10;
            metrics["v.b.consumption"] = random(130,240);
          }
 
          // v.b.soc
          if(metrics.containsKey("v.b.soc") && metrics["v.b.soc"]!=vbsoc){
            vbsoc = metrics["v.b.soc"];
            //USE_SERIAL.printf("[json] v.b.soc: %f\n", vbsoc);
          }
          if(slowRefreshTrigger) draw_vbsoc(vbsoc);
 
          // v.b.range.ideal
          if(metrics.containsKey("v.b.range.ideal") && metrics["v.b.range.ideal"]!=vbrangeideal){
            vbrangeideal = metrics["v.b.range.ideal"];
          }
          // v.b.range.full
          if(metrics.containsKey("v.b.range.full") && metrics["v.b.range.full"]!=vbrangefull){
            vbrangefull = metrics["v.b.range.full"];
          }
          if(slowRefreshTrigger) draw_vbrange();
 
          // v.e.temp
          if(metrics.containsKey("v.e.temp") && metrics["v.e.temp"]!=vetemp){
            vetemp = metrics["v.e.temp"];
          }
          // v.b.temp
          if(metrics.containsKey("v.b.temp") && metrics["v.b.temp"]!=vbtemp){
            vbtemp = metrics["v.b.temp"];
          }
          // xkn.b.inlet.temp
          if(metrics.containsKey("xkn.b.inlet.temp") && metrics["xkn.b.inlet.temp"]!=xknbinlettemp){
            xknbinlettemp = metrics["xkn.b.inlet.temp"];
          }
          if(mildRefreshTrigger) draw_temps();
 
          // xkn.b.inlet.temp
          if(metrics.containsKey("v.b.consumption") && metrics["v.b.consumption"]!=vbconsumption){
            vbconsumption = metrics["v.b.consumption"];
          }
          if(mildRefreshTrigger) draw_vbconsumption();
 
          // v.b.power
          if(metrics.containsKey("v.b.power")){
            vbpower = metrics["v.b.power"];
            //USE_SERIAL.printf("[json] v.b.power: %f\n", vbpower);
          }
 
          if(fastRefreshTrigger) draw_vbpower(vbpower);
 
          if(metrics.containsKey("m.monotonic")){
            //JsonVariant _mmonotonic=metrics["m.monotonic"];
            //int mmonotonic=_mmonotonic.as<int>();
            int mmonotonic=metrics["m.monotonic"].as<int>();
            if(mmonotonic % 2){
              tickerBox(DARKGREEN);
            }else{
              tickerBox(BLUE);
            }
          }
 
          if(M5.BtnA.wasReleased()) sendButtonA();
          if(M5.BtnB.wasReleased()) sendButtonB();
          if(M5.BtnC.wasReleased()) changeLCDBrightness();
 
        }
 
        if(M5triggerUpdate){
          M5.update();
          M5triggerUpdate = 0;
        }
        if(slowRefreshTrigger){
          slowRefreshTimer=0;
          //USE_SERIAL.printf("slowRefreshTimer reset\n");
        }
        if(mildRefreshTrigger){
          mildRefreshTimer=0;
          //USE_SERIAL.printf("mildRefreshTimer reset\n");
        }
        if(fastRefreshTrigger){
          fastRefreshTimer=0;
          //USE_SERIAL.printf("fastRefreshTimer reset\n");
        }
      }
			// send message to server
			// webSocket.sendTXT("message here");
			break;
		case WStype_BIN:
			USE_SERIAL.printf("[WSc] get binary length: %u\n", length);
			hexdump(payload, length);
 
			// send data to server
			// webSocket.sendBIN(payload, length);
			break;
		case WStype_ERROR:			
		case WStype_FRAGMENT_TEXT_START:
		case WStype_FRAGMENT_BIN_START:
		case WStype_FRAGMENT:
		case WStype_FRAGMENT_FIN:
			break;
	}
 
}
 
void setup() {
 
  M5.begin();
  M5.Power.begin();
  M5.Lcd.setBrightness(LCDBrightnessSteps[LCDBrightnessStep+1]);
	// USE_SERIAL.begin(921600);
	// USE_SERIAL.begin(115200);
  USE_SERIAL.begin(230400);
 
	//Serial.setDebugOutput(true);
	USE_SERIAL.setDebugOutput(true);
 
	USE_SERIAL.println();
 
  //M5.Lcd.fillScreen(BLACK);
 
  ledcDetachPin(SPEAKER_PIN);
  pinMode(SPEAKER_PIN, INPUT);
 
	for(uint8_t t = 5; t > 0; t--) {
		USE_SERIAL.printf("[SETUP] BOOT WAIT %d...\n", t);
		USE_SERIAL.flush();
		delay(500);
	}
 
  WiFiMulti.addAP("OVMSWifi", "W1f1P4ssW0rd"); // Change This to your OVMS
 
  USE_SERIAL.printf("[Wifi] Added APs...\n");
 
	WiFi.disconnect();
  if(WiFiMulti.run() != WL_CONNECTED){
    M5.Lcd.fillRect(0, 220, 320, 32, BLUE);
    M5.Lcd.setCursor(6, 222);
    M5.Lcd.setTextColor(WHITE);
    M5.Lcd.setTextSize(2);
    M5.Lcd.print("[Wifi] Connecting...");
    USE_SERIAL.printf("[Wifi] Connecting...\n");
    dummyCounter = 1;
    tickerBox(RED);
    while(WiFiMulti.run() != WL_CONNECTED) {
      USE_SERIAL.printf("[Wifi] Connecting... count %d\n",dummyCounter++);
      M5.Lcd.print(".");
      M5.update();
      delay(500);
      if (M5.BtnC.wasReleased()) changeLCDBrightness();
    }
 
    //hexdump(WiFi.SSID().c_str(), sizeof(WiFi.SSID().c_str()));
    USE_SERIAL.printf("[Wifi] Connected: %s\n",WiFi.SSID().c_str());
 
    M5.Lcd.fillRect(0, 220, 320, 32, BLACK);
    M5.update();
  }
 
 
	// server address, port and URL
	webSocket.begin("192.168.4.1", 80, "/");
 
	// event handler
	webSocket.onEvent(webSocketEvent);
 
	// use HTTP Basic Authorization this is optional remove if not needed
	webSocket.setAuthorization(OVMSUSER, OVMSPASS);
 
	// try ever 5000 again if connection has failed
	webSocket.setReconnectInterval(2000);
 
}
 
void loop() {
	webSocket.loop();
  if(WiFiMulti.run() != WL_CONNECTED){
    M5.Lcd.fillRect(0, 220, 320, 32, BLUE);
    M5.Lcd.setCursor(6, 222);
    M5.Lcd.setTextColor(WHITE);
    M5.Lcd.setTextSize(2);
    M5.Lcd.print("[Wifi] Reconnecting...");
     while(WiFiMulti.run() != WL_CONNECTED) {
      USE_SERIAL.printf("[Wifi] Connecting...\n");
      M5.Lcd.print(".");
      M5.update();
      delay(500);
      if (M5.BtnC.wasReleased()) changeLCDBrightness();
    }
 
    USE_SERIAL.printf("Wifi: %s\n",WiFi.SSID().c_str());
 
    M5.Lcd.fillRect(0, 220, 320, 32, BLUE);
    M5.Lcd.setCursor(6, 222);
    M5.Lcd.print("[Wifi] : ");
    M5.Lcd.print(WiFi.SSID().c_str());
    M5.update();
    M5.Speaker.mute();
    delay(500);
    M5.Lcd.fillRect(0, 220, 320, 32, BLACK);
    M5.update();
  }
  //M5.Lcd.fillScreen(BLACK);
  if(M5triggerUpdate){
    M5.update();
    M5triggerUpdate = 0;
  }
  //USE_SERIAL.printf("end of loop\n");
}