車速連動ボリュームを作った

#はじめに
電子工作モノを考えたときに「機能的には単純なんだけど設定UIを用意するのが大変」というケースがあると思います。

そんな場合に、ESP8266を使って、WiFiで接続したスマホで設定して、設定した後はWiFiをOFFって使う、という使い方が考えられるかなーと思っています。

設定値が固まった後はESP8266ではなく、WiFi機能を持たないナニカ(例えばArduino Nano(の互換機))にしてもいいわけですが、ESP8266の安さを考えると、乗せ換えなくてもOKですよね。

#そして作ったもの

車速連動ボリュームを作りました。

![DSC_0782.jpg](https://qiita-image-store.s3.amazonaws.com/0/104360/41b3b0cd-40bc-4db6-c502-763928b690f9.jpeg)
（赤いRCAジャックが品切れで、仕方なく黒に。）

高いクルマや高い後付けナビにはついている機能なんですが、速度に連動して音量が変わるというものがあります。走行音に負けない音量を出すわけですね。

それを自分の安グルマでも実現したいと。

最近はスマホの音楽を外部入力に入れて聞いているので、その間に自作機器を挟めば実現できそうです。

PICマイコンなどの安いチップでも実現できるのですが、「こんぐらいの速度の時に、こんぐらいの音量」という設定を、実車で試しながら追い込んでいくのは大変です。

そこで、手持ちのスマホで音量設定出来たら便利だ！ということで、はじめに述べた方式で作りました。

設定画面はこんな感じです。

![webui.png](https://qiita-image-store.s3.amazonaws.com/0/104360/723cd23e-ab0d-2499-1f92-8dc236b40aeb.png)


#ハードウェア構成

主要部品は、マイコン（ESP8266）、電子ボリューム、電源です。

##電子ボリューム

「電子ボリューム」でググったら、新日本無線の NJW1159 というのが安価で良さげです。
GPIOから「このボリュームで」とお願いするとその音量になってくれます。

秋月で買ったのですが、今は何故か買えません。

http://akizukidenshi.com/catalog/g/gI-04405/

今から買う方は、マルツあたりで購入してみましょう。

http://www.marutsu.co.jp/pc/i/19372/

##電源(NJW1159用)

NJW1159は両電源（±4.5~7.5V）が必要です。クルマは12Vの片電源なので、そこから両電源を作れる回路が必要となります。

クルマの12Vは、安定した12Vではなく、エンジン始動時に8Vぐらいまで下がったり、始動後は14.4Vぐらいが標準的ですが、16とか18Vぐらいは平気でぶちこんでくるので、そのレンジで壊れない電源が必要です。

使用温度範囲も、室内向けより広い幅が必要です。

また、アナログ回路（オーディオ回路）なので低ノイズであることも条件となります。

といったところを考慮して選んだのがコチラ

https://strawberry-linux.com/catalog/items?code=13260

LTC3260 です。

最大出力電流がギリギリのような気もしますが、とりあえず動いているので大丈夫っぽいです。

データシートのアプリケーションの所に「車載機器」などが書いてあれば安心できるのですが、書いてません。でもまあ大丈夫でしょう。。。

##電源(ESP8266用)

ESP8266のための、3.3Vの電源も必要です。

[ESP8266の電源にスイッチングレギュレータを使うことのススメ](http://qiita.com/rukihena/items/c05625d2239bfabe0532) で紹介されている M78AR033-0.5 を採用しました。

ノイズが心配なので、オーディオ回路とはできるだけ離して実装したほうがいいです。気分の問題かもしれませんが。

## 車速パルス

速度を検出するため、車速パルスを入力します。

回路としてはこんな感じで。

![speed_signal.png](https://qiita-image-store.s3.amazonaws.com/0/104360/68366478-cf89-04be-cee4-055906f02261.png)

ここのノウハウがあんまりないので、どんな車両でもOKかというと自信がないですが、とりあえず自分のクルマではこれで動作しています。

コレクタ側にプルアップ抵抗がないですが、当然ここはESP8266の内蔵プルアップを有効にしています。

## LED

ちょっとした動作の確認をするためにLEDを付けています。IO16に接続しています。今思えば、IO2に配線すればよかったかなと思っています。（参考：[ESP8266のIO2でLチカ](http://qiita.com/rukihena/items/46ec938cca4cf48b1d0f)）

また、IO16はdeepsleepからの復帰用になっており、ボードによっては既にRSTへ配線されているので、その場合はリセットループになってしまいます。

今はたぶん、ループがちゃんと回っているかの確認になっていると思います。（ちょっと前に作ったので詳細を忘れている）

## 設定スイッチ

設定モード（WiFi使用モード）に突入するためのスイッチがあります。IO4に接続しています。今思えば、IO0に配線すればよかったかなと思っています。（参考：[ESP8266のIO2でLチカ](http://qiita.com/rukihena/items/46ec938cca4cf48b1d0f)）

WiFi使用モードから抜ける方法は用意していません。チャタリング対策が面倒だったからです。電源を入れなおせば、使用しないモードで立ち上がります。

## その他部品

コンデンサや抵抗などが必要になります。具体的に必要な部品はNJW1159データシートを参照してください。コンデンサは耐熱と耐電圧に注意して選びましょう。

コネクタや線材なども少々必要です。

## 全体回路図

全体回路図は省略します。書くとミスりそう。。。

アナログ回路はNJW1159データシートの「内部出力バッファ回路使用例」の通りです。「外部出力バッファ回路使用例」の回路でも構いません。この辺りは宗派に合わせてくださいｗ

ボリューム指示の信号ライン(LATCH,CLOCK,DATA)は、ポート番号が後述のソースに書いてあるので、それをみて配線してください。

# ソフトウェア

## SPIFFS

ESP8266はフラッシュメモリが大量にあります。大量と言っても 3MBytes なんですけれども、500円の組み込みチップということを考えると大量です。

それをファイルシステム的に扱える仕組みがあり、それが SPIFFS と呼ばれています。

SPIFFSに、Webのファイル（HTMLや画像）と設定値（こんぐらいの速度の時にこんぐらいの音量というマッピングデータ）を置いています。

SPIFFS についての詳細は、ググってください。

## ESP8266WiFiMulti

自宅でのテスト時（家の無線LAN使用）と、実車での設定時（モバイルルータ使用）でコードを書き換えたくなかったため、ESP8266WiFiMulti.h を使っています。

[ESP8266で複数のSSIDを利用する](http://qiita.com/rukihena/items/c1b02efcaa37af2f3b32) を参照してください。

また、設定スイッチが押されるまでは WiFi をONにせず、押されたら初めて ON にします。

SSIDとパスワードは、SPIFFS にテキストファイルで置いています。ssid.txt というファイル名で、1行にSSIDとパスワードの組み合わせをカンマで区切って記述します。

ハードコーディングでも変更時の手間は大して変わらないかもしれません。むしろ、オレオレフォーマットの説明が必要な分、テキストファイルの方が不利かもしれません。Web UI から書き換えられるならSPIFFSに置くメリットが出てきます。

## ESP8266WebServer

設定画面はWeb UIにするので、ESP8266をWebサーバにするため ESP8266WebServer を用います。

ESP8266WebServer の普通の使い方だと、ページ毎に個別にハンドラを登録するのですが、今回は全部 onNotFound() で登録したハンドラで、SPIFFS にあるファイルを HTTP body として返す実装にしています。

CGI的なものはなく、すべて静的なページとし、動的な部分は WebSocket と JavaScript で実装しています。

## HTML5

スライダーを使いたかったので、HTML5 を使用しています。（厳密にHTML5の文法になっているかは自信がありません。）

## WebSocket

クライアントからの「スライダーを動かした」や、サーバからの「速度が変わった」などは WebSocket でやり取りしています。

## OTA

半完成状態からもファームウェアを書き換えたかったので、OTA（Over-The-Air）を使えるようにしました。

[ESP8266をArduinoIDEからオンライン書き込みしてみた](http://qiita.com/wamisnet/items/01e9856ac21c65c40ce7) などを参照してください。

## 速度検出

ポーリングループで、車速パルスを拾ってパルス間隔から速度を計算します。

詳細は忘れました。

特記事項としては、WiFiの処理があるとloop()がしばらく呼ばれないことがあるので、うっかりすると実際より遅いスピードと判断される場合があります。そんなときは**計算しない**という戦術を採っています。

**HOGE**という定数があります。車種ごとに異なると思います。

車速パルスは、タイヤの周長の整数分の一毎に上がってくると思っていたのですが、どうも違うようです。

[Wikipedia](https://ja.wikipedia.org/wiki/%E8%BB%8A%E9%80%9F%E4%BF%A1%E5%8F%B7#.E8.A6.8F.E6.A0.BC) を参考に定数を作っています。うちのクルマでは、それっぽい速度が計算できています。（メーター読みより速い数字なので、おそらく実際の速度に近い数字になっています。）

## 音量計算

20km/h で音量80、50km/hで音量90 が指定されていて、現在の速度が 30km/h です。みたいな場合に、うまい具合に間の音量を計算します。

文字で説明するのむずいです。雰囲気でいうと、斜めに線引いて交点でコレみたいな感じです。（疲れてる）

# ソース

## ino

```cpp:SpeedDependentVolumeControl.ino
#include <ESP8266WiFi.h>
#include <ESP8266WiFiMulti.h>
#include <WebSocketsServer.h>
#include <Hash.h>
#include <ESP8266WebServer.h>
#include <FS.h>
#include <ArduinoOTA.h>

#include <stdio.h>

const int MAX_SSID = 10;
char ssids[MAX_SSID][33];
char passwords[MAX_SSID][64];
int ssidMaxNum = 0; // 設定ファイルから読み取ったSSIDの数
int wifistatus = WL_NO_SHIELD;

const char* mdnsHostname = "SpDepVolCtrl";

const int PORT_LATCH = 13;
const int PORT_CLOCK = 12;
const int PORT_DATA = 14;
const int PORT_SPEED = 5;
const int PORT_LED = 16;
const int PORT_SW = 4;

const long HOGE = (3600/4*1000*10)/637;

boolean bUseWiFi = false;

int speedPortState;
byte ledState = LOW;

long speedPortTime = 0;
long prevTime = 0;
int speedPulseCount = 0;
boolean stoped = true;

int nowVolume = 0;

int speedVols[11];

ESP8266WebServer server = ESP8266WebServer(80);
WebSocketsServer ws = WebSocketsServer(81);
ESP8266WiFiMulti wifiMulti;

// ディレイ付きのdigitalWrite
void digitalWriteWithDelay(int port, int data)
{
  digitalWrite(port, data);
  delayMicroseconds(1);
}

// 電子ボリュームへのボリューム指示（下レイヤ）
void sendRawData(int data) {
  digitalWriteWithDelay(PORT_LATCH, LOW);
  for(int i=0; i<16; i++ ) {
    digitalWriteWithDelay(PORT_DATA, (data & 0x8000) ? HIGH : LOW);
    data <<= 1;
    digitalWriteWithDelay(PORT_CLOCK, LOW);
    digitalWriteWithDelay(PORT_CLOCK, HIGH);
  }
  digitalWriteWithDelay(PORT_LATCH, HIGH);
}

// 電子ボリュームへのボリューム指示（中レイヤ）
// db       : 0-95,127
// selAddr  : 0-1 (0:L 1:R)
// chipAddr : 0-3
void sendVolData(int db, int selAddr, int chipAddr) {
  sendRawData(db*512 + selAddr*16 + chipAddr);
}

// 電子ボリュームへのボリューム指示（上レイヤ）
// vol : 0小 - 100最大
void setVol(int vol) {
  int db = 100-vol;
  db = 95<db?127:db;  // 95以上はmute扱いで
  sendVolData(db, 0, 0);
  sendVolData(db, 1, 0);
}

void loadSsids()
{
  ssidMaxNum = 0;
  int ssidIndex=0;
  int passwordIndex=-1;
  String path = "/ssid.txt";
  File file = SPIFFS.open(path, "r");
  while(file.available()){
    char c = file.read();
    switch(c) {
      case ',':
        ssids[ssidMaxNum][ssidIndex] = '\0';
        ssidIndex = -1;
        passwordIndex = 0;
        break;
      case '\n':
        passwords[ssidMaxNum][passwordIndex] = '\0';
        ssidIndex = 0;
        passwordIndex = -1;
        ssidMaxNum++;
        break;
      case '\r':
        break;
      default :
        if ( passwordIndex == -1 ) {
          ssids[ssidMaxNum][ssidIndex++] = c;
        }
        else {
          passwords[ssidMaxNum][passwordIndex++] = c;
        }
        break;
    }
  }
  file.close();
  if ( 0 < passwordIndex ) {
    // 最終行の改行がない場合
    passwords[ssidMaxNum][passwordIndex++] = '\0';
    ssidMaxNum++;
  }
}

// flashから読込
void loadVols(char* path)
{
  boolean bRead = false;
  if(SPIFFS.exists(path)){
    File file = SPIFFS.open(path, "r");
    if ( file.size() == 11 ) {
      for(int i=0;i<=10;i++) {
        int val = file.read();
        speedVols[i] = val;
      }
      bRead = true;
    }
    file.close();
  }
  if (!bRead) {
    // 万が一読めなかった場合の初期値
    speedVols[0] = 80;
    for(int i=1;i<10;i++){
      speedVols[i] = 0;
    }
    speedVols[10] = 100;
  }
}

// flashに保存
void saveVols(char* path)
{
  File file = SPIFFS.open(path, "w");
  for(int i=0;i<=10;i++){
    file.write(speedVols[i]);
  }
  file.close();
}

// 鯖が持ってる値をクラに全部渡す
// （クラからの初回接続時に使う）
void sendAllVols(uint8_t num)
{
  if ( wifistatus != WL_CONNECTED ) {
    return;
  }

  // TODO 電文1個でやる
  char buff[256];
  for (int i=0;i<=10;i++ ) {
    sprintf(buff, "v,%d,%d", i*10, speedVols[i]);
    ws.sendTXT(num, buff, strlen(buff));
  }
}

// 鯖が持ってる値を全クラに全部渡す
// （flashからの値読み込み時に使う）
void sendAllVols()
{
  if ( wifistatus != WL_CONNECTED ) {
    return;
  }
  // TODO 電文1個でやる
  char buff[80];
  for (int i=0;i<=10;i++ ) {
    sprintf(buff, "v,%d,%d", i*10, speedVols[i]);
    ws.broadcastTXT(buff, strlen(buff));
  }
}

// 速度(の10倍)から音量を求める
int getVolBySpd(int speedX10)
{
  if ( 1000 < speedX10 ) {
    // 上限 100km/h
    speedX10 = 1000;
  }
  int speedL = speedX10/100;  // 10の位
  int speedH = speedL+1;

  long vol1 = speedVols[speedL];
  while(vol1==0) {
    // 0だったら下の方から拾う
    speedL--;
    vol1 = speedVols[speedL];
  }
  long vol2 = 11<=speedH ? vol1 : speedVols[speedH];
  while(vol2==0) {
    // 0だったら上の方から拾う
    speedH++;
    vol2 = speedVols[speedH];
  }

  // 線形補完で音量を求めマス
  int vol = vol1 + (vol2-vol1)*(speedX10-speedL*100)/(speedH-speedL)/100;
  Serial.printf("speedX10:%d,  spL:%d, spH:%d,  vol1:%d,vol2:%d    ", speedX10, speedL,speedH, vol1, vol2);
  return vol; 
}

void sendSpeed(int speedX10)
{
  // 音量取得
  int vol = getVolBySpd(speedX10);
  
  // 前回と違う場合のみ、実際の音量指示を行う
  if ( nowVolume != vol ) {
    setVol(vol);
    nowVolume = vol;
  }

  // WebSocketで速度と音量を送信
  int speedDecimal  = speedX10/10; // 整数部
  int speedIntegral = speedX10%10; // 小数部
  char buff[256];
  sprintf(buff, "s,%d.%d,%d", speedDecimal, speedIntegral, vol);
  Serial.printf("%s\r\n", buff);
  if ( wifistatus != WL_CONNECTED ) {
    return;
  }
  ws.broadcastTXT(buff, strlen(buff));
}

void wsEvent(uint8_t num, WStype_t type, uint8_t *payload, size_t length)
{
  Serial.printf("webSocketEvent(%d, %d, ...)\r\n", num, type);

  // 通信が発生すると取りこぼすので、仕切り直しステートへ
  speedPortState = 2;
  stoped = false;
  
  switch(type)
  {
    case WStype_DISCONNECTED:
      Serial.printf("[%u] Disconnected!\r\n", num);
      break;
    case WStype_CONNECTED:
      {
        IPAddress ip=ws.remoteIP(num);
        Serial.printf("[%u] Connected from %d.%d.%d.%d url: %s\r\n", num, ip[0],ip[1],ip[2],ip[3], payload);
        sendAllVols(num);
      }
      break;
    case WStype_TEXT:
      Serial.printf("[%u] get Text: %s\r\n", num, payload);

      char buff[length+1];
      strcpy(buff, (char*)payload);

      if ( strncmp("Save,", buff, 5 ) == 0 ) {
        char* filename = "/dataN.bin";
        filename[5] = buff[5];  // Nのところを上書き
        saveVols(filename);
      }
      else if ( strncmp("Load,", buff, 5 ) == 0 ) {
        char* filename = "/dataN.bin";
        filename[5] = buff[5];  // Nのところを上書き
        loadVols(filename);
        sendAllVols();
      }
      else if ( strncmp("v,", buff, 2 ) == 0 ) {
        // 速度ごとの音量設定
        int spd;
        int vol;

        char* p = strchr(buff+2, ',');
        *p = '\0';
        spd = atoi(buff+2);
        if ( strcmp(p+1,"true")==0 ) {
          // チェックボックスがONになったときは、
          // 線形補完した値を採用する
          vol = getVolBySpd(spd*10);
        }
        else if ( strcmp(p+1,"false")==0 ) {
          // チェックボックスがOFF→0
          vol = 0;
        }
        else {
          // range での値指定
          vol = atoi(p+1);
        }
        
        // ボリューム決定したものをブロードキャスト
        sprintf(buff,"v,%d,%d",spd,vol);
        ws.broadcastTXT(buff, strlen(buff));

        int i;
        i = spd/10;
        // TODO 値域チェック
        speedVols[i]=vol;
      }
      
      break;
    //case WStype_BIN:
    //  Serial.printf("[%u] get binary length: %u\r\n", num, length);
    //  break;
    default:
      Serial.printf("Invalid WStype [%d]\r\n", type);
      break;
  }
}

String getContentType(String filename){
  if(server.hasArg("download")) return "application/octet-stream";
  else if(filename.endsWith(".htm")) return "text/html";
  else if(filename.endsWith(".html")) return "text/html";
  else if(filename.endsWith(".css")) return "text/css";
  else if(filename.endsWith(".js")) return "application/javascript";
  else if(filename.endsWith(".png")) return "image/png";
  else if(filename.endsWith(".gif")) return "image/gif";
  else if(filename.endsWith(".jpg")) return "image/jpeg";
  else if(filename.endsWith(".ico")) return "image/x-icon";
  else if(filename.endsWith(".xml")) return "text/xml";
  else if(filename.endsWith(".pdf")) return "application/x-pdf";
  else if(filename.endsWith(".zip")) return "application/x-zip";
  else if(filename.endsWith(".gz")) return "application/x-gzip";
  return "text/plain";
}

bool handleFileRead(String path){
  Serial.println("handleFileRead: " + path);
  if(path.endsWith("/")) path += "index.html";
  if(SPIFFS.exists(path)){
    File file = SPIFFS.open(path, "r");
    size_t sent = server.streamFile(file, getContentType(path));
    file.close();
    return true;
  }
  return false;
}

void handleNotFound()
{
  // 通信が発生すると取りこぼすので、仕切り直しステートへ
  speedPortState = 2;
  stoped = false;

  if(!handleFileRead(server.uri())) {
    String message="File Not Found\n\n";
    message+="URI: "+server.uri();
    message+="\n\n";
    message+="\nMethod: "+(server.method()==HTTP_GET)?"GET":"POST";
    message+="\nArguments: ";
    message+=server.args()+"\n";
    for (uint8_t i=0; i<server.args(); i++) {
      message+=" "+server.argName(i)+": "+server.arg(i)+"\n";
    }

    message+="\n\n";
    Dir dir = SPIFFS.openDir("/");
    while (dir.next()) {
      message+=dir.fileName();
      File f = dir.openFile("r");
      message+=String(f.size());
      message+="\n";
    }
    server.send(404, "text/plain", message);
  }
}

void setupOTA()
{
  // Port defaults to 8266
  // ArduinoOTA.setPort(8266);
  
  char hostname[256];
  long chipId = ESP.getChipId();
  if ( chipId == 0xee832a ) {
    // 本番機の場合
    strcpy(hostname, mdnsHostname);
  }
  else {
    // 検証機の場合はchipIdを後ろに付ける
    sprintf(hostname, "%s-%06x", mdnsHostname, chipId);
  }
  Serial.printf("chipId %06x\r\n", chipId);
  
  // Hostname defaults to esp8266-[ChipID]
  ArduinoOTA.setHostname(hostname);

  // No authentication by default
  // ArduinoOTA.setPassword((const char *)"123");

  ArduinoOTA.onStart([]() {
    Serial.println("OTA - Start");
  });
  ArduinoOTA.onEnd([]() {
    Serial.println("\nOTA - End");
  });
  ArduinoOTA.onProgress([](unsigned int progress, unsigned int total) {
    Serial.printf("OTA - Progress: %u%%\r", (progress / (total / 100)));
  });
  ArduinoOTA.onError([](ota_error_t error) {
    Serial.printf("Error[%u]: ", error);
    if (error == OTA_AUTH_ERROR) Serial.println("OTA - Auth Failed");
    else if (error == OTA_BEGIN_ERROR) Serial.println("OTA - Begin Failed");
    else if (error == OTA_CONNECT_ERROR) Serial.println("OTA - Connect Failed");
    else if (error == OTA_RECEIVE_ERROR) Serial.println("OTA - Receive Failed");
    else if (error == OTA_END_ERROR) Serial.println("OTA - End Failed");
  });
  ArduinoOTA.begin();
}

void setup()
{
  Serial.begin(74880);
  Serial.println("Booting");

  bool res = SPIFFS.begin();
  if (!res) {
    Serial.println("SPIFFS.begin fail");
  }
  
  loadVols("/data1.bin");

  /// my setup
  pinMode(PORT_LATCH, OUTPUT);
  pinMode(PORT_CLOCK, OUTPUT);
  pinMode(PORT_DATA, OUTPUT);
  pinMode(PORT_SPEED, INPUT_PULLUP );
  pinMode(PORT_LED, OUTPUT );
  pinMode(PORT_SW, INPUT_PULLUP );
  
  digitalWrite(PORT_LATCH, HIGH);
  digitalWrite(PORT_CLOCK, HIGH);
  digitalWrite(PORT_DATA, HIGH);

  setVol(speedVols[0]);
  // ↑ まずはWiFiとかは置いといて、0km/h時のボリューム設定をします。

  loadSsids();
  for ( int i=0; i<ssidMaxNum; i++ ) {
    wifiMulti.addAP(ssids[i], passwords[i]);
  }

  setupOTA();

  server.onNotFound(handleNotFound);
  server.begin();
  
  ws.begin();
  ws.onEvent(wsEvent);
}


void loop()
{
  ledState = ledState==LOW?HIGH:LOW;
  digitalWrite(PORT_LED, ledState);

  if ( !bUseWiFi ) {
    if ( digitalRead(PORT_SW) == LOW ) {
      // ボタンが押されたら、WiFi使用モードにする（使用しないモードに戻る手段はリセットのみ）
      bUseWiFi = true;
    }
  }

  if ( bUseWiFi ) {
    int nowwifistatus = wifiMulti.run();
    if ( nowwifistatus != wifistatus ) {
      wifistatus = nowwifistatus;
      switch(nowwifistatus) {
        case WL_CONNECTED :
          Serial.print("WL_CONNECTED IP address: ");
          Serial.println(WiFi.localIP());
          break;
        case WL_NO_SHIELD :
          Serial.println("WL_NO_SHIELD");
          break;
        case WL_IDLE_STATUS :
          Serial.println("WL_IDLE_STATUS");
          break;
        case WL_NO_SSID_AVAIL :
          Serial.println("WL_NO_SSID_AVAIL");
          break;
        case WL_SCAN_COMPLETED :
          Serial.println("WL_SCAN_COMPLETED");
          break;
        case WL_CONNECT_FAILED :
          Serial.println("WL_CONNECT_FAILED");
          break;
        case WL_CONNECTION_LOST :
          Serial.println("WL_CONNECTION_LOST");
          break;
        case WL_DISCONNECTED :
          Serial.println("WL_DISCONNECTED");
          break;
      }
    }
  }

  if ( wifistatus == WL_CONNECTED ) {
    ArduinoOTA.handle();
    ws.loop();
    server.handleClient();
  }

  int speedPortLevel = digitalRead(PORT_SPEED);
  long newSpeedPortTime = millis();

  
  long diffTime = newSpeedPortTime - speedPortTime;

  if ( 5 < newSpeedPortTime-prevTime ) {
    // カウントの仕切り直し
    speedPortState = 2;
    speedPortTime = newSpeedPortTime;
    prevTime = newSpeedPortTime;
    return;
  }
  prevTime = newSpeedPortTime;
  
  switch(speedPortState) {
    case 0:
      if ( speedPortLevel ) {
        // 立上がりパルス→速度計算する、かも。
        stoped = false;
        speedPulseCount++;
  
        if ( 500 < diffTime ) {
          // 500msちょい毎に速度を計算します
          int speedX10 = HOGE*speedPulseCount/diffTime;

          //String s = String(diffTime) + ":" + String(speedPulseCount) + ":" + String(speedX10);
          //Serial.println(s);

          sendSpeed(speedX10);

          // 上の送信処理が重くてパスルを取りこぼすので、カウントの仕切り直しをします。
          speedPortState = 2;
          // stop誤判定よけ
          speedPortTime = newSpeedPortTime;
        }
        else {
          // まだ速度計算しない。。。
          speedPortState = 1;
        }
      }
      break;
    case 1:
      if ( !speedPortLevel ) {
        // 立下りパルス
        speedPortState = 0;
      }
      break;
    case 2:
      if ( !speedPortLevel ) {
        // 仕切り直しステートからの立下りパルス
        speedPortState = 3;

        // stop誤判定よけ
        speedPortTime = newSpeedPortTime;
      }
      break;
    default:
      if ( speedPortLevel ) {
        // 仕切り直しステートからの立上がりパルス→仕切り直し実行
        speedPortState = 1;

        speedPulseCount = 0;
        speedPortTime = newSpeedPortTime;
      }
      break;
  }

  // 停止判定
  if ( 1500 < diffTime ) {
    if ( !stoped ) {
      sendSpeed(0);
      //ws.broadcastTXT("s,0.0");
      stoped = true;
    }
  }
}
```

## index.html

最近のフロントエンド事情を知らない人が作っているのでいろいろ適当だと思います。

```html:index.html
<!DOCTYPE html>
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<style>
body{
   font-family: 'Hiragino Kaku Gothic ProN', 'ヒラギノ角ゴ ProN W3', Meiryo, メイリオ, Osaka, 'MS PGothic', arial, helvetica, sans-serif;
}
td,th{
   font-family: 'Hiragino Kaku Gothic ProN', 'ヒラギノ角ゴ ProN W3', Meiryo, メイリオ, Osaka, 'MS PGothic', arial, helvetica, sans-serif;
   display: table-cell;
}
.tch{
 width:5%;
 text-align:right;
}
.tsp{
 width:10%;
 text-align:right;
}
.tsl{
 width:90%
}
.big{
 font-size:40px;
 text-align:center;
 line-height:100%;
}
.sml{
 font-size:20px;
 text-align:center;
 line-height:100%;
}
</style>
<title>車速連動ボリューム制御</title>
<script>
var ws;
function $(id){return document.getElementById(id);}
function setup(){
  if ( window.location.hostname == '' ) {
    hostname = '192.168.148.126';
  }
  else {
    hostname = window.location.hostname;
  }
  
  console.log();
  ws=new WebSocket('ws://'+hostname+':81/');
  ws.onopen  = function(e){
    console.log('websock open');
  };
  ws.onclose = function(e){
    console.log('websock close');
    onDisconnect();
  };
  ws.onerror = function(e){
    console.log('websock error');
    console.log(e);
    onDisconnect();
  };
  ws.onmessage=function(e){
    console.log(e);
    command = e.data.charAt(0);
    if ( command == 's' ) {
      cols = e.data.split(',');
      spd = cols[1];
      vol = cols[2];

      $('SpeedStr').innerHTML = spd;
      $('VolumeStr').innerHTML = vol-100;

      spd = 100<spd?100:spd;
      $("VolumeBar").style.width=((vol*3)-200)+"%";
      $("SpeedBar").style.height=spd+"%";
    }
    else if ( command == 'v' ) {
      cols = e.data.split(',');
      spd = cols[1];
      vol = cols[2];
      if ( vol==0 ) {
        $('SLIDER_'+spd).hidden = true;
        //$('SLIDER_'+spd).value = vol;
        $('CHK_'+spd).checked = false;
      }
      else {
        $('SLIDER_'+spd).hidden = false;
        $('SLIDER_'+spd).value = vol;
        $('CHK_'+spd).checked = true;
      }
    }
  };
}
function onCngChk(t) {
  spd = t.id.substr(4);
  val = t.checked;
  ws.send('v,'+spd+','+val);
}
function onCngRng(t) {
  spd = t.id.substr(7);
  val = t.value;
  ws.send('v,'+spd+','+val);
}
function onDisconnect() {
  $('SpeedStr').innerHTML = "--.0";
  $('VolumeStr').innerHTML = "--";

  $("VolumeBar").style.width="0%";
  $("SpeedBar").style.height="0%";
}
</script>
</head>
<body onload=setup() style="margin:0">
<div style="text-align:center;">車速連動ボリューム制御</div>

<table style="border-collapse: collapse;width:100%">
<tr>
<td style="width:10%;color: #00ff33;">　</td>
<td style="width:50%;color: #5EB954;">Speed</td>
<td style="width:40%;color: #7D2982;">Volume</td>
</tr>
<tr>
<td></td>
<td><span class="big" id="SpeedStr">---.-</span><span class="sml">km/h</span></td>
<td><span class="big" id="VolumeStr">--</span><span class="sml">dB</span></td>
</tr>

<tr>
<td colspan="3" style="height:30px;line-height:100%;"><img src="volume.png" id="VolumeBar" style="width:100%;height:15px;"/></td>
</tr>
</table>

<hr>

<table style="border-collapse: collapse;width:100%">
<tr><td rowspan="11" style="width:5%;vertical-align:bottom;"><img src="speed.png" id="SpeedBar" style="width:100%;height:100%;"/></td>
    <td class="tch"><input id="CHK_100" type="checkbox" disabled="true" checked="true"></td><td class="tsp">100km/h</td><td><input class="tsl" id="SLIDER_100" type="range" value=80 min=80 max=100 step=1 onchange="onCngRng(this)"></td></tr>
<tr><td class="tch"><input id="CHK_90" type="checkbox" onchange="onCngChk(this)"></td><td class="tsp">90km/h</td><td><input class="tsl" id="SLIDER_90" type="range" value=80 min=80 max=100 step=1 hidden="true" onchange="onCngRng(this)"></td></tr>
<tr><td class="tch"><input id="CHK_80" type="checkbox" onchange="onCngChk(this)"></td><td class="tsp">80km/h</td><td><input class="tsl" id="SLIDER_80" type="range" value=80 min=80 max=100 step=1 hidden="true" onchange="onCngRng(this)"></td></tr>
<tr><td class="tch"><input id="CHK_70" type="checkbox" onchange="onCngChk(this)"></td><td class="tsp">70km/h</td><td><input class="tsl" id="SLIDER_70" type="range" value=80 min=80 max=100 step=1 hidden="true" onchange="onCngRng(this)"></td></tr>
<tr><td class="tch"><input id="CHK_60" type="checkbox" onchange="onCngChk(this)"></td><td class="tsp">60km/h</td><td><input class="tsl" id="SLIDER_60" type="range" value=80 min=80 max=100 step=1 hidden="true" onchange="onCngRng(this)"></td></tr>
<tr><td class="tch"><input id="CHK_50" type="checkbox" onchange="onCngChk(this)"></td><td class="tsp">50km/h</td><td><input class="tsl" id="SLIDER_50" type="range" value=80 min=80 max=100 step=1 hidden="true" onchange="onCngRng(this)"></td></tr>
<tr><td class="tch"><input id="CHK_40" type="checkbox" onchange="onCngChk(this)"></td><td class="tsp">40km/h</td><td><input class="tsl" id="SLIDER_40" type="range" value=80 min=80 max=100 step=1 hidden="true" onchange="onCngRng(this)"></td></tr>
<tr><td class="tch"><input id="CHK_30" type="checkbox" onchange="onCngChk(this)"></td><td class="tsp">30km/h</td><td><input class="tsl" id="SLIDER_30" type="range" value=80 min=80 max=100 step=1 hidden="true" onchange="onCngRng(this)"></td></tr>
<tr><td class="tch"><input id="CHK_20" type="checkbox" onchange="onCngChk(this)"></td><td class="tsp">20km/h</td><td><input class="tsl" id="SLIDER_20" type="range" value=80 min=80 max=100 step=1 hidden="true" onchange="onCngRng(this)"></td></tr>
<tr><td class="tch"><input id="CHK_10" type="checkbox" onchange="onCngChk(this)"></td><td class="tsp">10km/h</td><td><input class="tsl" id="SLIDER_10" type="range" value=80 min=80 max=100 step=1 hidden="true" onchange="onCngRng(this)"></td></tr>
<tr><td class="tch"><input id="CHK_0" type="checkbox" disabled="true" checked="true"></td><td class="tsp">0km/h</td><td><input class="tsl" id="SLIDER_0" type="range" value=80 min=80 max=100 step=1 onchange="onCngRng(this)"></td></tr>
</table>
<hr>
<div style="font-size:12px;text-align:center;">
設定１
<button onclick="ws.send('Save,1')">保存</button>
<button onclick="ws.send('Load,1')">読込</button>
&nbsp;&nbsp;
設定２
<button onclick="ws.send('Save,2')">保存</button>
<button onclick="ws.send('Load,2')">読込</button>
</div>
<hr>
</body>
</html>
```

## バーグラフ用の画像

速度表示と音量表示、2つのバーグラフがあります。それぞれ画像を用意する必要があります。

speed.png
![speed.png](https://qiita-image-store.s3.amazonaws.com/0/104360/f4823674-1664-2c13-7054-5c7ce0e7293f.png)

volume.png
![volume.png](https://qiita-image-store.s3.amazonaws.com/0/104360/97631faf-b15e-cbb6-23bb-1257f9cf84a8.png)


どいうでもいい話ですが、欅坂っぽい緑と、乃木坂っぽい紫を使っています。
また、微妙にグラデーションかけています。

## ssid.txt

使用する無線LANの、SSIDとパスワードを記述します。10個まで書けるようにしています。

```csv:ssid.txt
ssid1,password1
ssid2,password2
ssid3,password3
```


# できあがり

完成イメージ
![DSC_0780.jpg](https://qiita-image-store.s3.amazonaws.com/0/104360/222ee1c5-0cd5-4c4f-5d11-80a23a2f68cd.jpeg)

ボタンは裏側
![DSC_0778.jpg](https://qiita-image-store.s3.amazonaws.com/0/104360/c6f6e4f3-9198-f4a3-b4a1-83b46b554cee.jpeg)



# おわりに

・ESP8266のWiFiをフル活用しなくても、設定時のみ使うのもアリですよね。
・車速連動ボリュームの話は、その応用例としての紹介であり、オマケです。