M5 Stack websocket client and display

The OVMS has Wifi and also exposes a websocket server from which you can stream metrics from. I've glued together some horrible code that allows displaying some information to an M5Stack Core.

The buttons also allow to trigger hand crafted events that end up being managed by some javascript code in NodeRed (that subscribes to the MQTT broker used by the Protocol V3 stack in OVMS)

ABRP Plugin for OVMS

The ABRP plugin and explanations for OVMS can be found here

Beyond setting your car model with config set usr abrp.car_model <value> and personnal API token with config set usr abrp.user_token <value> you will need to change the default url with config set usr abrp.url <value>

ABRP GET bounce

The URL will point to code that looks a bit like this:

<?php
 
$CAR_MODEL = "kia:soul:19:64:other";
$OVMS_API_KEY = "32b2162f-9599-4647-8139-66e9f9528370";
$MY_TOKEN = "xxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxxx";
$URL = "http://api.iternio.com/1/tlm/send";
 
/*
include_once('libTorque.php');
processAndSend($_SERVER['QUERY_STRING']);
*/
 
$ctx = stream_context_create(array(
    'http' => array(
        'timeout' => 1
        )
    )
);
 
file_get_contents($URL.'?'.$_SERVER['QUERY_STRING'], 0, $ctx); // <-- simply copy in the end 🤷🏻‍♂️
 
//echo "OK!";

the libTorque.php used to be old code to parse the crazy stuff that the android Torque app used (with a Bluetooth OBD dongle)… because it started by sending data to inform on how to parse subsequent url calls… (where the ABRP plugin directly embedded in the OVMS has much easier to parse data) :

37.164.143.zz - - [28/Jul/2020:10:20:21 +0200] "GET /?api_key=32b2162f-9599-4647-8139-66e9f9528370&token=xxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxxx&tlm=%7B%22utc%22%3A1595924417%2C%22soc%22%3A61%2C%22soh%22%3A100%2C%22speed%22%3A0%2C%22car_model%22%3A%22kia%3Asoul%3A19%3A64%3Aother%22%2C%22lat%22%3A%22xyz.327%22%2C%22lon%22%3A%225.048%22%2C%22alt%22%3A%220%22%2C%22ext_temp%22%3A23.5%2C%22is_charging%22%3A0%2C%22batt_temp%22%3A28%2C%22voltage%22%3A371.2%2C%22current%22%3A1.2%2C%22aux12v%22%3A13.99%2C%22power%22%3A%220.4%22%7D HTTP/1.1" 200 5 "-" "ovms/v3.1 (joul 3.2.013-304-g599b0975/ota_1/edge (build idf v3.3.2-879-g0137aef47 Jul 18 2020 15:14:00))"

The tlm parameter decodes to a clean json string:

tlm={"utc":1595924417,"soc":61,"soh":100,"speed":0,"car_model":"kia:soul:19:64:other","lat":"xyz.327","lon":"5.048","alt":"0","ext_temp":23.5,"is_charging":0,"batt_temp":28,"voltage":371.2,"current":1.2,"aux12v":13.99,"power":"0.4"}

but here's the old stuff:

<?php
 
global $paramMap;
 
include_once('vendor/autoload.php');
 
function processQueryString($qs){
  // userUnitXXXXXX
  // userShortNameXXXXXX
  // userFullNameXXXXXX
  // defaultUnitXXXXXX
  // kXXXXXX
  // eml / v / session / id / time
 
  $result = array();
 
  $m = new Memcached();
  $m->addServer('localhost', 11211);
 
  parse_str($qs, $queryParts);
  foreach($queryParts as $qKey => $qVal){
 
    if (preg_match("/^(eml|v|session|id)/", $qKey, $matches)) {
      //echo "elem ".$matches[1]." val: $qVal \n";
    }
    else
    if (preg_match("/^(userUnit|userShortName|userFullName|defaultUnit)([0-9A-Fa-f]{2,6})/", $qKey, $matches)) {
      //echo "elem ".$matches[1]." id (".$matches[2].") val: $qVal \n";
      //echo "memcache set : ".$matches[1]."_".$matches[2].", $qVal\n";
      $m->set($matches[1]."_".$matches[2], $qVal, 24*3600*21);
    }
    else
    if (preg_match("/^(k)([0-9A-Fa-f]{2,6})/", $qKey, $matches)) {
      //echo $matches[0]." : $qVal (".$matches[2].")\n";
      $id=$matches[2];
      if($m->get("userShortName_".$id)){
        $keyName=$m->get("userShortName_".$id);
        if (in_array($keyName,array(""))){
          $result[$keyName]=$qVal;
        } else
        if (in_array($keyName,array("Min Cell V  No.","Charging"))){
          $result[$keyName]=intval($qVal);
        } else // Default to float
          $result[$keyName]=floatval($qVal);
      }
      //echo $m->get($tag.$id)." : ".$qVal." ($id)\n";
    }
    else
    if (preg_match("/^(plop|profileName|profileFuelType|profileWeight|profileVe|profileFuelCost|\/)/", $qKey)) {
      //Discard
    }
    else
    if (preg_match("/^(time)/", $qKey)) {
      //echo "elem ".$matches[1]." val: $qVal \n";
      $result[$qKey]=intval($qVal);
    }
    else
    {
      //echo "unhandled : $qKey => $qVal\n";
    }
  }
  //var_dump($result);
  return $result;
}
 
function sendToInfluxDB($data){
  // directly get the database object
  $database = InfluxDB\Client::fromDSN(sprintf('influxdb://kiasoul:soulsoul@%s:%s/%s', "10.33.57.XX", 8086, "kiasoul64"));
  // get the client to retrieve other databases
  $client = $database->getClient();
 
  // Extract ["time"]
  if(isset($data["time"])){
    $timestamp=$data["time"]*1000;
    unset($data["time"]);
  }else return false;
 
  // Extract ["SOC Display"]
  if(isset($data["SOC Display"])){
    $SOCdisplay=$data["SOC Display"];
    //unset($data["SOC Display"]);
  }else return false;
 
  $points = [
  	new InfluxDB\Point(
  		'SOCdisplay',
  		$SOCdisplay,
  		['car' => 'joul'],
  		$data,
  		$timestamp
  	)
  ];
  $newPoints = $database->writePoints($points, InfluxDB\Database::PRECISION_MICROSECONDS);
  return $newPoints;
}
 
function processAndSend($qs){
  sendToInfluxDB( processQueryString($qs) );
}
 
function loadParamMap(){
 
}

OVMS V3 Protocol

Since, I have simply configured a NodeRed server, that consumes and parses ovms metrics over MQTT.

The nodered flow will look like this:

[
    {
        "id": "6b91f982.e7a02",
        "type": "tab",
        "label": "Kia Joul",
        "disabled": false,
        "info": ""
    },
    {
        "id": "cc30af3f.2a19b",
        "type": "mqtt in",
        "z": "6b91f982.e7a02",
        "name": "joul@mqtt",
        "topic": "joul/#",
        "qos": "2",
        "datatype": "auto",
        "broker": "a83ee8f6.376a98",
        "x": 290,
        "y": 340,
        "wires": [
            [
                "f1bcd001.877828",
                "e75ed110.0438e"
            ]
        ]
    },
    {
        "id": "f1bcd001.877828",
        "type": "debug",
        "z": "6b91f982.e7a02",
        "name": "",
        "active": false,
        "tosidebar": true,
        "console": false,
        "tostatus": false,
        "complete": "true",
        "targetType": "full",
        "x": 870,
        "y": 400,
        "wires": []
    },
    {
        "id": "e75ed110.0438e",
        "type": "function",
        "z": "6b91f982.e7a02",
        "name": "mqtt2influx",
        "func": "msg.topic = msg.topic.replace(/\\//g,\"_\").replace(\"joul_\",\"\").replace(\"darthmaul_\",\"\");\n\n\nif(msg.topic.startsWith(\"metric_\")){\n    msg.topic = msg.topic.replace(\"metric_\",\"\");\n    var returnmsg = {};\n    \n    returnmsg.payload = [\n        {\n            measurement: msg.topic,\n            fields: {},\n            timestamp: new Date()\n        }\n    ];\n    \n    if(msg.payload.includes(\",\")){\n        var a = msg.payload.split(\",\"), i;\n        for (i = 0; i < a.length; i++) {\n            returnmsg.payload[0].fields[i]=parseFloat(a[i]);\n        }\n    }else{\n        returnmsg.payload[0].fields[msg.topic] = parseFloat(msg.payload);\n    }\n    \n    return returnmsg;\n}",
        "outputs": 1,
        "noerr": 0,
        "x": 530,
        "y": 520,
        "wires": [
            [
                "f1bcd001.877828",
                "d072b101.e61ea"
            ]
        ]
    },
    {
        "id": "d072b101.e61ea",
        "type": "influxdb batch",
        "z": "6b91f982.e7a02",
        "influxdb": "b0eafaa2.4c476",
        "precision": "s",
        "retentionPolicy": "",
        "name": "kiasoul@influx",
        "x": 900,
        "y": 680,
        "wires": []
    },
    {
        "id": "9eb93ce5.41b728",
        "type": "mqtt in",
        "z": "6b91f982.e7a02",
        "name": "darthmaul@mqtt",
        "topic": "darthmaul/#",
        "qos": "2",
        "datatype": "auto",
        "broker": "a83ee8f6.376a98",
        "x": 220,
        "y": 500,
        "wires": [
            [
                "e75ed110.0438e",
                "f1bcd001.877828"
            ]
        ]
    },
    {
        "id": "a83ee8f6.376a98",
        "type": "mqtt-broker",
        "z": "",
        "name": "joul",
        "broker": "10.33.57.YY",
        "port": "1883",
        "clientid": "",
        "usetls": false,
        "compatmode": false,
        "keepalive": "15",
        "cleansession": true,
        "birthTopic": "",
        "birthQos": "0",
        "birthPayload": "",
        "closeTopic": "",
        "closeQos": "0",
        "closePayload": "",
        "willTopic": "",
        "willQos": "0",
        "willPayload": ""
    },
    {
        "id": "b0eafaa2.4c476",
        "type": "influxdb",
        "z": "",
        "hostname": "10.33.57.XX",
        "port": "8086",
        "protocol": "http",
        "database": "kiasoul64",
        "name": "kiasoul@influx",
        "usetls": false,
        "tls": ""
    }
]

It listens to joul/# and darthmaul/# because I ended up renaming the car after a while.

 Nodered

Easier to understand, here's what's inside mqtt2influx:

msg.topic = msg.topic.replace(/\//g,"_").replace("joul_","").replace("darthmaul_","");
 
 
if(msg.topic.startsWith("metric_")){
    msg.topic = msg.topic.replace("metric_","");
    var returnmsg = {};
 
    returnmsg.payload = [
        {
            measurement: msg.topic,
            fields: {},
            timestamp: new Date()
        }
    ];
 
    if(msg.payload.includes(",")){
        var a = msg.payload.split(","), i;
        for (i = 0; i < a.length; i++) {
            returnmsg.payload[0].fields[i]=parseFloat(a[i]);
        }
    }else{
        returnmsg.payload[0].fields[msg.topic] = parseFloat(msg.payload);
    }
 
    return returnmsg;
}

Once you're all set, you can get precise metric data and plot it over in your Chronograf dashboard:

 Chronograf

Or even nicer with Grafana

 Kia Dashboard in Grafana

Fun with NodeRed

I ended up with the first tweeting car ;-)