前回までの作業で、OBD2経由でのデータモニター&データロガーの作成は完結したはずだった...のだが、モデルチェンジを実施してみよう。
まず、前ユニットの不満点と要改善点を整理してみよう。特に3番が致命的な問題となっているぞww
えーと、「いつ?」を明確にするためには時計を実装しなけりゃならないんだよね。RTCを組み込む?スマホとかと連動する? なんかめんどくさそう。
じゃぁ、「どこで?」ってのはどうしよう...あれ? そういや世の中にはGPSって素晴らしい技術があるじゃないか! それにGPSってグリニッジ標準時も取得できるので「いつ?」の問題も同時に解決する! これがいいんじゃないの?! で、いつもお世話になってる秋月のネットショップで探すと、アンテナ一体型のいいのがある。これにしよう!
ただし、それなりのサイズのモジュールなので筐体が大きくなって、ステアリングコラムの上に付けると邪魔になりそう...だったらいっそのこと、取付場所を一般的な追加メーターみたいにダッシュボードとかにして液晶もでっかくすればいいんじゃないの?
以上から、ODB2データロガーVer2の製作仕様を決定する。
今回仕様の注目点は下記だ。
項目 | メーカー | 機器 | 備考 |
---|---|---|---|
制御 | Esspressif | ESP32-DevKitC | |
表示 | 不明 | 3.5カラー液晶(タッチパネル付)MSP3520 | SPI制御、SDカードスロット付、480x320x65Kcolor |
電源 | 日清紡マイクロデバイス | NJM7805SDL1-TE1 | 12V->5V |
CANトランシーバー | Microchip Technology | MCP2562FD-E/P | |
GPS受信機器キット | 秋月電子(太陽誘電) | AE-GYSFDMAXB | 販売終了したかも。電池バックアップはしない。 |
電子ブザー | DB Products | UDB-05LFPN | いちおう、警告音を出せるように |
CdSセル | 不明 | パーツボックスに転がってた。暗抵抗15kΩ程度 | 照度制御用 |
タクトスイッチ | ZHEJIANG JIANFU ELECTRONICS | TVDP01G73AB | ハードウェアリセット用 |
タクトスイッチキャップ | COSLAND | KTSC-61-R | |
3.5mm4極ミニジャック基板取付用 | AVVICON ELECTRONIC | MJ-4PP-9 | 用途:12V、GND、CAN-H、CANL-L |
4極ミニプラグケーブル | 不明 | 1m品。片側はバラケーブル | |
OBD2オスケーブル | 不明 | ケーブル長20cm程度。端部バラケーブル | |
抵抗 | 不明 | 各種 | |
コンデンサー | 不明 | 各種 | |
ピンヘッダー | 不明 | 適量 |
さくさくっと設計を進めよう。だけど、毎回のことながら、回路図は見栄えよくするのに苦労する。部品配置は実際の形に似せて配置したうえで配線は美しく...って無理ww わかりゃいいのよ。
今回は液晶が大きくなったため、基板面積も大きくできる。前回は2階建てになってけど、今回は1枚のみにして、かつ、組立後も筐体が薄くできるような部品配置を心がけよう。
出来上がった基板に部品を実装していくよ。今回は、以下の方針だ。
以前の調査では、「とにかくサポートしてるパラメータIDを教えて」への回答から取得できるデータを整理したが、今回は念のため、同じ方法で、ECU10(CAN-ID=18DA10F1)に絞り込んだ問い合わせしてみよう。
レスポンス | 後に続く 実データ バイト数 | モード | パラメータID | 実データ各ビット 左からパラメータIDのアドレス0x01-0x20 | ダミー |
---|---|---|---|---|---|
064100B63CA81355 | 06 | 41 | 00 | 1011 0110 0011 1100 1010 1000 0001 0011 | 55 |
064120B005A01155 | 06 | 41 | 20 | 1011 0000 0000 0101 1010 0000 0001 0001 | 55 |
06414072C28C0155 | 06 | 41 | 40 | 0111 0010 1100 0010 1000 1100 0000 0001 | 55 |
0641600711400155 | 06 | 41 | 60 | 0000 0111 0001 0001 0100 0000 0000 0001 | 55 |
0641800000000255 | 06 | 41 | 80 | 0000 0000 0000 0000 0000 0000 0000 0010 | 55 |
これを整理すると取得できるデータは下記となる。なんか増えてるが、最初から取得できるものだったのか、FLASHPROでプログラムを書き換えたから取得できるようになったのかはよくわからないが、よしとしておこう。(参考url:https://en.wikipedia.org/wiki/OBD-II_PIDs)
パラメータID 16進 | データの種類 | バイト数 | 換算式 |
---|---|---|---|
01 | Monitor status | 4 | |
03 | Fuel system status | 2 | |
04 | エンジン負荷[%] | 1 | 値*100/255 |
06 | Short term fuel trim[%] | 1 | 値*100/255-100 |
07 | Long term fuel trim[%] | 1 | 値*100/255-100 |
0B | Intake manifold absolute pressure[kPa] | 1 | そのまま |
0C | エンジン回転数[rpm] | 2 | (256*[バイト1]+[バイト2])/4 |
0D | 車速[km/hr] | 1 | そのまま |
0E | 点火時期(上死点前)[度] | 1 | 値/2-64 |
11 | アクセル開度[%] | 1 | 値*100/255 |
13 | O2センサ位置 | 1 | 各ビットが1のところ |
15 | O2センサ2電圧V、燃料調整% | 2 | 電圧:[バイト1]/200 燃料調整:100*[バイト2]/128-100 |
1C | OBD standards | 1 | |
1F | エンジン始動後経過時間[sec] | 2 | 256*[バイト1]+[バイト2] |
21 | 走行距離[km] | 2 | 256*[バイト1]+[バイト2] |
23 | インジェクター燃料圧力[kPa] | 2 | 10*(256*[バイト1]+[バイト2]) |
24 | 空燃比λ、センサー電圧V | 4 | λ:(256*[バイト1]+[バイト2])/32768 電圧:(256*[バイト3]+[バイト4])/8192 |
2E | Commanded evaporative purge[%] | 1 | 100*[バイト1]/255 |
30 | Warm-ups since codes cleared | 1 | [バイト1] |
31 | Distance traveled since codes cleared[km] | 2 | 256*[バイト1]+[バイト2] |
33 | 気圧(絶対値)[kPa] | 1 | [バイト1] |
3C | 触媒温度[℃] | 2 | (256*[バイト1]+[バイト2])/10-40 |
42 | 制御モジュール電圧[V] | 2 | (256*[バイト1]+[バイト2])/1000 |
43 | 絶対的な負荷率[%] | 2 | 100*(256*[バイト1]+[バイト2])/255 |
44 | 空燃比λの狙い値 | 2 | (256*[バイト1]+[バイト2])/32768 |
47 | スロットル開度絶対値B[%] | 1 | 100*[バイト1]/255 |
49 | アクセルペダル位置D「%] | 1 | 100*[バイト1]/255 |
4A | アクセルペダル位置E[%] | 1 | 100*[バイト1]/255 |
4F | 空燃比最大値、センサ電圧最大値[V]、センサ電流最大値[mA]、吸気圧最大値[kPa] | 4 | 空燃比:10*[バイト1」 電圧:10*[バイト2」 電流:10*[バイト1」 圧力:10*[バイト4」 |
51 | 燃料のタイプ(ガソリン、軽油、ハイブリッドガソリン...とか) | 1 | |
55 | 排ガス酸素センサ(?)調整値(短期)バンク1・3 | 2 | 100*[バイト1]/128-100 100*[バイト1]/128-100 |
56 | 排ガス酸素センサ(?)調整値(長期)バンク1・3 | 2 | 100*[バイト1]/128-100 100*[バイト1]/128-100 |
66 | 空気流量[g/s] | 5 | バイト1:ビットでセンサA/Bの有効無効 センサA:(256*「バイト2]+[バイト3])/32 センサB:(256*「バイト4]+[バイト5])/32 |
67 | 冷却水温度[℃] | 3 | バイト1:ビットでセンサ1/2の有効無効 センサ1:「バイト2]-40 センサ2:「バイト3]-40 |
68 | 吸気温度[℃] ※問い合わせではデータを出せると言われたが実際にはデータを出してくれない | 3 | バイト1:ビットでセンサ1/2の有効無効 センサ1:「バイト2]-40 センサ2:「バイト3]-40 |
6C | スロットル制御狙い値とスロットル位置 | 5 | |
70 | 加給圧制御 | 10 | |
72 | ウェストゲート制御 | 5 | |
9F | Fuel System Percentage Use |
いろんな面白そうなデータもあるが、今回は以下のデータを取得し、表示・ロギングできるようにする。まぁ、気が向いたらその他のデータも取得できるしね。
項目 | 備考 |
---|---|
インマニ圧力[kPa_abs] | OBD2より。PID:0x0B。CAN-ID:0x18DA10F1 |
エンジン回転数[rpm] | OBD2より。PID:0x0C。CAN-ID:0x18DA10F1 |
スロットル開度[%] | OBD2より。PID:0x11。CAN-ID:0x18DA10F1 |
緯度・経度[deg] | GPSより。 |
時刻 | GPSより。 |
今回はさくっと動いた。やはり先人の資料とライブラリが整っていたのが大きいところだ。
完成だ。筐体に関する今回の注目点は以下の2点だ。
#include <FS.h>
#include <SD.h>
#include <SPI.h>
#include <time.h>
#include "CAN.h" // "CAN by Sandeep Mistry"
#include "Free_Fonts.h" // "TFT_eSPI by Bodmer"
#include "TFT_eSPI.h" // "TFT_eSPI by Bodmer"
#include "TinyGPS.h" // "TinyGPS by Mikal Hart"
//use Dual Core
TaskHandle_t thp[1];
#define pi 3.1415927
//io pin
// LCD&TouchPanel = HSPI
// Modified "User_Setup.h" in "TFT_eSPI"
// #define TFT_MISO 12(LCD = no wire, Touch = wired)
// #define TFT_MOSI 13
// #define TFT_SCLK 14
// #define TFT_CS 15
// #define TFT_DC 4
// #define TFT_RST 2
// #define TOUCH_CS 27
// #define SPI_FREQUENCY 40000000
// #define USE_HSPI_PORT
const byte SDmiso = 19; //SD card @VSPI
const byte SDmosi = 23; //SD
const byte SDsck = 18; //SD
const byte SDcs = 5; //SD
const byte CANrx = 32; //CAN
const byte CANtx = 33; //CAN
const byte LCDled = 25; //LCD backlight PWM
const byte LUM = 35; //luminance sensor
const byte BZR1 = 26; //Buzzer pin
const int BZR2 = 5800; // rpm for buzzer
//LCD initialize
#define CALIBRATION_FILE "/TouchCalData2" //touch caliblation data at internal Flash
TFT_eSPI tft = TFT_eSPI();
const float LUM0 = 900; //backlight luminance change level
const int LUM1 = 255; //backlight PWM @ day
const int LUM2 = 40; //backlight PWM @ night
//circle meter
const int Gdata[2][2] = {
{190, 150} // outer meter radius for IntakePress
, {145, 105} // inner meter radius for Throttle
};
const int GdataX = 240; //center x position
const int GdataY = 220; //center y posision
const int GdataS = 80; //start deg
const int GdataE = 280; //end deg
const int Gstep = 4; //Graphic step at deg
int GdataP[2] = {0, 0}; //graph position memory
unsigned long Gcolor[200]; //Graphic color in each deg
// on,off button data
TFT_eSPI_Button KEY[2];
const int KEYx[2] = {180, 300}; //Button x position
const int KEYy = 285; //Button Y position
const int KEYw = 80; //Button Wide
const int KEYh = 60; //Button Height
byte Status = 0; //OFF(initial)
char* KEYstr[2] = {"ON", "OFF"};
const int KEYc[2][2][3] = {
//status0:frame,backbround,string status1
{{TFT_WHITE, TFT_BLACK, TFT_WHITE},{TFT_WHITE, TFT_ORANGE, TFT_BLACK}}
,{{TFT_WHITE, TFT_CYAN , TFT_BLACK},{TFT_WHITE, TFT_BLACK , TFT_WHITE}}
};
// SD parameter
SPIClass vspi(VSPI);
int FileNum = 0; //file name number
byte SDlog = 0; //logging status
File LogData;
String FileTemp;
// CAN parameter
unsigned long CANtrgt = 0x18DA10F1; //CAN-ID
byte CANpid[3] = {0x0B , 0x11 , 0x0C}; //PID IntakePress, Throttle, rpm
float CANdata; //OBD2 data
// GPS parameter
TinyGPS GPS;
float GPSlat, GPSlon; //latitude, longitude
unsigned long GPSage, GPSdate, GPStm;
int GPSyear;
byte GPSmon, GPSday, GPShour, GPSmin, GPSsec, GPShdrd;
byte GPSrcv = 0;
byte GPSstat = 0;
char* GPSstr = "GPS";
char timestr[20];
tm GPStime; // GPS time (tm_wday, tm_year, tm_mon, tm_mday, tm_hour, tm_min, tm_sec)
tm* GPSltime; // local time (tm_year, tm_mon+1, tm_mday, wd[tm_wday], tm_hour, tm_min, tm_sec)
time_t GPSutime; // unix timestamp
const int GPSclr[2] = {TFT_BLACK, TFT_GREENYELLOW};
const int GPSx = 240; //"GPS" x position
const int GPSy = 232; //"GPS" Y position
void setup() {
Serial.begin(115200); //for serial monitor
Serial2.begin(9600); //for GPS
//init.buzzer pin
pinMode(BZR1, OUTPUT);
digitalWrite(BZR1, LOW);
//LCD initialize
tft.init();
tft.setRotation(3); //horizontal & upper side SD
analogWrite(LCDled, 0); //back light off
tft.fillScreen(TFT_BLACK);
//TouchPanel initialize
uint16_t calData[5];
uint8_t calDataOK = 0;
// check file system exists
if (!SPIFFS.begin()) {
Serial.println("Formating file system");
SPIFFS.format();
SPIFFS.begin();
}
// check if calibration file exists and size is correct
if (SPIFFS.exists(CALIBRATION_FILE)) {
File f = SPIFFS.open(CALIBRATION_FILE, "r");
if (f) {
if (f.readBytes((char *)calData, 14) == 14)
calDataOK = 1;
f.close();
}
}
if (calDataOK) {
// calibration data valid
tft.setTouch(calData);
}
//Graph bar color initialize
float ChangeStep = (GdataE-GdataS)/100; //value of 1%
for (int i=0; i<GdataE-GdataS; i++) { // i : deg value
int RED = 255;
int GREEN = 255;
int BLUE = 255;
// 0 - 25% : #00FF00-#FAFFFA
if (i<=25*ChangeStep) {
RED = floor(255/25 * i/ChangeStep);
BLUE = floor(255/25 * i/ChangeStep);
}
// 25 - 40% : white
// 40 - 70% : #FFFFFF-#FFFF05
if (i>=40*ChangeStep && i<=70*ChangeStep) {
BLUE = 255-floor(255/30 * (i-40*ChangeStep)/ChangeStep);
}
// 70% - : #FFFA00-#FF0000
if (i>70*ChangeStep) {
BLUE = 0;
GREEN = 255-floor(255/30 * (i-70*ChangeStep)/ChangeStep);
}
Gcolor[i] = tft.color565(RED, GREEN, BLUE); //R:5bit+G:6bit+B:5bit
}
//draw outer meter frame
for (int i=GdataS; i<=GdataE; i=i+10) {
int X1 = floor(GdataX-(Gdata[0][0]+10)*sin((float)i/180 * pi)+0.5);
int Y1 = floor(GdataY+(Gdata[0][0]+10)*cos((float)i/180 * pi)+0.5);
tft.drawWideLine(X1, Y1, GdataX, GdataY, 3, TFT_WHITE);
}
tft.fillCircle(GdataX, GdataY, Gdata[0][0]+4, TFT_BLACK); //erace line
tft.drawSmoothArc(GdataX, GdataY, Gdata[0][0]+5, Gdata[0][0]+4, GdataS, GdataE, TFT_WHITE, TFT_WHITE, false);
tft.setTextDatum(MC_DATUM); //text posision Middle Center defined in "TFT_eSPI.h"
tft.setTextColor(TFT_WHITE, TFT_BLACK); //font, background
tft.setFreeFont(FF21); // see \TFT_eSPI\Fonts\GFXFF\print.txt
tft.drawString( "0", 28, 252); //Press
tft.drawString( "50", 70, 82);
tft.drawString("100kPa", 242, 8);
tft.drawString( "150", 412, 82);
tft.drawString( "200", 458, 252);
//draw inner meter frame
for (int i=GdataS; i<=GdataE; i=i+20) {
int X1 = floor(GdataX-(Gdata[1][1]-5)*sin((float)i/180 * pi)+0.5);
int Y1 = floor(GdataY+(Gdata[1][1]-5)*cos((float)i/180 * pi)+0.5);
tft.drawWideLine(X1, Y1, GdataX, GdataY, 3, TFT_WHITE);
}
tft.fillCircle(GdataX, GdataY, Gdata[1][1]-10, TFT_BLACK); // erace line
tft.drawSmoothArc(GdataX, GdataY, Gdata[1][1]-4, Gdata[1][1]-5, GdataS, GdataE, TFT_WHITE, TFT_WHITE, false);
tft.drawString( "0", 157, 232);
tft.drawString("50%", 240, 136);
tft.drawString("100", 313, 232);
//draw logging sw
FuncDrawKeypad(); // Function drawkeypad()
// backlight on day level
analogWrite(LCDled, LUM1);
//SDcard initialize
vspi.end();
vspi.begin(SDsck, SDmiso, SDmosi, SDcs);
if (!SD.begin(SDcs, vspi, 12000000, "/sd", 5)) {
Serial.println("SD init error!");
}
File dir = SD.open("/");
while(1){
File entry = dir.openNextFile();
if(!entry){break;}
if (!entry.isDirectory()) {
FileTemp = entry.name();
}
entry.close();
FileNum = FileTemp.toInt()+1; // for file name by serial number
}
//CAN initialize
CAN.setPins(CANrx, CANtx);
if (!CAN.begin(500E3)) {
Serial.println("CAN-BUS init error!");
}
//for ESP32 setup register bit of CAN speed factor
volatile uint32_t* pREG_IER = (volatile uint32_t*)0x3ff6b010; //force bit4=0
*pREG_IER &= ~(uint8_t)0x10;
//LCD starting Effect
for (int i=GdataS; i<GdataE; i=i+Gstep) {
int COL = i-GdataS;
tft.drawSmoothArc(GdataX, GdataY, Gdata[0][0], Gdata[0][1], i, i+Gstep, Gcolor[COL], Gcolor[COL], false);
tft.drawSmoothArc(GdataX, GdataY, Gdata[1][0], Gdata[1][1], i, i+Gstep, Gcolor[COL], Gcolor[COL], false);
delay(20);
}
for (int j=GdataE; j>GdataS; j=j-Gstep) {
tft.drawSmoothArc(GdataX, GdataY, Gdata[0][0], Gdata[0][1], j-Gstep, j, TFT_BLACK, TFT_BLACK, false);
tft.drawSmoothArc(GdataX, GdataY, Gdata[1][0], Gdata[1][1], j-Gstep, j, TFT_BLACK, TFT_BLACK, false);
delay(50);
}
//core0a enable
xTaskCreatePinnedToCore(Core0a, "Core0a", 4096, NULL, 3, &thp[0], 0);
}
void loop() {
//GPS indicate
if (GPSstat != GPSrcv) {
tft.setFreeFont(FF21);
tft.setTextColor(GPSclr[GPSrcv], TFT_BLACK); //font, background
tft.drawString(GPSstr, GPSx, GPSy);
GPSstat = GPSrcv;
}
// Exec CAN command
for (int i=0; i<=2; i++) {
//send command
CAN.beginExtendedPacket(CANtrgt, 8);
CAN.write(0x02); // number of additional bytes
CAN.write(0x01); // show current data
CAN.write(CANpid[i]); // PID parameter
CAN.endPacket();
while (
CAN.parsePacket() == 0
|| CAN.read() < 3 // correct length
|| CAN.read() != 0x41 // correct mode
|| CAN.read() != CANpid[i] // correct PID
);
switch (i) {
//Intake Pressure
case 0:
CANdata = CAN.read(); //0-255kPa
FuncDrawMeter(floor((GdataE - GdataS)*CANdata/200), 0); //0-200 = 0-200kPa, outermeter
break;
//Throttle
case 1:
CANdata = (float)100*CAN.read()/255; //0-100%
FuncDrawMeter(floor((GdataE - GdataS)*CANdata/85), 1); //0-200 = 0-85%max, innermeter
break;
//rpm
case 2:
CANdata = ((float)256*CAN.read() + CAN.read())/4;
if (CANdata>=BZR2) {digitalWrite(BZR1, HIGH); tft.fillRect(GdataX-55, GdataY-60, 110, 55, TFT_RED);}
if (CANdata<BZR2) {digitalWrite(BZR1, LOW); tft.fillRect(GdataX-55, GdataY-60, 110, 55, TFT_BLACK);}
break;
}
// start data logging
if (SDlog == 0 && Status == 1) {
//status: no logging & switch on
String Fname = "/";
Fname.concat(String(FileNum)); Fname.concat(".csv");
LogData = SD.open(Fname, FILE_APPEND); //file open
SDlog = 1; // data logging start.
FuncGPSdata(); // output GPSposition,time
}
// data loging. output to SDcard
if (SDlog == 1) {
// output to SD
LogData.print(millis());LogData.print(",");LogData.print(CANpid[i]);
LogData.print(",");LogData.println(CANdata);
// if push "STOP" bottun, to stop data logging
if (Status == 0) { //switch off
FuncGPSdata(); // output GPSposition,time
LogData.close();
SDlog = 0; // stop logging
FileNum++;
}
}
}
//push touch key?
uint16_t touchX = 0, touchY = 0;
bool keyPress = tft.getTouch(&touchX, &touchY);
for (int i=0; i<2; i++) {
if (keyPress && KEY[i].contains(touchX, touchY)) {
if (i==1 && Status==0) {Status = 1;} // start logging
if (i==0 && Status==1) {Status = 0;} // end logging
FuncDrawKeypad();
}
}
unsigned long start1 = millis();
while (millis() - start1 < 100) {}
}
void Core0a(void *args) {
while (1) {
delay(1);
//backlight controll
float LUMread = analogRead(LUM);
if (LUMread>LUM0) {analogWrite(LCDled, LUM1);} else {analogWrite(LCDled, LUM2);}
//GPS
unsigned long start2 = millis();
while (Serial2.available() == 0 && millis()-start2<2000) {}
if (millis()-start2>=2000) {
// time out
GPSrcv =0;
} else if (GPS.encode(Serial2.read())) {
GPSrcv = 1;
GPS.f_get_position(&GPSlat, &GPSlon, &GPSage); //latitude longitude age
GPS.crack_datetime(&GPSyear, &GPSmon, &GPSday, &GPShour, &GPSmin, &GPSsec, &GPShdrd, &GPSage);
GPStime.tm_year = GPSyear-1900; //1900 is 0
GPStime.tm_mon = GPSmon-1; //jan is 0
GPStime.tm_mday = GPSday;
GPStime.tm_hour = GPShour+9; //japan
GPStime.tm_min = GPSmin;
GPStime.tm_sec = GPSsec;
GPSutime = mktime(&GPStime); //create unix timestamp
GPSltime = localtime(&GPSutime); //create local time
sprintf(
timestr, "%4d-%02d-%02d %02d:%02d:%02d"
, GPSltime->tm_year+1900, GPSltime->tm_mon+1, GPSltime->tm_mday
, GPSltime->tm_hour, GPSltime->tm_min, GPSltime->tm_sec
);
}
}
}
//Function draw button
void FuncDrawKeypad() {
tft.setFreeFont(FF22);
for(int i=0; i<2; i++) {
KEY[i].initButton(
&tft, KEYx[i], KEYy, KEYw, KEYh, KEYc[i][Status][0]
,KEYc[i][Status][1], KEYc[i][Status][2], KEYstr[i], 1
);
KEY[i].drawButton();
}
}
//Function Draw Meter
// DM1 : data value 0-max
// DM2 : data channel 0,1
void FuncDrawMeter(int DM1, byte DM2) {
if (DM1 != GdataP[DM2]) {
int DMdeg0 = GdataS + GdataP[DM2];
int DMdeg1 = GdataS + DM1;
if (DMdeg1 > DMdeg0) {
//increase graph bar
for (int DMi= DMdeg0; DMi<DMdeg1; DMi=DMi+Gstep) {
int COL = DMi-GdataS;
tft.drawSmoothArc(
GdataX, GdataY, Gdata[DM2][0], Gdata[DM2][1]
, DMi, DMi+Gstep, Gcolor[COL], Gcolor[COL], false
);
}
} else {
//decrease graph bar
tft.drawSmoothArc(
GdataX, GdataY, Gdata[DM2][0], Gdata[DM2][1]
, DMdeg1, DMdeg0+Gstep, TFT_BLACK, TFT_BLACK, false
);
}
GdataP[DM2] = DM1; //store position of GraphBar 0-200
}
}
void FuncGPSdata() {
//if GPS enable, output location and time
if (GPSstat == 1) {
LogData.print(GPSlat, 5); LogData.print(","); LogData.print(GPSlon, 5);
LogData.print(","); LogData.println(timestr);
}
}