레이블이 ESP32 mini인 게시물을 표시합니다. 모든 게시물 표시
레이블이 ESP32 mini인 게시물을 표시합니다. 모든 게시물 표시

2019년 10월 14일 월요일

가장 싸게 CCTV를 구현해 보자 (ESP32-CAM)

1. 카메라 모듈의 연구

여러 종류의 카메라 모듈을 연구했다. MCU와 결합해서 사진을 서버로 전송하는 기능을 만들기도하고, 원격 조정이 가능한 탱크(물론 이것 역시 제작한 것)에 올려서 스트리밍을 기능을 하는 것도 만들기도 했다. 하지만 카메라 모듈의 크기나 가격, 연구 난이도 등등 여러 이유때문에 많이 만들지는 않았다. (모듈별로 한두개 정도)

 
[ 처음 연구한 카메라 모듈 ]

[ 네번째 연구한 모듈 ]

[ 연구후에 기판에 납땜한 모습 ]


[ 배선이 복잡해서 양면으로 배선햇다. ㅠㅠ ]

이정도 까지 연구하고  나니, PCB를 떠서 다량으로 만들까 생각도 했지만, 카메라 모듈의 화질이 별로 여서 저정도까지 만들고 멈추었다. 

2. ESP-EYE, ESP32-CAM

 마지막에 연구한 것이 바로 ESP32와 카메라 모듈을 결합하는 형태인데, 이젠 아예 쉽게 결합이 가능한 형태로 만들어진 제품이 나오고 있었다. 즉 납땜할 필요가 없는 정도의 레벨로 제품이 나오고 있으며, 가격또한 저렴했다.

알리 기준으로 ESP32-CAM은 5$ 이하에 구입이 가능하다.

[ 외부에 나와 있는 핀들의 배열 ]

[ 내부 핀 정보 ]


3. 동작을 시키는 방법

 정말 손쉽다. 뭐 별거 할것이 없는 정도로 쉬웠다. 먼저 Arduino IDE에서 보드매니저를 열고 ESP32 보드를 추가하자.

먼저 IDE의 파일 > 환경설정창에 추가적인 보드매니저를 추가하자. 아래 그림의 표시한 부분 버튼을 누르고 https://dl.espressif.com/dl/package_esp32_index.json 를 추가 한다.

그리고 IDE의 툴 > 보드 > 보드 매니저 에서 esp32를 검색하여 설치
[ 보드 매니저에서 esp32를 검색하고 설치 ]


거의 다 되었다.  그다음 IDE의 파일 > 예제 > ESP32 > Camera > CameraWebServer를 열면된다.  소스코드에서 구매한 모듈과 맞는 핀번호를 설정해야 하는데, 핀별로 설정하는 것이 아니라 이미 define 된 것중에서 선택하면 손쉽다. 아래의 그림과 같이 다른 제품들의 define 내용은 코멘트 처리하고 AI-THINKER 쪽을 활성화 하자.

[ 소스코드에서 수정할 부분은 위에 표시한 곳 및 WiFi 관련 비번 정도 ]

그 다음은 IDE에서  툴 메뉴에서 보드 및 파티션 정보를 올바르게 넣으면 된다.
보드는 "ESP32 Wrover Modue" 이며,  파티션 스키마는 "Huge APP (3MB No OTA)"로 설정하면 된다. 

[ 보드및 파티션 스키마 !! ]

이제 컴파일 하고 스케치만 업로드 하면 된다. 

[ 이렇게 연결하면 업로드 가능 ]

위와 같이 연결하고 스케치 업로드한 후에 GND + IO 0 핀에 연결된 부분을 해제 하고 리셋버튼을 누르면 동작한다. 리셋하기 전에 Serial Monitor를 켜고 리셋하자. 리셋된 후에 Serial Monitor에 아이피가 표시될 것이다.

4. 결과는?

 웹브라우져에서 표시된 아이피를 입력하고 연결하면 아래와 같이 설정가능한 화면과 현재 모습이 라이브로 방송되고 있을 것이다.  

[ 고해상도도 가능하다 ]

5. 향후 작업 예정

 다중 접속 활성화, 웹페이지 개선, 리셋 되더라도 환경변수 기억 등등..



2019년 7월 14일 일요일

ESP로 오실로스코프를 만들자 - #5 ( 성능편)

1. 오실로스코프는 만들만큼 만들어 보았다.

 비교적 저렴한 부품을 이용하여 오실로스코프를 참 여러 종류 만들어 보았다.  내 기준의 필요성으로만 따져보면 ESP8266버전으로도 충분하지만 지인을 위하여 제작하다 보니 여러 버전을 만들게 되었다.  이번 버전을 마지막으로 필요성이 대두되지 않는한 더이상의 업그레이드 버전은 제작하지 않을 예정이다.

2. 마지막 버전은 무엇이 다른가?

 이전버전과 비교하여 논리적으로는 크게 차이가 없지만, 물리적으로 차이가 있다. 그것중 하나가 분압 저항이 소켓 방식이다.  필요에 따라 분압의 정도를 조절할 수 있다는 것이다. 사용자의 선택에 따라서 1/10, 1/20, 1/50 등의 분압 저항을 쓸수 있도록 제작하였다.

먼저 분압저항세트의 크기를 줄이기 위하여 이번에는 칩저항을 사용하였다. 물론 크기적인 문제도 있엇지만 칩저항이 가격적인 장점이 있어서 앞으로 지속적으로 사용할 예정이다. (칩저항이 3/4W 기준으로 일반 저항의 1/4W 보다 저렴하다)

칩저항에 대한 내용은 아래의 링크 참조
[칩 저항애 대하여 알아보자 ]

먼저 앞면은 아래의 사진 처럼 배치하고,
[ 분압 저항의 앞면 ]

뒷면은 아래와 같이 배치하고,
[ 분압 저항의 뒷면 ]
전선을 연결하고, 다리를 납땜하였다.

그리고 오실로스코프쪽에는 소켓을 배치하여 필요에 따라 꼽았다가 뺏다가 할 수 있도록 제작하였다. 분압저항세트는 3종류를 준비하였으며, 필요에 따라 얼마든지 분압 비율을 조절하여 제작하고 설치가 가능하도록 하였다. 이미 만든 세트는 오류를 방지하기 위하여 최대 내압을 프린트하여 붙였다.

[ 소켓 방식으로 제작 하여 탈착 가능 ]


예전에 ADC Chip(ADS7888)을 힘들게 납땜 하였는데, 그나마 이번에는 어뎁터를 구매하여 납땜하였다. 어뎁터 구매 이전에는 납땜 성공 확률이 30% 였는데, 어뎁터 사용으로 100% 성공하였다.
[ 어뎁터를 이용하여 1차 남땝 (초록색) ]



또하나의 특징이 바로 프로브 커넥터 부분이다. 이전까지의 모델은 실용적이긴 하나 커넥터 부분이 워낙 작고, 불안하여 지인은 늘 개조하여 사용하고 있는 점을 감안하여, 이번에는 바나나잭을 이용하여 탈착이 가능하게 하였다.

[ 색상도 맞춘 바나나잭 ]


3. 만드는 과정


 일단 모듈화 한 부품들이 배치될 자리를 살짝 올려 본다.
[ 이렇게 자리 잡으면 될 듯 ]

자리를 일단 잡고 나서 납땜을 했다. 손이 크다 보니 생각보다 어렵긴 했다.

[ 납땜을 이리 저리 하고 ... ]

전원 모듈은 핸드폰에서 뜯은 배터리를 사용하기로 했다.
(사실 새 배터리가 아니여서 생각보다 사용 시간이 짧다는 후기를 들었다. ㅠㅠ)
[ 배터리는 애플 정품? ㅋㅋ ]

기존에 사용하던 스위치도 오작동이 너무 많아서, 이번에는 스위치도 바꾸었다.
[ 스위치의 변신.. 단가가 높을 뿐이다 흐흐 ]

[ 분압 저항이 교체가 쉽도록 배치했건만.. 현실은..ㅠㅠ ]

4. 다 만든 모습

 이전 버전과 비교할때 전면부의 외형은 크게 다를 바가 없다.  두께가 좀 낮아졌을 뿐이다. 그외에 소프트웨어적으로 변경된 부분이라면 분압 저항에 따른 비율 변경 기능이 추가 되었을 뿐이다.

[ 선물용 버전 ]



동작하는 화면을 동영상으로 찍어보았다.



5. 끝으로

 늘 끝없는 도전으로 인생을 살고 있는 나에게 오실로스코프는 새로운 도전이었다. 기성화된 제품에 비교하여 턱없이 부족한 성능일지는 몰라도, 기능을 필요에 따라 특화가 가능하다는 점이 매력이 있다. (필요하면 해당 부분을 코딩하면 되니까..)
 글로는 소개하지 않았지만, 이 오실로스코프의 경우 UDP 통신을 이용한 리모콘이 있다. 오실로스코프의  기능을 리모콘 하나로 사용 가능하다.   로터리인코더를 사용한 버전인데 기회가 되면 따로 소개해야 겠다.



2018년 12월 3일 월요일

ESP32의 Dual Core를 활용하자.

1. ESP32와 ESP8266의 차이는?

 ESP32는 ESP8266과 많은 차이를 보인다.  그중 가장 으뜸을 꼽으라 하면 듀얼코어와 싱글코어의 차이라고 하겠다.  ESP32는 듀얼코어이다. !!

2. 음 듀얼코어라고? 난 안쓰는데?

 사실 이미 쓰고 있다. 느끼지 못할 뿐이다.  Arduino IDE로 프로그램을 만들어서 실행중이라면 이미 2개의 코어를 모두 쓰고 있다. 다만 모르고 있을 뿐이다.
일반적으로
  Core 0 : 무선 통신과 같은  ESP32 본연의 기능이 실행됨
  Core 1 : 컴파일된 Arduino 코드가 실행됨

3. 알아서 듀얼 코어를 잘 사용하는데 왜?

 사실 간단하게 온도나 측정하거나 LED나 껏다가 켜는 등의 별거 아닌것들을 하는 경우 큰 의미가 없다. 온도나 측정하는데 ESP32는 오버 스펙이다.  각설하고, ESP32의 코어 하나로 수행하기에 벅찬 작업을 하는 듀얼 코어를 사용한다.  ESP32의 Core 0은 무선 통신과 같은 작업을 수행하기에 상대적으로 널널하다. 즉 대부분 쉬고 있다. 이를 활용하면 ESP32의 기능을 극대화 할 수 있다.  또는 Core 0을 이용하여 Core 1의 패닉 상태를 조사하고, 필요시 리부팅을 할 수 있다.

4. 사용 예제는?

크게 어렵지 않은 예제 코드를 아래에 기록해 본다.  예제는 코어 2개에서 각각의 analogRead를 수행하는 예제이다.

/* -----------------------------------------------
   ESP32 Dual Core tester
  ----------------------------------------------- */

//-----------------------------------------------
// global functions
//-----------------------------------------------
void Task1(void * parameter)
{
  while (true) {
    unsigned long start = micros();
    // very long task..
    for ( uint32_t i = 0; i < 1000; i++) {
      analogRead(33);
    }
    Serial.printf("Task 1 done  (on Core : %d)  Elps = %d  µs \n", xPortGetCoreID(), micros() - start);
    delay(100);
  }
}
void Task2(void * parameter)
{
  while (true) {
    unsigned long start = micros();
    // very long task..
    for ( uint32_t i = 0; i < 1000; i++) {
      analogRead(35);
    }
    //workLoad();
    Serial.printf("Task 2 done  (on Core : %d)  Elps = %d  µs \n", xPortGetCoreID(), micros() - start);
    delay(100);
  }
}
TaskHandle_t TaskA, TaskB;

//-----------------------------------------------
// setup functions
//-----------------------------------------------
void setup() {
  Serial.begin(115200);
  delay(100);

  xTaskCreatePinnedToCore(
    Task1,                  /* pvTaskCode */
    "Workload1",            /* pcName */
    1000,                   /* usStackDepth */
    NULL,                   /* pvParameters */
    1,                      /* uxPriority */
    &TaskA,                 /* pxCreatedTask */
    0);                     /* xCoreID */

  xTaskCreatePinnedToCore(
    Task2,
    "Workload2",
    1000,
    NULL,
    1,
    &TaskB,
    1);
}

//-----------------------------------------------
// loop functions
//-----------------------------------------------
void loop() {
  // This task will run in the core 1
  unsigned long start = micros();
  for ( uint32_t i = 0; i < 1000; i++) {
    // do nothing
  }
  Serial.printf("Task 0 done  (on Core : %d)  Elps = %d  µs \n", xPortGetCoreID(), micros() - start);
  delay(10) ;
}



위의 예제를 실행 시키면 아래와 같은 출력을 볼 수 있다.

Task 0 done  (on Core : 1)  Elps = 0  µs 
Task 0 done  (on Core : 1)  Elps = 2  µs 
Task 1 done  (on Core : 0)  Elps = 12450  µs 
Task 2 done  (on Core : 1)  Elps = 12380  µs 
Task 0 done  (on Core : 1)  Elps = 0  µs 
Task 0 done  (on Core : 1)  Elps = 0  µs 
Task 0 done  (on Core : 1)  Elps = 0  µs 
Task 0 done  (on Core : 1)  Elps = 0  µs 
Task 0 done  (on Core : 1)  Elps = 0  µs 




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;
  }
}



ESP8266 Digital Input Pin의 불편한 진실

1. 믿었던 놈인데... ESP8266은 2017년도 부터 사용했기 때문에, 7년 정도 사용해 왔었다. 그동안 여러 종류의 내 프로젝트에 사용한 MCU이고, 이에 따라서 신뢰도가 상당히 높았다. 물론 순수한 ESP8266으로 사용하지는 않고 Wemos...