はじめに

ELM327とArduinoとLCD2004で車両情報を表示してみました。
ELM327ではTrqueやCar Scannerなどのアプリで色々な車両情報は見れますが、だいたい見る項目は決まってきましたので作ってみました。
アプリより表示のレスポンスが向上しました。

「Arduino ELM327」などでググると、以下のELMduinoのような便利なライブラリも見つかりましたが、今回は使っていません。
PowerBroker2/ELMduino: Arduino OBD-II Bluetooth Scanner Interface Library for Car Hacking Projects

使ったもの

ELM327殻割り

基板は3段構成になっていて一番上の青い基板はBluetooth Module(BTM)です。
BTMのチップ印字消されていますが、いろいろググってみるとHC-05のコンパチ?っぽそうです。
モジュールは3.3Vで駆動されていますが、typは3.6~6Vらしいですね。
RT,TX,3.3V,GND,5Vは以下の位置関係のようです。

alt text

参考

接続

ELM327はスマホで動作確認はできていますので、PCとシリアル通信してみます。
USB-TTLデバイスを用意して、以下のように接続します。

ELM327USB-TTL
5V5V
GNDGND
TXRX
RXTX

通常は定番のFTDIの石を使うと思いますが、持っていないのでCH32V003用に持っているWCH-LinkEを使いました。
COMポートとして認識されるデバイスなら通信できそうです。

Arduino UNO R2のUSB-シリアル変換部分も同じように使うことができます。その場合、以下のように接続します。
ArduinoのTX(pin1)はUSB-シリアル変換部分のRXに、RX(pin0)はTXに繋がっているので、クロス接続にはしません。 以下↓にも記載しています。
NCP-HG100 を 楽天モバイル Band3 に固定する | atooshi-note

ELM327Arduino UNO R2
5V5V
GNDGND
TXTX(pin1)
RXRX(pin0)

また、Arduinoは5VなのでTX,RXのも5Vです。
BTMは3.3Vなので、Arduinoを使う場合はBTMを外さないと動作しません。
3.3VのUSB-TTLを使用すれば、BTMを外さずに通信できると思います。(外してしまって、5Vで動作させていた為未確認です) それなら、bluetoothでの接続も生かせます。
その場合は、3.3Vの他に、ELM327石の動作用で5Vの給電が必要かもしれません。

ATコマンド

接続ができたら、ATコマンドを送信してELM327と通信してみます。
PC - ELM327 のように繋がっています。
Arduino UNO R2を使って、ArduinoIDEのシリアルモニターで確認してみました。

Baud rateは以下、ELM327のデータシートに記載があるように38400か9600のようで、38400でした。

Baud Rate (pin 6) This input controls the baud rate of the RS232 interface. If it is at a high level during power-up or reset, the baud rate will be set to 38400 (or the rate that has been set by PP 0C). If at a low level, the baud rate will be initialized to 9600

また、他の設定は以下のように記載があります。

Don’t forget to also set your connection for 8 data bits, no parity bits, and 1 stop bit, and to set it for the proper ‘line end’ mode. All of the responses from the ELM327 are terminated with a single carriage return character and, optionally, a linefeed character (depending on your settings).

ですので、まとめると以下になります。

  • speed : 38400
  • data : 8bit
  • parity : none
  • stop bits : 1bit
  • flow control : none

“ATZ"を送信し、v1.5の返答がありました。
また、“ATRV"では12.1Vの返答があり、ATコマンドが通ることが確認できました。
alt text

また、TeraTermで送信する場合は、”ATZ”の後にEnterを押します。
プログラム上では、“ATZ\r"と、末尾に\rつけて送信しなければ、ELM327側でコマンド受信完了の判断ができません。
ArduinoIDEのシリアルモニターでも、終端文字はCR+LF、もしくはCRにする必要があります。LFのみは不可です。

なお、Arduino UNOのメインの石(ATmega328)には何もさせないので、以下のプログラムを書き込んでTX,RX信号に影響が出ないようにしています。

void setup() {
  // put your setup code here, to run once:
  pinMode(0,INPUT);
  pinMode(1,INPUT);
  digitalWrite(0,HIGH);
  digitalWrite(1,HIGH);
}

void loop() {
  // put your main code here, to run repeatedly:

}

OBDコマンド

ATコマンドで、PCとELM327の通信が確認できたので、次はOBDコマンドで車両とやりとりしてみます。 PC - ELM327 - 車両 のように繋がります。
車両でデバッグするのは大変なので、Arduinoで作った車両を模擬する環境を使います。 こちらで紹介しています。
ArduinoでOBDをシミュレートしてELM327とつなぐ | atooshi-note

0105を送信し、41 05 FAの返答がありました。
alt text

プログラム

上記を踏まえて、OBDデータをLCDに表示させてみました。
メインの石はArduino Pro mini互換品を使用しています。
車両はソリオ(MA15S)です。

/*
Arduino Pro mini : 16bit
システムのクロック : 16MHz
電源電圧 : 5V

LCD
2004 20桁4行
I2C接続
SDA - A4
SCL - A5

ELM327との接続
ソフトウェアシリアル
RX pin2
TX pin3
*/

#include <SoftwareSerial.h>
#include <Wire.h>

#define lcdaddr 0x27 // スレーブアドレス0x27
#define lcdEN 0b00000100 // Enable
#define lcdBL 0b00001000 // Back light ON
#define setDDRAMaddr 0b10000000 // Set DDRAM Address
#define setRS 0b00000001 // RS=1
#define unsetRS 0b00000000 // RS=0

#define val1 20 // 配列の大きさ
#define aveval 8 // 平均を取る数

bool state = false; // Builtin LEDのステータス用
bool flg0 = false;
bool flg1 = false;
bool flg2 = false;

const byte rxPin = 2;
const byte txPin = 3;

SoftwareSerial mySerial (rxPin, txPin);

char receivevalue[val1] = {0}; // 受信文字数に応じて配列の大きさを調整 複数PIDなら大きく調整必要
int i0 = 0;
int n = 0;
unsigned char cnt0 = 0;
unsigned char cnt1 = 0;
unsigned char cnt2 = 0;
int valcon0 = 0;
int valcon1 = 0;
int coolant = 0;
int intake = 0;
unsigned int runtimesince = 0; // 16bitのため、unsigned int(~65535)としないとオーバーフローする
float modulevoltage = 0.0;
float throttle = 0.0;
unsigned int A = 0;
unsigned int B = 0;
char buf[6]; // Run time since engine startの最大値が65,535で5桁 Null終点文字が自動的に付加のため+1
unsigned long t0; // 時間計測用
unsigned int vspeed[aveval] = {0}; // 車速 平均化用の配列
unsigned char cnt3 = 0;
unsigned int vspeedsum = 0;
unsigned int vspeedave = 0;
unsigned int erpm[aveval] = {0}; // エンジン回転数 平均化用の配列
unsigned char cnt4 = 0;
unsigned int erpmsum = 0;
unsigned int erpmave = 0;

// 0105 : Engine coolant temperature A - 40
// 010c : Engine speed (256 * A + B) / 4
// 010d : Vehicle speed 	A
// 010f : Intake air temperature 	A - 40
// 0111 : Throttle position (100 / 255) * A
// 011f : Run time since engine start 256 * A + B
// 0142 : Control module voltage (256 * A + B) / 1000

// 取得頻度毎にPIDを分ける
char* PIDS0[]={"010c","010d","0142","0111"};
char* PIDS1[]={"011f"};
char* PIDS2[]={"0105","010f"};

int asciiTOhex(char c_high,char c_low){
  int i = String(c_high,HEX).toInt();
  int j = String(c_low,HEX).toInt();
  // ascii character to character code(hex)
  if(i < 41){ // ascii A 0x41 A~ より前 = 数値
    i = i - 30;
  }
  else{
    i = i - 31;
  }
  // hex to dec
  i = i << 4; // 2^4=16 乗算
  if(j < 41){
    j = j - 30;
  }
  else{
    j = j - 31;
  }
  int k = i + j;
  return k;
}

int asciiTOint(char c){
  int i = String(c,HEX).toInt(); // ascii character to character code(hex)
  return i;
}

void lcdinit()
{
  // 一旦8bitモードに設定(3回繰り返す)
  // 1回目
  lcdwrite0(0x30,unsetRS); // Funcition Set DL=1:8bitバス
  delay(5); // Wait for more than 4.1 ms

  // 2回目
  lcdwrite0(0x30,unsetRS);
  delayMicroseconds(150); // Wait for more than 100 µs

  // 3回目
  lcdwrite0(0x30,unsetRS);
  delayMicroseconds(100); // Wait for more than 40 µs

  lcdwrite0(0x20,unsetRS); // 4bitモードに設定 Funcition Set DL=0:4bitバス
  lcdwrite(0x28,unsetRS); // 2行表示モードに設定 Funcition Set DL=0:4bitバス,Set N=1:2桁表示
  lcdwrite(0x0F,unsetRS); // 表示ON/OFF D=1:文字表示ON,C=1:下線カーソルON,B=1:ブロックカーソルON
  lcdwrite(0x06,unsetRS); // エントリモードセット I/D=1:インクリメント
  lcdwrite(0x01,unsetRS); // 表示クリア
  lcdwrite(0x02,unsetRS); // カーソルホーム
}

// 8bit用
void lcdwrite0(byte bits,byte mode){
  i2cwrite(bits | mode);
}

// 4bit用
void lcdwrite(byte bits,byte mode){
  i2cwrite((bits & 0xF0) | mode); // 上位4bit
  i2cwrite(((bits << 4) & 0xF0) | mode); // 下位4bit
}

void i2cwrite(byte val){
  Wire.beginTransmission(lcdaddr);
  Wire.write(val | lcdBL);
  delayMicroseconds(1); // 0.22us以上
  Wire.write(val | lcdBL | lcdEN);
  delayMicroseconds(1); // 0.22us以上
  Wire.write(val | lcdBL);
  Wire.endTransmission();
  delayMicroseconds(100); // Wait for more than 40 µs とは言いつつ50usec待ちでは動かない 100usecぐらい余裕を持っておく
}

// row:行 col:列
void setCursor(int row, int col){
  int row_offsets[] = {0x00, 0x40, 0x14, 0x54}; // 0から数え始め
  lcdwrite((col + row_offsets[row]) | setDDRAMaddr,unsetRS);
}

void lcdText(String s){
  for(int i = 0; i < s.length(); i++){
    lcdwrite(s.charAt(i),setRS);
  }
}

void setup() {
  delay(50); // Wait for more than 15 ms after VCC rises to 4.5 V
  pinMode(LED_BUILTIN,OUTPUT);
  pinMode(rxPin, INPUT); // Define pin modes for TX and RX
  pinMode(txPin, OUTPUT); // Define pin modes for TX and RX
  mySerial.begin(38400);
  Serial.begin(38400);
  Serial.println("Connect to PC serial port");
  Wire.begin();
  lcdinit();

  // LCD表示。単位など、固定表示
  setCursor(0,6);
  lcdText("ECT");

  setCursor(1,6);
  lcdText("IAT");

  setCursor(1,16);
  lcdText("%");

  setCursor(2,6);
  lcdText("km/h");

  setCursor(2,16);
  lcdText("RPM");

  setCursor(3,6);
  lcdText("V");

  setCursor(3,16);
  lcdText("s");

  t0 = millis(); // 初期時刻
}

void loop() {

  // データ取得(送受信)
  if(flg0 == true || flg1 == true || flg2 == true){

    if(flg0 == true){
      // PIDS0 リクエスト 送信
      mySerial.println(PIDS0[n]);
    }
    else if(flg1 == true){
      mySerial.println(PIDS1[n]);
    }
    else if(flg2 == true){
      mySerial.println(PIDS2[n]);
    }
    else{}
    mySerial.print("\r");

    // データ受信
    while(mySerial.available()>0){
      receivevalue[i0] = mySerial.read();
      i0++;
    }

    valcon0 = asciiTOint(receivevalue[3]); // 受信PID
    valcon1 = asciiTOint(receivevalue[4]); // 受信PID
    A = asciiTOhex(receivevalue[6],receivevalue[7]); // データバイトの1バイト目
    B = asciiTOhex(receivevalue[9],receivevalue[10]); // データバイトの2バイト目

    if(valcon0 == 30){ //0=0x30
      if(valcon1 == 35){ // 5=0x35 0105 Engine coolant temperature A - 40
        coolant = A - 40;
      }
      else if(valcon1 == 43){ // C=0x43
        erpm[cnt4] = ((A<<8)+B)>>2;
        cnt4++;
        for(int i=0;i<aveval;i++){
          erpmsum += erpm[i];
          erpmave = erpmsum >> 3; // 8分の1
        }
        erpmsum = 0;
        if(cnt4 >= aveval){
          cnt4 = 0;
        }
      }
      else if(valcon1 == 44 ){ // D=0x44
        vspeed[cnt3] = A;
        cnt3++;
        for(int i=0;i<aveval;i++){
          vspeedsum += vspeed[i];
          vspeedave = vspeedsum >> 3; // 8分の1
        }
        vspeedsum = 0;
        if(cnt3 >= aveval){
          cnt3 = 0;
        }
      }
      else if(valcon1 == 46 ){ // F=0x46
        intake = A - 40;
      }
      else{}
    }
    else if(valcon0 == 31){ // 1=0x31
      if(valcon1 == 31){ // 1=0x31
        throttle = A*100; // 後ほど255でわる
      }
      if(valcon1 == 46){ // F=0x46
        runtimesince = (A<<8)+B;
      }
      else{}
    }
    else if(valcon0 = 34){ // 4=0x34
      if(valcon1 == 32){ // 2=0x32
        modulevoltage = (A<<8)+B; // 後ほど1000で割る ex)14277V → 14.277V

        // 受信する値の例
        // txData[3] = 0x37; // A dec 55
        // txData[4] = 0xC5; // B dec 197
        // (A * 256 + B)/1000
        // (55 * 256 + 197)/1000 = 14.277V

      }
      else{}
    }
    else{}

    delay(5); // 少し待たないと正しく送受信が完了しない
    i0 = 0;
    n++;

    if(flg0 == true){
      if(n >= (sizeof(PIDS0)>>1)){ // pid送受信終わり pid数に合わせる
        n = 0;
        flg0 = false;
      }
    }
    else if(flg1 == true){
      if(n >= (sizeof(PIDS1)>>1)){ // pid送受信終わり pid数に合わせる
        n = 0;
        flg1 = false;
      }
    }
    else if(flg2 == true){
      if(n >= (sizeof(PIDS2)>>1)){ // pid送受信終わり pid数に合わせる
        n = 0;
        flg2 = false;
      }
    }
    else{}

    // LCD表示部
    sprintf(buf,"%3d",coolant);
    setCursor(0,1);
    lcdText(buf);
    
    sprintf(buf,"%3d",intake);
    setCursor(1,1);
    lcdText(buf);

    dtostrf(throttle/255,4,1,buf);
    setCursor(1,11);
    lcdText(buf);
    
    sprintf(buf,"%3d",vspeedave);
    setCursor(2,1);
    lcdText(buf);
    
    sprintf(buf,"%4d",erpmave);
    setCursor(2,11);
    lcdText(buf);
    
    dtostrf(modulevoltage/1000,4,1,buf);
    setCursor(3,1);
    lcdText(buf);

    sprintf(buf,"%5d",runtimesince);
    setCursor(3,10);
    lcdText(buf);
  }

  unsigned long t1 = millis();

  if((t1-t0) > 100){ // 100ms計測
    // 動作確認用LED
    if(state){
      digitalWrite(LED_BUILTIN,LOW);
    }
    else{
      digitalWrite(LED_BUILTIN,HIGH);
    }
    state = !state;

    cnt0++;
    cnt1++;
    cnt2++;

    t0 = t1;
  }

  // lcd表示値の更新
  // lcd表示を
  // 400ms毎 早く更新したいもの
  if(cnt0 > 4){
    cnt0 = 0;
    flg0 = true;
  }
  // 1sec毎 runtimesinceの単位はsec
  if(cnt1 > 10){
    cnt1 = 0;
    flg1 = true;
  }
  // 5sec毎 温度関係
  if(cnt2 > 50){
    cnt2 = 0;
    flg2 = true;
  }
}

プログラムの概要

概要は以下です。

  • Arduinoから車両にOBDコマンドを送信し、結果をLCD2004に表示
  • LCDの表示制御はWire.hのみを使用
  • ソフトウェアシリアルでELM327と通信
    • 既存のシリアルポートをデバッグ用に使用したいためです。シリアルポートは1つしかありません。
    • ソフトウェアシリアルはこれ → SoftwareSerial Library
  • データ取得間隔はPID毎変えており、温度関係は5秒毎、車速やエンジン回転数は400ms毎、ランタイムは単位が1秒なので1秒毎
    • タイマーで100ms(大体)を作っています。
      • millis()を使ってメインループで時間を計測します。
      • タイマー割り込みを使って正確に時間を測りたかったのですが、I2CのWire.hが割り込みを使うため多重割り込みとなり動作が不安定でした。
      • 割り込みのプライオリティとか処理順を把握してれば良いですが、していません。
      • なのでタイマー割り込みは却下しました。
    • データ取得のときに、LCDも更新
      • LCD表示処理にdelayが入るため、時間計測もズレて大体になります(たぶん
    • 車速、エンジン回転数は値が暴れるので、平均化(8個の平均)
      • 毎周期、配列に代入し合計値から平均値を算出しています(移動平均(?)っぽい感じ。違うかもしれません)
      • 配列はリングバッファ(?)のようにぐるぐる使いまわします。

メモ

以下は個人的なメモです。なぜそうしたかの経緯を思い起こすために残しておきます。

受信文字を数値に変換したい

  • 車両から返ってくる値は文字でascii
  • そのまま数値に変換するとasciiコード表の数値になる
  • Fなら0x46
  • 0(0x30)を引く方法は9までの文字なら良いが、9以降の文字には非対応
  • atoiは使えない
    • 数字の文字を整数にするだけ
    • 数字の文字以外は0になる
  • if文で限定的に数字とA~(アルファベット)で引き算を分けることにした

LCDの表示文字が残ってしまう

  • 例えば、100の表示のあと、90と表示したいところ、3桁目の1が消えないので190と表示されてしまう。
  • LCDの表示はRAMに保存されるので、上書きしないと消えない
  • 例えば、3桁目の1を消したければ、1の座標にスペース(空白)を書き込むなど
  • それか、表示をクリアする
    • 一部消すことはできず、全消しのため表示が出て消えてを繰り返しチカチカする
    • 却下
  • sprintfで解決
    • 数値を文字に変換し、右詰めしてくれる
    • %04d 4桁0つめ
    • %4d 4桁
    • 整数のみ
  • 小数はdtostr関数
    • 小数を文字にする
    • arduinoでよく使用される
    • sprintfでも書けるらしい(やってない)

Run time since engine startの最大が65,535

  • OBD-II PIDs - Wikipediaを見ると、Run time since engine startの最大値は65,535
  • Arduino Pro miniのintは16bitのため、unsigned int(~65535)としないとオーバーフローする
  • このためunsigned intで宣言

ノイズ対策(USBケーブルを再利用、コンデンサ)

  • 車両はノイズだらけ
  • 放射ノイズを防ぐために信号をGNDでシールドしたい
  • USBケーブルは周りがGNDでシールドされている
  • 安い、長い
  • シールド線はELM327のシャーシGND(OBDコネクタ 4pin)に接続
  • 反対側はOPEN(LCDのケースにつけようと思ったが保留)
    • と思ったが、4pinがはんだ付けしづらい位置にあったのでやってない
  • 車でテスト中、液晶が暗くなり酷いちらつきが発生
    • エンジンが掛かってなければキレイのため、発電機周り(?)の電源ノイズが原因だと思う
    • エンジンが掛かっていてもキレイな時もあるし、だめな時もある
    • エアコン(コンプレッサーまわり)を切っても酷いので、これは関係なさそう
    • 0.1uFの積セラと25V330uFの電解コンをLCDのVCC-GND間直近につけて大体解消した
      • 車にて、Arduinoの書き込みのためのUSB-TTLデバイスをつけっぱなしで動作させた(外すの忘れてた)ら、やはり酷いちらつきが発生
      • 上記コンデンサは付いている状態
      • USB-TTLデバイスを外したらちらつきが解消したので、放射ノイズが酷いんだと思う

割込を使うならvolatile(未確認)

Arduinoが勝手にリセットされる

  • ELM327と通信して、シリアルモニターで送受信値を見ていると、Arduinoが勝手にリセットされている
  • 車両模擬側(OBD_Sim.ino)でもモニターしてみると、送信されていないはずの値(00だっり42だったり)が受信されていた
  • delayを入れると解消したため、少し待たないと正しく送受信が完了しないみたい
  • でも時間計測に影響が出るため、delayは入れたくない
  • 適正なdelayはトライアンドエラーで送受信データをシリアルモニターで見て、変な値があればdelayを長くする
  • delay(5)で5msは必要っぽい
  • これ以上のdelayは時間計測に影響が出る

float、割り算を使いたくない(結局使った)

  • Module Voltageやスロットルは12.5Vとか、13.3%とか小数になる
  • floatとなり、割り算も使用するが、割り算は負荷が大きいので、使いたくなかった
    • 2のべき乗(2,4,8,16など)の割り算ならシフト演算が使えるので良いが、それ以外の数は簡単にできなかった
  • 案1 : 整数と小数を別々にstringで出す
    • 数値ではなく文字
    • 12.5など、表示したい桁は決まっている
    • 小数点の位置をあらかじめ決める
    • 問題
      • 右詰めにならない
      • 14.27V は良いが、9.27Vだった場合、9 .27Vのように9と小数点の間に隙間が生じるし、LCDの表示が残る問題もある
    • 表示は一応できたが、却下
  • 案2 : シフト演算で2のべき乗以外を表現して(10で割るなど)、数値として整数と小数を別々に出して、sprintfで右詰めする
    • sprintfが使えるなら、右詰めができ、案1の問題が解消できる
    • sprintfにするなら、文字ではなく数値である必要がある
    • 数値で整数と小数を出すには、100倍したり、100で割ったりする必要がある
    • 計算をシフト演算でやりたい
      • が、上記したが、2のべき乗以外の割り算はシフト演算でできなかった
      • 10を掛けるなどの掛け算はシフト演算でできた
    • ややこしい
    • 却下
  • float使っちゃう、わり算使っちゃう

時間計測のメモ

変数値が0になる

  • これはかなりハマった
  • コンパイルエラーは出ず、書き込みも正常に行われる。なので気付かない
  • 例えば、intake温度の変数をintake0とintake1の2つ用意して、intake1=intake0とするも、intake1の値は0になる。(intake0は0以外の値)
  • プログラムの書く位置(行数)や順番を変えたりすると解消したりするし、別の変数でも同じことが起こったり起こらなかったり
  • 原因は、配列の大きさが小さいこと

char型の配列を扱う場合、文字数+1の大きさ(要素数)が必要なことに注意。char型の場合最後にNull終点文字が自動的に付加されるため。

電源の取り方

OBDコネクタは常時12Vなので、エンジンを切っても電圧があります。
毎回ELM327をつけ外しするのは大変なので、アクセサリ電源と連動するようにしました。
ELM327のpin16(12V)を折って、12Vを遮断し、シガーソケットからUSBアダプタを経由して5VをマイコンとLCDに供給します。(ELM327は5Vで動作します。車は12Vなので、元々内部で5Vに落として使ってると思います、たぶん) GNDは繋いで共通にしてあります。
CANは差動信号なので、CAN-HighとLowの2線だけで通信はできるのですが、ノイズの観点でGNDは接続します。
アクセサリ電源は、ヒューズボックスから取ったり、ナビ裏から取ったりと色々あると思いますが、裏を開けるのが面倒だったのと、この装置を外してデバッグすることを考えてシガーソケットが楽だと思いました。

FMラジオにノイズが入ることの対策

この装置に通電するとFMラジオにノイズが入ります。結構気になります。
とりあえずAMは未確認ですが、殆ど聞かないので保留です。
ArduinoからELM327へのシリアル通信が原因かと思ってます。配線の取り回しや設置位置を調整してちょっと改善しましたが、少し残ってるのでフェライトコアをつけたところ良さそうです。2ターン巻きました。 エレコムのフェライトコアです。

エレコム 高周波ノイズ吸収フェライトコア NF-37SS https://www.amazon.jp/dp/B00008B3J0

未実施事項

  • t0,t1のオーバーフロー対策
    • millis()は約50日でオーバーフロー
    • 車を50日もエンジンかけっぱないしはしないので、そのままにしています。
    • 問題になったら対策します。
    • Arduino日本語リファレンス millis()
  • LCDの調光
    • バックライトのFETをPWM駆動すると調光できるようです
    • 夜はLCDが明るすぎるので必要かと
    • でも夜にあまり運転しないので保留です

修正したいこと

  • 車速、エンジン回転数を400ms毎にデータ取得、表示 → 100ms毎にデータ取得、平均化、500ms毎に表示更新
    • 平均を取っているため、値にラグがある。しょうがないが、データ取得間隔を狭くすれば、ラグが減る
    • 8個の平均のため、100ms毎なら800msでデータが全て入れ替わる

スマホホルダーにテープで固定
ちゃんと箱に入れるのはいつになるやら笑

以上:)