Monitor your home remotely via web using Arduino Uno and pan-tilt Grove camera.


Demonstration





How it works

The following step shows data flow between Web Browser and Arduino:
  • User access webpage on PHPoC Shield for Arduino via Web Browser.
  • WebSocket connection is created between Arduino and Web Browser.
  • Arduino reads data (image) from grove serial camera
  • Arduino send the data (image) to Web Browser via websocket
  • Web browser display data (image) on webpage.
  • Since image is updated continuously. It looks like a live video.


Things we used in this project
  • Arduino UNO & Genuino UNO × 1
  • PHPoC WiFi Shield 2 for Arduino × 1
  • Grove Expansion Board × 1
  • Grove - Serial Camera Kit × 1
  • SG90 Micro-servo motor × 2


Assembly
  • stack PHPoC Shield on Arduino
  • stack grove expansion board on PHPoC Shield
  • connect grove camera to UART port of Grove expansion board
Source Code

There are two code parts: Arduino Code and Web User Interface code.

Arduino Code

Code:
#include <Phpoc.h>
#include <Servo.h>

#define PIC_PKT_LEN    256 //data length of each read, dont set this too big because ram is limited
#define CAM_ADDR       0

const byte cameraAddr = (CAM_ADDR << 5); // address
unsigned char pkt[PIC_PKT_LEN];
unsigned int pktCnt;
unsigned int lastPacketLength;

PhpocServer server(80);
PhpocClient client;

Servo servo_x;
Servo servo_y;

int current_angle_x;
int current_angle_y;

void setup(){
    Serial.begin(115200);

    Phpoc.begin(PF_LOG_SPI | PF_LOG_NET);
    server.beginWebSocketBinary("grove_camera", "uint8.phpoc");

    servo_x.attach(8);  // attaches the servo on pin 8 to the servo object
    servo_y.attach(9);  // attaches the servo on pin 9 to the servo object

    servo_x.write(90);
    servo_y.write(90);

    cameraInit();

    client = server.available();
}

void loop(){
    if(client && client.connected()){
        cameraGetPicture();
        for(unsigned int i = 0; i <= pktCnt; i++){
            getPacket(i);
            joystickLoop();
        }
    } else {
        client = server.available();
    }
}

void joystickLoop(){
    String data = "";

    while(1) {
        String data = client.readLine();

        if(data.length()){
            int pos = data.indexOf(':');
            long x = data.substring(0, pos).toInt();
            long y = data.substring(pos+1).toInt();

            // scale  from [-100; 100] to [0; 180]
            long angle_x = map(x, -100, 100, 0, 180);
            long angle_y = map(y, -100, 100, 0, 180);

            servo_x.write(angle_x);
            servo_y.write(angle_y);
        }
        else
            break;
    }
}

void clearRxBuf(){
    while (Serial.available()){
        Serial.read();
    }
}

void sendCmd(char cmd[], int cmd_len){
    for (char i = 0; i < cmd_len; i++) Serial.print(cmd[i]);
}

void cameraInit(){
    char cmd[] = {0xaa,0x0d|cameraAddr,0x00,0x00,0x00,0x00} ;
    unsigned char resp[6];

    Serial.setTimeout(500);
    while (1){
        sendCmd(cmd,6);

        if(Serial.readBytes((char *)resp, 6) != 6)
            continue;

        if(resp[0] == 0xaa && resp[1] == (0x0e | cameraAddr) && resp[2] == 0x0d && resp[4] == 0 && resp[5] == 0) {
            if(Serial.readBytes((char *)resp, 6) != 6) continue;
            if(resp[0] == 0xaa && resp[1] == (0x0d | cameraAddr) && resp[2] == 0 && resp[3] == 0 && resp[4] == 0 && resp[5] == 0) break;
        }
    }

    cmd[1] = 0x0e | cameraAddr;
    cmd[2] = 0x0d;
    sendCmd(cmd, 6);

    char cmd2[] = { 0xaa, 0x01 | cameraAddr, 0x00, 0x07, 0x03, 0x05};

    Serial.setTimeout(100);

    while (1){
        clearRxBuf();
        sendCmd(cmd2, 6);
        if(Serial.readBytes((char *)resp, 6) != 6) continue;
        if(resp[0] == 0xaa && resp[1] == (0x0e | cameraAddr) && resp[2] == 0x01 && resp[4] == 0 && resp[5] == 0) break;
    }

    char cmd3[] = { 0xaa, 0x06 | cameraAddr, 0x08, PIC_PKT_LEN & 0xff, (PIC_PKT_LEN>>8) & 0xff ,0};

    while (1) {
        clearRxBuf();
        sendCmd(cmd3, 6);
        if(Serial.readBytes((char *)resp, 6) != 6) continue;
        if(resp[0] == 0xaa && resp[1] == (0x0e | cameraAddr) && resp[2] == 0x06 && resp[4] == 0 && resp[5] == 0) break;
    }

    //Serial.println("Camera initialization done.");
}

void cameraGetPicture(){
    char cmd[] = { 0xaa, 0x04 | cameraAddr, 0x01, 0x00, 0x00, 0x00 };
    unsigned char resp[6];
    unsigned long picTotalLen = 0; // picture length

    while (1){
        clearRxBuf();
        sendCmd(cmd, 6);

        if(Serial.readBytes((char *)resp, 6) != 6) continue;
        if(resp[0] == 0xaa && resp[1] == (0x0e | cameraAddr) && resp[2] == 0x04 && resp[4] == 0 && resp[5] == 0){
            Serial.setTimeout(1000);
            if(Serial.readBytes((char *)resp, 6) != 6)
                continue;

            if(resp[0] == 0xaa && resp[1] == (0x0a | cameraAddr) && resp[2] == 0x01){
                picTotalLen = (resp[3]) | (resp[4] << 8) | (resp[5] << 16);
                break;
            }
        }
    }

    pktCnt = (picTotalLen) / (PIC_PKT_LEN - 6);
    lastPacketLength = PIC_PKT_LEN;

    if((picTotalLen % (PIC_PKT_LEN-6)) != 0){
        pktCnt += 1;
        lastPacketLength = picTotalLen % (PIC_PKT_LEN - 6) + 6;
    }
}
void getPacket(unsigned int i){
    char cmd[] = { 0xaa, 0x0e | cameraAddr, 0x00, 0x00, 0x00, 0x00 };

    Serial.setTimeout(100);

    if(i < pktCnt) {
        cmd[4] = i & 0xff;
        cmd[5] = (i >> 8) & 0xff;

        clearRxBuf();
        sendCmd(cmd, 6);

        int pkt_len;

        if(i < (pktCnt - 1))
            pkt_len = PIC_PKT_LEN ;
        else
            pkt_len = lastPacketLength;

        uint16_t cnt = Serial.readBytes((char *)pkt, pkt_len);

        while(client.availableForWrite() < (cnt - 6)) // check availability of websocket tx buffer.
            ;

        client.write((const uint8_t *)&pkt[4], cnt - 6);
    } else {
        cmd[4] = 0xf0;
        cmd[5] = 0xf0;
        sendCmd(cmd, 6);
    }
}


Compile and upload code to Arduino via Arduino IDE

Web User Interface

PHP Code:
<!DOCTYPE html>
<html>
<head>
<title>Arduino - PHPoC Shield</title>
<meta name="viewport" content="width=device-width, initial-scale=0.7, maximum-scale=0.7">
<style>
body { text-align: center; font-size: 15pt; }
h1 { font-weight: bold; font-size: 20pt; }
h2 { font-weight: bold; font-size: 15pt; }
button { font-weight: bold; font-size: 15pt; }
</style>
<script>
/* Camera variable */
var image = new Uint8Array(0);

/* Joystick variable */
var canvas_width = 500, canvas_height = 500;
var radius_base = 150;
var radius_handle = 72;
var radius_shaft = 120;
var range = canvas_width/2 - 10;
var step = 18;
var joystick = {x:0, y:0, x_percent: 0, y_percent: 0};
var click_state = 0;

var ws;

function init()
{
    /* Camera setting */
    var camera = document.getElementById("camera");
    camera.width = "320";
    camera.height = "240";

    /* Joystick setting */
    var ratio = 320.0 / canvas_width; // 320 is image's width

    canvas_width = Math.round(canvas_width * ratio);
    canvas_height = Math.round(canvas_height * ratio);
    radius_base = Math.round(radius_base * ratio);
    radius_handle = Math.round(radius_handle * ratio);
    radius_shaft = Math.round(radius_shaft * ratio);
    range = Math.round(range * ratio);
    step = Math.round(step * ratio);

    var canvas = document.getElementById("joystick");
    canvas.width = canvas_width;
    canvas.height = canvas_height;

    canvas.addEventListener("touchstart", mouse_down);
    canvas.addEventListener("touchend", mouse_up);
    canvas.addEventListener("touchmove", mouse_move);
    canvas.addEventListener("mousedown", mouse_down);
    canvas.addEventListener("mouseup", mouse_up);
    canvas.addEventListener("mousemove", mouse_move);

    var ctx = canvas.getContext("2d");
    ctx.translate(canvas_width/2, canvas_height/2);
    ctx.shadowBlur = 20;
    ctx.shadowColor = "LightGray";
    ctx.lineCap="round";
    ctx.lineJoin="round";

    update_view();
}
function connect_onclick()
{
    if(ws == null)
    {
        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 + "/grove_camera", "uint8.phpoc");
        document.getElementById("ws_state").innerHTML = "CONNECTING";
        ws.onopen = ws_onopen;
        ws.onclose = ws_onclose;
        ws.onmessage = ws_onmessage;
        ws.binaryType = "arraybuffer";
    }
    else
        ws.close();
}
function ws_onopen()
{
    document.getElementById("ws_state").innerHTML = "<font color='blue'>CONNECTED</font>";
    document.getElementById("bt_connect").innerHTML = "Disconnect";
    ws.send("0:0\r\n");
}
function ws_onclose()
{
    document.getElementById("ws_state").innerHTML = "<font color='gray'>CLOSED</font>";
    document.getElementById("bt_connect").innerHTML = "Connect";
    ws.onopen = null;
    ws.onclose = null;
    ws.onmessage = null;
    ws = null;
}

/* Camera functions */
function ws_onmessage(e_msg)
{
    e_msg = e_msg || window.event; // MessageEvent

    var u8view = new Uint8Array(e_msg.data);

    if(u8view[0] == 0xFF && u8view[1] == 0xD8) /* Start Of JPEG Image */
        image = u8view;
    else
    {
        var data = new Uint8Array(image.length + u8view.length);
        data.set(image);
        data.set(u8view, image.length);
        image = data;
    }

    if(u8view[u8view.length - 2] == 0xFF && u8view[u8view.length - 1] == 0xD9) /* End Of JPEG Image */
        document.getElementById("camera").src = "data:image/jpeg;base64,"+  arrayToBase64(image);
}
function arrayToBase64(byteArray)
{
    var hexString =  Array.from(byteArray, function(byte) {
            return ('0' + (byte & 0xFF).toString(16)).slice(-2);
        }).join('');

    return btoa(String.fromCharCode.apply(null, hexString.replace(/\r|\n/g, "").replace(/([\da-fA-F]{2}) ?/g, "0x$1 ").replace(/ +$/, "").split(" ")));
}

/* Joystick functions */
function send_data()
{
    console.log(joystick.x_percent + ":" + joystick.y_percent + "\r\n");
    if(ws != null)
        ws.send(joystick.x_percent + ":" + joystick.y_percent + "\r\n");
}
function update_view()
{
    var x = joystick.x, y = joystick.y;

    var canvas = document.getElementById("joystick");
    var ctx = canvas.getContext("2d");

    ctx.clearRect(-canvas_width/2, -canvas_height/2, canvas_width, canvas_height);

    ctx.lineWidth = 3;
    ctx.strokeStyle="gray";
    ctx.fillStyle = "LightGray";
    ctx.beginPath();
    ctx.arc(0, 0, range, 0, 2 * Math.PI);
    ctx.stroke();
    ctx.fill();

    ctx.strokeStyle="black";
    ctx.fillStyle = "hsl(0, 0%, 35%)";
    ctx.beginPath();
    ctx.arc(0, 0, radius_base, 0, 2 * Math.PI);
    ctx.stroke();
    ctx.fill();

    ctx.strokeStyle="red";

    var lineWidth = radius_shaft;
    var pre_x = pre_y = 0;
    var x_end = x/5;
    var y_end = y/5;
    var max_count  = (radius_shaft - 10)/step;
    var count = 1;

    while(lineWidth >= 10)
    {
        var cur_x = Math.round(count * x_end / max_count);
        var cur_y = Math.round(count * y_end / max_count);
        ctx.lineWidth = lineWidth;
        ctx.beginPath();
        ctx.lineTo(pre_x, pre_y);
        ctx.lineTo(cur_x, cur_y);
        ctx.stroke();

        lineWidth -= step;
        pre_x = cur_x;
        pre_y = cur_y;
        count++;
    }

    var x_start = Math.round(x / 3);
    var y_start = Math.round(y / 3);
    lineWidth += step;

    ctx.beginPath();
    ctx.lineTo(pre_x, pre_y);
    ctx.lineTo(x_start, y_start);
    ctx.stroke();

    count = 1;
    pre_x = x_start;
    pre_y = y_start;

    while(lineWidth < radius_shaft)
    {
        var cur_x = Math.round(x_start + count * (x - x_start) / max_count);
        var cur_y = Math.round(y_start + count * (y - y_start) / max_count);
        ctx.lineWidth = lineWidth;
        ctx.beginPath();
        ctx.lineTo(pre_x, pre_y);
        ctx.lineTo(cur_x, cur_y);
        ctx.stroke();

        lineWidth += step;
        pre_x = cur_x;
        pre_y = cur_y;
        count++;
    }

    var grd = ctx.createRadialGradient(x, y, 0, x, y, radius_handle);
    for(var i = 85; i >= 50; i-=5)
        grd.addColorStop((85 - i)/35, "hsl(0, 100%, "+ i + "%)");

    ctx.fillStyle = grd;
    ctx.beginPath();
    ctx.arc(x, y, radius_handle, 0, 2 * Math.PI);
    ctx.fill();
}
function process_event(event)
{
    var pos_x, pos_y;
    if(event.offsetX)
    {
        pos_x = event.offsetX - canvas_width/2;
        pos_y = event.offsetY - canvas_height/2;
    }
    else if(event.layerX)
    {
        pos_x = event.layerX - canvas_width/2;
        pos_y = event.layerY - canvas_height/2;
    }
    else
    {
        pos_x = (Math.round(event.touches[0].pageX - event.touches[0].target.offsetLeft)) - canvas_width/2;
        pos_y = (Math.round(event.touches[0].pageY - event.touches[0].target.offsetTop)) - canvas_height/2;
    }

    var x = joystick.x, y = joystick.y;
    var joystick_range = range - radius_handle;
    x_percent = Math.round(pos_x * 100 / joystick_range);
    y_percent = Math.round(-pos_y * 100 / joystick_range);

    return {x:pos_x, y:pos_y, x_percent:x_percent, y_percent:y_percent}
}
function mouse_down()
{
    if(ws == null)
        return;

    event.preventDefault();

    var pos = process_event(event);

    var delta_x = pos.x - joystick.x;
    var delta_y = pos.y - joystick.y;

    var dist = Math.sqrt(delta_x*delta_x + delta_y*delta_y);

    if(dist > radius_handle)
        return;

    click_state = 1;

    var radius = Math.sqrt(pos.x*pos.x + pos.y*pos.y);

    if(radius <(range - radius_handle))
    {
        delta_x = pos.x_percent - joystick.x_percent;
        delta_y = pos.y_percent - joystick.y_percent;

        dist = Math.sqrt(delta_x*delta_x + delta_y*delta_y);

        if(dist < 2)
            return;

        joystick = pos;
        send_data();
        update_view();
    }
}
function mouse_up()
{
    event.preventDefault();
    click_state = 0;
}
function mouse_move()
{
    if(ws == null)
        return;

    event.preventDefault();

    if(!click_state)
        return;

    var pos = process_event(event);

    var radius = Math.sqrt(pos.x*pos.x + pos.y*pos.y);

    if(radius <(range - radius_handle))
    {
        var delta_x = pos.x_percent - joystick.x_percent;
        var delta_y = pos.y_percent - joystick.y_percent;

        dist = Math.sqrt(delta_x*delta_x + delta_y*delta_y);

        if(dist < 2)
            return;

        joystick = pos;
        send_data();
        update_view();
    }
}

window.onload = init;
</script>
</head>

<body>
<h1>Arduino - Grove Camera - Web</h1>
<img id="camera"><br>
<canvas id="joystick"></canvas>
<h2>
<p>
WebSocket : <span id="ws_state">null</span><br>
</p>
<button id="bt_connect" type="button" onclick="connect_onclick();">Connect</button>
</h2>
</body>

</html>
Save this code to remote_camera.php and upload to PHPoC Shield via PHPoC Debugger according to this instruction



Setting and Test


Setup network information for PHPoC [WiFi] shield via this instruction

Access web camera by typing http://ip_address/remote_camera.php