374 lines
19 KiB
Plaintext
374 lines
19 KiB
Plaintext
<!DOCTYPE html>
|
|
<html lang="en">
|
|
<head>
|
|
<meta name="generator" content="HTML Tidy for HTML5 for Apple macOS version 5.8.0">
|
|
<!-- Copyright 2012, 2014, 2020 -->
|
|
|
|
<title>JMRI Remote Panels</title><!--#include virtual="/help/en/parts/Style.shtml" -->
|
|
</head>
|
|
<body>
|
|
<!--#include virtual="/help/en/parts/Header.shtml" -->
|
|
|
|
<div id="mBody">
|
|
<!--#include virtual="Sidebar.shtml" -->
|
|
|
|
<div id="mainContent">
|
|
<h1>JMRI Remote Panels</h1>
|
|
|
|
<p>JMRI displays panels via a web browser at the <strong>/panel</strong> relative URL.</p>
|
|
<a href="images/webServerPanelView.png"><img src="images/webServerPanelView.png" alt=
|
|
"Web Server Panel screenshot" width="553" height="363"></a>
|
|
<p>Remote Panel URLs are:</p>
|
|
|
|
<dl>
|
|
<dt>/panel</dt>
|
|
|
|
<dd>Without any additional parameters, <em>/panel</em> lists the open JMRI panels, as shown
|
|
in the screen shot above (panels may be hidden in PanelPro, but they must be loaded to show
|
|
up).<br>
|
|
Clicking or tapping on one of the listed panel names will open a functioning panel.</dd>
|
|
|
|
<dt>/panel?name=<type>/<name></dt>
|
|
|
|
<dd>Displays an open, operable panel. <em><type></em> is one of: <em>Panel</em>,
|
|
<em>ControlPanel</em>, <em>Switchboard</em> or <em>Layout</em>.</dd>
|
|
|
|
<dt>/panel/<type>/<name>?format=xml</dt>
|
|
|
|
<dd>Retrieves a portable XML representation of the panel. <em><type></em> is one of:
|
|
<em>Panel</em>, <em>ControlPanel</em>, <em>Switchboard</em> or <em>Layout</em>.</dd>
|
|
|
|
<dt>/panel/<type>/<name>?protect=yes</dt>
|
|
|
|
<dd>Displays a read-only representation of the specified panel.</dd>
|
|
</dl>
|
|
|
|
<p>Panel display requires a modern browser with HTML5 support including WebSocket.
|
|
The JMRI web server's About page includes a test for this.</p>
|
|
|
|
<h2 id="example">Technical Example: Behind the scenes of Web Server Panels</h2>
|
|
<a href="images/WebServerFlow.png"><img src="images/WebServerFlow.png" alt=
|
|
"Web Server Data Flow Diagram" width="341" height="640" class="floatRight" title=
|
|
"Simplified Web Server Flow (by EB)"></a>
|
|
<p>This chapter shows an overview of how JMRI Web Server operates between the JMRI
|
|
application and the Web Browser to demonstrate what code is involved, using Panels as an
|
|
example.</p>
|
|
|
|
<p>In <a href="#exampleA">part A</a> we show at how a panel initially shows up in the browser
|
|
of a (remote) user logged on to JMRI Web Server.</p>
|
|
|
|
<p><a href="#exampleB">Part B</a> focuses on the opposing flow of information. You will
|
|
discover how user action in the browser is picked up all the way back to JMRI and even to the
|
|
layout.</p>
|
|
Some definitions
|
|
<ul>
|
|
<li>Web Server displays panels, roster engines, tables and Operations in the web browser,
|
|
using a mix of XML, Javascript, jdom, json, jQuery and CSS.</li>
|
|
|
|
<li>Each of these categories is displayed in a separate frame, accessible in the browser
|
|
via tabs at the top edge of the browser window: Panels, Throttle, Operations, Tables:<br>
|
|
<a href="images/webServerMenubar.png"><img src="images/webServerMenubar.png" alt=
|
|
"Web Server Panel screenshot" width="250" height="90"></a>
|
|
</li>
|
|
|
|
<li>Each category has its own JavaScript code file, e.g., <code>/web/js/Panel.js</code>,
|
|
plus supporting "Servlets" to connect to JMRI, as documented in the <a href=
|
|
"https://www.jmri.org/JavaDoc/doc/jmri/web/server/package-summary.html">Web Server
|
|
Javadoc</a>. (Note: The code is run from <code>/web/js/</code>, but the real
|
|
source code is stored in <code>/web/ts/</code> to allow the use of
|
|
<a href="../doc/Technical/Patterns.shtml#typescript">compiled TypeScript</a>)
|
|
</li>
|
|
</ul>
|
|
|
|
<h3 id="exampleA">A. From JMRI to the browser</h3>
|
|
|
|
<p>We assume JMRI and Web Server are running and the user has a browser pointing to
|
|
<code>http://localhost:12080</code> as set in JMRI Web Server preferences.<br>
|
|
Configuration and current state of panel items is handled by the
|
|
<code>jmri/jmrit/display/xPanelEditor.java</code> code. This means that creating a panel and
|
|
items like Turnouts connected to the layout, and listening for changes has been completed
|
|
first in the JMRI application. A graphical control panel needs to be loaded in JMRI for it to
|
|
show up in the JMRI web browser drop down (it is OK if a panel is hidden in JMRI UI).</p>
|
|
<a href="images/webservletsexample_A.png"><img src="images/webservletsexample_A.png" alt=
|
|
"Web Server Sequence Diagram A (draft)" width="471" height="231"></a>
|
|
<ol>
|
|
<li>To open a panel the User uses the mouse to click the "Panel" combo on the menu
|
|
bar.</li>
|
|
|
|
<li>The web browser responds by asking Web Server to produce the panel by sending a GET
|
|
request together with a <code>/panel/"name"</code> URL.</li>
|
|
|
|
<li>This starts the <code>/web/js/panel.js</code> JavaScript code.</li>
|
|
|
|
<li>The main method in <code>panel.js</code> is <code>$(document).ready(function() {</code>
|
|
(around line 2000).<br>
|
|
It ends with a call to the <code>requestPanelXML(panelName);</code> method:<br>
|
|
<code><br>
|
|
141 var requestPanelXML = function(panelName) {<br>
|
|
142 $.ajax({<br>
|
|
143 type: "GET",<br>
|
|
144 url: "/panel/" + panelName + "?format=xml", [...]<br>
|
|
145 success: function(data, textStatus, jqXHR) {<br>
|
|
146 processPanelXML(data, textStatus, jqXHR);<br>
|
|
147 setTitle($gPanel["name"]); } [...]<br></code><br>
|
|
Here, line 142 sends an <a href="https://www.w3schools.com/xml/ajax_intro.asp">AJAX</a>
|
|
(Asynchronous JavaScript And XML) <code>GET XMLHttpRequest</code>code> for the panel
|
|
to be retrieved by its name to the JMRI PanelServlet.
|
|
</li>
|
|
|
|
<li>The connection between web server and JMRI categories is handled by
|
|
<strong>servlets</strong> in <code>JMRI/java/src/jmri/web/servlet/</code>, pieces of Java
|
|
code interacting with the web server. The extended HttpServlet
|
|
<code>jmri/web/servlet/panel/AbstractPanelServlet.java</code> links the web server requests
|
|
to the information kept in Java. The <code>doGet()</code> HttpServer message calls in
|
|
<code>panel.js</code> to answer the request.</li>
|
|
|
|
<li>In the case of Switchboards the
|
|
<code>/jmri/web/servlet/panel/SwitchboardServlet.java</code> contains methods such as:
|
|
<ul>
|
|
<li><code>String getPanelType()</code>
|
|
</li>
|
|
|
|
<li><code>String getXmlPanel(String name)</code>
|
|
</li>
|
|
</ul>
|
|
When you want to develop new features on web panels, that second method needs to be
|
|
expanded to include them when pulling new information from SwitchboardEditor and
|
|
individual BeanSwitches placed on that panel:<br>
|
|
<code><br>
|
|
40 protected String getXmlPanel(String name) {<br>
|
|
41 SwitchboardEditor editor = (SwitchboardEditor) getEditor(name);<br>
|
|
42 Element panel = new Element("panel");<br>
|
|
43 panel.setAttribute("shape", editor.getSwitchShape());<br>
|
|
44 Element color = new Element("backgroundColor");<br>
|
|
45 color.setAttribute("red",
|
|
Integer.toString(editor.getBackgroundColor().getRed()));<br>
|
|
46 [...] 47
|
|
panel.addContent(color);<br></code><br>
|
|
Line 45 allows us to later from <code>panel.js</code> call e.g.,<br>
|
|
<code>979 $("#panel-area").css({"background-color": "rgb(" + $widget.red + "," +
|
|
$widget.green + "," + $widget.blue + ")"});</code>
|
|
</li>
|
|
|
|
<li>From the Java code running JMRI, specific information is written out to an xml
|
|
"snapshot" for both the panel (JFrame) and the individual objects placed on that panel, so
|
|
called 'Positionables':
|
|
<ul>
|
|
<li>The complete panel is "stored" by the <code>store(object)</code> method in
|
|
<code>jmri/jmrit/display/switchboardEditor/configurexml/SwitchboardEditorXml.java</code>:<br>
|
|
|
|
<code><br>
|
|
41 public Element store(Object o) {<br>
|
|
42 SwitchboardEditor p = (SwitchboardEditor) o;<br>
|
|
43 Element panel = new Element("switchboardeditor");<br>
|
|
44 JFrame frame = p.getTargetFrame();<br>
|
|
45 panel.setAttribute("name", "" + frame.getTitle());<br>
|
|
46 panel.setAttribute("class",
|
|
"jmri.jmrit.display.switchboardEditor.configurexml.SwitchboardEditorXml");<br>
|
|
[...]<br></code><br>
|
|
followed by a for-each loop:<br>
|
|
<code><br>
|
|
for (BeanSwitch sub : _switches) {<br>
|
|
try {<br>
|
|
Element e = ConfigXmlManager.elementFromObject(sub);<br>
|
|
e.setAttribute("label",
|
|
sub.getNameString());<br></code><br></li>
|
|
|
|
<li>Additional properties of all BeanSwitches on the Switchboard panel are exported in
|
|
a for-each loop in
|
|
<code>jmri/jmrit/display/switchboardEditor/configurexml/BeanSwitchXml.java</code>:<br>
|
|
<code><br>
|
|
public Element store(Object o) {<br>
|
|
BeanSwitch bs = (BeanSwitch) o;<br>
|
|
Element element = new Element("beanswitch");<br>
|
|
element.setAttribute("label", bs.getNameString());<br>
|
|
[...]<br>
|
|
}<br></code><br></li>
|
|
</ul>
|
|
The result is an xml <code>out.outputString(doc)</code> in reply to the web server
|
|
request.
|
|
</li>
|
|
|
|
<li>To process the xml response returned for the <code>requestPanelXML</code> command, the
|
|
JavaScript code in <code>/web/js/panel.js</code> contains the function<br>
|
|
<code>processPanelXML($returnedData, $success, $xhr)</code> (around line 160)<br>
|
|
that renders from the xml data in <code>$xml</code> an object-based web panel html, ready
|
|
to forward to the browser and display client-side using CSS and HTML5.<br>
|
|
Among a lot of other properties, the background color is set from the <code>panel</code>
|
|
attribute:<br>
|
|
<code><br>
|
|
$("#panel-area").css({backgroundColor: $gPanel.backgroundcolor});<br></code><br>
|
|
where <code>$()</code> invokes jQuery as a selector (a function that returns a set of
|
|
elements found in the DOM of the web page).<br>
|
|
After the attributes of the main panel such as color, size and position are copied from the
|
|
xml, all individual elements in the panel xml are processed in a for-each loop, building a
|
|
persistent array of <code>$widget</code> arrays:<br>
|
|
<code><br>
|
|
236 $panel.contents().each(<br>
|
|
237 function() {<br>
|
|
238 var $widget = new Array();<br></code><br>
|
|
In such an array the attributes of each (positionable) item are copied in 1-on-1 from the
|
|
xml, filling the basic widget web object:<br>
|
|
<code>244 $(this.attributes).each(function() {<br>
|
|
245 $widget[this.name] = this.value;<br>
|
|
246 });<br></code></li>
|
|
|
|
<li>Next, every <code>$widget</code> array if enriched by extra fields, either using
|
|
attributes shared on the panel, like<br>
|
|
<code>666 var $cr = $gPanel.turnoutcirclesize * SIZE;</code><br>
|
|
with attributes of one of the elements: <code>1039 $widget['text8'] =
|
|
$(this).find('inconsistentText').attr('text');</code><br>
|
|
or with copies of other elements already present in the $widget array, for example:<br>
|
|
<code><br>
|
|
case "beanswitch" :<br>
|
|
$widget['name'] = $widget.label;</code><br>
|
|
which creates a 'name' item in the array and copies in the <code>label</code> item.</li>
|
|
|
|
<li>Next, the HTML is written:<br>
|
|
<code><br>
|
|
$("#panel-area").append("<div id=" + $widget.id + "r class='" +<br>
|
|
$widget.classes + "' " + $hoverText + "></div>");<br></code><br></li>
|
|
|
|
<li>The complete HTML "page" is sent out to user's screen as a structured but basically
|
|
static visual, composed of graphical "objects" that will be updated via the server as
|
|
needed at a later point in time when the status of one such item changes inside JMRI.</li>
|
|
|
|
<li>Next, <code>panel.js</code> starts up a process to send and listens for changes to each
|
|
panel element (called "nodes" in xml speak).<br>
|
|
The nodes in our example panel are called <strong>BeanSwitches</strong>. SensorIcons and
|
|
LayoutTracks are similar nodes. Each element is assigned a <code>$widget</code> variable in
|
|
<code>/web/js/panel.js</code>, to interact with the browser document object model (DOM)
|
|
HTML5+ entities via jdom (Java-based document object model for XML), jQuery and JavaScript
|
|
Object Notation (JSON).<br>
|
|
To be able to keep a link to every graphical element displayed on the browser we mainly use
|
|
<code><div/></code>s that the server will feed to the user's browser and using jQuery
|
|
can be contacted by their unique ID.</li>
|
|
|
|
<li>From that moment, <code>panel.js</code> follows interaction in
|
|
<code>java.src.jmrit.display.xPanelEditor</code> and the separate PositionableItems like
|
|
LayoutTurnout, CPE SensorIcon and Switchboard Beanswitch.</li>
|
|
|
|
<li>Now that the <code>$(document).ready(function()</code> main method is running, it will
|
|
listen for changes, and depending on the type (object) will start <code>updateWidgets(name,
|
|
state, data)</code>, which in turn calls <code>setWidgetState(widgetId, state, data)</code>
|
|
to update the shape displayed on screen, e.g., when a sensor on the layout changes
|
|
state.</li>
|
|
</ol>
|
|
|
|
<p><strong>What to Code:</strong>
|
|
</p>
|
|
|
|
<ol>
|
|
<li>Describe how to get your specific stuff out of JMRI and into the web server:
|
|
<ul>
|
|
<li>Shared: create the <code>store(panel)</code> method</li>
|
|
|
|
<li>Per item: add code in a <code>for-each</code> element loop in the same method</li>
|
|
</ul>
|
|
</li>
|
|
|
|
<li>Describe how to fill the <code>$widgets</code> for your items in the
|
|
<code>processPanelXML()</code> function.</li>
|
|
</ol>
|
|
|
|
<p><strong>Where to Code:</strong>
|
|
</p>
|
|
|
|
<ol>
|
|
<li>In <code>/jmri/jmrit/display/xEditor/configureXml/xEditorXml.java</code> + similar
|
|
files for any special classes your panel type relies on.</li>
|
|
|
|
<li>In <code>/web/js/panel.js</code></li>
|
|
</ol>
|
|
|
|
<h3 id="exampleB">B. From the browser to JMRI</h3>
|
|
<a href="images/webservletsexample_B.png"><img src="images/webservletsexample_B.png" alt=
|
|
"Web Server Sequence Diagram A (draft)" width="471" height="201"></a>
|
|
<p>Note: Web click events are only enabled if a panel is configured (in JMRI xPanelEditor) to
|
|
allow control.</p>
|
|
|
|
<ol>
|
|
<li>To perform regular click-handling, a <code>mouseup()</code> function is hooked up to
|
|
widgets in <code>/web/js/panel.js</code> (around line 1120):
|
|
<ul>
|
|
<li>a state toggle function for all non-momentary clickable widgets:<br>
|
|
<code>$('.clickable:not(.momentary)').bind(UPEVENT, $handleClick);<br></code></li>
|
|
|
|
<li>Momentary widgets go active on <code>mousedown</code>, and inactive on
|
|
<code>mouseup</code></li>
|
|
</ul>
|
|
</li>
|
|
|
|
<li>Sending commands to the JMRI items under control, and listening for changes to those
|
|
items in JMRI, is handled by the JSON WebSocket Server.
|
|
<ul>
|
|
<li>User interaction in the browser is picked up via <code>UserClicked(object)</code>
|
|
and GET/POST HTTP messages.</li>
|
|
|
|
<li>When a <code>mouseClickEvent</code> is heard, the <code>$handleClick(e)</code>
|
|
function in <code>panel.js</code> calls <code>sendElementChange($widget.jsonType,
|
|
$widget.systemName, $newState)</code>, resulting in <code>jmri.setObject(type, name,
|
|
state)</code> forwarding to the JMRI system.</li>
|
|
</ul>
|
|
</li>
|
|
|
|
<li>The <code>$handleClick(e)</code> function in <code>panel.js</code> (around line 1160)
|
|
updates the widgets involved:
|
|
<ul>
|
|
<li>Text based widgets are told how to change the <div> item that's already
|
|
displayed on screen: fetch them by their ID, update their contents e.g., show an ON
|
|
label instead of OFF.</li>
|
|
|
|
<li>Icon-based widget are redrawn to reflect changes to state or occupancy by drawing
|
|
on the graphical "canvas"</li>
|
|
|
|
<li>It is also possible to swap styles on a div using CSS, for example to change the
|
|
style of a widget from "css2" to a new style called "css4" (where 4 represents the
|
|
<code>int</code> value for Inactive and Thrown):<br>
|
|
<br>
|
|
<code>var $reDrawIcon = function($widget) {<br>
|
|
$('div#' + $id).css($widget['css' + $newState]);<br></code><br></li>
|
|
</ul>
|
|
The actual CSS styling code for the element is taken from <code>web/css/panel.css</code>
|
|
file by he user's browser.
|
|
</li>
|
|
|
|
<li>Via <code>sendElementChange</code> (around line 1750) a call is made to
|
|
<code>jmri.setObject(type, name, state);</code>, where <code>jmri</code> represents the
|
|
JMRI WebSocket on this server</li>
|
|
|
|
<li>
|
|
<code>setObject()</code> in <code>/web/js/jquery.jmri.js</code> <a href=
|
|
"https://jquery.com">jQuery JavaScript Library</a> instructs JMRI to set for example a
|
|
Light to "On" via a Post message:<br>
|
|
<code><br>
|
|
jmri.setLight = function (name, state) {<br>
|
|
jmri.socket.send("light", { name: name, state: state }, 'post');<br>
|
|
}<br></code>
|
|
</li>
|
|
|
|
<li>If properly connected, on the layout a light will start to light.</li>
|
|
</ol>
|
|
|
|
<p><strong>What to Code:</strong> Specify the response to user action by adding an
|
|
<code>if(...)</code> block to the <code>$setWidgetState(id, newState, data)</code> function
|
|
(near line 1570).</p>
|
|
|
|
<p><strong>Where to Code:</strong> <code>$handleClick()</code> function in
|
|
<code>jmri/web/js/panel.js</code></p>
|
|
|
|
<h3>Conclusion</h3>
|
|
|
|
<p>We hope the above example will help new developers understand which parts are required to
|
|
create and maintain web display for a panel type, and how each part is connected to display
|
|
stuff from JMRI in the web browser, and back.<br>
|
|
As always, start small and check often!</p>
|
|
<!--#include virtual="/help/en/parts/Footer.shtml" -->
|
|
</div>
|
|
<!-- closes #mainContent-->
|
|
</div>
|
|
<!-- closes #mBody-->
|
|
<script src="/js/help.js"></script>
|
|
</body>
|
|
</html>
|