Demonstration



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
  • Adjusting voltage division for each channel via a web knob
  • Adjusting display offset for each channel via a web knob
  • Tap or Click to show/hide setting area
  • Work on cross platform: Window, Linux, iOS, Android... (because of characteristic of web), which has a web browser.
Things used in this project
  • Arduino Uno or Mega
  • PHPoC Shield or PHPoC WiFi Shield
  • Jump wire

User Interface





How It Works


Because Web Oscilloscope is modification of Web Serial Monitor on PHPoC, I will explain how Web Serial Monitor works first.

PHPoC shield has a built-in embedded web app, called "Web Serial Monitor". It's similar to Serial Monitor on Arduino IDE. The difference between Serial Monitor on Arduino IDE is that:
  • Serial Monitor on Arduino IDE: Reading data from arduino tx pin via USB cable.
  • Web Serial Monitor on PHPoC Shield: Reading data from arduino tx pin via Internet

In detail, PHPoC Shield communicate with Arduino via SPI. When user access Web Serial Monitor from a web browser. After page is loaded, the page make a WebSocket connection to PHPoC Shield. At this time, PHPoC Shield is stacked on Arduino, it it capture data from Arduino TX pin, and then send this data to Web Serial Monitor on web browser via WebSocket. Web Serial receives the data and display on web.

This lets user not only see the data on PC but also see data on the mobile or any devices that support a web browser

Now let's see how Web Oscilloscope works.

Web Oscilloscope works the same as Web Serial Monitor, except for the last part.

In stead of displaying raw data on web, Web Oscilloscope visualize the data on the grapth. Other functions are added, which allow user adjust the UI and perform trigger.

About data, which is sent from Arduino

Arduino read data from ADC pin (from A0 to A5) and read timestamp (in microseconds)

ADC data is converted to voltage and print to serial port along with timestamp according to the following format:
  • Firstly, timestamp, follow by a blank or "\t" character.
  • And then, followed by voltage data. data of each channel is separated by a blank or "\t" character.
  • The end is newline character

Note that: maximum channel is 6. But you can also send only data of one, two only three channel.


Source Code


Souce code includes two parts:
  • Arduino Code (see the WebOscilloscope.ino)
  • Web app code (oscilloscope.php): this is user interface code. It needs to be uploaded to PHPoC Shield according to this instruction

WebOscilloscope.ino
Code:
#include <SPI.h>
#include <Phpoc.h>

#define AREF 5.0
#define ADC_MAX 1023.0


float ratio = AREF / ADC_MAX;

void setup() {

  Serial.begin(115200);

  Phpoc.begin();
}

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);
}
oscilloscope.php
PHP Code:
<!DOCTYPE html>
<html>
<head>
<title>PHPoC Shield - Web Oscilloscope for Arduino</title>
<meta name="viewport" content="width=device-width, initial-scale=0.7">
<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: 2px;
    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;
}
.button2, .select {
    border: none;
    padding: 5px;
    text-align: center;
    display: inline-block;
}
.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 = "<?echo _SERVER("HTTP_HOST")?>";

    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 + "/serial_monitor", "uint8.phpoc");
    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>


How to Use




Access Web Oscilloscope from web browser on your PC or smart phone by typing: http://replace_ip_address/oscilloscope.php

How to measure signal: Just need uses two wire, one to GND and one to any Analog pin of Arduino (A0 to A5). Connect these two pins to the point we want to check the signal.

Since there is 6 channel, we need 6 ground wires for convenient, However Arduino has only two GND pin. I do as below to extend more GND pins



wiring