Current Topic: The WizNet W5100 Ethernet shield provides a very simple solution for elevating an Arduino class controller to a networked device. It can deliver server or client or both capabilities simultaneously within the limit of only four active connections. Which is a pretty good match for a dinky little 8-bit processor.
Using The W5100 Ethernet Shield As A WebSocket Chat And White Board Server.
One practical use of the W5100 Arduino Ethernet Shield is as a collector and network server mostly for sensor data. As such. It would be doubly beneficial if the server spoke the WebSocket protocol making it servable to a standard Web browser over a persistent connection.
This example is basically a simple 'Echo' server based on the WebSockets protocol and running on an Arduino (clone) mega-2560 with the Wiznet W5100 Arduino compatible Ethernet shield. The server will perform the simple handshake with a client agent (browser) and, if successful, will form a persistent connection to a chat group where messages from any client are relayed to all clients. There is an exception with binary packets where the message is relayed to all clients except the originator. In this context it is assumed that the client sending the binary data has already consumed it and does not need the echo.
//----------------------------------------------------------------------------- // smegchat.ino // // Smegduino Web-Socket based White Board with Chat server example. // // A simple Web-Socket server that distributes incoming messages to connected // browsers. Essentially a smart echo server placing the context of the // message to relay at the browsers discretion. Thus the additional ability // to manage grachic exchange as well as text. // // Copyright (c) 2015 - No Fun Farms A.K.A. www.smegware.com // // All Smegware software is free; you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation; either version 2 of the License, or // (at your option) any later version. // // This software 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 General Public License for more details. // // Circuit: // * Wiznet W5100 Ethernet shield. // //----------------------------------------------------------------------------- // // History... // // $Source$ // $Author$ // $Revision$ // // $Log$ //----------------------------------------------------------------------------- #include <SPI.h> #include <Ethernet.h> #include <wsutils.h> //#define SMEGNETLOG //----------------------------------------------------------------------------- // Controls the max number of connected clients. #define ETHERNET_CLIENTS 4 // Smegchat listens on local port 89. #define ETHERNET_PORT 89 //----------------------------------------------------------------------------- // Define a MAC address for the Wiznet controller node. static byte mac[] = { 0xDE, 0xAD, 0xBE, 0xEF, 0xFE, 0xED }; //----------------------------------------------------------------------------- // Define the IP address, Gateway and subnet. IPAddress ip(10, 0, 1, 21); // Permanent address on private network. IPAddress gateway(10, 0, 1, 1); // Gateway to public network. IPAddress subnet(255, 0, 0, 0); // It's a large network. //----------------------------------------------------------------------------- // Smegnet. static EthernetServer server(ETHERNET_PORT); //----------------------------------------------------------------------------- // Define client tracking table. #define NICKNAMELEN 32 struct _client { EthernetClient client; uint16_t port; uint8_t ip[4]; uint8_t active; char nick[NICKNAMELEN]; }; static _client clients[ETHERNET_CLIENTS]; #define INVALID_NICKNAME "Invalid user name for this session!\r\n" //----------------------------------------------------------------------------- // Web socket interface. #define WSMASKDEFAULT 0X78563412 static WsHead head; static uint32_t wsmask; //----------------------------------------------------------------------------- // misc variables. #define MESSAGE_LENGTH 1024 static uint8_t cmsg[MESSAGE_LENGTH]; static uint8_t msgbuf[MESSAGE_LENGTH]; //----------------------------------------------------------------------------- // Create a method to log server messages. #ifdef SMEGNETLOG #define CONSOLE_LENGTH 98 static char console[CONSOLE_LENGTH]; static void smegnet_log(const char *format, ...) { va_list ap; va_start(ap, format); vsnprintf(console, CONSOLE_LENGTH, format, ap); va_end(ap); // Ensure termination... console[CONSOLE_LENGTH - 2] = '\n'; console[CONSOLE_LENGTH - 1] = 0; Serial.print(console); } #else #define smegnet_log(...) #endif //----------------------------------------------------------------------------- // Extract username (if any) from request URL. static uint8_t get_nickname(const char *request, char *nick) { uint8_t i; uint8_t p = 0; memset(nick, 0, NICKNAMELEN); for(i = 0; i < strlen(request); i++) { if(request[i] == '?') { i++; while(request[i] && (request[i] != ' ')) { nick[p++] = request[i++]; if(p == NICKNAMELEN - 1) { p = 0; break; } } break; } } return p; } //----------------------------------------------------------------------------- // Get new websocket mask. static uint32_t get_next_mask(void) { wsmask++; //return wsmask; // According to the Mozilla WebSocket Implementation guide the server to client // packet should always be unmasked since its purpose is not for security // but instead to help reduce the possibility of 'cache polution' especially // in regard to proxies. While in return the client to server packet must // always be masked. return 0; } //----------------------------------------------------------------------------- // Gracefully close (hopefuly) an existing connection. static void close_client(uint8_t cli, const uint8_t *msg) { ws_set_frame(msgbuf, msg, WS_FRAME_CLOSE, get_next_mask(), strlen((char*)msg)); clients[cli].client.write(msgbuf, ws_get_frame_size(msgbuf)); } //----------------------------------------------------------------------------- // Connect to a websocket client only. static int8_t client_connect(uint8_t cli) { memset(cmsg, 0, MESSAGE_LENGTH); clients[cli].client.getRemoteIP(clients[cli].ip); clients[cli].port = clients[cli].client.getRemotePort(); clients[cli].client.read(cmsg, MESSAGE_LENGTH); //Serial.print((char*)cmsg); // Note: pWsHeader is only valid until the call to ws_reply_handshake(). pWsHeader hdr = ws_parse_handshake(cmsg, MESSAGE_LENGTH); get_nickname(ws_get_handshake_request(hdr), clients[cli].nick); ws_reply_handshake(hdr, (char*)msgbuf); //Serial.print((char*)msgbuf); clients[cli].client.write(msgbuf, strlen((char*)msgbuf)); clients[cli].active = true; return 1; //client->connected? } //----------------------------------------------------------------------------- // Transmit global message to all connected clients. static void global_client_message(void) { uint8_t cli; for(cli = 0; cli < ETHERNET_CLIENTS; cli++) { if(clients[cli].client.connected()) { clients[cli].client.write(msgbuf, ws_get_frame_size(msgbuf)); } } } //----------------------------------------------------------------------------- // Inform all clients of new connection. static void new_client(uint8_t cli) { uint8_t usr; if(strlen(clients[cli].nick) == 0) { usr = 0; } else { for(usr = 0; usr < ETHERNET_CLIENTS; usr++) { if(clients[usr].client.connected()) { if((cli != usr) && strcmp(clients[usr].nick, clients[cli].nick) == 0) { break; } } } } if(usr >= ETHERNET_CLIENTS) { sprintf((char*)msgbuf, "%s@%u.%u.%u.%u:%u logged-in.\n", clients[cli].nick, clients[cli].ip[0], clients[cli].ip[1], clients[cli].ip[2], clients[cli].ip[3], clients[cli].port); ws_set_frame(msgbuf, msgbuf, WS_FRAME_TEXT, get_next_mask(), strlen((char*)msgbuf)); global_client_message(); } else { // Nickname invalid or already in use... Bump client. ws_set_frame(msgbuf, (const uint8_t*)INVALID_NICKNAME, WS_FRAME_TEXT, get_next_mask(), strlen(INVALID_NICKNAME)); clients[cli].client.write(msgbuf, ws_get_frame_size(msgbuf)); clients[cli].nick[0] = '\0'; close_client(cli, (const uint8_t*)INVALID_NICKNAME); } } //----------------------------------------------------------------------------- // Check network status. static void manage_clients(void) { uint8_t cli; for(cli = 0; cli < ETHERNET_CLIENTS; cli++) { EthernetClient client(cli); if(client.connected()) { clients[cli].client = client; if(!clients[cli].active) { if(clients[cli].client.available()) { client_connect(cli); smegnet_log("New connection accepted - %s@%u.%u.%u.%u:%u on socket:%u.\n", clients[cli].nick, (unsigned)clients[cli].ip[0], (unsigned)clients[cli].ip[1], (unsigned)clients[cli].ip[2], (unsigned)clients[cli].ip[3], (unsigned)clients[cli].port, (unsigned)client.getConnection()); new_client(cli); } } } else { if(clients[cli].active) { clients[cli].active = false; smegnet_log("Close request received - IP:%u.%u.%u.%u:%u.\n", (unsigned)clients[cli].ip[0], (unsigned)clients[cli].ip[1], (unsigned)clients[cli].ip[2], (unsigned)clients[cli].ip[3], (unsigned)clients[cli].port); sprintf((char*)msgbuf, "%s@%u.%u.%u.%u:%u logged-out.\n", clients[cli].nick, clients[cli].ip[0], clients[cli].ip[1], clients[cli].ip[2], clients[cli].ip[3], clients[cli].port); clients[cli].client.stop(); clients[cli].port = 0; clients[cli].ip[0] = 0; clients[cli].ip[1] = 0; clients[cli].ip[2] = 0; clients[cli].ip[3] = 0; if(clients[cli].nick[0]) { ws_set_frame(msgbuf, msgbuf, WS_FRAME_TEXT, get_next_mask(), strlen((char*)msgbuf)); global_client_message(); } } } } } //----------------------------------------------------------------------------- // Ardino env initialization. void setup(void) { uint8_t cli; // Initialize web-socket interface. wsmask = WSMASKDEFAULT; // Initialize client table. for(cli = 0; cli < ETHERNET_CLIENTS; cli++) { clients[cli].active = false; clients[cli].client = EthernetClient(ETHERNET_CLIENTS); clients[cli].port = 0; clients[cli].ip[0] = 0; clients[cli].ip[1] = 0; clients[cli].ip[2] = 0; clients[cli].ip[3] = 0; } // initialize the ethernet device Ethernet.begin(mac, ip, gateway, subnet); // start listening for clients server.begin(); // Open serial communications and wait for port to open: #ifdef SMEGNETLOG Serial.begin(9600); while(!Serial); // wait for serial port to connect. Needed for Leonardo only #endif delay(5); smegnet_log("Chat server address:%u.%u.%u.%u:%u started...\n", (unsigned)Ethernet.localIP()[0], (unsigned)Ethernet.localIP()[1], (unsigned)Ethernet.localIP()[2], (unsigned)Ethernet.localIP()[3], (unsigned)ETHERNET_PORT); } //----------------------------------------------------------------------------- // Arduino env main loop. void loop(void) { int8_t cli; size_t len; EthernetClient client; // wait for client activity. client = server.available(); manage_clients(); if(client) { if(client.available()) { // Read cient message. client.read(cmsg, MESSAGE_LENGTH); ws_get_frame_header(cmsg, &head); switch(head.opcode) { case WS_FRAME_CLOSE: // Client requests close... Just relay clients message back. smegnet_log("Close frame received from socket:%u.\n", (unsigned)client.getConnection()); ws_mask_unmask(head.data, head.data, head.mask, head.paylen); ws_set_frame(msgbuf, head.data, WS_FRAME_CLOSE, get_next_mask(), head.paylen); client.write(msgbuf, ws_get_frame_size(msgbuf)); // Close connection. client.stop(); break; case WS_FRAME_PING: // Client PING... Relay client message back as PONG (response). smegnet_log("PING frame received from socket:%u.\n", (unsigned)client.getConnection()); ws_mask_unmask(head.data, head.data, head.mask, head.paylen); ws_set_frame(msgbuf, head.data, WS_FRAME_PONG, get_next_mask(), head.paylen); client.write(msgbuf, ws_get_frame_size(msgbuf)); break; case WS_FRAME_PONG: // Spec says Ignore any PONG replies from Invalid (non) PING requests. smegnet_log("PONG frame received from socket:%u was Ignored.\n", (unsigned)client.getConnection()); break; case WS_FRAME_BINARY: // Relay to all clients except sender. smegnet_log("Binary frame received from socket:%u.\n", (unsigned)client.getConnection()); ws_unmask_inplace(cmsg); len = ws_get_frame_size(cmsg); for(cli = 0; cli < ETHERNET_CLIENTS; cli++) { if((client != clients[cli].client) && (clients[cli].client.connected())) { // Relay message. clients[cli].client.write(cmsg, len); } } break; default: // Relay to all clients. smegnet_log("Message frame received from socket:%u.\n", (unsigned)client.getConnection()); ws_mask_unmask(head.data, head.data, head.mask, head.paylen); ws_set_frame(msgbuf, head.data, WS_FRAME_TEXT, get_next_mask(), head.paylen); global_client_message(); break; } } } } //----------------------------------------------------------------------------- // end: smegchat.ino
The first demonstration considers a chat (text) channel with the addition of a whiteboard (drawing area). The challenge is keeping everybody in sync. The chat was not a big issue however the whiteboard was impossible to manage without some optimizing. That's where the non-echo-to-sender binary requirement manifested.
The WebSockets protocol defines two types of data packets. Text. And Binary. The Binary packet is sent as an unknown array of bytes. It is up to the agents involved to decide what the meaning of the actual data is.
There is also a connection handshake defined that is intended to ensure that the client does infact get connected to a WebSocket Server. The handshake is presented as an HTTP: request and on success will negotiate an upgrade to WS: protocol. Basically, the connection is handed off to a WebSocket server.
Additionally, client to server data is masked (modified) so that the client can't predict the data that the Server will actually see. Since the client does not have access to the mask a client would have great difficulties in trying to attack the Server since the actual data would appear random for even the same data on multiple attempts.
For the actual WebSocket connection and data management a custom library was developed to work with the Arduino.
//----------------------------------------------------------------------------- // wsutils.cpp // // WebSocket interface utilities implementation. // // Copyright (c) 2015 - No Fun Farms A.K.A. www.smegware.com // // All Smegware software is free; you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation; either version 2 of the License, or // (at your option) any later version. // // This software 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 General Public License for more details. // //----------------------------------------------------------------------------- // // History... // // $Source$ // $Author$ // $Revision$ // // $Log$ //----------------------------------------------------------------------------- #include <stdlib.h> #include <stdio.h> #include <stdint.h> #include <string.h> #include "utility/sha1.h" #include "wsutils.h" //----------------------------------------------------------------------------- #define HANDSHAKEFRAG "258EAFA5-E914-47DA-95CA-C5AB0DC85B11" #define WSFLAGFINMASK 0x80 #define WSFLAGRSVMASK 0x70 #define WSOPCODEMASK 0x0F #define WSFRAMEMASK 0x80 #define WSPAYLOADMASK 0x7F #define WSOPCODECONTINUE 0x00 #define WSOPCODETEXT 0x01 #define WSOPCODEBINARY 0x02 #define WSOPCODECLOSE 0x08 #define WSOPCODEPING 0x09 #define WSOPCODEPONG 0x0A #define HASHMAX 20 #define HASHBUFFER 80 struct _wshash { uint16_t length; uint8_t data[(HASHMAX * 2) + 40]; uint8_t hash[HASHMAX]; char b64[HASHMAX * 2]; }; #define HDRWSHKEY 96 #define HDRVALUEMAX 128 #define HDRKEYMAX 256 #define HDRREQUEST "GET /" #define HDRHOST "Host" #define HDRCONNECTION "Connection" #define HDRUPGRADE "Upgrade" #define HDRPROTOCOL "Sec-WebSocket-Protocol" #define HDRVERSION "Sec-Websocket-Version" #define HDRSECKEY "Sec-Websocket-Key" struct _wsheader { char request[HDRWSHKEY]; char host[HDRWSHKEY]; char connection[HDRWSHKEY]; char upgrade[HDRWSHKEY]; char protocol[HDRWSHKEY]; char version[HDRWSHKEY]; char seckey[HDRWSHKEY]; }; //----------------------------------------------------------------------------- // Base64 encoding table. static const char encoding_table[] = {'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z', 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z', '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', '+', '/'}; static const uint8_t mod_table[] = {0, 2, 1}; //----------------------------------------------------------------------------- // Using the base64 encoded handshake key formulate the WebSocket SHA1 response // key and base64 encode the result. int8_t ws_handshake_hash(uint8_t *msg) { uint8_t i; uint8_t j; uint32_t octet_a; uint32_t octet_b; uint32_t octet_c; uint32_t triple; struct _wshash wshash; memset(&wshash, 0, sizeof(struct _wshash)); sprintf((char*)wshash.data,"%s%s", msg, HANDSHAKEFRAG); wshash.length = strlen((char*)wshash.data); // Compute SHA HASH. libwebsockets_SHA1(wshash.data, wshash.length, wshash.hash); // Convert to BASE64. for(i = 0, j = 0; i < HASHMAX;) { octet_a = i < HASHMAX ? wshash.hash[i++] : 0; octet_b = i < HASHMAX ? wshash.hash[i++] : 0; octet_c = i < HASHMAX ? wshash.hash[i++] : 0; triple = (octet_a << 0x10) + (octet_b << 0x08) + octet_c; wshash.b64[j++] = encoding_table[(triple >> 3 * 6) & 0x3F]; wshash.b64[j++] = encoding_table[(triple >> 2 * 6) & 0x3F]; wshash.b64[j++] = encoding_table[(triple >> 1 * 6) & 0x3F]; wshash.b64[j++] = encoding_table[(triple >> 0 * 6) & 0x3F]; } j = strlen(wshash.b64); for(i = 0; i < mod_table[HASHMAX % 3]; i++) { wshash.b64[j - 1 - i] = '='; } memcpy(msg, wshash.b64, HASHMAX * 2); return 0; } //----------------------------------------------------------------------------- static void parse_key(struct _wsheader *phdr, const char *value, char *key) { if(!strncmp(value, HDRREQUEST, 5)) { strncpy(phdr->request, value, HDRWSHKEY - 1); } if(!strcasecmp(value, HDRHOST)) { strncpy(phdr->host, key, HDRWSHKEY - 1); } if(!strcasecmp(value, HDRCONNECTION)) { strncpy(phdr->connection, key, HDRWSHKEY - 1); } if(!strcasecmp(value, HDRUPGRADE)) { strncpy(phdr->upgrade, key, HDRWSHKEY - 1); } if(!strcasecmp(value, HDRPROTOCOL)) { strncpy(phdr->protocol, key, HDRWSHKEY - 1); } if(!strcasecmp(value, HDRVERSION)) { strncpy(phdr->version, key, HDRWSHKEY - 1); } if(!strcasecmp(value, HDRSECKEY)) { strncpy(phdr->seckey, key, HDRWSHKEY - 1); } } //----------------------------------------------------------------------------- struct _wsheader *ws_parse_handshake(uint8_t *header, uint16_t length) { int vl; int kl; uint16_t cnt; char value[HDRVALUEMAX + 1]; char key[HDRKEYMAX + 1]; uint8_t *head; struct _wsheader *phdr; phdr = (struct _wsheader*)malloc(sizeof(struct _wsheader)); if(phdr) { cnt = 0; memset(phdr, 0, sizeof(struct _wsheader)); while(header[cnt] && (cnt < length)) { head = &header[cnt]; for(vl = 0; head[vl] != ':'; vl++) { if(!head[vl] || (vl >= HDRVALUEMAX) || (head[vl] == '\r')) { break; } value[vl] = head[vl]; } value[vl] = '\0'; while(head[vl] == ':' || head[vl] == ' ') { vl++; }; for(kl = 0; head[vl] != '\r'; vl++, kl++) { if(!head[vl] || (kl >= HDRKEYMAX) || (head[vl] == '\r')) { break; } key[kl] = head[vl]; } key[kl] = '\0'; parse_key(phdr, (char*)value, (char*)key); while(head[vl] == '\r' || head[vl] == '\n') { vl++; }; cnt += vl; }; } return phdr; } //----------------------------------------------------------------------------- const char *ws_get_handshake_request(struct _wsheader *phdr) { char *req = NULL; if(phdr) { req= phdr->request; } return req; } //----------------------------------------------------------------------------- void ws_reply_handshake(struct _wsheader *phdr, char *header) { char hash[HASHBUFFER + 1]; if(phdr) { if(header) { strncpy(hash, phdr->seckey, HASHBUFFER); ws_handshake_hash((unsigned char*)hash); sprintf(header, "HTTP/1.1 101 Switching Protocols\r\n" "Upgrade: %s\r\n" "Connection: Upgrade\r\n" "Sec-WebSocket-Accept: %s\r\n" "Sec-WebSocket-Protocol: %s\r\n\r\n", phdr->upgrade, hash, phdr->protocol); } free(phdr); } } //----------------------------------------------------------------------------- uint16_t ws_get_frame_size(const uint8_t *frame) { uint8_t pnt; uint64_t len; uint8_t *plen; pnt = 2; if((frame[1] & WSPAYLOADMASK) == 126) { len = frame[pnt++] << 8; len |= frame[pnt++]; } else if((frame[1] & WSPAYLOADMASK) == 127) { plen = (uint8_t*)&len; plen[7] = frame[pnt++]; plen[6] = frame[pnt++]; plen[5] = frame[pnt++]; plen[4] = frame[pnt++]; plen[3] = frame[pnt++]; plen[2] = frame[pnt++]; plen[1] = frame[pnt++]; plen[0] = frame[pnt++]; } else { len = frame[1] & WSPAYLOADMASK; } if(frame[1] & WSFRAMEMASK) { pnt += 4; } return len + pnt; } //----------------------------------------------------------------------------- uint16_t ws_get_frame_length(const uint8_t *frame) { uint8_t pnt; uint64_t len; uint8_t *plen; pnt = 2; if((frame[1] & WSPAYLOADMASK) == 126) { len = frame[pnt++] << 8; len |= frame[pnt++]; } else if((frame[1] & WSPAYLOADMASK) == 127) { plen = (uint8_t*)&len; plen[7] = frame[pnt++]; plen[6] = frame[pnt++]; plen[5] = frame[pnt++]; plen[4] = frame[pnt++]; plen[3] = frame[pnt++]; plen[2] = frame[pnt++]; plen[1] = frame[pnt++]; plen[0] = frame[pnt++]; } else { len = frame[1] & WSPAYLOADMASK; } return len; } //----------------------------------------------------------------------------- uint32_t ws_get_frame_mask(const uint8_t *frame) { uint8_t pnt; uint32_t mask; uint8_t *pmask = (uint8_t*)&mask; mask = 0; if(frame[1] & WSFRAMEMASK) { pnt = 2; if((frame[1] & WSPAYLOADMASK) == 126) { pnt += 2; } else if((frame[1] & WSPAYLOADMASK) == 127) { pnt += 8; } pmask[3] = frame[pnt]; pmask[2] = frame[pnt + 1]; pmask[1] = frame[pnt + 2]; pmask[0] = frame[pnt + 3]; //mask = frame[pnt] << 24 | frame[pnt + 1] << 16 | frame[pnt + 2] << 8 | frame[pnt + 3]; } return mask; } //----------------------------------------------------------------------------- uint8_t ws_get_frame_opcode(const uint8_t *frame) { return frame[0] & WSOPCODEMASK; } //----------------------------------------------------------------------------- void ws_get_frame_header(uint8_t *frame, struct _wshead *head) { if(head && frame) { memset(head, 0, sizeof(struct _wshead)); head->size = ws_get_frame_size(frame); head->final = (frame[0] & WSFLAGFINMASK) != 0; head->opcode = ws_get_frame_opcode(frame); head->mask = ws_get_frame_mask(frame); head->paylen = ws_get_frame_length(frame); head->data = frame + (head->size - head->paylen); } } //----------------------------------------------------------------------------- void ws_set_frame_header(struct _wshead *head, uint8_t *frame) { if(head) { memset(head, 0, sizeof(struct _wshead)); head->size = ws_get_frame_size(frame); head->final = (frame[0] & WSFLAGFINMASK) != 0; head->opcode = ws_get_frame_opcode(frame); head->mask = ws_get_frame_mask(frame); head->paylen = ws_get_frame_length(frame); head->data = frame + (head->size - head->paylen); } } //----------------------------------------------------------------------------- void ws_mask_unmask(uint8_t *src, uint8_t *dst, uint32_t mask, uint16_t len) { uint16_t i; uint8_t msk[4]; if(mask) { msk[0] = mask >> 24; msk[1] = mask >> 16; msk[2] = mask >> 8; msk[3] = mask; for(i = 0; i < len; i++) { dst[i] = src[i] ^ msk[i % 4]; } } } //----------------------------------------------------------------------------- void ws_unmask_inplace(uint8_t *frame) { uint16_t len = 0;; uint32_t mask; uint8_t *pmask = (uint8_t*)&mask; uint8_t *dst; uint8_t *src = frame + 2; if(frame[1] & WSFRAMEMASK) { if((frame[1] & WSPAYLOADMASK) == 126) { len = *src++ << 8; len |= *src++; } else if((frame[1] & WSPAYLOADMASK) == 127) { dst += 8; // Correct. // Too big for implementation - Ignore. } else { len = frame[1] & WSPAYLOADMASK; } dst = src; // Quick and dirty - Not endian correct. pmask[3] = *src++; pmask[2] = *src++; pmask[1] = *src++; pmask[0] = *src++; ws_mask_unmask(src, dst, mask, len); frame[1] &= ~WSFRAMEMASK; } return; } //----------------------------------------------------------------------------- void ws_set_frame(uint8_t *frame, const uint8_t *data, uint8_t type, uint32_t mask, uint16_t paylen) { uint8_t p = 0; uint8_t hdrlen = 2; if(paylen > 125) { hdrlen += 2; } if(mask) { hdrlen += 4; } memmove(&frame[hdrlen], data, paylen); frame[p++] = WSFLAGFINMASK | (type & WSOPCODEMASK); if(paylen < 126) { frame[p++] = paylen; } else { frame[p++] = 126; frame[p++] = paylen >> 8; frame[p++] = paylen; } if(mask) { frame[1] |= WSFRAMEMASK; frame[p++] = mask >> 24; frame[p++] = mask >> 16; frame[p++] = mask >> 8; frame[p++] = mask; ws_mask_unmask(&frame[hdrlen], &frame[hdrlen], mask, paylen); } } //----------------------------------------------------------------------------- // end: wsutils.cpp
The Client Side Scripting Is What Really Makes This Work.
Making a practical use of this requires an HTML5 compliant web browser that supports the WebSocket protocol and the 'canvas' element and Java Script is enabled.
Additionally, as mentioned, the browser (client/user agent) is solely responsible for the data (packet) protocol since the Arduino is merely a smart echo server. This requires running Java-Script code in the browser to format the data to send to the server and to interpret the data returned from the server. Hopefully, in real time.
In this context it made sense to limit binary transfers and update clients only for final actions. In other words there is no need to show one client drawing a rectangle while it is being dragged and sized to dimension. The client needs to do this for itself and the final sized object is then propagated. Additionally drawing operations with the pen object turned out to be a special challenge. The Arduino was just not capable of pumping data packets, to multiple clients, at that rate. Therefore... The Pen (free) draw object buffers the pen strokes and then sends the list to the server, for propagation, when a threshold is reached.
/*-------------------------------------------------------------------------- * Text and Whiteboard chat common support. */ var ws; var wbArray; var wbList; var penArray; var penList; var wsColor; var wsUser; function ServerMessage(message) { var div = document.createElement("div"); var node = document.createTextNode(message); var element = document.getElementById("msglog"); div.style.color = wsColor; div.appendChild(node); element.appendChild(div); element.scrollTop = element.scrollHeight; } function chatOpen() { if("WebSocket" in window) { wsUser = document.getElementById("nick").value var U_R_L = "ws://www.smegware.com:8889/smegchat?" + wsUser; ws = new WebSocket(U_R_L,"whiteboard"); ws.binaryType = "arraybuffer"; ws.onopen = function(evt) { wbArray = new ArrayBuffer(20); wbList = new Uint16Array(wbArray); penArray = new ArrayBuffer(64); penList = new Uint16Array(penArray); var btn = document.getElementById("btnopen"); btn.disabled=true; var btn = document.getElementById("btnclose"); btn.disabled=false; }; ws.onmessage = function(evt) { if(evt.data instanceof ArrayBuffer) { pullDraw(evt.data); } else { var wsMsg = evt.data; if(wsMsg[0] == '#') { var data = wsMsg.split(',',3); wsColor = data[0]; ServerMessage(data[1] + ": " + data[2]); } else { // Generic Server messages have no additional info. wsColor = "#FFFFFF"; ServerMessage(wsMsg); } } }; ws.onclose = function(evt) { wsColor = "#FFFFFF"; ServerMessage("Connection is closed..."); var btn = document.getElementById("btnopen"); btn.disabled=false; var btn = document.getElementById("btnclose"); btn.disabled=true; }; ws.onerror = function(evt) { wsColor = "#FFFFFF"; ServerMessage("Error " + evt.data); }; } else { // The browser doesn't support WebSocket wsColor = "#FFFFFF"; ServerMessage("WebSocket API is NOT provided by your Browser!"); } } function chatClose() { ws.close(); } function chatTickle(evt) { var chat = document.getElementById("msgchat"); if(evt.keyCode == 13) { if(ws && ws.readyState == 1) { wsColor = document.getElementById("usrclr").value; ws.send(wsColor + "," + wsUser + "," + chat.value); } chat.value = ""; } } function onChatColorChange() { document.getElementById("msgchat").style.color = document.getElementById("usrclr").value; } /*-------------------------------------------------------------------------- * Whiteboard management. */ var mouseX = 0, mouseY = 0; var newMouseX = 0, newMouseY = 0; var oldMouseX = 0, oldMouseY = 0; var mouseDown = false; var color = "black"; var weight = 1; var shape = 1; var update = 1; var canvas; var wb; var wbctx; var wbColor; var overlay; var ovctx; const DRAW_PEN = 1; const DRAW_LINE = 2; const DRAW_ELLIPSE = 3; const DRAW_RECTANGLE = 4; const DRAW_TEXT = 8; const LINE_THIN = 1; const LINE_MEDIUM = 2; const LINE_THICK = 3; const LINE_DASHED = 4; function clearWB() { wbctx.fillStyle = "white"; wbctx.fillRect(0,0,640,360); } function preZeroHex(val) { if(val < 16) { return "0"; } else { return ""; } } function selectWBColor() { var clr = document.getElementById("clrchose"); color = clr.options[clr.selectedIndex].value; clr.style.backgroundColor=color; if(color=="black") { clr.style.color="white"; } else { clr.style.color="black"; } var c = clr.options[clr.selectedIndex].label.toString(16) wbColor = parseInt(c.substr(1,2),16) << 24 | parseInt(c.substr(3,2),16) << 16 | parseInt(c.substr(5,2),16) << 8 | 0; } function selectWBWeight(wide) { if(weight == LINE_DASHED) { wbctx.setLineDash([5]); } else { weight = wide; wbctx.setLineDash([1,0]); } } function initWBWeight() { var wgt = document.getElementById("wdash"); if(!wgt.checked) { wgt = document.getElementById("wthick"); if(!wgt.checked) { wgt = document.getElementById("wmedium"); if(!wgt.checked) { wgt = document.getElementById("wthin"); wgt.checked = true; weight = LINE_THIN; } else { weight = LINE_MEDIUM; } } else { weight = LINE_THICK; } } else { weight = LINE_DASHED; } selectWBWeight(weight); } function selectWBDraw(ls) { shape = ls; } function initWBDraw() { var shp = document.getElementById("srect"); if(!shp.checked) { var shp = document.getElementById("scircle"); if(!shp.checked) { var shp = document.getElementById("sline"); if(!shp.checked) { var shp = document.getElementById("spen"); shp.checked = true; shape = DRAW_PEN; } else { shape = DRAW_LINE; } } else { shape = DRAW_ELLIPSE; } } else { shape = DRAW_RECTANGLE; } } function selectWBUpdate(push) { if(push) { } else { var upd = document.getElementById("update"); var pud = document.getElementById("pushupdate"); if(upd.checked) { pud.disabled=true; } else { pud.disabled=false; } } } function initOverlay() { overlay = document.createElement("canvas"); overlay.id = "overlay"; overlay.width = wb.width; overlay.height = wb.height; wb.parentNode.appendChild(overlay); overlay.addEventListener("mousemove",onOverlayMouseMove,false); ovctx = overlay.getContext("2d"); overlay.style.cursor = "crosshair"; overlay.style.position = "absolute"; overlay.style.zIndex = -1; overlay.style.top = 0; overlay.style.left = 0; overlay.style.border = "1px solid black"; overlay.style.background = "transparent"; } function initWB() { wb = document.getElementById("whiteboard"); wbctx = wb.getContext("2d"); wsColor = "#FFFFFF"; wbctx.lineWidth = LINE_THIN; wb.addEventListener("mousedown",onCanvasMouseDown,false); wb.addEventListener("mousemove",onCanvasMouseMove,false); document.addEventListener("mouseup",onDocumentMouseUp,false); wb.width = 640; wb.height = 360; clearWB(); selectWBColor(); selectWBUpdate(0); initWBWeight(); initWBDraw(); initOverlay(); onChatColorChange(); } function pdrawRect(x,y,w,h) { wbctx.beginPath(); wbctx.rect(x,y,w,h); wbctx.stroke(); } function pdrawEllipse(x,y,xr,yr) { var r = xr > yr ? xr : yr; wbctx.beginPath(); wbctx.arc(x,y,r,0,360); wbctx.stroke(); } function pdraw(x1,y1,x2,y2) { wbctx.beginPath(); wbctx.moveTo(x1,y1); wbctx.lineTo(x2,y2); wbctx.closePath(); wbctx.stroke(); } function pushDraw(x1,y1,x2,y2,clr) { if(ws && ws.readyState == 1) { wbList[0] = shape; wbList[1] = weight; wbList[2] = clr >>> 16; wbList[3] = clr; wbList[4] = 1; wbList[5] = x1; wbList[6] = y1; wbList[7] = x2; wbList[8] = y2; ws.send(wbList); } } function pullDraw(packet) { var dl = new Uint16Array(packet); var rgba = dl[2] << 16 | dl[3]; var r = rgba >> 24 & 255; var g = rgba >> 16 & 255; var b = rgba >> 8 & 255; var a = rgba & 255; var clr = "#" + preZeroHex(r) + r.toString(16).toUpperCase() + preZeroHex(g) + g.toString(16).toUpperCase() + preZeroHex(b) + b.toString(16).toUpperCase(); wbctx.strokeStyle = clr; wbctx.lineWidth = dl[1]; if(dl[0] == DRAW_RECTANGLE) { pdrawRect(dl[5],dl[6],dl[7]-dl[5],dl[8]-dl[6]); } else if(dl[0] == DRAW_ELLIPSE) { pdrawEllipse(dl[5],dl[6],dl[7]-dl[5]>0?dl[7]-dl[5]:dl[5]-dl[7],dl[8]-dl[6]>0?dl[8]-dl[6]:dl[6]-dl[8]); } else if(dl[0] == DRAW_PEN) { for(var i = 5; i < (dl[4] + 5); i += 2) { pdraw(dl[i], dl[i + 1], dl[i + 2], dl[i + 3]); } } else { pdraw(dl[5],dl[6],dl[7],dl[8]); } delete dl; } function draw(x1,y1,x2,y2,clr) { wbctx.strokeStyle = clr; wbctx.lineWidth = weight; if(shape == DRAW_RECTANGLE) { pdrawRect(x1,y1,x2-x1,y2-y1); } else if(shape == DRAW_ELLIPSE) { var xr = oldMouseX - mouseX > 0 ? oldMouseX - mouseX : mouseX - oldMouseX; var yr = oldMouseY - mouseY > 0 ? oldMouseY - mouseY : mouseY - oldMouseY; pdrawEllipse(mouseX,mouseY,xr,yr); } else { pdraw(x1,y1,x2,y2); } } function onCanvasMouseDown(event) { mouseDown = true; event.preventDefault(); mouseX = event.layerX - this.offsetLeft; mouseY = event.layerY - this.offsetTop; oldMouseX = mouseX; oldMouseY = mouseY; if(shape == DRAW_PEN) { penList[0] = DRAW_PEN; penList[1] = weight; penList[2] = wbColor >>> 16; penList[3] = wbColor; penList[4] = 2; penList[5] = mouseX; penList[6] = mouseY; } else { overlay.style.zIndex = 99; } } function onCanvasMouseMove(event) { if(mouseDown) { if(shape == DRAW_PEN) { oldMouseX = mouseX; oldMouseY = mouseY; mouseX = event.layerX - this.offsetLeft; mouseY = event.layerY - this.offsetTop; draw(oldMouseX,oldMouseY,mouseX,mouseY,color); penList[penList[4]++ + 5] = mouseX; penList[penList[4]++ + 5] = mouseY; if(penList[4] > 24) { ws.send(penList); penList[4] = 2; penList[5] = mouseX; penList[6] = mouseY; } //pushDraw(oldMouseX,oldMouseY,mouseX,mouseY,wbColor); } } } function onOverlayMouseMove(event) { if(mouseDown) { ovctx.clearRect(0,0,overlay.width,overlay.height); ovctx.strokeStyle = color; ovctx.lineWidth = weight; oldMouseX = event.layerX - this.offsetLeft; oldMouseY = event.layerY - this.offsetTop; if(shape == DRAW_LINE) { ovctx.beginPath(); ovctx.moveTo(mouseX,mouseY); ovctx.lineTo(oldMouseX,oldMouseY); ovctx.closePath(); ovctx.stroke(); } if(shape == DRAW_ELLIPSE) { var x = oldMouseX - mouseX > 0 ? oldMouseX - mouseX : mouseX - oldMouseX; var y = oldMouseY - mouseY > 0 ? oldMouseY - mouseY : mouseY - oldMouseY; var r = x > y ? x : y; ovctx.beginPath(); ovctx.arc(mouseX,mouseY,r,0,360); ovctx.stroke(); } if(shape == DRAW_RECTANGLE) { ovctx.beginPath(); ovctx.rect(mouseX,mouseY, oldMouseX - mouseX, oldMouseY - mouseY); ovctx.stroke(); } } } function onDocumentMouseUp(event) { if(mouseDown) { ovctx.clearRect(0,0,overlay.width,overlay.height); if(shape == DRAW_PEN) { } else { draw(mouseX,mouseY,oldMouseX,oldMouseY,color); pushDraw(mouseX,mouseY,oldMouseX,oldMouseY,wbColor); } mouseDown = false; } overlay.style.zIndex = -1; }
Conclusion.
Although this project has been described in simple terms it is really at about the limit of what the hardware is capable of. The synchronization of multiple client connections for both text and graphic data is far more complex then the process of serving simple sensor data.
Thumbs up on this combo.
Additional Resources That May Be Useful...
IETF RFC6455 - The WebSocket Protocol.
Mozilla Developer Network MDN - Writing WebSocket servers.
Possible Live Demonstration.
Live White-Board Demonstration. Sometimes!
13108