Command Streaming

loadcmd.htm (hint: right click, save as)

  1<!--
  2  Test/Development/Documentation page; install as plugin to test
  3-->
  4
  5<div class="panel panel-primary">
  6  <div class="panel-heading">Asynchronous Command Streaming</div>
  7  <div class="panel-body">
  8
  9    <h3>Synopsis</h3>
 10
 11    <p class="lead"><code>[jqxhr=] loadcmd(command [,target] [,filter] [,timeout])</code></p>
 12
 13    <p>The <code>loadcmd()</code> function executes a shell command or evaluates javascript
 14      code with asynchronous streaming of the output into a target element or onto a function.</p>
 15
 16
 17    <h3>Basic Usage</h3>
 18
 19    <p>As this is the underlying function for command buttons, basic usage
 20      is very similar, but fully scriptable:</p>
 21
 22    <p>
 23      <button type="button" class="btn btn-default" onclick="loadcmd('boot status', '#out1')">
 24        Show boot status
 25      </button>
 26    </p>
 27    <pre id="out1" />
 28
 29    <p>As with command buttons, you can append the output by prefixing the target
 30      selector with "+". You can omit the output by passing null as the target. If target
 31      is a DOM element, loadcmd automatically sets the <code>loading</code> class on the
 32      target while the command is processed.</p>
 33
 34    <p>The target element's min-height is automatically fixed to the current height
 35      of the target before the output is set. This avoids page layout jumps when
 36      reusing a target for commands.</p>
 37
 38    <p>If the target is scrollable and the content exceeds the visible area, the
 39      element is automatically scrolled to show the new content, unless the user has
 40      done a manual scrolling on the element.</p>
 41
 42    <p>If the command output stops for <code>timeout</code> seconds, the request is
 43      aborted and a timeout error is shown. Default timeout is 20 seconds for standard
 44      commands, 300 seconds for known long running commands.</p>
 45
 46
 47    <h3>Evaluate Javascript Code</h3>
 48
 49    <p>To execute Javascript, either pass an object instead of the <code>command</code> string,
 50      containing the command as property "command" and a second property "type" with
 51      value "js", or simply use the wrapper call <code>loadjs()</code>. Example:</p>
 52
 53    <p>
 54      <button type="button" class="btn btn-default"
 55        onclick="loadjs('print(OvmsVehicle.Type())', '#out1js')">
 56        Show vehicle type
 57      </button>
 58    </p>
 59    <pre id="out1js" />
 60
 61    <p>Javascript evaluation is not limited to a single command or line. Hint: to avoid
 62      encoding complex JS code in the onclick attribute, store the code in some hidden
 63      DOM element and read it via <code>$(…).text()</code>.</p>
 64
 65
 66    <h3>Output Encoding &amp; Binary Streams</h3>
 67
 68    <p>To allow binary data to be sent by a command and to enable processing of the
 69      result by Javascript on the browser without character encoding issues, the command
 70      API supports output "binary" mode.</p>
 71    
 72    <p>Primary application for this is exchanging CBOR encoded objects between the
 73      module and the web frontend. To fully support this, the OVMS web frontent includes
 74      the CBOR library by Patrick Gansterer. Example use:</p>
 75
 76    <pre>
 77    loadjs({ command: "auxbatmon.dump('CBOR')", output: "binary" }).done((data) => {
 78      history = CBOR.decode(data);
 79    });
 80    </pre>
 81
 82    <p>See the AuxBatMon and PwrMon OVMS plugins for full examples.</p>
 83
 84    <p><code>output</code> modes supported by the command API are:</p>
 85    
 86    <ul>
 87      <li><code>text</code> → Content-Type: text/plain; charset=utf-8</li>
 88      <li><code>json</code> → Content-Type: application/json; charset=utf-8</li>
 89      <li><code>binary</code> → Content-Type: application/octet-stream; charset=x-user-defined</li>
 90      <li>default/other → Content-Type: application/octet-stream; charset=utf-8</li>
 91    </ul>
 92
 93
 94    <h3>jqXHR Object</h3>
 95
 96    <p><code>loadcmd()</code> returns the jqXHR (XMLHttpRequest) object in charge for
 97      the asynchronous execution of the request. This can be used to track the results
 98      of the execution, check for errors or to abort the command.</p>
 99
100    <p>
101      <button type="button" class="btn btn-default" id="startcg">Start chargen</button>
102      <button type="button" class="btn btn-default" id="abortcg" disabled>Abort chargen</button>
103    </p>
104    <pre id="out2" style="max-height:200px; overflow:auto;" />
105
106    <p>The jQuery XHR object is also a "thenable", so actions to be performed after
107      the command execution can simply be chained to the object. Example:</p>
108
109    <p><button type="button" class="btn btn-default" id="showstat">Show stat</button></p>
110
111    <p>See <a target="_blank" href="http://api.jquery.com/jQuery.ajax/#jqXHR">jQuery
112      documentation</a> for full details and options.</p>
113
114
115    <h3>Filter / Process Output</h3>
116
117    <p>Supply a filter function to hook into the asynchronous output stream. Use filters
118      to filter (ahem…) / preprocess / reformat the command output, scan the stream for some
119      info you'd like to know as soon as possible, or completely take over the output
120      processing.</p>
121
122    <p>The filter function is called when a new chunk of output has arrived or when
123      a stream error has occurred. The function gets a message object containing
124      the <code>request</code> object and either a <code>text</code> for normal outputs
125      or an <code>error</code>, which is a preformatted error output you can use or
126      ignore.</p>
127
128    <p>If the filter function returns a string, that will be added to the output target.
129      If it returns <code>null</code>, the target will remain untouched.</p>
130
131    <p>Hint: if you just want to scan the text for some info, you can pass on the message
132      after your scan to the default <code>standardTextFilter()</code>.</p>
133
134    <p>Example: let's reformat a <code>can status</code> dump into a nice Bootstrap table:</p>
135
136    <p><button type="button" class="btn btn-default" id="canstatus">CAN1 status</button></p>
137
138    <table class="table table-condensed table-border table-striped table-hover" id="canout">
139      <thead>
140        <tr><th class="col-xs-6">Key</th><th class="col-xs-6">Value</th></tr>
141      </thead>
142      <tbody/>
143    </table>
144
145  </div>
146</div>
147
148<script>
149(function(){
150
151  /* Show page source: */
152  var pagesrc = $('#main').html();
153  $('.panel-heading').prepend('<button type="button" class="btn btn-sm btn-info action-showsrc"' +
154    ' style="float:right; position:relative; top:-5px;">Show page source</button>');
155  $('.action-showsrc').on('click', function() {
156    $('<div/>').dialog({
157      title: 'Source Code',
158      body: '<pre style="font-size:85%; height:calc(100vh - 230px);">'
159        + encode_html(pagesrc) + '</pre>',
160      size: 'lg',
161    });
162  });
163
164  /* Example: jqXHR.abort() */
165  var xhr;
166  $('#startcg').on('click', function() {
167    xhr = loadcmd('test chargen 20 600', '#out2');
168    $('#abortcg').prop('disabled', false);
169  });
170  $('#abortcg').on('click', function() {
171    xhr.abort();
172  });
173
174  /* Example: jqXHR.then() */
175  $('#showstat').on('click', function() {
176    loadcmd('stat').then(function(output) {
177      confirmdialog('STAT result', '<samp>' + output + '</samp>', ['Close']);
178    });
179  });
180
181  /* Example: filter / output processor */
182  $('#canstatus').on('click', function() {
183    var $table = $('#canout > tbody');
184    var buf = '';
185    $table.empty();
186    loadcmd('can can1 status', function(msg) {
187      if (msg.error) {
188        // Render error into table row:
189        $('<tr class="danger"><td colspan="2">' + msg.error + '</td></tr>').appendTo($table);
190      }
191      else if (msg.text) {
192        // As this is a stream, the text chunks received need not be single or complete lines.
193        // We're interested in lines here, so we buffer the chunks and split the buffer at '\n':
194        buf += msg.text;
195        var lines = buf.split('\n');
196        if (lines.length > 1) {
197          buf = lines[lines.length-1];
198          for (var i = 0; i < lines.length-1; i++) {
199            // Skip empty lines:
200            if (lines[i] == "") continue;
201            // Split line into columns:
202            var col = lines[i].split(/: +/);
203            // Create table row & append to table, add some color on error counters:
204            if (col[0].match("[Ee]rr"))
205              $('<tr class="warning"><th>' + col[0] + '</th><td>' + col[1] + '</td></tr>').appendTo($table);
206            else
207              $('<tr><th>' + col[0] + '</th><td>' + col[1] + '</td></tr>').appendTo($table);
208          }
209        }
210      }
211      // Filter has handled everything:
212      return null;
213    });
214  });
215
216})();
217</script>