diff --git a/html/ui/ui.css b/html/ui/ui.css
new file mode 100644
index 0000000..b4f6ba3
--- /dev/null
+++ b/html/ui/ui.css
@@ -0,0 +1,80 @@
+
+body {
+ background-color: LightCyan;
+}
+
+
+.title_disconnected {
+ color: red;
+}
+
+.title_connected {
+ color: green;
+}
+
+input, label, button, select {
+ font-family: sans-serif;
+ font-size: 10px;
+ height: 20px;
+}
+
+
+div {
+ /*border: 1px solid black; */
+ background-color: LightSteelBlue;
+}
+
+div p {
+ font-family: sans-serif;
+ font-size: 12px;
+ margin-top: 2px;
+ margin-bottom: 2px;
+ background-color: LightSteelBlue;
+}
+
+label {
+ /*border: 1px solid red;*/
+ width: 50px;
+ padding-left: 10px;
+ margin-top: 3px;
+}
+
+
+.uiCtlDiv {
+ display: flex;
+ flex-direction: row;
+ align-items: center;
+ background-color: LightBlue;
+}
+
+.uiCtlDiv input {
+ background-color: PowderBlue;
+}
+
+.uiNumberDiv input {
+ width: 50px;
+}
+
+.uiNumbDispDiv {
+ width: 50px;
+ border: 1px solid #afafaf;
+}
+
+.uiPanel {
+ background-color: LightBlue;
+}
+
+.uiRow {
+ display: flex;
+ flex-direction: row;
+ align-items: center;
+ background-color: LightBlue;
+}
+
+.uiCol {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ background-color: LightBlue;
+}
+
diff --git a/html/ui/ui.js b/html/ui/ui.js
new file mode 100644
index 0000000..98b7063
--- /dev/null
+++ b/html/ui/ui.js
@@ -0,0 +1,867 @@
+var _ws = null;
+var _rootId = "0";
+var _nextEleId = 0;
+
+function set_app_title( suffix, className )
+{
+ var ele = document.getElementById('connectTitleId');
+ ele.innerHTML = suffix
+ ele.className = className
+}
+
+
+function uiOnError( msg, r)
+{
+ console.log("Error:" + msg);
+}
+
+function uiGetParent( r )
+{
+ parent_ele = document.getElementById(r.parent_id);
+
+ if( parent_ele == null )
+ {
+ uiOnError("Parent not found. parent_id:" + r.parent_id,r);
+ }
+
+ return parent_ele;
+}
+
+function uiCreateEle( r )
+{
+ var parent_ele;
+
+ if((parent_ele = uiGetParent(r)) != null )
+ {
+ ele = document.createElement(r.ele_type)
+ ele.id = r.ele_id;
+ ele.className = r.value;
+
+ parent_ele.appendChild(ele)
+ }
+}
+
+function uiRemoveChildren( r )
+{
+ ele = document.getElementById(r.ele_id)
+
+ while (ele.firstChild)
+ {
+ ele.removeChild(ele.firstChild);
+ }
+}
+
+function uiDivCreate( r )
+{ uiCreateEle(r) }
+
+function uiLabelCreate( r )
+{
+ var parent_ele;
+
+ if((parent_ele = uiGetParent(r)) != null )
+ {
+ ele = document.createElement("label")
+ ele.htmlFor = r.ele_id
+ ele.innerHTML = r.value;
+ parent_ele.appendChild(ele)
+ }
+
+}
+
+function uiSelectCreate( r )
+{
+ uiCreateEle(r)
+}
+
+function uiSelectClear( r )
+{ uiRemoveChildren(r) }
+
+function uiSelectInsert( r )
+{
+ var select_ele;
+
+ if((select_ele = uiGetParent(r)) != null )
+ {
+ var option = document.createElement('option');
+
+ option.id = r.ele_id;
+ option.innerHTML = r.value;
+ option.value = r.ele_id;
+ option.onclick = function() { uiOnSelectClick(this) }
+
+ select_ele.appendChild(option)
+ }
+}
+
+function uiSelectChoose( r )
+{
+ var select_ele;
+
+ if((select_ele = uiGetParent(r)) != null )
+ {
+ if( select_ele.hasChildNodes())
+ {
+ var children = select_ele.childNodes
+ for(var i=0; i 0)
+ {
+ label_ele = dom_create_ele("label");
+
+ label_ele.innerHTML = label;
+
+ div_ele.appendChild(label_ele)
+ }
+ }
+
+ return ui_create_ele( div_ele, ele_type, d, dfltEleClassName );
+}
+
+function ui_create_div( parent_ele, d )
+{
+ var div_ele = ui_create_ele( parent_ele, "div", d, "uiDiv" );
+
+ if( div_ele != null )
+ {
+
+ if( d.title != null )
+ {
+ var title = d.title.trim()
+
+ if( title.length > 0 )
+ {
+ var p_ele = dom_create_ele("p")
+
+ p_ele.innerHTML = title
+
+ div_ele.appendChild( p_ele )
+ }
+ }
+ }
+
+ return div_ele;
+}
+
+function ui_create_panel_div( parent_ele, d )
+{
+ d.type = "div"
+ var div_ele = ui_create_div( parent_ele, d );
+
+ if( !d.hasOwnProperty('className') )
+ div_ele.className = "uiPanel"
+
+ return div_ele
+}
+
+function ui_create_row_div( parent_ele, d )
+{
+ d.type = "div"
+ var div_ele = ui_create_div( parent_ele, d );
+
+ if( !d.hasOwnProperty('className') )
+ div_ele.className = "uiRow"
+
+ return div_ele
+}
+
+function ui_create_col_div( parent_ele, d )
+{
+ d.type = "div"
+ var div_ele = ui_create_div( parent_ele, d );
+
+ if( !d.hasOwnProperty('className') )
+ div_ele.className = "uiCol"
+
+ return div_ele
+}
+
+
+function ui_create_title( parent_ele, d )
+{
+ var ele = ui_create_ele( parent_ele, "label", d, "uiTitle" );
+
+ if( ele != null )
+ {
+ ele.innerHTML = d.title;
+ }
+
+ return ele;
+}
+
+function ui_create_button( parent_ele, d )
+{
+ var ele = ui_create_ctl( parent_ele, "button", null, d, "uiButton" );
+
+ if( ele != null )
+ {
+ ele.innerHTML = d.title;
+ ele.onclick = function() { ui_send_int_value(this,1); }
+ }
+
+ return ele;
+}
+
+function ui_create_check( parent_ele, d )
+{
+ var ele = ui_create_ctl( parent_ele, "input", d.title, d, "uiCheck" )
+
+ if( ele != null )
+ {
+ ele.type = "checkbox";
+
+ ele.onclick = function() { ui_send_bool_value(this,dom_get_checkbox(this.id)); }
+
+ if( !d.hasOwnProperty('value') )
+ ui_send_echo(ele)
+ else
+ {
+ dom_set_checkbox(ele.id, d.value );
+ ui_send_bool_value(ele,dom_get_checkbox(ele.id))
+ }
+
+ }
+ return ele;
+}
+
+//
+// Note: The value of a 'select' widget is always set by the 'appId'
+// of the selected 'option'. Likewise the 'appId' of the selected
+// option is returned as the value of the select widget.
+//
+function ui_on_select( ele )
+{
+ ui_send_int_value(ele,ele.options[ ele.selectedIndex ].appId);
+}
+
+function ui_select_set_from_option_app_id( sel_ele, option_appId )
+{
+ var i;
+ for(i=0; i