본문 바로가기
Hardware/MCU(Arduino,ESP8266)

아두이노 + ESP8266으로 무선으로 확인하는 오실로스코프 (Arduino -Web Oscilloscope 수정)

by lovey25 2021. 12. 16.
반응형

남아서 돌아다니는 아두이노와 wifi모듈을 이용해서 오실로스코프를 흉내 내 볼 수 있는 아주 재미난 설루션이 있어서 공유합니다.

배경

아두이노 등의 보드를 다루다 보면 디버깅이 지원되지 않는 환경에서 원하는 동작이 되지 않을 때 뭐가 문제인지 몰라서 답답하는 적이 있습니다. 이럴 때 아주 가~끔 오실로스코프가 있으면 확인해 볼 수 있을 것 같은데 라는 생각을 할 때가 있습니다. 물론 그 아주 가~끔의 활용도를 위해서 비싼 돈을 들여서 오실로 스코프를 사는 건 배보다 배꼽이 더 큰 상황이기 때문에 다른 방법을 찾게 되는데요. 오늘 소개할 설루션은 바로 이런 상황에서 사용해 볼 수 있을 유용한 팁 되겠습니다.

 

Arduino - Web Oscilloscope (support trigger) - PHPoC Forum

Demonstration https://www.youtube.com/watch?v=1QMgCSfq6Mk Features Support 6 channels Support single trigger, multiple triggers Selectable trigger mode: falling, rising, falling & rising Settable trigger value Adjusting time division via a web knob Adjusti

forum.phpoc.com

이 설루션은 아두이노를 오실로스코프로 활용할 수 있는 다양한 팁들을 검색하다가 찾은 것인데 UI가 가장 이쁘고 측정 결과를 네트워크로 보내줘서 굳이 PC가 없더라도 사용할 수 있다는 장점이 있어서 선택하게 되었습니다.  PHPoC forum이라는 곳에 소개된 방법인데 유일한 단점이라면 PHPoC라는 아두이노 실드를 구매해야 한다는 것인데 이 제품 가격이 무려 4~5만 원은 되는 거 같습니다. 그러나 다행히 내용을 보다 보니 제가 요즘 애정하고 있는 ESP8266 모듈을 활용한다면 PHPoC를 대체할 수도 있을 것 같았습니다.

설루션 분석

소개된 설루션은 이런 방식으로 동작합니다. 먼저 아두이노는 오실로스코프의 본체가 됩니다. 아두이노의 아날로그 핀이 검침 단자가 되는데 아날로그 핀에 걸리는 전압값을 읽어줍니다. 그리고 이 아두이노는 PHPoC라는 실드와 UART로 연결이 되는데 아두이노에서 일어들인 전압값을 특정 포맷에 맞추어 PHPoC로 전송합니다. PHPoC는 네트워킹 기능을 제공하는 실드로써 웹서버 역할을 할 수 있습니다. PHPoC는 클라이언트가 접속을 하면 UART로 들어오는 데이터를 클라이언트로 전달해 주는 역할을 하고 클라이언트는 서버에 등록된 자바스크립트를 통해서 데이터를 분석해 클라이언트 화면에 결과를 보여주게 됩니다.

PHPoC가 하는 일은 UART로 들어오는 데이터를 웹 프로토콜을 통해서 클라이언트로 전달해주는 일인데 이 정도 일이라면 ESP8266 모듈로도 충분히 할 수 있습니다. 그래서 제가 애용하는 Wemos D1 mini 모듈을 대신해서 사용해 보기로 하였습니다. 

참고로 ESP8266 모듈을 이용해서 본 설루션을 구현하는데 필요한 유사 기능을 구현해본 포스팅도 있으니 참고해 주세요. ^^

 

ESP8266 웹서버+웹소켓+파일시스템(SPIFFS) 예제

ESP8266 모듈을 사용해서 전등을 컨트롤하는 프로젝트를 진행했었습니다.  컨트롤 인터페이스를 위해서 ESP8266 모듈을 웹서버로 활용할 수 있는 기능을 사용했었는데요. HTML 코드를 문자열로 저장

kwonkyo.tistory.com

 

ESP8266 모듈로 무선(WiFi) 시리얼 모니터링

http://blog.daum.net/pg365/276 아두이노, ESP8266 등 MCU를 이용한 프로젝트를 할 때 디버깅의 중요한 툴 중 하나가 바로 시리얼 모니터입니다. 결과도 확인하고 컨트롤도 하고 여러 방면으로 많이 쓰이죠.

kwonkyo.tistory.com

하드웨어

하드웨어 구성은 아주 단순합니다. Wemos D1 mini모드와 Arduino Uno 보드를 연결만 해주면 되는데, Arduino의 Tx핀과 Wemos의 Rx 핀을 서로 연결합니다. 이때 Arduino는 동작전압이 5V이고 Wemos는 3.3V이기 때문에 1kΩ, 2.2kΩ 저항을 이용해서 Arduino Tx신호를 전압 분배해서 Wemos Rx로 연결했습니다.

그리고 Wemos 보드는 동작전압은 3.3V이지만 5V 핀이 내장되어 있기 때문에 전원용으로 5V와 GND를 각각 묶어주었습니다. 

그림은 지저분하게 그려져 있지만 핵심은 '아두이노 아날로그 핀의 전압을 읽어서 Tx핀으로 시리얼 데이터를 보내주고 이 데이터 신호는 5V라서 전압 분배해서 Wemos D1 모듈의 Rx핀으로 들어간다' 이게 전부입니다. 

그리고 편하게 사용하기 위해서 아두이노 우노의 실드 형태로 좀 묶어봤습니다.

소프트웨어

이제 펌웨어를 올려보겠습니다.

아두이노 쪽은 포럼에 공개된 소스에서 PHPoC관련 내용은 필요 없기 때문에 제외시켰습니다. 그리고 12행과 44행은 뒤에서 설명할 문제점 때문에 약간 수정을 했는데요. 결론적으로는 Arduino와 Wemos 간의 통신 baudrate를 최대로 올리는 것으로 수정했습니다.

analogRead() 함수를 이용해서 아날로그 핀 전압을 각각 읽어서 timestamp와 함께 시리얼 포트로 출력하는 로직입니다.

#include <Arduino.h>

#include <SPI.h>

#define AREF 5.0
#define ADC_MAX 1023.0


float ratio = AREF / ADC_MAX;

void setup() {
  Serial.begin(230400);
}

void loop() {
  //read system time
  unsigned long time_a = micros();
 
  // read the analog value  and convert to voltage:
  float voltageChannel0 = analogRead(A0) * ratio;
  float voltageChannel1 = analogRead(A1) * ratio;
  float voltageChannel2 = analogRead(A2) * ratio;
  float voltageChannel3 = analogRead(A3) * ratio;
  float voltageChannel4 = analogRead(A4) * ratio;
  float voltageChannel5 = analogRead(A5) * ratio;

  // send system time first
  Serial.print(time_a);
  Serial.print(" ");

  // send value of each channel, seperated by " " or "\t".
  Serial.print(voltageChannel0);
  Serial.print(" ");
  Serial.print(voltageChannel1);
  Serial.print(" ");
  Serial.print(voltageChannel2);
  Serial.print(" ");
  Serial.print(voltageChannel3);
  Serial.print(" ");
  Serial.print(voltageChannel4);
  Serial.print(" ");
  // the last channel must send with new line charaters
  Serial.println(voltageChannel5);
  //delayMicroseconds(150);
}

다음으로 Wemos D1 mini 쪽 소스입니다. Wemos 쪽은 크게 2가지로 구분시킬 수 있는데요. 첫 번째는 Wemos D1 mini를 동작시키는 펌웨어이고 두 번째는 그 위에서 GUI를 구현하는 웹서비스 관련 소스입니다. 먼저 펌웨어입니다.

#include <Arduino.h>

#include <ArduinoOTA.h>
#ifdef ESP32
#include <FS.h>
#include <SPIFFS.h>
#include <ESPmDNS.h>
#include <WiFi.h>
#include <AsyncTCP.h>
#elif defined(ESP8266)
#include <ESP8266WiFi.h>
#include <ESPAsyncTCP.h>
#include <ESP8266mDNS.h>
#endif
#include <ESPAsyncWebServer.h>
#include <SPIFFSEditor.h>

AsyncWebServer server(80);
AsyncWebSocket ws("/ws");
AsyncEventSource events("/events");

const char* ssid = "SSID";
const char* password = "PASSWORD";
const char * hostName = "oscilloscope";
const char* http_username = "admin";
const char* http_password = "admin";

void setupCONNECTION() {
  WiFi.mode(WIFI_AP_STA);
  WiFi.softAP(hostName);
  WiFi.begin(ssid, password);
  Serial.println("\n");
  while (WiFi.status() != WL_CONNECTED) {
    delay(500);
    Serial.print(".");
  }  
  Serial.println('\n');
  Serial.println("Connected!");
  Serial.print("IP address:\t");     
  Serial.println(WiFi.localIP());     // 할당받은 IP주소 표시

  if (WiFi.waitForConnectResult() != WL_CONNECTED) {
    Serial.printf("STA: Failed!\n");
    WiFi.disconnect(false);
    delay(1000);
    WiFi.begin(ssid, password);
  }
}

void setupOTA() {
  //ArduinoOTA.setPort(8266);           // Port defaults to 8266
  //ArduinoOTA.setHostname(hostName);   // Hostname defaults to esp8266-[ChipID]
  ArduinoOTA.setPassword("admin");      // No authentication by default

  // Password can be set with it's md5 value as well
  // MD5(admin) = 21232f297a57a5a743894a0e4a801fc3
  // ArduinoOTA.setPasswordHash("21232f297a57a5a743894a0e4a801fc3");

  ArduinoOTA.onStart([]() { events.send("Update Start", "ota"); });
  ArduinoOTA.onEnd([]() { events.send("Update End", "ota"); });
  ArduinoOTA.onProgress([](unsigned int progress, unsigned int total) {
    char p[32];
    sprintf(p, "Progress: %u%%\n", (progress/(total/100)));
    events.send(p, "ota");
  });
  ArduinoOTA.onError([](ota_error_t error) {
    if(error == OTA_AUTH_ERROR) events.send("Auth Failed", "ota");
    else if(error == OTA_BEGIN_ERROR) events.send("Begin Failed", "ota");
    else if(error == OTA_CONNECT_ERROR) events.send("Connect Failed", "ota");
    else if(error == OTA_RECEIVE_ERROR) events.send("Recieve Failed", "ota");
    else if(error == OTA_END_ERROR) events.send("End Failed", "ota");
  });
  
  ArduinoOTA.begin();
  Serial.println("OTA Ready");
}

void webSocketEvent(AsyncWebSocket * server, AsyncWebSocketClient * client, AwsEventType type, void * arg, uint8_t *data, size_t len){
  if(type == WS_EVT_CONNECT){
    Serial.printf("ws[%s][%u] connect\n", server->url(), client->id());
    client->printf("Hello Client %u :)", client->id());
    client->ping();
  } else if(type == WS_EVT_DISCONNECT){
    Serial.printf("ws[%s][%u] disconnect\n", server->url(), client->id());
  } else if(type == WS_EVT_ERROR){
    Serial.printf("ws[%s][%u] error(%u): %s\n", server->url(), client->id(), *((uint16_t*)arg), (char*)data);
  } else if(type == WS_EVT_PONG){
    Serial.printf("ws[%s][%u] pong[%u]: %s\n", server->url(), client->id(), len, (len)?(char*)data:"");
  } else if(type == WS_EVT_DATA){
    AwsFrameInfo * info = (AwsFrameInfo*)arg;
    String msg = "";
    if(info->final && info->index == 0 && info->len == len){
      //the whole message is in a single frame and we got all of it's data
      Serial.printf("ws[%s][%u] %s-message[%llu]: ", server->url(), client->id(), (info->opcode == WS_TEXT)?"text":"binary", info->len);

      if(info->opcode == WS_TEXT){
        for(size_t i=0; i < info->len; i++) {
          msg += (char) data[i];
        }
      } else {
        char buff[3];
        for(size_t i=0; i < info->len; i++) {
          sprintf(buff, "%02x ", (uint8_t) data[i]);
          msg += buff ;
        }
      }
      Serial.printf("%s\n",msg.c_str());

      if(info->opcode == WS_TEXT)
        client->text("I got your text message");
      else
        client->binary("I got your binary message");
    } else {
      //message is comprised of multiple frames or the frame is split into multiple packets
      if(info->index == 0){
        if(info->num == 0)
          Serial.printf("ws[%s][%u] %s-message start\n", server->url(), client->id(), (info->message_opcode == WS_TEXT)?"text":"binary");
        Serial.printf("ws[%s][%u] frame[%u] start[%llu]\n", server->url(), client->id(), info->num, info->len);
      }

      Serial.printf("ws[%s][%u] frame[%u] %s[%llu - %llu]: ", server->url(), client->id(), info->num, (info->message_opcode == WS_TEXT)?"text":"binary", info->index, info->index + len);

      if(info->opcode == WS_TEXT){
        for(size_t i=0; i < len; i++) {
          msg += (char) data[i];
        }
      } else {
        char buff[3];
        for(size_t i=0; i < len; i++) {
          sprintf(buff, "%02x ", (uint8_t) data[i]);
          msg += buff ;
        }
      }
      Serial.printf("%s\n",msg.c_str());

      if((info->index + len) == info->len){
        Serial.printf("ws[%s][%u] frame[%u] end[%llu]\n", server->url(), client->id(), info->num, info->len);
        if(info->final){
          Serial.printf("ws[%s][%u] %s-message end\n", server->url(), client->id(), (info->message_opcode == WS_TEXT)?"text":"binary");
          if(info->message_opcode == WS_TEXT)
            client->text("I got your text message");
          else
            client->binary("I got your binary message");
        }
      }
    }
  }
}

void setupWEBSOCKET() {
  ws.onEvent(webSocketEvent);
  server.addHandler(&ws);
  Serial.println("WebSocket server started.");
}

void setupWEBPAGE() {
  SPIFFS.begin();
  
  events.onConnect([](AsyncEventSourceClient *client){
    client->send("hello!",NULL,millis(),1000);
  });
  server.addHandler(&events);

#ifdef ESP32
  server.addHandler(new SPIFFSEditor(SPIFFS, http_username,http_password));
#elif defined(ESP8266)
  server.addHandler(new SPIFFSEditor(http_username,http_password));
#endif
  
  server.on("/heap", HTTP_GET, [](AsyncWebServerRequest *request){
    request->send(200, "text/plain", String(ESP.getFreeHeap()));
  });

  server.serveStatic("/", SPIFFS, "/").setDefaultFile("index.html");

  server.onNotFound([](AsyncWebServerRequest *request){
    Serial.printf("NOT_FOUND: ");
    if(request->method() == HTTP_GET)
      Serial.printf("GET");
    else if(request->method() == HTTP_POST)
      Serial.printf("POST");
    else if(request->method() == HTTP_DELETE)
      Serial.printf("DELETE");
    else if(request->method() == HTTP_PUT)
      Serial.printf("PUT");
    else if(request->method() == HTTP_PATCH)
      Serial.printf("PATCH");
    else if(request->method() == HTTP_HEAD)
      Serial.printf("HEAD");
    else if(request->method() == HTTP_OPTIONS)
      Serial.printf("OPTIONS");
    else
      Serial.printf("UNKNOWN");
    Serial.printf(" http://%s%s\n", request->host().c_str(), request->url().c_str());

    if(request->contentLength()){
      Serial.printf("_CONTENT_TYPE: %s\n", request->contentType().c_str());
      Serial.printf("_CONTENT_LENGTH: %u\n", request->contentLength());
    }

    int headers = request->headers();
    int i;
    for(i=0;i<headers;i++){
      AsyncWebHeader* h = request->getHeader(i);
      Serial.printf("_HEADER[%s]: %s\n", h->name().c_str(), h->value().c_str());
    }

    int params = request->params();
    for(i=0;i<params;i++){
      AsyncWebParameter* p = request->getParam(i);
      if(p->isFile()){
        Serial.printf("_FILE[%s]: %s, size: %u\n", p->name().c_str(), p->value().c_str(), p->size());
      } else if(p->isPost()){
        Serial.printf("_POST[%s]: %s\n", p->name().c_str(), p->value().c_str());
      } else {
        Serial.printf("_GET[%s]: %s\n", p->name().c_str(), p->value().c_str());
      }
    }

    request->send(404);
  });
  server.onFileUpload([](AsyncWebServerRequest *request, const String& filename, size_t index, uint8_t *data, size_t len, bool final){
    if(!index)
      Serial.printf("UploadStart: %s\n", filename.c_str());
    Serial.printf("%s", (const char*)data);
    if(final)
      Serial.printf("UploadEnd: %s (%u)\n", filename.c_str(), index+len);
  });
  server.onRequestBody([](AsyncWebServerRequest *request, uint8_t *data, size_t len, size_t index, size_t total){
    if(!index)
      Serial.printf("BodyStart: %u\n", total);
    Serial.printf("%s", (const char*)data);
    if(index + len == total)
      Serial.printf("BodyEnd: %u\n", total);
  });
  server.begin();
}

void setup(){
  Serial.begin(230400);
  //Serial.setDebugOutput(true);

  setupCONNECTION();
  setupOTA();
  setupWEBSOCKET();
  setupWEBPAGE();   

  MDNS.addService("http","tcp",80);
}

#define bufferSize 8192

uint8_t buf[bufferSize];
uint16_t i=0;
char rc;

void loop(){
  ArduinoOTA.handle();
  ws.cleanupClients();

  Serial.println("I'm out");
  
  while (Serial.available() > 0) {
    rc = Serial.read(); // read char from UART
    buf[i] = rc;
    if(i<bufferSize-1) i++;
  
    if (rc == '\n') {    
      ws.binaryAll(buf, i);
      Serial.write(buf, i);
      i=0;
    }
    
  }
}

 

다음으로 웹페이지 인터페이스 부분입니다. 굳이 나누자면 위에 있는 펌웨어 코드는 백엔드이고 아래 코드는 프런트엔드가 되겠네요. 참고로 아래의 HTML 코드는 Wemos D1 mini에 Filesystem 이미지로 업로드되어야 합니다. 상세한 내용은 이전 포스팅인 https://kwonkyo.tistory.com/435를 참고해주세요.

<!DOCTYPE html>
<html>
<head>
<title>PHPoC Shield - Web Oscilloscope for Arduino</title>
<meta name="viewport" content="width=device-width, initial-scale=1">
<style>
body { text-align:center; background-color: #595959 ; color: white; display: flex; justify-content:space-between;}
.graph_container {display:inline-block;}
.setting_container {display:inline-block; float:right; background-color: #595959; padding: 0px; width: 260px;}
.setting {
    justify-content: center;
	width: 260px;
	padding: 3px;
	display: flex;
	justify-content:space-between;
}
.button {
    background-color: #999999;
    border: none;
    color: white;
    padding: 5px;
    text-align: center;
    display: inline-block;
	font-weight: bold;
	font-size: 115%;
}
.button2, .select {
    border: none;
    padding: 5px;
    text-align: center;
    display: inline-block;
	font-weight: bold;
	font-size: 115%;
}
.select {width: 100%; background-color: #F2F2F2; color: #4CAF50;}
.button_channel {border-radius: 50%; width: 14%;}
.button_trigger {border-radius: 4px; width: 60%;}
.button_go {border-radius: 4px; width: 35%;}
.button_mode {border-radius: 8px; width: 32%;}
.button_setting {border-radius: 45%; width: 33%;}
.input_text {width: 50%; background-color: #F2F2F2; color: #4CAF50;}
.label {width: 50%; background-color: transparent; color: #4CAF50;}
button:focus{
    outline: none;
}
#knob {
	padding: 5px;
}
</style>
<script>
var COLOR_BACKGROUND	= "#404040";
var COLOR_TEXT			= "#FFFFFF";
var COLOR_BOUND			= "#FFFFFF";
var COLOR_GRIDLINE		= "gray";
var COLOR_LINE = ["#33FFFF", "#FF00FF", "#FF0000", "#00FF00", "#FF8C00", "#666666"];
//var COLOR_LINE = ["#0000FF", "#FF0000", "#00FF00", "#FF8C00", "#00FF00"];
//var COLOR_LINE = ["#33FFFF", "#FF0000", "#00FF00", "#FF8C00", "#00FF00"];
//var COLOR_LINE = ["#0000FF", "#FF0000", "#009900", "#FF9900", "#CC00CC", "#666666", "#00CCFF", "#000000"];

var LEGEND_WIDTH = 10;
var X_AXIS_TITLE_HEIGHT	= 40;
var Y_AXIS_VALUE_WIDTH	= 100;
var PLOT_AREA_PADDING = 2;
var X_GRIDLINE_NUM = 5;
var Y_GRIDLINE_NUM = 7;

var offset_y = [5, 10, 15, 20, 25, 30];
var voltage_div = [5, 5, 5, 5, 5, 5];
var time_div_ms = 1000; // ms
var full_screen = false;

var WSP_WIDTH;
var WSP_HEIGHT;
var X_AXIS_MIN_MS = 0;
var X_AXIS_MAX_MS = time_div_ms * X_GRIDLINE_NUM;
var Y_AXIS_MIN = 0;
var Y_AXIS_MAX = 35;

var PLOT_AREA_WIDTH;
var PLOT_AREA_HEIGHT;
var PLOT_AREA_PIVOT_X;
var PLOT_AREA_PIVOT_Y;

var knob_center_x;
var knob_center_y;
var knob_radius = 60;
var knob_angle = 0;
var knob_last_angle = 0;
var knob_click_state = 0;
var knob_mouse_xyra = {x:0, y:0, r:0.0, a:0.0};
var MIN_TOUCH_RADIUS = 20;
var MAX_TOUCH_RADIUS = 60;

var ws;
var canvas;
var knob;
var ctx1;
var ctx2;

var buffer = "";
var live_data = [];
var live_time_us = [];
var trigger_data = [];
var trigger_time_us = [];

var BASE_TIME_MS;
var DISPLAY_LIVE = 0;
var DISPLAY_TRIGGER = 1;

var TRIGGER_FALLING	= 0;
var TRIGGER_RISING	= 1;
var TRIGGER_BOTH	= 2;

var TRIGGER_STATE_IDLE	= 0;
var TRIGGER_STATE_RUN	= 1;
var TRIGGER_STATE_TRIGGERED	= 2;

var mode = "AUTO"; // no trigger
var display_mode = DISPLAY_LIVE;
var selected_channel = 0;
var setting_option = "OFFSET";
var trigger_state = TRIGGER_STATE_IDLE;
var trigger_type = TRIGGER_BOTH;
var trigger_value = 1.5;
var previous_state_value = 0;
var stop_points = [];
var trigger_count = 0;
var time_diff_us = 0;
var arduino_last_time_us = 0;
var arduino_time_overflow_count = 0;

function init() {
	canvas = document.getElementById("graph");
	//canvas.style.backgroundColor = COLOR_BACKGROUND;
	canvas.addEventListener("click", function(event){
		full_screen = !full_screen;
		canvas_resize();
	});
	
	knob = document.getElementById("knob");
	knob.addEventListener("touchstart", mouse_down);
	knob.addEventListener("touchend", mouse_up);
	knob.addEventListener("touchmove", mouse_move);
	knob.addEventListener("mousedown", mouse_down);
	knob.addEventListener("mouseup", mouse_up);
	knob.addEventListener("mousemove", mouse_move);

	var button_channel = document.getElementsByClassName("button_channel");
	for(var i = 0; i < button_channel.length; i++)
		button_channel[i].addEventListener("click", function(event){
			var channelId = parseInt(event.target.id);
			if(channelId < live_data.length)
				selected_channel = channelId;
			event.target.style.backgroundColor = "white"; 
		});

	document.getElementById("trigger_type").addEventListener("change", function(event){ trigger_type = parseInt(event.target.value);});

	document.getElementById("GO").addEventListener("click", function(event){ 
			if((mode == "SINGLE" && (trigger_state == TRIGGER_STATE_IDLE ||(trigger_state == TRIGGER_STATE_TRIGGERED &&  display_mode == DISPLAY_TRIGGER))) || mode == "NORMAL" && trigger_state == TRIGGER_STATE_IDLE) {
				trigger_state = TRIGGER_STATE_RUN;
				stop_points.splice(0, stop_points.length);
				event.target.style.backgroundColor = "white";
				trigger_value = parseFloat(document.getElementById("trigger_value").value);
				var latest_value = live_data[selected_channel][live_data[selected_channel].length - 1];
				previous_state_value = (latest_value - trigger_value) < 0 ? 0 : 1;
			}
		});

	var button_mode = document.getElementsByClassName("button_mode");
	for(var i = 0; i < button_mode.length; i++)
		button_mode[i].addEventListener("click", function(event){
				if(mode != event.target.id) {
					trigger_state = TRIGGER_STATE_IDLE;
					trigger_count = 0;
				}
				mode = event.target.id; event.target.style.backgroundColor = "white";
			});

	var button_setting = document.getElementsByClassName("button_setting");
	for(var i = 0; i < button_setting.length; i++)
		button_setting[i].addEventListener("click", function(event){ setting_option = event.target.id; event.target.style.backgroundColor = "white"; });

	BASE_TIME_MS = (new Date()).getTime();

	canvas_resize();

	var ws_host_addr = location.hostname;

	if((navigator.platform.indexOf("Win") != -1) && (ws_host_addr.charAt(0) == "[")) {
		// network resource identifier to UNC path name conversion
		ws_host_addr = ws_host_addr.replace(/[\[\]]/g, '');
		ws_host_addr = ws_host_addr.replace(/:/g, "-");
		ws_host_addr += ".ipv6-literal.net";
	}

	ws = new WebSocket("ws://" + ws_host_addr + "/ws", "arduino");
	ws.onopen = ws_onopen;
	ws.onclose = ws_onclose;
	ws.onmessage = ws_onmessage;
	ws.binaryType = "arraybuffer";
}
function ws_onopen() {
	ws.send("wsm_baud=115200\r\n");
}
function ws_onclose() {
	alert("WebSocket was closed. Please reload page!");
	ws.onopen = null;
	ws.onclose = null;
	ws.onmessage = null;
	ws = null;
}
function ws_onmessage(e_msg) {
	e_msg = e_msg || window.event; // MessageEvent

	var u8view = new Uint8Array(e_msg.data);
	buffer += String.fromCharCode.apply(null, u8view);
	buffer = buffer.replace(/\r\n/g, "\n");
	buffer = buffer.replace(/\r/g, "\n");

	while(buffer.indexOf("\n") == 0)
		buffer = buffer.substr(1);

	if(buffer.lastIndexOf("\n") <= 0)
		return;

	var pos = buffer.lastIndexOf("\n");
	var str = buffer.substr(0, pos);
	var new_sample_arr = str.split("\n");
	buffer = buffer.substr(pos + 1);

	for(var si = 0; si < new_sample_arr.length; si++) {
		var str = new_sample_arr[si];
		var arr = [];

		if(str.indexOf("\t") > 0)
			arr = str.split("\t");
		else
			arr = str.split(" ");

		var time_us = parseInt(arr[0]);

		// to avoid arduino time overflow and reset to zero
		if(time_us < arduino_last_time_us) {
			arduino_time_overflow_count++;
		}

		arduino_last_time_us = time_us;
		time_us += arduino_time_overflow_count * 4294967295; // unsigned long max value

		var cur_time_ms = (new Date()).getTime() - BASE_TIME_MS;

		time_diff_us = cur_time_ms * 1000 - time_us;

		for(var id = 1; id < arr.length; id++) {
			var value = parseFloat(arr[id]);
			var i = id - 1;

			if(isNaN(value))
				continue;

			if(i >= live_data.length) {
				live_data.push([value]); // new channel
				live_time_us.push([time_us]);
			} else {
				live_data[i].push(value);
				live_time_us[i].push(time_us);
			}

			if(i == selected_channel)
				handle_trigger(value);
		}

		if(live_data.length > 0) {
			for(var i = 0; i < live_data.length; i++) {
				if(live_data[i].length > 1) {
					while((time_us - live_time_us[i][0]) > (X_AXIS_MAX_MS * 1000)) {
						live_data[i].splice(0, 1);
						live_time_us[i].splice(0, 1);
					}
				}
			}
		}
	}
}
function handle_trigger(value) {
	if(value != trigger_value) {
		var current_state_value = (value - trigger_value) < 0 ? 0 : 1;

		if(current_state_value != previous_state_value) {
			var is_triggered = false;

			if(trigger_type == TRIGGER_BOTH)
				is_triggered = true;
			else if(trigger_type == TRIGGER_RISING && previous_state_value == 0)
				is_triggered = true;
			else if(trigger_type == TRIGGER_FALLING && previous_state_value == 1)
				is_triggered = true;

			previous_state_value = current_state_value;

			if(is_triggered) {
				if(trigger_state != TRIGGER_STATE_IDLE && (mode == "NORMAL" || (mode == "SINGLE" && trigger_state == TRIGGER_STATE_RUN))) {
					var cur_time_ms = (new Date()).getTime() - BASE_TIME_MS;
					var stop_time_ms = Math.round(cur_time_ms + X_AXIS_MAX_MS / 2);
					trigger_state = TRIGGER_STATE_TRIGGERED;
					stop_points.push({copied:false, stop_time_ms: stop_time_ms}); // not stop immidiately 
				}
			}
		}
	}
}
function map(x, in_min, in_max, out_min, out_max) {
  return (x - in_min) * (out_max - out_min) / (in_max - in_min) + out_min;
}
function update_loop() {
	update_data();
	update_view();
}
function update_data() {
	var cur_time_ms = (new Date()).getTime() - BASE_TIME_MS;

	if(trigger_state == TRIGGER_STATE_TRIGGERED && stop_points.length > 0 && stop_points[0].stop_time_ms < cur_time_ms) {
		display_mode = DISPLAY_TRIGGER;

		if(!stop_points[0].copied) {
			trigger_data = JSON.parse(JSON.stringify(live_data)); // deep copy
			trigger_time_us = JSON.parse(JSON.stringify(live_time_us)); // deep copy
			stop_points[0].copied = true;
			trigger_count++;
		}

		if(stop_points.length > 1) {
			if(stop_points[1].stop_time_ms < cur_time_ms) {
				stop_points.splice(0, 1);
			}
		}
	} else {
		if(display_mode == DISPLAY_TRIGGER) {
			display_mode = DISPLAY_LIVE;
			trigger_data.splice(0, trigger_data.length);
			trigger_time_us.splice(0, trigger_time_us.length);
		}
	}
}
function update_view() {
	//ctx1.font = "16px Arial";
	ctx1.textBaseline = "middle";

	ctx1.clearRect(0, 0, WSP_WIDTH, WSP_HEIGHT);
	ctx1.save();

	ctx1.translate(PLOT_AREA_PIVOT_X, PLOT_AREA_PIVOT_Y);
	ctx1.fillStyle = "black";
	ctx1.fillRect(0, -PLOT_AREA_HEIGHT, PLOT_AREA_WIDTH, PLOT_AREA_HEIGHT);

	var time_div_text = time_div_ms.toFixed(2) + " ms / div";
	ctx1.fillStyle = COLOR_TEXT;
	ctx1.textAlign = "center";
	ctx1.fillText(time_div_text, PLOT_AREA_WIDTH / 2, X_AXIS_TITLE_HEIGHT / 2);

	draw_gridline();
	draw_graph();
	ctx1.restore();

	draw_knob(knob_radius, knob_angle);
	update_button();
}
function draw_gridline() {
	ctx1.strokeStyle = COLOR_GRIDLINE;
	ctx1.lineWidth = 1;

	for(var i = 0; i <= Y_GRIDLINE_NUM; i++) {
		var y_gridline_px = -map(i, 0, Y_GRIDLINE_NUM, 0, (PLOT_AREA_HEIGHT - 2 * PLOT_AREA_PADDING));
		y_gridline_px = Math.round(y_gridline_px) - PLOT_AREA_PADDING + 0.5;
		ctx1.beginPath();
		ctx1.moveTo(0, y_gridline_px);
		ctx1.lineTo(PLOT_AREA_WIDTH, y_gridline_px);
		ctx1.stroke();
	}

	for(var i = 0; i <= X_GRIDLINE_NUM; i++) {
		var x_gridline_px = map(i, 0, X_GRIDLINE_NUM, 0, PLOT_AREA_WIDTH - 2 * PLOT_AREA_PADDING);
		x_gridline_px = Math.round(x_gridline_px) + PLOT_AREA_PADDING + 0.5;
		ctx1.beginPath();
		ctx1.moveTo(x_gridline_px, 0);
		ctx1.lineTo(x_gridline_px, -PLOT_AREA_HEIGHT);
		ctx1.stroke();
	}
}
function draw_graph() {
	var data_display;
	var time_display;

	if(display_mode == DISPLAY_LIVE) {
		data_display = live_data;
		time_display = live_time_us;
	} else {
		data_display = trigger_data;
		time_display = trigger_time_us;
	}

	var line_num = data_display.length;
	
	if(!line_num)
		return;

	var root_time_us;

	if(display_mode == DISPLAY_LIVE)
		root_time_us = ((new Date()).getTime() - BASE_TIME_MS) * 1000;
	else
		root_time_us = time_display[0][time_display[0].length - 1] + time_diff_us;

	ctx1.textAlign = "left";
	ctx1.lineWidth = 2;

	for(var channel = 0; channel < line_num; channel++) {
		var sample_num = data_display[channel].length;

		if(sample_num >= 2) {
			var offset = -map(offset_y[channel], Y_AXIS_MIN, Y_AXIS_MAX, 0, PLOT_AREA_HEIGHT);
			ctx1.strokeStyle = COLOR_LINE[channel];
			ctx1.fillStyle = COLOR_LINE[channel];
			ctx1.fillText("CH" + channel + " " + voltage_div[channel].toFixed(1) + " v / div", -Y_AXIS_VALUE_WIDTH, offset);

			ctx1.beginPath();

			for(var i = sample_num - 1; i >= 0; i--) {
				var sample_time_us = time_display[channel][i] + time_diff_us;
				var sample_value = data_display[channel][i];

				x_value = sample_time_us - (root_time_us - (X_AXIS_MAX_MS * 1000));

				if(x_value > 0) {
					y_value = map(sample_value, 0, 5, 0, voltage_div[channel]);
					y_value += offset_y[channel];

					x_px = map(x_value, X_AXIS_MIN_MS * 1000, X_AXIS_MAX_MS * 1000, 0, PLOT_AREA_WIDTH);
					y_px = -map(y_value, Y_AXIS_MIN, Y_AXIS_MAX, 0, PLOT_AREA_HEIGHT) - PLOT_AREA_PADDING;

					if(i == sample_num - 1)
						ctx1.moveTo(x_px, y_px);
					else
						ctx1.lineTo(x_px, y_px);
				}
			}

			ctx1.stroke();
		}
	}
}
function draw_knob(radius, angle) {
	ctx2.save();

	//ctx2.shadowBlur = 2;
	//ctx2.shadowColor = "LightGray";
	ctx2.translate(knob_center_x, knob_center_y);
	ctx2.rotate(-angle * Math.PI / 180);
	var grd = ctx2.createLinearGradient(-radius, 0, radius, 0);
	grd.addColorStop(0, "#DC143C");
	grd.addColorStop(0.1, "#DC143C");
	grd.addColorStop(0.30, "#F5D6D6");
	grd.addColorStop(0.47, "#DC143C");
	grd.addColorStop(0.53, "#DC143C");
	grd.addColorStop(0.70, "#F5D6D6");
	grd.addColorStop(0.9, "#DC143C");
	grd.addColorStop(1, "#DC143C");
	ctx2.fillStyle = grd;
	ctx2.beginPath();
	ctx2.arc(0, 0, radius, 0, 2 * Math.PI);
	ctx2.fill();
	ctx2.fillStyle = "#DC143C";
	ctx2.beginPath();
	ctx2.arc(0, 0, radius / 4, 0, 2 * Math.PI);
	ctx2.fill();
	ctx2.restore();
}
function update_button() {
	var button_channel = document.getElementsByClassName("button_channel");
	for(var i = 0; i < button_channel.length; i++) {
		if(button_channel[i].id == selected_channel) {
			button_channel[i].style.backgroundColor = COLOR_LINE[i];
			button_channel[i].style.border = "3px solid " + COLOR_LINE[i];
		} else {
			button_channel[i].style.backgroundColor = "#999999";

			if(i < live_data.length) {
				button_channel[i].style.border = "3px solid " + COLOR_LINE[i];
			}
			else {
				button_channel[i].style.border = "none";
			}
		}
	}

	if((mode == "SINGLE" && (trigger_state == TRIGGER_STATE_IDLE ||(trigger_state == TRIGGER_STATE_TRIGGERED &&  display_mode == DISPLAY_TRIGGER))) || mode == "NORMAL" && trigger_state == TRIGGER_STATE_IDLE)
		document.getElementById("GO").style.backgroundColor = "#4CAF50";
	else
		document.getElementById("GO").style.backgroundColor = "#999999";

	if(trigger_state != TRIGGER_STATE_TRIGGERED) {
		document.getElementById("TRIGGER").style.backgroundColor = "#999999";
	} else {
		document.getElementById("TRIGGER").style.backgroundColor = "#4CAF50";
	}
	document.getElementById("TRIGGER").innerHTML = "TRIGGER (" + trigger_count + ")";

	var button_mode = document.getElementsByClassName("button_mode");
	for(var i = 0; i < button_mode.length; i++) {
		if(button_mode[i].id == mode)
			button_mode[i].style.backgroundColor = "#4CAF50";
		else
			button_mode[i].style.backgroundColor = "#999999";
	}

	var button_setting = document.getElementsByClassName("button_setting");
	for(var i = 0; i < button_setting.length; i++) {
		if(button_setting[i].id == setting_option)
			button_setting[i].style.backgroundColor = "#DC143C";
		else
			button_setting[i].style.backgroundColor = "#999999";
	}
}
function check_update_xyra(event, knob_mouse_xyra) {
	var x, y, r, a;

	if(event.touches) {
		var touches = event.touches;

		x = (touches[0].pageX - touches[0].target.offsetLeft) - knob_center_x;
		y = knob_center_y - (touches[0].pageY - touches[0].target.offsetTop);
	} else {
		x = event.offsetX - knob_center_x;
		y = knob_center_y - event.offsetY;
	}

	/* cartesian to polar coordinate conversion */
	r = Math.sqrt(x * x + y * y);
	a = Math.atan2(y, x);

	//knob_mouse_xyra = {x:x, y:y, r:r, a:a};
	knob_mouse_xyra.x = x;
	knob_mouse_xyra.y = y;
	knob_mouse_xyra.r = r;
	knob_mouse_xyra.a = a;

	if((r >= MIN_TOUCH_RADIUS) && (r <= MAX_TOUCH_RADIUS))
		return true;
	else
		return false;
}
function mouse_down() {
	event.preventDefault();
	if(event.touches && (event.touches.length > 1))
		knob_click_state = event.touches.length;

	if(knob_click_state > 1)
		return;

	if(check_update_xyra(event, knob_mouse_xyra)) {
		knob_click_state = 1;
		knob_last_angle = knob_mouse_xyra.a / Math.PI * 180.0;
	}
}
function mouse_up() {
	event.preventDefault();
	knob_click_state = 0;
}
function mouse_move() {
	event.preventDefault();
	var angle_pos, angle_offset;

	if(event.touches && (event.touches.length > 1))
		knob_click_state = event.touches.length;

	if(knob_click_state != 1)
		return;

	if(!check_update_xyra(event, knob_mouse_xyra)) {
		knob_click_state = 0;
		return;
	}

	angle_pos = knob_mouse_xyra.a / Math.PI * 180.0;

	if(angle_pos < 0.0)
		angle_pos = angle_pos + 360.0;

	angle_offset = angle_pos - knob_last_angle;
	knob_last_angle = angle_pos;

	if(angle_offset > 180.0)
		angle_offset = -360.0 + angle_offset;
	else if(angle_offset < -180.0)
		angle_offset = 360 + angle_offset;

	knob_angle += angle_offset;

	if(setting_option == "TIME") {
		time_div_ms -= Math.round(angle_offset / 90 * time_div_ms);
		X_AXIS_MAX_MS = time_div_ms * X_GRIDLINE_NUM;
	} else if(setting_option == "VOLTAGE") {
		voltage_div[selected_channel] -= angle_offset / 90 * voltage_div[selected_channel];
	} else if(setting_option == "OFFSET") {
		offset_y[selected_channel] -= angle_offset / 90 * 5;
	}
}
function canvas_resize() {
	canvas.width = 0; // to avoid wrong screen size
	canvas.height = 0;

	var width = window.innerWidth - 20;
	var height = window.innerHeight - 20;

	if(full_screen) {
		document.getElementById("setting_container").style.display = "none";
		WSP_WIDTH = width;
		WSP_HEIGHT = height;
	} else {
		if(height >= 1.5 * width) {
			document.getElementById("setting_container").style.display = "block";
			WSP_WIDTH = width;
		} else {
			document.getElementById("setting_container").style.display = "inline-block";
			WSP_WIDTH = width - document.getElementById('setting_container').offsetWidth;
			WSP_HEIGHT = height;
		}
	}

	canvas.width = WSP_WIDTH;
	canvas.height = WSP_HEIGHT;

	PLOT_AREA_WIDTH		= WSP_WIDTH - Y_AXIS_VALUE_WIDTH;
	PLOT_AREA_HEIGHT	= WSP_HEIGHT - X_AXIS_TITLE_HEIGHT;
	PLOT_AREA_PIVOT_X	= Y_AXIS_VALUE_WIDTH;
	PLOT_AREA_PIVOT_Y	= PLOT_AREA_HEIGHT;

	knob.width = 250 - 8 * 2;
	knob.height = 150 - 8 * 2;

	knob_center_x = Math.round(knob.width / 2 );
	knob_center_y = Math.round(knob.height / 2);

	ctx1 = canvas.getContext("2d");
	ctx2 = knob.getContext("2d");

	ctx1.font = "14px Arial";
}

setInterval(update_loop, 1000 / 60);
window.onload = init;
</script>
</head>
<body onresize="canvas_resize()">

<div class="graph_container" id="graph_container">
	<canvas id="graph"></canvas>
</div>

<div class="setting_container" id="setting_container">
	<div class="setting" id="setting_channel">
	<button class="button button_channel" id="0">0</button>
	<button class="button button_channel" id="1">1</button>
	<button class="button button_channel" id="2">2</button>
	<button class="button button_channel" id="3">3</button>
	<button class="button button_channel" id="4">4</button>
	<button class="button button_channel" id="5">5</button>
	</div>

	<div class="setting">
		<input type="text" class="button2 label" value="Trigger Value: " disabled>
		<input type="text" class="button2 input_text" id="trigger_value" value="1.5">
	</div>

	<div class="setting">
	<select class="select" id="trigger_type">
		<option value="0">Falling</option>
		<option value="1">Rising</option>
		<option value="2" selected>Falling & Rising</option>
	</select>
	</div>

	<div class="setting" id="setting_trigger">
	<button class="button button_trigger" id="TRIGGER" disabled>TRIGGER</button>
	<button class="button button_go" id="GO">GO</button>
	</div>

	<div class="setting" id="setting_mode">
	<button class="button button_mode" id="AUTO">AUTO</button>
	<button class="button button_mode" id="NORMAL">NORMAL</button>
	<button class="button button_mode" id="SINGLE">SINGLE</button>
	</div>

	<canvas id="knob"></canvas>

	<div class="setting" id="setting_option">
	<button class="button button_setting" id="TIME">ms / div</button><br>
	<button class="button button_setting" id="VOLTAGE">V / div</button><br>
	<button class="button button_setting" id="OFFSET">Offset</button><br>
	</div>
</div>
</body>
</html>

 

하드웨어와 소프트웨어, 모든 게 준비가 되었다면 이제 테스트를 해 보겠습니다.

딱히 테스트할만한 거리가 없어서 아두이노 나노를 이용해서 3개 채널에 주기가 다른 펄스 신호가 나오게 스케치를 올리고 지금까지 만든 오실로스코프에 연결을 해 봤습니다. 웹 인터페이스에는 총 6개 채널을 동시에 읽을 수 있도록 되어 있습니다만, 아무것도 연결되지 않은 채널은 주변 포트의 신호에 영향을 받아서 의도하지 않은 그래프가 나타나기도 합니다. 그래서 1, 3, 5번 채널에  신호선을 물리고 나머지 2, 4, 6은 GND에 물렸습니다.

그리고 Wemos D1 mini와 동일한 공유기에 연결되어 있는 컴퓨터에서 웹페이지 접속을 해보면 위의 오른쪽 그림과 같이 좀 어설프긴 하지만 각각의 주기가 다른 신호를 확인할 수 있습니다.

그리고 이 프로젝트가 특별한 이유는 바로 오실로스코프의 UI가 여느 공개 프로젝트와 다르게 화려하다는 겁니다. 기능적으로는 여러 채널의 신호를 한 화면에서 확인할 수 있고 그리고 각 신호들에 대해서 화면 전시 속도, 민감도, 오프셋 등을 손쉽게 조절할 수 있습니다.

뿐만 아니라 기준 전압을 설정하면 Falling과 Rising을 감지해서 멈추게 하거나 감지된 횟수를 카운팅도 할 수 있습니다. 무엇보다 웹페이지를 통해서 결과를 확인하기 때문에 굳이 컴퓨터가 없더라도 이렇게 스마트폰으로 무선으로 접속해서 결과를 확인할 수 있기 때문에 사용이 더 편리합니다. 센서 신호의 유무나 아두이노 등의 하드웨어 레벨에서 간단한 디버깅 용으로는 활용해 볼 수 있을 것 같습니다. (사이즈가 태블릿에서 사용하기에 딱입니다. 원래는 아이패드에서도 접속이 잘 됐었는데 iOS15로 업데이트한 다음부터인지 요즘은 아이패드에서는 접속이 안되고 있습니다.)

물론 단점도 있습니다. 신호의 raw값이 전달되는 게 아니라 아두이노가 신호 값을 텍스트로 변환하고 변환된 값을 시리얼 통신으로 wemos D1 mini 모듈로 전달하고 그리고 다시 한번 전달된 신호를 텍스트로 변환하고 변환된 텍스트를 웹페이지에서 파싱 해서 화면에 표현을 하는 다소 복잡한 과정을 거치게 됩니다. 그러다 보니 속도에 한계가 있어서 정교한 신호 분석을 하기에는 무리가 있습니다.

문제 해결

그런데 최종 결과를 확인하기 전까지 우여곡절이 있었습니다. 아래는 이 프로젝트를 구현해 보면서 겪었던 간단한 오류에 대한 기록입니다. 처음에는 신호를 나타내는 그래프가 계속 이어지지 않고 주기적으로 끊어지는 모습을 보였습니다. 그래프가 멈추는 순간에는 wemos 모듈이 리셋되는 현상이 나타났습니다. wemos를 USB 케이블로 PC와 연결하고 시리얼 모니터를 통해서 통과하는 데이터를 확인해 봤는데요. 아래와 같이 중간중간 포맷이 깨지는 듯한 모습도 보이고 일정 시간이 지나면 soft watchdog이 발동하는 걸 확인할 수 있습니다. 그래서 wemos에서 처리하기에 부담스러울 정도로 데이터 양이 많아서 그런가 싶어서 데이터를 발생시키는 간격을 약간 조정해 봤습니다. 이때 아두이노와 wemos 간의 통신속도는 baudrate 115200였고 신호 간 50㎲의 간격을 인위적으로 넣어주었을 때 결과였습니다.

baudrate 115200, delay 50㎲

wemos 모듈의 펌웨어는 시리얼로 들어온 데이터가 완전히 처리될 때까지 while 반복문을 돌게 되고 버퍼에 처리할 데이터가 더 이상 없을 때 반복문을 탈출하고 "I'm out" 메시지를 출력하도록 되어있습니다. 그런데 위에서는 반복문을 탈출하지 못해서 ESP8266 칩셋이 무한루프에 빠진 것 같은 효과가 나타나는 것 같습니다. 잘 이해는 되지 않지만 시리얼 버퍼를 처리하는 속도보다 쌓이는 속도가 더 빠르다는 건데 그래서 간격을 조금 더 늘려 봤습니다.

Baudrate 115200, delay 150㎲

위 결과는 데이터 간에 150㎲의 간격을 임의적으로 넣었을 정상 동작하는 모습입니다. 약 10개 데이터가 모였다가 처리되는듯한 모습으로 보이네요.

이번에는 시리얼 통신 속도를 조금 더 빠르게 고쳐봤습니다. Baudrate를 230400으로 늘리고 딜레이는 따로 주지 않았습니다. 

이번에는 딜레이가 없는데도 불구하고 데이터가 들어오는 속도보다 처리가 훨씬 빠른 것처럼 보이죠?! 시리얼 통신이라는 방법적인 면에서 속도에 손해가 많이 있는 것 같네요.

아래 코드는 위에 있는 오실로 스코프의 웹페이지와 마찬가지로 wemos 파일 시스템에서 동작하는 웹소켓 모니터입니다. 이 코드를 추가로 업로드하고 웹브라우저로 접속을 해보면 최종적으로 웹브라우저에 도달하는 데이터의 텍스트 값을 그대로 확인할 수 있습니다.

<!--
  FSWebServer - Example Index Page
  Copyright (c) 2015 Hristo Gochkov. All rights reserved.
  This file is part of the ESP8266WebServer library for Arduino environment.

  This library is free software; you can redistribute it and/or
  modify it under the terms of the GNU Lesser General Public
  License as published by the Free Software Foundation; either
  version 2.1 of the License, or (at your option) any later version.
  This library is distributed in the hope that it will be useful,
  but WITHOUT ANY WARRANTY; without even the implied warranty of
  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
  Lesser General Public License for more details.
  You should have received a copy of the GNU Lesser General Public
  License along with this library; if not, write to the Free Software
  Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA
-->
<!DOCTYPE html>
<html>
  <head>
    <meta http-equiv="Content-type" content="text/html; charset=utf-8">
    <title>WebSocketTester</title>
    <style type="text/css" media="screen">
    body {
      margin:0;
      padding:0;
      background-color: black;
    }

    #dbg, #input_div, #input_el {
      font-family: monaco;
      font-size: 12px;
      line-height: 13px;
      color: #AAA;
    }

    #dbg, #input_div {
      margin:0;
      padding:0;
      padding-left:4px;
    }

    #input_el {
      width:98%;
      background-color: rgba(0,0,0,0);
      border: 0px;
    }
    #input_el:focus {
      outline: none;
    }
    </style>
    <script type="text/javascript">
    var ws = null;
    function ge(s){ return document.getElementById(s);}
    function ce(s){ return document.createElement(s);}
    function stb(){ window.scrollTo(0, document.body.scrollHeight || document.documentElement.scrollHeight); }
    function sendBlob(str){
      var buf = new Uint8Array(str.length);
      for (var i = 0; i < str.length; ++i) buf[i] = str.charCodeAt(i);
      ws.send(buf);
    }
    function addMessage(m){
      var msg = ce("div");
      msg.innerText = m;
      ge("dbg").appendChild(msg);
      stb();
    }
    function startSocket(){
      ws = new WebSocket('ws://'+document.location.host+'/ws',['arduino']);
      ws.binaryType = "arraybuffer";
      ws.onopen = function(e){
        addMessage("Connected");
      };
      ws.onclose = function(e){
        addMessage("Disconnected");
      };
      ws.onerror = function(e){
        console.log("ws error", e);
        addMessage("Error");
      };
      ws.onmessage = function(e){
        var msg = "";
        if(e.data instanceof ArrayBuffer){
          msg = "BIN:";
          var bytes = new Uint8Array(e.data);
          for (var i = 0; i < bytes.length; i++) {
            msg += String.fromCharCode(bytes[i]);
          }
        } else {
          msg = "TXT:"+e.data;
        }
        addMessage(msg);
      };
      ge("input_el").onkeydown = function(e){
        stb();
        if(e.keyCode == 13 && ge("input_el").value != ""){
          ws.send(ge("input_el").value);
          ge("input_el").value = "";
        }
      }
    }
    function startEvents(){
      var es = new EventSource('/events');
      es.onopen = function(e) {
        addMessage("Events Opened");
      };
      es.onerror = function(e) {
        if (e.target.readyState != EventSource.OPEN) {
          addMessage("Events Closed");
        }
      };
      es.onmessage = function(e) {
        addMessage("Event: " + e.data);
      };
      es.addEventListener('ota', function(e) {
        addMessage("Event[ota]: " + e.data);
      }, false);
    }
    function onBodyLoad(){
      startSocket();
      startEvents();
    }
    </script>
  </head>
  <body id="body" onload="onBodyLoad()">
    <pre id="dbg"></pre>
    <div id="input_div">
      $<input type="text" value="" id="input_el">
    </div>
  </body>
</html>

 

최종 목적지까지 데이터가 도망가지 않고 잘 들어오는 것 같습니다.

이상 아두이노와 ESP8266 모듈을 활용한 오실로스코프 프로젝트 리뷰였습니다.

 

끝!

반응형

댓글