2018년 11월 29일 목요일

ESP로 오실로스코프를 만들자 - #4 (최소 크기 및 비용편)

1. 제작 동기

 나는 거의 대부분의 부품 조달을 알리에서 진행하는데, 마침 배송에 따른 공백기가 생겼다. 즉 부품들을 주문했지만, 배달까지의 시간이 워낙 오래 걸리기 때문에 가끔 공백기가 찾아온다.  이 시기에 집, 직장을 오가면서 연구(?)  활동중인 나에게 아주 작고 휴대가 간편한 오실로스코프를 하나 만들어야 겠다는 생각이 갑자기 들었다.

[ 다 만들면 저정도 크기. 기판 = 5X7 Cm ]

- 이 버전은 자체적으로 정보 표시용 디스플레이가 없다. 측정 정보는 Wi-Fi로 오실로스코프에 연결한 후에 웹브라우저를 이용하여 확인이 가능하다.  아래쪽에 올린 동영상 역시 PC에서 오실로스코프로 연결한 후에 촬영하였다.

2. 요구 조건 

  즉 결과물은 아래의 조건을 만족 시킨다.
 - 3개 채널 : 3상 전기 측정이 가능하여야 한다.
 - 최소크기 : 바지 주머니 정도에 넣을 수 있어야 한다.
 - 최대 10KHz 측정 가능(음향기기나 모터 PWM 신호 정도는 측정이 가능한 수준이다 )

3. 필요 부품 및 구매 가격
 - ESP32 모듈 1개 = 5000원  [구매링크]
          ESP32계열이라면 어떠한 모듈이던 관계없다. 
          소켓과 연결핀등이 함께 동봉되어 있다. 
 - 5 X 7 Cm 기판(PCB) 2개 * 120원 = 240원 [구매링크]
          기판은 상판, 하판 해서 2장 쓰인다. 
          이 글의 첫부분에 표시한 사진은 상판을 아크릴판으로 교체한 버전이다. 
 - 스페이서 16개 * 평균 30원 = 480원 
          플라스틱으로 된거 싼거 쓰면 된다.
 - 저항 6개  * 10원 = 60원   
          1/4W 짜리이고, 사실 개당 가격은 10원 이하이다.
 - 버리기 아까운 랜케이블 30Cm = 0원
         불량난 랜케이블 버리지 않고 잘라서 사용
 - 인두, 납, 니퍼 등의 미리 준비된 것들.
  
 위의 내용을 다 더하면 5780원... 어떤것은 배송료도 포함되어 있지만, 어찌 하던 $10 이하에 모든 재료가 준비가 된다. 만약 $10 이하로 위의 재료를 공수하지 못한다면,  모든 것을 뒤로 하고 인터넷 쇼핑 스킬을 먼저 업그레이드 하는 것을 추천한다. 


3. 회로도

부품이 적은 만큼, 회로도도 간단하다.
여기서 주의할 점은 나는 330R 과 100R의 두 종류 저항을 사용하였는데, 이것은 사용자의 상황에 따라 다르다. 나는 5V 신호를 측정하기 위하여 이렇게 구성하였으며, 만약 24V  신호를 측정하려면 330R대신 1.2K나 1.5K 정도의 저항을 사용하여 분압하여야 한다.  분압 공식을 잘 모르시겟다면 구글 검색을 하자.  [구글 검색 분압계산 링크]
위의 회로와 동일한 저항을 사용한다면 약 8V까지 구별이 가능하며(즉 8V 이상 측정하면 8V라고 표시된다.) 이론상 13V 정도의 입력이 이루어지면 타는 냄새와 함께, 미량의 연기를 관찰할 수 있게 될 것이다.


4. 제작 과정

기껏해야 저항 6개 납땜 하는 정도라 아주 간단 하다.  나는 ESP32 Mini를 탈착하기위하여 소켓 방식으로 제작하였지만, 소켓 방식을 포기하면 적어도 부피는 1/2 이하로 줄어든다. 저항의 배치를 바꾸면 내가 만든 결과물의 1/ 4 부피로도 가능할듯 하다.




ESP32 Mini 보드를 끼우면 위의 그림과 같다.
아래쪽에 4개의 검정색은 측정단자를 꼽기 위한 간이형 터미널이다.  굳이 재료에 소개안한 이유는 여기 저기에  있는 것을 주서다가 썻기 때문이다.

그리고 스페이서와 다른 기판 한장을 이용하여 아래와 같이 만든다.  이 포스트의 맨 처음에 소개한 사진은 아래 그림에서 상판을 아크릴로 대체한 형태이다.





기본 컨셉은 휴대성을 위하여 아주 작게 만드는 버전이기 때문에, 전력은 별도의 장치에서 공급하는 방식이지만, 굳이 배터리를 이용해야 겠다면 이전 포스트에서 소개한 컨셉으로 만들어서 아래와 같이 붙일 수 있다.

배터리관련하여 만드는 방법은 아래의 링크로 확인이 가능하다.
[클립 2개로 만드는 18650 배터리팩]



5.  테스트 환경 및 동작 소개

신호 생성기를 따로 보유하고 있지 않는 내 입장에서 가장 손쉬운 방법은 http://onlinetonegenerator.com/ 를 이용하는 것이며, PC나 핸드폰의 오디오 단자를 이용하여 출력하면 된다.  이 신호를 이용하여 각 측정용 채널에 연결하면 된다. 아래의 사진은 동영상이나 스크린샷을 찍기 위하여 잠시 연결한 상태를 보여준다. 
[ 빵판에 꼽혀있는 모든 것들이 사용된 것은 아니다 ]


익숙하지 않은 동영상 촬영을 하여 유투브에 올려 보았다.  오실로스코프를 AP로 동작 시키고, PC에서 WiFi로 오실로스코프에 연결한뒤, 구글 크롬을 이용한 동영상이다.



PC의 스크린샷은 아래와 같다.

[ PC + 1433Hz + 3채널 분리 ]

[ PC + 1555Hz + 3채널 통합 ]

[ PC + 10KHz + 3채널 분리 ]


핸드폰의 크롬 브라우저에서 오실로스코프의 AP로 접속한 화면은 아래와 같다. PC의 웹브라우저에서는 그나마 사용하기 쉽지만, 모바일에서는 상황이 약간 다르다. 향후 기회가 되면 모바일쪽에서 동작하는 스크립트를 수정해야 할듯 하다.
[ 모바일 + 1433Hz + 1채널 ]

각 버튼에 대한 설명은 이전에 포스트한 내용에서 '5. 기능소개' 부분을 참조하면 된다.
[ ESP로 오실로스코프를 만들자 - #3 (ESP32 편) ]


6. 소스코드 및 설명

 이 버전은 ESP32의 기본 라이브러리를 제외하고 전체 소스코드를 공개한다. 즉 내가 코딩한 전체 부분을 공개 하니,  어느 용자분이 써보시고 웹 관련쪽은 수정의 댓글을 올려주시길 기대해본다.
 ESP32쪽은 코드부분에 define된 부분을 수정하여 AP모드나 Station 모드로 동작이 가능하다. 웹 페이지 관련해서는 SPIFFS를 사용하기 때문에 미리 'ESP32 Sketch data upload' 기능을 사용하여 ESP32쪽에 올려 두어야 한다.  ( [ ESP의 SPIFFS에 파일 업로드 방법 ] )

이 오실로스코프는 총 4개의 소스코드 파일로 이루어져 있으며, 각 부분의 설명은 소스코드내에 코멘트로 처리되어 있다. (이 포스트에 모든 내용을 담으려고 하였으나, 아무래도 글자수 제한이 있는듯 하다.  불가피하게 3개의 포스트로 나누어 올렸다. )

[ Web oscilloscope source code part 1 ]
1. ./Web_Oscilloscope_32.ino
   Webserver, WebsocketServer등을 이용하여 통신을 담당하고 중계하는 메인 파일
2. ./OscilloscopeClass.h
  Oscilloscope의 기능과 관련된 Class를 정의한 파일
3. /OscilloscopeClass.cpp
 Oscilloscope의 기능이 포함된 파일


[ Web oscilloscope source code part 2 ]
4. ./data/web_interface.html
  Web으로 연결할때 사용하는 파일


ESP로 오실로스코프를 만들자 - #4 (최소 크기 및 비용편) - 소스코드 #1

이 소스코드는 [ 최소 비용의 오실로스코프 만들기 ] 에 포함된 소스코드이다. 상세 내용은 링크를 참조하자.

1.  ./Web_Oscilloscope_32.ino
/* ------------------------------------------------------
  Tiny + Cheap Oscilloscope
  [[ AP MODE  Info]]  SSID : OSCIL_V06,  PWD  : 12345678

  +-------- GPL License --------------------------------+
  do not remove contact & url
  contact :  terminal0070@gmail.com
  blog :  https://andy-power.blogspot.com/
-------------------------------------------------------- */
#include <WiFi.h>
#include <WebServer.h>
#include <WebSocketsServer.h>
#include <SPIFFS.h>
#include "OscilloscopeClass.h"

#define LED_PIN       2               // ESP32 mini
#define SSID      "your router SSID"  // AP 모드인 경우 사용하지 않음
#define PWD       "your router PWD"   // AP 모드인 경우 사용하지 않음
#define AP_MODE   true                // AP 모드인지 아닌지..
                                      // Station 모드인 경우 false 하면 된다.
//--------------------------------------------------------------
// Global variables
//--------------------------------------------------------------                        
uint32_t g_dPrevMainInfoTime = 0// 화면 정보를 마지막으로 전달한 시각
uint32_t g_dPrevSubInfoTime = 0;  // 부가 정보를 마지막으로 전달한 시각
uint8_t  g_dClientCnt = 0;        // 연결된 클라이언트 
WebServer g_WebServer(80);         // WebServer
WebSocketsServer g_WebSocket(8080); // Websocket 서버
OscilloscopeClass oscilloscope;     // 오실로스코프

//--------------------------------------------------------------
// Setup function
// start wifi + webserver + websocket server + oscilloscope
//--------------------------------------------------------------
void setup() {
  Serial.begin(115200);
  if (!SPIFFS.begin()) {
    Serial.println("SPIFFS Mount Failed");
    return;
  }
  pinMode(LED_PIN, OUTPUT);
  wifiProcess();      // start wifi
  g_WebSocket.begin();
  g_WebSocket.onEvent(webSocketEvent);
  g_WebServer.begin();
  g_WebServer.on("/", handleWebRoot);
  oscilloscope.begin();
}

//--------------------------------------------------------------
// Setup function
// start wifi + webserver + websocket server + oscilloscope
//--------------------------------------------------------------
void loop() {
  if (AP_MODE == false) {
    wifiProcess();
  }
  g_WebSocket.loop();
  g_WebServer.handleClient();
  if (oscilloscope.loop()) {
    return;
  }
  uint32_t now = millis();
  if (now - g_dPrevMainInfoTime < 100) {
    if (now - g_dPrevSubInfoTime > 520) {
      // send sub data ( 2 frames per 1s )
      g_WebSocket.broadcastBIN(oscilloscope.getSubInfo(), oscilloscope.getSubInfoSize());
      g_dPrevSubInfoTime = now;
    } else {
      delay(2);
    }
    return;
  }
  // send main data  ( 10 frames per 1s)
  g_WebSocket.broadcastBIN(oscilloscope.getScreenData(), oscilloscope.getScreenDataSize());

  g_dPrevMainInfoTime = now;
  return;
}

//--------------------------------------------------------------
// build wifi connection
//--------------------------------------------------------------
void wifiProcess() {
  if (AP_MODE) {
    // start soft ap
    WiFi.mode(WIFI_AP_STA);
    WiFi.softAP("OSCIL_V06""12345678");
  } else {
    // 무선 공유기에 붙여서 쓰는 경우
    if (WiFi.status() != WL_CONNECTED) {
      bool ledOn = false;
      WiFi.begin(SSID, PWD);
      Serial.println("Connecting to WiFi..");
      while (WiFi.status() != WL_CONNECTED) {
        digitalWrite(LED_PIN, ledOn);
        ledOn = !ledOn;
        delay(200);
      }
      Serial.println(WiFi.localIP());
    }
  }
}

//--------------------------------------------------------------
// send html
//--------------------------------------------------------------
void handleWebRoot() {
  File file = SPIFFS.open("/web_interface.html""r");
  String contents = file.readStringUntil(NULL);
  if (!AP_MODE) {
    contents.replace("192.168.4.1", WiFi.localIP().toString());
  }
  g_WebServer.send200"text/html", contents );
}

//--------------------------------------------------------------
// process websocket event
//--------------------------------------------------------------
void webSocketEvent(uint8_t num, WStype_t type, uint8_t * payload, size_t length) {
  switch (type) {
    case WStype_CONNECTED:
      g_dClientCnt++;
      if (g_dClientCnt > 0) { // 연결된 클라이언트가 있으면 LED 켜기
        digitalWrite(LED_PIN, HIGH);
      }
      break;
    case WStype_DISCONNECTED:
      g_dClientCnt--;
      if (g_dClientCnt <= 0) { // 연결된 클라이언트가 없으면 LED 끄기
        digitalWrite(LED_PIN, LOW);
      }
      break;
    case WStype_BIN: // 모든 데이터는 바이너리로 온다.
      if (length >= 2) {
        oscilloscope.processUI(payload);
        g_dPrevSubInfoTime = 0;
      }
      break;
    case WStype_TEXT: break;
    case WStype_ERROR:
    case WStype_FRAGMENT_TEXT_START:
    case WStype_FRAGMENT_BIN_START:
    case WStype_FRAGMENT:
    case WStype_FRAGMENT_FIN:
      break;
  }
}


2. ./Oscilloscope.h
/* ------------------------------------------------------
  Tiny + Cheap Oscilloscope
  +-------- GPL License --------------------------------+
  do not remove contact & url
  contact :  terminal0070@gmail.com
  blog :  https://andy-power.blogspot.com/
  -------------------------------------------------------- */
#ifndef _OSCILLOSCOPE_CLASS_H_
#define _OSCILLOSCOPE_CLASS_H_

#include <Arduino.h>
#define ANALOG_PIN0   34  // 아날로그 입력 첫번째 .
#define ANALOG_PIN1   33
#define ANALOG_PIN2   35

#define CHART_WIDTH       300     // 차트 부분만 해당하는 가로 크기
                                  //  숫자는 화면에 한번에 표시하는 너비이며,
                                  // 웹방식에서는 통신을 고려해야 하기 때문에  숫자는 그닥이다.
#define CHART_HEIGHT      190     // 차트 부분만 해당하는 세로 크기
#define MAX_READ_NUM      (CHART_WIDTH + 80)  // 최대 데이터 읽기..화면에 표시하는 범위보다 살짝 넓게..
#define SECTOR_WIDTH      (MAX_READ_NUM * 8)  // maximum of each channel
#define CHANNEL_NUM       3      // 현재는 3 채널만 읽는다..(4개가 필요할까?)

class OscilloscopeClass {
  public:
    OscilloscopeClass();
    void begin(void);
    uint16_t getScreenDataSize(void);
    uint8_t *getScreenData(void);
    uint16_t getSubInfoSize(void);
    uint8_t *getSubInfo(void);
    void processUI(uint8_t* payload);
    float getVolt(uint16_t analogPinValue);
    bool loop(void);
  private:
     // 현재 상황에서 ADC  통하여 읽어야 하는 데이터 개수..
     // Tracking mode 경우 평소의 3배까지 읽는다.
    uint32_t m_dNowReadNum;      

    // ADC 부터 데이터를 읽어서 저장하는 부분
    // 일반적으로는 MAX_READ_NUM * 2 크기만큼 읽지만, Tracking모드인 경우 네배까지 읽음.
    uint8_t* m_pOriginDatas;    // 측정한 값들을 저장해 두는 
    uint8_t* m_pScreenDatas;    // 화면출력용으로 정제된 데이터를 저장하는 
    uint8_t  m_arySubInfos[40];           // 클라이언트에게 부가정보 보내기용 버퍼
    uint16_t m_aryMaxValues[CHANNEL_NUM]; //  채널별 측정된 최고값
    uint8_t  m_dChNum;                    // 현재 몇개의 채널이 켜져 있는가?
    bool     m_bCollapseChannel;          // 모아 보기 인지나누어 보기 인지 ..
   
    uint8_t  m_dTracking;       // Tracking 모드인가 아닌가..
    bool     m_bWaitHigh;       // Tracking 모드에서 기다리는 신호가 무엇인지(0에서 신호가 올라가는 것을 대기?)
    uint16_t m_dScrollStart;    // 측정된 데이터중에서 화면에 표시하는 시작 위치 (스크롤 위치)

    // 정밀도.. 1 가장 좋은 것이고.. 커질수록 나빠진다...
    //  m_dScale  3 경우 ADC 부터 세번 읽어서 두개는 버리고 하나의 값만 쓴다.
    uint8_t  m_dScale;   
    // 이건 한번 측정한 값을 얼마나.. 넓게 표시하냐의 단위이다
    //  값이 3 경우, 1 측정한 값을 화면에 3개의 픽셀로 표시한다.
    uint8_t  m_dSubScale;
   
    uint16_t m_dFrequency;        // 채널1 주파수..

    void readADC();               // 아날로그 핀들을 이용하여 데이터를 읽는다.
    void makeDisplayingtDatas();  // 화면 표시에 필요한 정보를 만든다.
    void enterTrackingMode();     // Tracking 모드로 진입한다.
    void trackingModeProcess();   // Tracking 모드에서 위상 변화를 대기중일때 처리함수
    void leaveTrackingMode();     // Tracking 모드를 종료한다.
};

#endif //_OSCILLOSCOPE_CLASS_H_


3. ./Oscilloscope.cpp
/* ------------------------------------------------------
  Tiny + Cheap Oscilloscope
  +-------- GPL License --------------------------------+
  do not remove contact & url
  contact :  terminal0070@gmail.com
  blog :  https://andy-power.blogspot.com/
  -------------------------------------------------------- */
#include "OscilloscopeClass.h"

#define DEFAULT_MAX  7
#define min(a,b)  (a > b ? b : a)
#define max(a,b)  (a > b ? a : b)

OscilloscopeClass::OscilloscopeClass() {
  m_dChNum = 3;
  m_bCollapseChannel = false;
  m_dTracking = 0;    //  값은 3종류 이며 0이면 노말모드, 1이면 트래킹 대기 모드, 2이면 트래킹정보 표시 모드
  m_dScale = 1;       // 화면 축소비율
  m_dSubScale = 1;    // 화면 확대 비율
  m_dScrollStart = 0;
  m_dFrequency = 200// 패널 1 기준 주파수..
  m_dNowReadNum = MAX_READ_NUM * 2;
}

// 반드시 초기에 한번 호출되어야 한다.
void OscilloscopeClass::begin() {
  m_pOriginDatas = (uint8_t*)malloc(SECTOR_WIDTH * CHANNEL_NUM);  // almost 10K
  m_pScreenDatas  = (uint8_t*)malloc(CHART_WIDTH * CHANNEL_NUM + 2);

  // 측정용  정의
  pinMode(ANALOG_PIN0, INPUT);
  pinMode(ANALOG_PIN1, INPUT);
  pinMode(ANALOG_PIN2, INPUT);

  // ESP32 측정 데시벨 정의
  analogSetAttenuation(ADC_6db); // 2.2v 이상이면 최대값  // 11db  하면 노이즈가 생긴다.
  // 10자리 수로 읽는다. (16비트까지 가능할것 같은데.. 의미가 있는지 모르겟슴)
  analogReadResolution(10);      // 최대 값이 1024 설정  비트수가 너무 높으면노이즈가 생긴다.
  delay(10);
}

// 최대 3개의 ADC 핀으로 부터 정보를 읽는다.
// 읽은 것은 m_pOriginDatas 저장한다.
void OscilloscopeClass::readADC() {
  int i, j;
  if (m_dTracking == 0) {   // Tracking  모드가 아닌경우
    int check = 0;
    uint32_t tstart;
    // 파형의 첫번째 올라 가는 부분 찾기...
    while (check++ < 2000) {
      if (analogRead(ANALOG_PIN0) < 3 ) {
        check = 0;
        while (check++ < 2000) {
          if (analogRead(ANALOG_PIN0) > 6) {
            tstart = micros();
            break;
          }
        }
        break;
      }
    }

    // 파형의 두번째 올라 가는 부분 찾기...
    check = 0;
    while (check++ < 2000) {
      if (analogRead(ANALOG_PIN0) < 3 ) {
        check = 0;
        while (check++ < 2000) {
          if (analogRead(ANALOG_PIN0) > 6) {
            break;
          }
        }
        break;
      }
    }

    // find frequency
    uint32_t tend = micros();
    double pulseTime = (double)(tend - tstart);
    pulseTime = max(pulseTime, 1);
    m_dFrequency = 1001000 / pulseTime; //약간의 보정을 위해서 100.01% 값으로 계산
    m_dFrequency = max(m_dFrequency, 10);
  }

  // 정해진 숫자만큼 데이터를 읽는다.
  for (i = 0; i < m_dNowReadNum; i ++) {
    for (j = 0; j < m_dScale; j++) { // 이건 스킵의 개념이다.
      m_pOriginDatas[SECTOR_WIDTH * 0 + i] = (uint8_t)(analogRead(ANALOG_PIN0) >> 2);
      if (m_dChNum >= 2) {
        m_pOriginDatas[SECTOR_WIDTH * 1 + i] = (uint8_t)(analogRead(ANALOG_PIN1) >> 2);
      }
      if (m_dChNum >= 3) {
        m_pOriginDatas[SECTOR_WIDTH * 2 + i] = (uint8_t)(analogRead(ANALOG_PIN2) >> 2);
      }
    }
  }

  //  채널별로 최대값을 찾는다.
  m_aryMaxValues[0] = DEFAULT_MAX; // 기본 최대값을 설정.
  m_aryMaxValues[1] = DEFAULT_MAX; // 어느 정도 레벨 이하는 바닥에 깔리게 만들기 위함임
  m_aryMaxValues[2] = DEFAULT_MAX; // 측정하는 것을 고려하여 변경해야 할수도 있음..
  for (uint16_t i = 0; i < m_dNowReadNum; i ++) {
    m_aryMaxValues[0] = max(m_aryMaxValues[0], m_pOriginDatas[i + SECTOR_WIDTH * 0]);
    m_aryMaxValues[1] = max(m_aryMaxValues[1], m_pOriginDatas[i + SECTOR_WIDTH * 1]);
    m_aryMaxValues[2] = max(m_aryMaxValues[2], m_pOriginDatas[i + SECTOR_WIDTH * 2]);
  }
}

// 이미 읽어 놓은 ADC 값들을 화면 표시용 버퍼에 계산해서 넣는다.
// 클라이언트에 전달할때는 최대값을 256으로 맞추어 전달한다.
void OscilloscopeClass::makeDisplayingtDatas() {
  uint16_t i, j, k;
  float aryRatio[3];

  if (m_bCollapseChannel) {
    int maxpos = max(m_aryMaxValues[0], m_aryMaxValues[1]);
    maxpos = max(maxpos, m_aryMaxValues[2]);
    aryRatio[0] = 250 / (float)maxpos;
    aryRatio[1] = 250 / (float)maxpos;
    aryRatio[2] = 250 / (float)maxpos;
  } else {
    aryRatio[0] = 250 / (float)m_aryMaxValues[0];
    aryRatio[1] = 250 / (float)m_aryMaxValues[1];
    aryRatio[2] = 250 / (float)m_aryMaxValues[2];
  }

  // 측정값들이 미세한 경우 상하폭을 확대하지 않음.
  for (i = 0; i < 3; i ++) {
    if (m_aryMaxValues[i] < DEFAULT_MAX) aryRatio[i] = 1.0;
  }

  m_pScreenDatas[0] = 0;         // main data flag
  m_pScreenDatas[1] = m_dChNum;  // 2nd byte is #channel
  uint8_t * tmpPointer = m_pScreenDatas + 2;
  int dataIndex = m_dScrollStart;
  for (i = 0; i < CHART_WIDTH;) {
    for (j = 0; j < m_dSubScale; j++) {
      tmpPointer[i + CHART_WIDTH * 0] = (uint8_t)(aryRatio[0] * m_pOriginDatas[dataIndex + SECTOR_WIDTH * 0]);
      tmpPointer[i + CHART_WIDTH * 1] = (uint8_t)(aryRatio[1] * m_pOriginDatas[dataIndex + SECTOR_WIDTH * 1]);
      tmpPointer[i + CHART_WIDTH * 2] = (uint8_t)(aryRatio[2] * m_pOriginDatas[dataIndex + SECTOR_WIDTH * 2]);
      i++;
      if (i >= CHART_WIDTH) break;
    }
    dataIndex++;
  }
}

//----------------------------------------------------------------------
// 전압 계산
//  330R 저항 하나, 100R 저항을 연결하여 분압한 경우이다.
//----------------------------------------------------------------------
//   signal ----- 330R ---+---- 100R ----------  GND
//                        |
//                       ADC
//----------------------------------------------------------------------
// 여러 차수 방정식을 고려해 보았는데 1 방정식이 가장 무난했다.
float OscilloscopeClass::getVolt(uint16_t analogPinValue) {
  if (DEFAULT_MAX >= analogPinValue) { // 너무 적은 값이 측정되면 전압을 0v 표시한다.
    return 0.000001;
  } else {
    return (0.0289 * analogPinValue + 0.592);
  }
}

//----------------------------------------------------------------------
// 중요 모드인 트래킹 모드 진입함수 이다.
// 트래킹 모드는 대기 모드후에 위상변화가 발생하면
// 그때부터 MAX_READ_NUM * 4 만큼 측정하여 화면에 표시한다.
// 트래킹 모드를 종료하기 전까지는 이미 측정된 내용만을 사용한다.
//----------------------------------------------------------------------
void OscilloscopeClass::enterTrackingMode() {
  m_dTracking = 1;
  m_dScrollStart = 0;
  m_dSubScale = 1;
  m_dScale = 1;
  m_dNowReadNum = MAX_READ_NUM * 4;

  // 일단 현재의 채널 값으로 차트를 평행선만 들어가게 만들고.
  uint8_t ch0 =  analogRead(ANALOG_PIN0) >> 2;
  uint8_t ch1 =  analogRead(ANALOG_PIN1) >> 2;
  uint8_t ch2 =  analogRead(ANALOG_PIN2) >> 2;
  for (int i = 0; i < m_dNowReadNum; i ++) {
    m_pOriginDatas[i + SECTOR_WIDTH * 0] = ch0;
    m_pOriginDatas[i + SECTOR_WIDTH * 1] = ch1;
    m_pOriginDatas[i + SECTOR_WIDTH * 2] = ch2;
  }

  // 위상 추적을 위에서 아래로 내려올때 할것인가,
  // 아래에서 위로 올라갈때 할것인가를 정하는 부분
  uint32_t loopCnt = 0;
  uint16_t chk =  analogRead(ANALOG_PIN0);
  for (int i = 0; i < 100; i ++) {
    chk = max(chk, analogRead(ANALOG_PIN0));
  }
  m_bWaitHigh = true;
  //  100 측정한 최대값이 일정값 이상이면,
  // 지금부터 내려가는 시점을 찾게 된다.
  if (chk > DEFAULT_MAX) {
    m_bWaitHigh = false;
  }
}

// 일단 트래킹 모드가 시작되면 채널기준 위상 변화가 생기지 않으면
// 그대로 멈춰 있는다위상 변화가 감지되면 그때부터 MAX_READ_NUM * 4 회를 측정하고
// 화면에 표시한다.
void OscilloscopeClass::trackingModeProcess() {
  uint16_t loopCnt = 0;
  uint16_t chk;

  // 위상이 변할때 까지 기다린다.
  // 다만 어느정도 기다리면 그외에 ESP 해야  일이 있을지도
  // 모르기 때문에 잠시 다른 작업을 하기 위하여
  // 함수를 빠져 나간다.
  while (loopCnt < 10000) {
    chk =  analogRead(ANALOG_PIN0);
    if (m_bWaitHigh && chk > DEFAULT_MAX) {
      break;
    } else if (!m_bWaitHigh && chk < 2) {
      break;
    }
    loopCnt++;
  }

  if (loopCnt >= 10000) {
    return;
  }

  // 위상 변화가 감지 되었으면 그때부터 MAX_READ_NUM * 4 회를 읽고 화면에 표시한다.
  readADC();
  makeDisplayingtDatas();
  m_dTracking = 2;  //  값은 3종류 이며 0이면 노말모드, 1이면 트래킹 대기 모드, 2이면 트래킹정보 표시 모드
}

// 트래킹된 정보를 표시하다가
// 사용자가 버튼을 눌러서 노말 모드로 돌아가는 경우
void OscilloscopeClass::leaveTrackingMode() {
  m_dTracking = 0;
  m_dNowReadNum = MAX_READ_NUM * 2;
  m_dScrollStart = 0;
  m_dSubScale = 1;
  m_dScale = 1;
}

// 화면 표시 데이터 크기
uint16_t OscilloscopeClass::getScreenDataSize(void) {
  return 2 + CHART_WIDTH * m_dChNum;
}

// 화면 표시용 데이터를 반환한다.
uint8_t *OscilloscopeClass::getScreenData(void) {
  if (m_dTracking == 0) {
    readADC();
    makeDisplayingtDatas();
  } else if (m_dTracking == 2) {
    makeDisplayingtDatas();
  }
  return m_pScreenDatas;
}

// 메인데이터 이외에 부가 데이터의 크기
uint16_t OscilloscopeClass::getSubInfoSize(void) {
  return 1 + 16;  // heaer + data
}

//----------------------------------------------------------
// 부가 데이터 순서 ( sub info bytes order)
//   0    1               2           3        4           5
// code, collapsed flag, sub scale, scale,  freq / 256, freq % 256,
//      6, 8, 10                       7,9, 11                  
//    volt (Integer part),  volt (Fraction part * 10) 
//    12                         13           
//  #total_data /  256,      #total_data % 256,
//    14                               15
//  m_dScrollStart / 256,    m_dScrollStart % 256,
//    16
//  m_dTracking
//----------------------------------------------------------
uint8_t *OscilloscopeClass::getSubInfo(void) {
  m_arySubInfos[0] = 1// main data flag
  m_arySubInfos[1] = (m_bCollapseChannel ? 1 : 0);
  m_arySubInfos[2] = m_dSubScale;
  m_arySubInfos[3] = m_dScale;
  m_arySubInfos[4] = (uint8_t)(m_dFrequency >> 8);
  m_arySubInfos[5] = (uint8_t)(m_dFrequency & 0xff);

  float volt = getVolt(m_aryMaxValues[0]);
  m_arySubInfos[6] = (uint8_t)volt;
  m_arySubInfos[7] = (uint8_t)((volt - (int)volt) * 10);
  volt = getVolt(m_aryMaxValues[1]);
  m_arySubInfos[8] = (uint8_t)volt;
  m_arySubInfos[9] = (uint8_t)((volt - (int)volt) * 10);
  volt = getVolt(m_aryMaxValues[2]);
  m_arySubInfos[10] = (uint8_t)volt;
  m_arySubInfos[11] = (uint8_t)((volt - (int)volt) * 10);
  m_arySubInfos[12] = (uint8_t)(m_dNowReadNum >> 8);
  m_arySubInfos[13] = (uint8_t)(m_dNowReadNum & 0xff);
  m_arySubInfos[14] = (uint8_t)(m_dScrollStart >> 8);
  m_arySubInfos[15] = (uint8_t)(m_dScrollStart & 0xff);
  m_arySubInfos[16] = (uint8_t)(m_dTracking);
  return m_arySubInfos;
}

//---------------------------------------------------
// 사용자 입력 처리
void OscilloscopeClass::processUI(uint8_t*  payload) {
  if (m_dTracking == 1) { // 위상 변화 대기중인 경우 무엇을 누르던 간에.. 그걸 우선 멈춘다...
    readADC();
    makeDisplayingtDatas();
    m_dTracking = 2;
    return;
  } else if (payload[0] == 1) { // button ui processing
    switch (payload[1]) {
      case 0:
        switch (m_dTracking) {
          case 0:
            m_dTracking = 1;
            enterTrackingMode();
            break;
          case 1//  함수의 첫부분에서 처리했슴
            // canceled by user
            break;
          case 2:
            leaveTrackingMode();
            break;
        }
        break;
      case 1// zoom in
        if (m_dScale > 1) {
          m_dScale--;
        } else {
          m_dSubScale ++;
          m_dSubScale = min(8, m_dSubScale);
        }
        break;
      case 2// zoom out
        if (m_dSubScale > 1) {
          m_dSubScale--;
        } else {
          m_dScale++;
          m_dScale = min(8, m_dScale);
          if (m_dTracking != 0) {
            m_dScale = min(1, m_dScale);
          }
        }
        break;
      case 3//  <  scroll left
        if (m_dTracking == 0) {
          m_dScrollStart = max(m_dScrollStart - 60);
        } else {
          m_dScrollStart = max(m_dScrollStart - 180);
        }
        break;
      case 4//  > scroll right
        if (m_dTracking == 0) {
          m_dScrollStart = min(m_dScrollStart + 6, m_dNowReadNum - CHART_WIDTH - 1);
        } else {
          m_dScrollStart = min(m_dScrollStart + 18, m_dNowReadNum - CHART_WIDTH - 1);
        }
        break;
      case 5//  collapse
        m_bCollapseChannel = !m_bCollapseChannel;
        break;
      case 6// change  number of channel
        m_dChNum++;
        if (m_dChNum >= 4) m_dChNum = 1;
        // Serial.println(m_dChNum);
        break;
      case 10// scroll position
        m_dScrollStart = payload[2] * 256 + payload[3];
        m_dScrollStart = max(m_dScrollStart, 0);
        m_dScrollStart = min(m_dScrollStart, m_dNowReadNum - CHART_WIDTH - 1);
    }
  }
}

// 정보를 클라이언트에게 보내야 하는 경우에는 false 반환
bool OscilloscopeClass::loop(void) {
  if (m_dTracking == 1) {
    trackingModeProcess();
    return true;
  } else {
    return false;
  }
}



3단 6핀 스위치로 DC 모터의 회전 방향을 바꾸어 보자

1. 필요는 연구의 어머니 항상 느끼는 부분이다. 필요하지 않으면 연구하지 않으며, 필요하면 연구한다. DC 모터를 조건에 따라서 정방향 또는 역방향으로 회전시켜야 하는 필요가 생겼다. 처음에는 MCU 및 Relay Switch를 이용하는 방법을 생각...