Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Web socket upgrade #101

Merged
merged 24 commits into from
Mar 8, 2024
Merged
Show file tree
Hide file tree
Changes from 13 commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ jobs:
check_mode: "compile"
no_compile_flag: false
exclude_dirs: '["failing", "experiments"]'
compiler_ref: ${{ needs.find-latest-release.outputs.ref }}
compiler_ref: "master" # ${{ needs.find-latest-release.outputs.ref }}

check-format:
runs-on: ubuntu-latest
Expand Down
1 change: 1 addition & 0 deletions examples/C/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

(in alphabetical order)

* [Browser UIs](src/browser-ui/README.md): How to create a browser-based UI for an LF program.
* [Car Brake](src/car-brake/README.md): Sketch of ADAS system illustrating the CAL theorem.
* [Deadlines](src/deadlines/README.md): Uses of deadlines in Lingua Franca.
* [Distributed](src/distributed/README.md): Basic federated hello-world examples.
Expand Down
23 changes: 23 additions & 0 deletions examples/C/src/browser-ui/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
# Browser UI

These examples show how to create user interfaces for a Lingua Franca program.
The UI runs in the browser and connects to the program via either HTTP or via a web socket.

<table>
<tr>
<td> <img src="img/BrowserUI.png" alt="BrowserUI" width="400">
<td> <a href="BrowserUI.lf">BrowserUI.lf</a>: This version starts a web server that serves a specified web page and enables implementing an HTTP-based API to control your LF program. When the program is running, you can point your browser to <a href="http://localhost:8080">http://localhost:8080</a> to get a page. Adding a path to the URL, as in for example, http://localhost:8080/count, will cause the ServerUI reactor to produce an output that your LF program can react to and send a (text) response.</td>
</tr>
<tr>
<td> <img src="img/WebSocket.png" alt="WebSocket" width="400">
<td> <a href="WebSocket.lf">WebSocket.lf</a>: This example uses the much more versatile WebSocketServer reactor. When the program is running, you can open an HTML page that includes JavaScript that connects to the server. Messages can be sent in both directions over the web socket, from the LF program to the browser and vice versa.</td>
</tr>
<tr>
<td> <img src="img/WebSocketString.png" alt="WebSocketString" width="400">
<td> <a href="WebSocketString.lf">WebSocketString.lf</a>: This version uses the simpler WebSocketServerString reactor, which is simpler in that it restricts the messages transported over the web socket to be of string types and it allows only one client to connect.</td>
</tr>
<tr>
<td> <img src="img/Uptime.png" alt="Uptime" width="400">
<td> <a href="Uptime.lf"> Uptime.lf</a>: This version combines ServerUI with WebSocketServer to serve a web page and then feed it data continuously through a web socket. The application displays the total time that application has been running and updates this time once per second.</td>
</tr>
</table>
51 changes: 51 additions & 0 deletions examples/C/src/browser-ui/Uptime.lf
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
/**
* This example combines `ServerUI` with `WebSocketServer`. The former starts a web server that
* listens for HTTP requests on port 8080 and serves the web page defined in `uptime.html`. That web
* page includes JavaScript that connects to a web socket on port 8000 that is provided by the
* `WebSocketServer` reactor. The resulting web page simply reports the total time that this program
* has been running. That time is updated on the web page once per second.
*
* This uses the <a href="https://libwebsockets.org">libwebsockets</a> (see <a
* href="https://libwebsockets.org/lws-api-doc-main/html/index.html">API documentation</a> and <a
* href="https://libwebsockets.org/lws-api-doc-main/html/md_READMEs_README_build.html">installation
* instructions</a>). To install on MacOS, we recommending using brew:
* <pre> brew install libwebsockets
* </pre> This puts the compiled libraries in {@code /usr/local/lib}, and these libraries can be
* linked to using the {@code -lwebsockets} compile option or the {@code WebSocketCmake.txt} Cmake
* include file.
*
* @author Edward A. Lee
*/
target C {
build-type: debug,
keepalive: true
}

import ServerUI from "../lib/ServerUI.lf"
import WebSocketServer from "../lib/WebSocketServer.lf"

main reactor {
timer seconds(0, 1 s)

s = new ServerUI(hostport=8080, initial_file="Uptime.html")
w = new WebSocketServer()

reaction(startup) {=
lf_print("Point your browser to http://localhost:8080");
=}

reaction(seconds) -> w.send {=
instant_t uptime = lf_time_logical_elapsed();
// Truncate to the nearest second.
uptime = (uptime / SEC(1)) * SEC(1);
char* message = (char*)malloc(LF_TIME_BUFFER_LENGTH * sizeof(char));
size_t length = lf_readable_time(message, uptime);

// Broadcast to all connected sockets. This is accomplished by providing a NULL wsi.
web_socket_message_t* to_send = (web_socket_message_t*)malloc(sizeof(web_socket_message_t));
to_send->wsi = NULL;
to_send->length = length;
to_send->message = message;
lf_set(w.send, to_send);
=}
}
16 changes: 9 additions & 7 deletions examples/C/src/browser-ui/WebSocket.html
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,8 @@
// Get references to elements on the page.
var form = document.getElementById('message-form');
var messageField = document.getElementById('message');
var messagesList = document.getElementById('messages');
var incoming = document.getElementById('messages');
var outgoing = document.getElementById('outgoing');
var socketStatus = document.getElementById('status');
var closeBtn = document.getElementById('close');

Expand Down Expand Up @@ -38,9 +39,9 @@
// Send the message through the WebSocket.
if (socket.readyState == WebSocket.OPEN) {
socket.send(message);
messagesList.innerHTML = '<li class="sent"><span>Sent:</span>' + message + '</li>';
outgoing.innerHTML = message;
} else {
messagesList.innerHTML = '<li class="sent"><span>Socket is not open!</span></li>';
outgoing.innerHTML = 'Socket is not open!';
}

// Clear out the message field.
Expand All @@ -52,8 +53,7 @@
// Handle messages sent by the server.
socket.onmessage = function(event) {
var message = event.data;
messagesList.innerHTML = '<li class="received"><span>Received:</span>' +
message + '</li>';
incoming.innerHTML = message;
};

// Show a disconnected message when the WebSocket is closed.
Expand All @@ -76,8 +76,10 @@
<h1>WebSockets Demo</h1>

<div id="status">Connecting...</div>

<ul id="messages"></ul>
<ul>
<li> Incoming message: <span id="messages">nothing yet</span></li>
<li> Outgoing message: <span id="outgoing">nothing yet</span></li>
</ul>

<form id="message-form" action="#" method="post">
<textarea id="message" placeholder="Write your message here..." required></textarea>
Expand Down
67 changes: 25 additions & 42 deletions examples/C/src/browser-ui/WebSocket.lf
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,18 @@
* the WebSocket.html web page more than twice, only the first two attempts will succeed in
* connecting. By default, WebSocketServer imposes no such limit.
*
* This example also shows one way to keep track of multiple connections. It uses a hashset
* containing pointers to the wsi (web socket interface) fields, which are unique to each
* connection. In this example, if you connect two clients, one will receive even numbered counts,
* and the other will receive odd numbered counts.
* In this example, if you connect two clients, one will receive even numbered counts, and the other
* will receive odd numbered counts. This is because the count state variable is shared across all
* clients.
*
* This uses the <a href="https://libwebsockets.org">libwebsockets</a> (see <a
* href="https://libwebsockets.org/lws-api-doc-main/html/index.html">API documentation</a> and <a
* href="https://libwebsockets.org/lws-api-doc-main/html/md_READMEs_README_build.html">installation
* instructions</a>). To install on MacOS, we recommending using brew:
* <pre> brew install libwebsockets
* </pre> This puts the compiled libraries in {@code /usr/local/lib}, and these libraries can be
* linked to using the {@code -lwebsockets} compile option or the {@code WebSocketCmake.txt} Cmake
* include file.
*
* @author Edward A. Lee
*/
Expand All @@ -21,68 +29,43 @@ target C {

import WebSocketServer from "../lib/WebSocketServer.lf"

preamble {=
#include "hashset/hashset.h" // For storing web socket instances that are connected.
=}

main reactor {
state count: int = 0
logical action send_action: web_socket_instance_t*

state connected_instances: hashset_t = {= NULL =}

s = new WebSocketServer(max_clients=2) // Limit number of clients to 2.

reaction(startup) -> s.send {=
lf_set_destructor(s.send, web_socket_message_destructor);
lf_set_copy_constructor(s.send, web_socket_message_copy_constructor);

reaction(startup) {=
lf_print("======== Starting server. Open WebSocket.html in your favorite browser.");
self->connected_instances = hashset_create(2); // Default capacity for four instances.
=}

reaction(send_action) -> s.send, send_action {=
// If the web socket is no longer connected, the instance will not be in the hashset.
if (hashset_is_member(self->connected_instances, send_action->value->wsi)) {
char* message;
asprintf(&message, "Count is: %d", self->count++);
char* message;
asprintf(&message, "Count is: %d", self->count++);

web_socket_message_t* container = (web_socket_message_t*)malloc(sizeof(web_socket_message_t));
container->message = message;
container->length = strlen(message) + 1;
container->wsi = send_action->value->wsi;
web_socket_message_t* container = (web_socket_message_t*)malloc(sizeof(web_socket_message_t));
container->message = message;
container->length = strlen(message) + 1;
container->wsi = send_action->value->wsi;

lf_set(s.send, container);
// Schedule the next send.
lf_schedule_token(send_action, SEC(1), send_action->token);
}
lf_set(s.send, container);
// Schedule the next send.
lf_schedule_token(send_action, SEC(1), send_action->token);
=}

reaction(s.connected) -> send_action {=
if (s.connected->value.connected) {
lf_print("======== Connected a new client");
} else {
lf_print("======== Disconnected client");
}
if (s.connected->value.connected) {
// New connection.
hashset_add(self->connected_instances, s.connected->value.wsi);

// Start sending.
lf_schedule_copy(send_action, 0, &s.connected->value, 1);
} else {
// Disconnecting. Remove from hashset to prevent further scheduling.
hashset_remove(self->connected_instances, s.connected->value.wsi);
lf_print("======== Disconnected client");
}
=}

reaction(s.received) {=
lf_print("======== Application received: %s", s.received->value->message);
=}

reaction(shutdown) {=
if (self->connected_instances != NULL) {
hashset_destroy(self->connected_instances);
}
// NOTE: The WebSocketServer ensures that the received message is null terminated.
lf_print("======== Application received: %s", (char*)s.received->value->message);
=}
}
64 changes: 64 additions & 0 deletions examples/C/src/browser-ui/WebSocketString.lf
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
/**
* Demo of a use of WebSocketServerString enabling a user interface realized in the browser. Compile
* and run this program, then open WebSocket.html in your favorite browser. This example program
* sends to the web page a counting sequence. It also accepts text messages from the web page and
* prints them on standard output.
*
* This example limits the number of connections to just one. Its purpose is to show how to use the
* simpler WebSocketServerString when you need just one connection and the data exchanged is always
* a string.
*
* @author Edward A. Lee
*/
target C {
keepalive: true
}

import WebSocketServerString from "../lib/WebSocketServerString.lf"

main reactor {
state count: int = 0
state connected: bool = false

logical action send_action

s = new WebSocketServerString() // Limit number of clients to 2.

reaction(startup) {=
lf_print("======== Starting server. Open WebSocket.html in your favorite browser.");
=}

reaction(send_action) -> s.in_dynamic, send_action {=
// Send using the `in_dynamic` port because the string sent is dynamically allocated.
// If the web socket is no longer connected, do nothing.
if (self->connected) {
// Create the message.
char* message;
asprintf(&message, "Count is: %d", self->count++);

// Send the message.
lf_set(s.in_dynamic, message);

// Schedule the next send.
lf_schedule(send_action, SEC(1));
}
=}

reaction(s.connected) -> send_action {=
if (s.connected->value) {
lf_print("======== Client has connected");
} else {
lf_print("======== Client has disconnected");
}
self->connected = s.connected->value;
if (s.connected->value) {
// Start sending.
lf_schedule(send_action, 0);
}
=}

reaction(s.received) {=
// NOTE: The WebSocketServerString ensures that the received message is null terminated.
lf_print("======== Application received: %s", (char*)s.received->value);
=}
}
Binary file added examples/C/src/browser-ui/img/BrowserUI.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added examples/C/src/browser-ui/img/Uptime.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added examples/C/src/browser-ui/img/WebSocket.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added examples/C/src/browser-ui/img/WebSocketString.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
42 changes: 42 additions & 0 deletions examples/C/src/browser-ui/uptime.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
<!DOCTYPE html>
<html>
<head><title>Uptime Lingua Franca</title></head>
<body>
<h1>The Uptime Lingua Franca program has been running for:</h1>
<h1 id="uptime">No data yet</h1>
This page connects to a Lingua Franca program that feeds it time data through a web socket.
<p id="status">Not connected</p>
<script>
window.onload = function() {

// Change the address here to that of the hosting machine.
const socket = new WebSocket('ws://localhost:8000', 'ws');

// Get references to elements on the page.
var uptime = document.getElementById('uptime');
var status = document.getElementById('status');

socket.addEventListener('open', (event) => {
console.log('WebSocket connection established');
status.innerHTML = 'Connected to: ' + event.currentTarget.url;
});

socket.onerror = function(error) {
console.log('WebSocket Error: ' + error);
status.innerHTML = 'Not connected';
};

// Handle messages sent by the server.
socket.onmessage = function(event) {
uptime.innerHTML = event.data;
};

// Show a disconnected message when the WebSocket is closed.
socket.onclose = function(event) {
event.preventDefault();
status.innerHTML = 'Not connected';
};
};
</script>
</body>
</html>
1 change: 0 additions & 1 deletion examples/C/src/leader-election/NRP_FD.lf
Original file line number Diff line number Diff line change
Expand Up @@ -128,7 +128,6 @@ reactor Node(
=}
}

// mode Waiting
mode Primary {
timer heartbeat(0, heartbeat_period)
timer ping_NRP_timer(routine_ping_offset, heartbeat_period)
Expand Down
Loading
Loading