WebSocket Messaging Library Users' Guide
You can find examples of websocket communication by starting Mosaic IDE Plus and creating the "Websocket Basic IO Demo" or the "Websocket Binary Arrays Demo".
WebSocket messages are passed between the PDQ Board and the browser using a single TCP/IP connection. Messages can be initiated by either side and are transmitted with very low latency. There are three types of messages used by Mosaic's WebSocket messaging library:
- Variable Messages
- Event Messages
- Websocket Binary Arrays
These messages can travel in either direction, creating six options total. This page describes in detail the sending and receiving of each of these six types of messages. Examples of these three types of messages can be found in MosaicIDE Plus demos: "Websocket Basic IO Demo" and "Websocket Binary Arrays Demo".
Variable and Event Messages are formatted as JSON objects. JSON, an acronym for "JavaScript Object Notation" is a lightweight data interchange format based on JavaScript object literal syntax that has become popular for Internet applications. As a programmer using Mosaic's WebSocket messaging library you don't need to know how to format JSON messages, but it may be useful background information.
Variable Messages should be used to send data that is real-time, always updating, and not sensitive to the number of transactions. For example the present measured speed of a conveyor is best sent using a Variable Message. The speed in this example may be changing, and if you look away from the screen for a moment you will miss some intermediate values, but this is OK. All that matters is the most recent speed of the conveyor. In this example the PDQ Board should run a loop which measures the speed and sends the value immediately. As described below, Variable Messages will need to be throttled in many cases to avoid causing a backlog.
Event Messages are analogous to remote procedure calls in other frameworks, and should be used to send data that requires more complex processing. Event Messages differ from Variable Messages in that you can specify program code (defined in a function) that runs with each message. This code can do things like increment a counter with every data point, keep a running sum, or keep track of the minimum and maximum values received. Event messages can also be used to track things that don't happen repeatedly. For example: the press of a button, device power on, an alarm, or other "Events" that occur on a less frequent basis. Event Messages can also be used for real-time data, just be sure that the function that executes with each data point doesn't spend a lot of time computing.
Websocket Binary Arrays are useful when you need to send large amounts of data. They are part of the websocket binary extension to the websocket driver. Websocket Binary Arrays do not use JSON. They use a simple packing structure designed at Mosaic Industries. They allow you to send arrays of data in a single websocket frame. Since there is no conversion to string data like there is with JSON and since there is only overhead with a single array (not each element), there is less networking overhead and less PDQ cpu overhead when compared to sending each element with Variable Messages. Using Websocket Binary Arrays can allow you to send up 240 4-byte data types a second (such as single precision float or 32bit integers)
Initialization of the network stack and the websockets
Before you can perform any websocket functions you must first initialize the network, initialize websockets (and binary websockets), declare and activate websocket variables and functions, and attach the websocket handlers to handle incoming data. The skeleton code to do this is shown below (the code isn't compilation complete, it's a modified fragment from "Websocket Binary Arrays Demo"):
void websocket_handler( int modulenum ) // handler for websocket connections { xaddr inbuf = HTTP_Inbuf( modulenum ); Wbskt_Handle_Frame( inbuf, modulenum ); } void websocket_bin_handler( int modulenum) { //the same inbuf used for regular websockets //can be used for the websockets binary extension xaddr inbuf = HTTP_Inbuf( modulenum ); Wbskt_Handle_Bin_Frame( inbuf, modulenum ); } _Q int IP_Set_Defaults( void ) // Works for EtherSmart or Wifi Wildcard. Call this AFTER calling main. // Sets mosaic factory defaults; returns error code. // Sets local IP and gateway to 0.0.0.0 = unassigned, so IP address // gets assigned via DHCP (Dynamic Host Configuration Protocol) by the LAN's gateway. // See Wildcard documentation for more information. { SAVE_STRING( ether_setdefaults_msg, "\r\nSetting defaults, requesting DHCP IP address..." ); SAVE_STRING( ether_setdefaults_errmsgfmt, "\r\nConfiguring network interface failed with error: %d\r\n" ); FLASH_PRINT( ether_setdefaults_msg ); Ether_XPort_Defaults( E_MODULENUM ); // works for xport or wiport (ethernet/wifi) int result = ( int )Ether_Await_Response( E_MODULENUM ); // error code is in lsword if( result ) FLASH_IPRINTF( ether_setdefaults_errmsgfmt, result ); return result; } void init_network() // initialization of ethernet { Ether_C_Task_Setup( E_MODULENUM); // Init RAM buffers and start task for one Wildcard only. // IP_Set_Defaults() uses DHCP to obtain an network information. See the // "Websocket Basic IO Demo" for manually entering network IP information IP_Set_Defaults(); } //================= Main =========================================== int main() { // First initialize EtherSmart or Wifi Wildcard, but do not yet add handler functions. init_network(); // blocking_errors parameter specifies whether an incoming frame error that occurs // when a previous error is still pending will block the websocket task until the // previous error is retrieved with Wbskt_Incoming_Error(). // If FALSE, new errors immediately overwrite previous errors. Wbskt_Init( FALSE ); Wbskt_Bin_Init(); Ether_IP_Info_Report( E_MODULENUM ); // Application-specific initialization. // Your websocket update-able variables, functions, and // arrays are typically declared and activated in your // application code StartTimeslicer(); transfer_service_init(); calc_init(); // Now that websocket JSON handling library, the websocket binary // handler, and applications are initialized, // add handler functions to begin processing requests from network clients. WS_Add_Handler( COMMON_XADDR( websocket_handler ), E_MODULENUM ); WS_Add_Bin_Handler( COMMON_XADDR( websocket_bin_handler), E_MODULENUM); return 0; }
WebSocket-Updatable PDQ Board Global Variables
This section describes how to declare, initialize, and access variables
on the PDQ Board to be updated via WebSocket messages.
The simplest form of WebSocket communication from a web browser to the
PDQ Board involves simply sending a new value for a global variable.
The variable to be updated via WebSocket messages may be of type
long
, float
, or boolean
(the last being a nonstandard type
that will evaluate to true or false in conditional tests based on
being updated with a JavaScript boolean true
or false
value).
There are three steps in configuring and using a global variable on
the PDQ Board updated via WebSocket messages that differ from normal
declaration and usage of a C global variable:
- Declare the variable as WebSocket-updatable
- Initialize the reception of update messages for the variable
- Access the variable with interrupt-protected function calls
Declaring WebSocket-Updatable Global Variable
The first step is declaring a variable to be updated via WebSocket
messages. The delcaration is placed outside of any function in a C
source file, using the macros WBSKT_FLOAT(),
WBSKT_LONG(), and WBSKT_BOOLEAN(), and specifying
both the name of the variable and its initial value when main()
is
executed:
WBSKT_LONG( bean_count, 42 ); WBSKT_FLOAT( bean_velocity, 0.0f ); WBSKT_BOOLEAN( beans_are_cooked, FALSE );
Initializing Global Variables For Updating
When your application is initialized, and AFTER the WebSocket messaging system is initialized with Wbskt_Init(), each WebSocket-updatable variable declared as described above must have metadata set up allowing it to be updated by WebSocket messages. This is accomplished by passing the name of each WebSocket-updatable variable to the macro WBSKT_CATCH(). For example:
void init_application() { Wbskt_Init( FALSE ); WBSKT_CATCH( bean_count ); WBSKT_CATCH( bean_velocity ); WBSKT_CATCH( beans_are_cooked ); }
After the code generated by these macros is executed at runtime, the variables are ready to be updated by received WebSocket messages. See sending-pdq-board-variable-values-from-javascript for instructions on sending messages from JavaScript to update a variable declared and initialized with the above macros on the PDQ Board.
Accessing WebSocket-Updated Global Variables
Accessing variables that are shared between multiple tasks requires special care, and this includes all WebSocket-updatable variables. In short, variables declared with WBSKT_LONG() may only be read by calling FETCH_LONG_PROTECTED(), and variables declared with WBSKT_FLOAT() may only be read by calling FETCH_FLOAT_PROTECTED().
As described in
uninterruptable-memory-operations,
the values of 32-bit types such as long
and float
are not read
or written atomically. As a result, when a 32-bit variable is
accessed from multiple TASKs, it is necessary to disable
interrupts when reading and writing it to ensure the validity of data
being read. This requirement applies to all variables declared with
WBSKT_LONG() or WBSKT_FLOAT(), since these variables
will be updated by the WebSocket messaging task that is started by
Wbskt_Init(), whenever a value update message is received.
Variables declared with WBSKT_BOOLEAN() are not subject to
this requirement, as they are read and written atomically.
As an example of how invalid data can be read if interrupts are not
disabled while reading a 32-bit variable, consider a scenario in which
an application task begins reading the value of 32-bit long int
variable bean_count
. At this moment, say the value of
bean_count
is 1,040,000 which is represented in a 32-bit long
int
as 0x000fde80
in hexadecimal. First the upper 16 bits are
read as 0x000f
, and then a timeslicer interrupt happens to occur.
The WebSocket messaging task is granted some processor time, and it
processes a recently-received message that updates the value of
bean_count
to 1,050,000 which is 0x00100590
in a 32-bit long
int
. When another timeslicer interrupt occurs and the application
task finishes reading the value of bean_count
by retrieving the
lower 16 bits from memory, it gets 0x0590
, and combines this with
the previously-read upper 16 bits to get the value 0x000f0590
,
which is equal to 984,464 – not a valid value of bean_count
either before or after the update message was processed.
Similarly, if the WebSocket messaging task were to write a new value
to bean_count
without disabling interrupts, it is possible that
the upper 16 bits could be written, then a timeslicer interrupt could
occur and the application task would get the new value of the upper 16
bits and the old value of the lower 16 bits, or 0x0010de80
, before
another timeslicer interrupt occurs and WebSocket messaging task
updates the lower 16 bits. In this case, the application would end up
reading the value 1,105,536. This is why it is necessary to disable
interrupts both when writing a 32-bit value that may be read in
another task, and when reading a 32-bit value that may be updated in
another task.
In fact, the WebSocket messaging task uses STORE_LONG_PROTECTED() and STORE_FLOAT_PROTECTED() to write the values received in update messages for 32-bit variables, and these functions disable interrupts while writing the new value. If your application changes the value of any WebSocket-updated variables, it should also do so using STORE_LONG_PROTECTED() or STORE_FLOAT_PROTECTED(). In the more common case of reading variables whose values are updated via WebSocket messages, use the functions FETCH_LONG_PROTECTED() and FETCH_FLOAT_PROTECTED(). For reading and writing variables declared with WBSKT_BOOLEAN(), no special procedure is required, as these variables are read and written atomically. See the glossary entries linked above, and also see uninterruptable-memory-operations for more information.
If you wish to update a string value on the PDQ Board from JavaScript, you must do so using a specially-defined function that either disables interrupts or calls GET() and RELEASE() on a RESOURCE variable defined for each string value to be updated, in order to ensure valid data is present in the string when read. See the end of the next section for more information.
WebSocket-Callable PDQ Board Functions
This section describes how to declare, initialize, and implement functions on the PDQ Board to be executed via WebSocket messages. Updating the value of a global variable is the simplest method of transferring data from JavaScript running in a web browser to the PDQ Board, but a more powerful feature is the ability to execute functions on the PDQ Board, optionally with arguments sent from the JavaScript code. The steps for setting up a WebSocket-callable function are similar to those described above for setting up a WebSocket-updatable variable:
- Define a function as WebSocket-callable,
- Initialize reception of messages for calling the function, and
- Ensure thread-safe usage of any shared data or resources within the function.
Define WebSocket-Callable Function
Function calls are referred to as an "event" in the Mosaic WebSocket message handling library. When defining a function that will be callable with WebSocket messages, the macro WBSKT_EVENT() is used in place of the normal C function signature. The first argument to the macro is the name of the function, and subsequent arguments specify function arguments that must be pointers of type long*
, float*
, boolean*
, or char*
. One additional argument, the EtherSmart Wildcard module number, is also passed automatically to the defined function as the first argument, int modulenum
. For example, the following definition:
WBSKT_EVENT( bean_label_create, long* can_count, char* brand ) { ... }
defines a WebSocket-callable function with the following signature (you need not enter the function definition as shown below; rather, this example shows the equivalent function definition created by the WBSKT_EVENT() macro used as shown above):
void bean_label_create( int modulenum, long* can_count, char* brand ) { ... }
Mosaic's WebSocket message handling library passes all arguments to functions defined with WBSKT_EVENT() as pointers, and such functions may accept up to 14 arguments.
Initialize Reception of Event Messages
WebSocket-callable functions use the same macro WBSKT_CATCH() that is used to initialize WebSocket message updates for variables, as described in a previous section. It must be called at runtime as follows:
void init_application() { Wbskt_Init( FALSE ); WBSKT_CATCH( bean_label_create ); }
After a WebSocket-callable function has been declared and initialized using the above macros on the PDQ Board, see sending-pdq-board-function-calls-from-javascript for instructions on sending remote function calls, known as event messages, from JavaScript.
Thread-Safe Usage of Shared Data
Functions declared with WBSKT_EVENT() are executed by the WebSocket service task, and as such any data shared with your application task is subject to the same risk of corruption due to task-switching that is described above for WebSocket-updatable variables. Thus, any global variables of type float
or long
, or typedefs for these such as xaddr
, need to be accessed with FETCH_LONG_PROTECTED(), FETCH_FLOAT_PROTECTED(), STORE_LONG_PROTECTED(), and STORE_FLOAT_PROTECTED() as described above for WebSocket-updatable variables.
However, if you wish to update strings, or arrays or structs whose members need to be consistent, it will be necessary to use a RESOURCE variable, also known as a mutex (for "mutual exclusion"), to protect access to the data in question.
The macros GET() and RELEASE() are used with RESOURCE variables to ensure atomic operations on data. As long as each task that uses a shared set of data calls GET() on an associated dedicated RESOURCE variable prior to accessing or modifying that data, no two tasks will access it at the same time. GET() accomplishes this by disabling interrupts just long enough to check the value of the RESOURCE variable, and if it is zero, write the present task's memory address to it before re-enabling interrupts. If the value of the RESOURCE variable is not zero and not equal to the present task's memory address, that means another task is currently holding it, and GET() will re-enable interrupts and call Pause() to cede control to other tasks, and then check the RESOURCE variable again in this manner repeatedly until it is released by the other task holding it. This way, task-switching can still occur while one task is using shared data, and execution of other tasks will only be delayed if they need to access that specific shared data; otherwise, other tasks continue to run normally. Once you are done accessing shared data, you must call RELEASE() on the associated RESOURCE variable to signal to other tasks that they may now access the shared data.
For detailed information see the glossary entries linked above as well as The PDQ Board guide to resource-variables. Another method mentioned earlier is to simply disable interrupts for the duration of access to shared data. This prevents task switching from occurring, thus assuring in a brute-force manner that no other task will access the shared data. For a given set of shared data, all tasks using it must either disable interrupts or use a RESOURCE variable dedicated to protecting that data; the methods may not be mixed for a particular set of data.
Below is an example of using a RESOURCE variable to protect access to a string, and disabling interrupts to protect access to an array.
char current_brand[16]; RESOURCE brand_resource; #define CASE_COUNT 3 long cases[CASE_COUNT]; WBSKT_EVENT( bean_label_create, long* can_count, char* brand ) { int i; GET( &brand_resource ); // snprintf may be used as a safe alternative to strncpy. snprintf( current_brand, sizeof( current_brand ), "%s", brand ); RELEASE( &brand_resource ); DISABLE_INTERRUPTS(); for( i = 0; i < CASE_COUNT; ++i ) { cases[i] += *can_count; } ENABLE_INTERRUPTS(); } void bean_case_report() { int i; GET( &brand_resource ); printf( "Current brand: %.16s\n", current_brand ); RELEASE( &brand_resource ); DISABLE_INTERRUPTS(); for( i = 0; i < CASE_COUNT; ++i ) { printf( "Case %u label count: %lu\n", i, cases[i] ); } ENABLE_INTERRUPTS(); } int main() { unsigned long cur_secs, prev_secs = 0; InitElapsedTime(); Wbskt_Init( FALSE ); WBSKT_CATCH( bean_label_create ); for(;;) { cur_secs = ReadElapsedSeconds(); if( cur_secs - prev_secs > 5 ) { prev_secs = cur_secs; bean_case_report(); } } return 0; }
The MosaicPDQ JavaScript API
Complementing the controller-side WebSocket API described above, Mosaic provides a JavaScript API, implemented according to the revealing module pattern, that defines a single global object called MosaicPDQ
. Functions are provided for opening a WebSocket connection to the PDQ Board, sending global variable values and function calls to the PDQ Board, updating element attributes and executing JavaScript functions based on WebSocket messages received from the PDQ Board (described below), and logging status and exception messages to a text box on the page.
Establishing a WebSocket Connection
As described in the Ethernet WiFi Device Driver Library, there is a two-second delay between closing one connection to the EtherSmart Wildcard before another connection to it may be opened. As such, if the webpage is loaded from the PDQ Board, it is necessary to wait at least 2.5 seconds after the page finishes loading before opening a WebSocket connection. To be tolerant of the variety of web browsers in use, including some that will automatically attempt to open additional connections to the server after the page finishes loading, we recommend waiting six seconds before opening a WebSocket connection.
A WebSocket connection is opened from JavaScript using the function MosaicPDQ.
websocketConnect(), and this may be done automatically with a setTimeout()
call triggered with the onload
event as follows:
window.onload = function() { MosaicPDQ.log( "Please wait for six seconds." ); setTimeout( MosaicPDQ.websocketConnect, 6000 ); };
If the webpage is instead loaded from a separate server or stored locally on the device providing the browser interface, and the EtherSmart Wildcard is only used for WebSocket connections, then the delay is not necessary. However, in that case you must specify the hostname or IP address of the EtherSmart Wildcard by passing a string to MosaicPDQ.
setPDQHostname:
MosaicPDQ.setPDQHostname( '10.0.1.151' ); window.onload = MosaicPDQ.websocketConnect;
Logging to an HTML textarea
The function MosaicPDQ.
log() will append text to a textarea
element on the page with an id
attribute equal to log
. This function is used internally by MosaicPDQ
for displaying status and exception messages to the user, and may also be called by your application JavaScript code to insert application status messages if desired. The textarea
element should be declared readonly
; here is an example tag declaring a suitable textarea
for log messages:
<textarea id="log" readonly="readonly" style="width:100%; resize:none;" rows="8"></textarea>
If you do not place a textarea
in the page with id="log"
, messages will instead be logged to the browser console.
Updating PDQ Board Global Variables From JavaScript
For variables declared on the PDQ Board with WBSKT_BOOLEAN(), WBSKT_LONG(), or WBSKT_FLOAT(), new values may be sent from the browser using the function MosaicPDQ.
send_value(). A WebSocket connection must already have been established. Generally, the value sent will be entered by the user in a form, either directly in a text input or selected with other input types. If user-entered text is used as the value, a verification should be performed in JavaScript to assure the value is in the correct format before sending it to the PDQ Board.
As an example, if a text box is used to enter a temperature value that is to be an integer, as follows:
<input type="text" id="oven_setpoint" />
Then, a function which sends the value in this textbox may validate the user input as follows before sending it:
function oven_setpoint_command() { var setpoint_string = document.getElementById( "oven_setpoint" ).value; var setpoint_degC = parseInt( setpoint_string, 10 ); // Check both not NaN and in expected range. if( setpoint_degC >= 100 && setpoint_degC <= 250 ) { MosaicPDQ.sendValue( "setpoint_temperature", setpoint_degC ); } else { MosaicPDQ.log( "Unexpected oven setpoint value: " + setpoint_string ); } } document.getElementById( "oven_setpoint" ).addEventListener( "input", oven_setpoint_command );
See updating-pdq-board-global-variables-from-javascript for instructions on declaring and initializing variables on the PDQ Board for being updated via WebSocket messages from JavaScript in this manner.
Calling PDQ Board Functions from JavaScript
Functions that have been defined on the PDQ Board using WBSKT_EVENT() are called from the browser using the function MosaicPDQ.
sendEvent(), which takes the name of the PDQ Board function to be executed in a string as its first argument. A WebSocket connection must already have been established. Any arguments to be passed to the PDQ Board function are passed to MosaicPDQ.
sendEvent() after the function name. The type of each argument must be one of the JavaScript types boolean
, number
, or string
. When the C function is executed on the PDQ Board, any arguments that were originally of JavaScript type number
will be interpreted as either long
or float
C type depending on the corresponding type specified in the arguments list of WBSKT_EVENT().
The following example uses the bean_label_create
function defined previously.
function bean_label_command() { var bean_label_string = document.getElementById( "bean_label" ).value; var can_count_string = document.getElementById( "can_count" ).value; var can_count = parseInt( can_count_string, 10 ); // Check both not NaN and in expected range. if( can_count > 0 && can_count < 10000 ) { MosaicPDQ.sendEvent( "bean_label_create", can_count, bean_label_string ); } else { MosaicPDQ.log( "Unexpected can count value: " + can_count_string ); } }
See executing-pdq-board-functions-from-javascript for instructions on declaring and initializing functions on the PDQ Board for being executed via WebSocket messages from JavaScript in this manner.
Throttling WebSocket Messages From Browser
HTML5 provides new input types in supporting browsers that allow for more intuitive interfaces without relying on browser plugins. One example is <input type="range">
, which generally provides a linear slider in supporting browsers, allowing the user to imprecisely but quickly select an integer from within a defined range. For instance, the oven setpoint example above could instead be taken from an HTML5 range
input:
<input type="range" id="oven_setpoint" min="100" max="250" value="175" />
Then, the range
input can be configured in JavaScript to generate a WebSocket message every time it is changed:
document.getElementById( "oven_setpoint" ).addEventListener( "input", oven_setpoint_command );
However, a problem with this approach is that a WebSocket message will be generated for every integer value the slider crosses as the user moves it to a new final value. Each message will be received by the EtherSmart Wildcard and processed by the PDQ Board, but the rate at which messages are received as the slider is moved may cause message processing to back up, and the user to notice a lag in the system adopting the newly-selected value.
The solution to the lag caused by the rapid generation of messages as the user moves the slider is to store the last time a message was sent, and not send any new messages until a certain time period has passed. In most cases this time period can be short enough that the user will not notice it, while still allowing messages to be processed by the PDQ Board without backing up. Here is the oven_setpoint_command
function above, modified to throttle update messages to once every ¼ second:
var throttle = 0; function oven_setpoint_command() { var now = Date.now(); if( now - throttle >= 250 ) { throttle = now; var setpoint_string = document.getElementById( "oven_setpoint" ).value; var setpoint_degC = parseInt( setpoint_string, 10 ); // Check both not NaN and in expected range. if( setpoint_degC >= 100 && setpoint_degC <= 250 ) { MosaicPDQ.sendValue( "setpoint_temperature", setpoint_degC ); } else { MosaicPDQ.log( "Unexpected oven setpoint value: " + setpoint_string ); } } } document.getElementById( "oven_setpoint" ).addEventListener( "input", oven_setpoint_command );
Client-Side Page Navigation
While a WebSocket connection is open, the EtherSmart Wildcard cannot accept any additional connections, and has a two-second delay between closing one connection and opening another. This means that if your web pages are served from the EtherSmart Wildcard, and you follow a normal web link to another page, and that page needs to open a new WebSocket connection to display data, there will be long delay in changing pages if you wish to design an interface divided into multiple pages. Having the HTML and CSS served from a separate web server can alleviate the delay in loading new pages, but there will still be a delay in re-opening the WebSocket connection to the EtherSmart Wildcard.
This problem can be resolved by loading the HTML and CSS for all pages in a multi-page interface at once, as separate elements of the same HTML document. The provided demo code includes the file gotoPage.js
providing a simple JavaScript function for switching between top-level <div>
elements in the HTML documents to be displayed. Each of these elements shares the class name client_side_page
and the function gotoPage()
assures that only one is visible, and the rest hidden, by setting the CSS property display
to either none
or inline
.
function gotoPage( page_id ) { if( typeof page_id === 'string' ) { var page_elem = document.getElementById( page_id ); if( page_elem ) { var client_side_pages = document.getElementsByClassName( 'client_side_page' ); for( var i = 0; i < client_side_pages.length; ++i ) { client_side_pages[i].style.display = 'none'; } page_elem.style.display = 'inline'; } else MosaicPDQ.log( 'gotoPage did not find element: ' + page_id ) } else MosaicPDQ.log( 'gotoPage called without element id.' ); }
This function is provided separately from the MosaicPDQ
module, but uses its logging functionality. Of course, it may be customized as needed whether web pages are served from the EtherSmart Wildcard or a separate web server.
In the provided demo code, each <div>
element implementing a client side page is in a separate file for clear organization based on the various available functionalities, but the files are not complete HTML documents.
<div class="client_side_page" id="digital_io"> ... </div>
<div class="client_side_page" id="analog_in"> ... </div>
A navigation header contains links to call gotoPage()
with the id
string for the appropriate client side page div.
... <a href="#" onclick="gotoPage('digital_io');">Digital I/O Demo</a> <a href="#" onclick="gotoPage('analog_in');">Analog Input Demo</a> ...
The file with the navigation header is sent first, followed by the various client side page files containing one <div>
element with the appropriate class string and different id
strings, are sent consecutively to the browser within the HTTP handler in order to form a complete HTML document.
void http_handler( int modulenum ) { HTTP_Send_HTML_Headers( TRUE, modulenum ); HTTP_SEND_HEADSTART( "Mosaic Websocket Front Panel Demo", modulenum ); HTTP_SEND_CSS( STYLE, modulenum ); HTTP_SEND_JAVASCRIPT( MOSAICPDQ, modulenum ); HTTP_SEND_JAVASCRIPT( GOTOPAGE, modulenum ); HTTP_PRINTF( modulenum, "\r\n</head><body>\r\n" ); HTTP_SEND_HTML( NAV_HEADER, modulenum ); HTTP_SEND_HTML( DIGITAL_IO, modulenum ); HTTP_SEND_HTML( ANALOG_IN, modulenum ); ... HTTP_SEND_JAVASCRIPT( DIGITAL_IO, modulenum ); HTTP_SEND_JAVASCRIPT( ANALOG_IN, modulenum ); ... HTTP_PRINTF( modulenum, "\r\n</body></html>\r\n" ); }
Displaying Data in the Web Interface from the PDQ Board
The Simplest Method: Macros Without Error Checking
The simplest form of communication from the PDQ Board to the web browser involves sending a new string to display on the webpage, which may contain numeric or non-numeric data. For most HTML elements, the content they display is specified by what appears between their beginning and end tags in the HTML code.
<p id="content_holder">This is content.</p>
When an HTML element is manipulated as a JavaScript object, for instance after looking it up using the JavaScript function document.getElementById()
, the content between the beginning and end tags is represented by the innerHTML
member of the object. So, displaying data in most HTML elements involves finding the element, and then assigning to its innerHTML
member:
var display_element = document.getElementById( 'content_holder' ); if( display_element ) display_element['innerHTML'] = 'Content changed.';
Mosaic's WebSocket message handling library provides four easy macros for sending a value to be assigned to the innerHTML
of an HTML element on the web page: WBSKT_SEND_BOOLEAN(), WBSKT_SEND_LONG(), WBSKT_SEND_FLOAT(), and WBSKT_SEND_STRING(). Each of these macros takes as its first argument the id
of the HTML element, the value (of the appropriate type: boolean
[int
], long
, float
, or char*
respectively) to assign to that element's innerHTML
, and the module number of the EtherSmart Wildcard with an open WebSocket connection.
WBSKT_SEND_LONG( "content_holder", 42, WETH_MODULENUM );
Error Checking for Value Messages
The macros described above simplify the most common method of displaying real-time data on a web page, but your application may require detecting any potential errors, or may require assigning a value to a different attribute of an HTML element, such as the value
of a read-only <input>
. In those cases, you can directly construct an outgoing WebSocket frame using the functions Wbskt_Boolean_LoadFrame(), Wbskt_Long_LoadFrame(), Wbskt_Float_LoadFrame(), or Wbskt_String_LoadFrame() and send it using WS_Send(), verifying the return values of each operation. The buffer specified by Ether_Outbuf()
and Ether_Outbufsize()
should generally be used for formatting outgoing messages from your application code, outside of HTTP handler functions or WebSocket handler functions.
xaddr module_outbuf = Ether_Outbuf( WETH_MODULENUM ); unsigned module_outbuf_size = Ether_Outbufsize( WETH_MODULENUM ); unsigned bytes_to_send = Wbskt_Long_LoadFrame( module_outbuf, module_outbuf_size, "content_holder", "innerHTML", 42 ); if( bytes_to_send != 0 ) { unsigned bytes_sent = WS_Send( module_outbuf, bytes_to_send, WETH_MODULENUM ); if( bytes_sent != bytes_to_send ) { printf( "Tried to send %u bytes, sent %u bytes.\n", bytes_to_send, bytes_sent ); } } else { printf( "WebSocket frame did not fit in buffer of size %u.\n", module_outbuf_size ); }
Calling JavaScript Functions from the PDQ Board
Calling a JavaScript function provides far more functionality than simply sending data to display. It is possible to alter the user interface in any way needed to implement the workflow of your application, whereas the previous method of sending data will only display a single value in a fixed element on the page.
Remote function calls are referred to as an "event" in the Mosaic WebSocket message handling library. Calling a JavaScript function from the PDQ Board involves sending an event message from the PDQ Board with the name of the function to be called, and a string containing the arguments to be passed to the function as a JavaScript array literal. Your application must generate the arguments array literal, i.e. using sniprintf()
and the LOADED_STRING buffer, such that it may be parsed with JavaScript's JSON.parse()
, and the result used in a function call with the apply()
method of the function. Thus, the string must begin with an opening square bracket, end with a closing square bracket, and contain a comma-separated set of JavaScript literals to be used as the arguments to the function to be executed.
In order for a JavaScript function to be callable from the PDQ Board, the function must be named in the global context, i.e. outside of any other function. Below we will show how to remotely execute an example function with the following signature.
function voltage_display( index, voltage ) { ... }
The Simplest Way to Call a JavaScript Function
Similarly to sending data to display on the web page, there is a macro WBSKT_SEND_EVENT() that simplifies calling JavaScript functions from C code running on the PDQ Board in cases where error checking is not required. If there are non-constant arguments to be passed to the function, first construct a string containing JavaScript literals for the values of those arguments, then simply call WBSKT_SEND_EVENT() with the name of the function in a string as the first argument, and the string of JavaScript literals as the second argument.
char javascript_params[16]; int result = snprintf( javascript_params, sizeof( javascript_params ), "[%u,%.2f]", 0, atd_voltages[0] ); if( result > 0 && result < sizeof( javascript_params ) ) { WBSKT_SEND_EVENT( "voltage_display", javascript_params, WETH_MODULENUM ); }
Calling a JavaScript Function with Error Checking
Again similarly to sending a value to display, you can check for errors in sending an event message by first loading the message into a buffer and then sending the buffer, examining return values of both operations. The buffer specified by Ether_Outbuf()
and Ether_Outbufsize()
should generally be used for formatting outgoing messages from your application code, outside of HTTP handler functions or WebSocket handler functions. The function to load an event message into the buffer is Wbskt_Event_LoadFrame() and the message is then sent with WS_Send.
char javascript_params[16]; int result = snprintf( javascript_params, sizeof( javascript_params ), "[%u,%.2f]", 0, atd_voltages[0] ); xaddr module_outbuf = Ether_Outbuf( WETH_MODULENUM ); unsigned module_outbuf_size = Ether_Outbufsize( WETH_MODULENUM ); unsigned bytes_to_send = Wbskt_Event_LoadFrame( module_outbuf, module_outbuf_size, "voltage_display", javascript_params ); if( bytes_to_send != 0 ) { unsigned bytes_sent = WS_Send( module_outbuf, bytes_to_send, WETH_MODULENUM ); if( bytes_sent != bytes_to_send ) { printf( "Tried to send %u bytes, sent %u bytes.\n", bytes_to_send, bytes_sent ); } } else { printf( "WebSocket frame did not fit in buffer of size %u.\n", module_outbuf_size ); }
Websocket Binary Arrays: Sending data from PDQ board to client
PDQ C Procedure for Sending Data
An Array of data can be sent directly from a C array and be put into a javascript global array.
First,
The array data must be formatted and loaded into a buffer to be sent out using the Wbskt_Array_LoadFrame_Bin
function. Wbskt_Array_LoadFrame_Bin
takes 7 parameters as seen below:
unsigned Wbskt_Array_LoadFrame_Bin( const xaddr xbuffer, const unsigned maxbytes, const char *js_str_name, const uint8_t size_of_element, const uint16_t size_of_array, const xaddr array_to_load, const uint8_t wbskt_bin_type );
xbuffer and maxbytes describe the buffer and its size used to build the websocket binary frame. Typically Ether_Outbuf( int modulenum);
and Ether_Outbufsize( int modulenum);
are used for the xbuffer and maxbytes. js_str_name
is the null terminated string with the javascript variable name that the frame will be copied to. size_of_element
is the size of each element in bytes. For example, you could put sizeof(float) or sizeof(char) as the parameter. size_of_array
is the size of the entire array in bytes. It can be calculated by multiplying size_of_element
by the number of elements in the array. array_to_load
is the c name of the array you want sent. wbskt_bin_type
is sent with the websocket binary frame and allows the javascript code to determine what kind of data is being sent. For binary arrays it must be one of the following:
WBSKT_BIN_TYPE_ARRAY_INT8
WBSKT_BIN_TYPE_ARRAY_UINT8
WBSKT_BIN_TYPE_ARRAY_INT16
WBSKT_BIN_TYPE_ARRAY_UINT16
WBSKT_BIN_TYPE_ARRAY_INT32
WBSKT_BIN_TYPE_ARRAY_UINT32
WBSKT_BIN_TYPE_ARRAY_FLOAT32
The function will return 0 if the size of the frame being built exceeds the size of maxbytes
. Otherwise it returns the size of the frame.
Once the frame has been built it should be sent out using void WS_Send_Frame_Bin( xaddr xbuffer, uint count, int modulenum);
This function is a non-blocking function so Ether_Await_Response
should be called afterwards to wait for the data to be sent out before trying to send data again. xbuffer
should be the same as in Wbskt_Array_LoadFrame_Bin
, count
should typically be the returned size of the frame from Wbskt_Array_LoadFrame_Bin
, modulenum
is the wildcard bus address of your ethernet or wifi wildcard (this depends on which wildcard bus the wildcard is on and what the address jumper positions are set at). Below is a send function created to send float arrays:
void Send_float_array( int modulenum, xaddr outbuf, unsigned outbuf_size, const char* varName, float floatsToSend[], int num_elements ) { unsigned result_len; result_len = Wbskt_Array_LoadFrame_Bin( outbuf, outbuf_size, varName, sizeof(float), num_elements*sizeof(float), COMMON_XADDR(floatsToSend), WBSKT_BIN_TYPE_ARRAY_FLOAT32 ); if(result_len){ WS_Send_Frame_Bin(outbuf, result_len, modulenum); Ether_Await_Response(modulenum); } else { print("There was an error in formatting the float array into the buffer.\n"); } }
MosaicPDQ Javascript Client Procedure for Receiving Data
You can receive data using the MosaicPDQ javascript library. To have an incoming websocket binary array deposited into your global javascript array there are two things you must do. First, the global variable must be declared as an array (you can declare it as an empty array). Second, the global variable must have the property "MosaicWritable" set to true. The code belows shows an example of a global variable that is ready to be written to.
var varName = []; varName.MosaicWritable = true;
Just like in regular text-based websocket updates, before you can receive data you must first make a websocket connection. See Establishing a WebSocket Connection
above in the The MosaicPDQ JavaScript API
Section.
Once your websocket connection is open, and the javascript array has been declared as shown above, the PDQ board can send arrays of data to the variable by using the C functions Wbskt_Array_LoadFrame_Bin
and WS_Send_Frame_Bin
.
Websocket Binary Arrays: Sending data to the MosaicPDQ JavaScript Library
PDQ C Procedure for Receiving Data
In order to receive websocket binary arrays first make sure the network stack and websockets has been initialized as described in the Initialization of the network stack and the websockets
section. Once that is complete you can perform the below steps in your application code.
Within global space declare your array with the macro WBSKT_BIN_ARRAY( NAME, NUM_ELEM, TYPE )
. NAME
is the name you want to give the array, NUM_ELEM
is the number of elements in the array, and TYPE
is the C type (for example float or char). This macro creates your array, defines a struct WSArrayHead
whos name is a concatenation of "wbskt_array_head_" and NAME
, and defines a function that activates the array whos name is a concatenation of "Wbskt_Bin_Activate_" and NAME
.
To activate the array you call the functioned defined by the macro above.
For example, if you want your array named var_name with 10 elements of type int then you would have the code below:
WBSKT_BIN_ARRAY( var_name, 10, int); void application_init() { Wbskt_Bin_Activate_var_name(); }
The array is declared as a volatile global variable. You can access the variable directly by its name (from the above example var_name[0] will return the first element of the array). However, it's important to note that the variable can be updated asynchronously to your code. This means you must take special care when reading 32bit values like long and float. You can read about the precautions you should take in Accessing WebSocket-Updated Global Variables
in the WebSocket-Updatable PDQ Board Global Variables
section. Furthermore, if it is important to you that none of the elements in the array change while you read the array you can set the WBSKT_BIN_STATUS_LOCK_AND_DENY
bit of status
data member of wbskt_array_head_##NAME
(concatenation of "wbskt_array_head_" and NAME
) and then clear it afterwards. Websockets will not update your binary array while this bit is set, but any incoming data will be lost as long as the bit is set. Set and clearing the status
member should be done with SetBits
and ClearBits
. An example code fragment based off of the above code is given below:
void readArray(){ // You don't necessarily need to lock the array while you read it, // however, if it's important that the array data doesn't get updated // while you operate on it then you can follow this example. int i; // lock the websocket array SetBits( WBSKT_BIN_STATUS_LOCK_AND_DENY, wbskt_array_head_var_name.status); // perform your operations on the data here, websockets will not update it // during this time. Any incoming websocket data destined for the array // "var_name" will be lost until we clear the WBSKT_BIN_STATUS_LOCK_AND_DENY // bit. As an example, below we loop through the indexes and print them // using printf for( i =0; i < 10, ++i){ printf("Data in index %i is %i", i, var_name[i]); } // unlock the websocket array ClearBits( WBSKT_BIN_STATUS_LOCK_AND_DENY, wbskt_array_head_var_name.status); // Websockets will now continue updating this variable as its receives incoming // data. }
MosaicPDQ Javascript Client Procedure for Sending Data
You can send data using the MosaicPDQ javascript library with the function sendArrayBin
. The prototype for the function is MosaicPDQ.sendArrayBin( nameOfVariable, arrayToSend, wbsktBinType);
nameOfVariable
is the name of the array that the data is destined for (declared with WBSKT_BIN_ARRAY in c code). wbsktBinType
specifies what type of binary data is being sent. For binary arrays it should specify the data type and must be one of the following:
WBSKT_BIN_TYPE_ARRAY_INT8
WBSKT_BIN_TYPE_ARRAY_UINT8
WBSKT_BIN_TYPE_ARRAY_INT16
WBSKT_BIN_TYPE_ARRAY_UINT16
WBSKT_BIN_TYPE_ARRAY_INT32
WBSKT_BIN_TYPE_ARRAY_UINT32
WBSKT_BIN_TYPE_ARRAY_FLOAT32
If the size of the array being sent exceeds the size of the c array declared with WBSKT_BIN_ARRAY then an error will be issued from the c driver.