intermediate: xdr mappings
This commit is contained in:
parent
fbc955cd53
commit
d927861cdf
|
@ -9,8 +9,11 @@ from datetime import datetime
|
|||
Import("env")
|
||||
GEN_DIR='generated'
|
||||
CFG_FILE='web/config.json'
|
||||
FILES=['web/index.html',CFG_FILE,'web/index.js','web/index.css']
|
||||
XDR_FILE='web/xdrconfig.json'
|
||||
FILES=['web/index.html',CFG_FILE,XDR_FILE,'web/index.js','web/index.css']
|
||||
CFG_INCLUDE='GwConfigDefinitions.h'
|
||||
XDR_INCLUDE='GwXdrTypeMappings.h'
|
||||
|
||||
|
||||
def basePath():
|
||||
#see: https://stackoverflow.com/questions/16771894/python-nameerror-global-name-file-is-not-defined
|
||||
|
@ -90,11 +93,71 @@ def generateCfg():
|
|||
os.unlink(outfile)
|
||||
raise
|
||||
|
||||
def generateXdrMappings():
|
||||
outfile=os.path.join(outPath(),XDR_INCLUDE)
|
||||
infile=os.path.join(basePath(),XDR_FILE)
|
||||
if isCurrent(infile,outfile):
|
||||
return
|
||||
print("creating %s"%XDR_INCLUDE)
|
||||
oh=None
|
||||
|
||||
with open(infile,"rb") as fp:
|
||||
jdoc=json.load(fp)
|
||||
try:
|
||||
with open(outfile,"w") as oh:
|
||||
oh.write("static GwXDRTypeMapping* typeMappings[]={\n")
|
||||
first=True
|
||||
for cat in jdoc:
|
||||
item=jdoc[cat]
|
||||
cid=item.get('id')
|
||||
if cid is None:
|
||||
continue
|
||||
tc=item.get('type')
|
||||
if tc is not None:
|
||||
if first:
|
||||
first=False
|
||||
else:
|
||||
oh.write(",\n")
|
||||
oh.write(" new GwXDRTypeMapping(%d,%d,0)"%(cid,tc))
|
||||
fields=item.get('fields')
|
||||
if fields is None:
|
||||
continue
|
||||
idx=0
|
||||
for fe in fields:
|
||||
if not isinstance(fe,dict):
|
||||
continue
|
||||
tc=fe.get('t')
|
||||
id=fe.get('v')
|
||||
if id is None:
|
||||
id=idx
|
||||
idx+=1
|
||||
l=fe.get('l') or ''
|
||||
if tc is None or id is None:
|
||||
continue
|
||||
if first:
|
||||
first=False
|
||||
else:
|
||||
oh.write(",\n")
|
||||
oh.write(" new GwXDRTypeMapping(%d,%d,%d) /*%s*/"%(cid,tc,id,l))
|
||||
oh.write("\n")
|
||||
oh.write("};\n")
|
||||
except Exception as e:
|
||||
if oh:
|
||||
try:
|
||||
oh.close()
|
||||
except:
|
||||
pass
|
||||
os.unlink(outfile)
|
||||
raise
|
||||
|
||||
|
||||
|
||||
if not checkDir():
|
||||
sys.exit(1)
|
||||
for f in FILES:
|
||||
print("compressing %s"%f)
|
||||
compressFile(f)
|
||||
generateCfg()
|
||||
generateXdrMappings()
|
||||
version="dev"+datetime.now().strftime("%Y%m%d")
|
||||
env.Append(CPPDEFINES=[('GWDEVVERSION',version)])
|
||||
|
|
|
@ -1,100 +1,183 @@
|
|||
#include "GwXDRMappings.h"
|
||||
#include "N2kMessages.h"
|
||||
double PtoBar(double v){
|
||||
if (N2kIsNA(v)) return v;
|
||||
return v/100000L;
|
||||
double PtoBar(double v)
|
||||
{
|
||||
if (N2kIsNA(v))
|
||||
return v;
|
||||
return v / 100000L;
|
||||
}
|
||||
double BarToP(double v){
|
||||
if (N2kIsNA(v)) return v;
|
||||
return v*100000L;
|
||||
double BarToP(double v)
|
||||
{
|
||||
if (N2kIsNA(v))
|
||||
return v;
|
||||
return v * 100000L;
|
||||
}
|
||||
GwXDRType * types[]={
|
||||
new GwXDRType(GwXDRType::PRESS,"P","P"),
|
||||
new GwXDRType(GwXDRType::PRESS,"P","B",
|
||||
PtoBar,
|
||||
BarToP),
|
||||
new GwXDRType(GwXDRType::VOLT,"U","V"),
|
||||
new GwXDRType(GwXDRType::AMP,"I","A"),
|
||||
NULL
|
||||
};
|
||||
double ltrTom3(double v)
|
||||
{
|
||||
if (N2kIsNA(v))
|
||||
return v;
|
||||
return v / 1000.0;
|
||||
}
|
||||
double m3ToL(double v)
|
||||
{
|
||||
if (N2kIsNA(v))
|
||||
return v;
|
||||
return v * 1000.0;
|
||||
}
|
||||
double ph2ps(double v)
|
||||
{
|
||||
if (N2kIsNA(v))
|
||||
return v;
|
||||
return v * 3600.0;
|
||||
}
|
||||
double ps2ph(double v)
|
||||
{
|
||||
if (N2kIsNA(v))
|
||||
return v;
|
||||
return v / 3600.0;
|
||||
}
|
||||
//https://www.nmea.org/Assets/20190916%20Standards%20Update%20NMEA%20Conferencev2.pdf
|
||||
GwXDRType *types[] = {
|
||||
new GwXDRType(GwXDRType::PRESS, "P", "P"),
|
||||
new GwXDRType(GwXDRType::PRESS, "P", "B",
|
||||
PtoBar,
|
||||
BarToP),
|
||||
new GwXDRType(GwXDRType::VOLT, "U", "V"),
|
||||
new GwXDRType(GwXDRType::AMP, "I", "A"),
|
||||
new GwXDRType(GwXDRType::TEMP, "C", "K"),
|
||||
new GwXDRType(GwXDRType::TEMP, "C", "C", CToKelvin, KelvinToC),
|
||||
new GwXDRType(GwXDRType::HUMID, "H", "P"), //percent
|
||||
new GwXDRType(GwXDRType::VOLPERCENT, "V", "P"),
|
||||
new GwXDRType(GwXDRType::VOLUME, "V", "M", m3ToL, ltrTom3),
|
||||
new GwXDRType(GwXDRType::FLOW, "R", "I", ps2ph, ph2ps),
|
||||
//important to have 2x NULL!
|
||||
NULL,
|
||||
NULL};
|
||||
|
||||
static GwXDRType *findType(GwXDRType::TypeCode type, int *start = NULL)
|
||||
{
|
||||
int from = 0;
|
||||
if (start != NULL)
|
||||
from = *start;
|
||||
if (types[from] == NULL)
|
||||
return NULL;
|
||||
int i = from;
|
||||
for (; types[i] != NULL; i++)
|
||||
{
|
||||
if (types[i]->code == type)
|
||||
{
|
||||
if (start != NULL)
|
||||
*start = i + 1;
|
||||
return types[i];
|
||||
}
|
||||
}
|
||||
if (start != NULL)
|
||||
*start = i;
|
||||
return NULL;
|
||||
}
|
||||
|
||||
String GwXDRMappingDef::toString(){
|
||||
String rt="";
|
||||
rt+=String((int)category);
|
||||
rt+=",";
|
||||
rt+=String((int)direction);
|
||||
rt+=",";
|
||||
rt+=String(selector);
|
||||
rt+=",";
|
||||
rt+=String(field);
|
||||
rt+=",";
|
||||
rt+=String(instanceMode);
|
||||
rt+=",";
|
||||
rt+=String(instanceId);
|
||||
#include "GwXdrTypeMappings.h"
|
||||
|
||||
static GwXDRType::TypeCode findTypeMapping(GwXDRCategory category, int field)
|
||||
{
|
||||
for (int i = 0; typeMappings[i] != NULL; i++)
|
||||
{
|
||||
if (typeMappings[i]->fieldIndex == field &&
|
||||
typeMappings[i]->category == category)
|
||||
{
|
||||
return typeMappings[i]->type;
|
||||
}
|
||||
}
|
||||
return GwXDRType::UNKNOWN;
|
||||
}
|
||||
|
||||
String GwXDRMappingDef::toString()
|
||||
{
|
||||
String rt = "";
|
||||
rt += String((int)category);
|
||||
rt += ",";
|
||||
rt += String((int)direction);
|
||||
rt += ",";
|
||||
rt += String(selector);
|
||||
rt += ",";
|
||||
rt += String(field);
|
||||
rt += ",";
|
||||
rt += String(instanceMode);
|
||||
rt += ",";
|
||||
rt += String(instanceId);
|
||||
return rt;
|
||||
}
|
||||
bool GwXDRMappingDef::handleToken(String tok,int index,GwXDRMappingDef *def){
|
||||
switch(index){
|
||||
bool GwXDRMappingDef::handleToken(String tok, int index, GwXDRMappingDef *def)
|
||||
{
|
||||
switch (index)
|
||||
{
|
||||
int i;
|
||||
case 0:
|
||||
//category
|
||||
i=tok.toInt();
|
||||
if (i< XDRTEMP || i > XDRENGINE) return false;
|
||||
def->category=(GwXDRCategory)i;
|
||||
return true;
|
||||
case 1:
|
||||
//direction
|
||||
i=tok.toInt();
|
||||
if (i < GwXDRMappingDef::M_DISABLED ||
|
||||
i>= GwXDRMappingDef::M_LAST) return false;
|
||||
def->direction=(GwXDRMappingDef::Direction)i;
|
||||
return true;
|
||||
case 2:
|
||||
//selector
|
||||
//TODO: check selector?
|
||||
i=tok.toInt();
|
||||
def->selector=i;
|
||||
return true;
|
||||
case 3:
|
||||
//field
|
||||
i=tok.toInt();
|
||||
def->field=i;
|
||||
return true;
|
||||
case 4:
|
||||
//instance mode
|
||||
i=tok.toInt();
|
||||
if (i < GwXDRMappingDef::IS_SINGLE ||
|
||||
i>= GwXDRMappingDef::IS_LAST) return false;
|
||||
def->instanceMode=(GwXDRMappingDef::InstanceMode)i;
|
||||
return true;
|
||||
case 5:
|
||||
//instance id
|
||||
i=tok.toInt();
|
||||
def->instanceId=i;
|
||||
return true;
|
||||
default:
|
||||
break;
|
||||
case 0:
|
||||
//category
|
||||
i = tok.toInt();
|
||||
if (i < XDRTEMP || i > XDRENGINE)
|
||||
return false;
|
||||
def->category = (GwXDRCategory)i;
|
||||
return true;
|
||||
case 1:
|
||||
//direction
|
||||
i = tok.toInt();
|
||||
if (i < GwXDRMappingDef::M_DISABLED ||
|
||||
i >= GwXDRMappingDef::M_LAST)
|
||||
return false;
|
||||
def->direction = (GwXDRMappingDef::Direction)i;
|
||||
return true;
|
||||
case 2:
|
||||
//selector
|
||||
//TODO: check selector?
|
||||
i = tok.toInt();
|
||||
def->selector = i;
|
||||
return true;
|
||||
case 3:
|
||||
//field
|
||||
i = tok.toInt();
|
||||
def->field = i;
|
||||
return true;
|
||||
case 4:
|
||||
//instance mode
|
||||
i = tok.toInt();
|
||||
if (i < GwXDRMappingDef::IS_SINGLE ||
|
||||
i >= GwXDRMappingDef::IS_LAST)
|
||||
return false;
|
||||
def->instanceMode = (GwXDRMappingDef::InstanceMode)i;
|
||||
return true;
|
||||
case 5:
|
||||
//instance id
|
||||
i = tok.toInt();
|
||||
def->instanceId = i;
|
||||
return true;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
GwXDRMappingDef *GwXDRMappingDef::fromString(String s){
|
||||
int found=0;
|
||||
int last=0;
|
||||
int index=0;
|
||||
GwXDRMappingDef *rt=new GwXDRMappingDef();
|
||||
while ((found = s.indexOf(',',last)) >= 0){
|
||||
String tok=s.substring(last,found);
|
||||
if (!handleToken(tok,index,rt)){
|
||||
GwXDRMappingDef *GwXDRMappingDef::fromString(String s)
|
||||
{
|
||||
int found = 0;
|
||||
int last = 0;
|
||||
int index = 0;
|
||||
GwXDRMappingDef *rt = new GwXDRMappingDef();
|
||||
while ((found = s.indexOf(',', last)) >= 0)
|
||||
{
|
||||
String tok = s.substring(last, found);
|
||||
if (!handleToken(tok, index, rt))
|
||||
{
|
||||
delete rt;
|
||||
return NULL;
|
||||
}
|
||||
last=found+1;
|
||||
last = found + 1;
|
||||
index++;
|
||||
}
|
||||
if (last < s.length()){
|
||||
String tok=s.substring(last);
|
||||
if (!handleToken(tok,index,rt)){
|
||||
if (last < s.length())
|
||||
{
|
||||
String tok = s.substring(last);
|
||||
if (!handleToken(tok, index, rt))
|
||||
{
|
||||
delete rt;
|
||||
return NULL;
|
||||
}
|
||||
|
@ -109,16 +192,73 @@ GwXDRMappings::GwXDRMappings(GwLog *logger, GwConfigHandler *config)
|
|||
}
|
||||
|
||||
#define MAX_MAPPINGS 100
|
||||
void GwXDRMappings::begin(){
|
||||
void GwXDRMappings::begin()
|
||||
{
|
||||
char namebuf[10];
|
||||
for (int i=0;i<MAX_MAPPINGS;i++){
|
||||
snprintf(namebuf,9,"XDR%d",i);
|
||||
namebuf[9]=0;
|
||||
GwConfigInterface *cfg=config->getConfigItem(String(namebuf));
|
||||
if (cfg){
|
||||
GwXDRMappingDef *mapping=GwXDRMappingDef::fromString(cfg->asCString());
|
||||
if (mapping){
|
||||
LOG_DEBUG(GwLog::DEBUG,"read xdr mapping %s",mapping->toString().c_str());
|
||||
/*
|
||||
build our mappings
|
||||
for each defined mapping we fetch the type code and type definition
|
||||
and create a mapping entries in our 2 maps:
|
||||
n2kmap: map category,selector and field index to a mapping
|
||||
n183map: map transducer name, transducer type and transducer unit to a mapping
|
||||
a #nnn will be stripped from the transducer name
|
||||
entries in the map are lists of mappings as potentially
|
||||
we have different mappings for different instances
|
||||
*/
|
||||
for (int i = 0; i < MAX_MAPPINGS; i++)
|
||||
{
|
||||
snprintf(namebuf, 9, "XDR%d", i);
|
||||
namebuf[9] = 0;
|
||||
GwConfigInterface *cfg = config->getConfigItem(String(namebuf));
|
||||
if (cfg)
|
||||
{
|
||||
GwXDRMappingDef *def = GwXDRMappingDef::fromString(cfg->asCString());
|
||||
if (def)
|
||||
{
|
||||
int typeIndex = 0;
|
||||
LOG_DEBUG(GwLog::DEBUG, "read xdr mapping %s", def->toString().c_str());
|
||||
//n2k: find first matching type mapping
|
||||
GwXDRType::TypeCode code = findTypeMapping(def->category, def->field);
|
||||
if (code == GwXDRType::UNKNOWN)
|
||||
{
|
||||
LOG_DEBUG(GwLog::DEBUG, "no type mapping for %s", def->toString().c_str());
|
||||
continue;
|
||||
}
|
||||
GwXDRType *type = findType(code, &typeIndex);
|
||||
if (!type)
|
||||
{
|
||||
LOG_DEBUG(GwLog::DEBUG, "no type definition for %s", def->toString().c_str());
|
||||
continue;
|
||||
}
|
||||
long n2kkey=def->n2kKey();
|
||||
auto it=n2kMap.find(n2kkey);
|
||||
if (it == n2kMap.end()){
|
||||
LOG_DEBUG(GwLog::DEBUG,"insert mapping with key %ld",n2kkey);
|
||||
GwXDRMapping *mapping = new GwXDRMapping(def, type);
|
||||
n2kMap[n2kkey]=mapping;
|
||||
}
|
||||
else{
|
||||
LOG_DEBUG(GwLog::DEBUG,"append mapping with key %ld",n2kkey);
|
||||
it->second->addMappingDef(def);
|
||||
}
|
||||
//for nmea0183 there could be multiple entries
|
||||
//as potentially there are different units that we can handle
|
||||
//so after we inserted the definition we do additional type lookups
|
||||
while (type != NULL){
|
||||
String n183key=GwXDRMappingDef::n183key(def->xdrName,
|
||||
type->xdrtype,type->xdrunit);
|
||||
auto it=n183Map.find(n183key);
|
||||
if (it == n183Map.end()){
|
||||
LOG_DEBUG(GwLog::DEBUG,"insert mapping with n183key %s",n183key.c_str());
|
||||
n183Map[n183key]=new GwXDRMapping(def,type);
|
||||
}
|
||||
else{
|
||||
LOG_DEBUG(GwLog::DEBUG,"append mapping with n183key %s",n183key.c_str());
|
||||
it->second->addMappingDef(def);
|
||||
}
|
||||
type=findType(code,&typeIndex);
|
||||
if (! type) break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -4,6 +4,7 @@
|
|||
#include "GWConfig.h"
|
||||
#include <WString.h>
|
||||
#include <vector>
|
||||
#include <map>
|
||||
//enum must match the defines in xdrconfig.json
|
||||
typedef enum {
|
||||
XDRTEMP=0,
|
||||
|
@ -24,7 +25,13 @@ class GwXDRType{
|
|||
PRESS=0,
|
||||
PERCENT=1,
|
||||
VOLT=2,
|
||||
AMP=3
|
||||
AMP=3,
|
||||
TEMP=4,
|
||||
HUMID=5,
|
||||
VOLPERCENT=6,
|
||||
VOLUME=7,
|
||||
FLOW=8,
|
||||
UNKNOWN=99
|
||||
}TypeCode;
|
||||
typedef double (* convert)(double);
|
||||
TypeCode code;
|
||||
|
@ -45,11 +52,11 @@ class GwXDRTypeMapping{
|
|||
GwXDRCategory category;
|
||||
int fieldIndex;
|
||||
GwXDRType::TypeCode type;
|
||||
GwXDRTypeMapping(GwXDRCategory category,
|
||||
GwXDRTypeMapping(int category,
|
||||
int fieldIndex,
|
||||
GwXDRType::TypeCode type){
|
||||
this->category=category;
|
||||
this->type=type;
|
||||
int type){
|
||||
this->category=(GwXDRCategory) category;
|
||||
this->type=(GwXDRType::TypeCode)type;
|
||||
this->fieldIndex=fieldIndex;
|
||||
}
|
||||
};
|
||||
|
@ -91,44 +98,63 @@ class GwXDRMappingDef{
|
|||
category=XDRTEMP;
|
||||
}
|
||||
String toString();
|
||||
static GwXDRMappingDef *fromString(String s);
|
||||
static GwXDRMappingDef *fromString(String s);
|
||||
//we allow 100 entities of code,selector and field nid
|
||||
static long n2kKey(GwXDRCategory category, int selector, int field)
|
||||
{
|
||||
long rt = (int)category;
|
||||
if (selector < 0)
|
||||
selector = 0;
|
||||
rt = rt * 100 + selector;
|
||||
if (field < 0)
|
||||
field = 0;
|
||||
rt = rt * 100 * field;
|
||||
return rt;
|
||||
}
|
||||
long n2kKey(){
|
||||
return n2kKey(category,selector,field);
|
||||
}
|
||||
static String n183key(String xdrName, String xdrType, String xdrUnit)
|
||||
{
|
||||
String rt = xdrName;
|
||||
rt += ",";
|
||||
rt += xdrType;
|
||||
rt += ",";
|
||||
rt += xdrUnit;
|
||||
return rt;
|
||||
}
|
||||
typedef std::vector<GwXDRMappingDef*> MappingList;
|
||||
private:
|
||||
static bool handleToken(String tok,int index,GwXDRMappingDef *def);
|
||||
};
|
||||
class GwXDRMapping{
|
||||
public:
|
||||
GwXDRMappingDef *definition;
|
||||
GwXDRMappingDef::MappingList definitions;
|
||||
GwXDRType *type;
|
||||
GwXDRMapping(GwXDRMappingDef *definition,GwXDRType *type){
|
||||
this->definition=definition;
|
||||
this->definitions.push_back(definition);
|
||||
this->type=type;
|
||||
}
|
||||
//we allow 100 entities of code,selector and field nid
|
||||
static long n2kKey(GwXDRType::TypeCode code,int selector,int field){
|
||||
long rt=(int)code;
|
||||
if (selector < 0) selector=0;
|
||||
rt=rt*100+selector;
|
||||
if (field < 0) field=0;
|
||||
rt=rt*100*field;
|
||||
return rt;
|
||||
}
|
||||
static String n183key(String xdrName,String xdrType,String xdrUnit){
|
||||
String rt=xdrName;
|
||||
rt+=",";
|
||||
rt+=xdrType;
|
||||
rt+=",";
|
||||
rt+=xdrUnit;
|
||||
return rt;
|
||||
void addMappingDef(GwXDRMappingDef *definition){
|
||||
this->definitions.push_back(definition);
|
||||
}
|
||||
typedef std::map<String,GwXDRMapping*> N138Map;
|
||||
typedef std::map<long,GwXDRMapping*> N2KMap;
|
||||
};
|
||||
|
||||
class GwXDRMappings{
|
||||
private:
|
||||
GwLog *logger;
|
||||
GwConfigHandler *config;
|
||||
GwXDRMapping::N138Map n183Map;
|
||||
GwXDRMapping::N2KMap n2kMap;
|
||||
public:
|
||||
GwXDRMappings(GwLog *logger,GwConfigHandler *config);
|
||||
void begin();
|
||||
//get the mappings
|
||||
//the returned mapping will exactly contain one mapping def
|
||||
GwXDRMapping getMapping(String xName,String xType,String xUnit);
|
||||
GwXDRMapping getMapping(GwXDRCategory category,int selector,int field=0,int instance=0);
|
||||
|
||||
};
|
||||
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
{
|
||||
"Temperature": {
|
||||
"id":0,
|
||||
"type":4,
|
||||
"selector": [
|
||||
{
|
||||
"l": "SeaTemperature(0)",
|
||||
|
@ -66,6 +67,7 @@
|
|||
},
|
||||
"Humidity": {
|
||||
"id":1,
|
||||
"type":5,
|
||||
"selector": [
|
||||
{
|
||||
"l": "InsideHumidity(0)",
|
||||
|
@ -83,6 +85,7 @@
|
|||
},
|
||||
"Pressure": {
|
||||
"id":2,
|
||||
"type":0,
|
||||
"selector": [
|
||||
{
|
||||
"l": "Atmospheric(0)",
|
||||
|
@ -194,8 +197,8 @@
|
|||
}
|
||||
],
|
||||
"fields":[
|
||||
{"l":"Level","v":0,"t":-1},
|
||||
{"l":"Capacity","v":1,"t":-1}
|
||||
{"l":"Level","v":0,"t":6},
|
||||
{"l":"Capacity","v":1,"t":7}
|
||||
]
|
||||
},
|
||||
"DCType": {
|
||||
|
@ -290,24 +293,24 @@
|
|||
"d":"127508",
|
||||
"id":9,
|
||||
"fields":[
|
||||
"BatteryVoltage",
|
||||
"BatteryCurrent",
|
||||
"BatteryTemperature"
|
||||
{"l":"BatteryVoltage","t":2},
|
||||
{"l":"BatteryCurrent","t":3},
|
||||
{"l":"BatteryTemperature","t":4}
|
||||
]
|
||||
},
|
||||
"Engine":{
|
||||
"d":"127489",
|
||||
"id":10,
|
||||
"fields":[
|
||||
"EngineOilPress",
|
||||
"EngineOilTemp",
|
||||
"EngineCoolantTemp",
|
||||
"FuelRate",
|
||||
"EngineHours",
|
||||
"EngineCoolantPress",
|
||||
"EngineFuelPress",
|
||||
"EngineLoad",
|
||||
"EngineTorque"
|
||||
{"l":"EngineOilPress","t":0},
|
||||
{"l":"EngineOilTemp","t":4},
|
||||
{"l":"EngineCoolantTemp","t":4},
|
||||
{"l":"FuelRate","t":8},
|
||||
{"l":"EngineHours","t":99},
|
||||
{"l":"EngineCoolantPress","t":0},
|
||||
{"l":"EngineFuelPress","t":0},
|
||||
{"l":"EngineLoad","t":99},
|
||||
{"l":"EngineTorque","t":99}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue