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