-
Notifications
You must be signed in to change notification settings - Fork 13
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #92 from lf-lang/tug-of-war-flask-ui
Add TugOfWar demo, which illustrates using a flask-based Python UI with an LF program.
- Loading branch information
Showing
10 changed files
with
501 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,239 @@ | ||
import pandas as pd | ||
import dash | ||
from dash import dcc, html, Input, Output | ||
from dash.exceptions import PreventUpdate | ||
import plotly.express as px | ||
from flask import Flask, request | ||
import json | ||
import app_helper as ap | ||
import dash_bootstrap_components as dbc | ||
import dash_daq as daq | ||
|
||
|
||
player_force = dict({ | ||
'players': ['Team_A_Player_0', 'Team_A_Player_1', 'Team_B_Player_0', 'Team_B_Player_1'], | ||
'forces': [0, 0, 0, 0], | ||
'score': 'Advantage: None', | ||
'rope_mark': 20 | ||
}) | ||
|
||
print(player_force) | ||
|
||
limit1 = 5 | ||
limit2 = 35 | ||
rope_range = 40 | ||
|
||
|
||
# Create Flask server | ||
server = Flask(__name__) | ||
|
||
# Create Dash app | ||
app = dash.Dash(__name__, server=server,external_stylesheets=[dbc.themes.BOOTSTRAP]) | ||
|
||
df = ap.build_rope_dataframe(limit1, limit2, player_force['rope_mark']) | ||
|
||
# Define layout | ||
app.layout = html.Div([ | ||
dbc.Card( | ||
dbc.CardBody([ | ||
|
||
# Row 1: title | ||
dbc.Row([ | ||
dbc.Col([ | ||
html.Div( | ||
dbc.CardImg(src='assets/lf.png', | ||
style={'backgroundColor': '#013163', 'width': '40%', 'height': '40%'}, | ||
id='logo' | ||
) | ||
, style={'align': 'center'}) | ||
], align='center', width=2), | ||
dbc.Col([ | ||
html.Div(ap.drawTextCard("TUG OF WAR"), | ||
id='title'), | ||
], width=10), | ||
], style={'backgroundColor': '#013163'}, align='center'), | ||
html.Br(), | ||
|
||
### Row 2: Score | ||
dbc.Row([ | ||
dbc.Col([ | ||
html.Div( | ||
dbc.Card([ | ||
dbc.CardBody([ | ||
html.Div([ | ||
dcc.Input(id='score', value=player_force['score'], type="text", style={'fontSize': '20px'}) | ||
], style={'textAlign': 'center'}) | ||
]) | ||
]) | ||
), | ||
], width=12), | ||
], align='center'), | ||
html.Br(), | ||
|
||
### Row 3: Rope | ||
dbc.Row([ | ||
dbc.Col([ | ||
html.Div( | ||
dbc.Card( | ||
dbc.CardBody([ | ||
dcc.Graph( | ||
id='rope', | ||
figure=px.scatter(df, | ||
x="x", | ||
y="y", | ||
color="val", | ||
symbol='val', | ||
size='val', | ||
range_x=[1,rope_range], | ||
range_y=[0, 0] | ||
).update_layout( | ||
template='plotly_dark', | ||
plot_bgcolor= 'rgba(0, 0, 0, 0)', | ||
paper_bgcolor= 'rgba(0, 0, 0, 0)', | ||
xaxis={'visible':False}, | ||
coloraxis={'showscale':False} | ||
), | ||
config={ | ||
'displayModeBar': False | ||
} | ||
|
||
) | ||
]) | ||
), | ||
) | ||
], width=12), | ||
], align='center'), | ||
html.Br(), | ||
|
||
### Row 4: Gauges | ||
dbc.Row([ | ||
## Gauge 1 | ||
dbc.Col([ | ||
html.Div( | ||
dbc.Card([ | ||
dbc.CardBody([ | ||
html.Div([ | ||
daq.Gauge( | ||
value=player_force['forces'][0], | ||
label=player_force['players'][0], | ||
max=20, | ||
min=0, id='gauge1' | ||
) | ||
], style={'color': 'dark', 'margin': '2', 'textAlign': 'center'}) | ||
]) | ||
]) | ||
), | ||
], width=3), | ||
|
||
## Gauge 2 | ||
dbc.Col([ | ||
html.Div( | ||
dbc.Card([ | ||
dbc.CardBody([ | ||
html.Div([ | ||
daq.Gauge( | ||
value=player_force['forces'][1], | ||
label=player_force['players'][1], | ||
max=20, | ||
min=0, id='gauge2' | ||
) | ||
], style={'color': 'dark', 'margin': '2', 'textAlign': 'center'}) | ||
]) | ||
]) | ||
), | ||
], width=3), | ||
|
||
## Gauge 3 | ||
dbc.Col([ | ||
html.Div( | ||
dbc.Card([ | ||
dbc.CardBody([ | ||
html.Div([ | ||
daq.Gauge( | ||
value=player_force['forces'][2], | ||
label=player_force['players'][2], | ||
max=20, | ||
min=0, id='gauge3' | ||
) | ||
], style={'color': 'dark', 'margin': '2', 'textAlign': 'center'}) | ||
]) | ||
]) | ||
), | ||
], width=3), | ||
|
||
## Gauge 4 | ||
dbc.Col([ | ||
html.Div( | ||
dbc.Card([ | ||
dbc.CardBody([ | ||
html.Div([ | ||
daq.Gauge( | ||
value=player_force['forces'][3], | ||
label=player_force['players'][3], | ||
max=20, | ||
min=0, id='gauge4' | ||
) | ||
], style={'color': 'dark', 'margin': '2', 'textAlign': 'center'}) | ||
]) | ||
]) | ||
), | ||
], width=3), | ||
],align='center'), | ||
|
||
]) | ||
), dcc.Interval(id='interval-component', interval=1000, n_intervals=0) | ||
]) | ||
|
||
@app.callback([ | ||
Output('gauge1', 'value'), | ||
Output('gauge2', 'value'), | ||
Output('gauge3', 'value'), | ||
Output('gauge4', 'value'), | ||
Output('rope', 'figure'), | ||
Output('score', 'value') | ||
], | ||
Input('interval-component', 'n_intervals') | ||
) | ||
def update_layout(n): | ||
global player_force | ||
global rope_range | ||
global limit1 | ||
global limit2 | ||
df = ap.build_rope_dataframe(limit1, limit2, player_force['rope_mark']) | ||
fig = px.scatter(df, | ||
x="x", | ||
y="y", | ||
color="val", | ||
symbol='val', | ||
size='val', | ||
range_x=[1,rope_range], | ||
range_y=[0, 0] | ||
).update_layout( | ||
template='plotly_dark', | ||
plot_bgcolor= 'rgba(0, 0, 0, 0)', | ||
paper_bgcolor= 'rgba(0, 0, 0, 0)', | ||
xaxis={'visible':False}, | ||
coloraxis={'showscale':False} | ||
) | ||
return player_force['forces'][0], \ | ||
player_force['forces'][1], \ | ||
player_force['forces'][2], \ | ||
player_force['forces'][3], \ | ||
fig, \ | ||
player_force['score'] | ||
|
||
@server.route('/update_force', methods=['POST']) | ||
def update_force(): | ||
global player_force | ||
try: | ||
data = json.loads(request.data) | ||
player_force = data[0] | ||
print(player_force) | ||
# player_force['players'] = data['players'] | ||
# player_force['rope_mark'] = int(data['rope_mark']) | ||
return 'Force values updated successfully' | ||
except Exception as e: | ||
return str(e), 400 | ||
|
||
if __name__ == '__main__': | ||
server.run(port=5004) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,63 @@ | ||
from dash import dcc, html | ||
import dash_bootstrap_components as dbc | ||
import plotly.express as px | ||
import dash_daq as daq | ||
import pandas as pd | ||
import plotly.graph_objects as go | ||
|
||
def drawFigureCard(l1, l2, m): | ||
df = build_rope_dataframe(l1, l2, m) | ||
return dbc.Card( | ||
dbc.CardBody([ | ||
dcc.Graph( | ||
figure=px.scatter(df, | ||
x="x", | ||
y="y", | ||
color="val", | ||
symbol='val', | ||
size='val', | ||
range_x=[0,40], | ||
range_y=[0, 0] | ||
).update_layout( | ||
template='plotly_dark', | ||
plot_bgcolor= 'rgba(0, 0, 0, 0)', | ||
paper_bgcolor= 'rgba(0, 0, 0, 0)', | ||
xaxis={'visible':False}, | ||
coloraxis={'showscale':False} | ||
), | ||
config={ | ||
'displayModeBar': False | ||
} | ||
) | ||
]) | ||
) | ||
|
||
# Title field | ||
def drawTextCard(tt): | ||
return dbc.Card( | ||
dbc.CardBody([ | ||
html.Div([ | ||
html.H1(tt, style={'color': 'white'}), | ||
], style={'textAlign': 'center'}) | ||
], style={'backgroundColor': '#013163'}) | ||
) | ||
|
||
# Build the dataframe for the heatmap | ||
def build_rope_dataframe(limit1, limit2, mark): | ||
# Create a DataFrame of zeros | ||
# Create lists for 'x', 'y', and 'val' | ||
x_values = list(range(1, 39)) | ||
y_values = [0] | ||
val_values = [0] * len(x_values) | ||
data = {'x': [], 'y': [], 'val': []} | ||
for y in y_values: | ||
data['x'].extend(x_values) | ||
data['y'].extend([y] * len(x_values)) | ||
data['val'].extend(val_values) | ||
df = pd.DataFrame(data) | ||
# Render the limits | ||
df.loc[(df['x'] == limit1) & (df['y'] == 0), 'val'] = 50 | ||
df.loc[(df['x'] == limit2) & (df['y'] == 0), 'val'] = 50 | ||
# # Render the mark | ||
df.loc[(df['x'] == mark) & (df['y'] == 0), 'val'] = 100 | ||
return df |
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,7 @@ | ||
curl -X POST http://127.0.0.1:5004/update_force -H "Content-Type: application/json" -d '[ | ||
{"players": ["Team_A_Player_0", "Team_A_Player_1", "Team_B_Player_0", "Team_B_Player_1"], | ||
"forces": [12, 13, 11, 12], | ||
"score": "ttt", | ||
"rope_mark": 1 | ||
} | ||
]' |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,8 @@ | ||
flask==2.2.0 | ||
dash==2.11.1 | ||
Werkzeug==2.2.0 | ||
pandas | ||
plotly_express | ||
dash_bootstrap_components | ||
dash_daq | ||
setuptools |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,18 @@ | ||
#!/bin/bash | ||
if [ ! -f ".lf_env/bin/activate" ] | ||
then | ||
# Create a virtual environment | ||
virtualenv .venv | ||
|
||
# Activate the virtual environment | ||
source .venv/bin/activate | ||
|
||
# Install Flask | ||
pip install -r requirements.txt | ||
else | ||
# Activate the virtual environment | ||
source .venv/bin/activate | ||
fi | ||
|
||
# Start the Flask and Dash application | ||
python3 app.py |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,17 @@ | ||
target C | ||
|
||
preamble {= | ||
#include <stdlib.h> | ||
=} | ||
|
||
reactor Player(max_force: int = 7) { | ||
output out: int | ||
timer t(0, 1 s) | ||
|
||
reaction(t) -> out {= | ||
int lower = 1; | ||
int force = (rand() % (self->max_force - lower + 1)) + lower; | ||
lf_print("Force = %d", force); | ||
lf_set(out, force); | ||
=} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,17 @@ | ||
## Tug of War | ||
|
||
Tug of War is a team-based game where two teams pull on opposite ends of a rope to bring the other team across a center marker. The demo involves two players per team, each characterized by a parameter. Players apply a randomly generated force within the interval 1 and a specified maximum force parameter. The sum of forces on each side moves the marker, and when it reaches one of the limits, the game ends. | ||
|
||
## How it works | ||
|
||
This example features a browser-based UI. The server is constructed using [Flask](https://flask.palletsprojects.com/en/3.0.x/), a Python web framework, and [Dash](https://dash.plotly.com/) components. | ||
|
||
Every second, player forces are generated and their values are send to the server as | ||
a post request. The UI's gauges are updated with these values, along with the position of the mark (yellow square). When the mark reaches one of the limits (pink diamond), the label on the top of the page is updated with the result. | ||
|
||
## Steps: | ||
|
||
1. Compile `TugOfWar.lf`. | ||
2. Launch the UI by running the script `start.sh` in the `C/src/TugOfWar/Interface` directory. The script will create a virtual environment and install the requirements listed in `requirements.txt`, if not there already. It then lauches Flask server, accessible on: [http://127.0.0.1:5004](http://127.0.0.1:5004). | ||
3. Run the launching script under `bin` and watch the game on the UI. | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,21 @@ | ||
target C { | ||
keepalive: true | ||
} | ||
|
||
import Player from "Player.lf" | ||
import TugOfWarGame from "TugOfWarGame.lf" | ||
|
||
preamble {= | ||
#include <string.h> | ||
#include <unistd.h> | ||
=} | ||
|
||
federated reactor TugOfWar { | ||
towg = new TugOfWarGame() | ||
p1 = new Player(max_force=5) | ||
p2 = new Player(max_force=6) | ||
p3 = new Player(max_force=3) | ||
p4 = new Player(max_force=9) | ||
|
||
p1.out, p2.out, p3.out, p4.out -> towg.force | ||
} |
Oops, something went wrong.